├── .github ├── FUNDING.yml ├── workflows │ ├── build.yml │ ├── releaseDev.yml │ └── releaseMaster.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── esbuild.config.ts ├── pnpm-workspace.yaml ├── tsconfig.json ├── .gitignore ├── plugins ├── TidalTags │ ├── src │ │ ├── index.safe.ts │ │ ├── lib │ │ │ ├── isElement.ts │ │ │ ├── hexToRgba.ts │ │ │ ├── setColumn.ts │ │ │ └── ensureColumnHeader.ts │ │ ├── styles.css │ │ ├── index.ts │ │ ├── setQualityTags.ts │ │ ├── Settings.tsx │ │ ├── setInfoColumns.ts │ │ └── setFormatInfo.ts │ └── package.json ├── RealMax │ ├── src │ │ ├── index.safe.ts │ │ ├── Settings.tsx │ │ ├── contextMenu.ts │ │ └── index.ts │ └── package.json ├── Themer │ ├── src │ │ ├── editor.preload.js │ │ ├── Settings.tsx │ │ ├── index.ts │ │ ├── editor.native.ts │ │ └── editor.html │ └── package.json ├── SmallWindow │ ├── src │ │ ├── index.ts │ │ └── size.native.ts │ └── package.json ├── Avatar │ ├── src │ │ ├── md5.native.ts │ │ └── index.tsx │ └── package.json ├── LastFM │ ├── package.json │ └── src │ │ ├── types │ │ ├── NowPlaying.ts │ │ └── Scrobble.ts │ │ ├── hash.native.ts │ │ ├── Settings.tsx │ │ ├── index.ts │ │ └── LastFM.ts ├── Shazam │ ├── fixShazamio.js │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ ├── shazam.native.ts │ │ ├── api.types.ts │ │ └── index.ts ├── SongDownloader │ ├── src │ │ ├── downloadButton.css │ │ ├── helpers.ts │ │ ├── index.ts │ │ └── Settings.tsx │ └── package.json ├── PersistSettings │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── Settings.tsx ├── ListenBrainz │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── Settings.tsx │ │ ├── ListenBrainz.ts │ │ ├── makeTrackPayload.ts │ │ └── ListenBrainzTypes.ts ├── NoBuffer │ ├── package.json │ └── src │ │ ├── voidTrack.native.ts │ │ └── index.ts ├── DiscordRPC │ ├── package.json │ └── src │ │ ├── discord.native.ts │ │ ├── index.ts │ │ ├── Settings.tsx │ │ └── updateActivity.ts ├── NativeFullscreen │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ └── index.ts ├── DesktopConnect │ ├── package.json │ └── src │ │ ├── remoteService.native.ts │ │ └── index.ts ├── CoverTheme │ ├── package.json │ └── src │ │ ├── vibrant.native.ts │ │ ├── Settings.tsx │ │ ├── transparent.css │ │ └── index.tsx └── VolumeScroll │ ├── package.json │ └── src │ ├── Settings.tsx │ └── index.ts ├── themes ├── example.css └── blur.css ├── package.json ├── README.md └── pnpm-lock.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [inrixia] 2 | -------------------------------------------------------------------------------- /esbuild.config.ts: -------------------------------------------------------------------------------- 1 | import "luna/buildPlugins"; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "plugins/*" 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "luna/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | cert.pem 4 | key.pem 5 | tmp/ 6 | .vscode/ 7 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/index.safe.ts: -------------------------------------------------------------------------------- 1 | import type { LunaUnload } from "@luna/core"; 2 | 3 | export const unloads = new Set(); 4 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/lib/isElement.ts: -------------------------------------------------------------------------------- 1 | export const isElement = (node: Node | undefined): node is Element => node?.nodeType === Node.ELEMENT_NODE; 2 | -------------------------------------------------------------------------------- /plugins/RealMax/src/index.safe.ts: -------------------------------------------------------------------------------- 1 | import { Tracer, type LunaUnload } from "@luna/core"; 2 | export const unloads = new Set(); 3 | export const { trace, errSignal } = Tracer("[RealMAX]"); 4 | -------------------------------------------------------------------------------- /plugins/Themer/src/editor.preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require("electron"); 2 | contextBridge.exposeInMainWorld("ipcRenderer", { 3 | setCSS: (css) => ipcRenderer.send("themer.setCSS", css), 4 | }); 5 | -------------------------------------------------------------------------------- /plugins/SmallWindow/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { LunaUnload } from "@luna/core"; 2 | import { removeLimits, restoreLimits } from "./size.native"; 3 | 4 | removeLimits(); 5 | export const unloads = new Set(); 6 | unloads.add(restoreLimits); 7 | -------------------------------------------------------------------------------- /plugins/Avatar/src/md5.native.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "crypto"; 2 | 3 | /** 4 | * Returns the MD5 hash of the given string. 5 | * @param input The string to hash. 6 | * @returns The MD5 hash as a hex string. 7 | */ 8 | export const md5 = async (input: string): Promise => createHash("md5").update(input).digest("hex"); 9 | -------------------------------------------------------------------------------- /plugins/LastFM/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LastFM", 3 | "description": "Scrobbles and sets currently playing for last.fm", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#LastFM", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "exports": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/LastFM/src/types/NowPlaying.ts: -------------------------------------------------------------------------------- 1 | export interface NowPlaying { 2 | nowplaying?: Nowplaying; 3 | } 4 | 5 | interface Nowplaying { 6 | artist?: Album; 7 | track?: Album; 8 | ignoredMessage?: IgnoredMessage; 9 | albumArtist?: Album; 10 | album?: Album; 11 | } 12 | 13 | interface Album { 14 | corrected?: string; 15 | "#text"?: string; 16 | } 17 | 18 | interface IgnoredMessage { 19 | code?: string; 20 | "#text"?: string; 21 | } 22 | -------------------------------------------------------------------------------- /plugins/Shazam/fixShazamio.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const shazamPath = "./node_modules/shazamio-core/web"; 4 | const shazamFile = `${shazamPath}/shazamio-core.js`; 5 | const wasmBase64 = fs.readFileSync(`${shazamPath}/shazamio-core_bg.wasm`).toString("base64"); 6 | fs.writeFileSync(shazamFile, fs.readFileSync(shazamFile).toString().replaceAll("new URL('shazamio-core_bg.wasm', import.meta.url);", `Buffer.from("${wasmBase64}", 'base64')`)); 7 | -------------------------------------------------------------------------------- /plugins/SongDownloader/src/downloadButton.css: -------------------------------------------------------------------------------- 1 | .download-button { 2 | z-index: 1; 3 | position: relative; 4 | display: inline-block; 5 | overflow: hidden; 6 | border-radius: 10px; 7 | } 8 | 9 | .download-button::before { 10 | content: ""; 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | bottom: 0; 15 | width: var(--progress, 0%); 16 | background-color: #9e46ff; 17 | z-index: -1; 18 | transition: width 0.1s ease-out; 19 | } 20 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/lib/hexToRgba.ts: -------------------------------------------------------------------------------- 1 | export function hexToRgba(hex: string, alpha: number) { 2 | // Remove the hash at the start if it's there 3 | hex = hex.replace(/^#/, ""); 4 | // Parse the r, g, b values 5 | const r = parseInt(hex.substring(0, 2), 16); 6 | const g = parseInt(hex.substring(2, 4), 16); 7 | const b = parseInt(hex.substring(4, 6), 16); 8 | // Return the RGBA string 9 | return `rgba(${r}, ${g}, ${b}, ${alpha})`; 10 | } 11 | -------------------------------------------------------------------------------- /plugins/TidalTags/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tidal Tags", 3 | "description": "Adds tags showing track qualities and current song quality.", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#TidalTags", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "main": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/PersistSettings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PersistSettings", 3 | "description": "Ensures given settings are always applied", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#PersistSettings", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "exports": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/ListenBrainz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ListenBrainz", 3 | "description": "Scrobbles and sets currently playing for listenbrainz.org", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#ListenBrainz", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "exports": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/RealMax/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RealMAX", 3 | "description": "When playing songs if there is a HiRes version available use that", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#RealMAX", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "exports": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/Themer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Themer", 3 | "description": "Create your own theme with a built-in CSS editor, powered by Monaco Editor", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#Themer", 5 | "author": { 6 | "name": "Nick Oates", 7 | "url": "https://github.com/n1ckoates", 8 | "avatarUrl": "https://1.gravatar.com/avatar/665fef45b1c988d52f011b049b99417485b9b558947169bc4b726b8eb69a2226" 9 | }, 10 | "exports": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/NoBuffer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NoBuffer", 3 | "description": "Kicks the Tidal cdn if the current playback stalls to stop stuttering or stalling for that track", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#NoBuffer", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "exports": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/NoBuffer/src/voidTrack.native.ts: -------------------------------------------------------------------------------- 1 | import type { PlaybackInfo } from "@luna/lib"; 2 | import { fetchMediaItemStream } from "@luna/lib.native"; 3 | import { Writable } from "stream"; 4 | 5 | const VoidWriable = new Writable({ write: (_, __, cb) => cb() }); 6 | export const voidTrack = async (playbackInfo: PlaybackInfo): Promise => { 7 | const stream = await fetchMediaItemStream(playbackInfo); 8 | return new Promise((res) => stream.pipe(VoidWriable).on("end", res)); 9 | }; 10 | -------------------------------------------------------------------------------- /plugins/Avatar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Avatar", 3 | "description": "Allows setting a custom Avatar profile picture url. Uses your Gravatar profile picture as your avatar if you dont have one set", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#Avatar", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "exports": "./src/index.tsx" 11 | } -------------------------------------------------------------------------------- /plugins/SongDownloader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Song Downloader", 3 | "description": "Download tidal songs as FLAC files.", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#SongDownloader", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "main": "./src/index.ts", 11 | "dependencies": { 12 | "sanitize-filename": "^1.6.3" 13 | } 14 | } -------------------------------------------------------------------------------- /plugins/SmallWindow/src/size.native.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from "electron"; 2 | 3 | let initialLimits: number[] | undefined; 4 | 5 | export const removeLimits = () => { 6 | const win = BrowserWindow.getAllWindows()[0]; 7 | if (!initialLimits) initialLimits = win.getMinimumSize(); 8 | win.setMinimumSize(0, 0); 9 | }; 10 | 11 | export const restoreLimits = () => { 12 | const win = BrowserWindow.getAllWindows()[0]; 13 | if (initialLimits) win.setMinimumSize(initialLimits[0], initialLimits[1]); 14 | }; 15 | -------------------------------------------------------------------------------- /plugins/DiscordRPC/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DiscordRPC", 3 | "description": "Show off what you are listening to in your Discord status", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#DiscordRPC", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "exports": "./src/index.ts", 11 | "dependencies": { 12 | "@xhayper/discord-rpc": "^1.3.0" 13 | } 14 | } -------------------------------------------------------------------------------- /plugins/NativeFullscreen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NativeFullscreen", 3 | "description": "Add F11 hotkey for fullscreen to either make the normal UI fullscreen or tidal native fullscreen in a window", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#NativeFullscreen", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "exports": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/SmallWindow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SmallWindow", 3 | "description": "Removes the minimum width and height limits on the window. Causes some UI bugs but can be useful if you want a smaller window", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#SmallWindow", 5 | "author": { 6 | "name": "Nick Oates", 7 | "url": "https://github.com/n1ckoates", 8 | "avatarUrl": "https://1.gravatar.com/avatar/665fef45b1c988d52f011b049b99417485b9b558947169bc4b726b8eb69a2226" 9 | }, 10 | "exports": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/DesktopConnect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DesktopConnect", 3 | "description": "Remote control the TIDAL Desktop client via TIDAL Connect", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#DesktopConnect", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "exports": "./src/index.ts", 11 | "dependencies": { 12 | "@homebridge/ciao": "^1.3.1" 13 | } 14 | } -------------------------------------------------------------------------------- /plugins/CoverTheme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CoverTheme", 3 | "description": "Theme based on the current playing song. Also adds CSS variables to be used in custom themes", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#CoverTheme", 5 | "author": { 6 | "name": "Nick Oates", 7 | "url": "https://github.com/n1ckoates", 8 | "avatarUrl": "https://1.gravatar.com/avatar/665fef45b1c988d52f011b049b99417485b9b558947169bc4b726b8eb69a2226" 9 | }, 10 | "exports": "./src/index.tsx", 11 | "dependencies": { 12 | "node-vibrant": "4.0.3" 13 | } 14 | } -------------------------------------------------------------------------------- /plugins/VolumeScroll/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VolumeScroll", 3 | "description": "Lets you scroll on the volume icon to change the volume by 10%. Can configure the step size, including different amounts for when you hold SHIFT", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#VolumeScroll", 5 | "author": { 6 | "name": "Nick Oates", 7 | "url": "https://github.com/n1ckoates", 8 | "avatarUrl": "https://1.gravatar.com/avatar/665fef45b1c988d52f011b049b99417485b9b558947169bc4b726b8eb69a2226" 9 | }, 10 | "exports": "./src/index.ts" 11 | } -------------------------------------------------------------------------------- /plugins/CoverTheme/src/vibrant.native.ts: -------------------------------------------------------------------------------- 1 | import { Vibrant } from "node-vibrant/node"; 2 | 3 | export type Palette = Record; 4 | export type RGBSwatch = [r: number, g: number, b: number]; 5 | export const getPalette = async (coverUrl: string) => { 6 | const vibrantPalette = await new Vibrant(coverUrl, { quality: 1, useWorker: false }).getPalette(); 7 | const palette: Palette = {}; 8 | for (const color in vibrantPalette) { 9 | const swatch = vibrantPalette[color]; 10 | if (swatch) palette[color] = swatch.rgb; 11 | } 12 | return palette; 13 | }; 14 | -------------------------------------------------------------------------------- /plugins/LastFM/src/hash.native.ts: -------------------------------------------------------------------------------- 1 | import { createHash, type Encoding, type HashOptions, type BinaryToTextEncoding } from "crypto"; 2 | 3 | export const hash = ( 4 | data: string, 5 | options: { 6 | algorithm?: string; 7 | hashOptions?: HashOptions; 8 | inputEncoding?: Encoding; 9 | digestEncoding?: BinaryToTextEncoding; 10 | } = {} 11 | ) => { 12 | options.algorithm ??= "md5"; 13 | options.inputEncoding ??= "utf8"; 14 | options.digestEncoding ??= "hex"; 15 | return createHash(options.algorithm).update(data, options.inputEncoding).digest(options.digestEncoding); 16 | }; 17 | -------------------------------------------------------------------------------- /plugins/Themer/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { LunaButtonSetting, LunaLink, LunaSettings } from "@luna/ui"; 2 | import React from "react"; 3 | import { openEditor } from "."; 4 | 5 | export const Settings = () => ( 6 | 7 | 11 | Click the button or press CTRL + E to open the{" "} 12 | 13 | 14 | } 15 | onClick={openEditor} 16 | children="Open Editor" 17 | /> 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /plugins/LastFM/src/types/Scrobble.ts: -------------------------------------------------------------------------------- 1 | export interface Scrobble { 2 | scrobbles?: Scrobbles; 3 | } 4 | 5 | interface Scrobbles { 6 | scrobble?: ScrobbleClass; 7 | "@attr"?: Attr; 8 | } 9 | 10 | export interface Attr { 11 | ignored?: number; 12 | accepted?: number; 13 | } 14 | 15 | interface ScrobbleClass { 16 | artist?: Album; 17 | album?: Album; 18 | track?: Album; 19 | ignoredMessage?: IgnoredMessage; 20 | albumArtist?: Album; 21 | timestamp?: string; 22 | } 23 | 24 | interface Album { 25 | corrected?: string; 26 | "#text"?: string; 27 | } 28 | 29 | interface IgnoredMessage { 30 | code?: string; 31 | "#text"?: string; 32 | } 33 | -------------------------------------------------------------------------------- /plugins/Shazam/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Shazam", 3 | "description": "Drop any file into a playlist to search Shazam and add it", 4 | "homepage": "https://github.com/Inrixia/luna-plugins#Shazam", 5 | "author": { 6 | "name": "Inrixia", 7 | "url": "https://github.com/Inrixia", 8 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 9 | }, 10 | "main": "./src/index.js", 11 | "dependencies": { 12 | "shazamio-core": "^1.3.1", 13 | "uuid": "^11.1.0" 14 | }, 15 | "scripts": { 16 | "postinstall": "node ./fixShazamio.js" 17 | }, 18 | "devDependencies": { 19 | "@types/uuid": "^10.0.0" 20 | } 21 | } -------------------------------------------------------------------------------- /plugins/TidalTags/src/styles.css: -------------------------------------------------------------------------------- 1 | div[class*="titleCell"] { 2 | width: auto !important; 3 | } 4 | 5 | .quality-tag-container { 6 | overflow: none; 7 | display: inline-flex; 8 | height: 24px; 9 | font-size: 12px; 10 | line-height: 24px; 11 | } 12 | 13 | .quality-tag { 14 | justify-content: center; 15 | align-items: center; 16 | padding: 0 8px; 17 | border-radius: 6px; 18 | background-color: #222222; 19 | box-sizing: border-box; 20 | transition: background-color 0.2s; 21 | margin-left: 5px; 22 | } 23 | 24 | .format-info { 25 | width: 100px; 26 | min-height: 32px; 27 | text-align: center; 28 | padding: 4px; 29 | font-size: 13px; 30 | border-radius: 8px; 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Validate & Build" 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | Sanity: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Install pnpm 📥 15 | uses: pnpm/action-setup@v4 16 | with: 17 | version: latest 18 | 19 | - name: Install Node.js 📥 20 | uses: actions/setup-node@v4 21 | with: 22 | cache: pnpm 23 | node-version: latest 24 | 25 | - name: Install dependencies 📥 26 | run: pnpm install 27 | 28 | - name: Build 29 | run: pnpm run build 30 | 31 | - name: Upload Build Artifacts 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: luna-artifacts 35 | path: ./dist 36 | -------------------------------------------------------------------------------- /themes/example.css: -------------------------------------------------------------------------------- 1 | /* 2 | { 3 | "name": "Example", 4 | "author": "@Inrixia", 5 | "description": "An example theme..." 6 | } 7 | */ 8 | * { 9 | font-family: "Comic Sans MS", "Comic Sans", cursive; 10 | } 11 | 12 | [id^="main"] { 13 | background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 1) 100%), url("https://pbs.twimg.com/media/FxkvrreWcAAMPwe?format=jpg&name=large") no-repeat; 14 | background-size: cover; 15 | } 16 | 17 | [id^="titleCell"], 18 | [id^="item"], 19 | [id^="albumText"], 20 | [id^="timeColumn"], 21 | [id^="contributionText"], 22 | [id^="releasedDateColumn"] { 23 | color: #c4ab60; 24 | } 25 | 26 | [id^="headerColumn"], 27 | .wave-text-description-demi, 28 | .wave-text-title-bold { 29 | color: #e26b69; 30 | font-weight: 800; 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] [PluginName] " 5 | labels: enhancement 6 | assignees: Inrixia 7 | 8 | --- 9 | 10 | ## Prerequisites 11 | Please check the following before submitting: 12 | - [ ] I have searched for [**existing issues**](https://github.com/Inrixia/luna-plugins/issues?q=is%3Aissue) to ensure this is not a duplicate, including **closed** issues. 13 | - [ ] This is a issue for a **Plugin** from the [**@inrixia/luna-plugins**](https://github.com/Inrixia/luna-plugin) repo, not the [Luna Client](https://github.com/Inrixia/TidaLuna) or a plugin from a different repo 14 | 15 | ## Describe the feature you would like 16 | A clear and concise description of what you want 17 | -------------------------------------------------------------------------------- /plugins/RealMax/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactiveStore } from "@luna/core"; 2 | import { LunaSettings, LunaSwitchSetting } from "@luna/ui"; 3 | import React from "react"; 4 | 5 | export const settings = await ReactiveStore.getPluginStorage("RealMAX", { 6 | displayInfoPopups: true, 7 | }); 8 | 9 | export const Settings = () => { 10 | const [displayInfoPopups, setDisplayInfoPopups] = React.useState(settings.displayInfoPopups); 11 | return ( 12 | 13 | { 18 | setDisplayInfoPopups((settings.displayInfoPopups = checked)); 19 | }} 20 | /> 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/releaseDev.yml: -------------------------------------------------------------------------------- 1 | name: "[master] Release" 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | paths-ignore: 8 | - "**/*.md" 9 | - ".vscode/**" 10 | 11 | jobs: 12 | Build: 13 | uses: ./.github/workflows/build.yml 14 | 15 | Release: 16 | name: Release dev on GitHub 17 | needs: Build 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Download All Artifacts 22 | uses: actions/download-artifact@v4 23 | with: 24 | name: luna-artifacts 25 | path: ./dist/ 26 | 27 | - name: Publish latest release on GitHub 28 | uses: marvinpinto/action-automatic-releases@latest 29 | with: 30 | repo_token: ${{ secrets.GITHUB_TOKEN }} 31 | automatic_release_tag: dev 32 | prerelease: true 33 | title: Latest Release 34 | files: ./dist/** 35 | -------------------------------------------------------------------------------- /plugins/DiscordRPC/src/discord.native.ts: -------------------------------------------------------------------------------- 1 | import { Client, type SetActivity } from "@xhayper/discord-rpc"; 2 | 3 | let rpcClient: Client | null = null; 4 | export const getClient = async () => { 5 | const isAvailable = rpcClient && rpcClient.transport.isConnected && rpcClient.user; 6 | if (isAvailable) return rpcClient!; 7 | 8 | if (rpcClient) await rpcClient.destroy(); 9 | rpcClient = new Client({ clientId: "1130698654987067493" }); 10 | await rpcClient.connect(); 11 | 12 | return rpcClient; 13 | }; 14 | 15 | export const setActivity = async (activity?: SetActivity) => { 16 | const client = await getClient(); 17 | if (!client.user) return; 18 | if (!activity) return client.user.clearActivity(); 19 | return client.user.setActivity(activity); 20 | }; 21 | 22 | export const cleanupRPC = () => rpcClient?.destroy()!; 23 | -------------------------------------------------------------------------------- /.github/workflows/releaseMaster.yml: -------------------------------------------------------------------------------- 1 | name: "[master] Release" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - "**/*.md" 9 | - ".vscode/**" 10 | 11 | jobs: 12 | Build: 13 | uses: ./.github/workflows/build.yml 14 | 15 | Release: 16 | name: Release latest on GitHub 17 | needs: Build 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Download All Artifacts 22 | uses: actions/download-artifact@v4 23 | with: 24 | name: luna-artifacts 25 | path: ./dist/ 26 | 27 | - name: Publish latest release on GitHub 28 | uses: marvinpinto/action-automatic-releases@latest 29 | with: 30 | repo_token: ${{ secrets.GITHUB_TOKEN }} 31 | automatic_release_tag: latest 32 | prerelease: false 33 | title: Latest Release 34 | files: ./dist/** 35 | -------------------------------------------------------------------------------- /plugins/DiscordRPC/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Tracer, type LunaUnload } from "@luna/core"; 2 | import { MediaItem, redux } from "@luna/lib"; 3 | 4 | import { cleanupRPC } from "./discord.native"; 5 | import { updateActivity } from "./updateActivity"; 6 | 7 | export const unloads = new Set(); 8 | export const { trace, errSignal } = Tracer("[DiscordRPC]"); 9 | export { Settings } from "./Settings"; 10 | 11 | redux.intercept(["playbackControls/TIME_UPDATE", "playbackControls/SEEK", "playbackControls/SET_PLAYBACK_STATE"], unloads, () => { 12 | updateActivity() 13 | .then(() => (errSignal!._ = undefined)) 14 | .catch(trace.err.withContext("Failed to set activity")); 15 | }); 16 | unloads.add(MediaItem.onMediaTransition(unloads, updateActivity)); 17 | unloads.add(cleanupRPC.bind(cleanupRPC)); 18 | 19 | setTimeout(updateActivity); 20 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/lib/setColumn.ts: -------------------------------------------------------------------------------- 1 | import { isElement } from "./isElement"; 2 | 3 | export const setColumn = (trackRow: Element, name: string, sourceSelector: string, content: HTMLElement, beforeSelector?: string | Element) => { 4 | let column = trackRow.querySelector(`div[data-test="${name}"]`); 5 | if (column !== null) return; 6 | 7 | const sourceColumn = trackRow.querySelector(sourceSelector); 8 | if (sourceColumn === null) return; 9 | 10 | column = sourceColumn?.cloneNode(true); 11 | if (!isElement(column)) return; 12 | 13 | column.setAttribute("data-test", name); 14 | column.innerHTML = ""; 15 | column.appendChild(content); 16 | return sourceColumn.parentElement!.insertBefore( 17 | column, 18 | beforeSelector instanceof Element ? beforeSelector : beforeSelector ? trackRow.querySelector(beforeSelector) : sourceColumn 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /plugins/NoBuffer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { asyncDebounce } from "@inrixia/helpers"; 2 | import { Tracer, type LunaUnload } from "@luna/core"; 3 | import { MediaItem, PlayState, type redux } from "@luna/lib"; 4 | 5 | import { voidTrack } from "./voidTrack.native"; 6 | 7 | export const { trace, errSignal } = Tracer("[NoBuffer]"); 8 | 9 | const kickCache = new Set(); 10 | const onStalled = asyncDebounce(async () => { 11 | const mediaItem = await MediaItem.fromPlaybackContext(); 12 | if (mediaItem === undefined || kickCache.has(mediaItem.id)) return; 13 | kickCache.add(mediaItem.id); 14 | trace.msg.log(`Playback stalled... Kicking tidal CDN!`); 15 | await voidTrack(await mediaItem?.playbackInfo()).catch(trace.err.withContext("voidTrack")); 16 | }); 17 | 18 | export const unloads = new Set(); 19 | PlayState.onState(unloads, (state) => { 20 | if (state === "STALLED") onStalled(); 21 | }); 22 | -------------------------------------------------------------------------------- /plugins/ListenBrainz/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Tracer, type LunaUnload } from "@luna/core"; 2 | import { MediaItem, PlayState } from "@luna/lib"; 3 | 4 | export const { trace, errSignal } = Tracer("[ListenBrainz]"); 5 | 6 | import { ListenBrainz } from "./ListenBrainz"; 7 | import { makeTrackPayload } from "./makeTrackPayload"; 8 | 9 | export { Settings } from "./Settings"; 10 | 11 | export const unloads = new Set(); 12 | unloads.add( 13 | MediaItem.onMediaTransition(unloads, async (mediaItem) => { 14 | const payload = await makeTrackPayload(mediaItem); 15 | ListenBrainz.updateNowPlaying(payload).catch(trace.msg.err.withContext(`Failed to update NowPlaying!`)); 16 | }) 17 | ); 18 | unloads.add( 19 | PlayState.onScrobble(unloads, async (mediaItem) => { 20 | const payload = await makeTrackPayload(mediaItem); 21 | ListenBrainz.scrobble(payload).catch(trace.msg.err.withContext(`Failed to scrobble!`)); 22 | }) 23 | ); 24 | -------------------------------------------------------------------------------- /plugins/Themer/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Settings } from "./Settings"; 2 | 3 | import { ReactiveStore, type LunaUnload } from "@luna/core"; 4 | import "./editor.native"; 5 | 6 | import { ipcRenderer, StyleTag } from "@luna/lib"; 7 | import { closeEditor, openEditor as openEditorNative } from "./editor.native"; 8 | 9 | const storage = await ReactiveStore.getPluginStorage("Themer", { css: "" }); 10 | 11 | export const unloads = new Set(); 12 | 13 | export const openEditor = () => openEditorNative(storage.css); 14 | const style = new StyleTag("Themer", unloads, storage.css); 15 | 16 | ipcRenderer.on(unloads, "THEMER_SET_CSS", (css: string) => (storage.css = style.css = css)); 17 | 18 | const onKeyDown = (event: KeyboardEvent) => event.ctrlKey && event.key === "e" && openEditor(); 19 | document.addEventListener("keydown", onKeyDown); 20 | 21 | unloads.add(closeEditor); 22 | unloads.add(() => document.removeEventListener("keydown", onKeyDown)); 23 | -------------------------------------------------------------------------------- /plugins/CoverTheme/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { ReactiveStore } from "@luna/core"; 4 | import { LunaSettings, LunaSwitchSetting } from "@luna/ui"; 5 | 6 | import { style } from "."; 7 | 8 | export const storage = ReactiveStore.getStore("CoverTheme"); 9 | export const settings = await ReactiveStore.getPluginStorage("CoverTheme", { applyTheme: true }); 10 | 11 | export const Settings = () => { 12 | const [applyTheme, setApplyTheme] = React.useState(settings.applyTheme); 13 | return ( 14 | 15 | { 21 | setApplyTheme((settings.applyTheme = checked)); 22 | checked ? style.add() : style.remove(); 23 | }} 24 | /> 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/lib/ensureColumnHeader.ts: -------------------------------------------------------------------------------- 1 | export const ensureColumnHeader = (trackList: Element, name: string, sourceSelector: string, beforeSelector?: string | Element) => { 2 | let columnHeader = trackList.querySelector(`span[data-test="${name}"][role="columnheader"]`); 3 | if (columnHeader !== null) return; 4 | 5 | const sourceColumn = trackList.querySelector(sourceSelector); 6 | if (!(sourceColumn instanceof HTMLElement)) return; 7 | 8 | columnHeader = sourceColumn.cloneNode(true); 9 | if ((columnHeader.firstChild?.childNodes?.length ?? -1) > 1) columnHeader.firstChild?.lastChild?.remove(); 10 | columnHeader.setAttribute("data-test", name); 11 | columnHeader.firstChild!.firstChild!.textContent = name; 12 | 13 | return sourceColumn.parentElement!.insertBefore( 14 | columnHeader, 15 | beforeSelector instanceof Element ? beforeSelector : beforeSelector ? trackList.querySelector(beforeSelector) : sourceColumn 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /plugins/ListenBrainz/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactiveStore } from "@luna/core"; 2 | import { LunaLink, LunaSecureTextSetting, LunaSettings } from "@luna/ui"; 3 | 4 | import React from "react"; 5 | 6 | import { errSignal } from "."; 7 | 8 | export const storage = await ReactiveStore.getPluginStorage<{ 9 | userToken?: string; 10 | }>("ListenBrainz"); 11 | 12 | export const Settings = () => { 13 | const [token, setToken] = React.useState(storage.userToken); 14 | 15 | React.useEffect(() => { 16 | errSignal!._ = (token ?? "") === "" ? "User token not set." : undefined; 17 | }, [token]); 18 | return ( 19 | 20 | 24 | User token from{" "} 25 | 26 | listenbrainz.org/settings 27 | 28 | 29 | } 30 | value={token} 31 | onChange={(e) => setToken((storage.userToken = e.target.value))} 32 | error={!token} 33 | /> 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /plugins/VolumeScroll/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactiveStore } from "@luna/core"; 2 | import { LunaNumberSetting, LunaSettings } from "@luna/ui"; 3 | import React from "react"; 4 | 5 | export const storage = await ReactiveStore.getPluginStorage("VolumeScroll", { 6 | changeBy: 10, 7 | changeByShift: 1, 8 | }); 9 | 10 | export const Settings = () => { 11 | const [changeBy, setChangeBy] = React.useState(storage.changeBy); 12 | const [changeByShift, setChangeByShift] = React.useState(storage.changeByShift); 13 | return ( 14 | 15 | setChangeBy((storage.changeBy = num))} 22 | /> 23 | setChangeByShift((storage.changeByShift = num))} 28 | /> 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /plugins/Shazam/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactiveStore } from "@luna/core"; 2 | import { LunaSettings, LunaSwitchSetting } from "@luna/ui"; 3 | 4 | import React from "react"; 5 | 6 | export const storage = await ReactiveStore.getPluginStorage("Shazam", { 7 | startInMiddle: true, 8 | exitOnFirstMatch: true, 9 | }); 10 | 11 | export const Settings = () => { 12 | const [startInMiddle, setStartInMiddle] = React.useState(storage.startInMiddle); 13 | const [exitOnFirstMatch, setExitOnFirstMatch] = React.useState(storage.exitOnFirstMatch); 14 | 15 | return ( 16 | 17 | { 22 | setStartInMiddle((storage.startInMiddle = checked)); 23 | }} 24 | /> 25 | { 30 | setExitOnFirstMatch((storage.exitOnFirstMatch = checked)); 31 | }} 32 | /> 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/index.ts: -------------------------------------------------------------------------------- 1 | import { MediaItem, observe, StyleTag } from "@luna/lib"; 2 | 3 | import styles from "file://styles.css?minify"; 4 | import { unloads } from "./index.safe"; 5 | import { setFormatInfo } from "./setFormatInfo"; 6 | import { setInfoColumns as setFormatColumns } from "./setInfoColumns"; 7 | import { setQualityTags } from "./setQualityTags"; 8 | import { settings, Settings } from "./Settings"; 9 | 10 | export { Settings, unloads }; 11 | 12 | new StyleTag("TidalTags", unloads, styles); 13 | 14 | observe(unloads, 'div[data-test="tracklist-row"]', async (trackRow) => { 15 | if (!settings.displayQalityTags && !settings.displayFormatColumns) return; 16 | const trackId = trackRow.getAttribute("data-track-id"); 17 | if (trackId == null) return; 18 | 19 | const mediaItem = await MediaItem.fromId(trackId); 20 | if (mediaItem === undefined) return; 21 | 22 | if (settings.displayQalityTags) setQualityTags(trackRow, mediaItem); 23 | if (settings.displayFormatColumns) await setFormatColumns(trackRow, mediaItem); 24 | }); 25 | 26 | MediaItem.onMediaTransition(unloads, setFormatInfo); 27 | MediaItem.fromPlaybackContext().then(setFormatInfo); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@inrixia/luna-plugins", 3 | "description": "Plugins for Tidal Luna", 4 | "author": { 5 | "name": "Inrixia", 6 | "url": "https://github.com/Inrixia", 7 | "avatarUrl": "https://2.gravatar.com/avatar/eeaffef9eb9b436dccc58c6c44c9fe8c3528e83e3bf64e1c736a68dbe8c097d3" 8 | }, 9 | "homepage": "https://github.com/Inrixia/luna-plugins", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Inrixia/luna-plugins.git" 13 | }, 14 | "type": "module", 15 | "scripts": { 16 | "watch": "concurrently npm:esWatch npm:serve", 17 | "build": "rimraf ./dist && tsx esbuild.config.ts", 18 | "esWatch": "rimraf ./dist && tsx ./esbuild.config.ts --watch", 19 | "serve": "http-server ./dist -p 3000 -s --cors -c-1" 20 | }, 21 | "devDependencies": { 22 | "@inrixia/helpers": "^3.20.2", 23 | "@types/node": "^24.0.0", 24 | "@types/react": "^19.1.7", 25 | "@types/react-dom": "^19.1.6", 26 | "concurrently": "^9.1.2", 27 | "electron": "^36.4.0", 28 | "http-server": "^14.1.1", 29 | "luna": "github:inrixia/TidaLuna#10ae3d0", 30 | "oby": "^15.1.2", 31 | "rimraf": "^6.0.1", 32 | "tsx": "^4.20.0", 33 | "typescript": "^5.8.3" 34 | } 35 | } -------------------------------------------------------------------------------- /plugins/ListenBrainz/src/ListenBrainz.ts: -------------------------------------------------------------------------------- 1 | import type { Payload } from "./ListenBrainzTypes"; 2 | import { storage } from "./Settings"; 3 | 4 | type NowPlayingPayload = Omit; 5 | 6 | export class ListenBrainz { 7 | private static async sendRequest(body?: { listen_type: "single" | "playing_now"; payload: Payload[] | NowPlayingPayload[] }) { 8 | if (storage.userToken === "") throw new Error("User token not set"); 9 | return fetch(`https://api.listenbrainz.org/1/submit-listens`, { 10 | headers: { 11 | "Content-type": "application/json", 12 | Accept: "application/json", 13 | Authorization: `Token ${storage.userToken}`, 14 | }, 15 | method: "POST", 16 | body: JSON.stringify(body), 17 | }); 18 | } 19 | 20 | public static updateNowPlaying(payload: NowPlayingPayload) { 21 | // @ts-expect-error Ensure this doesnt exist 22 | delete payload.listened_at; 23 | return ListenBrainz.sendRequest({ 24 | listen_type: "playing_now", 25 | payload: [payload], 26 | }); 27 | } 28 | 29 | public static scrobble(payload: Payload) { 30 | return ListenBrainz.sendRequest({ 31 | listen_type: "single", 32 | payload: [payload], 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugins/SongDownloader/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { MediaItem } from "@luna/lib"; 2 | import { showOpenDialog, showSaveDialog } from "@luna/lib.native"; 3 | import { settings } from "./Settings"; 4 | 5 | import sanitize from "sanitize-filename"; 6 | 7 | export const getDownloadFolder = async () => { 8 | const { canceled, filePaths } = await showOpenDialog({ properties: ["openDirectory", "createDirectory"] }); 9 | if (!canceled) return filePaths[0]; 10 | }; 11 | export const getDownloadPath = async (defaultPath: string) => { 12 | const { canceled, filePath } = await showSaveDialog({ 13 | defaultPath, 14 | filters: [{ name: "", extensions: [defaultPath ?? "*"] }], 15 | }); 16 | if (!canceled) return filePath; 17 | }; 18 | export const getFileName = async (mediaItem: MediaItem) => { 19 | let fileName = `${settings.pathFormat}.${await mediaItem.fileExtension()}`; 20 | const { tags } = await mediaItem.flacTags(); 21 | for (const tag of MediaItem.availableTags) { 22 | let tagValue = tags[tag]; 23 | if (Array.isArray(tagValue)) tagValue = tagValue[0]; 24 | if (tagValue === undefined) continue; 25 | fileName = fileName.replaceAll(`{${tag}}`, sanitize(tagValue)); 26 | } 27 | return fileName; 28 | }; 29 | -------------------------------------------------------------------------------- /plugins/ListenBrainz/src/makeTrackPayload.ts: -------------------------------------------------------------------------------- 1 | import type { MediaItem } from "@luna/lib"; 2 | 3 | import { type Payload, MusicServiceDomain } from "./ListenBrainzTypes"; 4 | 5 | const delUndefined = >(obj: O) => { 6 | for (const key in obj) if (obj[key] === undefined) delete obj[key]; 7 | }; 8 | 9 | export const makeTrackPayload = async (mediaItem: MediaItem): Promise => { 10 | const album = await mediaItem.album(); 11 | 12 | const trackPayload: Payload = { 13 | listened_at: Math.floor(Date.now() / 1000), 14 | track_metadata: { 15 | artist_name: (await mediaItem.artist())?.name!, 16 | track_name: (await mediaItem.title())!, 17 | release_name: await album?.title(), 18 | }, 19 | }; 20 | 21 | trackPayload.track_metadata.additional_info = { 22 | recording_mbid: await mediaItem.brainzId(), 23 | isrc: await mediaItem.isrc(), 24 | tracknumber: mediaItem.trackNumber, 25 | music_service: MusicServiceDomain.TIDAL, 26 | origin_url: mediaItem.url, 27 | duration: mediaItem.duration, 28 | media_player: "Tidal Desktop", 29 | submission_client: "TidaLuna Scrobbler", 30 | }; 31 | delUndefined(trackPayload.track_metadata.additional_info); 32 | return trackPayload; 33 | }; 34 | -------------------------------------------------------------------------------- /plugins/PersistSettings/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { LunaUnload } from "@luna/core"; 2 | import { redux } from "@luna/lib"; 3 | 4 | import { storage } from "./Settings"; 5 | 6 | const interval = setInterval(() => { 7 | const { player, settings } = redux.store.getState(); 8 | 9 | // Player specific settings 10 | if (storage.exclusiveMode && player.activeDeviceMode === "shared") redux.actions["player/SET_DEVICE_MODE"]("exclusive"); 11 | 12 | const playerForceVolume = player.forceVolume[player.activeDeviceId]; 13 | if (storage.forceVolume !== !!playerForceVolume) { 14 | redux.actions["player/SET_FORCE_VOLUME"]({ deviceId: player.activeDeviceId, on: storage.forceVolume }); 15 | } 16 | 17 | // General settings 18 | if (storage.autoplay !== settings.autoPlay) redux.actions["settings/TOGGLE_AUTOPLAY"](); 19 | if (storage.normalizeVolume !== (settings.audioNormalization !== "NONE")) redux.actions["settings/TOGGLE_NORMALIZATION"](); 20 | if (storage.explicitContent !== settings.explicitContentEnabled) { 21 | redux.actions["settings/SET_EXPLICIT_CONTENT_TOGGLE"]({ isEnabled: storage.explicitContent }); 22 | } 23 | }, 1000); 24 | 25 | export { Settings } from "./Settings"; 26 | export const unloads = new Set(); 27 | unloads.add(() => clearInterval(interval)); 28 | -------------------------------------------------------------------------------- /plugins/NativeFullscreen/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactiveStore } from "@luna/core"; 2 | import { LunaSettings, LunaSwitchSetting } from "@luna/ui"; 3 | 4 | import { setTopBarVisibility } from "."; 5 | 6 | import React from "react"; 7 | 8 | export const storage = await ReactiveStore.getPluginStorage("NativeFullscreen", { 9 | useTidalFullscreen: false, 10 | hideTopBar: false, 11 | }); 12 | 13 | export const Settings = () => { 14 | const [useTidalFullscreen, setUseTidalFullscreen] = React.useState(storage.useTidalFullscreen); 15 | const [hideTopBar, setHideTopBar] = React.useState(storage.hideTopBar); 16 | return ( 17 | 18 | setUseTidalFullscreen((storage.useTidalFullscreen = checked))} 23 | /> 24 | { 29 | setTopBarVisibility(!checked); 30 | setHideTopBar((storage.hideTopBar = checked)); 31 | }} 32 | /> 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /plugins/VolumeScroll/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { LunaUnload } from "@luna/core"; 2 | import { redux } from "@luna/lib"; 3 | import { storage } from "./Settings"; 4 | export { Settings } from "./Settings"; 5 | 6 | function onScroll(event: WheelEvent) { 7 | if (!event.deltaY) return; 8 | const { playbackControls } = redux.store.getState(); 9 | const changeBy = event.shiftKey ? storage.changeByShift : storage.changeBy; 10 | const volumeChange = event.deltaY > 0 ? -changeBy : changeBy; 11 | const newVolume = playbackControls.volume + volumeChange; 12 | const clampVolume = Math.min(100, Math.max(0, newVolume)); 13 | redux.actions["playbackControls/SET_VOLUME"]({ 14 | volume: clampVolume, 15 | }); 16 | } 17 | 18 | let element: HTMLDivElement | null = null; 19 | 20 | function initElement() { 21 | if (element) return; 22 | const elements = document.querySelectorAll('div[class^="_sliderContainer"]'); 23 | if (elements.length === 0) return; 24 | element = elements[0] as HTMLDivElement; 25 | element.addEventListener("wheel", onScroll); 26 | } 27 | 28 | export const unloads = new Set(); 29 | unloads.add(() => element?.removeEventListener("wheel", onScroll)); 30 | 31 | // Element doesn't exist until the page is loaded 32 | redux.intercept("page/SET_PAGE_ID", unloads, initElement); 33 | 34 | // Initialize element if it already exists (e.g. plugin is installed or reloaded) 35 | initElement(); 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug or issue 4 | title: "[BUG] [PluginName] " 5 | labels: bug 6 | assignees: Inrixia 7 | 8 | --- 9 | 10 | ## Prerequisites 11 | Please check the following before continuing: 12 | - [ ] I have searched for [**existing issues**](https://github.com/Inrixia/luna-plugins/issues?q=is%3Aissue), including **closed** issues to ensure this is not a duplicate 13 | - [ ] This is a issue for a **Plugin** from the [**@inrixia/luna-plugins**](https://github.com/Inrixia/luna-plugin) repo, not the [Luna Client](https://github.com/Inrixia/TidaLuna) or a plugin from a different repo 14 | 15 | ## Environment 16 | - **OS**: Windows, Linux or MacOS 17 | 18 | ## Description 19 | A clear and concise description of what the bug/issue is. 20 | 21 | ## Steps to Reproduce 22 | Steps to reproduce the behavior: 23 | 1. Go to '...' 24 | 2. Click on '...' 25 | 3. Scroll down to '...' 26 | 4. See error 27 | 28 | ## Expected Behavior 29 | A clear and concise description of what you expected to happen. 30 | 31 | ## Actual Behavior 32 | A clear and concise description of what actually happened. 33 | 34 | ## Screenshots / Logs 35 | To open console to view logs & errors press **Ctrl-Shift-I**. 36 | If applicable, add screenshots or paste error logs to help explain your problem. 37 | 38 | ## Additional Context 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /plugins/LastFM/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { LunaButtonSetting, LunaSettings } from "@luna/ui"; 2 | import { errSignal, trace } from "."; 3 | import { LastFM, type LastFmSession } from "./LastFM"; 4 | 5 | import { ReactiveStore } from "@luna/core"; 6 | 7 | import React from "react"; 8 | 9 | export const storage = await ReactiveStore.getPluginStorage<{ 10 | session?: LastFmSession; 11 | }>("LastFM"); 12 | 13 | export const Settings = () => { 14 | const [session, setSession] = React.useState(storage.session); 15 | const [loading, setLoading] = React.useState(false); 16 | 17 | const connected = session !== undefined; 18 | 19 | return ( 20 | 21 | { 26 | setLoading(true); 27 | const res = await LastFM.authenticate().catch(trace.err.withContext("Authenticating")); 28 | 29 | setSession((storage.session = res?.session)); 30 | if (storage.session !== undefined) errSignal!._ = undefined; 31 | setLoading(false); 32 | }} 33 | loading={loading} 34 | sx={{ 35 | color: connected ? "green" : undefined, 36 | }} 37 | > 38 | {loading ? "Loading..." : connected ? "Reconnect" : "Connect"} 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/setQualityTags.ts: -------------------------------------------------------------------------------- 1 | import { Quality, type MediaItem } from "@luna/lib"; 2 | 3 | export const setQualityTags = (trackRow: Element, mediaItem: MediaItem) => { 4 | const trackTitle = trackRow.querySelector(`[data-test="table-row-title"]`); 5 | if (trackTitle === null) return; 6 | 7 | const { qualityTags, bestQuality } = mediaItem; 8 | const id = String(mediaItem.id); 9 | if (qualityTags.length === 0) return; 10 | 11 | if (qualityTags.length === 1 && qualityTags[0] === Quality.High && bestQuality === Quality.High) return; 12 | 13 | let span = trackTitle.querySelector(".quality-tag-container"); 14 | if (span?.getAttribute("track-id") === id) return; 15 | 16 | span?.remove(); 17 | span = document.createElement("span"); 18 | 19 | span.className = "quality-tag-container"; 20 | span.setAttribute("track-id", id); 21 | 22 | if (bestQuality < Quality.High) { 23 | const tagElement = document.createElement("span"); 24 | tagElement.className = "quality-tag"; 25 | tagElement.textContent = bestQuality.name; 26 | tagElement.style.color = bestQuality.color; 27 | span.appendChild(tagElement); 28 | } 29 | 30 | for (const quality of qualityTags) { 31 | if (quality === Quality.High) continue; 32 | 33 | const tagElement = document.createElement("span"); 34 | tagElement.className = "quality-tag"; 35 | tagElement.textContent = quality.name; 36 | tagElement.style.color = quality.color; 37 | span.appendChild(tagElement); 38 | } 39 | 40 | trackTitle.appendChild(span); 41 | }; 42 | -------------------------------------------------------------------------------- /plugins/DesktopConnect/src/remoteService.native.ts: -------------------------------------------------------------------------------- 1 | import { getResponder } from "@homebridge/ciao"; 2 | import { hostname } from "os"; 3 | 4 | const RemoteDesktopController = require("./original.asar/app/main/remoteDesktop/RemoteDesktopController.js").default; 5 | const { generateDeviceId } = require("./original.asar/app/main/mdns/broadcast.js"); 6 | 7 | const websocket = require("./original.asar/app/main/remoteDesktop/websocket.js").default; 8 | 9 | export const setup = () => { 10 | if (RemoteDesktopController.__running) return console.warn("RemoteDesktopController is already running"); 11 | 12 | const responder = getResponder(); 13 | const deviceId = generateDeviceId(); 14 | 15 | const service = responder.createService({ 16 | disabledIpv6: true, 17 | port: 2019, 18 | type: "tidalconnect", 19 | name: `RemoteDesktop-${deviceId}`, 20 | txt: { 21 | mn: "RemoteDesktop", 22 | id: deviceId, 23 | fn: `TidaLuna: ${hostname().split(".")[0]}`, 24 | ca: "0", 25 | ve: "1", 26 | }, 27 | }); 28 | 29 | RemoteDesktopController.__running = true; 30 | const lunaRemoteDesktop = new RemoteDesktopController(); 31 | lunaRemoteDesktop.mdnsStartBroadcasting = () => service.advertise(); 32 | lunaRemoteDesktop.mdnsStopBroadcasting = () => service.end(); 33 | 34 | lunaRemoteDesktop.initialize("https://desktop.tidal.com"); 35 | lunaRemoteDesktop.mdnsStartBroadcasting(); 36 | 37 | lunaRemoteDesktop.remoteDesktopPlayer.remotePlayerProcess.stdout.on("data", (...args: any[]) => 38 | console.log("DesktopConnect.remotePlayerProcess.stdout", ...args) 39 | ); 40 | }; 41 | setup(); 42 | 43 | export const send = websocket.send.bind(websocket); 44 | -------------------------------------------------------------------------------- /plugins/Avatar/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { LunaUnload, ReactiveStore } from "@luna/core"; 2 | import { redux, safeTimeout, StyleTag } from "@luna/lib"; 3 | import { LunaSettings, LunaTextSetting } from "@luna/ui"; 4 | import React from "react"; 5 | import { md5 } from "./md5.native"; 6 | 7 | export const unloads = new Set(); 8 | const avatarCSS = new StyleTag("avatarCSS", unloads); 9 | 10 | export const settings = await ReactiveStore.getPluginStorage<{ 11 | customUrl?: string; 12 | }>("Avatar"); 13 | 14 | const setAvatar = async (customUrl?: string) => { 15 | if (customUrl === "" || customUrl === undefined) { 16 | const emailHash = await md5(redux.store.getState().user.meta.email); 17 | return (settings.customUrl = `https://www.gravatar.com/avatar/${emailHash}?d=identicon`); 18 | } 19 | // Thx @n1ckoates 20 | avatarCSS.css = ` 21 | [class^="_profilePicture_"] { 22 | background-image: url("${customUrl}"); 23 | background-size: cover; 24 | } 25 | 26 | [class^="_profilePicture_"] svg { 27 | display: none; 28 | } 29 | `; 30 | }; 31 | 32 | safeTimeout( 33 | unloads, 34 | () => { 35 | setAvatar(settings.customUrl); 36 | }, 37 | 250 38 | ); 39 | 40 | export const Settings = () => { 41 | const [customUrl, setCustomUrl] = React.useState(settings.customUrl); 42 | 43 | return ( 44 | 45 | { 50 | settings.customUrl = e.target.value; 51 | setCustomUrl(await setAvatar(settings.customUrl)); 52 | }} 53 | /> 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /plugins/Themer/src/editor.native.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, shell } from "electron"; 2 | import editor from "file://editor.html?base64&minify"; 3 | import preloadCode from "file://editor.preload.js"; 4 | import { rm, writeFile } from "fs/promises"; 5 | import path from "path"; 6 | 7 | let win: BrowserWindow | null = null; 8 | export const openEditor = async (css: string) => { 9 | if (win && !win.isDestroyed()) return win.focus(); 10 | 11 | const preloadPath = path.join(app.getPath("temp"), `${Math.random().toString()}.preload.js`); 12 | try { 13 | await writeFile(preloadPath, preloadCode + `window.themerCSS = ${JSON.stringify(css)}`, "utf-8"); 14 | 15 | win = new BrowserWindow({ 16 | title: "TIDAL CSS Editor", 17 | width: 1000, 18 | height: 1000, 19 | webPreferences: { 20 | preload: preloadPath, 21 | }, 22 | autoHideMenuBar: true, 23 | backgroundColor: "#1e1e1e", 24 | }); 25 | 26 | // Open links in default browser 27 | win.webContents.setWindowOpenHandler(({ url }) => { 28 | shell.openExternal(url); 29 | return { action: "deny" }; 30 | }); 31 | 32 | await win.loadURL(`data:text/html;base64,${editor}`); 33 | } finally { 34 | await rm(preloadPath, { force: true }); 35 | } 36 | }; 37 | 38 | ipcMain.removeAllListeners("themer.setCSS"); 39 | ipcMain.on("themer.setCSS", async (_: unknown, css: string) => { 40 | const tidalWindow = BrowserWindow.fromId(1); 41 | if (tidalWindow?.title !== "TIDAL") console.warn(`Themer: BrowserWindow.fromId(1).title is ${tidalWindow?.title} expected "TIDAL"`); 42 | tidalWindow?.webContents?.send("THEMER_SET_CSS", css); 43 | }); 44 | 45 | export const closeEditor = async () => { 46 | if (win && !win.isDestroyed()) win.close(); 47 | ipcMain.removeAllListeners("themer.setCSS"); 48 | }; 49 | -------------------------------------------------------------------------------- /plugins/LastFM/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Tracer, type LunaUnload } from "@luna/core"; 2 | import { MediaItem, PlayState, redux } from "@luna/lib"; 3 | 4 | export const { trace, errSignal } = Tracer("[last.fm]"); 5 | 6 | import { LastFM, ScrobbleOpts } from "./LastFM"; 7 | 8 | redux.actions["lastFm/DISCONNECT"](); 9 | 10 | const delUndefined = >(obj: O) => { 11 | for (const key in obj) if (obj[key] === undefined) delete obj[key]; 12 | }; 13 | 14 | const makeScrobbleOpts = async (mediaItem: MediaItem): Promise => { 15 | const album = await mediaItem.album(); 16 | const scrobbleOpts: Partial = { 17 | track: await mediaItem.title(), 18 | artist: (await mediaItem.artist())?.name, 19 | album: await album?.title(), 20 | albumArtist: (await album?.artist())?.name, 21 | trackNumber: mediaItem.trackNumber?.toString(), 22 | mbid: await mediaItem.brainzId(), 23 | timestamp: (Date.now() / 1000).toFixed(0), 24 | }; 25 | delUndefined(scrobbleOpts); 26 | return scrobbleOpts as ScrobbleOpts; 27 | }; 28 | 29 | export { Settings } from "./Settings"; 30 | 31 | export const unloads = new Set(); 32 | unloads.add( 33 | MediaItem.onMediaTransition(unloads, (mediaItem) => { 34 | makeScrobbleOpts(mediaItem).then(LastFM.updateNowPlaying).catch(trace.msg.err.withContext(`Failed to updateNowPlaying!`)); 35 | }) 36 | ); 37 | unloads.add( 38 | PlayState.onScrobble(unloads, async (mediaItem) => { 39 | const scrobbleOpts = await makeScrobbleOpts(mediaItem); 40 | LastFM.scrobble(scrobbleOpts) 41 | .catch(trace.msg.err.withContext(`Failed to scrobble!`)) 42 | .then((res) => { 43 | if (res?.scrobbles) trace.log("Scrobbled", scrobbleOpts, res?.scrobbles["@attr"], res.scrobbles.scrobble); 44 | }); 45 | }) 46 | ); 47 | -------------------------------------------------------------------------------- /plugins/Themer/src/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | TIDAL CSS Editor 9 | 13 | 27 | 28 | 29 | 30 |
31 | 34 | 35 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /plugins/Shazam/src/shazam.native.ts: -------------------------------------------------------------------------------- 1 | import init, { recognizeBytes, type DecodedSignature } from "shazamio-core/web"; 2 | init(); 3 | 4 | import { memoize } from "@inrixia/helpers"; 5 | import { v4 } from "uuid"; 6 | 7 | import type { ShazamData } from "./api.types"; 8 | 9 | const fetchShazamData = memoize(async (signature: { samplems: number; uri: string }): Promise => { 10 | // TODO: Re implement lib.native 11 | const res = await fetch( 12 | `https://amp.shazam.com/discovery/v5/en-US/US/iphone/-/tag/${v4()}/${v4()}?sync=true&webv3=true&sampling=true&connected=&shazamapiversion=v3&sharehub=true&hubv5minorversion=v5.1&hidelb=true&video=v3`, 13 | { 14 | headers: { "Content-Type": "application/json" }, 15 | method: "POST", 16 | body: JSON.stringify({ signature }), 17 | } 18 | ); 19 | if (!res.ok) throw new Error(`Failed to fetch Shazam data: ${res.statusText}`); 20 | return res.json(); 21 | }); 22 | 23 | type Opts = { 24 | bytes: ArrayBuffer; 25 | startInMiddle: boolean; 26 | exitOnFirstMatch: boolean; 27 | }; 28 | 29 | const using = async (signatures: DecodedSignature[], fun: (signatures: ReadonlyArray) => T) => { 30 | const ret = await fun(signatures); 31 | for (const signature of signatures) signature.free(); 32 | return ret; 33 | }; 34 | 35 | export const recognizeTrack = async ({ bytes, startInMiddle, exitOnFirstMatch }: Opts) => { 36 | const matches: ShazamData[] = []; 37 | await using(recognizeBytes(new Uint8Array(bytes), 0, Number.MAX_SAFE_INTEGER), async (signatures) => { 38 | let i = startInMiddle ? Math.floor(signatures.length / 2) : 1; 39 | for (; i < signatures.length; i += 4) { 40 | const sig = signatures[i]; 41 | const shazamData = await fetchShazamData({ samplems: sig.samplems, uri: sig.uri }); 42 | matches.push(shazamData); 43 | if (shazamData.matches.length === 0) continue; 44 | if (exitOnFirstMatch) return; 45 | } 46 | }); 47 | return matches; 48 | }; 49 | -------------------------------------------------------------------------------- /plugins/NativeFullscreen/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { LunaUnload } from "@luna/core"; 2 | import { redux } from "@luna/lib"; 3 | import { storage } from "./Settings"; 4 | export { Settings } from "./Settings"; 5 | 6 | export const unloads = new Set(); 7 | 8 | let enterNormalFullscreen: true | undefined = undefined; 9 | redux.intercept("view/FULLSCREEN_ALLOWED", unloads, () => { 10 | if (enterNormalFullscreen || storage.useTidalFullscreen) return (enterNormalFullscreen = undefined); 11 | return true; 12 | }); 13 | redux.intercept("view/REQUEST_FULLSCREEN", unloads, () => (enterNormalFullscreen = true)); 14 | 15 | export const setTopBarVisibility = (visible: boolean) => { 16 | const bar = document.querySelector("div[class^='_bar']"); 17 | if (bar) bar.style.display = visible ? "" : "none"; 18 | }; 19 | if (storage.hideTopBar) setTopBarVisibility(false); 20 | 21 | const onKeyDown = (event: KeyboardEvent) => { 22 | if (event.key === "F11") { 23 | event.preventDefault(); 24 | 25 | const contentContainer = document.querySelector("div[class^='_mainContainer'] > div[class^='_containerRow']"); 26 | const wimp = document.querySelector("#wimp > div"); 27 | 28 | if (document.fullscreenElement || wimp?.classList.contains("is-fullscreen")) { 29 | // Exiting fullscreen 30 | document.exitFullscreen(); 31 | if (wimp) wimp.classList.remove("is-fullscreen"); 32 | if (!storage.hideTopBar) setTopBarVisibility(true); 33 | if (contentContainer) contentContainer.style.maxHeight = ""; 34 | } else { 35 | // Entering fullscreen 36 | if (storage.useTidalFullscreen) { 37 | if (wimp) wimp.classList.add("is-fullscreen"); 38 | } else { 39 | document.documentElement.requestFullscreen(); 40 | setTopBarVisibility(false); 41 | if (contentContainer) contentContainer.style.maxHeight = `100%`; 42 | } 43 | } 44 | } 45 | }; 46 | 47 | window.addEventListener("keydown", onKeyDown); 48 | unloads.add(() => window.removeEventListener("keydown", onKeyDown)); 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [TidaLuna](https://github.com/Inrixia/TidaLuna) Plugins 2 | 3 | This is a repository containing plugins I have made for the [TidaLuna Client](https://github.com/Inrixia/TidaLuna). 4 | Want to chat, ask questions or hang out? Join the discord! **[discord.gg/jK3uHrJGx4](https://discord.gg/jK3uHrJGx4)** 5 | 6 | If you like TidaLuna and my Plugins and want to support me you can do so on my [Sponsor Page](https://github.com/sponsors/Inrixia) ❤️ 7 | 8 | ## Installing Plugins 9 | 10 | 1. Install the [TidaLuna Client](https://github.com/Inrixia/TidaLuna) 11 | 2. Open **Luna Settings** 12 | ![image](https://github.com/user-attachments/assets/5fbfdda5-5272-45ef-bb4f-e12eef919358) 13 | 3. Click on **Plugin Store** 14 | ![image](https://github.com/user-attachments/assets/86145ddd-90d4-4cc8-9abd-2a94393faf55) 15 | 4. Install the plugins you want from the stores 16 | ![image](https://github.com/user-attachments/assets/f9824d1f-6fb7-4c2b-b6fc-e904d6a6ad1b) 17 | 18 | ## Contributing 19 | Contributing is super simple and really appreciated! 20 | **If you want to make your own plugins please instead fork [luna-template](https://github.com/Inrixia/luna-template)** 21 | 22 | 1. Ensure you have **node** and **pnpm** installed. 23 | Install NVM (https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating) 24 | ```bash 25 | nvm install latest 26 | nvm use latest 27 | corepack enable 28 | ``` 29 | 30 | 2. [Fork](https://github.com/Inrixia/luna-plugins/fork) the repo 31 | 32 | 2. Clone the repo 33 | ```bash 34 | git clone github.com/yournamehere/luna-plugins 35 | cd luna-plugins 36 | ``` 37 | 38 | 3. Install the packages 39 | ```bash 40 | pnpm i 41 | ``` 42 | 43 | 4. Start dev environment 44 | ```bash 45 | pnpm run watch 46 | ``` 47 | 48 | 5. Work on DEV plugins 49 | When running `pnpm run watch` a *DEV* store will appear in client allowing installing *DEV* versions of plugins. 50 | ![image](https://github.com/user-attachments/assets/c159bf00-6feb-41c8-8884-3d9e63070c19) 51 | -------------------------------------------------------------------------------- /plugins/ListenBrainz/src/ListenBrainzTypes.ts: -------------------------------------------------------------------------------- 1 | export type Payload = { 2 | listened_at?: number; // The timestamp when the track was listened to (Unix time). Omit for "playing_now". 3 | track_metadata: { 4 | artist_name: string; // The name of the artist 5 | track_name: string; // The name of the track 6 | release_name?: string; // The name of the release (optional) 7 | additional_info?: AdditionalInfo; 8 | }; 9 | }; 10 | 11 | export type AdditionalInfo = { 12 | artist_mbids?: string[]; // List of MusicBrainz Artist IDs 13 | release_group_mbid?: string; // MusicBrainz Release Group ID 14 | release_mbid?: string; // MusicBrainz Release ID 15 | recording_mbid?: string; // MusicBrainz Recording ID 16 | track_mbid?: string; // MusicBrainz Track ID 17 | work_mbids?: string[]; // List of MusicBrainz Work IDs 18 | tracknumber?: number; // Track number 19 | isrc?: string; // ISRC code 20 | spotify_id?: string; // Spotify track URL 21 | tags?: string[]; // User-defined tags 22 | media_player?: string; // Media player used for playback 23 | media_player_version?: string; // Version of the media player 24 | submission_client?: string; // Client used to submit the listen 25 | submission_client_version?: string; // Version of the submission client 26 | music_service?: MusicServiceDomain; // Domain of the music service 27 | music_service_name?: MusicServiceName; // Textual name of the music service (if domain is unavailable) 28 | origin_url?: string; // URL of the source of the listen 29 | duration_ms?: number; // Duration of the track in milliseconds 30 | duration?: number; // Duration of the track in seconds 31 | }; 32 | 33 | export enum MusicServiceDomain { 34 | Spotify = "spotify.com", 35 | YouTube = "youtube.com", 36 | Bandcamp = "bandcamp.com", 37 | Deezer = "deezer.com", 38 | TIDAL = "tidal.com", 39 | Soundcloud = "soundcloud.com", 40 | AppleMusic = "music.apple.com", 41 | } 42 | 43 | export enum MusicServiceName { 44 | Spotify = "Spotify", 45 | YouTube = "YouTube", 46 | Bandcamp = "Bandcamp", 47 | Deezer = "Deezer", 48 | TIDAL = "TIDAL", 49 | Soundcloud = "Soundcloud", 50 | AppleMusic = "Apple Music", 51 | } 52 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactiveStore } from "@luna/core"; 2 | import { MediaItem } from "@luna/lib"; 3 | import { LunaSettings, LunaSwitchSetting } from "@luna/ui"; 4 | import React from "react"; 5 | import { formatInfoElem, setFormatInfo } from "./setFormatInfo"; 6 | 7 | export const settings = await ReactiveStore.getPluginStorage("TidalTags", { 8 | displayFormatBorder: true, 9 | displayQalityTags: true, 10 | displayFormatColumns: true, 11 | autoPopulateColumns: false, 12 | }); 13 | 14 | export const Settings = () => { 15 | const [displayFormatBorder, setDisplayFormatBorder] = React.useState(settings.displayFormatBorder); 16 | const [displayQalityTags, setDisplayQalityTags] = React.useState(settings.displayQalityTags); 17 | const [displayFormatColumns, setDisplayFormatColumns] = React.useState(settings.displayFormatColumns); 18 | const [autoPopulateColumns, setAutoPopulateColumns] = React.useState(settings.autoPopulateColumns); 19 | return ( 20 | 21 | { 26 | setDisplayFormatBorder((settings.displayFormatBorder = checked)); 27 | if (!checked) formatInfoElem.style.border = "none"; 28 | else MediaItem.fromPlaybackContext().then(setFormatInfo); 29 | }} 30 | /> 31 | { 36 | setDisplayQalityTags((settings.displayQalityTags = checked)); 37 | }} 38 | /> 39 | { 44 | setDisplayFormatColumns((settings.displayFormatColumns = checked)); 45 | }} 46 | /> 47 | { 52 | setAutoPopulateColumns((settings.autoPopulateColumns = checked)); 53 | }} 54 | /> 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /plugins/PersistSettings/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactiveStore } from "@luna/core"; 2 | import { LunaSettings, LunaSwitchSetting } from "@luna/ui"; 3 | 4 | import React from "react"; 5 | 6 | export const storage = await ReactiveStore.getPluginStorage("PersistSettings", { 7 | exclusiveMode: true, 8 | forceVolume: false, 9 | normalizeVolume: false, 10 | autoplay: true, 11 | explicitContent: true, 12 | }); 13 | 14 | export const Settings = () => { 15 | const [exclusiveMode, setExclusiveMode] = React.useState(storage.exclusiveMode); 16 | const [forceVolume, setForceVolume] = React.useState(storage.forceVolume); 17 | const [normalizeVolume, setNormalizeVolume] = React.useState(storage.normalizeVolume); 18 | 19 | const [autoplay, setAutoplay] = React.useState(storage.autoplay); 20 | const [explicitContent, setExplicitContent] = React.useState(storage.explicitContent); 21 | 22 | return ( 23 | 24 | { 29 | setExclusiveMode((storage.exclusiveMode = checked)); 30 | }} 31 | /> 32 | { 37 | setForceVolume((storage.forceVolume = checked)); 38 | }} 39 | /> 40 | { 45 | setNormalizeVolume((storage.normalizeVolume = checked)); 46 | }} 47 | /> 48 | { 53 | setAutoplay((storage.autoplay = checked)); 54 | }} 55 | /> 56 | { 61 | setExplicitContent((storage.explicitContent = checked)); 62 | }} 63 | /> 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/setInfoColumns.ts: -------------------------------------------------------------------------------- 1 | import { debounce } from "@inrixia/helpers"; 2 | import type { MediaItem } from "@luna/lib"; 3 | import { unloads } from "./index.safe"; 4 | import { ensureColumnHeader } from "./lib/ensureColumnHeader"; 5 | import { setColumn } from "./lib/setColumn"; 6 | import { settings } from "./Settings"; 7 | 8 | export const setInfoColumnHeaders = debounce(() => { 9 | for (const trackList of document.querySelectorAll(`div[aria-label="Tracklist"]`)) { 10 | const bitDepthColumn = ensureColumnHeader( 11 | trackList, 12 | "Depth", 13 | `span[class^="_timeColumn"][role="columnheader"]`, 14 | `span[class^="_timeColumn"][role="columnheader"]` 15 | ); 16 | bitDepthColumn?.style.setProperty("min-width", "40px"); 17 | const sampleRateColumn = ensureColumnHeader(trackList, "Sample Rate", `span[class^="_timeColumn"][role="columnheader"]`, bitDepthColumn); 18 | sampleRateColumn?.style.setProperty("min-width", "110px"); 19 | const bitrateColumn = ensureColumnHeader(trackList, "Bitrate", `span[class^="_timeColumn"][role="columnheader"]`, sampleRateColumn); 20 | bitrateColumn?.style.setProperty("min-width", "100px"); 21 | } 22 | }, 50); 23 | 24 | export const setInfoColumns = async (trackRow: Element, mediaItem: MediaItem) => { 25 | setInfoColumnHeaders(); 26 | const bitDepthContent = document.createElement("span"); 27 | const bitDepthColumn = setColumn(trackRow, "Depth", `div[data-test="duration"]`, bitDepthContent, `div[data-test="duration"]`); 28 | bitDepthColumn?.style.setProperty("min-width", "40px"); 29 | 30 | const sampleRateContent = document.createElement("span"); 31 | const sampleRateColumn = setColumn(trackRow, "Sample Rate", `div[data-test="duration"]`, sampleRateContent, bitDepthColumn); 32 | sampleRateColumn?.style.setProperty("min-width", "110px"); 33 | 34 | const bitrateContent = document.createElement("span"); 35 | const bitrateColumn = setColumn(trackRow, "Bitrate", `div[data-test="duration"]`, bitrateContent, sampleRateColumn); 36 | bitrateColumn?.style.setProperty("min-width", "100px"); 37 | 38 | const quality = mediaItem.bestQuality; 39 | 40 | bitDepthContent.style.color = sampleRateContent.style.color = bitrateContent.style.color = quality.color; 41 | 42 | mediaItem.withFormat(unloads, quality.audioQuality, ({ sampleRate, bitDepth, bitrate }) => { 43 | if (!!sampleRate) sampleRateContent.textContent = `${sampleRate / 1000}kHz`; 44 | if (!!bitDepth) bitDepthContent.textContent = `${bitDepth}bit`; 45 | if (!!bitrate) bitrateContent.textContent = `${Math.floor(bitrate / 1000).toLocaleString()}kbps`; 46 | }); 47 | 48 | if (settings.autoPopulateColumns) await mediaItem.updateFormat(quality.audioQuality); 49 | }; 50 | -------------------------------------------------------------------------------- /plugins/RealMax/src/contextMenu.ts: -------------------------------------------------------------------------------- 1 | import { trace, unloads } from "./index.safe"; 2 | 3 | import { chunkArray } from "@inrixia/helpers"; 4 | import { ContextMenu, redux } from "@luna/lib"; 5 | 6 | const maxNewPlaylistSize = 450; 7 | 8 | const maxButton = ContextMenu.addButton(unloads); 9 | 10 | ContextMenu.onMediaItem(unloads, async ({ mediaCollection, contextMenu }) => { 11 | const itemCount = await mediaCollection.count(); 12 | if (itemCount === 0) return; 13 | 14 | const defaultText = (maxButton.text = `RealMAX ${itemCount} tracks`); 15 | 16 | maxButton.onClick(async () => { 17 | let trackIds: redux.ItemId[] = []; 18 | const sourceTitle = await mediaCollection.title(); 19 | maxButton.text = `RealMAX Loading...`; 20 | 21 | try { 22 | let newItems = 0; 23 | for await (const mediaItem of await mediaCollection.mediaItems()) { 24 | const maxItem = await mediaItem.max(); 25 | maxButton.text = `RealMAX ${trackIds.length}/${itemCount} done. Found ${newItems} replacements`; 26 | if (maxItem === undefined) { 27 | trackIds.push(mediaItem.id); 28 | newItems++; 29 | continue; 30 | } 31 | trackIds.push(maxItem.id); 32 | trace.msg.log(`Found Max replacement for ${maxItem.tidalItem.title}!`); 33 | } 34 | 35 | if (newItems === 0) { 36 | return trace.msg.err(`No replacements found for ${sourceTitle}`); 37 | } 38 | 39 | maxButton.text = `RealMAX Creating playlist...`; 40 | const { playlist } = await redux.interceptActionResp( 41 | () => 42 | redux.actions["folders/CREATE_PLAYLIST"]({ 43 | description: "Automatically generated by RealMAX", 44 | folderId: "root", 45 | fromPlaylist: undefined, 46 | title: `[RealMAX] ${sourceTitle}`, 47 | ids: trackIds.length > maxNewPlaylistSize ? undefined : trackIds, 48 | }), 49 | unloads, 50 | ["content/LOAD_PLAYLIST_SUCCESS"], 51 | ["content/LOAD_PLAYLIST_FAIL"] 52 | ); 53 | if (trackIds.length > maxNewPlaylistSize) { 54 | for (const trackIdsChunk of chunkArray(trackIds, maxNewPlaylistSize)) { 55 | await redux.interceptActionResp( 56 | () => 57 | redux.actions["content/ADD_MEDIA_ITEMS_TO_PLAYLIST"]({ 58 | addToIndex: -1, 59 | mediaItemIdsToAdd: trackIdsChunk, 60 | onDupes: "ADD", 61 | playlistUUID: playlist.uuid!, 62 | }), 63 | unloads, 64 | ["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_SUCCESS"], 65 | ["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_FAIL"] 66 | ); 67 | } 68 | } 69 | if (playlist?.uuid === undefined) { 70 | return trace.msg.err(`Failed to create playlist "${sourceTitle}"`); 71 | } 72 | trace.msg.log(`Created playlist "${sourceTitle}" with ${newItems} replacements!`); 73 | } finally { 74 | maxButton.text = defaultText; 75 | } 76 | }); 77 | 78 | await maxButton.show(contextMenu); 79 | }); 80 | -------------------------------------------------------------------------------- /plugins/SongDownloader/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Tracer, type LunaUnload } from "@luna/core"; 2 | import { ContextMenu, safeInterval, StyleTag } from "@luna/lib"; 3 | 4 | import { join } from "path"; 5 | 6 | import { getDownloadFolder, getDownloadPath, getFileName } from "./helpers"; 7 | import { settings } from "./Settings"; 8 | 9 | import styles from "file://downloadButton.css?minify"; 10 | 11 | export const { errSignal, trace } = Tracer("[SongDownloader]"); 12 | export const unloads = new Set(); 13 | 14 | new StyleTag("SongDownloader", unloads, styles); 15 | 16 | const downloadButton = ContextMenu.addButton(unloads); 17 | 18 | export { Settings } from "./Settings"; 19 | ContextMenu.onMediaItem(unloads, async ({ mediaCollection, contextMenu }) => { 20 | const trackCount = await mediaCollection.count(); 21 | if (trackCount === 0) return; 22 | 23 | const defaultText = (downloadButton.text = `Download ${trackCount} tracks`); 24 | 25 | downloadButton.onClick(async () => { 26 | if (downloadButton.elem === undefined) return; 27 | const downloadFolder = settings.defaultPath ?? (trackCount > 1 ? await getDownloadFolder() : undefined); 28 | downloadButton.elem.classList.add("download-button"); 29 | for await (let mediaItem of await mediaCollection.mediaItems()) { 30 | if (settings.useRealMAX) { 31 | downloadButton.text = `Checking RealMax...`; 32 | mediaItem = (await mediaItem.max()) ?? mediaItem; 33 | } 34 | 35 | downloadButton.text = `Loading tags...`; 36 | await mediaItem.flacTags(); 37 | 38 | downloadButton.text = `Fetching filename...`; 39 | const fileName = await getFileName(mediaItem); 40 | 41 | downloadButton.text = `Fetching download path...`; 42 | const path = downloadFolder !== undefined ? join(downloadFolder, fileName) : await getDownloadPath(fileName); 43 | if (path === undefined) return; 44 | 45 | downloadButton.text = `Downloading...`; 46 | const clearInterval = safeInterval( 47 | unloads, 48 | async () => { 49 | const progress = await mediaItem.downloadProgress(); 50 | if (progress === undefined) return; 51 | const { total, downloaded } = progress; 52 | if (total === undefined || downloaded === undefined) return; 53 | const percent = (downloaded / total) * 100; 54 | downloadButton.elem!.style.setProperty("--progress", `${percent}%`); 55 | const downloadedMB = (downloaded / 1048576).toFixed(0); 56 | const totalMB = (total / 1048576).toFixed(0); 57 | downloadButton.text = `Downloading... ${downloadedMB}/${totalMB}MB ${percent.toFixed(0)}%`; 58 | }, 59 | 50 60 | ); 61 | await mediaItem.download(path, settings.downloadQuality); 62 | clearInterval(); 63 | } 64 | downloadButton.text = defaultText; 65 | downloadButton.elem.classList.remove("download-button"); 66 | }); 67 | 68 | await downloadButton.show(contextMenu); 69 | }); 70 | -------------------------------------------------------------------------------- /plugins/DiscordRPC/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { LunaSelectItem, LunaSelectSetting, LunaSettings, LunaSwitchSetting } from "@luna/ui"; 2 | 3 | import { ReactiveStore } from "@luna/core"; 4 | 5 | import React from "react"; 6 | import { errSignal, trace } from "."; 7 | import { updateActivity } from "./updateActivity"; 8 | 9 | export const settings = await ReactiveStore.getPluginStorage("DiscordRPC", { 10 | displayOnPause: true, 11 | displayArtistIcon: true, 12 | displayPlaylistButton: true, 13 | status: 1, 14 | }); 15 | 16 | export const Settings = () => { 17 | const [displayOnPause, setDisplayOnPause] = React.useState(settings.displayOnPause); 18 | const [displayArtistIcon, setDisplayArtistIcon] = React.useState(settings.displayArtistIcon); 19 | const [displayPlaylistButton, setDisplayPlaylistButton] = React.useState(settings.displayPlaylistButton) 20 | const [status, setStatus] = React.useState(settings.status); 21 | 22 | return ( 23 | 24 | { 30 | setDisplayOnPause((settings.displayOnPause = checked)); 31 | updateActivity() 32 | .then(() => (errSignal!._ = undefined)) 33 | .catch(trace.err.withContext("Failed to set activity")); 34 | }} 35 | /> 36 | { 42 | setDisplayArtistIcon((settings.displayArtistIcon = checked)); 43 | updateActivity() 44 | .then(() => (errSignal!._ = undefined)) 45 | .catch(trace.err.withContext("Failed to set activity")); 46 | }} 47 | /> 48 | { 54 | setDisplayPlaylistButton((settings.displayPlaylistButton = checked)); 55 | updateActivity() 56 | .then(() => (errSignal!._ = undefined)) 57 | .catch(trace.err.withContext("Failed to set activity")); 58 | }} 59 | /> 60 | setStatus((settings.status = parseInt(e.target.value)))} 65 | > 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /plugins/DiscordRPC/src/updateActivity.ts: -------------------------------------------------------------------------------- 1 | import { asyncDebounce } from "@inrixia/helpers"; 2 | import { MediaItem, PlayState, redux } from "@luna/lib"; 3 | 4 | import type { SetActivity } from "@xhayper/discord-rpc"; 5 | import { setActivity } from "./discord.native"; 6 | import { settings } from "./Settings"; 7 | 8 | const STR_MAX_LEN = 127; 9 | const fmtStr = (s?: string) => { 10 | if (!s) return; 11 | if (s.length < 2) s += " "; 12 | return s.length >= STR_MAX_LEN ? s.slice(0, STR_MAX_LEN - 3) + "..." : s; 13 | }; 14 | 15 | export const updateActivity = asyncDebounce(async (mediaItem?: MediaItem) => { 16 | if (!PlayState.playing && !settings.displayOnPause) return await setActivity(); 17 | 18 | mediaItem ??= await MediaItem.fromPlaybackContext(); 19 | if (mediaItem === undefined) return; 20 | 21 | const { sourceUrl, sourceEntityType } = redux.store.getState().playQueue; 22 | 23 | const activity: SetActivity = { type: 2 }; // Listening type 24 | 25 | const trackUrl = `https://tidal.com/${mediaItem.tidalItem.contentType}/${mediaItem.id}/u` 26 | const trackSourceUrl = `https://tidal.com/browse${sourceUrl}`; 27 | 28 | activity.buttons = [ 29 | { 30 | url: trackUrl, 31 | label: "Play Song", 32 | } 33 | ]; 34 | 35 | if (sourceEntityType === "playlist" && settings.displayPlaylistButton) { 36 | activity.buttons.push({ 37 | url: trackSourceUrl, 38 | label: "Playlist", 39 | }); 40 | } 41 | 42 | const artist = await mediaItem.artist(); 43 | const artistUrl = `https://tidal.com/artist/${artist?.id}/u`; 44 | 45 | // Status text 46 | activity.statusDisplayType = settings.status; 47 | 48 | // Title 49 | activity.details = await mediaItem.title().then(fmtStr); 50 | activity.detailsUrl = trackUrl; 51 | // Artists 52 | const artistNames = await MediaItem.artistNames(await mediaItem.artists()); 53 | activity.state = fmtStr(artistNames.join(", ")) ?? "Unknown Artist"; 54 | activity.stateUrl = artistUrl; 55 | 56 | // Pause indicator 57 | if (PlayState.playing) { 58 | // Small Artist image 59 | if (settings.displayArtistIcon) { 60 | activity.smallImageKey = artist?.coverUrl("320"); 61 | activity.smallImageText = fmtStr(artist?.name); 62 | activity.smallImageUrl = artistUrl; 63 | } 64 | 65 | // Playback/Time 66 | if (mediaItem.duration !== undefined) { 67 | activity.startTimestamp = Date.now() - PlayState.playTime * 1000; 68 | activity.endTimestamp = activity.startTimestamp + mediaItem.duration * 1000; 69 | } 70 | } else { 71 | activity.smallImageKey = "paused-icon"; 72 | activity.smallImageText = "Paused"; 73 | activity.endTimestamp = Date.now(); 74 | } 75 | 76 | // Album 77 | const album = await mediaItem.album(); 78 | if (album) { 79 | activity.largeImageKey = album.coverUrl(); 80 | activity.largeImageText = await album.title().then(fmtStr); 81 | activity.largeImageUrl = `https://tidal.com/album/${album.id}/u`; 82 | } 83 | 84 | await setActivity(activity); 85 | }, true); 86 | -------------------------------------------------------------------------------- /plugins/Shazam/src/api.types.ts: -------------------------------------------------------------------------------- 1 | export interface ShazamData { 2 | matches: Match[]; 3 | timestamp?: number; 4 | track?: Track; 5 | tagid?: string; 6 | } 7 | 8 | export interface Match { 9 | id?: string; 10 | offset?: number; 11 | timeskew?: number; 12 | frequencyskew?: number; 13 | } 14 | 15 | export interface Track { 16 | layout?: string; 17 | type?: string; 18 | key?: string; 19 | title?: string; 20 | subtitle?: string; 21 | images?: TrackImages; 22 | share?: Share; 23 | hub?: Hub; 24 | sections?: Section[]; 25 | url?: string; 26 | artists?: Artist[]; 27 | isrc?: string; 28 | genres?: Genres; 29 | urlparams?: Urlparams; 30 | myshazam?: Myshazam; 31 | highlightsurls?: Highlightsurls; 32 | relatedtracksurl?: string; 33 | albumadamid?: string; 34 | } 35 | 36 | export interface Artist { 37 | id?: string; 38 | adamid?: string; 39 | } 40 | 41 | export interface Genres { 42 | primary?: string; 43 | } 44 | 45 | export interface Highlightsurls {} 46 | 47 | export interface Hub { 48 | type?: string; 49 | image?: string; 50 | actions?: Action[]; 51 | options?: Option[]; 52 | providers?: Provider[]; 53 | explicit?: boolean; 54 | displayname?: string; 55 | } 56 | 57 | export interface Action { 58 | name?: string; 59 | type?: string; 60 | id?: string; 61 | uri?: string; 62 | } 63 | 64 | export interface Option { 65 | caption?: string; 66 | actions?: Action[]; 67 | beacondata?: Beacondata; 68 | image?: string; 69 | type?: string; 70 | listcaption?: string; 71 | overflowimage?: string; 72 | colouroverflowimage?: boolean; 73 | providername?: string; 74 | } 75 | 76 | export interface Beacondata { 77 | type?: string; 78 | providername?: string; 79 | } 80 | 81 | export interface Provider { 82 | caption?: string; 83 | images?: ProviderImages; 84 | actions?: Action[]; 85 | type?: string; 86 | } 87 | 88 | export interface ProviderImages { 89 | overflow?: string; 90 | default?: string; 91 | } 92 | 93 | export interface TrackImages { 94 | background?: string; 95 | coverart?: string; 96 | coverarthq?: string; 97 | joecolor?: string; 98 | } 99 | 100 | export interface Myshazam { 101 | apple?: Apple; 102 | } 103 | 104 | export interface Apple { 105 | actions?: Action[]; 106 | } 107 | 108 | export interface Section { 109 | type?: string; 110 | metapages?: Metapage[]; 111 | tabname?: string; 112 | metadata?: Metadatum[]; 113 | url?: string; 114 | } 115 | 116 | export interface Metadatum { 117 | title?: string; 118 | text?: string; 119 | } 120 | 121 | export interface Metapage { 122 | image?: string; 123 | caption?: string; 124 | } 125 | 126 | export interface Share { 127 | subject?: string; 128 | text?: string; 129 | href?: string; 130 | image?: string; 131 | twitter?: string; 132 | html?: string; 133 | snapchat?: string; 134 | } 135 | 136 | export interface Urlparams { 137 | "{tracktitle}"?: string; 138 | "{trackartist}"?: string; 139 | } 140 | -------------------------------------------------------------------------------- /plugins/RealMax/src/index.ts: -------------------------------------------------------------------------------- 1 | import { trace, unloads } from "./index.safe"; 2 | 3 | import { MediaItem, PlayState, redux } from "@luna/lib"; 4 | 5 | import "./contextMenu"; 6 | import { settings } from "./Settings"; 7 | 8 | export { errSignal, unloads } from "./index.safe"; 9 | 10 | const getMaxItem = async (mediaItem?: MediaItem) => { 11 | const maxItem = await mediaItem?.max(); 12 | if (maxItem === undefined) return; 13 | if (settings.displayInfoPopups) trace.msg.log(`Found replacement for ${mediaItem!.tidalItem.title}`); 14 | return maxItem; 15 | }; 16 | 17 | const playMaxItem = async (elements: redux.PlayQueueElement[], index: number) => { 18 | const newElements = [...elements]; 19 | if (newElements[index]?.mediaItemId === undefined) return false; 20 | 21 | const mediaItem = await MediaItem.fromId(newElements[index].mediaItemId); 22 | const maxItem = await getMaxItem(mediaItem); 23 | if (maxItem === undefined) return false; 24 | 25 | newElements[index] = { ...newElements[index], mediaItemId: maxItem.id }; 26 | PlayState.updatePlayQueue({ 27 | elements: newElements, 28 | currentIndex: index, 29 | }); 30 | return true; 31 | }; 32 | 33 | export { Settings } from "./Settings"; 34 | 35 | // Prefetch max on preload 36 | MediaItem.onPreload(unloads, (mediaItem) => mediaItem.max().catch(trace.err.withContext("onPreload.max"))); 37 | 38 | MediaItem.onPreMediaTransition(unloads, async (mediaItem) => { 39 | PlayState.pause(); 40 | try { 41 | const maxItem = await getMaxItem(mediaItem); 42 | if (maxItem !== undefined) PlayState.playNext(maxItem.id); 43 | } catch (err) { 44 | trace.msg.err.withContext("addNext")(err); 45 | } 46 | PlayState.play(); 47 | 48 | // Preload next item 49 | const nextItem = await PlayState.nextMediaItem(); 50 | nextItem?.max().catch(trace.err.withContext("onPreMediaTransition.nextItem.max")); 51 | }); 52 | redux.intercept("playQueue/ADD_NOW", unloads, (payload) => { 53 | (async () => { 54 | const mediaItemIds = [...payload.mediaItemIds]; 55 | const currentIndex = payload.fromIndex ?? 0; 56 | try { 57 | const mediaItem = await MediaItem.fromId(mediaItemIds[currentIndex]); 58 | const maxItem = await getMaxItem(mediaItem); 59 | if (maxItem !== undefined) mediaItemIds[currentIndex] = maxItem.id; 60 | } catch (err) { 61 | trace.msg.err.withContext("playQueue/ADD_NOW")(err); 62 | } 63 | redux.actions["playQueue/ADD_NOW"]({ ...payload, mediaItemIds }); 64 | })(); 65 | return true; 66 | }); 67 | 68 | redux.intercept(["playQueue/MOVE_TO", "playQueue/MOVE_NEXT", "playQueue/MOVE_PREVIOUS"], unloads, (payload, action) => { 69 | (async () => { 70 | const { elements, currentIndex } = PlayState.playQueue; 71 | switch (action) { 72 | case "playQueue/MOVE_NEXT": 73 | if (!(await playMaxItem(elements, currentIndex + 1))) PlayState.next(); 74 | break; 75 | case "playQueue/MOVE_PREVIOUS": 76 | if (!(await playMaxItem(elements, currentIndex - 1))) PlayState.previous(); 77 | break; 78 | case "playQueue/MOVE_TO": 79 | if (!(await playMaxItem(elements, payload ?? currentIndex))) PlayState.moveTo(payload ?? currentIndex); 80 | break; 81 | } 82 | PlayState.play(); 83 | })(); 84 | return true; 85 | }); 86 | -------------------------------------------------------------------------------- /plugins/CoverTheme/src/transparent.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --cover-opacity: 0.5; 3 | --cover-gradient: linear-gradient( 4 | 90deg, 5 | rgb(var(--cover-DarkVibrant), var(--cover-opacity)), 6 | rgb(var(--cover-LightVibrant), var(--cover-opacity)) 7 | ); 8 | } 9 | 10 | body, 11 | #nowPlaying { 12 | background-image: radial-gradient(ellipse at top left, rgb(var(--cover-DarkVibrant), var(--cover-opacity)), transparent 70%), 13 | radial-gradient(ellipse at center left, rgb(var(--cover-Vibrant), var(--cover-opacity)), transparent 70%), 14 | radial-gradient(ellipse at bottom left, rgb(var(--cover-LightMuted), var(--cover-opacity)), transparent 70%), 15 | radial-gradient(ellipse at top right, rgb(var(--cover-LightVibrant), var(--cover-opacity)), transparent 70%), 16 | radial-gradient(ellipse at center right, rgb(var(--cover-Muted), var(--cover-opacity)), transparent 70%), 17 | radial-gradient(ellipse at bottom right, rgb(var(--cover-DarkMuted), var(--cover-opacity)), transparent 70%) !important; 18 | } 19 | 20 | #wimp, 21 | main, 22 | [class^="_sidebarWrapper"], 23 | [class^="_mainContainer"], 24 | [class*="smallHeader"] { 25 | background: unset !important; 26 | } 27 | 28 | #footerPlayer, 29 | #sidebar, 30 | [class^="_bar"], 31 | [class^="_sidebarItem"]:hover, 32 | .enable-scrollbar-styles ::-webkit-scrollbar-corner, 33 | .enable-scrollbar-styles ::-webkit-scrollbar-track { 34 | background-color: color-mix(in srgb, var(--wave-color-solid-base-brighter), transparent 70%) !important; 35 | } 36 | 37 | /* Fix play queue overlapping with player */ 38 | #nowPlaying > [class^="_innerContainer"] { 39 | height: calc(100vh - 126px); 40 | overflow: hidden; 41 | } 42 | 43 | /* This looks weird when the background isn't dark, better to just remove it. */ 44 | [class^="_bottomGradient"] { 45 | display: none; 46 | } 47 | 48 | /* Use cover colors in album/artist/playlist overlay */ 49 | [class*="smallHeader--"]::before { 50 | background-image: var(--cover-gradient) !important; 51 | background-color: var(--wave-color-solid-base-brighter); 52 | filter: unset; 53 | background-blend-mode: normal; 54 | } 55 | 56 | /* Cover colors in some of the icons */ 57 | [class*="emptyStateImage"] { 58 | background-color: transparent !important; 59 | } 60 | 61 | /* Cover colors in search results header (top, normal) */ 62 | [data-test="search-results-top"] > [class*="container"]::before, 63 | [data-test="search-results-normal"] > [class*="container"]::before { 64 | background-image: var(--cover-gradient); 65 | z-index: -1; 66 | left: -36px; 67 | right: -36px; 68 | height: calc(var(--topSpacing) + 50px); 69 | } 70 | 71 | /* Hides remaining black rectangle from the normal search results. There might be a better way to do this */ 72 | [data-test="search-results-normal"] > [class*="container"] > [class*="divider"] { 73 | display: none; 74 | } 75 | 76 | [data-test="search-results-top"] > [class*="container"], 77 | [data-test="search-results-top"] > [class*="container"] > [class*="divider"] { 78 | background-color: unset; 79 | } 80 | 81 | /* Tabs on user profile pages */ 82 | [class^="_tabListWrapper"] { 83 | background-image: linear-gradient(180deg, rgb(var(--cover-DarkMuted)) 70%, transparent) !important; 84 | } 85 | -------------------------------------------------------------------------------- /plugins/Shazam/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Tracer, type LunaUnload } from "@luna/core"; 2 | 3 | import { recognizeTrack } from "./shazam.native"; 4 | 5 | import { MediaItem, redux } from "@luna/lib"; 6 | import { storage } from "./Settings"; 7 | export { Settings } from "./Settings"; 8 | 9 | export const { trace, errSignal } = Tracer("[Shazam]"); 10 | export const unloads = new Set(); 11 | 12 | const addToPlaylist = async (playlistUUID: string, mediaItemIdsToAdd: string[]) => { 13 | await redux.interceptActionResp( 14 | () => redux.actions["content/ADD_MEDIA_ITEMS_TO_PLAYLIST"]({ mediaItemIdsToAdd, onDupes: "SKIP", playlistUUID }), 15 | unloads, 16 | ["etag/SET_PLAYLIST_ETAG", "content/ADD_MEDIA_ITEMS_TO_PLAYLIST_SUCCESS"], 17 | ["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_FAIL"] 18 | ); 19 | redux.actions["content/LOAD_LIST_ITEMS_PAGE"]({ listName: `playlists/${playlistUUID}`, listType: "mediaItems", reset: false }); 20 | setTimeout( 21 | () => redux.actions["content/LOAD_LIST_ITEMS_PAGE"]({ listName: `playlists/${playlistUUID}`, listType: "mediaItems", reset: true }), 22 | 1000 23 | ); 24 | }; 25 | 26 | // Define the function 27 | const handleDrop = async (event: DragEvent) => { 28 | try { 29 | event.preventDefault(); 30 | event.stopPropagation(); 31 | 32 | const { currentPath, currentParams } = redux.store.getState().router; 33 | 34 | if (!currentPath.startsWith("/playlist/")) { 35 | return trace.msg.err(`This is not a playlist!`); 36 | } 37 | const playlistUUID: string = currentParams.playlistId; 38 | for (const file of event.dataTransfer?.files ?? []) { 39 | const bytes = await file.arrayBuffer(); 40 | if (bytes === undefined) continue; 41 | trace.msg.log(`Matching ${file.name}...`); 42 | try { 43 | const matches = await recognizeTrack({ 44 | bytes, 45 | startInMiddle: storage.startInMiddle, 46 | exitOnFirstMatch: storage.exitOnFirstMatch, 47 | }); 48 | if (matches.length === 0) return trace.msg.warn(`No matches for ${file.name}`); 49 | for (const shazamData of matches) { 50 | const trackName = 51 | shazamData.track?.share?.text ?? `${shazamData.track?.title ?? "unknown"} by ${shazamData.track?.artists?.[0] ?? "unknown"}"`; 52 | const prefix = `[File: ${file.name}, Match: ${trackName}]`; 53 | const isrc = shazamData.track?.isrc; 54 | trace.log(shazamData); 55 | if (isrc === undefined) { 56 | trace.msg.log(`${prefix} No isrc returned from Shazam cannot add to playlist.`); 57 | continue; 58 | } 59 | const mediaItem = await MediaItem.fromIsrc(isrc); 60 | if (mediaItem !== undefined) { 61 | trace.msg.log(`Adding ${prefix}...`); 62 | return await addToPlaylist(playlistUUID, [mediaItem.id.toString()]); 63 | } 64 | trace.msg.err(`${prefix} Not avalible in Tidal.`); 65 | } 66 | } catch (err) { 67 | trace.msg.err.withContext(`[File: ${file.name}] Failed to recognize!`)(err); 68 | } 69 | } 70 | } catch (err) { 71 | trace.msg.err.withContext(`Unexpected error!`)(err); 72 | } 73 | }; 74 | 75 | // Register the event listener 76 | document.addEventListener("drop", handleDrop); 77 | unloads.add(() => document.removeEventListener("drop", handleDrop)); 78 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/setFormatInfo.ts: -------------------------------------------------------------------------------- 1 | import { memoizeArgless } from "@inrixia/helpers"; 2 | import { observePromise, PlayState, Quality, type MediaItem } from "@luna/lib"; 3 | import { hexToRgba } from "./lib/hexToRgba"; 4 | 5 | import { unloads } from "./index.safe"; 6 | 7 | import type { LunaUnload } from "@luna/core"; 8 | import { settings } from "./Settings"; 9 | 10 | export const formatInfoElem = document.createElement("span"); 11 | formatInfoElem.className = "format-info"; 12 | unloads.add(() => formatInfoElem.remove()); 13 | 14 | const setupInfoElem = memoizeArgless(async () => { 15 | const qualitySelector = await observePromise(unloads, `[data-test-media-state-indicator-streaming-quality]`); 16 | if (qualitySelector == null) throw new Error("Failed to find tidal media-state-indicator element!"); 17 | 18 | const qualityIndicator = qualitySelector.firstChild; 19 | if (qualityIndicator === null) throw new Error("Failed to find tidal media-state-indicator element children!"); 20 | 21 | const qualityElementContainer = qualitySelector.parentElement; 22 | if (qualityElementContainer == null) throw new Error("Failed to find tidal media-state-indicator element parent!"); 23 | 24 | // Ensure no duplicate/leftover elements before prepending 25 | qualityElementContainer.prepend(formatInfoElem); 26 | // Fix for grid spacing issues 27 | qualityElementContainer.style.setProperty("grid-auto-columns", "auto"); 28 | 29 | const progressBar = document.getElementById("progressBar"); 30 | if (progressBar === null) throw new Error("Failed to find tidal progressBar element!"); 31 | 32 | return { progressBar, qualityIndicator }; 33 | }); 34 | 35 | let formatUnload: LunaUnload | undefined; 36 | export const setFormatInfo = async (mediaItem?: MediaItem) => { 37 | if (mediaItem === undefined) return; 38 | formatInfoElem.textContent = `Loading...`; 39 | 40 | const { progressBar, qualityIndicator } = await setupInfoElem(); 41 | 42 | if (mediaItem.id != PlayState.playbackContext.actualProductId) return; 43 | const audioQuality = PlayState.playbackContext.actualAudioQuality; 44 | 45 | const qualityColor = Quality.fromAudioQuality(audioQuality); 46 | const color = (progressBar.style.color = qualityIndicator.style.color = qualityColor?.color ?? "#cfcfcf"); 47 | if (settings.displayFormatBorder) formatInfoElem.style.border = `solid 1px ${hexToRgba(color, 0.3)}`; 48 | else formatInfoElem.style.border = "none"; 49 | 50 | formatUnload?.(); 51 | formatUnload = mediaItem.withFormat(unloads, audioQuality, ({ sampleRate, bitDepth, bitrate }) => { 52 | formatInfoElem.textContent = ""; 53 | if (!!sampleRate) formatInfoElem.textContent += `${sampleRate / 1000}kHz `; 54 | if (!!bitDepth) formatInfoElem.textContent += `${bitDepth}bit `; 55 | if (!!bitrate) formatInfoElem.textContent += `${Math.floor(bitrate / 1000).toLocaleString()}kb/s`; 56 | if (formatInfoElem.textContent === "") formatInfoElem.textContent = "Unknown"; 57 | }); 58 | 59 | try { 60 | await mediaItem.updateFormat(); 61 | } catch (err) { 62 | formatInfoElem.style.border = "solid 1px red"; 63 | const errorText = (err).message.substring(0, 64); 64 | formatInfoElem.textContent = errorText; 65 | throw err; 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /plugins/DesktopConnect/src/index.ts: -------------------------------------------------------------------------------- 1 | import { LunaUnload, unloadSet } from "@luna/core"; 2 | import { ipcRenderer, MediaItem, PlayState, redux } from "@luna/lib"; 3 | import { send } from "./remoteService.native"; 4 | 5 | export * from "./remoteService.native"; 6 | 7 | export const unloads = new Set(); 8 | 9 | // #region From remote 10 | ipcRenderer.on(unloads, "remote.desktop.notify.media.changed", async ({ mediaId }) => { 11 | const mediaItem = await MediaItem.fromId(mediaId); 12 | mediaItem?.play(); 13 | send({ command: "onRequestNextMedia", type: "media" }); 14 | }); 15 | ipcRenderer.on(unloads, "remote.desktop.prefetch", ({ mediaId, mediaType }) => { 16 | redux.actions["player/PRELOAD_ITEM"]({ productId: mediaId, productType: mediaType === 0 ? "track" : "video" }); 17 | }); 18 | ipcRenderer.on(unloads, "remote.desktop.seek", (time: number) => PlayState.seek(time / 1000)); 19 | ipcRenderer.on(unloads, "remote.desktop.play", PlayState.play.bind(PlayState)); 20 | ipcRenderer.on(unloads, "remote.desktop.pause", PlayState.pause.bind(PlayState)); 21 | ipcRenderer.on(unloads, "remote.desktop.set.shuffle", PlayState.setShuffle.bind(PlayState)); 22 | ipcRenderer.on(unloads, "remote.desktop.set.repeat.mode", PlayState.setRepeatMode.bind(PlayState)); 23 | ipcRenderer.on(unloads, "remote.destop.set.volume.mute", ({ level, mute }: { level: number; mute: boolean }) => { 24 | redux.actions["playbackControls/SET_MUTE"](mute); 25 | redux.actions["playbackControls/SET_VOLUME"]({ 26 | volume: Math.min(level * 100, 100), 27 | }); 28 | }); 29 | // #endregion 30 | 31 | // #region To remote 32 | // Using send from remoteService.native as its more consistent 33 | const sessionUnloads = new Set(); 34 | ipcRenderer.on(unloads, "remote.desktop.notify.session.state", (state) => { 35 | if (state === 0) unloadSet(sessionUnloads); 36 | if (sessionUnloads.size !== 0) return; 37 | ipcRenderer.on(sessionUnloads, "client.playback.playersignal", ({ time }: { time: number }) => { 38 | send({ 39 | command: "onProgressUpdated", 40 | duration: 0, 41 | progress: time * 1000, 42 | type: "media", 43 | }); 44 | }); 45 | redux.intercept("playbackControls/SET_PLAYBACK_STATE", sessionUnloads, (state) => { 46 | switch (state) { 47 | case "IDLE": 48 | return send({ command: "onStatusUpdated", playerState: "idle", type: "media" }); 49 | case "NOT_PLAYING": 50 | return send({ command: "onStatusUpdated", playerState: "paused", type: "media" }); 51 | case "PLAYING": 52 | return send({ command: "onStatusUpdated", playerState: "playing", type: "media" }); 53 | case "STALLED": 54 | return send({ command: "onStatusUpdated", playerState: "buffering", type: "media" }); 55 | } 56 | }); 57 | redux.intercept("playbackControls/ENDED", sessionUnloads, ({ reason }) => { 58 | if (reason === "completed") send({ command: "onPlaybackCompleted", hasNextMedia: false, type: "media" }); 59 | return true; 60 | }); 61 | redux.intercept("playbackControls/SKIP_NEXT", sessionUnloads, () => { 62 | PlayState.pause(); 63 | send({ command: "onStatusUpdated", playerState: "idle", type: "media" }); 64 | send({ command: "onPlaybackCompleted", hasNextMedia: false, type: "media" }); 65 | return true; 66 | }); 67 | }); 68 | unloads.add(() => unloadSet(sessionUnloads)); 69 | // #endregion 70 | -------------------------------------------------------------------------------- /plugins/SongDownloader/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactiveStore } from "@luna/core"; 2 | import { MediaItem, Quality, type redux } from "@luna/lib"; 3 | import { LunaButtonSetting, LunaSelectItem, LunaSelectSetting, LunaSettings, LunaSwitchSetting, LunaTextSetting } from "@luna/ui"; 4 | 5 | import React from "react"; 6 | import { getDownloadFolder } from "./helpers"; 7 | 8 | const defaultFilenameFormat = "{artist} - {album} - {title}"; 9 | 10 | type Settings = { 11 | downloadQuality: redux.AudioQuality; 12 | defaultPath?: string; 13 | pathFormat: string; 14 | useRealMAX: boolean; 15 | }; 16 | export const settings = await ReactiveStore.getPluginStorage("SongDownloader", { 17 | downloadQuality: Quality.Max.audioQuality, 18 | pathFormat: defaultFilenameFormat, 19 | useRealMAX: true, 20 | }); 21 | 22 | // Sanitize download quality 23 | if (Quality.fromAudioQuality(settings.downloadQuality) === undefined) settings.downloadQuality = Quality.Max.audioQuality; 24 | 25 | export const Settings = () => { 26 | const [downloadQuality, setDownloadQuality] = React.useState(settings.downloadQuality); 27 | const [defaultPath, setDefaultPath] = React.useState(settings.defaultPath); 28 | const [pathFormat, setPathFormat] = React.useState(settings.pathFormat); 29 | const [useRealMAX, setUseRealMAX] = React.useState(settings.useRealMAX); 30 | 31 | return ( 32 | 33 | setDownloadQuality((settings.downloadQuality = e.target.value))} 37 | > 38 | {Object.values(Quality.lookups.audioQuality).map((quality) => { 39 | if (typeof quality !== "string") return ; 40 | })} 41 | 42 | setUseRealMAX((settings.useRealMAX = checked))} 46 | /> 47 | 51 | Set a default folder to save files to (will disable prompting for path on download) 52 | {defaultPath && ( 53 | <> 54 |
55 | Using {defaultPath} 56 | 57 | )} 58 | 59 | } 60 | children={defaultPath === undefined ? "Set default folder" : "Clear default folder"} 61 | onClick={async () => { 62 | if (defaultPath !== undefined) return setDefaultPath((settings.defaultPath = undefined)); 63 | setDefaultPath((settings.defaultPath = await getDownloadFolder())); 64 | }} 65 | /> 66 | 70 | Define subfolders using /. 71 |
72 | For example: {"{artist}/{album}/{title}"} 73 |
74 | Saves in subfolder artist/album/ named title.flac. 75 |
76 | You can use the following tags: 77 |
    78 | {MediaItem.availableTags.map((tag) => ( 79 |
  • {tag}
  • 80 | ))} 81 |
