├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ ├── releaseDev.yml │ └── releaseMaster.yml ├── .gitignore ├── README.md ├── esbuild.config.ts ├── package.json ├── plugins ├── Avatar │ ├── package.json │ └── src │ │ ├── index.tsx │ │ └── md5.native.ts ├── CoverTheme │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ ├── index.tsx │ │ ├── transparent.css │ │ └── vibrant.native.ts ├── DesktopConnect │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── remoteService.native.ts ├── DiscordRPC │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ ├── discord.native.ts │ │ ├── index.ts │ │ └── updateActivity.ts ├── LastFM │ ├── package.json │ └── src │ │ ├── LastFM.ts │ │ ├── Settings.tsx │ │ ├── hash.native.ts │ │ ├── index.ts │ │ └── types │ │ ├── NowPlaying.ts │ │ └── Scrobble.ts ├── ListenBrainz │ ├── package.json │ └── src │ │ ├── ListenBrainz.ts │ │ ├── ListenBrainzTypes.ts │ │ ├── Settings.tsx │ │ ├── index.ts │ │ └── makeTrackPayload.ts ├── NativeFullscreen │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ └── index.ts ├── NoBuffer │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── voidTrack.native.ts ├── PersistSettings │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ └── index.ts ├── RealMax │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ ├── contextMenu.ts │ │ ├── index.safe.ts │ │ └── index.ts ├── Shazam │ ├── fixShazamio.js │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ ├── api.types.ts │ │ ├── index.ts │ │ └── shazam.native.ts ├── SmallWindow │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── size.native.ts ├── SongDownloader │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ ├── downloadButton.css │ │ ├── helpers.ts │ │ └── index.ts ├── Themer │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ ├── editor.html │ │ ├── editor.native.ts │ │ ├── editor.preload.js │ │ └── index.ts ├── TidalTags │ ├── package.json │ └── src │ │ ├── Settings.tsx │ │ ├── index.safe.ts │ │ ├── index.ts │ │ ├── lib │ │ ├── ensureColumnHeader.ts │ │ ├── hexToRgba.ts │ │ ├── isElement.ts │ │ └── setColumn.ts │ │ ├── setFormatInfo.ts │ │ ├── setInfoColumns.ts │ │ ├── setQualityTags.ts │ │ └── styles.css └── VolumeScroll │ ├── package.json │ └── src │ ├── Settings.tsx │ └── index.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── themes ├── blur.css └── example.css └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [inrixia] 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | cert.pem 4 | key.pem 5 | tmp/ 6 | .vscode/ 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /esbuild.config.ts: -------------------------------------------------------------------------------- 1 | import "luna/buildPlugins"; 2 | -------------------------------------------------------------------------------- /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.15.1", 23 | "@types/node": "^22.15.0", 24 | "@types/react": "^19.1.2", 25 | "@types/react-dom": "^19.1.2", 26 | "concurrently": "^9.1.2", 27 | "electron": "^36.1.0", 28 | "http-server": "^14.1.1", 29 | "luna": "github:inrixia/TidaLuna#98b7912", 30 | "oby": "^15.1.2", 31 | "rimraf": "^6.0.1", 32 | "tsx": "^4.19.3", 33 | "typescript": "^5.8.3" 34 | } 35 | } -------------------------------------------------------------------------------- /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/Avatar/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { LunaUnload, ReactiveStore } from "@luna/core"; 2 | import { redux, 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 | export const Settings = () => { 15 | const [customUrl, setCustomUrl] = React.useState(settings.customUrl); 16 | 17 | React.useEffect(() => { 18 | if (customUrl === "" || customUrl === undefined) { 19 | md5(redux.store.getState().user.meta.email).then((emailHash) => { 20 | setCustomUrl(`https://www.gravatar.com/avatar/${emailHash}?d=identicon`); 21 | }); 22 | } else { 23 | // Thx @n1ckoates 24 | avatarCSS.css = ` 25 | [class^="_profilePicture_"] { 26 | background-image: url("${customUrl}"); 27 | background-size: cover; 28 | } 29 | 30 | [class^="_profilePicture_"] svg { 31 | display: none; 32 | } 33 | `; 34 | } 35 | }, [customUrl]); 36 | 37 | return ( 38 | 39 | setCustomUrl((settings.customUrl = e.target.value))} 44 | /> 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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.2.1" 13 | } 14 | } -------------------------------------------------------------------------------- /plugins/DiscordRPC/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { 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 | }); 13 | 14 | export const Settings = () => { 15 | const [displayOnPause, setDisplayOnPause] = React.useState(settings.displayOnPause); 16 | const [displayArtistIcon, setDisplayArtistIcon] = React.useState(settings.displayArtistIcon); 17 | 18 | return ( 19 | 20 | { 26 | setDisplayOnPause((settings.displayOnPause = checked)); 27 | updateActivity() 28 | .then(() => (errSignal!._ = undefined)) 29 | .catch(trace.err.withContext("Failed to set activity")); 30 | }} 31 | /> 32 | { 38 | setDisplayOnPause((settings.displayArtistIcon = checked)); 39 | updateActivity() 40 | .then(() => (errSignal!._ = undefined)) 41 | .catch(trace.err.withContext("Failed to set activity")); 42 | }} 43 | /> 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/DiscordRPC/src/updateActivity.ts: -------------------------------------------------------------------------------- 1 | import { asyncDebounce } from "@inrixia/helpers"; 2 | import { MediaItem, PlayState } 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 activity: SetActivity = { type: 2 }; // Listening type 22 | 23 | activity.buttons = [ 24 | { 25 | url: `https://tidal.com/browse/${mediaItem.tidalItem.contentType}/${mediaItem.id}?u`, 26 | label: "Play Song", 27 | }, 28 | ]; 29 | 30 | // Title 31 | activity.details = await mediaItem.title().then(fmtStr); 32 | // Artists 33 | const artistNames = await MediaItem.artistNames(await mediaItem.artists()); 34 | activity.state = fmtStr(artistNames.join(", ")) ?? "Unknown Artist"; 35 | 36 | // Pause indicator 37 | if (PlayState.playing) { 38 | // Small Artist image 39 | if (settings.displayArtistIcon) { 40 | const artist = await mediaItem.artist(); 41 | activity.smallImageKey = artist?.coverUrl("320"); 42 | activity.smallImageText = fmtStr(artist?.name); 43 | } 44 | 45 | // Playback/Time 46 | if (mediaItem.duration !== undefined) { 47 | activity.startTimestamp = Date.now() - PlayState.playTime * 1000; 48 | activity.endTimestamp = activity.startTimestamp + mediaItem.duration * 1000; 49 | } 50 | } else { 51 | activity.smallImageKey = "paused-icon"; 52 | activity.smallImageText = "Paused"; 53 | activity.endTimestamp = Date.now(); 54 | } 55 | 56 | // Album 57 | const album = await mediaItem.album(); 58 | if (album) { 59 | activity.largeImageKey = album.coverUrl(); 60 | activity.largeImageText = await album.title().then(fmtStr); 61 | } 62 | 63 | await setActivity(activity); 64 | }, true); 65 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | ContextMenu.onMediaItem(unloads, async ({ mediaCollection, contextMenu }) => { 9 | const itemCount = await mediaCollection.count(); 10 | if (itemCount === 0) return; 11 | 12 | const defaultText = `RealMAX ${itemCount} tracks`; 13 | 14 | const maxButton = contextMenu.addButton(defaultText, async () => { 15 | let trackIds: redux.ItemId[] = []; 16 | const sourceTitle = await mediaCollection.title(); 17 | maxButton.innerText = `RealMAX Loading...`; 18 | 19 | try { 20 | let maxItems = 0; 21 | for await (const mediaItem of await mediaCollection.mediaItems()) { 22 | const maxItem = await mediaItem.max(); 23 | maxButton.innerText = `RealMAX ${trackIds.length}/${itemCount} done. Found ${maxItems} replacements`; 24 | if (maxItem === undefined) { 25 | trackIds.push(mediaItem.id); 26 | continue; 27 | } 28 | trackIds.push(maxItem.id); 29 | maxItems++; 30 | trace.msg.log(`Found Max replacement for ${maxItem.tidalItem.title}!`); 31 | } 32 | if (trackIds.length !== itemCount) { 33 | return trace.msg.err(`Failed to create playlist "${sourceTitle}" item count mismatch ${trackIds.length} != ${itemCount}`); 34 | } 35 | 36 | if (maxItems === 0) { 37 | return trace.msg.err(`No replacements found for ${sourceTitle}`); 38 | } 39 | 40 | maxButton.innerText = `RealMAX Creating playlist...`; 41 | const { playlist } = await redux.interceptActionResp( 42 | () => 43 | redux.actions["folders/CREATE_PLAYLIST"]({ 44 | description: "Automatically generated by RealMAX", 45 | folderId: "root", 46 | fromPlaylist: undefined, 47 | title: `[RealMAX] ${sourceTitle}`, 48 | ids: trackIds.length > maxNewPlaylistSize ? undefined : trackIds, 49 | }), 50 | unloads, 51 | ["content/LOAD_PLAYLIST_SUCCESS"], 52 | ["content/LOAD_PLAYLIST_FAIL"] 53 | ); 54 | if (trackIds.length > maxNewPlaylistSize) { 55 | for (const trackIdsChunk of chunkArray(trackIds, maxNewPlaylistSize)) { 56 | await redux.interceptActionResp( 57 | () => 58 | redux.actions["content/ADD_MEDIA_ITEMS_TO_PLAYLIST"]({ 59 | addToIndex: -1, 60 | mediaItemIdsToAdd: trackIdsChunk, 61 | onDupes: "ADD", 62 | playlistUUID: playlist.uuid!, 63 | }), 64 | unloads, 65 | ["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_SUCCESS"], 66 | ["content/ADD_MEDIA_ITEMS_TO_PLAYLIST_FAIL"] 67 | ); 68 | } 69 | } 70 | if (playlist?.uuid === undefined) { 71 | return trace.msg.err(`Failed to create playlist "${sourceTitle}"`); 72 | } 73 | trace.msg.log(`Created playlist "${sourceTitle}" with ${maxItems} replacements!`); 74 | } finally { 75 | maxButton.innerText = defaultText; 76 | } 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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 | } -------------------------------------------------------------------------------- /plugins/SongDownloader/src/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactiveStore } from "@luna/core"; 2 | import { MediaItem, Quality } 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: string; 12 | defaultPath?: string; 13 | pathFormat: string; 14 | useRealMAX: boolean; 15 | }; 16 | export const settings = await ReactiveStore.getPluginStorage("SongDownloader", { 17 | downloadQuality: Quality.Max.name, 18 | pathFormat: defaultFilenameFormat, 19 | useRealMAX: true, 20 | }); 21 | 22 | export const Settings = () => { 23 | const [downloadQuality, setDownloadQuality] = React.useState(settings.downloadQuality); 24 | const [defaultPath, setDefaultPath] = React.useState(settings.defaultPath); 25 | const [pathFormat, setPathFormat] = React.useState(settings.pathFormat); 26 | const [useRealMAX, setUseRealMAX] = React.useState(settings.useRealMAX); 27 | 28 | return ( 29 | 30 | setDownloadQuality((settings.downloadQuality = e.target.value))} 34 | > 35 | {Object.values(Quality.lookups.audioQuality).map((quality) => { 36 | if (typeof quality !== "string") return ; 37 | })} 38 | 39 | setUseRealMAX((settings.useRealMAX = checked))} 43 | /> 44 | 48 | Set a default folder to save files to (will disable prompting for path on download) 49 | {defaultPath && ( 50 | <> 51 |
52 | Using {defaultPath} 53 | 54 | )} 55 | 56 | } 57 | children={defaultPath === undefined ? "Set default folder" : "Clear default folder"} 58 | onClick={async () => { 59 | if (defaultPath !== undefined) return setDefaultPath((settings.defaultPath = undefined)); 60 | setDefaultPath((settings.defaultPath = await getDownloadFolder())); 61 | }} 62 | /> 63 | 67 | Define subfolders using /. 68 |
69 | For example: {"{artist}/{album}/{title}"} 70 |
71 | Saves in subfolder artist/album/ named title.flac. 72 |
73 | You can use the following tags: 74 |
    75 | {MediaItem.availableTags.map((tag) => ( 76 |
  • {tag}
  • 77 | ))} 78 |
79 | 80 | } 81 | value={pathFormat} 82 | onChange={(e) => setPathFormat((settings.pathFormat = e.target.value))} 83 | /> 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /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/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 | export const getDownloadFolder = async () => { 6 | const { canceled, filePaths } = await showOpenDialog({ properties: ["openDirectory", "createDirectory"] }); 7 | if (!canceled) return filePaths[0]; 8 | }; 9 | export const getDownloadPath = async (defaultPath: string) => { 10 | const { canceled, filePath } = await showSaveDialog({ 11 | defaultPath, 12 | filters: [{ name: "", extensions: [defaultPath ?? "*"] }], 13 | }); 14 | if (!canceled) return filePath; 15 | }; 16 | export const getFileName = async (mediaItem: MediaItem) => { 17 | let fileName = `${settings.pathFormat}.${await mediaItem.fileExtension()}`; 18 | const { tags } = await mediaItem.flacTags(); 19 | for (const tag of MediaItem.availableTags) { 20 | let tagValue = tags[tag]; 21 | if (Array.isArray(tagValue)) tagValue = tagValue[0]; 22 | if (tagValue === undefined) continue; 23 | fileName = fileName.replaceAll(`{${tag}}`, tagValue); 24 | } 25 | return fileName; 26 | }; 27 | -------------------------------------------------------------------------------- /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 | export { Settings } from "./Settings"; 17 | ContextMenu.onMediaItem(unloads, async ({ mediaCollection, contextMenu }) => { 18 | const trackCount = await mediaCollection.count(); 19 | if (trackCount === 0) return; 20 | 21 | const defaultText = `Download ${trackCount} tracks`; 22 | const downloadButton = contextMenu.addButton(defaultText, async () => { 23 | const downloadFolder = settings.defaultPath ?? (trackCount > 1 ? await getDownloadFolder() : undefined); 24 | downloadButton.classList.add("download-button"); 25 | for await (let mediaItem of await mediaCollection.mediaItems()) { 26 | if (settings.useRealMAX) { 27 | downloadButton.innerText = `Checking RealMax...`; 28 | mediaItem = (await mediaItem.max()) ?? mediaItem; 29 | } 30 | 31 | downloadButton.innerText = `Loading tags...`; 32 | await mediaItem.flacTags(); 33 | 34 | downloadButton.innerText = `Fetching filename...`; 35 | const fileName = await getFileName(mediaItem); 36 | 37 | downloadButton.innerText = `Fetching download path...`; 38 | const path = downloadFolder !== undefined ? join(downloadFolder, fileName) : await getDownloadPath(fileName); 39 | if (path === undefined) return; 40 | 41 | downloadButton.innerText = `Downloading...`; 42 | const clearInterval = safeInterval( 43 | unloads, 44 | async () => { 45 | const progress = await mediaItem.downloadProgress(); 46 | if (progress === undefined) return; 47 | const { total, downloaded } = progress; 48 | if (total === undefined || downloaded === undefined) return; 49 | const percent = (downloaded / total) * 100; 50 | downloadButton.style.setProperty("--progress", `${percent}%`); 51 | const downloadedMB = (downloaded / 1048576).toFixed(0); 52 | const totalMB = (total / 1048576).toFixed(0); 53 | downloadButton.innerText = `Downloading... ${downloadedMB}/${totalMB}MB ${percent.toFixed(0)}%`; 54 | }, 55 | 50 56 | ); 57 | await mediaItem.download(path); 58 | clearInterval(); 59 | } 60 | downloadButton.innerText = defaultText; 61 | downloadButton.classList.remove("download-button"); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | }); 12 | 13 | export const Settings = () => { 14 | const [displayFormatBorder, setDisplayFormatBorder] = React.useState(settings.displayFormatBorder); 15 | const [displayQalityTags, setDisplayQalityTags] = React.useState(settings.displayQalityTags); 16 | const [displayFormatColumns, setDisplayFormatColumns] = React.useState(settings.displayFormatColumns); 17 | return ( 18 | 19 | { 24 | setDisplayFormatBorder((settings.displayFormatBorder = checked)); 25 | if (!checked) formatInfoElem.style.border = "none"; 26 | else MediaItem.fromPlaybackContext().then(setFormatInfo); 27 | }} 28 | /> 29 | { 34 | setDisplayQalityTags((settings.displayQalityTags = checked)); 35 | }} 36 | /> 37 | { 42 | setDisplayFormatColumns((settings.displayFormatColumns = checked)); 43 | }} 44 | /> 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /plugins/TidalTags/src/index.safe.ts: -------------------------------------------------------------------------------- 1 | import type { LunaUnload } from "@luna/core"; 2 | 3 | export const unloads = new Set(); 4 | -------------------------------------------------------------------------------- /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) setFormatColumns(trackRow, mediaItem); 24 | }); 25 | 26 | MediaItem.onMediaTransition(unloads, setFormatInfo); 27 | MediaItem.fromPlaybackContext().then(setFormatInfo); 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/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/src/lib/isElement.ts: -------------------------------------------------------------------------------- 1 | export const isElement = (node: Node | undefined): node is Element => node?.nodeType === Node.ELEMENT_NODE; 2 | -------------------------------------------------------------------------------- /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/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 | 49 | formatUnload?.(); 50 | formatUnload = mediaItem.withFormat(unloads, audioQuality, ({ sampleRate, bitDepth, bitrate }) => { 51 | formatInfoElem.textContent = ""; 52 | if (!!sampleRate) formatInfoElem.textContent += `${sampleRate / 1000}kHz `; 53 | if (!!bitDepth) formatInfoElem.textContent += `${bitDepth}bit `; 54 | if (!!bitrate) formatInfoElem.textContent += `${Math.floor(bitrate / 1000).toLocaleString()}kb/s`; 55 | if (formatInfoElem.textContent === "") formatInfoElem.textContent = "Unknown"; 56 | }); 57 | 58 | try { 59 | await mediaItem.updateFormat(); 60 | } catch (err) { 61 | formatInfoElem.style.border = "solid 1px red"; 62 | const errorText = (err).message.substring(0, 64); 63 | formatInfoElem.textContent = errorText; 64 | throw err; 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 | 7 | export const setInfoColumnHeaders = debounce(() => { 8 | for (const trackList of document.querySelectorAll(`div[aria-label="Tracklist"]`)) { 9 | const bitDepthColumn = ensureColumnHeader( 10 | trackList, 11 | "Depth", 12 | `span[class^="_timeColumn"][role="columnheader"]`, 13 | `span[class^="_timeColumn"][role="columnheader"]` 14 | ); 15 | bitDepthColumn?.style.setProperty("min-width", "40px"); 16 | const sampleRateColumn = ensureColumnHeader(trackList, "Sample Rate", `span[class^="_timeColumn"][role="columnheader"]`, bitDepthColumn); 17 | sampleRateColumn?.style.setProperty("min-width", "110px"); 18 | const bitrateColumn = ensureColumnHeader(trackList, "Bitrate", `span[class^="_timeColumn"][role="columnheader"]`, sampleRateColumn); 19 | bitrateColumn?.style.setProperty("min-width", "100px"); 20 | } 21 | }, 50); 22 | 23 | export const setInfoColumns = (trackRow: Element, mediaItem: MediaItem) => { 24 | setInfoColumnHeaders(); 25 | const bitDepthContent = document.createElement("span"); 26 | const bitDepthColumn = setColumn(trackRow, "Depth", `div[data-test="duration"]`, bitDepthContent, `div[data-test="duration"]`); 27 | bitDepthColumn?.style.setProperty("min-width", "40px"); 28 | 29 | const sampleRateContent = document.createElement("span"); 30 | const sampleRateColumn = setColumn(trackRow, "Sample Rate", `div[data-test="duration"]`, sampleRateContent, bitDepthColumn); 31 | sampleRateColumn?.style.setProperty("min-width", "110px"); 32 | 33 | const bitrateContent = document.createElement("span"); 34 | const bitrateColumn = setColumn(trackRow, "Bitrate", `div[data-test="duration"]`, bitrateContent, sampleRateColumn); 35 | bitrateColumn?.style.setProperty("min-width", "100px"); 36 | 37 | bitDepthContent.style.color = sampleRateContent.style.color = bitrateContent.style.color = mediaItem.bestQuality.color; 38 | 39 | mediaItem.withFormat(unloads, mediaItem.bestQuality.audioQuality, ({ sampleRate, bitDepth, bitrate }) => { 40 | if (!!sampleRate) sampleRateContent.textContent = `${sampleRate / 1000}kHz`; 41 | if (!!bitDepth) bitDepthContent.textContent = `${bitDepth}bit`; 42 | if (!!bitrate) bitrateContent.textContent = `${Math.floor(bitrate / 1000).toLocaleString()}kbps`; 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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.15.1 13 | version: 3.15.1 14 | '@types/node': 15 | specifier: ^22.15.0 16 | version: 22.15.0 17 | '@types/react': 18 | specifier: ^19.1.2 19 | version: 19.1.2 20 | '@types/react-dom': 21 | specifier: ^19.1.2 22 | version: 19.1.2(@types/react@19.1.2) 23 | concurrently: 24 | specifier: ^9.1.2 25 | version: 9.1.2 26 | electron: 27 | specifier: ^36.1.0 28 | version: 36.1.0 29 | http-server: 30 | specifier: ^14.1.1 31 | version: 14.1.1 32 | luna: 33 | specifier: github:inrixia/TidaLuna#98b7912 34 | version: https://codeload.github.com/inrixia/TidaLuna/tar.gz/98b7912 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.19.3 43 | version: 4.19.3 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.1 61 | 62 | plugins/DiscordRPC: 63 | dependencies: 64 | '@xhayper/discord-rpc': 65 | specifier: ^1.2.1 66 | version: 1.2.1 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 | 97 | plugins/Themer: {} 98 | 99 | plugins/VolumeScroll: {} 100 | 101 | packages: 102 | 103 | '@discordjs/collection@2.1.1': 104 | resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} 105 | engines: {node: '>=18'} 106 | 107 | '@discordjs/rest@2.5.0': 108 | resolution: {integrity: sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==} 109 | engines: {node: '>=18'} 110 | 111 | '@discordjs/util@1.1.1': 112 | resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} 113 | engines: {node: '>=18'} 114 | 115 | '@electron/get@2.0.3': 116 | resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} 117 | engines: {node: '>=12'} 118 | 119 | '@esbuild/aix-ppc64@0.25.3': 120 | resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} 121 | engines: {node: '>=18'} 122 | cpu: [ppc64] 123 | os: [aix] 124 | 125 | '@esbuild/aix-ppc64@0.25.4': 126 | resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} 127 | engines: {node: '>=18'} 128 | cpu: [ppc64] 129 | os: [aix] 130 | 131 | '@esbuild/android-arm64@0.25.3': 132 | resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} 133 | engines: {node: '>=18'} 134 | cpu: [arm64] 135 | os: [android] 136 | 137 | '@esbuild/android-arm64@0.25.4': 138 | resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} 139 | engines: {node: '>=18'} 140 | cpu: [arm64] 141 | os: [android] 142 | 143 | '@esbuild/android-arm@0.25.3': 144 | resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} 145 | engines: {node: '>=18'} 146 | cpu: [arm] 147 | os: [android] 148 | 149 | '@esbuild/android-arm@0.25.4': 150 | resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} 151 | engines: {node: '>=18'} 152 | cpu: [arm] 153 | os: [android] 154 | 155 | '@esbuild/android-x64@0.25.3': 156 | resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} 157 | engines: {node: '>=18'} 158 | cpu: [x64] 159 | os: [android] 160 | 161 | '@esbuild/android-x64@0.25.4': 162 | resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} 163 | engines: {node: '>=18'} 164 | cpu: [x64] 165 | os: [android] 166 | 167 | '@esbuild/darwin-arm64@0.25.3': 168 | resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} 169 | engines: {node: '>=18'} 170 | cpu: [arm64] 171 | os: [darwin] 172 | 173 | '@esbuild/darwin-arm64@0.25.4': 174 | resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} 175 | engines: {node: '>=18'} 176 | cpu: [arm64] 177 | os: [darwin] 178 | 179 | '@esbuild/darwin-x64@0.25.3': 180 | resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} 181 | engines: {node: '>=18'} 182 | cpu: [x64] 183 | os: [darwin] 184 | 185 | '@esbuild/darwin-x64@0.25.4': 186 | resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} 187 | engines: {node: '>=18'} 188 | cpu: [x64] 189 | os: [darwin] 190 | 191 | '@esbuild/freebsd-arm64@0.25.3': 192 | resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} 193 | engines: {node: '>=18'} 194 | cpu: [arm64] 195 | os: [freebsd] 196 | 197 | '@esbuild/freebsd-arm64@0.25.4': 198 | resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} 199 | engines: {node: '>=18'} 200 | cpu: [arm64] 201 | os: [freebsd] 202 | 203 | '@esbuild/freebsd-x64@0.25.3': 204 | resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} 205 | engines: {node: '>=18'} 206 | cpu: [x64] 207 | os: [freebsd] 208 | 209 | '@esbuild/freebsd-x64@0.25.4': 210 | resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} 211 | engines: {node: '>=18'} 212 | cpu: [x64] 213 | os: [freebsd] 214 | 215 | '@esbuild/linux-arm64@0.25.3': 216 | resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} 217 | engines: {node: '>=18'} 218 | cpu: [arm64] 219 | os: [linux] 220 | 221 | '@esbuild/linux-arm64@0.25.4': 222 | resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} 223 | engines: {node: '>=18'} 224 | cpu: [arm64] 225 | os: [linux] 226 | 227 | '@esbuild/linux-arm@0.25.3': 228 | resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} 229 | engines: {node: '>=18'} 230 | cpu: [arm] 231 | os: [linux] 232 | 233 | '@esbuild/linux-arm@0.25.4': 234 | resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} 235 | engines: {node: '>=18'} 236 | cpu: [arm] 237 | os: [linux] 238 | 239 | '@esbuild/linux-ia32@0.25.3': 240 | resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} 241 | engines: {node: '>=18'} 242 | cpu: [ia32] 243 | os: [linux] 244 | 245 | '@esbuild/linux-ia32@0.25.4': 246 | resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} 247 | engines: {node: '>=18'} 248 | cpu: [ia32] 249 | os: [linux] 250 | 251 | '@esbuild/linux-loong64@0.25.3': 252 | resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} 253 | engines: {node: '>=18'} 254 | cpu: [loong64] 255 | os: [linux] 256 | 257 | '@esbuild/linux-loong64@0.25.4': 258 | resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} 259 | engines: {node: '>=18'} 260 | cpu: [loong64] 261 | os: [linux] 262 | 263 | '@esbuild/linux-mips64el@0.25.3': 264 | resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} 265 | engines: {node: '>=18'} 266 | cpu: [mips64el] 267 | os: [linux] 268 | 269 | '@esbuild/linux-mips64el@0.25.4': 270 | resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} 271 | engines: {node: '>=18'} 272 | cpu: [mips64el] 273 | os: [linux] 274 | 275 | '@esbuild/linux-ppc64@0.25.3': 276 | resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} 277 | engines: {node: '>=18'} 278 | cpu: [ppc64] 279 | os: [linux] 280 | 281 | '@esbuild/linux-ppc64@0.25.4': 282 | resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} 283 | engines: {node: '>=18'} 284 | cpu: [ppc64] 285 | os: [linux] 286 | 287 | '@esbuild/linux-riscv64@0.25.3': 288 | resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} 289 | engines: {node: '>=18'} 290 | cpu: [riscv64] 291 | os: [linux] 292 | 293 | '@esbuild/linux-riscv64@0.25.4': 294 | resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} 295 | engines: {node: '>=18'} 296 | cpu: [riscv64] 297 | os: [linux] 298 | 299 | '@esbuild/linux-s390x@0.25.3': 300 | resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} 301 | engines: {node: '>=18'} 302 | cpu: [s390x] 303 | os: [linux] 304 | 305 | '@esbuild/linux-s390x@0.25.4': 306 | resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} 307 | engines: {node: '>=18'} 308 | cpu: [s390x] 309 | os: [linux] 310 | 311 | '@esbuild/linux-x64@0.25.3': 312 | resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} 313 | engines: {node: '>=18'} 314 | cpu: [x64] 315 | os: [linux] 316 | 317 | '@esbuild/linux-x64@0.25.4': 318 | resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} 319 | engines: {node: '>=18'} 320 | cpu: [x64] 321 | os: [linux] 322 | 323 | '@esbuild/netbsd-arm64@0.25.3': 324 | resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} 325 | engines: {node: '>=18'} 326 | cpu: [arm64] 327 | os: [netbsd] 328 | 329 | '@esbuild/netbsd-arm64@0.25.4': 330 | resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} 331 | engines: {node: '>=18'} 332 | cpu: [arm64] 333 | os: [netbsd] 334 | 335 | '@esbuild/netbsd-x64@0.25.3': 336 | resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} 337 | engines: {node: '>=18'} 338 | cpu: [x64] 339 | os: [netbsd] 340 | 341 | '@esbuild/netbsd-x64@0.25.4': 342 | resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} 343 | engines: {node: '>=18'} 344 | cpu: [x64] 345 | os: [netbsd] 346 | 347 | '@esbuild/openbsd-arm64@0.25.3': 348 | resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} 349 | engines: {node: '>=18'} 350 | cpu: [arm64] 351 | os: [openbsd] 352 | 353 | '@esbuild/openbsd-arm64@0.25.4': 354 | resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} 355 | engines: {node: '>=18'} 356 | cpu: [arm64] 357 | os: [openbsd] 358 | 359 | '@esbuild/openbsd-x64@0.25.3': 360 | resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} 361 | engines: {node: '>=18'} 362 | cpu: [x64] 363 | os: [openbsd] 364 | 365 | '@esbuild/openbsd-x64@0.25.4': 366 | resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} 367 | engines: {node: '>=18'} 368 | cpu: [x64] 369 | os: [openbsd] 370 | 371 | '@esbuild/sunos-x64@0.25.3': 372 | resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} 373 | engines: {node: '>=18'} 374 | cpu: [x64] 375 | os: [sunos] 376 | 377 | '@esbuild/sunos-x64@0.25.4': 378 | resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} 379 | engines: {node: '>=18'} 380 | cpu: [x64] 381 | os: [sunos] 382 | 383 | '@esbuild/win32-arm64@0.25.3': 384 | resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} 385 | engines: {node: '>=18'} 386 | cpu: [arm64] 387 | os: [win32] 388 | 389 | '@esbuild/win32-arm64@0.25.4': 390 | resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} 391 | engines: {node: '>=18'} 392 | cpu: [arm64] 393 | os: [win32] 394 | 395 | '@esbuild/win32-ia32@0.25.3': 396 | resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} 397 | engines: {node: '>=18'} 398 | cpu: [ia32] 399 | os: [win32] 400 | 401 | '@esbuild/win32-ia32@0.25.4': 402 | resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} 403 | engines: {node: '>=18'} 404 | cpu: [ia32] 405 | os: [win32] 406 | 407 | '@esbuild/win32-x64@0.25.3': 408 | resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} 409 | engines: {node: '>=18'} 410 | cpu: [x64] 411 | os: [win32] 412 | 413 | '@esbuild/win32-x64@0.25.4': 414 | resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} 415 | engines: {node: '>=18'} 416 | cpu: [x64] 417 | os: [win32] 418 | 419 | '@homebridge/ciao@1.3.1': 420 | resolution: {integrity: sha512-87tQCBNNnTymlbg8pKlQjRsk7a5uuqhWBpCbUriVYUebz3voJkLbbTmp0TQg7Sa6Jnpk/Uo6LA8zAOy2sbK9bw==} 421 | engines: {node: ^18 || ^20 || ^22} 422 | hasBin: true 423 | 424 | '@inrixia/helpers@3.15.1': 425 | resolution: {integrity: sha512-Z58SoDwhmAyJnbPvL7xHjiZb7zz1vpwAihgMhS0mZ5R0Du4PaS4MNhe9qFM8LwePxLMsCUY8wqTfgSpD3Offtw==} 426 | 427 | '@inrixia/helpers@3.20.0': 428 | resolution: {integrity: sha512-RkWZ8ZIdB3cpQvTcLsY3A8ux+4Q8PNZU0uDKJT2xVMAP0FvCsLNSJemRz9vlDalWBDkBhI9v0O/br3cF9vPnWw==} 429 | 430 | '@isaacs/cliui@8.0.2': 431 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 432 | engines: {node: '>=12'} 433 | 434 | '@jimp/bmp@0.22.12': 435 | resolution: {integrity: sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==} 436 | peerDependencies: 437 | '@jimp/custom': '>=0.3.5' 438 | 439 | '@jimp/core@0.22.12': 440 | resolution: {integrity: sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==} 441 | 442 | '@jimp/custom@0.22.12': 443 | resolution: {integrity: sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==} 444 | 445 | '@jimp/gif@0.22.12': 446 | resolution: {integrity: sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==} 447 | peerDependencies: 448 | '@jimp/custom': '>=0.3.5' 449 | 450 | '@jimp/jpeg@0.22.12': 451 | resolution: {integrity: sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==} 452 | peerDependencies: 453 | '@jimp/custom': '>=0.3.5' 454 | 455 | '@jimp/plugin-resize@0.22.12': 456 | resolution: {integrity: sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==} 457 | peerDependencies: 458 | '@jimp/custom': '>=0.3.5' 459 | 460 | '@jimp/png@0.22.12': 461 | resolution: {integrity: sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==} 462 | peerDependencies: 463 | '@jimp/custom': '>=0.3.5' 464 | 465 | '@jimp/tiff@0.22.12': 466 | resolution: {integrity: sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==} 467 | peerDependencies: 468 | '@jimp/custom': '>=0.3.5' 469 | 470 | '@jimp/types@0.22.12': 471 | resolution: {integrity: sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==} 472 | peerDependencies: 473 | '@jimp/custom': '>=0.3.5' 474 | 475 | '@jimp/utils@0.22.12': 476 | resolution: {integrity: sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==} 477 | 478 | '@jridgewell/gen-mapping@0.3.8': 479 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 480 | engines: {node: '>=6.0.0'} 481 | 482 | '@jridgewell/resolve-uri@3.1.2': 483 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 484 | engines: {node: '>=6.0.0'} 485 | 486 | '@jridgewell/set-array@1.2.1': 487 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 488 | engines: {node: '>=6.0.0'} 489 | 490 | '@jridgewell/source-map@0.3.6': 491 | resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} 492 | 493 | '@jridgewell/sourcemap-codec@1.5.0': 494 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 495 | 496 | '@jridgewell/trace-mapping@0.3.25': 497 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 498 | 499 | '@sapphire/async-queue@1.5.5': 500 | resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} 501 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 502 | 503 | '@sapphire/snowflake@3.5.5': 504 | resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==} 505 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 506 | 507 | '@sindresorhus/is@4.6.0': 508 | resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} 509 | engines: {node: '>=10'} 510 | 511 | '@szmarczak/http-timer@4.0.6': 512 | resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} 513 | engines: {node: '>=10'} 514 | 515 | '@tokenizer/token@0.3.0': 516 | resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} 517 | 518 | '@types/cacheable-request@6.0.3': 519 | resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} 520 | 521 | '@types/clean-css@4.2.11': 522 | resolution: {integrity: sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==} 523 | 524 | '@types/html-minifier-terser@7.0.2': 525 | resolution: {integrity: sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==} 526 | 527 | '@types/http-cache-semantics@4.0.4': 528 | resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} 529 | 530 | '@types/keyv@3.1.4': 531 | resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} 532 | 533 | '@types/node@16.9.1': 534 | resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} 535 | 536 | '@types/node@18.19.86': 537 | resolution: {integrity: sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==} 538 | 539 | '@types/node@22.15.0': 540 | resolution: {integrity: sha512-99S8dWD2DkeE6PBaEDw+In3aar7hdoBvjyJMR6vaKBTzpvR0P00ClzJMOoVrj9D2+Sy/YCwACYHnBTpMhg1UCA==} 541 | 542 | '@types/node@22.15.17': 543 | resolution: {integrity: sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==} 544 | 545 | '@types/react-dom@19.1.2': 546 | resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} 547 | peerDependencies: 548 | '@types/react': ^19.0.0 549 | 550 | '@types/react@19.1.2': 551 | resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==} 552 | 553 | '@types/responselike@1.0.3': 554 | resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} 555 | 556 | '@types/uuid@10.0.0': 557 | resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} 558 | 559 | '@types/yauzl@2.10.3': 560 | resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} 561 | 562 | '@vibrant/color@4.0.0': 563 | resolution: {integrity: sha512-S9ItdqS1135wTXoIIqAJu8df9dqlOo6Boc5Y4MGsBTu9UmUOvOwfj5b4Ga6S5yrLAKmKYIactkz7zYJdMddkig==} 564 | 565 | '@vibrant/core@4.0.0': 566 | resolution: {integrity: sha512-fqlVRUTDjEws9VNKvI3cDXM4wUT7fMFS+cVqEjJk3im+R5EvjJzPF6OAbNhfPzW04NvHNE555eY9FfhYuX3PRw==} 567 | 568 | '@vibrant/generator-default@4.0.3': 569 | resolution: {integrity: sha512-HZlfp19sDokODEkZF4p70QceARHgjP3a1Dmxg+dlblYMJM98jPq+azA0fzqKNR7R17JJNHxexpJEepEsNlG0gw==} 570 | 571 | '@vibrant/generator@4.0.0': 572 | resolution: {integrity: sha512-CqKAjmgHVDXJVo3Q5+9pUJOvksR7cN3bzx/6MbURYh7lA4rhsIewkUK155M6q0vfcUN3ETi/eTneCi0tLuM2Sg==} 573 | 574 | '@vibrant/image-browser@4.0.0': 575 | resolution: {integrity: sha512-mXckzvJWiP575Y/wNtP87W/TPgyJoGlPBjW4E9YmNS6n4Jb6RqyHQA0ZVulqDslOxjSsihDzY7gpAORRclaoLg==} 576 | 577 | '@vibrant/image-node@4.0.0': 578 | resolution: {integrity: sha512-m7yfnQtmo2y8z+tOjRFBx6q/qGnhl/ax2uCaj4TBkm4TtXfR4Dsn90wT6OWXmCFFzxIKHXKKEBShkxR+4RHseA==} 579 | 580 | '@vibrant/image@4.0.0': 581 | resolution: {integrity: sha512-Asv/7R/L701norosgvbjOVkodFiwcFihkXixA/gbAd6C+5GCts1Wm1NPk14FNKnM7eKkfAN+0wwPkdOH+PY/YA==} 582 | 583 | '@vibrant/quantizer-mmcq@4.0.0': 584 | resolution: {integrity: sha512-TZqNiRoGGyCP8fH1XE6rvhFwLNv9D8MP1Xhz3K8tsuUweC6buWax3qLfrfEnkhtQnPJHaqvTfTOlIIXVMfRpow==} 585 | 586 | '@vibrant/quantizer@4.0.0': 587 | resolution: {integrity: sha512-YDGxmCv/RvHFtZghDlVRwH5GMxdGGozWS1JpUOUt73/F5zAKGiiier8F31K1npIXARn6/Gspvg/Rbg7qqyEr2A==} 588 | 589 | '@vibrant/types@4.0.0': 590 | resolution: {integrity: sha512-tA5TAbuROXcPkt+PWjmGfoaiEXyySVaNnCZovf6vXhCbMdrTTCQXvNCde2geiVl6YwtuU/Qrj9iZxS5jZ6yVIw==} 591 | 592 | '@vibrant/worker@4.0.0': 593 | resolution: {integrity: sha512-nSaZZwWQKOgN/nPYUAIRF0/uoa7KpK91A+gjLmZZDgfN1enqxaiihmn+75ayNadW0c6cxAEpEFEHTONR5u9tMw==} 594 | 595 | '@vladfrangu/async_event_emitter@2.4.6': 596 | resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} 597 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 598 | 599 | '@xhayper/discord-rpc@1.2.1': 600 | resolution: {integrity: sha512-Ch04/7hq0nfV47nJzDcLIKx0SLUcPOMlkYV43faWpKtEO9SgLrTD4FAOMBBT+JORceQytnzBMPvktW2q9ZCMiw==} 601 | engines: {node: '>=18.20.7'} 602 | 603 | abort-controller@3.0.0: 604 | resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 605 | engines: {node: '>=6.5'} 606 | 607 | acorn@8.14.1: 608 | resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} 609 | engines: {node: '>=0.4.0'} 610 | hasBin: true 611 | 612 | ansi-regex@5.0.1: 613 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 614 | engines: {node: '>=8'} 615 | 616 | ansi-regex@6.1.0: 617 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 618 | engines: {node: '>=12'} 619 | 620 | ansi-styles@4.3.0: 621 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 622 | engines: {node: '>=8'} 623 | 624 | ansi-styles@6.2.1: 625 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 626 | engines: {node: '>=12'} 627 | 628 | any-base@1.1.0: 629 | resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} 630 | 631 | async@3.2.6: 632 | resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} 633 | 634 | balanced-match@1.0.2: 635 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 636 | 637 | base64-js@1.5.1: 638 | resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 639 | 640 | basic-auth@2.0.1: 641 | resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} 642 | engines: {node: '>= 0.8'} 643 | 644 | bmp-js@0.1.0: 645 | resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} 646 | 647 | boolean@3.2.0: 648 | resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} 649 | deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. 650 | 651 | brace-expansion@2.0.1: 652 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 653 | 654 | buffer-crc32@0.2.13: 655 | resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} 656 | 657 | buffer-from@1.1.2: 658 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 659 | 660 | buffer@5.7.1: 661 | resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} 662 | 663 | buffer@6.0.3: 664 | resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} 665 | 666 | cacheable-lookup@5.0.4: 667 | resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} 668 | engines: {node: '>=10.6.0'} 669 | 670 | cacheable-request@7.0.4: 671 | resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} 672 | engines: {node: '>=8'} 673 | 674 | call-bind-apply-helpers@1.0.2: 675 | resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 676 | engines: {node: '>= 0.4'} 677 | 678 | call-bound@1.0.4: 679 | resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} 680 | engines: {node: '>= 0.4'} 681 | 682 | camel-case@4.1.2: 683 | resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} 684 | 685 | chalk@4.1.2: 686 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 687 | engines: {node: '>=10'} 688 | 689 | clean-css@5.3.3: 690 | resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} 691 | engines: {node: '>= 10.0'} 692 | 693 | cliui@8.0.1: 694 | resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 695 | engines: {node: '>=12'} 696 | 697 | clone-response@1.0.3: 698 | resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} 699 | 700 | color-convert@2.0.1: 701 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 702 | engines: {node: '>=7.0.0'} 703 | 704 | color-name@1.1.4: 705 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 706 | 707 | commander@10.0.1: 708 | resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} 709 | engines: {node: '>=14'} 710 | 711 | commander@2.20.3: 712 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 713 | 714 | concurrently@9.1.2: 715 | resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==} 716 | engines: {node: '>=18'} 717 | hasBin: true 718 | 719 | corser@2.0.1: 720 | resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} 721 | engines: {node: '>= 0.4.0'} 722 | 723 | cross-spawn@7.0.6: 724 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 725 | engines: {node: '>= 8'} 726 | 727 | csstype@3.1.3: 728 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 729 | 730 | debug@4.4.0: 731 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 732 | engines: {node: '>=6.0'} 733 | peerDependencies: 734 | supports-color: '*' 735 | peerDependenciesMeta: 736 | supports-color: 737 | optional: true 738 | 739 | decompress-response@6.0.0: 740 | resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} 741 | engines: {node: '>=10'} 742 | 743 | defer-to-connect@2.0.1: 744 | resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} 745 | engines: {node: '>=10'} 746 | 747 | define-data-property@1.1.4: 748 | resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} 749 | engines: {node: '>= 0.4'} 750 | 751 | define-properties@1.2.1: 752 | resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} 753 | engines: {node: '>= 0.4'} 754 | 755 | dequal@2.0.3: 756 | resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 757 | engines: {node: '>=6'} 758 | 759 | detect-node@2.1.0: 760 | resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} 761 | 762 | discord-api-types@0.37.120: 763 | resolution: {integrity: sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==} 764 | 765 | discord-api-types@0.38.1: 766 | resolution: {integrity: sha512-vsjsqjAuxsPhiwbPjTBeGQaDPlizFmSkU0mTzFGMgRxqCDIRBR7iTY74HacpzrDV0QtERHRKQEk1tq7drZUtHg==} 767 | 768 | dot-case@3.0.4: 769 | resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} 770 | 771 | dunder-proto@1.0.1: 772 | resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 773 | engines: {node: '>= 0.4'} 774 | 775 | eastasianwidth@0.2.0: 776 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 777 | 778 | electron@36.1.0: 779 | resolution: {integrity: sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg==} 780 | engines: {node: '>= 12.20.55'} 781 | hasBin: true 782 | 783 | emoji-regex@8.0.0: 784 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 785 | 786 | emoji-regex@9.2.2: 787 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 788 | 789 | end-of-stream@1.4.4: 790 | resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} 791 | 792 | entities@4.5.0: 793 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 794 | engines: {node: '>=0.12'} 795 | 796 | env-paths@2.2.1: 797 | resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} 798 | engines: {node: '>=6'} 799 | 800 | es-define-property@1.0.1: 801 | resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 802 | engines: {node: '>= 0.4'} 803 | 804 | es-errors@1.3.0: 805 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 806 | engines: {node: '>= 0.4'} 807 | 808 | es-object-atoms@1.1.1: 809 | resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 810 | engines: {node: '>= 0.4'} 811 | 812 | es6-error@4.1.1: 813 | resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} 814 | 815 | esbuild@0.25.3: 816 | resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} 817 | engines: {node: '>=18'} 818 | hasBin: true 819 | 820 | esbuild@0.25.4: 821 | resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} 822 | engines: {node: '>=18'} 823 | hasBin: true 824 | 825 | escalade@3.2.0: 826 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 827 | engines: {node: '>=6'} 828 | 829 | escape-string-regexp@4.0.0: 830 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 831 | engines: {node: '>=10'} 832 | 833 | event-target-shim@5.0.1: 834 | resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} 835 | engines: {node: '>=6'} 836 | 837 | eventemitter3@4.0.7: 838 | resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} 839 | 840 | events@3.3.0: 841 | resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 842 | engines: {node: '>=0.8.x'} 843 | 844 | exif-parser@0.1.12: 845 | resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} 846 | 847 | extract-zip@2.0.1: 848 | resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} 849 | engines: {node: '>= 10.17.0'} 850 | hasBin: true 851 | 852 | fast-deep-equal@3.1.3: 853 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 854 | 855 | fd-slicer@1.1.0: 856 | resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} 857 | 858 | file-type@16.5.4: 859 | resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} 860 | engines: {node: '>=10'} 861 | 862 | follow-redirects@1.15.9: 863 | resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} 864 | engines: {node: '>=4.0'} 865 | peerDependencies: 866 | debug: '*' 867 | peerDependenciesMeta: 868 | debug: 869 | optional: true 870 | 871 | foreground-child@3.3.1: 872 | resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} 873 | engines: {node: '>=14'} 874 | 875 | fs-extra@8.1.0: 876 | resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} 877 | engines: {node: '>=6 <7 || >=8'} 878 | 879 | fsevents@2.3.3: 880 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 881 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 882 | os: [darwin] 883 | 884 | function-bind@1.1.2: 885 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 886 | 887 | get-caller-file@2.0.5: 888 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 889 | engines: {node: 6.* || 8.* || >= 10.*} 890 | 891 | get-intrinsic@1.3.0: 892 | resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 893 | engines: {node: '>= 0.4'} 894 | 895 | get-proto@1.0.1: 896 | resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 897 | engines: {node: '>= 0.4'} 898 | 899 | get-stream@5.2.0: 900 | resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} 901 | engines: {node: '>=8'} 902 | 903 | get-tsconfig@4.10.0: 904 | resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} 905 | 906 | gifwrap@0.10.1: 907 | resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} 908 | 909 | glob@11.0.2: 910 | resolution: {integrity: sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==} 911 | engines: {node: 20 || >=22} 912 | hasBin: true 913 | 914 | global-agent@3.0.0: 915 | resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} 916 | engines: {node: '>=10.0'} 917 | 918 | globalthis@1.0.4: 919 | resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} 920 | engines: {node: '>= 0.4'} 921 | 922 | gopd@1.2.0: 923 | resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 924 | engines: {node: '>= 0.4'} 925 | 926 | got@11.8.6: 927 | resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} 928 | engines: {node: '>=10.19.0'} 929 | 930 | graceful-fs@4.2.11: 931 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 932 | 933 | has-flag@4.0.0: 934 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 935 | engines: {node: '>=8'} 936 | 937 | has-property-descriptors@1.0.2: 938 | resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} 939 | 940 | has-symbols@1.1.0: 941 | resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 942 | engines: {node: '>= 0.4'} 943 | 944 | hasown@2.0.2: 945 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 946 | engines: {node: '>= 0.4'} 947 | 948 | he@1.2.0: 949 | resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 950 | hasBin: true 951 | 952 | html-encoding-sniffer@3.0.0: 953 | resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} 954 | engines: {node: '>=12'} 955 | 956 | html-minifier-terser@7.2.0: 957 | resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} 958 | engines: {node: ^14.13.1 || >=16.0.0} 959 | hasBin: true 960 | 961 | http-cache-semantics@4.1.1: 962 | resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} 963 | 964 | http-proxy@1.18.1: 965 | resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} 966 | engines: {node: '>=8.0.0'} 967 | 968 | http-server@14.1.1: 969 | resolution: {integrity: sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==} 970 | engines: {node: '>=12'} 971 | hasBin: true 972 | 973 | http2-wrapper@1.0.3: 974 | resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} 975 | engines: {node: '>=10.19.0'} 976 | 977 | iconv-lite@0.6.3: 978 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 979 | engines: {node: '>=0.10.0'} 980 | 981 | ieee754@1.2.1: 982 | resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 983 | 984 | image-q@4.0.0: 985 | resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} 986 | 987 | is-fullwidth-code-point@3.0.0: 988 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 989 | engines: {node: '>=8'} 990 | 991 | isexe@2.0.0: 992 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 993 | 994 | isomorphic-fetch@3.0.0: 995 | resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} 996 | 997 | jackspeak@4.1.0: 998 | resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} 999 | engines: {node: 20 || >=22} 1000 | 1001 | jpeg-js@0.4.4: 1002 | resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} 1003 | 1004 | json-buffer@3.0.1: 1005 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 1006 | 1007 | json-stringify-safe@5.0.1: 1008 | resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} 1009 | 1010 | jsonfile@4.0.0: 1011 | resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 1012 | 1013 | keyv@4.5.4: 1014 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 1015 | 1016 | lodash@4.17.21: 1017 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 1018 | 1019 | lower-case@2.0.2: 1020 | resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} 1021 | 1022 | lowercase-keys@2.0.0: 1023 | resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} 1024 | engines: {node: '>=8'} 1025 | 1026 | lru-cache@11.1.0: 1027 | resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} 1028 | engines: {node: 20 || >=22} 1029 | 1030 | luna@https://codeload.github.com/inrixia/TidaLuna/tar.gz/98b7912: 1031 | resolution: {tarball: https://codeload.github.com/inrixia/TidaLuna/tar.gz/98b7912} 1032 | version: 1.2.6-alpha 1033 | 1034 | magic-bytes.js@1.12.1: 1035 | resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} 1036 | 1037 | matcher@3.0.0: 1038 | resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} 1039 | engines: {node: '>=10'} 1040 | 1041 | math-intrinsics@1.1.0: 1042 | resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 1043 | engines: {node: '>= 0.4'} 1044 | 1045 | mime@1.6.0: 1046 | resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 1047 | engines: {node: '>=4'} 1048 | hasBin: true 1049 | 1050 | mimic-response@1.0.1: 1051 | resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} 1052 | engines: {node: '>=4'} 1053 | 1054 | mimic-response@3.1.0: 1055 | resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} 1056 | engines: {node: '>=10'} 1057 | 1058 | minimatch@10.0.1: 1059 | resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} 1060 | engines: {node: 20 || >=22} 1061 | 1062 | minimist@1.2.8: 1063 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 1064 | 1065 | minipass@7.1.2: 1066 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 1067 | engines: {node: '>=16 || 14 >=14.17'} 1068 | 1069 | ms@2.1.3: 1070 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1071 | 1072 | no-case@3.0.4: 1073 | resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} 1074 | 1075 | node-fetch@2.7.0: 1076 | resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} 1077 | engines: {node: 4.x || >=6.0.0} 1078 | peerDependencies: 1079 | encoding: ^0.1.0 1080 | peerDependenciesMeta: 1081 | encoding: 1082 | optional: true 1083 | 1084 | node-vibrant@4.0.3: 1085 | resolution: {integrity: sha512-kzoIuJK90BH/k65Avt077JCX4Nhqz1LNc8cIOm2rnYEvFdJIYd8b3SQwU1MTpzcHtr8z8jxkl1qdaCfbP3olFg==} 1086 | 1087 | normalize-url@6.1.0: 1088 | resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} 1089 | engines: {node: '>=10'} 1090 | 1091 | object-inspect@1.13.4: 1092 | resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} 1093 | engines: {node: '>= 0.4'} 1094 | 1095 | object-keys@1.1.1: 1096 | resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} 1097 | engines: {node: '>= 0.4'} 1098 | 1099 | oby@15.1.2: 1100 | resolution: {integrity: sha512-6QD9iEoPzV+pMDdcg3RtFWhgDX8pS5hZouVHvgXGDy3Q9RxFfnI3CYv9i62keeuX+qk6iN2z5E9FD3q3OckZ6A==} 1101 | 1102 | omggif@1.0.10: 1103 | resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} 1104 | 1105 | once@1.4.0: 1106 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1107 | 1108 | opener@1.5.2: 1109 | resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} 1110 | hasBin: true 1111 | 1112 | p-cancelable@2.1.1: 1113 | resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} 1114 | engines: {node: '>=8'} 1115 | 1116 | package-json-from-dist@1.0.1: 1117 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 1118 | 1119 | pako@1.0.11: 1120 | resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} 1121 | 1122 | param-case@3.0.4: 1123 | resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} 1124 | 1125 | pascal-case@3.1.2: 1126 | resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} 1127 | 1128 | path-key@3.1.1: 1129 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1130 | engines: {node: '>=8'} 1131 | 1132 | path-scurry@2.0.0: 1133 | resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} 1134 | engines: {node: 20 || >=22} 1135 | 1136 | peek-readable@4.1.0: 1137 | resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} 1138 | engines: {node: '>=8'} 1139 | 1140 | pend@1.2.0: 1141 | resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} 1142 | 1143 | pixelmatch@4.0.2: 1144 | resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} 1145 | hasBin: true 1146 | 1147 | pngjs@3.4.0: 1148 | resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} 1149 | engines: {node: '>=4.0.0'} 1150 | 1151 | pngjs@6.0.0: 1152 | resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} 1153 | engines: {node: '>=12.13.0'} 1154 | 1155 | portfinder@1.0.36: 1156 | resolution: {integrity: sha512-gMKUzCoP+feA7t45moaSx7UniU7PgGN3hA8acAB+3Qn7/js0/lJ07fYZlxt9riE9S3myyxDCyAFzSrLlta0c9g==} 1157 | engines: {node: '>= 10.12'} 1158 | 1159 | process@0.11.10: 1160 | resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} 1161 | engines: {node: '>= 0.6.0'} 1162 | 1163 | progress@2.0.3: 1164 | resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} 1165 | engines: {node: '>=0.4.0'} 1166 | 1167 | pump@3.0.2: 1168 | resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} 1169 | 1170 | qs@6.14.0: 1171 | resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} 1172 | engines: {node: '>=0.6'} 1173 | 1174 | quick-lru@5.1.1: 1175 | resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} 1176 | engines: {node: '>=10'} 1177 | 1178 | readable-stream@4.7.0: 1179 | resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} 1180 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 1181 | 1182 | readable-web-to-node-stream@3.0.4: 1183 | resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} 1184 | engines: {node: '>=8'} 1185 | 1186 | regenerator-runtime@0.13.11: 1187 | resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} 1188 | 1189 | relateurl@0.2.7: 1190 | resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} 1191 | engines: {node: '>= 0.10'} 1192 | 1193 | require-directory@2.1.1: 1194 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 1195 | engines: {node: '>=0.10.0'} 1196 | 1197 | requires-port@1.0.0: 1198 | resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} 1199 | 1200 | resolve-alpn@1.2.1: 1201 | resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} 1202 | 1203 | resolve-pkg-maps@1.0.0: 1204 | resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1205 | 1206 | responselike@2.0.1: 1207 | resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} 1208 | 1209 | rimraf@6.0.1: 1210 | resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} 1211 | engines: {node: 20 || >=22} 1212 | hasBin: true 1213 | 1214 | roarr@2.15.4: 1215 | resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} 1216 | engines: {node: '>=8.0'} 1217 | 1218 | rxjs@7.8.2: 1219 | resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} 1220 | 1221 | safe-buffer@5.1.2: 1222 | resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 1223 | 1224 | safe-buffer@5.2.1: 1225 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1226 | 1227 | safer-buffer@2.1.2: 1228 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 1229 | 1230 | secure-compare@3.0.1: 1231 | resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} 1232 | 1233 | semver-compare@1.0.0: 1234 | resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} 1235 | 1236 | semver@6.3.1: 1237 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 1238 | hasBin: true 1239 | 1240 | semver@7.7.1: 1241 | resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} 1242 | engines: {node: '>=10'} 1243 | hasBin: true 1244 | 1245 | serialize-error@7.0.1: 1246 | resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} 1247 | engines: {node: '>=10'} 1248 | 1249 | shazamio-core@1.3.1: 1250 | resolution: {integrity: sha512-wzYxaL+Tzj4hv5UO1kCbJSjnL02rfcPqxtklf2vg1ykwE1U/FXpB83SRTS4/0OU2uTcVcvQN+xTqzwZQl4CIMg==} 1251 | 1252 | shebang-command@2.0.0: 1253 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1254 | engines: {node: '>=8'} 1255 | 1256 | shebang-regex@3.0.0: 1257 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1258 | engines: {node: '>=8'} 1259 | 1260 | shell-quote@1.8.2: 1261 | resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} 1262 | engines: {node: '>= 0.4'} 1263 | 1264 | side-channel-list@1.0.0: 1265 | resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} 1266 | engines: {node: '>= 0.4'} 1267 | 1268 | side-channel-map@1.0.1: 1269 | resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} 1270 | engines: {node: '>= 0.4'} 1271 | 1272 | side-channel-weakmap@1.0.2: 1273 | resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} 1274 | engines: {node: '>= 0.4'} 1275 | 1276 | side-channel@1.1.0: 1277 | resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} 1278 | engines: {node: '>= 0.4'} 1279 | 1280 | signal-exit@4.1.0: 1281 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1282 | engines: {node: '>=14'} 1283 | 1284 | source-map-support@0.5.21: 1285 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 1286 | 1287 | source-map@0.6.1: 1288 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 1289 | engines: {node: '>=0.10.0'} 1290 | 1291 | sprintf-js@1.1.3: 1292 | resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} 1293 | 1294 | string-width@4.2.3: 1295 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1296 | engines: {node: '>=8'} 1297 | 1298 | string-width@5.1.2: 1299 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 1300 | engines: {node: '>=12'} 1301 | 1302 | string_decoder@1.3.0: 1303 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 1304 | 1305 | strip-ansi@6.0.1: 1306 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1307 | engines: {node: '>=8'} 1308 | 1309 | strip-ansi@7.1.0: 1310 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 1311 | engines: {node: '>=12'} 1312 | 1313 | strtok3@6.3.0: 1314 | resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} 1315 | engines: {node: '>=10'} 1316 | 1317 | sumchecker@3.0.1: 1318 | resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} 1319 | engines: {node: '>= 8.0'} 1320 | 1321 | supports-color@7.2.0: 1322 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1323 | engines: {node: '>=8'} 1324 | 1325 | supports-color@8.1.1: 1326 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 1327 | engines: {node: '>=10'} 1328 | 1329 | terser@5.39.0: 1330 | resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} 1331 | engines: {node: '>=10'} 1332 | hasBin: true 1333 | 1334 | timm@1.7.1: 1335 | resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} 1336 | 1337 | tinycolor2@1.6.0: 1338 | resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} 1339 | 1340 | token-types@4.2.1: 1341 | resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} 1342 | engines: {node: '>=10'} 1343 | 1344 | tr46@0.0.3: 1345 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} 1346 | 1347 | tree-kill@1.2.2: 1348 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 1349 | hasBin: true 1350 | 1351 | tslib@2.8.1: 1352 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1353 | 1354 | tsx@4.19.3: 1355 | resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} 1356 | engines: {node: '>=18.0.0'} 1357 | hasBin: true 1358 | 1359 | type-fest@0.13.1: 1360 | resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} 1361 | engines: {node: '>=10'} 1362 | 1363 | typescript@5.8.3: 1364 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 1365 | engines: {node: '>=14.17'} 1366 | hasBin: true 1367 | 1368 | undici-types@5.26.5: 1369 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 1370 | 1371 | undici-types@6.21.0: 1372 | resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 1373 | 1374 | undici@6.21.1: 1375 | resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} 1376 | engines: {node: '>=18.17'} 1377 | 1378 | union@0.5.0: 1379 | resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} 1380 | engines: {node: '>= 0.8.0'} 1381 | 1382 | universalify@0.1.2: 1383 | resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} 1384 | engines: {node: '>= 4.0.0'} 1385 | 1386 | url-join@4.0.1: 1387 | resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} 1388 | 1389 | utif2@4.1.0: 1390 | resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} 1391 | 1392 | uuid@11.1.0: 1393 | resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} 1394 | hasBin: true 1395 | 1396 | webidl-conversions@3.0.1: 1397 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} 1398 | 1399 | whatwg-encoding@2.0.0: 1400 | resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} 1401 | engines: {node: '>=12'} 1402 | 1403 | whatwg-fetch@3.6.20: 1404 | resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} 1405 | 1406 | whatwg-url@5.0.0: 1407 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} 1408 | 1409 | which@2.0.2: 1410 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1411 | engines: {node: '>= 8'} 1412 | hasBin: true 1413 | 1414 | wrap-ansi@7.0.0: 1415 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 1416 | engines: {node: '>=10'} 1417 | 1418 | wrap-ansi@8.1.0: 1419 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 1420 | engines: {node: '>=12'} 1421 | 1422 | wrappy@1.0.2: 1423 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1424 | 1425 | ws@8.18.1: 1426 | resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} 1427 | engines: {node: '>=10.0.0'} 1428 | peerDependencies: 1429 | bufferutil: ^4.0.1 1430 | utf-8-validate: '>=5.0.2' 1431 | peerDependenciesMeta: 1432 | bufferutil: 1433 | optional: true 1434 | utf-8-validate: 1435 | optional: true 1436 | 1437 | y18n@5.0.8: 1438 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 1439 | engines: {node: '>=10'} 1440 | 1441 | yargs-parser@21.1.1: 1442 | resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 1443 | engines: {node: '>=12'} 1444 | 1445 | yargs@17.7.2: 1446 | resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} 1447 | engines: {node: '>=12'} 1448 | 1449 | yauzl@2.10.0: 1450 | resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} 1451 | 1452 | snapshots: 1453 | 1454 | '@discordjs/collection@2.1.1': {} 1455 | 1456 | '@discordjs/rest@2.5.0': 1457 | dependencies: 1458 | '@discordjs/collection': 2.1.1 1459 | '@discordjs/util': 1.1.1 1460 | '@sapphire/async-queue': 1.5.5 1461 | '@sapphire/snowflake': 3.5.5 1462 | '@vladfrangu/async_event_emitter': 2.4.6 1463 | discord-api-types: 0.38.1 1464 | magic-bytes.js: 1.12.1 1465 | tslib: 2.8.1 1466 | undici: 6.21.1 1467 | 1468 | '@discordjs/util@1.1.1': {} 1469 | 1470 | '@electron/get@2.0.3': 1471 | dependencies: 1472 | debug: 4.4.0 1473 | env-paths: 2.2.1 1474 | fs-extra: 8.1.0 1475 | got: 11.8.6 1476 | progress: 2.0.3 1477 | semver: 6.3.1 1478 | sumchecker: 3.0.1 1479 | optionalDependencies: 1480 | global-agent: 3.0.0 1481 | transitivePeerDependencies: 1482 | - supports-color 1483 | 1484 | '@esbuild/aix-ppc64@0.25.3': 1485 | optional: true 1486 | 1487 | '@esbuild/aix-ppc64@0.25.4': 1488 | optional: true 1489 | 1490 | '@esbuild/android-arm64@0.25.3': 1491 | optional: true 1492 | 1493 | '@esbuild/android-arm64@0.25.4': 1494 | optional: true 1495 | 1496 | '@esbuild/android-arm@0.25.3': 1497 | optional: true 1498 | 1499 | '@esbuild/android-arm@0.25.4': 1500 | optional: true 1501 | 1502 | '@esbuild/android-x64@0.25.3': 1503 | optional: true 1504 | 1505 | '@esbuild/android-x64@0.25.4': 1506 | optional: true 1507 | 1508 | '@esbuild/darwin-arm64@0.25.3': 1509 | optional: true 1510 | 1511 | '@esbuild/darwin-arm64@0.25.4': 1512 | optional: true 1513 | 1514 | '@esbuild/darwin-x64@0.25.3': 1515 | optional: true 1516 | 1517 | '@esbuild/darwin-x64@0.25.4': 1518 | optional: true 1519 | 1520 | '@esbuild/freebsd-arm64@0.25.3': 1521 | optional: true 1522 | 1523 | '@esbuild/freebsd-arm64@0.25.4': 1524 | optional: true 1525 | 1526 | '@esbuild/freebsd-x64@0.25.3': 1527 | optional: true 1528 | 1529 | '@esbuild/freebsd-x64@0.25.4': 1530 | optional: true 1531 | 1532 | '@esbuild/linux-arm64@0.25.3': 1533 | optional: true 1534 | 1535 | '@esbuild/linux-arm64@0.25.4': 1536 | optional: true 1537 | 1538 | '@esbuild/linux-arm@0.25.3': 1539 | optional: true 1540 | 1541 | '@esbuild/linux-arm@0.25.4': 1542 | optional: true 1543 | 1544 | '@esbuild/linux-ia32@0.25.3': 1545 | optional: true 1546 | 1547 | '@esbuild/linux-ia32@0.25.4': 1548 | optional: true 1549 | 1550 | '@esbuild/linux-loong64@0.25.3': 1551 | optional: true 1552 | 1553 | '@esbuild/linux-loong64@0.25.4': 1554 | optional: true 1555 | 1556 | '@esbuild/linux-mips64el@0.25.3': 1557 | optional: true 1558 | 1559 | '@esbuild/linux-mips64el@0.25.4': 1560 | optional: true 1561 | 1562 | '@esbuild/linux-ppc64@0.25.3': 1563 | optional: true 1564 | 1565 | '@esbuild/linux-ppc64@0.25.4': 1566 | optional: true 1567 | 1568 | '@esbuild/linux-riscv64@0.25.3': 1569 | optional: true 1570 | 1571 | '@esbuild/linux-riscv64@0.25.4': 1572 | optional: true 1573 | 1574 | '@esbuild/linux-s390x@0.25.3': 1575 | optional: true 1576 | 1577 | '@esbuild/linux-s390x@0.25.4': 1578 | optional: true 1579 | 1580 | '@esbuild/linux-x64@0.25.3': 1581 | optional: true 1582 | 1583 | '@esbuild/linux-x64@0.25.4': 1584 | optional: true 1585 | 1586 | '@esbuild/netbsd-arm64@0.25.3': 1587 | optional: true 1588 | 1589 | '@esbuild/netbsd-arm64@0.25.4': 1590 | optional: true 1591 | 1592 | '@esbuild/netbsd-x64@0.25.3': 1593 | optional: true 1594 | 1595 | '@esbuild/netbsd-x64@0.25.4': 1596 | optional: true 1597 | 1598 | '@esbuild/openbsd-arm64@0.25.3': 1599 | optional: true 1600 | 1601 | '@esbuild/openbsd-arm64@0.25.4': 1602 | optional: true 1603 | 1604 | '@esbuild/openbsd-x64@0.25.3': 1605 | optional: true 1606 | 1607 | '@esbuild/openbsd-x64@0.25.4': 1608 | optional: true 1609 | 1610 | '@esbuild/sunos-x64@0.25.3': 1611 | optional: true 1612 | 1613 | '@esbuild/sunos-x64@0.25.4': 1614 | optional: true 1615 | 1616 | '@esbuild/win32-arm64@0.25.3': 1617 | optional: true 1618 | 1619 | '@esbuild/win32-arm64@0.25.4': 1620 | optional: true 1621 | 1622 | '@esbuild/win32-ia32@0.25.3': 1623 | optional: true 1624 | 1625 | '@esbuild/win32-ia32@0.25.4': 1626 | optional: true 1627 | 1628 | '@esbuild/win32-x64@0.25.3': 1629 | optional: true 1630 | 1631 | '@esbuild/win32-x64@0.25.4': 1632 | optional: true 1633 | 1634 | '@homebridge/ciao@1.3.1': 1635 | dependencies: 1636 | debug: 4.4.0 1637 | fast-deep-equal: 3.1.3 1638 | source-map-support: 0.5.21 1639 | tslib: 2.8.1 1640 | transitivePeerDependencies: 1641 | - supports-color 1642 | 1643 | '@inrixia/helpers@3.15.1': 1644 | dependencies: 1645 | dequal: 2.0.3 1646 | 1647 | '@inrixia/helpers@3.20.0': 1648 | dependencies: 1649 | dequal: 2.0.3 1650 | 1651 | '@isaacs/cliui@8.0.2': 1652 | dependencies: 1653 | string-width: 5.1.2 1654 | string-width-cjs: string-width@4.2.3 1655 | strip-ansi: 7.1.0 1656 | strip-ansi-cjs: strip-ansi@6.0.1 1657 | wrap-ansi: 8.1.0 1658 | wrap-ansi-cjs: wrap-ansi@7.0.0 1659 | 1660 | '@jimp/bmp@0.22.12(@jimp/custom@0.22.12)': 1661 | dependencies: 1662 | '@jimp/custom': 0.22.12 1663 | '@jimp/utils': 0.22.12 1664 | bmp-js: 0.1.0 1665 | 1666 | '@jimp/core@0.22.12': 1667 | dependencies: 1668 | '@jimp/utils': 0.22.12 1669 | any-base: 1.1.0 1670 | buffer: 5.7.1 1671 | exif-parser: 0.1.12 1672 | file-type: 16.5.4 1673 | isomorphic-fetch: 3.0.0 1674 | pixelmatch: 4.0.2 1675 | tinycolor2: 1.6.0 1676 | transitivePeerDependencies: 1677 | - encoding 1678 | 1679 | '@jimp/custom@0.22.12': 1680 | dependencies: 1681 | '@jimp/core': 0.22.12 1682 | transitivePeerDependencies: 1683 | - encoding 1684 | 1685 | '@jimp/gif@0.22.12(@jimp/custom@0.22.12)': 1686 | dependencies: 1687 | '@jimp/custom': 0.22.12 1688 | '@jimp/utils': 0.22.12 1689 | gifwrap: 0.10.1 1690 | omggif: 1.0.10 1691 | 1692 | '@jimp/jpeg@0.22.12(@jimp/custom@0.22.12)': 1693 | dependencies: 1694 | '@jimp/custom': 0.22.12 1695 | '@jimp/utils': 0.22.12 1696 | jpeg-js: 0.4.4 1697 | 1698 | '@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)': 1699 | dependencies: 1700 | '@jimp/custom': 0.22.12 1701 | '@jimp/utils': 0.22.12 1702 | 1703 | '@jimp/png@0.22.12(@jimp/custom@0.22.12)': 1704 | dependencies: 1705 | '@jimp/custom': 0.22.12 1706 | '@jimp/utils': 0.22.12 1707 | pngjs: 6.0.0 1708 | 1709 | '@jimp/tiff@0.22.12(@jimp/custom@0.22.12)': 1710 | dependencies: 1711 | '@jimp/custom': 0.22.12 1712 | utif2: 4.1.0 1713 | 1714 | '@jimp/types@0.22.12(@jimp/custom@0.22.12)': 1715 | dependencies: 1716 | '@jimp/bmp': 0.22.12(@jimp/custom@0.22.12) 1717 | '@jimp/custom': 0.22.12 1718 | '@jimp/gif': 0.22.12(@jimp/custom@0.22.12) 1719 | '@jimp/jpeg': 0.22.12(@jimp/custom@0.22.12) 1720 | '@jimp/png': 0.22.12(@jimp/custom@0.22.12) 1721 | '@jimp/tiff': 0.22.12(@jimp/custom@0.22.12) 1722 | timm: 1.7.1 1723 | 1724 | '@jimp/utils@0.22.12': 1725 | dependencies: 1726 | regenerator-runtime: 0.13.11 1727 | 1728 | '@jridgewell/gen-mapping@0.3.8': 1729 | dependencies: 1730 | '@jridgewell/set-array': 1.2.1 1731 | '@jridgewell/sourcemap-codec': 1.5.0 1732 | '@jridgewell/trace-mapping': 0.3.25 1733 | 1734 | '@jridgewell/resolve-uri@3.1.2': {} 1735 | 1736 | '@jridgewell/set-array@1.2.1': {} 1737 | 1738 | '@jridgewell/source-map@0.3.6': 1739 | dependencies: 1740 | '@jridgewell/gen-mapping': 0.3.8 1741 | '@jridgewell/trace-mapping': 0.3.25 1742 | 1743 | '@jridgewell/sourcemap-codec@1.5.0': {} 1744 | 1745 | '@jridgewell/trace-mapping@0.3.25': 1746 | dependencies: 1747 | '@jridgewell/resolve-uri': 3.1.2 1748 | '@jridgewell/sourcemap-codec': 1.5.0 1749 | 1750 | '@sapphire/async-queue@1.5.5': {} 1751 | 1752 | '@sapphire/snowflake@3.5.5': {} 1753 | 1754 | '@sindresorhus/is@4.6.0': {} 1755 | 1756 | '@szmarczak/http-timer@4.0.6': 1757 | dependencies: 1758 | defer-to-connect: 2.0.1 1759 | 1760 | '@tokenizer/token@0.3.0': {} 1761 | 1762 | '@types/cacheable-request@6.0.3': 1763 | dependencies: 1764 | '@types/http-cache-semantics': 4.0.4 1765 | '@types/keyv': 3.1.4 1766 | '@types/node': 22.15.0 1767 | '@types/responselike': 1.0.3 1768 | 1769 | '@types/clean-css@4.2.11': 1770 | dependencies: 1771 | '@types/node': 22.15.17 1772 | source-map: 0.6.1 1773 | 1774 | '@types/html-minifier-terser@7.0.2': {} 1775 | 1776 | '@types/http-cache-semantics@4.0.4': {} 1777 | 1778 | '@types/keyv@3.1.4': 1779 | dependencies: 1780 | '@types/node': 22.15.0 1781 | 1782 | '@types/node@16.9.1': {} 1783 | 1784 | '@types/node@18.19.86': 1785 | dependencies: 1786 | undici-types: 5.26.5 1787 | 1788 | '@types/node@22.15.0': 1789 | dependencies: 1790 | undici-types: 6.21.0 1791 | 1792 | '@types/node@22.15.17': 1793 | dependencies: 1794 | undici-types: 6.21.0 1795 | 1796 | '@types/react-dom@19.1.2(@types/react@19.1.2)': 1797 | dependencies: 1798 | '@types/react': 19.1.2 1799 | 1800 | '@types/react@19.1.2': 1801 | dependencies: 1802 | csstype: 3.1.3 1803 | 1804 | '@types/responselike@1.0.3': 1805 | dependencies: 1806 | '@types/node': 22.15.0 1807 | 1808 | '@types/uuid@10.0.0': {} 1809 | 1810 | '@types/yauzl@2.10.3': 1811 | dependencies: 1812 | '@types/node': 22.15.0 1813 | optional: true 1814 | 1815 | '@vibrant/color@4.0.0': {} 1816 | 1817 | '@vibrant/core@4.0.0': 1818 | dependencies: 1819 | '@vibrant/color': 4.0.0 1820 | '@vibrant/generator': 4.0.0 1821 | '@vibrant/image': 4.0.0 1822 | '@vibrant/quantizer': 4.0.0 1823 | '@vibrant/worker': 4.0.0 1824 | 1825 | '@vibrant/generator-default@4.0.3': 1826 | dependencies: 1827 | '@vibrant/color': 4.0.0 1828 | '@vibrant/generator': 4.0.0 1829 | 1830 | '@vibrant/generator@4.0.0': 1831 | dependencies: 1832 | '@vibrant/color': 4.0.0 1833 | '@vibrant/types': 4.0.0 1834 | 1835 | '@vibrant/image-browser@4.0.0': 1836 | dependencies: 1837 | '@vibrant/image': 4.0.0 1838 | 1839 | '@vibrant/image-node@4.0.0': 1840 | dependencies: 1841 | '@jimp/custom': 0.22.12 1842 | '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) 1843 | '@jimp/types': 0.22.12(@jimp/custom@0.22.12) 1844 | '@vibrant/image': 4.0.0 1845 | transitivePeerDependencies: 1846 | - encoding 1847 | 1848 | '@vibrant/image@4.0.0': 1849 | dependencies: 1850 | '@vibrant/color': 4.0.0 1851 | 1852 | '@vibrant/quantizer-mmcq@4.0.0': 1853 | dependencies: 1854 | '@vibrant/color': 4.0.0 1855 | '@vibrant/image': 4.0.0 1856 | '@vibrant/quantizer': 4.0.0 1857 | 1858 | '@vibrant/quantizer@4.0.0': 1859 | dependencies: 1860 | '@vibrant/color': 4.0.0 1861 | '@vibrant/image': 4.0.0 1862 | '@vibrant/types': 4.0.0 1863 | 1864 | '@vibrant/types@4.0.0': {} 1865 | 1866 | '@vibrant/worker@4.0.0': 1867 | dependencies: 1868 | '@vibrant/types': 4.0.0 1869 | 1870 | '@vladfrangu/async_event_emitter@2.4.6': {} 1871 | 1872 | '@xhayper/discord-rpc@1.2.1': 1873 | dependencies: 1874 | '@discordjs/rest': 2.5.0 1875 | '@vladfrangu/async_event_emitter': 2.4.6 1876 | discord-api-types: 0.37.120 1877 | ws: 8.18.1 1878 | transitivePeerDependencies: 1879 | - bufferutil 1880 | - utf-8-validate 1881 | 1882 | abort-controller@3.0.0: 1883 | dependencies: 1884 | event-target-shim: 5.0.1 1885 | 1886 | acorn@8.14.1: {} 1887 | 1888 | ansi-regex@5.0.1: {} 1889 | 1890 | ansi-regex@6.1.0: {} 1891 | 1892 | ansi-styles@4.3.0: 1893 | dependencies: 1894 | color-convert: 2.0.1 1895 | 1896 | ansi-styles@6.2.1: {} 1897 | 1898 | any-base@1.1.0: {} 1899 | 1900 | async@3.2.6: {} 1901 | 1902 | balanced-match@1.0.2: {} 1903 | 1904 | base64-js@1.5.1: {} 1905 | 1906 | basic-auth@2.0.1: 1907 | dependencies: 1908 | safe-buffer: 5.1.2 1909 | 1910 | bmp-js@0.1.0: {} 1911 | 1912 | boolean@3.2.0: 1913 | optional: true 1914 | 1915 | brace-expansion@2.0.1: 1916 | dependencies: 1917 | balanced-match: 1.0.2 1918 | 1919 | buffer-crc32@0.2.13: {} 1920 | 1921 | buffer-from@1.1.2: {} 1922 | 1923 | buffer@5.7.1: 1924 | dependencies: 1925 | base64-js: 1.5.1 1926 | ieee754: 1.2.1 1927 | 1928 | buffer@6.0.3: 1929 | dependencies: 1930 | base64-js: 1.5.1 1931 | ieee754: 1.2.1 1932 | 1933 | cacheable-lookup@5.0.4: {} 1934 | 1935 | cacheable-request@7.0.4: 1936 | dependencies: 1937 | clone-response: 1.0.3 1938 | get-stream: 5.2.0 1939 | http-cache-semantics: 4.1.1 1940 | keyv: 4.5.4 1941 | lowercase-keys: 2.0.0 1942 | normalize-url: 6.1.0 1943 | responselike: 2.0.1 1944 | 1945 | call-bind-apply-helpers@1.0.2: 1946 | dependencies: 1947 | es-errors: 1.3.0 1948 | function-bind: 1.1.2 1949 | 1950 | call-bound@1.0.4: 1951 | dependencies: 1952 | call-bind-apply-helpers: 1.0.2 1953 | get-intrinsic: 1.3.0 1954 | 1955 | camel-case@4.1.2: 1956 | dependencies: 1957 | pascal-case: 3.1.2 1958 | tslib: 2.8.1 1959 | 1960 | chalk@4.1.2: 1961 | dependencies: 1962 | ansi-styles: 4.3.0 1963 | supports-color: 7.2.0 1964 | 1965 | clean-css@5.3.3: 1966 | dependencies: 1967 | source-map: 0.6.1 1968 | 1969 | cliui@8.0.1: 1970 | dependencies: 1971 | string-width: 4.2.3 1972 | strip-ansi: 6.0.1 1973 | wrap-ansi: 7.0.0 1974 | 1975 | clone-response@1.0.3: 1976 | dependencies: 1977 | mimic-response: 1.0.1 1978 | 1979 | color-convert@2.0.1: 1980 | dependencies: 1981 | color-name: 1.1.4 1982 | 1983 | color-name@1.1.4: {} 1984 | 1985 | commander@10.0.1: {} 1986 | 1987 | commander@2.20.3: {} 1988 | 1989 | concurrently@9.1.2: 1990 | dependencies: 1991 | chalk: 4.1.2 1992 | lodash: 4.17.21 1993 | rxjs: 7.8.2 1994 | shell-quote: 1.8.2 1995 | supports-color: 8.1.1 1996 | tree-kill: 1.2.2 1997 | yargs: 17.7.2 1998 | 1999 | corser@2.0.1: {} 2000 | 2001 | cross-spawn@7.0.6: 2002 | dependencies: 2003 | path-key: 3.1.1 2004 | shebang-command: 2.0.0 2005 | which: 2.0.2 2006 | 2007 | csstype@3.1.3: {} 2008 | 2009 | debug@4.4.0: 2010 | dependencies: 2011 | ms: 2.1.3 2012 | 2013 | decompress-response@6.0.0: 2014 | dependencies: 2015 | mimic-response: 3.1.0 2016 | 2017 | defer-to-connect@2.0.1: {} 2018 | 2019 | define-data-property@1.1.4: 2020 | dependencies: 2021 | es-define-property: 1.0.1 2022 | es-errors: 1.3.0 2023 | gopd: 1.2.0 2024 | optional: true 2025 | 2026 | define-properties@1.2.1: 2027 | dependencies: 2028 | define-data-property: 1.1.4 2029 | has-property-descriptors: 1.0.2 2030 | object-keys: 1.1.1 2031 | optional: true 2032 | 2033 | dequal@2.0.3: {} 2034 | 2035 | detect-node@2.1.0: 2036 | optional: true 2037 | 2038 | discord-api-types@0.37.120: {} 2039 | 2040 | discord-api-types@0.38.1: {} 2041 | 2042 | dot-case@3.0.4: 2043 | dependencies: 2044 | no-case: 3.0.4 2045 | tslib: 2.8.1 2046 | 2047 | dunder-proto@1.0.1: 2048 | dependencies: 2049 | call-bind-apply-helpers: 1.0.2 2050 | es-errors: 1.3.0 2051 | gopd: 1.2.0 2052 | 2053 | eastasianwidth@0.2.0: {} 2054 | 2055 | electron@36.1.0: 2056 | dependencies: 2057 | '@electron/get': 2.0.3 2058 | '@types/node': 22.15.0 2059 | extract-zip: 2.0.1 2060 | transitivePeerDependencies: 2061 | - supports-color 2062 | 2063 | emoji-regex@8.0.0: {} 2064 | 2065 | emoji-regex@9.2.2: {} 2066 | 2067 | end-of-stream@1.4.4: 2068 | dependencies: 2069 | once: 1.4.0 2070 | 2071 | entities@4.5.0: {} 2072 | 2073 | env-paths@2.2.1: {} 2074 | 2075 | es-define-property@1.0.1: {} 2076 | 2077 | es-errors@1.3.0: {} 2078 | 2079 | es-object-atoms@1.1.1: 2080 | dependencies: 2081 | es-errors: 1.3.0 2082 | 2083 | es6-error@4.1.1: 2084 | optional: true 2085 | 2086 | esbuild@0.25.3: 2087 | optionalDependencies: 2088 | '@esbuild/aix-ppc64': 0.25.3 2089 | '@esbuild/android-arm': 0.25.3 2090 | '@esbuild/android-arm64': 0.25.3 2091 | '@esbuild/android-x64': 0.25.3 2092 | '@esbuild/darwin-arm64': 0.25.3 2093 | '@esbuild/darwin-x64': 0.25.3 2094 | '@esbuild/freebsd-arm64': 0.25.3 2095 | '@esbuild/freebsd-x64': 0.25.3 2096 | '@esbuild/linux-arm': 0.25.3 2097 | '@esbuild/linux-arm64': 0.25.3 2098 | '@esbuild/linux-ia32': 0.25.3 2099 | '@esbuild/linux-loong64': 0.25.3 2100 | '@esbuild/linux-mips64el': 0.25.3 2101 | '@esbuild/linux-ppc64': 0.25.3 2102 | '@esbuild/linux-riscv64': 0.25.3 2103 | '@esbuild/linux-s390x': 0.25.3 2104 | '@esbuild/linux-x64': 0.25.3 2105 | '@esbuild/netbsd-arm64': 0.25.3 2106 | '@esbuild/netbsd-x64': 0.25.3 2107 | '@esbuild/openbsd-arm64': 0.25.3 2108 | '@esbuild/openbsd-x64': 0.25.3 2109 | '@esbuild/sunos-x64': 0.25.3 2110 | '@esbuild/win32-arm64': 0.25.3 2111 | '@esbuild/win32-ia32': 0.25.3 2112 | '@esbuild/win32-x64': 0.25.3 2113 | 2114 | esbuild@0.25.4: 2115 | optionalDependencies: 2116 | '@esbuild/aix-ppc64': 0.25.4 2117 | '@esbuild/android-arm': 0.25.4 2118 | '@esbuild/android-arm64': 0.25.4 2119 | '@esbuild/android-x64': 0.25.4 2120 | '@esbuild/darwin-arm64': 0.25.4 2121 | '@esbuild/darwin-x64': 0.25.4 2122 | '@esbuild/freebsd-arm64': 0.25.4 2123 | '@esbuild/freebsd-x64': 0.25.4 2124 | '@esbuild/linux-arm': 0.25.4 2125 | '@esbuild/linux-arm64': 0.25.4 2126 | '@esbuild/linux-ia32': 0.25.4 2127 | '@esbuild/linux-loong64': 0.25.4 2128 | '@esbuild/linux-mips64el': 0.25.4 2129 | '@esbuild/linux-ppc64': 0.25.4 2130 | '@esbuild/linux-riscv64': 0.25.4 2131 | '@esbuild/linux-s390x': 0.25.4 2132 | '@esbuild/linux-x64': 0.25.4 2133 | '@esbuild/netbsd-arm64': 0.25.4 2134 | '@esbuild/netbsd-x64': 0.25.4 2135 | '@esbuild/openbsd-arm64': 0.25.4 2136 | '@esbuild/openbsd-x64': 0.25.4 2137 | '@esbuild/sunos-x64': 0.25.4 2138 | '@esbuild/win32-arm64': 0.25.4 2139 | '@esbuild/win32-ia32': 0.25.4 2140 | '@esbuild/win32-x64': 0.25.4 2141 | 2142 | escalade@3.2.0: {} 2143 | 2144 | escape-string-regexp@4.0.0: 2145 | optional: true 2146 | 2147 | event-target-shim@5.0.1: {} 2148 | 2149 | eventemitter3@4.0.7: {} 2150 | 2151 | events@3.3.0: {} 2152 | 2153 | exif-parser@0.1.12: {} 2154 | 2155 | extract-zip@2.0.1: 2156 | dependencies: 2157 | debug: 4.4.0 2158 | get-stream: 5.2.0 2159 | yauzl: 2.10.0 2160 | optionalDependencies: 2161 | '@types/yauzl': 2.10.3 2162 | transitivePeerDependencies: 2163 | - supports-color 2164 | 2165 | fast-deep-equal@3.1.3: {} 2166 | 2167 | fd-slicer@1.1.0: 2168 | dependencies: 2169 | pend: 1.2.0 2170 | 2171 | file-type@16.5.4: 2172 | dependencies: 2173 | readable-web-to-node-stream: 3.0.4 2174 | strtok3: 6.3.0 2175 | token-types: 4.2.1 2176 | 2177 | follow-redirects@1.15.9: {} 2178 | 2179 | foreground-child@3.3.1: 2180 | dependencies: 2181 | cross-spawn: 7.0.6 2182 | signal-exit: 4.1.0 2183 | 2184 | fs-extra@8.1.0: 2185 | dependencies: 2186 | graceful-fs: 4.2.11 2187 | jsonfile: 4.0.0 2188 | universalify: 0.1.2 2189 | 2190 | fsevents@2.3.3: 2191 | optional: true 2192 | 2193 | function-bind@1.1.2: {} 2194 | 2195 | get-caller-file@2.0.5: {} 2196 | 2197 | get-intrinsic@1.3.0: 2198 | dependencies: 2199 | call-bind-apply-helpers: 1.0.2 2200 | es-define-property: 1.0.1 2201 | es-errors: 1.3.0 2202 | es-object-atoms: 1.1.1 2203 | function-bind: 1.1.2 2204 | get-proto: 1.0.1 2205 | gopd: 1.2.0 2206 | has-symbols: 1.1.0 2207 | hasown: 2.0.2 2208 | math-intrinsics: 1.1.0 2209 | 2210 | get-proto@1.0.1: 2211 | dependencies: 2212 | dunder-proto: 1.0.1 2213 | es-object-atoms: 1.1.1 2214 | 2215 | get-stream@5.2.0: 2216 | dependencies: 2217 | pump: 3.0.2 2218 | 2219 | get-tsconfig@4.10.0: 2220 | dependencies: 2221 | resolve-pkg-maps: 1.0.0 2222 | 2223 | gifwrap@0.10.1: 2224 | dependencies: 2225 | image-q: 4.0.0 2226 | omggif: 1.0.10 2227 | 2228 | glob@11.0.2: 2229 | dependencies: 2230 | foreground-child: 3.3.1 2231 | jackspeak: 4.1.0 2232 | minimatch: 10.0.1 2233 | minipass: 7.1.2 2234 | package-json-from-dist: 1.0.1 2235 | path-scurry: 2.0.0 2236 | 2237 | global-agent@3.0.0: 2238 | dependencies: 2239 | boolean: 3.2.0 2240 | es6-error: 4.1.1 2241 | matcher: 3.0.0 2242 | roarr: 2.15.4 2243 | semver: 7.7.1 2244 | serialize-error: 7.0.1 2245 | optional: true 2246 | 2247 | globalthis@1.0.4: 2248 | dependencies: 2249 | define-properties: 1.2.1 2250 | gopd: 1.2.0 2251 | optional: true 2252 | 2253 | gopd@1.2.0: {} 2254 | 2255 | got@11.8.6: 2256 | dependencies: 2257 | '@sindresorhus/is': 4.6.0 2258 | '@szmarczak/http-timer': 4.0.6 2259 | '@types/cacheable-request': 6.0.3 2260 | '@types/responselike': 1.0.3 2261 | cacheable-lookup: 5.0.4 2262 | cacheable-request: 7.0.4 2263 | decompress-response: 6.0.0 2264 | http2-wrapper: 1.0.3 2265 | lowercase-keys: 2.0.0 2266 | p-cancelable: 2.1.1 2267 | responselike: 2.0.1 2268 | 2269 | graceful-fs@4.2.11: {} 2270 | 2271 | has-flag@4.0.0: {} 2272 | 2273 | has-property-descriptors@1.0.2: 2274 | dependencies: 2275 | es-define-property: 1.0.1 2276 | optional: true 2277 | 2278 | has-symbols@1.1.0: {} 2279 | 2280 | hasown@2.0.2: 2281 | dependencies: 2282 | function-bind: 1.1.2 2283 | 2284 | he@1.2.0: {} 2285 | 2286 | html-encoding-sniffer@3.0.0: 2287 | dependencies: 2288 | whatwg-encoding: 2.0.0 2289 | 2290 | html-minifier-terser@7.2.0: 2291 | dependencies: 2292 | camel-case: 4.1.2 2293 | clean-css: 5.3.3 2294 | commander: 10.0.1 2295 | entities: 4.5.0 2296 | param-case: 3.0.4 2297 | relateurl: 0.2.7 2298 | terser: 5.39.0 2299 | 2300 | http-cache-semantics@4.1.1: {} 2301 | 2302 | http-proxy@1.18.1: 2303 | dependencies: 2304 | eventemitter3: 4.0.7 2305 | follow-redirects: 1.15.9 2306 | requires-port: 1.0.0 2307 | transitivePeerDependencies: 2308 | - debug 2309 | 2310 | http-server@14.1.1: 2311 | dependencies: 2312 | basic-auth: 2.0.1 2313 | chalk: 4.1.2 2314 | corser: 2.0.1 2315 | he: 1.2.0 2316 | html-encoding-sniffer: 3.0.0 2317 | http-proxy: 1.18.1 2318 | mime: 1.6.0 2319 | minimist: 1.2.8 2320 | opener: 1.5.2 2321 | portfinder: 1.0.36 2322 | secure-compare: 3.0.1 2323 | union: 0.5.0 2324 | url-join: 4.0.1 2325 | transitivePeerDependencies: 2326 | - debug 2327 | - supports-color 2328 | 2329 | http2-wrapper@1.0.3: 2330 | dependencies: 2331 | quick-lru: 5.1.1 2332 | resolve-alpn: 1.2.1 2333 | 2334 | iconv-lite@0.6.3: 2335 | dependencies: 2336 | safer-buffer: 2.1.2 2337 | 2338 | ieee754@1.2.1: {} 2339 | 2340 | image-q@4.0.0: 2341 | dependencies: 2342 | '@types/node': 16.9.1 2343 | 2344 | is-fullwidth-code-point@3.0.0: {} 2345 | 2346 | isexe@2.0.0: {} 2347 | 2348 | isomorphic-fetch@3.0.0: 2349 | dependencies: 2350 | node-fetch: 2.7.0 2351 | whatwg-fetch: 3.6.20 2352 | transitivePeerDependencies: 2353 | - encoding 2354 | 2355 | jackspeak@4.1.0: 2356 | dependencies: 2357 | '@isaacs/cliui': 8.0.2 2358 | 2359 | jpeg-js@0.4.4: {} 2360 | 2361 | json-buffer@3.0.1: {} 2362 | 2363 | json-stringify-safe@5.0.1: 2364 | optional: true 2365 | 2366 | jsonfile@4.0.0: 2367 | optionalDependencies: 2368 | graceful-fs: 4.2.11 2369 | 2370 | keyv@4.5.4: 2371 | dependencies: 2372 | json-buffer: 3.0.1 2373 | 2374 | lodash@4.17.21: {} 2375 | 2376 | lower-case@2.0.2: 2377 | dependencies: 2378 | tslib: 2.8.1 2379 | 2380 | lowercase-keys@2.0.0: {} 2381 | 2382 | lru-cache@11.1.0: {} 2383 | 2384 | luna@https://codeload.github.com/inrixia/TidaLuna/tar.gz/98b7912: 2385 | dependencies: 2386 | '@inrixia/helpers': 3.20.0 2387 | '@types/clean-css': 4.2.11 2388 | '@types/html-minifier-terser': 7.0.2 2389 | '@types/node': 22.15.17 2390 | clean-css: 5.3.3 2391 | esbuild: 0.25.4 2392 | html-minifier-terser: 7.2.0 2393 | 2394 | magic-bytes.js@1.12.1: {} 2395 | 2396 | matcher@3.0.0: 2397 | dependencies: 2398 | escape-string-regexp: 4.0.0 2399 | optional: true 2400 | 2401 | math-intrinsics@1.1.0: {} 2402 | 2403 | mime@1.6.0: {} 2404 | 2405 | mimic-response@1.0.1: {} 2406 | 2407 | mimic-response@3.1.0: {} 2408 | 2409 | minimatch@10.0.1: 2410 | dependencies: 2411 | brace-expansion: 2.0.1 2412 | 2413 | minimist@1.2.8: {} 2414 | 2415 | minipass@7.1.2: {} 2416 | 2417 | ms@2.1.3: {} 2418 | 2419 | no-case@3.0.4: 2420 | dependencies: 2421 | lower-case: 2.0.2 2422 | tslib: 2.8.1 2423 | 2424 | node-fetch@2.7.0: 2425 | dependencies: 2426 | whatwg-url: 5.0.0 2427 | 2428 | node-vibrant@4.0.3: 2429 | dependencies: 2430 | '@types/node': 18.19.86 2431 | '@vibrant/core': 4.0.0 2432 | '@vibrant/generator-default': 4.0.3 2433 | '@vibrant/image-browser': 4.0.0 2434 | '@vibrant/image-node': 4.0.0 2435 | '@vibrant/quantizer-mmcq': 4.0.0 2436 | transitivePeerDependencies: 2437 | - encoding 2438 | 2439 | normalize-url@6.1.0: {} 2440 | 2441 | object-inspect@1.13.4: {} 2442 | 2443 | object-keys@1.1.1: 2444 | optional: true 2445 | 2446 | oby@15.1.2: {} 2447 | 2448 | omggif@1.0.10: {} 2449 | 2450 | once@1.4.0: 2451 | dependencies: 2452 | wrappy: 1.0.2 2453 | 2454 | opener@1.5.2: {} 2455 | 2456 | p-cancelable@2.1.1: {} 2457 | 2458 | package-json-from-dist@1.0.1: {} 2459 | 2460 | pako@1.0.11: {} 2461 | 2462 | param-case@3.0.4: 2463 | dependencies: 2464 | dot-case: 3.0.4 2465 | tslib: 2.8.1 2466 | 2467 | pascal-case@3.1.2: 2468 | dependencies: 2469 | no-case: 3.0.4 2470 | tslib: 2.8.1 2471 | 2472 | path-key@3.1.1: {} 2473 | 2474 | path-scurry@2.0.0: 2475 | dependencies: 2476 | lru-cache: 11.1.0 2477 | minipass: 7.1.2 2478 | 2479 | peek-readable@4.1.0: {} 2480 | 2481 | pend@1.2.0: {} 2482 | 2483 | pixelmatch@4.0.2: 2484 | dependencies: 2485 | pngjs: 3.4.0 2486 | 2487 | pngjs@3.4.0: {} 2488 | 2489 | pngjs@6.0.0: {} 2490 | 2491 | portfinder@1.0.36: 2492 | dependencies: 2493 | async: 3.2.6 2494 | debug: 4.4.0 2495 | transitivePeerDependencies: 2496 | - supports-color 2497 | 2498 | process@0.11.10: {} 2499 | 2500 | progress@2.0.3: {} 2501 | 2502 | pump@3.0.2: 2503 | dependencies: 2504 | end-of-stream: 1.4.4 2505 | once: 1.4.0 2506 | 2507 | qs@6.14.0: 2508 | dependencies: 2509 | side-channel: 1.1.0 2510 | 2511 | quick-lru@5.1.1: {} 2512 | 2513 | readable-stream@4.7.0: 2514 | dependencies: 2515 | abort-controller: 3.0.0 2516 | buffer: 6.0.3 2517 | events: 3.3.0 2518 | process: 0.11.10 2519 | string_decoder: 1.3.0 2520 | 2521 | readable-web-to-node-stream@3.0.4: 2522 | dependencies: 2523 | readable-stream: 4.7.0 2524 | 2525 | regenerator-runtime@0.13.11: {} 2526 | 2527 | relateurl@0.2.7: {} 2528 | 2529 | require-directory@2.1.1: {} 2530 | 2531 | requires-port@1.0.0: {} 2532 | 2533 | resolve-alpn@1.2.1: {} 2534 | 2535 | resolve-pkg-maps@1.0.0: {} 2536 | 2537 | responselike@2.0.1: 2538 | dependencies: 2539 | lowercase-keys: 2.0.0 2540 | 2541 | rimraf@6.0.1: 2542 | dependencies: 2543 | glob: 11.0.2 2544 | package-json-from-dist: 1.0.1 2545 | 2546 | roarr@2.15.4: 2547 | dependencies: 2548 | boolean: 3.2.0 2549 | detect-node: 2.1.0 2550 | globalthis: 1.0.4 2551 | json-stringify-safe: 5.0.1 2552 | semver-compare: 1.0.0 2553 | sprintf-js: 1.1.3 2554 | optional: true 2555 | 2556 | rxjs@7.8.2: 2557 | dependencies: 2558 | tslib: 2.8.1 2559 | 2560 | safe-buffer@5.1.2: {} 2561 | 2562 | safe-buffer@5.2.1: {} 2563 | 2564 | safer-buffer@2.1.2: {} 2565 | 2566 | secure-compare@3.0.1: {} 2567 | 2568 | semver-compare@1.0.0: 2569 | optional: true 2570 | 2571 | semver@6.3.1: {} 2572 | 2573 | semver@7.7.1: 2574 | optional: true 2575 | 2576 | serialize-error@7.0.1: 2577 | dependencies: 2578 | type-fest: 0.13.1 2579 | optional: true 2580 | 2581 | shazamio-core@1.3.1: {} 2582 | 2583 | shebang-command@2.0.0: 2584 | dependencies: 2585 | shebang-regex: 3.0.0 2586 | 2587 | shebang-regex@3.0.0: {} 2588 | 2589 | shell-quote@1.8.2: {} 2590 | 2591 | side-channel-list@1.0.0: 2592 | dependencies: 2593 | es-errors: 1.3.0 2594 | object-inspect: 1.13.4 2595 | 2596 | side-channel-map@1.0.1: 2597 | dependencies: 2598 | call-bound: 1.0.4 2599 | es-errors: 1.3.0 2600 | get-intrinsic: 1.3.0 2601 | object-inspect: 1.13.4 2602 | 2603 | side-channel-weakmap@1.0.2: 2604 | dependencies: 2605 | call-bound: 1.0.4 2606 | es-errors: 1.3.0 2607 | get-intrinsic: 1.3.0 2608 | object-inspect: 1.13.4 2609 | side-channel-map: 1.0.1 2610 | 2611 | side-channel@1.1.0: 2612 | dependencies: 2613 | es-errors: 1.3.0 2614 | object-inspect: 1.13.4 2615 | side-channel-list: 1.0.0 2616 | side-channel-map: 1.0.1 2617 | side-channel-weakmap: 1.0.2 2618 | 2619 | signal-exit@4.1.0: {} 2620 | 2621 | source-map-support@0.5.21: 2622 | dependencies: 2623 | buffer-from: 1.1.2 2624 | source-map: 0.6.1 2625 | 2626 | source-map@0.6.1: {} 2627 | 2628 | sprintf-js@1.1.3: 2629 | optional: true 2630 | 2631 | string-width@4.2.3: 2632 | dependencies: 2633 | emoji-regex: 8.0.0 2634 | is-fullwidth-code-point: 3.0.0 2635 | strip-ansi: 6.0.1 2636 | 2637 | string-width@5.1.2: 2638 | dependencies: 2639 | eastasianwidth: 0.2.0 2640 | emoji-regex: 9.2.2 2641 | strip-ansi: 7.1.0 2642 | 2643 | string_decoder@1.3.0: 2644 | dependencies: 2645 | safe-buffer: 5.2.1 2646 | 2647 | strip-ansi@6.0.1: 2648 | dependencies: 2649 | ansi-regex: 5.0.1 2650 | 2651 | strip-ansi@7.1.0: 2652 | dependencies: 2653 | ansi-regex: 6.1.0 2654 | 2655 | strtok3@6.3.0: 2656 | dependencies: 2657 | '@tokenizer/token': 0.3.0 2658 | peek-readable: 4.1.0 2659 | 2660 | sumchecker@3.0.1: 2661 | dependencies: 2662 | debug: 4.4.0 2663 | transitivePeerDependencies: 2664 | - supports-color 2665 | 2666 | supports-color@7.2.0: 2667 | dependencies: 2668 | has-flag: 4.0.0 2669 | 2670 | supports-color@8.1.1: 2671 | dependencies: 2672 | has-flag: 4.0.0 2673 | 2674 | terser@5.39.0: 2675 | dependencies: 2676 | '@jridgewell/source-map': 0.3.6 2677 | acorn: 8.14.1 2678 | commander: 2.20.3 2679 | source-map-support: 0.5.21 2680 | 2681 | timm@1.7.1: {} 2682 | 2683 | tinycolor2@1.6.0: {} 2684 | 2685 | token-types@4.2.1: 2686 | dependencies: 2687 | '@tokenizer/token': 0.3.0 2688 | ieee754: 1.2.1 2689 | 2690 | tr46@0.0.3: {} 2691 | 2692 | tree-kill@1.2.2: {} 2693 | 2694 | tslib@2.8.1: {} 2695 | 2696 | tsx@4.19.3: 2697 | dependencies: 2698 | esbuild: 0.25.3 2699 | get-tsconfig: 4.10.0 2700 | optionalDependencies: 2701 | fsevents: 2.3.3 2702 | 2703 | type-fest@0.13.1: 2704 | optional: true 2705 | 2706 | typescript@5.8.3: {} 2707 | 2708 | undici-types@5.26.5: {} 2709 | 2710 | undici-types@6.21.0: {} 2711 | 2712 | undici@6.21.1: {} 2713 | 2714 | union@0.5.0: 2715 | dependencies: 2716 | qs: 6.14.0 2717 | 2718 | universalify@0.1.2: {} 2719 | 2720 | url-join@4.0.1: {} 2721 | 2722 | utif2@4.1.0: 2723 | dependencies: 2724 | pako: 1.0.11 2725 | 2726 | uuid@11.1.0: {} 2727 | 2728 | webidl-conversions@3.0.1: {} 2729 | 2730 | whatwg-encoding@2.0.0: 2731 | dependencies: 2732 | iconv-lite: 0.6.3 2733 | 2734 | whatwg-fetch@3.6.20: {} 2735 | 2736 | whatwg-url@5.0.0: 2737 | dependencies: 2738 | tr46: 0.0.3 2739 | webidl-conversions: 3.0.1 2740 | 2741 | which@2.0.2: 2742 | dependencies: 2743 | isexe: 2.0.0 2744 | 2745 | wrap-ansi@7.0.0: 2746 | dependencies: 2747 | ansi-styles: 4.3.0 2748 | string-width: 4.2.3 2749 | strip-ansi: 6.0.1 2750 | 2751 | wrap-ansi@8.1.0: 2752 | dependencies: 2753 | ansi-styles: 6.2.1 2754 | string-width: 5.1.2 2755 | strip-ansi: 7.1.0 2756 | 2757 | wrappy@1.0.2: {} 2758 | 2759 | ws@8.18.1: {} 2760 | 2761 | y18n@5.0.8: {} 2762 | 2763 | yargs-parser@21.1.1: {} 2764 | 2765 | yargs@17.7.2: 2766 | dependencies: 2767 | cliui: 8.0.1 2768 | escalade: 3.2.0 2769 | get-caller-file: 2.0.5 2770 | require-directory: 2.1.1 2771 | string-width: 4.2.3 2772 | y18n: 5.0.8 2773 | yargs-parser: 21.1.1 2774 | 2775 | yauzl@2.10.0: 2776 | dependencies: 2777 | buffer-crc32: 0.2.13 2778 | fd-slicer: 1.1.0 2779 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "plugins/*" 3 | -------------------------------------------------------------------------------- /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 | 9 | :root { 10 | --blur-background: color-mix(in srgb, var(--wave-color-solid-base-brighter), transparent 60%); 11 | --blur-radius: 16px; 12 | } 13 | 14 | #footerPlayer { 15 | backdrop-filter: blur(var(--blur-radius)); 16 | } 17 | 18 | [class^="_containerRow_"] { 19 | max-height: none !important; 20 | } 21 | 22 | [class^="_mainContainer_"] { 23 | height: 100vh !important; 24 | background-color: inherit; 25 | } 26 | 27 | [class^="_bar_"] { 28 | position: absolute; 29 | z-index: 100; 30 | backdrop-filter: blur(var(--blur-radius)); 31 | } 32 | 33 | [class^="_sidebarWrapper"], 34 | [class^="_contentArea"], 35 | #main { 36 | padding-top: 30px; 37 | } 38 | 39 | [class^="_contextMenu"]::before, 40 | [class^="_subMenu_"]::before { 41 | content: ""; 42 | position: absolute; 43 | width: 100%; 44 | height: 100%; 45 | top: 0; 46 | left: 0; 47 | backdrop-filter: blur(var(--blur-radius)); 48 | border-radius: 9px; 49 | pointer-events: none; 50 | z-index: -5; 51 | background-color: var(--blur-background); 52 | } 53 | 54 | [class^="_contextMenu_"], 55 | [class^="_subMenu_"] { 56 | position: relative; 57 | background-color: transparent; 58 | } 59 | 60 | #footerPlayer, 61 | #sidebar, 62 | [class^="_bar_"], 63 | [class*="_audioQualityContainerHover_"]:hover, 64 | [class*="_selectItem_"]:hover, 65 | [class*="_createNewPlaylist_"]:hover { 66 | background-color: var(--blur-background) !important; 67 | } 68 | 69 | [class^="_sidebarWrapper_"] { 70 | padding-bottom: 96px; 71 | } 72 | 73 | #feedSidebar, 74 | #playQueueSidebar, 75 | [class*="_playQueueWithoutHeader_"] button { 76 | background-color: var(--blur-background); 77 | backdrop-filter: blur(var(--blur-radius)); 78 | } 79 | 80 | /* Blur background of homepage shortcut items */ 81 | @container (width > 200px) { 82 | [class*="_shortcutItem_"]::after { 83 | backdrop-filter: blur(8px); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "luna/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------