82 | 83 | } 84 | value={pathFormat} 85 | onChange={(e) => setPathFormat((settings.pathFormat = e.target.value))} 86 | /> 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /plugins/LastFM/src/LastFM.ts: -------------------------------------------------------------------------------- 1 | import { NowPlaying } from "./types/NowPlaying"; 2 | import { Scrobble } from "./types/Scrobble"; 3 | 4 | import type { AnyRecord } from "@inrixia/helpers"; 5 | import { findModuleProperty, ftch } from "@luna/core"; 6 | import { storage } from "./Settings"; 7 | import { hash } from "./hash.native"; 8 | 9 | export type NowPlayingOpts = { 10 | track: string; 11 | artist: string; 12 | album?: string; 13 | trackNumber?: string; 14 | context?: string; 15 | mbid?: string; 16 | duration?: string; 17 | albumArtist?: string; 18 | }; 19 | 20 | export interface ScrobbleOpts extends NowPlayingOpts { 21 | timestamp: string; 22 | streamId?: string; 23 | chosenByUser?: string; 24 | } 25 | 26 | type ResponseType = 27 | | (T & { message?: undefined }) 28 | | { 29 | message: string; 30 | }; 31 | 32 | export type LastFmSession = { 33 | name: string; 34 | key: string; 35 | subscriber: number; 36 | }; 37 | 38 | export class LastFM { 39 | public static readonly apiKey?: string = findModuleProperty((key, value) => key === "lastFmApiKey" && typeof value === "string")!.value; 40 | public static readonly secret?: string = findModuleProperty((key, value) => key === "lastFmSecret" && typeof value === "string")!.value; 41 | static { 42 | if (this.secret === undefined) throw new Error("Last.fm secret not found"); 43 | if (this.apiKey === undefined) throw new Error("Last.fm API key not found"); 44 | } 45 | 46 | private static async apiSignature(params: AnyRecord) { 47 | const sig = 48 | Object.keys(params) 49 | .filter((key) => key !== "format" && key !== undefined) 50 | .sort() 51 | .map((key) => `${key}${params[key]}`) 52 | .join("") + this.secret; 53 | return hash(sig); 54 | } 55 | 56 | private static async sendRequest(method: string, params?: AnyRecord) { 57 | params ??= {}; 58 | if (!method.startsWith("auth")) params.sk = this.session.key; 59 | params.method = method; 60 | params.api_key = this.apiKey!; 61 | params.format = "json"; 62 | params.api_sig = await LastFM.apiSignature(params); 63 | 64 | const data = await ftch.json>(`https://ws.audioscrobbler.com/2.0/`, { 65 | headers: { 66 | "Content-type": "application/x-www-form-urlencoded", 67 | "Accept-Charset": "utf-8", 68 | }, 69 | method: "POST", 70 | body: new URLSearchParams(params).toString(), 71 | }); 72 | 73 | if (data.message) throw new Error(data.message); 74 | 75 | return data; 76 | } 77 | 78 | public static async authenticate() { 79 | const { token } = await LastFM.sendRequest<{ token: string }>("auth.getToken"); 80 | window.open(`https://www.last.fm/api/auth/?api_key=${this.apiKey}&token=${token}`, "_blank"); 81 | for (let i = 0; i < 10; i++) { 82 | const session = await this.sendRequest<{ session: LastFmSession }>("auth.getSession", { token }).catch(() => undefined); 83 | if (session !== undefined) return session; 84 | await new Promise((res) => setTimeout(res, 1000)); 85 | } 86 | throw new Error("Timed out waiting for user to confirm session in browser"); 87 | } 88 | 89 | public static get session() { 90 | if (storage.session === undefined) throw new Error("Session not set, please update via settings!"); 91 | return storage.session; 92 | } 93 | 94 | public static async updateNowPlaying(opts: NowPlayingOpts) { 95 | return LastFM.sendRequest("track.updateNowPlaying", opts); 96 | } 97 | 98 | public static async scrobble(opts: ScrobbleOpts) { 99 | return LastFM.sendRequest("track.scrobble", opts); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /plugins/CoverTheme/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tracer, type LunaUnload } from "@luna/core"; 2 | import { MediaItem, PlayState, redux, StyleTag } from "@luna/lib"; 3 | 4 | const { trace, errSignal } = Tracer("[CoverTheme]"); 5 | export { errSignal, trace }; 6 | 7 | import transparent from "file://transparent.css?minify"; 8 | 9 | import { settings, storage } from "./Settings"; 10 | import { getPalette, type Palette, type RGBSwatch } from "./vibrant.native"; 11 | 12 | const cachePalette = async (mediaItem?: MediaItem): Promise => { 13 | if (mediaItem === undefined) return; 14 | const coverUrl = await mediaItem.coverUrl("640"); 15 | if (coverUrl === undefined) return; 16 | return await storage.ensure(`palette_v2.${coverUrl}`, () => getPalette(coverUrl)); 17 | }; 18 | 19 | const docStyle = document.documentElement.style; 20 | const lerp = (a: number, b: number, t: number) => Math.round(a + (b - a) * t); 21 | const animateCssVar = (varName: string, from: RGBSwatch | undefined, to: RGBSwatch, duration = 250) => { 22 | if (from === undefined || from.every((v, i) => v === to[i])) return docStyle.setProperty(varName, to.join(",")); 23 | const start = performance.now(); 24 | const frame = (now: number) => { 25 | const t = Math.min(1, (now - start) / duration); 26 | const current = from.map((v, i) => lerp(v, to[i], t)); 27 | docStyle.setProperty(varName, current.join(",")); 28 | if (t < 1) requestAnimationFrame(frame); 29 | }; 30 | requestAnimationFrame(frame); 31 | }; 32 | 33 | const vars = new Set(); 34 | let currentItemId: redux.ItemId; 35 | let currentPalette: Palette; 36 | const updateBackground = async (mediaItem?: MediaItem) => { 37 | if (mediaItem === undefined || mediaItem.id === currentItemId) return; 38 | currentItemId = mediaItem.id; 39 | const palette = await cachePalette(mediaItem).catch(trace.msg.err.withContext("Failed to update background")); 40 | if (palette === undefined || currentPalette === palette) return; 41 | 42 | for (const colorName in palette) { 43 | const nextColor = palette[colorName]; 44 | const variableName = `--cover-${colorName}`; 45 | vars.add(variableName); 46 | animateCssVar(variableName, currentPalette?.[colorName], nextColor, 250); 47 | } 48 | currentPalette = palette; 49 | return true; 50 | }; 51 | 52 | export { Settings } from "./Settings"; 53 | 54 | export const unloads = new Set(); 55 | export const style = new StyleTag("CoverTheme", unloads, settings.applyTheme ? transparent : ""); 56 | setTimeout(async () => { 57 | const mediaItem = await MediaItem.fromPlaybackContext() 58 | .then(updateBackground) 59 | .catch(trace.msg.err.withContext("MediaItem.fromPlaybackContext.updateBackground")); 60 | if (mediaItem) return; 61 | 62 | // Fallback for if no media is playing 63 | const mediaItems = redux.store.getState().content.mediaItems; 64 | for (const itemId in mediaItems) { 65 | await MediaItem.fromId(itemId).then(updateBackground).catch(trace.msg.err.withContext("MediaItem.fromId.updateBackground")); 66 | return; 67 | } 68 | }); 69 | 70 | MediaItem.onMediaTransition(unloads, async (mediaItem) => { 71 | await updateBackground(mediaItem); 72 | // Preload next palette 73 | await cachePalette(await PlayState.nextMediaItem()); 74 | }); 75 | MediaItem.onPreload(unloads, cachePalette); 76 | MediaItem.onPreMediaTransition(unloads, updateBackground); 77 | redux.intercept("playQueue/MOVE_TO", unloads, (payload) => { 78 | const { mediaItemId } = PlayState.playQueue.elements[payload]; 79 | MediaItem.fromId(mediaItemId).then(updateBackground).catch(trace.msg.err.withContext("playQueue/MOVE_TO")); 80 | }); 81 | redux.intercept("playQueue/MOVE_NEXT", unloads, () => { 82 | PlayState.nextMediaItem().then(updateBackground).catch(trace.msg.err.withContext("playQueue/MOVE_NEXT")); 83 | }); 84 | redux.intercept("playQueue/MOVE_PREVIOUS", unloads, () => { 85 | PlayState.previousMediaItem().then(updateBackground).catch(trace.msg.err.withContext("playQueue/MOVE_PREVIOUS")); 86 | }); 87 | 88 | unloads.add(() => vars.forEach((variable) => document.documentElement.style.removeProperty(variable))); 89 | -------------------------------------------------------------------------------- /themes/blur.css: -------------------------------------------------------------------------------- 1 | /* 2 | { 3 | "name": "Blur", 4 | "author": "Nick Oates", 5 | "description": "Adds backdrop blur behind the player, title bar, and context menus." 6 | } 7 | */ 8 | :root { 9 | --blur-background: color-mix(in srgb, var(--wave-color-solid-base-brighter), transparent 60%); 10 | --blur-radius: 16px; 11 | --player-height: 96px; /* Adjust this to match your player height */ 12 | } 13 | 14 | /* Main layout adjustments */ 15 | [class^="_containerRow_"] { 16 | max-height: none !important; 17 | } 18 | 19 | [class^="_mainContainer_"] { 20 | height: 100vh !important; 21 | background-color: inherit; 22 | } 23 | 24 | /* Top bar */ 25 | [class^="_bar_"] { 26 | position: absolute; 27 | z-index: 100; 28 | backdrop-filter: blur(var(--blur-radius)); 29 | } 30 | 31 | /* Padding adjustments */ 32 | [class^="_sidebarWrapper"], 33 | [class^="_contentArea"], 34 | #main { 35 | padding-top: 30px; 36 | } 37 | 38 | [class^="_contentArea"], 39 | #main { 40 | padding-bottom: 0 !important; 41 | margin-bottom: calc(var(--player-height) + 24px) !important; 42 | } 43 | 44 | /* Add spacing after content */ 45 | [class^="_contentArea"]::after, 46 | #main::after { 47 | content: ""; 48 | display: block; 49 | height: calc(var(--player-height) + 24px); 50 | width: 100%; 51 | } 52 | 53 | /* Remove double spacing */ 54 | [class^="_contentArea"] > *:last-child, 55 | #main > *:last-child { 56 | margin-bottom: 0 !important; 57 | padding-bottom: 0 !important; 58 | } 59 | 60 | table:last-child, 61 | [role="table"]:last-child { 62 | margin-bottom: 0 !important; 63 | } 64 | 65 | /* Context menus */ 66 | [class^="_contextMenu"]::before, 67 | [class^="_subMenu_"]::before { 68 | content: ""; 69 | position: absolute; 70 | width: 100%; 71 | height: 100%; 72 | top: 0; 73 | left: 0; 74 | backdrop-filter: blur(var(--blur-radius)); 75 | border-radius: 9px; 76 | pointer-events: none; 77 | z-index: -5; 78 | background-color: var(--blur-background); 79 | } 80 | 81 | [class^="_contextMenu_"], 82 | [class^="_subMenu_"] { 83 | position: relative; 84 | background-color: transparent; 85 | } 86 | 87 | /* Main blur backgrounds */ 88 | #footerPlayer { 89 | backdrop-filter: blur(var(--blur-radius)); 90 | background-color: var(--blur-background) !important; 91 | } 92 | 93 | [class^="_sidebarWrapper_"] { 94 | padding-bottom: 96px; 95 | } 96 | 97 | #feedSidebar, 98 | #playQueueSidebar, 99 | [class*="_playQueueWithoutHeader_"] button { 100 | background-color: var(--blur-background); 101 | backdrop-filter: blur(var(--blur-radius)); 102 | } 103 | 104 | /* Hover states */ 105 | [class*="_audioQualityContainerHover_"]:hover, 106 | [class*="_selectItem_"]:hover, 107 | [class*="_createNewPlaylist_"]:hover { 108 | background-color: var(--blur-background) !important; 109 | } 110 | 111 | /* Shortcut playlist cards */ 112 | @container (width > 200px) { 113 | [class*="_shortcutItem_"]::after { 114 | backdrop-filter: blur(8px); 115 | } 116 | } 117 | 118 | [class*="_shortcutItem_"] { 119 | background-color: var(--blur-background) !important; 120 | backdrop-filter: blur(var(--blur-radius)); 121 | } 122 | 123 | /* Selected/active sidebar items */ 124 | [class^="_sidebarItem_"][class*="_active_"], 125 | [class*="_item_"][class*="_selected_"] { 126 | background-color: var(--blur-background) !important; 127 | backdrop-filter: blur(var(--blur-radius)); 128 | } 129 | 130 | /* Header backgrounds - remove images and add blur */ 131 | [class^="_dataContainer_"]:before, 132 | [class*="_dataContainer_"]:before { 133 | --img: none; 134 | background-image: none !important; 135 | backdrop-filter: blur(var(--blur-radius)); 136 | background-color: var(--blur-background) !important; 137 | } 138 | 139 | [class^="_smallHeader_"]:before, 140 | [class*="_smallHeader_"]:before { 141 | --img: none; 142 | background-image: none !important; 143 | background-color: var(--blur-background) !important; 144 | backdrop-filter: blur(var(--blur-radius)); 145 | filter: none !important; 146 | } 147 | 148 | [class*="_backgroundGradient_"], 149 | [class*="_headerBackground_"], 150 | [class*="_gradient_"]:not([class*="_progressGradient_"]) { 151 | backdrop-filter: blur(var(--blur-radius)); 152 | background-color: var(--blur-background) !important; 153 | } 154 | 155 | [class*="_header_"]:not([class*="_tableHeader_"])::before, 156 | [class*="_header_"]:not([class*="_tableHeader_"])::after { 157 | background-image: none !important; 158 | backdrop-filter: blur(var(--blur-radius)); 159 | } -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@inrixia/helpers': 12 | specifier: ^3.20.2 13 | version: 3.20.2 14 | '@types/node': 15 | specifier: ^24.0.0 16 | version: 24.0.0 17 | '@types/react': 18 | specifier: ^19.1.7 19 | version: 19.1.7 20 | '@types/react-dom': 21 | specifier: ^19.1.6 22 | version: 19.1.6(@types/react@19.1.7) 23 | concurrently: 24 | specifier: ^9.1.2 25 | version: 9.1.2 26 | electron: 27 | specifier: ^36.4.0 28 | version: 36.4.0 29 | http-server: 30 | specifier: ^14.1.1 31 | version: 14.1.1 32 | luna: 33 | specifier: github:inrixia/TidaLuna#10ae3d0 34 | version: https://codeload.github.com/inrixia/TidaLuna/tar.gz/10ae3d0 35 | oby: 36 | specifier: ^15.1.2 37 | version: 15.1.2 38 | rimraf: 39 | specifier: ^6.0.1 40 | version: 6.0.1 41 | tsx: 42 | specifier: ^4.20.0 43 | version: 4.20.0 44 | typescript: 45 | specifier: ^5.8.3 46 | version: 5.8.3 47 | 48 | plugins/Avatar: {} 49 | 50 | plugins/CoverTheme: 51 | dependencies: 52 | node-vibrant: 53 | specifier: 4.0.3 54 | version: 4.0.3 55 | 56 | plugins/DesktopConnect: 57 | dependencies: 58 | '@homebridge/ciao': 59 | specifier: ^1.3.1 60 | version: 1.3.2 61 | 62 | plugins/DiscordRPC: 63 | dependencies: 64 | '@xhayper/discord-rpc': 65 | specifier: ^1.2.1 66 | version: 1.2.2 67 | 68 | plugins/LastFM: {} 69 | 70 | plugins/ListenBrainz: {} 71 | 72 | plugins/NativeFullscreen: {} 73 | 74 | plugins/NoBuffer: {} 75 | 76 | plugins/PersistSettings: {} 77 | 78 | plugins/RealMax: {} 79 | 80 | plugins/Shazam: 81 | dependencies: 82 | shazamio-core: 83 | specifier: ^1.3.1 84 | version: 1.3.1 85 | uuid: 86 | specifier: ^11.1.0 87 | version: 11.1.0 88 | devDependencies: 89 | '@types/uuid': 90 | specifier: ^10.0.0 91 | version: 10.0.0 92 | 93 | plugins/SmallWindow: {} 94 | 95 | plugins/SongDownloader: 96 | dependencies: 97 | sanitize-filename: 98 | specifier: ^1.6.3 99 | version: 1.6.3 100 | 101 | plugins/Themer: {} 102 | 103 | plugins/TidalTags: {} 104 | 105 | plugins/VolumeScroll: {} 106 | 107 | packages: 108 | 109 | '@discordjs/collection@2.1.1': 110 | resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} 111 | engines: {node: '>=18'} 112 | 113 | '@discordjs/rest@2.5.1': 114 | resolution: {integrity: sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw==} 115 | engines: {node: '>=18'} 116 | 117 | '@discordjs/util@1.1.1': 118 | resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} 119 | engines: {node: '>=18'} 120 | 121 | '@electron/get@2.0.3': 122 | resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} 123 | engines: {node: '>=12'} 124 | 125 | '@esbuild/aix-ppc64@0.25.5': 126 | resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} 127 | engines: {node: '>=18'} 128 | cpu: [ppc64] 129 | os: [aix] 130 | 131 | '@esbuild/android-arm64@0.25.5': 132 | resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} 133 | engines: {node: '>=18'} 134 | cpu: [arm64] 135 | os: [android] 136 | 137 | '@esbuild/android-arm@0.25.5': 138 | resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} 139 | engines: {node: '>=18'} 140 | cpu: [arm] 141 | os: [android] 142 | 143 | '@esbuild/android-x64@0.25.5': 144 | resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} 145 | engines: {node: '>=18'} 146 | cpu: [x64] 147 | os: [android] 148 | 149 | '@esbuild/darwin-arm64@0.25.5': 150 | resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} 151 | engines: {node: '>=18'} 152 | cpu: [arm64] 153 | os: [darwin] 154 | 155 | '@esbuild/darwin-x64@0.25.5': 156 | resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} 157 | engines: {node: '>=18'} 158 | cpu: [x64] 159 | os: [darwin] 160 | 161 | '@esbuild/freebsd-arm64@0.25.5': 162 | resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} 163 | engines: {node: '>=18'} 164 | cpu: [arm64] 165 | os: [freebsd] 166 | 167 | '@esbuild/freebsd-x64@0.25.5': 168 | resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} 169 | engines: {node: '>=18'} 170 | cpu: [x64] 171 | os: [freebsd] 172 | 173 | '@esbuild/linux-arm64@0.25.5': 174 | resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} 175 | engines: {node: '>=18'} 176 | cpu: [arm64] 177 | os: [linux] 178 | 179 | '@esbuild/linux-arm@0.25.5': 180 | resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} 181 | engines: {node: '>=18'} 182 | cpu: [arm] 183 | os: [linux] 184 | 185 | '@esbuild/linux-ia32@0.25.5': 186 | resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} 187 | engines: {node: '>=18'} 188 | cpu: [ia32] 189 | os: [linux] 190 | 191 | '@esbuild/linux-loong64@0.25.5': 192 | resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} 193 | engines: {node: '>=18'} 194 | cpu: [loong64] 195 | os: [linux] 196 | 197 | '@esbuild/linux-mips64el@0.25.5': 198 | resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} 199 | engines: {node: '>=18'} 200 | cpu: [mips64el] 201 | os: [linux] 202 | 203 | '@esbuild/linux-ppc64@0.25.5': 204 | resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} 205 | engines: {node: '>=18'} 206 | cpu: [ppc64] 207 | os: [linux] 208 | 209 | '@esbuild/linux-riscv64@0.25.5': 210 | resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} 211 | engines: {node: '>=18'} 212 | cpu: [riscv64] 213 | os: [linux] 214 | 215 | '@esbuild/linux-s390x@0.25.5': 216 | resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} 217 | engines: {node: '>=18'} 218 | cpu: [s390x] 219 | os: [linux] 220 | 221 | '@esbuild/linux-x64@0.25.5': 222 | resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} 223 | engines: {node: '>=18'} 224 | cpu: [x64] 225 | os: [linux] 226 | 227 | '@esbuild/netbsd-arm64@0.25.5': 228 | resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} 229 | engines: {node: '>=18'} 230 | cpu: [arm64] 231 | os: [netbsd] 232 | 233 | '@esbuild/netbsd-x64@0.25.5': 234 | resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} 235 | engines: {node: '>=18'} 236 | cpu: [x64] 237 | os: [netbsd] 238 | 239 | '@esbuild/openbsd-arm64@0.25.5': 240 | resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} 241 | engines: {node: '>=18'} 242 | cpu: [arm64] 243 | os: [openbsd] 244 | 245 | '@esbuild/openbsd-x64@0.25.5': 246 | resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} 247 | engines: {node: '>=18'} 248 | cpu: [x64] 249 | os: [openbsd] 250 | 251 | '@esbuild/sunos-x64@0.25.5': 252 | resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} 253 | engines: {node: '>=18'} 254 | cpu: [x64] 255 | os: [sunos] 256 | 257 | '@esbuild/win32-arm64@0.25.5': 258 | resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} 259 | engines: {node: '>=18'} 260 | cpu: [arm64] 261 | os: [win32] 262 | 263 | '@esbuild/win32-ia32@0.25.5': 264 | resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} 265 | engines: {node: '>=18'} 266 | cpu: [ia32] 267 | os: [win32] 268 | 269 | '@esbuild/win32-x64@0.25.5': 270 | resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} 271 | engines: {node: '>=18'} 272 | cpu: [x64] 273 | os: [win32] 274 | 275 | '@homebridge/ciao@1.3.2': 276 | resolution: {integrity: sha512-RBQaEM5/9UPznhWL/QgJYgbVU84v3eOSdMbr827PQtvnpQBp5iJW7vBSH4YzWh/GKB0yS0mPf0wgOiCcLiOI1A==} 277 | engines: {node: ^18 || ^20 || ^22} 278 | hasBin: true 279 | 280 | '@inrixia/helpers@3.20.2': 281 | resolution: {integrity: sha512-iRD7YI9aF2SIhboH/8aBbTI33Iur8eo540ZyP+Fq4VGtPIKa7s6PBq8kYAugHud/zCWIZsHj9TP3GUekNpNKHA==} 282 | 283 | '@isaacs/cliui@8.0.2': 284 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 285 | engines: {node: '>=12'} 286 | 287 | '@jimp/bmp@0.22.12': 288 | resolution: {integrity: sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==} 289 | peerDependencies: 290 | '@jimp/custom': '>=0.3.5' 291 | 292 | '@jimp/core@0.22.12': 293 | resolution: {integrity: sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==} 294 | 295 | '@jimp/custom@0.22.12': 296 | resolution: {integrity: sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==} 297 | 298 | '@jimp/gif@0.22.12': 299 | resolution: {integrity: sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==} 300 | peerDependencies: 301 | '@jimp/custom': '>=0.3.5' 302 | 303 | '@jimp/jpeg@0.22.12': 304 | resolution: {integrity: sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==} 305 | peerDependencies: 306 | '@jimp/custom': '>=0.3.5' 307 | 308 | '@jimp/plugin-resize@0.22.12': 309 | resolution: {integrity: sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==} 310 | peerDependencies: 311 | '@jimp/custom': '>=0.3.5' 312 | 313 | '@jimp/png@0.22.12': 314 | resolution: {integrity: sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==} 315 | peerDependencies: 316 | '@jimp/custom': '>=0.3.5' 317 | 318 | '@jimp/tiff@0.22.12': 319 | resolution: {integrity: sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==} 320 | peerDependencies: 321 | '@jimp/custom': '>=0.3.5' 322 | 323 | '@jimp/types@0.22.12': 324 | resolution: {integrity: sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==} 325 | peerDependencies: 326 | '@jimp/custom': '>=0.3.5' 327 | 328 | '@jimp/utils@0.22.12': 329 | resolution: {integrity: sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==} 330 | 331 | '@jridgewell/gen-mapping@0.3.8': 332 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 333 | engines: {node: '>=6.0.0'} 334 | 335 | '@jridgewell/resolve-uri@3.1.2': 336 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 337 | engines: {node: '>=6.0.0'} 338 | 339 | '@jridgewell/set-array@1.2.1': 340 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 341 | engines: {node: '>=6.0.0'} 342 | 343 | '@jridgewell/source-map@0.3.6': 344 | resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} 345 | 346 | '@jridgewell/sourcemap-codec@1.5.0': 347 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 348 | 349 | '@jridgewell/trace-mapping@0.3.25': 350 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 351 | 352 | '@sapphire/async-queue@1.5.5': 353 | resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} 354 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 355 | 356 | '@sapphire/snowflake@3.5.5': 357 | resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==} 358 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 359 | 360 | '@sindresorhus/is@4.6.0': 361 | resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} 362 | engines: {node: '>=10'} 363 | 364 | '@szmarczak/http-timer@4.0.6': 365 | resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} 366 | engines: {node: '>=10'} 367 | 368 | '@tokenizer/token@0.3.0': 369 | resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} 370 | 371 | '@types/cacheable-request@6.0.3': 372 | resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} 373 | 374 | '@types/clean-css@4.2.11': 375 | resolution: {integrity: sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==} 376 | 377 | '@types/html-minifier-terser@7.0.2': 378 | resolution: {integrity: sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==} 379 | 380 | '@types/http-cache-semantics@4.0.4': 381 | resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} 382 | 383 | '@types/keyv@3.1.4': 384 | resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} 385 | 386 | '@types/node@16.9.1': 387 | resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} 388 | 389 | '@types/node@18.19.111': 390 | resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==} 391 | 392 | '@types/node@22.15.31': 393 | resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==} 394 | 395 | '@types/node@24.0.0': 396 | resolution: {integrity: sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==} 397 | 398 | '@types/react-dom@19.1.6': 399 | resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} 400 | peerDependencies: 401 | '@types/react': ^19.0.0 402 | 403 | '@types/react@19.1.7': 404 | resolution: {integrity: sha512-BnsPLV43ddr05N71gaGzyZ5hzkCmGwhMvYc8zmvI8Ci1bRkkDSzDDVfAXfN2tk748OwI7ediiPX6PfT9p0QGVg==} 405 | 406 | '@types/responselike@1.0.3': 407 | resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} 408 | 409 | '@types/uuid@10.0.0': 410 | resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} 411 | 412 | '@types/yauzl@2.10.3': 413 | resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} 414 | 415 | '@vibrant/color@4.0.0': 416 | resolution: {integrity: sha512-S9ItdqS1135wTXoIIqAJu8df9dqlOo6Boc5Y4MGsBTu9UmUOvOwfj5b4Ga6S5yrLAKmKYIactkz7zYJdMddkig==} 417 | 418 | '@vibrant/core@4.0.0': 419 | resolution: {integrity: sha512-fqlVRUTDjEws9VNKvI3cDXM4wUT7fMFS+cVqEjJk3im+R5EvjJzPF6OAbNhfPzW04NvHNE555eY9FfhYuX3PRw==} 420 | 421 | '@vibrant/generator-default@4.0.3': 422 | resolution: {integrity: sha512-HZlfp19sDokODEkZF4p70QceARHgjP3a1Dmxg+dlblYMJM98jPq+azA0fzqKNR7R17JJNHxexpJEepEsNlG0gw==} 423 | 424 | '@vibrant/generator@4.0.0': 425 | resolution: {integrity: sha512-CqKAjmgHVDXJVo3Q5+9pUJOvksR7cN3bzx/6MbURYh7lA4rhsIewkUK155M6q0vfcUN3ETi/eTneCi0tLuM2Sg==} 426 | 427 | '@vibrant/image-browser@4.0.0': 428 | resolution: {integrity: sha512-mXckzvJWiP575Y/wNtP87W/TPgyJoGlPBjW4E9YmNS6n4Jb6RqyHQA0ZVulqDslOxjSsihDzY7gpAORRclaoLg==} 429 | 430 | '@vibrant/image-node@4.0.0': 431 | resolution: {integrity: sha512-m7yfnQtmo2y8z+tOjRFBx6q/qGnhl/ax2uCaj4TBkm4TtXfR4Dsn90wT6OWXmCFFzxIKHXKKEBShkxR+4RHseA==} 432 | 433 | '@vibrant/image@4.0.0': 434 | resolution: {integrity: sha512-Asv/7R/L701norosgvbjOVkodFiwcFihkXixA/gbAd6C+5GCts1Wm1NPk14FNKnM7eKkfAN+0wwPkdOH+PY/YA==} 435 | 436 | '@vibrant/quantizer-mmcq@4.0.0': 437 | resolution: {integrity: sha512-TZqNiRoGGyCP8fH1XE6rvhFwLNv9D8MP1Xhz3K8tsuUweC6buWax3qLfrfEnkhtQnPJHaqvTfTOlIIXVMfRpow==} 438 | 439 | '@vibrant/quantizer@4.0.0': 440 | resolution: {integrity: sha512-YDGxmCv/RvHFtZghDlVRwH5GMxdGGozWS1JpUOUt73/F5zAKGiiier8F31K1npIXARn6/Gspvg/Rbg7qqyEr2A==} 441 | 442 | '@vibrant/types@4.0.0': 443 | resolution: {integrity: sha512-tA5TAbuROXcPkt+PWjmGfoaiEXyySVaNnCZovf6vXhCbMdrTTCQXvNCde2geiVl6YwtuU/Qrj9iZxS5jZ6yVIw==} 444 | 445 | '@vibrant/worker@4.0.0': 446 | resolution: {integrity: sha512-nSaZZwWQKOgN/nPYUAIRF0/uoa7KpK91A+gjLmZZDgfN1enqxaiihmn+75ayNadW0c6cxAEpEFEHTONR5u9tMw==} 447 | 448 | '@vladfrangu/async_event_emitter@2.4.6': 449 | resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} 450 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 451 | 452 | '@xhayper/discord-rpc@1.2.2': 453 | resolution: {integrity: sha512-P3+uF2Hb7zVNEtk2ZleV7FF2cj1lCZ0Caa82RECwa5oaPNCF12CDhsx8GdBqFaD0zRRVrkheP3LJn0dmbd0KoA== 454 | engines: {node: '>=18.20.7'} 455 | 456 | abort-controller@3.0.0: 457 | resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 458 | engines: {node: '>=6.5'} 459 | 460 | acorn@8.15.0: 461 | resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 462 | engines: {node: '>=0.4.0'} 463 | hasBin: true 464 | 465 | ansi-regex@5.0.1: 466 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 467 | engines: {node: '>=8'} 468 | 469 | ansi-regex@6.1.0: 470 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 471 | engines: {node: '>=12'} 472 | 473 | ansi-styles@4.3.0: 474 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 475 | engines: {node: '>=8'} 476 | 477 | ansi-styles@6.2.1: 478 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 479 | engines: {node: '>=12'} 480 | 481 | any-base@1.1.0: 482 | resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} 483 | 484 | async@3.2.6: 485 | resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} 486 | 487 | balanced-match@1.0.2: 488 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 489 | 490 | base64-js@1.5.1: 491 | resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 492 | 493 | basic-auth@2.0.1: 494 | resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} 495 | engines: {node: '>= 0.8'} 496 | 497 | bmp-js@0.1.0: 498 | resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} 499 | 500 | boolean@3.2.0: 501 | resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} 502 | deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. 503 | 504 | brace-expansion@2.0.1: 505 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 506 | 507 | buffer-crc32@0.2.13: 508 | resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} 509 | 510 | buffer-from@1.1.2: 511 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 512 | 513 | buffer@5.7.1: 514 | resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} 515 | 516 | buffer@6.0.3: 517 | resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} 518 | 519 | cacheable-lookup@5.0.4: 520 | resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} 521 | engines: {node: '>=10.6.0'} 522 | 523 | cacheable-request@7.0.4: 524 | resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} 525 | engines: {node: '>=8'} 526 | 527 | call-bind-apply-helpers@1.0.2: 528 | resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 529 | engines: {node: '>= 0.4'} 530 | 531 | call-bound@1.0.4: 532 | resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} 533 | engines: {node: '>= 0.4'} 534 | 535 | camel-case@4.1.2: 536 | resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} 537 | 538 | chalk@4.1.2: 539 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 540 | engines: {node: '>=10'} 541 | 542 | clean-css@5.3.3: 543 | resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} 544 | engines: {node: '>= 10.0'} 545 | 546 | cliui@8.0.1: 547 | resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 548 | engines: {node: '>=12'} 549 | 550 | clone-response@1.0.3: 551 | resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} 552 | 553 | color-convert@2.0.1: 554 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 555 | engines: {node: '>=7.0.0'} 556 | 557 | color-name@1.1.4: 558 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 559 | 560 | commander@10.0.1: 561 | resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} 562 | engines: {node: '>=14'} 563 | 564 | commander@2.20.3: 565 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 566 | 567 | concurrently@9.1.2: 568 | resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==} 569 | engines: {node: '>=18'} 570 | hasBin: true 571 | 572 | corser@2.0.1: 573 | resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} 574 | engines: {node: '>= 0.4.0'} 575 | 576 | cross-spawn@7.0.6: 577 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 578 | engines: {node: '>= 8'} 579 | 580 | csstype@3.1.3: 581 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 582 | 583 | debug@4.4.1: 584 | resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} 585 | engines: {node: '>=6.0'} 586 | peerDependencies: 587 | supports-color: '*' 588 | peerDependenciesMeta: 589 | supports-color: 590 | optional: true 591 | 592 | decompress-response@6.0.0: 593 | resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} 594 | engines: {node: '>=10'} 595 | 596 | defer-to-connect@2.0.1: 597 | resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} 598 | engines: {node: '>=10'} 599 | 600 | define-data-property@1.1.4: 601 | resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} 602 | engines: {node: '>= 0.4'} 603 | 604 | define-properties@1.2.1: 605 | resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} 606 | engines: {node: '>= 0.4'} 607 | 608 | dequal@2.0.3: 609 | resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 610 | engines: {node: '>=6'} 611 | 612 | detect-node@2.1.0: 613 | resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} 614 | 615 | discord-api-types@0.38.16: 616 | resolution: {integrity: sha512-Cz42dC5WqJD17Yk0bRy7YLTJmh3NKo4FGpxZuA8MHqT0RPxKSrll5YhlODZ2z5DiEV/gpHMeTSrTFTWpSXjT1Q==} 617 | 618 | dot-case@3.0.4: 619 | resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} 620 | 621 | dunder-proto@1.0.1: 622 | resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 623 | engines: {node: '>= 0.4'} 624 | 625 | eastasianwidth@0.2.0: 626 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 627 | 628 | electron@36.4.0: 629 | resolution: {integrity: sha512-LLOOZEuW5oqvnjC7HBQhIqjIIJAZCIFjQxltQGLfEC7XFsBoZgQ3u3iFj+Kzw68Xj97u1n57Jdt7P98qLvUibQ==} 630 | engines: {node: '>= 12.20.55'} 631 | hasBin: true 632 | 633 | emoji-regex@8.0.0: 634 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 635 | 636 | emoji-regex@9.2.2: 637 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 638 | 639 | end-of-stream@1.4.4: 640 | resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} 641 | 642 | entities@4.5.0: 643 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 644 | engines: {node: '>=0.12'} 645 | 646 | env-paths@2.2.1: 647 | resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} 648 | engines: {node: '>=6'} 649 | 650 | es-define-property@1.0.1: 651 | resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 652 | engines: {node: '>= 0.4'} 653 | 654 | es-errors@1.3.0: 655 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 656 | engines: {node: '>= 0.4'} 657 | 658 | es-object-atoms@1.1.1: 659 | resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 660 | engines: {node: '>= 0.4'} 661 | 662 | es6-error@4.1.1: 663 | resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} 664 | 665 | esbuild@0.25.5: 666 | resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} 667 | engines: {node: '>=18'} 668 | hasBin: true 669 | 670 | escalade@3.2.0: 671 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 672 | engines: {node: '>=6'} 673 | 674 | escape-string-regexp@4.0.0: 675 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 676 | engines: {node: '>=10'} 677 | 678 | event-target-shim@5.0.1: 679 | resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} 680 | engines: {node: '>=6'} 681 | 682 | eventemitter3@4.0.7: 683 | resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} 684 | 685 | events@3.3.0: 686 | resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 687 | engines: {node: '>=0.8.x'} 688 | 689 | exif-parser@0.1.12: 690 | resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} 691 | 692 | extract-zip@2.0.1: 693 | resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} 694 | engines: {node: '>= 10.17.0'} 695 | hasBin: true 696 | 697 | fast-deep-equal@3.1.3: 698 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 699 | 700 | fd-slicer@1.1.0: 701 | resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} 702 | 703 | file-type@16.5.4: 704 | resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} 705 | engines: {node: '>=10'} 706 | 707 | follow-redirects@1.15.9: 708 | resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} 709 | engines: {node: '>=4.0'} 710 | peerDependencies: 711 | debug: '*' 712 | peerDependenciesMeta: 713 | debug: 714 | optional: true 715 | 716 | foreground-child@3.3.1: 717 | resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} 718 | engines: {node: '>=14'} 719 | 720 | fs-extra@8.1.0: 721 | resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} 722 | engines: {node: '>=6 <7 || >=8'} 723 | 724 | fsevents@2.3.3: 725 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 726 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 727 | os: [darwin] 728 | 729 | function-bind@1.1.2: 730 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 731 | 732 | get-caller-file@2.0.5: 733 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 734 | engines: {node: 6.* || 8.* || >= 10.*} 735 | 736 | get-intrinsic@1.3.0: 737 | resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 738 | engines: {node: '>= 0.4'} 739 | 740 | get-proto@1.0.1: 741 | resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 742 | engines: {node: '>= 0.4'} 743 | 744 | get-stream@5.2.0: 745 | resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} 746 | engines: {node: '>=8'} 747 | 748 | get-tsconfig@4.10.1: 749 | resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} 750 | 751 | gifwrap@0.10.1: 752 | resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} 753 | 754 | glob@11.0.2: 755 | resolution: {integrity: sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==} 756 | engines: {node: 20 || >=22} 757 | hasBin: true 758 | 759 | global-agent@3.0.0: 760 | resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} 761 | engines: {node: '>=10.0'} 762 | 763 | globalthis@1.0.4: 764 | resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} 765 | engines: {node: '>= 0.4'} 766 | 767 | gopd@1.2.0: 768 | resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 769 | engines: {node: '>= 0.4'} 770 | 771 | got@11.8.6: 772 | resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} 773 | engines: {node: '>=10.19.0'} 774 | 775 | graceful-fs@4.2.11: 776 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 777 | 778 | has-flag@4.0.0: 779 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 780 | engines: {node: '>=8'} 781 | 782 | has-property-descriptors@1.0.2: 783 | resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} 784 | 785 | has-symbols@1.1.0: 786 | resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 787 | engines: {node: '>= 0.4'} 788 | 789 | hasown@2.0.2: 790 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 791 | engines: {node: '>= 0.4'} 792 | 793 | he@1.2.0: 794 | resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 795 | hasBin: true 796 | 797 | html-encoding-sniffer@3.0.0: 798 | resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} 799 | engines: {node: '>=12'} 800 | 801 | html-minifier-terser@7.2.0: 802 | resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} 803 | engines: {node: ^14.13.1 || >=16.0.0} 804 | hasBin: true 805 | 806 | http-cache-semantics@4.2.0: 807 | resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} 808 | 809 | http-proxy@1.18.1: 810 | resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} 811 | engines: {node: '>=8.0.0'} 812 | 813 | http-server@14.1.1: 814 | resolution: {integrity: sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==} 815 | engines: {node: '>=12'} 816 | hasBin: true 817 | 818 | http2-wrapper@1.0.3: 819 | resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} 820 | engines: {node: '>=10.19.0'} 821 | 822 | iconv-lite@0.6.3: 823 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 824 | engines: {node: '>=0.10.0'} 825 | 826 | ieee754@1.2.1: 827 | resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 828 | 829 | image-q@4.0.0: 830 | resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} 831 | 832 | is-fullwidth-code-point@3.0.0: 833 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 834 | engines: {node: '>=8'} 835 | 836 | isexe@2.0.0: 837 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 838 | 839 | isomorphic-fetch@3.0.0: 840 | resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} 841 | 842 | jackspeak@4.1.1: 843 | resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} 844 | engines: {node: 20 || >=22} 845 | 846 | jpeg-js@0.4.4: 847 | resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} 848 | 849 | json-buffer@3.0.1: 850 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 851 | 852 | json-stringify-safe@5.0.1: 853 | resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} 854 | 855 | jsonfile@4.0.0: 856 | resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 857 | 858 | keyv@4.5.4: 859 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 860 | 861 | lodash@4.17.21: 862 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 863 | 864 | lower-case@2.0.2: 865 | resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} 866 | 867 | lowercase-keys@2.0.0: 868 | resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} 869 | engines: {node: '>=8'} 870 | 871 | lru-cache@11.1.0: 872 | resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} 873 | engines: {node: 20 || >=22} 874 | 875 | luna@https://codeload.github.com/inrixia/TidaLuna/tar.gz/10ae3d0: 876 | resolution: {tarball: https://codeload.github.com/inrixia/TidaLuna/tar.gz/10ae3d0} 877 | version: 1.6.0-beta 878 | 879 | magic-bytes.js@1.12.1: 880 | resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} 881 | 882 | matcher@3.0.0: 883 | resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} 884 | engines: {node: '>=10'} 885 | 886 | math-intrinsics@1.1.0: 887 | resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 888 | engines: {node: '>= 0.4'} 889 | 890 | mime@1.6.0: 891 | resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 892 | engines: {node: '>=4'} 893 | hasBin: true 894 | 895 | mimic-response@1.0.1: 896 | resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} 897 | engines: {node: '>=4'} 898 | 899 | mimic-response@3.1.0: 900 | resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} 901 | engines: {node: '>=10'} 902 | 903 | minimatch@10.0.1: 904 | resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} 905 | engines: {node: 20 || >=22} 906 | 907 | minimist@1.2.8: 908 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 909 | 910 | minipass@7.1.2: 911 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 912 | engines: {node: '>=16 || 14 >=14.17'} 913 | 914 | ms@2.1.3: 915 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 916 | 917 | no-case@3.0.4: 918 | resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} 919 | 920 | node-fetch@2.7.0: 921 | resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} 922 | engines: {node: 4.x || >=6.0.0} 923 | peerDependencies: 924 | encoding: ^0.1.0 925 | peerDependenciesMeta: 926 | encoding: 927 | optional: true 928 | 929 | node-vibrant@4.0.3: 930 | resolution: {integrity: sha512-kzoIuJK90BH/k65Avt077JCX4Nhqz1LNc8cIOm2rnYEvFdJIYd8b3SQwU1MTpzcHtr8z8jxkl1qdaCfbP3olFg==} 931 | 932 | normalize-url@6.1.0: 933 | resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} 934 | engines: {node: '>=10'} 935 | 936 | object-inspect@1.13.4: 937 | resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} 938 | engines: {node: '>= 0.4'} 939 | 940 | object-keys@1.1.1: 941 | resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} 942 | engines: {node: '>= 0.4'} 943 | 944 | oby@15.1.2: 945 | resolution: {integrity: sha512-6QD9iEoPzV+pMDdcg3RtFWhgDX8pS5hZouVHvgXGDy3Q9RxFfnI3CYv9i62keeuX+qk6iN2z5E9FD3q3OckZ6A==} 946 | 947 | omggif@1.0.10: 948 | resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} 949 | 950 | once@1.4.0: 951 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 952 | 953 | opener@1.5.2: 954 | resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} 955 | hasBin: true 956 | 957 | p-cancelable@2.1.1: 958 | resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} 959 | engines: {node: '>=8'} 960 | 961 | package-json-from-dist@1.0.1: 962 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 963 | 964 | pako@1.0.11: 965 | resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} 966 | 967 | param-case@3.0.4: 968 | resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} 969 | 970 | pascal-case@3.1.2: 971 | resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} 972 | 973 | path-key@3.1.1: 974 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 975 | engines: {node: '>=8'} 976 | 977 | path-scurry@2.0.0: 978 | resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} 979 | engines: {node: 20 || >=22} 980 | 981 | peek-readable@4.1.0: 982 | resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} 983 | engines: {node: '>=8'} 984 | 985 | pend@1.2.0: 986 | resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} 987 | 988 | pixelmatch@4.0.2: 989 | resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} 990 | hasBin: true 991 | 992 | pngjs@3.4.0: 993 | resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} 994 | engines: {node: '>=4.0.0'} 995 | 996 | pngjs@6.0.0: 997 | resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} 998 | engines: {node: '>=12.13.0'} 999 | 1000 | portfinder@1.0.37: 1001 | resolution: {integrity: sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==} 1002 | engines: {node: '>= 10.12'} 1003 | 1004 | process@0.11.10: 1005 | resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} 1006 | engines: {node: '>= 0.6.0'} 1007 | 1008 | progress@2.0.3: 1009 | resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} 1010 | engines: {node: '>=0.4.0'} 1011 | 1012 | pump@3.0.2: 1013 | resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} 1014 | 1015 | qs@6.14.0: 1016 | resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} 1017 | engines: {node: '>=0.6'} 1018 | 1019 | quick-lru@5.1.1: 1020 | resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} 1021 | engines: {node: '>=10'} 1022 | 1023 | readable-stream@4.7.0: 1024 | resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} 1025 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 1026 | 1027 | readable-web-to-node-stream@3.0.4: 1028 | resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} 1029 | engines: {node: '>=8'} 1030 | 1031 | regenerator-runtime@0.13.11: 1032 | resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} 1033 | 1034 | relateurl@0.2.7: 1035 | resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} 1036 | engines: {node: '>= 0.10'} 1037 | 1038 | require-directory@2.1.1: 1039 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 1040 | engines: {node: '>=0.10.0'} 1041 | 1042 | requires-port@1.0.0: 1043 | resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} 1044 | 1045 | resolve-alpn@1.2.1: 1046 | resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} 1047 | 1048 | resolve-pkg-maps@1.0.0: 1049 | resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1050 | 1051 | responselike@2.0.1: 1052 | resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} 1053 | 1054 | rimraf@6.0.1: 1055 | resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} 1056 | engines: {node: 20 || >=22} 1057 | hasBin: true 1058 | 1059 | roarr@2.15.4: 1060 | resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} 1061 | engines: {node: '>=8.0'} 1062 | 1063 | rxjs@7.8.2: 1064 | resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} 1065 | 1066 | safe-buffer@5.1.2: 1067 | resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 1068 | 1069 | safe-buffer@5.2.1: 1070 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1071 | 1072 | safer-buffer@2.1.2: 1073 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 1074 | 1075 | sanitize-filename@1.6.3: 1076 | resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} 1077 | 1078 | secure-compare@3.0.1: 1079 | resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} 1080 | 1081 | semver-compare@1.0.0: 1082 | resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} 1083 | 1084 | semver@6.3.1: 1085 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 1086 | hasBin: true 1087 | 1088 | semver@7.7.2: 1089 | resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} 1090 | engines: {node: '>=10'} 1091 | hasBin: true 1092 | 1093 | serialize-error@7.0.1: 1094 | resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} 1095 | engines: {node: '>=10'} 1096 | 1097 | shazamio-core@1.3.1: 1098 | resolution: {integrity: sha512-wzYxaL+Tzj4hv5UO1kCbJSjnL02rfcPqxtklf2vg1ykwE1U/FXpB83SRTS4/0OU2uTcVcvQN+xTqzwZQl4CIMg==} 1099 | 1100 | shebang-command@2.0.0: 1101 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1102 | engines: {node: '>=8'} 1103 | 1104 | shebang-regex@3.0.0: 1105 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1106 | engines: {node: '>=8'} 1107 | 1108 | shell-quote@1.8.3: 1109 | resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} 1110 | engines: {node: '>= 0.4'} 1111 | 1112 | side-channel-list@1.0.0: 1113 | resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} 1114 | engines: {node: '>= 0.4'} 1115 | 1116 | side-channel-map@1.0.1: 1117 | resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} 1118 | engines: {node: '>= 0.4'} 1119 | 1120 | side-channel-weakmap@1.0.2: 1121 | resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} 1122 | engines: {node: '>= 0.4'} 1123 | 1124 | side-channel@1.1.0: 1125 | resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} 1126 | engines: {node: '>= 0.4'} 1127 | 1128 | signal-exit@4.1.0: 1129 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1130 | engines: {node: '>=14'} 1131 | 1132 | source-map-support@0.5.21: 1133 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 1134 | 1135 | source-map@0.6.1: 1136 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 1137 | engines: {node: '>=0.10.0'} 1138 | 1139 | sprintf-js@1.1.3: 1140 | resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} 1141 | 1142 | string-width@4.2.3: 1143 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1144 | engines: {node: '>=8'} 1145 | 1146 | string-width@5.1.2: 1147 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 1148 | engines: {node: '>=12'} 1149 | 1150 | string_decoder@1.3.0: 1151 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 1152 | 1153 | strip-ansi@6.0.1: 1154 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1155 | engines: {node: '>=8'} 1156 | 1157 | strip-ansi@7.1.0: 1158 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 1159 | engines: {node: '>=12'} 1160 | 1161 | strtok3@6.3.0: 1162 | resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} 1163 | engines: {node: '>=10'} 1164 | 1165 | sumchecker@3.0.1: 1166 | resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} 1167 | engines: {node: '>= 8.0'} 1168 | 1169 | supports-color@7.2.0: 1170 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1171 | engines: {node: '>=8'} 1172 | 1173 | supports-color@8.1.1: 1174 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 1175 | engines: {node: '>=10'} 1176 | 1177 | terser@5.42.0: 1178 | resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} 1179 | engines: {node: '>=10'} 1180 | hasBin: true 1181 | 1182 | timm@1.7.1: 1183 | resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} 1184 | 1185 | tinycolor2@1.6.0: 1186 | resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} 1187 | 1188 | token-types@4.2.1: 1189 | resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} 1190 | engines: {node: '>=10'} 1191 | 1192 | tr46@0.0.3: 1193 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} 1194 | 1195 | tree-kill@1.2.2: 1196 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 1197 | hasBin: true 1198 | 1199 | truncate-utf8-bytes@1.0.2: 1200 | resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} 1201 | 1202 | tslib@2.8.1: 1203 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1204 | 1205 | tsx@4.20.0: 1206 | resolution: {integrity: sha512-TsmdeXxcZYiJ2MKV7fdq38na0CKyLRtCeMqTeHMmrVXQ/gf5yTeJohh+sgr7MnMGsjeKXzHzy+TwOOTR1arl+Q==} 1207 | engines: {node: '>=18.0.0'} 1208 | hasBin: true 1209 | 1210 | type-fest@0.13.1: 1211 | resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} 1212 | engines: {node: '>=10'} 1213 | 1214 | typescript@5.8.3: 1215 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 1216 | engines: {node: '>=14.17'} 1217 | hasBin: true 1218 | 1219 | undici-types@5.26.5: 1220 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 1221 | 1222 | undici-types@6.21.0: 1223 | resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 1224 | 1225 | undici-types@7.8.0: 1226 | resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} 1227 | 1228 | undici@6.21.1: 1229 | resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} 1230 | engines: {node: '>=18.17'} 1231 | 1232 | union@0.5.0: 1233 | resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} 1234 | engines: {node: '>= 0.8.0'} 1235 | 1236 | universalify@0.1.2: 1237 | resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} 1238 | engines: {node: '>= 4.0.0'} 1239 | 1240 | url-join@4.0.1: 1241 | resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} 1242 | 1243 | utf8-byte-length@1.0.5: 1244 | resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} 1245 | 1246 | utif2@4.1.0: 1247 | resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} 1248 | 1249 | uuid@11.1.0: 1250 | resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} 1251 | hasBin: true 1252 | 1253 | webidl-conversions@3.0.1: 1254 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} 1255 | 1256 | whatwg-encoding@2.0.0: 1257 | resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} 1258 | engines: {node: '>=12'} 1259 | 1260 | whatwg-fetch@3.6.20: 1261 | resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} 1262 | 1263 | whatwg-url@5.0.0: 1264 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} 1265 | 1266 | which@2.0.2: 1267 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1268 | engines: {node: '>= 8'} 1269 | hasBin: true 1270 | 1271 | wrap-ansi@7.0.0: 1272 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 1273 | engines: {node: '>=10'} 1274 | 1275 | wrap-ansi@8.1.0: 1276 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 1277 | engines: {node: '>=12'} 1278 | 1279 | wrappy@1.0.2: 1280 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1281 | 1282 | ws@8.18.2: 1283 | resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} 1284 | engines: {node: '>=10.0.0'} 1285 | peerDependencies: 1286 | bufferutil: ^4.0.1 1287 | utf-8-validate: '>=5.0.2' 1288 | peerDependenciesMeta: 1289 | bufferutil: 1290 | optional: true 1291 | utf-8-validate: 1292 | optional: true 1293 | 1294 | y18n@5.0.8: 1295 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 1296 | engines: {node: '>=10'} 1297 | 1298 | yargs-parser@21.1.1: 1299 | resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 1300 | engines: {node: '>=12'} 1301 | 1302 | yargs@17.7.2: 1303 | resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} 1304 | engines: {node: '>=12'} 1305 | 1306 | yauzl@2.10.0: 1307 | resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} 1308 | 1309 | snapshots: 1310 | 1311 | '@discordjs/collection@2.1.1': {} 1312 | 1313 | '@discordjs/rest@2.5.1': 1314 | dependencies: 1315 | '@discordjs/collection': 2.1.1 1316 | '@discordjs/util': 1.1.1 1317 | '@sapphire/async-queue': 1.5.5 1318 | '@sapphire/snowflake': 3.5.5 1319 | '@vladfrangu/async_event_emitter': 2.4.6 1320 | discord-api-types: 0.38.16 1321 | magic-bytes.js: 1.12.1 1322 | tslib: 2.8.1 1323 | undici: 6.21.3 1324 | 1325 | '@discordjs/util@1.1.1': {} 1326 | 1327 | '@electron/get@2.0.3': 1328 | dependencies: 1329 | debug: 4.4.1 1330 | env-paths: 2.2.1 1331 | fs-extra: 8.1.0 1332 | got: 11.8.6 1333 | progress: 2.0.3 1334 | semver: 6.3.1 1335 | sumchecker: 3.0.1 1336 | optionalDependencies: 1337 | global-agent: 3.0.0 1338 | transitivePeerDependencies: 1339 | - supports-color 1340 | 1341 | '@esbuild/aix-ppc64@0.25.5': 1342 | optional: true 1343 | 1344 | '@esbuild/android-arm64@0.25.5': 1345 | optional: true 1346 | 1347 | '@esbuild/android-arm@0.25.5': 1348 | optional: true 1349 | 1350 | '@esbuild/android-x64@0.25.5': 1351 | optional: true 1352 | 1353 | '@esbuild/darwin-arm64@0.25.5': 1354 | optional: true 1355 | 1356 | '@esbuild/darwin-x64@0.25.5': 1357 | optional: true 1358 | 1359 | '@esbuild/freebsd-arm64@0.25.5': 1360 | optional: true 1361 | 1362 | '@esbuild/freebsd-x64@0.25.5': 1363 | optional: true 1364 | 1365 | '@esbuild/linux-arm64@0.25.5': 1366 | optional: true 1367 | 1368 | '@esbuild/linux-arm@0.25.5': 1369 | optional: true 1370 | 1371 | '@esbuild/linux-ia32@0.25.5': 1372 | optional: true 1373 | 1374 | '@esbuild/linux-loong64@0.25.5': 1375 | optional: true 1376 | 1377 | '@esbuild/linux-mips64el@0.25.5': 1378 | optional: true 1379 | 1380 | '@esbuild/linux-ppc64@0.25.5': 1381 | optional: true 1382 | 1383 | '@esbuild/linux-riscv64@0.25.5': 1384 | optional: true 1385 | 1386 | '@esbuild/linux-s390x@0.25.5': 1387 | optional: true 1388 | 1389 | '@esbuild/linux-x64@0.25.5': 1390 | optional: true 1391 | 1392 | '@esbuild/netbsd-arm64@0.25.5': 1393 | optional: true 1394 | 1395 | '@esbuild/netbsd-x64@0.25.5': 1396 | optional: true 1397 | 1398 | '@esbuild/openbsd-arm64@0.25.5': 1399 | optional: true 1400 | 1401 | '@esbuild/openbsd-x64@0.25.5': 1402 | optional: true 1403 | 1404 | '@esbuild/sunos-x64@0.25.5': 1405 | optional: true 1406 | 1407 | '@esbuild/win32-arm64@0.25.5': 1408 | optional: true 1409 | 1410 | '@esbuild/win32-ia32@0.25.5': 1411 | optional: true 1412 | 1413 | '@esbuild/win32-x64@0.25.5': 1414 | optional: true 1415 | 1416 | '@homebridge/ciao@1.3.2': 1417 | dependencies: 1418 | debug: 4.4.1 1419 | fast-deep-equal: 3.1.3 1420 | source-map-support: 0.5.21 1421 | tslib: 2.8.1 1422 | transitivePeerDependencies: 1423 | - supports-color 1424 | 1425 | '@inrixia/helpers@3.20.2': 1426 | dependencies: 1427 | dequal: 2.0.3 1428 | 1429 | '@isaacs/cliui@8.0.2': 1430 | dependencies: 1431 | string-width: 5.1.2 1432 | string-width-cjs: string-width@4.2.3 1433 | strip-ansi: 7.1.0 1434 | strip-ansi-cjs: strip-ansi@6.0.1 1435 | wrap-ansi: 8.1.0 1436 | wrap-ansi-cjs: wrap-ansi@7.0.0 1437 | 1438 | '@jimp/bmp@0.22.12(@jimp/custom@0.22.12)': 1439 | dependencies: 1440 | '@jimp/custom': 0.22.12 1441 | '@jimp/utils': 0.22.12 1442 | bmp-js: 0.1.0 1443 | 1444 | '@jimp/core@0.22.12': 1445 | dependencies: 1446 | '@jimp/utils': 0.22.12 1447 | any-base: 1.1.0 1448 | buffer: 5.7.1 1449 | exif-parser: 0.1.12 1450 | file-type: 16.5.4 1451 | isomorphic-fetch: 3.0.0 1452 | pixelmatch: 4.0.2 1453 | tinycolor2: 1.6.0 1454 | transitivePeerDependencies: 1455 | - encoding 1456 | 1457 | '@jimp/custom@0.22.12': 1458 | dependencies: 1459 | '@jimp/core': 0.22.12 1460 | transitivePeerDependencies: 1461 | - encoding 1462 | 1463 | '@jimp/gif@0.22.12(@jimp/custom@0.22.12)': 1464 | dependencies: 1465 | '@jimp/custom': 0.22.12 1466 | '@jimp/utils': 0.22.12 1467 | gifwrap: 0.10.1 1468 | omggif: 1.0.10 1469 | 1470 | '@jimp/jpeg@0.22.12(@jimp/custom@0.22.12)': 1471 | dependencies: 1472 | '@jimp/custom': 0.22.12 1473 | '@jimp/utils': 0.22.12 1474 | jpeg-js: 0.4.4 1475 | 1476 | '@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)': 1477 | dependencies: 1478 | '@jimp/custom': 0.22.12 1479 | '@jimp/utils': 0.22.12 1480 | 1481 | '@jimp/png@0.22.12(@jimp/custom@0.22.12)': 1482 | dependencies: 1483 | '@jimp/custom': 0.22.12 1484 | '@jimp/utils': 0.22.12 1485 | pngjs: 6.0.0 1486 | 1487 | '@jimp/tiff@0.22.12(@jimp/custom@0.22.12)': 1488 | dependencies: 1489 | '@jimp/custom': 0.22.12 1490 | utif2: 4.1.0 1491 | 1492 | '@jimp/types@0.22.12(@jimp/custom@0.22.12)': 1493 | dependencies: 1494 | '@jimp/bmp': 0.22.12(@jimp/custom@0.22.12) 1495 | '@jimp/custom': 0.22.12 1496 | '@jimp/gif': 0.22.12(@jimp/custom@0.22.12) 1497 | '@jimp/jpeg': 0.22.12(@jimp/custom@0.22.12) 1498 | '@jimp/png': 0.22.12(@jimp/custom@0.22.12) 1499 | '@jimp/tiff': 0.22.12(@jimp/custom@0.22.12) 1500 | timm: 1.7.1 1501 | 1502 | '@jimp/utils@0.22.12': 1503 | dependencies: 1504 | regenerator-runtime: 0.13.11 1505 | 1506 | '@jridgewell/gen-mapping@0.3.8': 1507 | dependencies: 1508 | '@jridgewell/set-array': 1.2.1 1509 | '@jridgewell/sourcemap-codec': 1.5.0 1510 | '@jridgewell/trace-mapping': 0.3.25 1511 | 1512 | '@jridgewell/resolve-uri@3.1.2': {} 1513 | 1514 | '@jridgewell/set-array@1.2.1': {} 1515 | 1516 | '@jridgewell/source-map@0.3.6': 1517 | dependencies: 1518 | '@jridgewell/gen-mapping': 0.3.8 1519 | '@jridgewell/trace-mapping': 0.3.25 1520 | 1521 | '@jridgewell/sourcemap-codec@1.5.0': {} 1522 | 1523 | '@jridgewell/trace-mapping@0.3.25': 1524 | dependencies: 1525 | '@jridgewell/resolve-uri': 3.1.2 1526 | '@jridgewell/sourcemap-codec': 1.5.0 1527 | 1528 | '@sapphire/async-queue@1.5.5': {} 1529 | 1530 | '@sapphire/snowflake@3.5.5': {} 1531 | 1532 | '@sindresorhus/is@4.6.0': {} 1533 | 1534 | '@szmarczak/http-timer@4.0.6': 1535 | dependencies: 1536 | defer-to-connect: 2.0.1 1537 | 1538 | '@tokenizer/token@0.3.0': {} 1539 | 1540 | '@types/cacheable-request@6.0.3': 1541 | dependencies: 1542 | '@types/http-cache-semantics': 4.0.4 1543 | '@types/keyv': 3.1.4 1544 | '@types/node': 24.0.0 1545 | '@types/responselike': 1.0.3 1546 | 1547 | '@types/clean-css@4.2.11': 1548 | dependencies: 1549 | '@types/node': 24.0.0 1550 | source-map: 0.6.1 1551 | 1552 | '@types/html-minifier-terser@7.0.2': {} 1553 | 1554 | '@types/http-cache-semantics@4.0.4': {} 1555 | 1556 | '@types/keyv@3.1.4': 1557 | dependencies: 1558 | '@types/node': 24.0.0 1559 | 1560 | '@types/node@16.9.1': {} 1561 | 1562 | '@types/node@18.19.111': 1563 | dependencies: 1564 | undici-types: 5.26.5 1565 | 1566 | '@types/node@22.15.31': 1567 | dependencies: 1568 | undici-types: 6.21.0 1569 | 1570 | '@types/node@24.0.0': 1571 | dependencies: 1572 | undici-types: 7.8.0 1573 | 1574 | '@types/react-dom@19.1.6(@types/react@19.1.7)': 1575 | dependencies: 1576 | '@types/react': 19.1.7 1577 | 1578 | '@types/react@19.1.7': 1579 | dependencies: 1580 | csstype: 3.1.3 1581 | 1582 | '@types/responselike@1.0.3': 1583 | dependencies: 1584 | '@types/node': 24.0.0 1585 | 1586 | '@types/uuid@10.0.0': {} 1587 | 1588 | '@types/yauzl@2.10.3': 1589 | dependencies: 1590 | '@types/node': 24.0.0 1591 | optional: true 1592 | 1593 | '@vibrant/color@4.0.0': {} 1594 | 1595 | '@vibrant/core@4.0.0': 1596 | dependencies: 1597 | '@vibrant/color': 4.0.0 1598 | '@vibrant/generator': 4.0.0 1599 | '@vibrant/image': 4.0.0 1600 | '@vibrant/quantizer': 4.0.0 1601 | '@vibrant/worker': 4.0.0 1602 | 1603 | '@vibrant/generator-default@4.0.3': 1604 | dependencies: 1605 | '@vibrant/color': 4.0.0 1606 | '@vibrant/generator': 4.0.0 1607 | 1608 | '@vibrant/generator@4.0.0': 1609 | dependencies: 1610 | '@vibrant/color': 4.0.0 1611 | '@vibrant/types': 4.0.0 1612 | 1613 | '@vibrant/image-browser@4.0.0': 1614 | dependencies: 1615 | '@vibrant/image': 4.0.0 1616 | 1617 | '@vibrant/image-node@4.0.0': 1618 | dependencies: 1619 | '@jimp/custom': 0.22.12 1620 | '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) 1621 | '@jimp/types': 0.22.12(@jimp/custom@0.22.12) 1622 | '@vibrant/image': 4.0.0 1623 | transitivePeerDependencies: 1624 | - encoding 1625 | 1626 | '@vibrant/image@4.0.0': 1627 | dependencies: 1628 | '@vibrant/color': 4.0.0 1629 | 1630 | '@vibrant/quantizer-mmcq@4.0.0': 1631 | dependencies: 1632 | '@vibrant/color': 4.0.0 1633 | '@vibrant/image': 4.0.0 1634 | '@vibrant/quantizer': 4.0.0 1635 | 1636 | '@vibrant/quantizer@4.0.0': 1637 | dependencies: 1638 | '@vibrant/color': 4.0.0 1639 | '@vibrant/image': 4.0.0 1640 | '@vibrant/types': 4.0.0 1641 | 1642 | '@vibrant/types@4.0.0': {} 1643 | 1644 | '@vibrant/worker@4.0.0': 1645 | dependencies: 1646 | '@vibrant/types': 4.0.0 1647 | 1648 | '@vladfrangu/async_event_emitter@2.4.6': {} 1649 | 1650 | '@xhayper/discord-rpc@1.2.2': 1651 | dependencies: 1652 | '@discordjs/rest': 2.5.1 1653 | '@vladfrangu/async_event_emitter': 2.4.6 1654 | discord-api-types: 0.37.120 1655 | ws: 8.18.2 1656 | transitivePeerDependencies: 1657 | - bufferutil 1658 | - utf-8-validate 1659 | 1660 | abort-controller@3.0.0: 1661 | dependencies: 1662 | event-target-shim: 5.0.1 1663 | 1664 | acorn@8.15.0: {} 1665 | 1666 | ansi-regex@5.0.1: {} 1667 | 1668 | ansi-regex@6.1.0: {} 1669 | 1670 | ansi-styles@4.3.0: 1671 | dependencies: 1672 | color-convert: 2.0.1 1673 | 1674 | ansi-styles@6.2.1: {} 1675 | 1676 | any-base@1.1.0: {} 1677 | 1678 | async@3.2.6: {} 1679 | 1680 | balanced-match@1.0.2: {} 1681 | 1682 | base64-js@1.5.1: {} 1683 | 1684 | basic-auth@2.0.1: 1685 | dependencies: 1686 | safe-buffer: 5.1.2 1687 | 1688 | bmp-js@0.1.0: {} 1689 | 1690 | boolean@3.2.0: 1691 | optional: true 1692 | 1693 | brace-expansion@2.0.1: 1694 | dependencies: 1695 | balanced-match: 1.0.2 1696 | 1697 | buffer-crc32@0.2.13: {} 1698 | 1699 | buffer-from@1.1.2: {} 1700 | 1701 | buffer@5.7.1: 1702 | dependencies: 1703 | base64-js: 1.5.1 1704 | ieee754: 1.2.1 1705 | 1706 | buffer@6.0.3: 1707 | dependencies: 1708 | base64-js: 1.5.1 1709 | ieee754: 1.2.1 1710 | 1711 | cacheable-lookup@5.0.4: {} 1712 | 1713 | cacheable-request@7.0.4: 1714 | dependencies: 1715 | clone-response: 1.0.3 1716 | get-stream: 5.2.0 1717 | http-cache-semantics: 4.2.0 1718 | keyv: 4.5.4 1719 | lowercase-keys: 2.0.0 1720 | normalize-url: 6.1.0 1721 | responselike: 2.0.1 1722 | 1723 | call-bind-apply-helpers@1.0.2: 1724 | dependencies: 1725 | es-errors: 1.3.0 1726 | function-bind: 1.1.2 1727 | 1728 | call-bound@1.0.4: 1729 | dependencies: 1730 | call-bind-apply-helpers: 1.0.2 1731 | get-intrinsic: 1.3.0 1732 | 1733 | camel-case@4.1.2: 1734 | dependencies: 1735 | pascal-case: 3.1.2 1736 | tslib: 2.8.1 1737 | 1738 | chalk@4.1.2: 1739 | dependencies: 1740 | ansi-styles: 4.3.0 1741 | supports-color: 7.2.0 1742 | 1743 | clean-css@5.3.3: 1744 | dependencies: 1745 | source-map: 0.6.1 1746 | 1747 | cliui@8.0.1: 1748 | dependencies: 1749 | string-width: 4.2.3 1750 | strip-ansi: 6.0.1 1751 | wrap-ansi: 7.0.0 1752 | 1753 | clone-response@1.0.3: 1754 | dependencies: 1755 | mimic-response: 1.0.1 1756 | 1757 | color-convert@2.0.1: 1758 | dependencies: 1759 | color-name: 1.1.4 1760 | 1761 | color-name@1.1.4: {} 1762 | 1763 | commander@10.0.1: {} 1764 | 1765 | commander@2.20.3: {} 1766 | 1767 | concurrently@9.1.2: 1768 | dependencies: 1769 | chalk: 4.1.2 1770 | lodash: 4.17.21 1771 | rxjs: 7.8.2 1772 | shell-quote: 1.8.3 1773 | supports-color: 8.1.1 1774 | tree-kill: 1.2.2 1775 | yargs: 17.7.2 1776 | 1777 | corser@2.0.1: {} 1778 | 1779 | cross-spawn@7.0.6: 1780 | dependencies: 1781 | path-key: 3.1.1 1782 | shebang-command: 2.0.0 1783 | which: 2.0.2 1784 | 1785 | csstype@3.1.3: {} 1786 | 1787 | debug@4.4.1: 1788 | dependencies: 1789 | ms: 2.1.3 1790 | 1791 | decompress-response@6.0.0: 1792 | dependencies: 1793 | mimic-response: 3.1.0 1794 | 1795 | defer-to-connect@2.0.1: {} 1796 | 1797 | define-data-property@1.1.4: 1798 | dependencies: 1799 | es-define-property: 1.0.1 1800 | es-errors: 1.3.0 1801 | gopd: 1.2.0 1802 | optional: true 1803 | 1804 | define-properties@1.2.1: 1805 | dependencies: 1806 | define-data-property: 1.1.4 1807 | has-property-descriptors: 1.0.2 1808 | object-keys: 1.1.1 1809 | optional: true 1810 | 1811 | dequal@2.0.3: {} 1812 | 1813 | detect-node@2.1.0: 1814 | optional: true 1815 | 1816 | discord-api-types@0.38.16: {} 1817 | 1818 | dot-case@3.0.4: 1819 | dependencies: 1820 | no-case: 3.0.4 1821 | tslib: 2.8.1 1822 | 1823 | dunder-proto@1.0.1: 1824 | dependencies: 1825 | call-bind-apply-helpers: 1.0.2 1826 | es-errors: 1.3.0 1827 | gopd: 1.2.0 1828 | 1829 | eastasianwidth@0.2.0: {} 1830 | 1831 | electron@36.4.0: 1832 | dependencies: 1833 | '@electron/get': 2.0.3 1834 | '@types/node': 22.15.31 1835 | extract-zip: 2.0.1 1836 | transitivePeerDependencies: 1837 | - supports-color 1838 | 1839 | emoji-regex@8.0.0: {} 1840 | 1841 | emoji-regex@9.2.2: {} 1842 | 1843 | end-of-stream@1.4.4: 1844 | dependencies: 1845 | once: 1.4.0 1846 | 1847 | entities@4.5.0: {} 1848 | 1849 | env-paths@2.2.1: {} 1850 | 1851 | es-define-property@1.0.1: {} 1852 | 1853 | es-errors@1.3.0: {} 1854 | 1855 | es-object-atoms@1.1.1: 1856 | dependencies: 1857 | es-errors: 1.3.0 1858 | 1859 | es6-error@4.1.1: 1860 | optional: true 1861 | 1862 | esbuild@0.25.5: 1863 | optionalDependencies: 1864 | '@esbuild/aix-ppc64': 0.25.5 1865 | '@esbuild/android-arm': 0.25.5 1866 | '@esbuild/android-arm64': 0.25.5 1867 | '@esbuild/android-x64': 0.25.5 1868 | '@esbuild/darwin-arm64': 0.25.5 1869 | '@esbuild/darwin-x64': 0.25.5 1870 | '@esbuild/freebsd-arm64': 0.25.5 1871 | '@esbuild/freebsd-x64': 0.25.5 1872 | '@esbuild/linux-arm': 0.25.5 1873 | '@esbuild/linux-arm64': 0.25.5 1874 | '@esbuild/linux-ia32': 0.25.5 1875 | '@esbuild/linux-loong64': 0.25.5 1876 | '@esbuild/linux-mips64el': 0.25.5 1877 | '@esbuild/linux-ppc64': 0.25.5 1878 | '@esbuild/linux-riscv64': 0.25.5 1879 | '@esbuild/linux-s390x': 0.25.5 1880 | '@esbuild/linux-x64': 0.25.5 1881 | '@esbuild/netbsd-arm64': 0.25.5 1882 | '@esbuild/netbsd-x64': 0.25.5 1883 | '@esbuild/openbsd-arm64': 0.25.5 1884 | '@esbuild/openbsd-x64': 0.25.5 1885 | '@esbuild/sunos-x64': 0.25.5 1886 | '@esbuild/win32-arm64': 0.25.5 1887 | '@esbuild/win32-ia32': 0.25.5 1888 | '@esbuild/win32-x64': 0.25.5 1889 | 1890 | escalade@3.2.0: {} 1891 | 1892 | escape-string-regexp@4.0.0: 1893 | optional: true 1894 | 1895 | event-target-shim@5.0.1: {} 1896 | 1897 | eventemitter3@4.0.7: {} 1898 | 1899 | events@3.3.0: {} 1900 | 1901 | exif-parser@0.1.12: {} 1902 | 1903 | extract-zip@2.0.1: 1904 | dependencies: 1905 | debug: 4.4.1 1906 | get-stream: 5.2.0 1907 | yauzl: 2.10.0 1908 | optionalDependencies: 1909 | '@types/yauzl': 2.10.3 1910 | transitivePeerDependencies: 1911 | - supports-color 1912 | 1913 | fast-deep-equal@3.1.3: {} 1914 | 1915 | fd-slicer@1.1.0: 1916 | dependencies: 1917 | pend: 1.2.0 1918 | 1919 | file-type@16.5.4: 1920 | dependencies: 1921 | readable-web-to-node-stream: 3.0.4 1922 | strtok3: 6.3.0 1923 | token-types: 4.2.1 1924 | 1925 | follow-redirects@1.15.9: {} 1926 | 1927 | foreground-child@3.3.1: 1928 | dependencies: 1929 | cross-spawn: 7.0.6 1930 | signal-exit: 4.1.0 1931 | 1932 | fs-extra@8.1.0: 1933 | dependencies: 1934 | graceful-fs: 4.2.11 1935 | jsonfile: 4.0.0 1936 | universalify: 0.1.2 1937 | 1938 | fsevents@2.3.3: 1939 | optional: true 1940 | 1941 | function-bind@1.1.2: {} 1942 | 1943 | get-caller-file@2.0.5: {} 1944 | 1945 | get-intrinsic@1.3.0: 1946 | dependencies: 1947 | call-bind-apply-helpers: 1.0.2 1948 | es-define-property: 1.0.1 1949 | es-errors: 1.3.0 1950 | es-object-atoms: 1.1.1 1951 | function-bind: 1.1.2 1952 | get-proto: 1.0.1 1953 | gopd: 1.2.0 1954 | has-symbols: 1.1.0 1955 | hasown: 2.0.2 1956 | math-intrinsics: 1.1.0 1957 | 1958 | get-proto@1.0.1: 1959 | dependencies: 1960 | dunder-proto: 1.0.1 1961 | es-object-atoms: 1.1.1 1962 | 1963 | get-stream@5.2.0: 1964 | dependencies: 1965 | pump: 3.0.2 1966 | 1967 | get-tsconfig@4.10.1: 1968 | dependencies: 1969 | resolve-pkg-maps: 1.0.0 1970 | 1971 | gifwrap@0.10.1: 1972 | dependencies: 1973 | image-q: 4.0.0 1974 | omggif: 1.0.10 1975 | 1976 | glob@11.0.2: 1977 | dependencies: 1978 | foreground-child: 3.3.1 1979 | jackspeak: 4.1.1 1980 | minimatch: 10.0.1 1981 | minipass: 7.1.2 1982 | package-json-from-dist: 1.0.1 1983 | path-scurry: 2.0.0 1984 | 1985 | global-agent@3.0.0: 1986 | dependencies: 1987 | boolean: 3.2.0 1988 | es6-error: 4.1.1 1989 | matcher: 3.0.0 1990 | roarr: 2.15.4 1991 | semver: 7.7.2 1992 | serialize-error: 7.0.1 1993 | optional: true 1994 | 1995 | globalthis@1.0.4: 1996 | dependencies: 1997 | define-properties: 1.2.1 1998 | gopd: 1.2.0 1999 | optional: true 2000 | 2001 | gopd@1.2.0: {} 2002 | 2003 | got@11.8.6: 2004 | dependencies: 2005 | '@sindresorhus/is': 4.6.0 2006 | '@szmarczak/http-timer': 4.0.6 2007 | '@types/cacheable-request': 6.0.3 2008 | '@types/responselike': 1.0.3 2009 | cacheable-lookup: 5.0.4 2010 | cacheable-request: 7.0.4 2011 | decompress-response: 6.0.0 2012 | http2-wrapper: 1.0.3 2013 | lowercase-keys: 2.0.0 2014 | p-cancelable: 2.1.1 2015 | responselike: 2.0.1 2016 | 2017 | graceful-fs@4.2.11: {} 2018 | 2019 | has-flag@4.0.0: {} 2020 | 2021 | has-property-descriptors@1.0.2: 2022 | dependencies: 2023 | es-define-property: 1.0.1 2024 | optional: true 2025 | 2026 | has-symbols@1.1.0: {} 2027 | 2028 | hasown@2.0.2: 2029 | dependencies: 2030 | function-bind: 1.1.2 2031 | 2032 | he@1.2.0: {} 2033 | 2034 | html-encoding-sniffer@3.0.0: 2035 | dependencies: 2036 | whatwg-encoding: 2.0.0 2037 | 2038 | html-minifier-terser@7.2.0: 2039 | dependencies: 2040 | camel-case: 4.1.2 2041 | clean-css: 5.3.3 2042 | commander: 10.0.1 2043 | entities: 4.5.0 2044 | param-case: 3.0.4 2045 | relateurl: 0.2.7 2046 | terser: 5.42.0 2047 | 2048 | http-cache-semantics@4.2.0: {} 2049 | 2050 | http-proxy@1.18.1: 2051 | dependencies: 2052 | eventemitter3: 4.0.7 2053 | follow-redirects: 1.15.9 2054 | requires-port: 1.0.0 2055 | transitivePeerDependencies: 2056 | - debug 2057 | 2058 | http-server@14.1.1: 2059 | dependencies: 2060 | basic-auth: 2.0.1 2061 | chalk: 4.1.2 2062 | corser: 2.0.1 2063 | he: 1.2.0 2064 | html-encoding-sniffer: 3.0.0 2065 | http-proxy: 1.18.1 2066 | mime: 1.6.0 2067 | minimist: 1.2.8 2068 | opener: 1.5.2 2069 | portfinder: 1.0.37 2070 | secure-compare: 3.0.1 2071 | union: 0.5.0 2072 | url-join: 4.0.1 2073 | transitivePeerDependencies: 2074 | - debug 2075 | - supports-color 2076 | 2077 | http2-wrapper@1.0.3: 2078 | dependencies: 2079 | quick-lru: 5.1.1 2080 | resolve-alpn: 1.2.1 2081 | 2082 | iconv-lite@0.6.3: 2083 | dependencies: 2084 | safer-buffer: 2.1.2 2085 | 2086 | ieee754@1.2.1: {} 2087 | 2088 | image-q@4.0.0: 2089 | dependencies: 2090 | '@types/node': 16.9.1 2091 | 2092 | is-fullwidth-code-point@3.0.0: {} 2093 | 2094 | isexe@2.0.0: {} 2095 | 2096 | isomorphic-fetch@3.0.0: 2097 | dependencies: 2098 | node-fetch: 2.7.0 2099 | whatwg-fetch: 3.6.20 2100 | transitivePeerDependencies: 2101 | - encoding 2102 | 2103 | jackspeak@4.1.1: 2104 | dependencies: 2105 | '@isaacs/cliui': 8.0.2 2106 | 2107 | jpeg-js@0.4.4: {} 2108 | 2109 | json-buffer@3.0.1: {} 2110 | 2111 | json-stringify-safe@5.0.1: 2112 | optional: true 2113 | 2114 | jsonfile@4.0.0: 2115 | optionalDependencies: 2116 | graceful-fs: 4.2.11 2117 | 2118 | keyv@4.5.4: 2119 | dependencies: 2120 | json-buffer: 3.0.1 2121 | 2122 | lodash@4.17.21: {} 2123 | 2124 | lower-case@2.0.2: 2125 | dependencies: 2126 | tslib: 2.8.1 2127 | 2128 | lowercase-keys@2.0.0: {} 2129 | 2130 | lru-cache@11.1.0: {} 2131 | 2132 | luna@https://codeload.github.com/inrixia/TidaLuna/tar.gz/10ae3d0: 2133 | dependencies: 2134 | '@inrixia/helpers': 3.20.2 2135 | '@types/clean-css': 4.2.11 2136 | '@types/html-minifier-terser': 7.0.2 2137 | '@types/node': 22.15.31 2138 | clean-css: 5.3.3 2139 | esbuild: 0.25.5 2140 | html-minifier-terser: 7.2.0 2141 | 2142 | magic-bytes.js@1.12.1: {} 2143 | 2144 | matcher@3.0.0: 2145 | dependencies: 2146 | escape-string-regexp: 4.0.0 2147 | optional: true 2148 | 2149 | math-intrinsics@1.1.0: {} 2150 | 2151 | mime@1.6.0: {} 2152 | 2153 | mimic-response@1.0.1: {} 2154 | 2155 | mimic-response@3.1.0: {} 2156 | 2157 | minimatch@10.0.1: 2158 | dependencies: 2159 | brace-expansion: 2.0.1 2160 | 2161 | minimist@1.2.8: {} 2162 | 2163 | minipass@7.1.2: {} 2164 | 2165 | ms@2.1.3: {} 2166 | 2167 | no-case@3.0.4: 2168 | dependencies: 2169 | lower-case: 2.0.2 2170 | tslib: 2.8.1 2171 | 2172 | node-fetch@2.7.0: 2173 | dependencies: 2174 | whatwg-url: 5.0.0 2175 | 2176 | node-vibrant@4.0.3: 2177 | dependencies: 2178 | '@types/node': 18.19.111 2179 | '@vibrant/core': 4.0.0 2180 | '@vibrant/generator-default': 4.0.3 2181 | '@vibrant/image-browser': 4.0.0 2182 | '@vibrant/image-node': 4.0.0 2183 | '@vibrant/quantizer-mmcq': 4.0.0 2184 | transitivePeerDependencies: 2185 | - encoding 2186 | 2187 | normalize-url@6.1.0: {} 2188 | 2189 | object-inspect@1.13.4: {} 2190 | 2191 | object-keys@1.1.1: 2192 | optional: true 2193 | 2194 | oby@15.1.2: {} 2195 | 2196 | omggif@1.0.10: {} 2197 | 2198 | once@1.4.0: 2199 | dependencies: 2200 | wrappy: 1.0.2 2201 | 2202 | opener@1.5.2: {} 2203 | 2204 | p-cancelable@2.1.1: {} 2205 | 2206 | package-json-from-dist@1.0.1: {} 2207 | 2208 | pako@1.0.11: {} 2209 | 2210 | param-case@3.0.4: 2211 | dependencies: 2212 | dot-case: 3.0.4 2213 | tslib: 2.8.1 2214 | 2215 | pascal-case@3.1.2: 2216 | dependencies: 2217 | no-case: 3.0.4 2218 | tslib: 2.8.1 2219 | 2220 | path-key@3.1.1: {} 2221 | 2222 | path-scurry@2.0.0: 2223 | dependencies: 2224 | lru-cache: 11.1.0 2225 | minipass: 7.1.2 2226 | 2227 | peek-readable@4.1.0: {} 2228 | 2229 | pend@1.2.0: {} 2230 | 2231 | pixelmatch@4.0.2: 2232 | dependencies: 2233 | pngjs: 3.4.0 2234 | 2235 | pngjs@3.4.0: {} 2236 | 2237 | pngjs@6.0.0: {} 2238 | 2239 | portfinder@1.0.37: 2240 | dependencies: 2241 | async: 3.2.6 2242 | debug: 4.4.1 2243 | transitivePeerDependencies: 2244 | - supports-color 2245 | 2246 | process@0.11.10: {} 2247 | 2248 | progress@2.0.3: {} 2249 | 2250 | pump@3.0.2: 2251 | dependencies: 2252 | end-of-stream: 1.4.4 2253 | once: 1.4.0 2254 | 2255 | qs@6.14.0: 2256 | dependencies: 2257 | side-channel: 1.1.0 2258 | 2259 | quick-lru@5.1.1: {} 2260 | 2261 | readable-stream@4.7.0: 2262 | dependencies: 2263 | abort-controller: 3.0.0 2264 | buffer: 6.0.3 2265 | events: 3.3.0 2266 | process: 0.11.10 2267 | string_decoder: 1.3.0 2268 | 2269 | readable-web-to-node-stream@3.0.4: 2270 | dependencies: 2271 | readable-stream: 4.7.0 2272 | 2273 | regenerator-runtime@0.13.11: {} 2274 | 2275 | relateurl@0.2.7: {} 2276 | 2277 | require-directory@2.1.1: {} 2278 | 2279 | requires-port@1.0.0: {} 2280 | 2281 | resolve-alpn@1.2.1: {} 2282 | 2283 | resolve-pkg-maps@1.0.0: {} 2284 | 2285 | responselike@2.0.1: 2286 | dependencies: 2287 | lowercase-keys: 2.0.0 2288 | 2289 | rimraf@6.0.1: 2290 | dependencies: 2291 | glob: 11.0.2 2292 | package-json-from-dist: 1.0.1 2293 | 2294 | roarr@2.15.4: 2295 | dependencies: 2296 | boolean: 3.2.0 2297 | detect-node: 2.1.0 2298 | globalthis: 1.0.4 2299 | json-stringify-safe: 5.0.1 2300 | semver-compare: 1.0.0 2301 | sprintf-js: 1.1.3 2302 | optional: true 2303 | 2304 | rxjs@7.8.2: 2305 | dependencies: 2306 | tslib: 2.8.1 2307 | 2308 | safe-buffer@5.1.2: {} 2309 | 2310 | safe-buffer@5.2.1: {} 2311 | 2312 | safer-buffer@2.1.2: {} 2313 | 2314 | sanitize-filename@1.6.3: 2315 | dependencies: 2316 | truncate-utf8-bytes: 1.0.2 2317 | 2318 | secure-compare@3.0.1: {} 2319 | 2320 | semver-compare@1.0.0: 2321 | optional: true 2322 | 2323 | semver@6.3.1: {} 2324 | 2325 | semver@7.7.2: 2326 | optional: true 2327 | 2328 | serialize-error@7.0.1: 2329 | dependencies: 2330 | type-fest: 0.13.1 2331 | optional: true 2332 | 2333 | shazamio-core@1.3.1: {} 2334 | 2335 | shebang-command@2.0.0: 2336 | dependencies: 2337 | shebang-regex: 3.0.0 2338 | 2339 | shebang-regex@3.0.0: {} 2340 | 2341 | shell-quote@1.8.3: {} 2342 | 2343 | side-channel-list@1.0.0: 2344 | dependencies: 2345 | es-errors: 1.3.0 2346 | object-inspect: 1.13.4 2347 | 2348 | side-channel-map@1.0.1: 2349 | dependencies: 2350 | call-bound: 1.0.4 2351 | es-errors: 1.3.0 2352 | get-intrinsic: 1.3.0 2353 | object-inspect: 1.13.4 2354 | 2355 | side-channel-weakmap@1.0.2: 2356 | dependencies: 2357 | call-bound: 1.0.4 2358 | es-errors: 1.3.0 2359 | get-intrinsic: 1.3.0 2360 | object-inspect: 1.13.4 2361 | side-channel-map: 1.0.1 2362 | 2363 | side-channel@1.1.0: 2364 | dependencies: 2365 | es-errors: 1.3.0 2366 | object-inspect: 1.13.4 2367 | side-channel-list: 1.0.0 2368 | side-channel-map: 1.0.1 2369 | side-channel-weakmap: 1.0.2 2370 | 2371 | signal-exit@4.1.0: {} 2372 | 2373 | source-map-support@0.5.21: 2374 | dependencies: 2375 | buffer-from: 1.1.2 2376 | source-map: 0.6.1 2377 | 2378 | source-map@0.6.1: {} 2379 | 2380 | sprintf-js@1.1.3: 2381 | optional: true 2382 | 2383 | string-width@4.2.3: 2384 | dependencies: 2385 | emoji-regex: 8.0.0 2386 | is-fullwidth-code-point: 3.0.0 2387 | strip-ansi: 6.0.1 2388 | 2389 | string-width@5.1.2: 2390 | dependencies: 2391 | eastasianwidth: 0.2.0 2392 | emoji-regex: 9.2.2 2393 | strip-ansi: 7.1.0 2394 | 2395 | string_decoder@1.3.0: 2396 | dependencies: 2397 | safe-buffer: 5.2.1 2398 | 2399 | strip-ansi@6.0.1: 2400 | dependencies: 2401 | ansi-regex: 5.0.1 2402 | 2403 | strip-ansi@7.1.0: 2404 | dependencies: 2405 | ansi-regex: 6.1.0 2406 | 2407 | strtok3@6.3.0: 2408 | dependencies: 2409 | '@tokenizer/token': 0.3.0 2410 | peek-readable: 4.1.0 2411 | 2412 | sumchecker@3.0.1: 2413 | dependencies: 2414 | debug: 4.4.1 2415 | transitivePeerDependencies: 2416 | - supports-color 2417 | 2418 | supports-color@7.2.0: 2419 | dependencies: 2420 | has-flag: 4.0.0 2421 | 2422 | supports-color@8.1.1: 2423 | dependencies: 2424 | has-flag: 4.0.0 2425 | 2426 | terser@5.42.0: 2427 | dependencies: 2428 | '@jridgewell/source-map': 0.3.6 2429 | acorn: 8.15.0 2430 | commander: 2.20.3 2431 | source-map-support: 0.5.21 2432 | 2433 | timm@1.7.1: {} 2434 | 2435 | tinycolor2@1.6.0: {} 2436 | 2437 | token-types@4.2.1: 2438 | dependencies: 2439 | '@tokenizer/token': 0.3.0 2440 | ieee754: 1.2.1 2441 | 2442 | tr46@0.0.3: {} 2443 | 2444 | tree-kill@1.2.2: {} 2445 | 2446 | truncate-utf8-bytes@1.0.2: 2447 | dependencies: 2448 | utf8-byte-length: 1.0.5 2449 | 2450 | tslib@2.8.1: {} 2451 | 2452 | tsx@4.20.0: 2453 | dependencies: 2454 | esbuild: 0.25.5 2455 | get-tsconfig: 4.10.1 2456 | optionalDependencies: 2457 | fsevents: 2.3.3 2458 | 2459 | type-fest@0.13.1: 2460 | optional: true 2461 | 2462 | typescript@5.8.3: {} 2463 | 2464 | undici-types@5.26.5: {} 2465 | 2466 | undici-types@6.21.0: {} 2467 | 2468 | undici-types@7.8.0: {} 2469 | 2470 | undici@6.21.1: {} 2471 | 2472 | union@0.5.0: 2473 | dependencies: 2474 | qs: 6.14.0 2475 | 2476 | universalify@0.1.2: {} 2477 | 2478 | url-join@4.0.1: {} 2479 | 2480 | utf8-byte-length@1.0.5: {} 2481 | 2482 | utif2@4.1.0: 2483 | dependencies: 2484 | pako: 1.0.11 2485 | 2486 | uuid@11.1.0: {} 2487 | 2488 | webidl-conversions@3.0.1: {} 2489 | 2490 | whatwg-encoding@2.0.0: 2491 | dependencies: 2492 | iconv-lite: 0.6.3 2493 | 2494 | whatwg-fetch@3.6.20: {} 2495 | 2496 | whatwg-url@5.0.0: 2497 | dependencies: 2498 | tr46: 0.0.3 2499 | webidl-conversions: 3.0.1 2500 | 2501 | which@2.0.2: 2502 | dependencies: 2503 | isexe: 2.0.0 2504 | 2505 | wrap-ansi@7.0.0: 2506 | dependencies: 2507 | ansi-styles: 4.3.0 2508 | string-width: 4.2.3 2509 | strip-ansi: 6.0.1 2510 | 2511 | wrap-ansi@8.1.0: 2512 | dependencies: 2513 | ansi-styles: 6.2.1 2514 | string-width: 5.1.2 2515 | strip-ansi: 7.1.0 2516 | 2517 | wrappy@1.0.2: {} 2518 | 2519 | ws@8.18.2: {} 2520 | 2521 | y18n@5.0.8: {} 2522 | 2523 | yargs-parser@21.1.1: {} 2524 | 2525 | yargs@17.7.2: 2526 | dependencies: 2527 | cliui: 8.0.1 2528 | escalade: 3.2.0 2529 | get-caller-file: 2.0.5 2530 | require-directory: 2.1.1 2531 | string-width: 4.2.3 2532 | y18n: 5.0.8 2533 | yargs-parser: 21.1.1 2534 | 2535 | yauzl@2.10.0: 2536 | dependencies: 2537 | buffer-crc32: 0.2.13 2538 | fd-slicer: 1.1.0 2539 | --------------------------------------------------------------------------------