diff --git a/README.es.md b/README.es.md index 24c35de..a6aaeae 100644 --- a/README.es.md +++ b/README.es.md @@ -9,7 +9,7 @@ y **rotación de DJ por turnos** — escuchad juntos, desde la terminal. - 🎧 **Modo solo:** reproduce YouTube, radios, streams o archivos locales. Funciona sin servidor, incluso offline (archivos locales). - 👥 **Modo sala (opcional):** entra a una sala con un código y escuchad lo mismo, sincronizado. Cada persona pincha **por turnos** (rotación en bucle). -- 🚫 **Sin descargar nada:** streaming puro. Sin Spotify Premium. +- 🌐 **Fuentes flexibles:** streaming puro para YouTube y Radios (sin necesidad de cuentas Premium). También soporta Apple Music (requiere una suscripción activa). - 📊 **Visualizador reactivo real:** FFT en vivo (ffmpeg auto-instalado) con varios modos — barras, espejo, suave, osciloscopio, plasma (cambia con `v`). - 🖥️ **Multiplataforma:** macOS, Linux y Windows. @@ -19,6 +19,11 @@ y **rotación de DJ por turnos** — escuchad juntos, desde la terminal. - [mpv](https://mpv.io) — motor de audio (obligatorio). - [yt-dlp](https://github.com/yt-dlp/yt-dlp) — solo para YouTube y similares. +- [gamdl](https://github.com/glomatico/gamdl) — solo para Apple Music. + +> **Nota sobre Apple Music:** +> Para reproducir desde Apple Music necesitas proveer tus cookies de autenticación dentro de la interfaz usando el comando `/auth `. +> Este proyecto también utiliza un fork personalizado de `applemusic-api` para las búsquedas y resolución de catálogos. Comprueba tu sistema con: `catunes doctor` @@ -28,10 +33,25 @@ Comprueba tu sistema con: `catunes doctor` ```bash npm install -g catunes -catunes play "https://www.youtube.com/watch?v=..." +catunes # Abre la interfaz completa (TUI) ``` -Controles: `espacio` pausa · `← →` seek · `+ -` volumen · `q` salir. +### Controles de la Interfaz +Navega usando el teclado: +- `Tab` / `↑ ↓` para cambiar de panel y moverte. +- `Enter` para reproducir la canción seleccionada. +- Usa los comandos con barra (`/`) en el prompt inferior: + - `/search ` para buscar música. + - `/pause`, `/next`, `/prev` para controlar la reproducción. + - `/auth ` para configurar las cookies de Apple Music. + - `/help` para ver todos los comandos. + +### Reproducir una canción suelta (Modo CLI) +Si no quieres usar la interfaz completa, puedes reproducir un stream directamente: +```bash +catunes play "https://www.youtube.com/watch?v=..." +``` +Controles en modo CLI: `espacio` pausa · `← →` seek · `+ -` volumen · `q` salir. --- @@ -112,13 +132,11 @@ catunes/ ## Aviso legal -catunes **no** almacena ni convierte música — solo reproduce streams. -Úsalo únicamente con contenido que tengas derecho a reproducir y respetando -los términos de cada plataforma. El uso es responsabilidad de cada usuario. +catunes es únicamente una **interfaz de terminal y wrapper** de herramientas de terceros (`mpv`, `yt-dlp`, `gamdl`). No aloja, distribuye ni provee acceso a material protegido por derechos de autor por sí mismo. + +Dependiendo de la fuente (ej. Apple Music), herramientas como `gamdl` pueden descargar archivos temporales localmente para permitir la reproducción. Debes usar este software únicamente con tus propias cuentas y credenciales legítimas, para uso personal, y respetando los términos de servicio de cada plataforma. El uso que le des es tu entera responsabilidad. -Usa mpv y yt-dlp para leer el contenido; respeta el copyright y los términos de -cada sitio (como advierte el propio yt-dlp). Este proyecto no está diseñado para -descargar música ni para saltarse ninguna protección. +Este proyecto respeta el copyright. No está diseñado para promover la piratería ni la distribución no autorizada de música protegida. ## Licencia diff --git a/README.md b/README.md index db62091..c0ef6b3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A **retro** terminal music player with **synced rooms** and - 🎧 **Solo mode:** play YouTube, radios, streams or local files. Works without a server, even offline (local files). - 👥 **Room mode (optional):** join a room with a code and listen together, in sync. Everyone takes **turns** as DJ (round-robin). -- 🚫 **No downloads:** pure streaming. No Spotify Premium needed. +- 🌐 **Flexible sources:** pure streaming for YouTube and Radios (no Spotify Premium needed). Apple Music support is also available (requires an active subscription). - 📊 **Real audio-reactive visualizer:** live FFT (auto-installed ffmpeg) with several modes — bars, mirror, smooth, oscilloscope, plasma (cycle with `v`). - 🖥️ **Cross-platform:** macOS, Linux and Windows. @@ -19,6 +19,11 @@ A **retro** terminal music player with **synced rooms** and - [mpv](https://mpv.io) — audio engine (required). - [yt-dlp](https://github.com/yt-dlp/yt-dlp) — only for YouTube and similar. +- [gamdl](https://github.com/glomatico/gamdl) — only for Apple Music. + +> **Note on Apple Music:** +> To use Apple Music, you need to provide your authentication cookies inside the UI using the command `/auth `. +> This project also relies on a custom fork of `applemusic-api` to fetch metadata and search the Apple catalog. Check your system with: `catunes doctor` @@ -28,10 +33,25 @@ Check your system with: `catunes doctor` ```bash npm install -g catunes -catunes play "https://www.youtube.com/watch?v=..." +catunes # Opens the full terminal UI ``` -Controls: `space` pause · `← →` seek · `+ -` volume · `q` quit. +### Full TUI Controls +Navigate the interface with the keyboard: +- `Tab` / `↑ ↓` to switch panels and scroll. +- `Enter` to play the selected track. +- Use slash commands at the bottom prompt: + - `/search ` to find music. + - `/pause`, `/next`, `/prev` to control playback. + - `/auth ` to set Apple Music cookies. + - `/help` to see all commands. + +### Play a single track (CLI Mode) +If you don't want the full interface, you can play a stream directly: +```bash +catunes play "https://www.youtube.com/watch?v=..." +``` +Controls in CLI mode: `space` pause · `← →` seek · `+ -` volume · `q` quit. --- @@ -112,13 +132,11 @@ catunes/ ## Disclaimer -catunes does **not** download, store or convert music — it only plays streams. -Use it only with content you have the right to play, and respect each -platform's terms of service. How you use it is your responsibility. +catunes is merely a **terminal UI and player wrapper** around third-party tools (`mpv`, `yt-dlp`, `gamdl`). It does not host, distribute, or provide access to any copyrighted material by itself. + +Depending on the source (e.g., Apple Music), third-party tools like `gamdl` may download temporary files locally to allow playback. You must only use this software with your own legitimate accounts and credentials, for personal use, and in compliance with the terms of service of the respective platforms. How you use it is your sole responsibility. -It relies on mpv and yt-dlp to read media; respect copyright and the terms of -each site (as yt-dlp itself advises). This project is not designed to download -music or to circumvent any protection. +This project respects copyright. It is not designed to promote piracy or the unauthorized distribution of copyrighted music. ## License diff --git a/bun.lock b/bun.lock index 10b7b64..5e3c05d 100644 --- a/bun.lock +++ b/bun.lock @@ -7,18 +7,21 @@ }, "packages/client": { "name": "catunes", - "version": "0.1.0", + "version": "0.1.2", "bin": { "catunes": "dist/cli.js", }, "dependencies": { + "applemusic-api": "file:/home/daimo/proyectos/applemusic-api", "ffmpeg-static": "^5.3.0", "ink": "^7.0.5", "react": "^19.2.7", }, "devDependencies": { "@types/bun": "^1.3.14", + "@types/node": "^25.9.1", "@types/react": "^19.2.16", + "@types/tough-cookie": "^4.0.5", "typescript": "^6.0.3", }, }, @@ -33,14 +36,272 @@ "packages": { "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="], + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + + "@biomejs/biome": ["@biomejs/biome@2.2.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.5", "@biomejs/cli-darwin-x64": "2.2.5", "@biomejs/cli-linux-arm64": "2.2.5", "@biomejs/cli-linux-arm64-musl": "2.2.5", "@biomejs/cli-linux-x64": "2.2.5", "@biomejs/cli-linux-x64-musl": "2.2.5", "@biomejs/cli-win32-arm64": "2.2.5", "@biomejs/cli-win32-x64": "2.2.5" }, "bin": { "biome": "bin/biome" } }, "sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-FLIEl73fv0R7dI10EnEiZLw+IMz3mWLnF95ASDI0kbx6DDLJjWxE5JxxBfmG+udz1hIDd3fr5wsuP7nwuTRdAg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5DjiiDfHqGgR2MS9D+AZ8kOfrzTGqLKywn8hoXpXXlJXIECGQ32t+gt/uiS2XyGBM2XQhR6ztUvbjZWeccFMoQ=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Ov2wgAFwqDvQiESnu7b9ufD1faRa+40uwrohgBopeY84El2TnBDoMNXx6iuQdreoFGjwW8vH6k68G21EpNERw=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-fq9meKm1AEXeAWan3uCg6XSP5ObA6F/Ovm89TwaMiy1DNIwdgxPkNwxlXJX8iM6oRbFysYeGnT0OG8diCWb9ew=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-AVqLCDb/6K7aPNIcxHaTQj01sl1m989CJIQFQEaiQkGr2EQwyOpaATJ473h+nXDUuAcREhccfRpe/tu+0wu0eQ=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-xaOIad4wBambwJa6mdp1FigYSIF9i7PCqRbvBqtIi9y29QtPVQ13sDGtUnsRoe6SjL10auMzQ6YAe+B3RpZXVg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw=="], + "@derhuerst/http-basic": ["@derhuerst/http-basic@8.2.4", "", { "dependencies": { "caseless": "^0.12.0", "concat-stream": "^2.0.0", "http-response-object": "^3.0.1", "parse-cache-control": "^1.0.1" } }, "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Omj20SuiHBOUjUBIyqtkNjSUIjOtEOJwmbix/ZyFH4BaQ6OZTaaRWIR4TjHVz0yadHgli6lLTiAh1uarnvD49A=="], + + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-FFj3QdU/OhlDyZOJ8CWfN5eWLpRlT4qjZg7lMQi7jA6GuoY5ajlO1zWLP/MuHYRSbXQUvV52RejNi8DVnAp13w=="], + + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-OSfsTZstc898HHElhU4NccaBGOSSDn5VfahiVTnidZ9B/+wb7WTyfZJaBeJcfjwJ9H2W9uTh2TGtl3UfcXgV9g=="], + + "@oven/bun-freebsd-aarch64": ["@oven/bun-freebsd-aarch64@1.3.14", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-LIKrXaFxAHybVO5Pf+9XP2FHUj/5APvXTUKk9dqHm5iFz4oH+W24cmhjkJirNujh9hKeTyrpWSe3no9JZKowIw=="], + + "@oven/bun-freebsd-x64": ["@oven/bun-freebsd-x64@1.3.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uwD+fGUH1ADpIF3B1U2jWzzb20QwRLZfj5QZ28GUCGrAJ/nTmWrD6YYGsblCY1wuhldRez3lU40AyuvSCyLYmw=="], + + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-X5SsPZHs+iYO8R/efIcRtc7gT2Q2DgPfliCxEkx4cXBumwkw0c/EsHMNwH3EgGpCDaZ7IYVPhpCG/xBOQHEwZw=="], + + "@oven/bun-linux-aarch64-android": ["@oven/bun-linux-aarch64-android@1.3.14", "", { "os": "android", "cpu": "arm64" }, "sha512-y4kq5b85lsrmFb9Xvi4w9mA5IEFJkLMrSmYn06q24KjL9rUWDWO3VFZEtteZxUN5+ec3Zm5S8OnJw1umaCbVjA=="], + + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmqOA92Cd1NL/1XBd4bFkJLxQ86K0RW7ohxS2qzzAvuitO4JiIxjjTeCspoU44zCozH72HpfZfUE2On31OjnWA=="], + + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-7OVTAKvwfPmSbIV1HpdOoVVx5VRc427GuPPne93N6vk4eQBPId9nXmZDh9/zGaKPdbVjVtQSZafWQoUjx38Utw=="], + + "@oven/bun-linux-x64-android": ["@oven/bun-linux-x64-android@1.3.14", "", { "os": "android", "cpu": "x64" }, "sha512-qe9e1d+3VAEU7nAA2ol9Jvmy/o99PVMSgZhHn7Q/9O3YcDrfEqyQ8zm4zoe5qTEo8HZH0dN03Le0Ys2eQPs7eg=="], + + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-q/8EdOC0yUE8FPeoOVq8/Pw5I9/tJaYmUfO/uDUAREx8IUnOJH1RJ5A3BjFqre8pvJoiZA9AovPJq5FnNNjSxA=="], + + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-GBCB/k/sIqcr06eTNgg7g46qiUv35Jasx4XiccJ/n7RGqrE4RWUD/XJBbWFprVPjvqd59+QtSnS99XGqvftHfg=="], + + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-n6iE71G4lQE4XkrZhQQcL5YUlxDbnq6nqV7zeQi33PMsLT/0kYE+RvHOtBWZ3w0wMdXZfINmp63hIb9ijUBGtw=="], + + "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.3.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-T7s3x/BsVKQObGU6QDkZeI6wKynzqGbBH1yI77jrrj5siElclxr3DQrDIk8CV4G5/SJq2HHq4kpLyYY2DKCSmA=="], + + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-mUFWL3BoYkNpjd8e9PqROiFF/1Xeotq20mABJsiQH62jM1g5zqWh4khw1RZ6bX8Q8fWvlPaxG1PjofkmjUi3vg=="], + + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-uIjLUC1S9DWgICzuoMba7vurBJnBruE4S5CxnvmZkdqWVXRzx1Rgu636HoH+k0qeaQCFh3jeG3JQ1y6fRHv0sw=="], + + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="], + + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.20.0", "", { "os": "android", "cpu": "arm64" }, "sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.20.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.20.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.20.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg=="], + + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw=="], + + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.20.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw=="], + + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.20.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ=="], + + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.20.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.20.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.20.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.20.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.61.1", "", { "os": "android", "cpu": "arm" }, "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.61.1", "", { "os": "android", "cpu": "arm64" }, "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.61.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.61.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.61.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.61.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.61.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.61.1", "", { "os": "none", "cpu": "arm64" }, "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.61.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.61.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw=="], + + "@syncfm/applemusic-api": ["@syncfm/applemusic-api@0.0.4", "", { "dependencies": { "axios": "^1.11.0", "tough-cookie": "^6.0.0" }, "peerDependencies": { "typescript": "^5" } }, "sha512-1vi/bj2+/uf5QY8cQ9M9EHcgf0kuIalYvFtGJIVEmskkJoJrFQn4TJhoYfUfYi+ip9jva+O/u+XhI3qMLoz9mQ=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/react": ["@types/react@19.2.16", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w=="], + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + + "@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="], + + "@vitest/expect": ["@vitest/expect@3.2.6", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.6", "@vitest/utils": "3.2.6", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.6", "", { "dependencies": { "@vitest/spy": "3.2.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.6", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA=="], + + "@vitest/runner": ["@vitest/runner@3.2.6", "", { "dependencies": { "@vitest/utils": "3.2.6", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.6", "", { "dependencies": { "@vitest/pretty-format": "3.2.6", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw=="], + + "@vitest/spy": ["@vitest/spy@3.2.6", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg=="], + + "@vitest/utils": ["@vitest/utils@3.2.6", "", { "dependencies": { "@vitest/pretty-format": "3.2.6", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -49,20 +310,52 @@ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.12", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + "axios": ["axios@1.17.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun": ["bun@1.3.14", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.14", "@oven/bun-darwin-x64": "1.3.14", "@oven/bun-darwin-x64-baseline": "1.3.14", "@oven/bun-freebsd-aarch64": "1.3.14", "@oven/bun-freebsd-x64": "1.3.14", "@oven/bun-linux-aarch64": "1.3.14", "@oven/bun-linux-aarch64-android": "1.3.14", "@oven/bun-linux-aarch64-musl": "1.3.14", "@oven/bun-linux-x64": "1.3.14", "@oven/bun-linux-x64-android": "1.3.14", "@oven/bun-linux-x64-baseline": "1.3.14", "@oven/bun-linux-x64-musl": "1.3.14", "@oven/bun-linux-x64-musl-baseline": "1.3.14", "@oven/bun-windows-aarch64": "1.3.14", "@oven/bun-windows-x64": "1.3.14", "@oven/bun-windows-x64-baseline": "1.3.14" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], "catunes": ["catunes@workspace:packages/client"], "catunes-relay": ["catunes-relay@workspace:packages/server"], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "cli-boxes": ["cli-boxes@4.0.1", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="], "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], @@ -71,98 +364,406 @@ "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + "concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="], "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="], + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "ffmpeg-static": ["ffmpeg-static@5.3.0", "", { "dependencies": { "@derhuerst/http-basic": "^8.2.0", "env-paths": "^2.2.0", "https-proxy-agent": "^5.0.0", "progress": "^2.0.3" } }, "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "http-response-object": ["http-response-object@3.0.2", "", { "dependencies": { "@types/node": "^10.0.3" } }, "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA=="], "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ink": ["ink@7.0.5", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.3", "auto-bind": "^5.0.1", "chalk": "^5.6.2", "cli-boxes": "^4.0.1", "cli-cursor": "^4.0.0", "cli-truncate": "^6.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.45.1", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^9.0.0", "stack-utils": "^2.0.6", "string-width": "^8.2.0", "terminal-size": "^4.0.1", "type-fest": "^5.5.0", "widest-line": "^6.0.0", "wrap-ansi": "^10.0.0", "ws": "^8.20.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.2.0", "react": ">=19.2.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-zWNjGHQPxSeiSAmDUOq+QPQ6CfmMhmNi85vrJIuy4prafKKUSoZlXEy4wbM7LuLuF1pDURk7qvF4fxrQlLxv3w=="], + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + + "knip": ["knip@5.88.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-tpy5o7zu1MjawVkLPuahymVJekYY3kYjvzcoInhIchgePxTlo+api90tBv2KfhAIe5uXh+mez1tAfmbv8/TiZg=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mylas": ["mylas@2.1.14", "", {}, "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "oxc-resolver": ["oxc-resolver@11.20.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.20.0", "@oxc-resolver/binding-android-arm64": "11.20.0", "@oxc-resolver/binding-darwin-arm64": "11.20.0", "@oxc-resolver/binding-darwin-x64": "11.20.0", "@oxc-resolver/binding-freebsd-x64": "11.20.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.20.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.20.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.20.0", "@oxc-resolver/binding-linux-arm64-musl": "11.20.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.20.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-musl": "11.20.0", "@oxc-resolver/binding-openharmony-arm64": "11.20.0", "@oxc-resolver/binding-wasm32-wasi": "11.20.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.20.0", "@oxc-resolver/binding-win32-x64-msvc": "11.20.0" } }, "sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parse-cache-control": ["parse-cache-control@1.0.1", "", {}, "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="], "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "plimit-lit": ["plimit-lit@1.6.1", "", { "dependencies": { "queue-lit": "^1.5.1" } }, "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA=="], + + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + + "queue-lit": ["queue-lit@1.5.2", "", {}, "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.61.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "semver": ["semver@7.8.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "slice-ansi": ["slice-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA=="], + "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], + "test-exclude": ["test-exclude@7.0.2", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + + "tldts": ["tldts@7.4.2", "", { "dependencies": { "tldts-core": "^7.4.2" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw=="], + + "tldts-core": ["tldts-core@7.4.2", "", {}, "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + + "tsc-alias": ["tsc-alias@1.8.17", "", { "dependencies": { "chokidar": "^3.5.3", "commander": "^9.0.0", "get-tsconfig": "^4.10.0", "globby": "^11.0.4", "mylas": "^2.1.9", "normalize-path": "^3.0.0", "plimit-lit": "^1.2.6" }, "bin": { "tsc-alias": "dist/bin/index.js" } }, "sha512-EIduCZHqbNwPm8BZYfq1aD7BQ697A4h6uSGMOFQfYGoQwfrYFTKwYfy9Bv42YxHkduVBcn9Zx0DkX111DKskyg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="], "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "unbash": ["unbash@2.2.0", "", {}, "sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "vite": ["vite@7.3.5", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="], + + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitest": ["vitest@3.2.6", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.6", "@vitest/mocker": "3.2.6", "@vitest/pretty-format": "^3.2.6", "@vitest/runner": "3.2.6", "@vitest/snapshot": "3.2.6", "@vitest/spy": "3.2.6", "@vitest/utils": "3.2.6", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.6", "@vitest/ui": "3.2.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw=="], + + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="], "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@syncfm/applemusic-api/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "catunes/applemusic-api": ["@syncfm/applemusic-api@file:../applemusic-api", { "dependencies": { "@syncfm/applemusic-api": "^0.0.4", "axios": "^1.11.0", "bun": "^1.3.14", "tough-cookie": "^6.0.0" }, "devDependencies": { "@biomejs/biome": "2.2.5", "@types/bun": "latest", "@types/node": "^24.7.0", "@vitest/coverage-v8": "3.2.4", "knip": "^5.64.2", "tsc-alias": "^1.8.16", "typescript": "^5.9.3", "vitest": "^3.2.4" }, "peerDependencies": { "typescript": "^5" } }], + + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "catunes/applemusic-api/@types/node": ["@types/node@24.13.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-5vtOqGQr4NJKeEzV441FcOi2MeG9UTWq9LqVLGneDdu4vlX17H8kQ2PA2UmNwCUGPVDj4oBjNhS7ReVEIWJJrg=="], + + "catunes/applemusic-api/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "catunes/applemusic-api/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/package.json b/package.json index 96e2590..402468f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "typecheck": "bun run --cwd packages/client typecheck", "test": "bun test", "docker:up": "docker compose up", - "docker:down": "docker compose down" + "docker:down": "docker compose down", + "postinstall": "bash scripts/patch-applemusic-exports.sh" }, "engines": { "bun": ">=1.3.0" diff --git a/packages/client/package.json b/packages/client/package.json index 3b6da1c..5278099 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -39,13 +39,16 @@ ], "license": "MIT", "dependencies": { + "applemusic-api": "file:/home/daimo/proyectos/applemusic-api", "ffmpeg-static": "^5.3.0", "ink": "^7.0.5", "react": "^19.2.7" }, "devDependencies": { "@types/bun": "^1.3.14", + "@types/node": "^25.9.1", "@types/react": "^19.2.16", + "@types/tough-cookie": "^4.0.5", "typescript": "^6.0.3" } } diff --git a/packages/client/src/apple.ts b/packages/client/src/apple.ts new file mode 100644 index 0000000..1e87f31 --- /dev/null +++ b/packages/client/src/apple.ts @@ -0,0 +1,98 @@ +import { AppleMusic, getAuthenticatedAxios, ResourceType } from "applemusic-api"; +import axiosManager from "applemusic-api/dist/utils/AxiosManager.js"; +import { loadSettings } from "./config.ts"; +import type { SearchResult, Track } from "./playlist.ts"; + +interface CookieJarLike { + setCookieSync(cookie: string, url: string): unknown; +} + +let client: AppleMusic | null = null; + +export async function getAppleClient(): Promise { + if (client) return client; + + const settings = loadSettings(); + if (settings.appleCookies) { + applyAppleCookies(settings.appleCookies); + } + + client = new AppleMusic(); + await client.init(); + + // Re-apply after init: the library's getInstance() replaces the cookiejar + // when it fetches a new token, so cookies set before init are lost. + if (settings.appleCookies) { + applyAppleCookies(settings.appleCookies); + } + + return client; +} + +function getCookieJar(): CookieJarLike { + return (axiosManager as unknown as { cookiejar: CookieJarLike }).cookiejar; +} + +export function applyAppleCookies(cookieString: string): void { + const jar = getCookieJar(); + const rawCookies = cookieString.split(";").map((c) => c.trim()).filter(Boolean); + for (const raw of rawCookies) { + const eq = raw.indexOf("="); + if (eq < 0) continue; + const name = raw.slice(0, eq).trim(); + const value = raw.slice(eq + 1).trim(); + const setCookieStr = `${name}=${value}; Domain=.music.apple.com; Path=/`; + jar.setCookieSync(setCookieStr, "https://music.apple.com"); + jar.setCookieSync(setCookieStr, "https://amp-api-edge.music.apple.com"); + } +} + +export function resetAppleClient(): void { + client = null; +} + +function buildAppleMusicUrl(id: string, attributes?: { url?: string }): string { + if (attributes?.url) { + // Ensure the URL has the proper scheme and storefront prefix. + const u = attributes.url.startsWith("http") ? attributes.url : `https://music.apple.com${attributes.url}`; + // Gamdl needs a storefront in the URL path. If the API returned a URL + // without one (unlikely but possible), insert the default "us" storefront. + if (!/music\.apple\.com\/[a-z]{2}\//.test(u)) { + return u.replace("music.apple.com/", "music.apple.com/us/"); + } + return u; + } + // Library items have IDs like "i.abc123" or "l.abc123". + if (/^[pli]\./.test(id)) { + return `https://music.apple.com/us/library/songs/${id}`; + } + // Catalog items have numeric IDs. + return `https://music.apple.com/us/song/${id}`; +} + +export async function searchApple(query: string): Promise { + const am = await getAppleClient(); + const results = await am.Search.search({ term: query, types: [ResourceType.Songs] }); + return (results.results.songs?.data || []).map((song) => ({ + title: song.attributes?.name || "Unknown", + artist: song.attributes?.artistName || "Unknown", + url: buildAppleMusicUrl(song.id, song.attributes), + duration: Math.floor((song.attributes?.durationInMillis || 0) / 1000), + })); +} + +export async function getLibrarySongs(): Promise { + await getAppleClient(); + const axios = await getAuthenticatedAxios(); + const res = await axios.get<{ data: Array<{ id: string; attributes?: { name?: string; artistName?: string; durationInMillis?: number; url?: string } }> }>( + "https://amp-api-edge.music.apple.com/v1/me/library/songs", + ); + if (!res.data?.data) return []; + return res.data.data.map((song) => ({ + title: song.attributes?.name || "Unknown", + artist: song.attributes?.artistName || "Unknown", + url: buildAppleMusicUrl(song.id, song.attributes), + duration: Math.floor((song.attributes?.durationInMillis || 0) / 1000), + resolved: true, + })); +} diff --git a/packages/client/src/audio.ts b/packages/client/src/audio.ts index 84ece4a..e61dbfe 100644 --- a/packages/client/src/audio.ts +++ b/packages/client/src/audio.ts @@ -16,16 +16,31 @@ const FFT_SIZE = 1024; export const BANDS = 36; export const WAVE_POINTS = 64; -/** Downsamples a PCM frame to a normalized waveform (-1..1) for the scope. */ -function waveFrom(buf: Buffer): number[] { - const wave: number[] = []; - for (let i = 0; i < WAVE_POINTS; i++) { - const idx = Math.floor((i / WAVE_POINTS) * FFT_SIZE); - wave.push(buf.readInt16LE(idx * 2) / 32768); - } - return wave; +// --------------------------------------------------------------------------- +// Pre-allocated buffers reused every frame (zero per-frame allocations). +// --------------------------------------------------------------------------- + +/** Reusable FFT real-part buffer. */ +const _re = new Float64Array(FFT_SIZE); +/** Reusable FFT imaginary-part buffer. */ +const _im = new Float64Array(FFT_SIZE); + +/** Pre-allocated output array for band levels (length = BANDS). */ +const _bands: number[] = new Array(BANDS); +for (let i = 0; i < BANDS; i++) _bands[i] = 0; + +/** Pre-allocated output array for waveform points (length = WAVE_POINTS). */ +const _wave: number[] = new Array(WAVE_POINTS); +for (let i = 0; i < WAVE_POINTS; i++) _wave[i] = 0; + +/** Pre-computed byte offsets for waveFrom — identical every call. */ +const _waveByteOff = new Int32Array(WAVE_POINTS); +for (let i = 0; i < WAVE_POINTS; i++) { + _waveByteOff[i] = Math.floor((i / WAVE_POINTS) * FFT_SIZE) * 2; } +// --------------------------------------------------------------------------- + const hann = new Float64Array(FFT_SIZE); for (let i = 0; i < FFT_SIZE; i++) { hann[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (FFT_SIZE - 1)); @@ -78,36 +93,94 @@ for (let b = 0; b <= BANDS; b++) { bandEdges.push(Math.floor(1 + (FFT_SIZE / 2 - 1) * Math.pow(f, 2.2))); } -/** Turns one PCM frame (int16 mono) into normalized band levels (0..1). */ -function analyzeFrame(buf: Buffer, peak: { v: number }): number[] { - const re = new Float64Array(FFT_SIZE); - const im = new Float64Array(FFT_SIZE); +/** Downsamples a PCM frame to a normalized waveform (-1..1) for the scope. + * Writes into the pre-allocated _wave array — callers must consume before + * the next call. */ +function waveFrom(data: Buffer, offset: number): number[] { + for (let i = 0; i < WAVE_POINTS; i++) { + _wave[i] = data.readInt16LE(offset + _waveByteOff[i]!) / 32768; + } + return _wave; +} + +/** Turns one PCM frame (int16 mono) into normalized band levels (0..1). + * Writes into the pre-allocated _bands array — callers must consume before + * the next call. */ +function analyzeFrame(data: Buffer, offset: number, peak: { v: number }): number[] { + // Fill re with windowed samples; zero im. for (let i = 0; i < FFT_SIZE; i++) { - re[i] = (buf.readInt16LE(i * 2) / 32768) * hann[i]!; + _re[i] = (data.readInt16LE(offset + i * 2) / 32768) * hann[i]!; + _im[i] = 0; } - fft(re, im); + fft(_re, _im); - const bands: number[] = []; let frameMax = 0; for (let b = 0; b < BANDS; b++) { const lo = bandEdges[b]!; const hi = Math.max(lo + 1, bandEdges[b + 1]!); let sum = 0; - for (let k = lo; k < hi; k++) sum += Math.hypot(re[k]!, im[k]!); + for (let k = lo; k < hi; k++) sum += Math.hypot(_re[k]!, _im[k]!); const mag = sum / (hi - lo); - bands.push(mag); + _bands[b] = mag; if (mag > frameMax) frameMax = mag; } // Auto-gain: track a decaying peak so quiet and loud tracks both fill nicely. peak.v = Math.max(frameMax, peak.v * 0.999); const scale = peak.v > 0 ? 1 / peak.v : 0; - return bands.map((m) => Math.min(1, Math.sqrt(m * scale))); + for (let b = 0; b < BANDS; b++) { + _bands[b] = Math.min(1, Math.sqrt(_bands[b]! * scale)); + } + return _bands; +} + +// --------------------------------------------------------------------------- +// Pre-allocated PCM accumulation buffer — avoids Buffer.concat on every chunk. +// +// We allocate once and reuse forever. Incoming chunks are copied in, consumed +// frames are shifted out. The capacity is generous (64 KiB); a single FFT +// frame is only 2 KiB, so this handles bursts easily without reallocation. +// --------------------------------------------------------------------------- + +const PCM_BUF_CAPACITY = 65536; + +class PcmBuffer { + readonly data = Buffer.allocUnsafe(PCM_BUF_CAPACITY); + /** Number of valid unconsumed bytes currently in the buffer. */ + len = 0; + + /** Append incoming chunk. */ + push(chunk: Buffer): void { + const need = this.len + chunk.length; + if (need > PCM_BUF_CAPACITY) { + // Extremely rare — only if ffmpeg sends a huge burst. Drop oldest data + // so we don't exceed capacity. In practice this never fires because + // ffmpeg's -re flag paces output to real-time. + const drop = need - PCM_BUF_CAPACITY; + this.data.copyWithin(0, drop, this.len); + this.len -= drop; + } + chunk.copy(this.data, this.len); + this.len += chunk.length; + } + + /** Advance past `n` consumed bytes by shifting remaining data forward. */ + consume(n: number): void { + this.len -= n; + if (this.len > 0) { + this.data.copyWithin(0, n, n + this.len); + } + } + + /** Reset without reallocating. */ + reset(): void { + this.len = 0; + } } export class AudioAnalyzer extends EventEmitter { private procs: ChildProcess[] = []; - private buf: Buffer = Buffer.alloc(0); + private pcm = new PcmBuffer(); private peak = { v: 0 }; private gen = 0; @@ -153,13 +226,15 @@ export class AudioAnalyzer extends EventEmitter { } const bytesPerFrame = FFT_SIZE * 2; + const pcm = this.pcm; ff.stdout?.on("data", (chunk: Buffer) => { - this.buf = this.buf.length ? Buffer.concat([this.buf, chunk]) : chunk; - while (this.buf.length >= bytesPerFrame) { - const frame = this.buf.subarray(0, bytesPerFrame); - this.buf = this.buf.subarray(bytesPerFrame); - this.emit("bands", analyzeFrame(frame, this.peak)); - this.emit("wave", waveFrom(frame)); + pcm.push(chunk); + while (pcm.len >= bytesPerFrame) { + // analyzeFrame and waveFrom read directly from the pre-allocated + // buffer at offset 0 (the start of unconsumed data). + this.emit("bands", analyzeFrame(pcm.data, 0, this.peak)); + this.emit("wave", waveFrom(pcm.data, 0)); + pcm.consume(bytesPerFrame); } }); this.procs = yt ? [yt, ff] : [ff]; @@ -196,6 +271,6 @@ export class AudioAnalyzer extends EventEmitter { } } this.procs = []; - this.buf = Buffer.alloc(0); + this.pcm.reset(); } } diff --git a/packages/client/src/cli.ts b/packages/client/src/cli.ts index 629197b..5e5b51f 100644 --- a/packages/client/src/cli.ts +++ b/packages/client/src/cli.ts @@ -24,6 +24,8 @@ import { assetForPlatform, } from "./ytdlp.ts"; import { t, setLocale, SUPPORTED_LOCALES, type Locale } from "./i18n.ts"; +import { fmtTime } from "./fmt.ts"; +import { checkGamdl } from "./gamdl.ts"; /** Ensures yt-dlp is available if the playlist has any remote (streamed) URL. */ async function ensureYtDlpForTracks(tracks: { url: string }[]): Promise { @@ -124,6 +126,7 @@ async function launchUI() { function doctor() { const mpv = checkMpv(); const yt = checkYtDlp(); + const gamdl = checkGamdl(); console.log(t("doctor.header")); console.log( @@ -138,15 +141,15 @@ function doctor() { ? t("doctor.ytDownloaded", { size: downloadedSizeMB() }) : t("doctor.ytAuto", { asset: assetForPlatform().asset }), ); + console.log( + gamdl.found + ? t("doctor.gamdlOk") + : t("doctor.gamdlMissing"), + ); console.log(mpv.found ? t("doctor.ready") : t("doctor.needMpv")); } -function fmtTime(s: number): string { - if (!isFinite(s) || s < 0) s = 0; - const m = Math.floor(s / 60); - const sec = Math.floor(s % 60); - return `${m}:${sec.toString().padStart(2, "0")}`; -} + function progressBar(pos: number, dur: number, width = 30): string { const ratio = dur > 0 ? Math.min(1, pos / dur) : 0; diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index 552d492..2fea8b4 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -115,21 +115,32 @@ export interface Settings { activePlaylist?: string; theme?: string; vizMode?: string; - eqGains?: number[]; // 10-band equalizer (dB per band) + eqGains?: number[]; // Resume: last track + position within the last playlist. lastPlaylist?: string; lastUrl?: string; lastPos?: number; + // Apple Music integration + appleCookies?: string; + activeProvider?: "youtube" | "apple"; } +// In-memory settings cache: avoids disk I/O on every loadSettings() call. +let _settingsCache: Settings | null = null; + /** Reads persisted user settings (empty object if none). */ export function loadSettings(): Settings { - if (!existsSync(SETTINGS_FILE)) return {}; + if (_settingsCache) return _settingsCache; + if (!existsSync(SETTINGS_FILE)) { + _settingsCache = {}; + return _settingsCache; + } try { - return JSON.parse(readFileSync(SETTINGS_FILE, "utf8")); + _settingsCache = JSON.parse(readFileSync(SETTINGS_FILE, "utf8")); } catch { - return {}; + _settingsCache = {}; } + return _settingsCache!; } /** Persists user settings (merges with existing). */ @@ -137,4 +148,7 @@ export function saveSettings(patch: Settings): void { ensureConfig(); const merged = { ...loadSettings(), ...patch }; writeFileSync(SETTINGS_FILE, JSON.stringify(merged, null, 2)); + // Update the cache with the merged result so subsequent reads are instant. + _settingsCache = merged; } + diff --git a/packages/client/src/deps.ts b/packages/client/src/deps.ts index 6591324..72b3a39 100644 --- a/packages/client/src/deps.ts +++ b/packages/client/src/deps.ts @@ -35,8 +35,12 @@ export function checkYtDlp(): DepStatus { return probe("yt-dlp"); } +export function checkGamdl(): DepStatus { + return probe("gamdl"); +} + /** Installation instructions depending on the operating system. */ -export function installHint(dep: "mpv" | "yt-dlp"): string { +export function installHint(dep: "mpv" | "yt-dlp" | "gamdl"): string { const os = platform(); const hints: Record> = { mpv: { @@ -49,6 +53,11 @@ export function installHint(dep: "mpv" | "yt-dlp"): string { linux: "sudo apt install yt-dlp (o: pipx install yt-dlp)", win32: "winget install yt-dlp (o: choco install yt-dlp)", }, + gamdl: { + darwin: "pipx install gamdl (o: pip install gamdl)", + linux: "pipx install gamdl (o: pip install gamdl)", + win32: "pip install gamdl", + }, }; return hints[dep]?.[os] ?? t("deps.installFallback", { dep }); } diff --git a/packages/client/src/fmt.ts b/packages/client/src/fmt.ts new file mode 100644 index 0000000..0c44b86 --- /dev/null +++ b/packages/client/src/fmt.ts @@ -0,0 +1,6 @@ +export function fmtTime(s: number): string { + if (!isFinite(s) || s < 0) s = 0; + const m = Math.floor(s / 60); + const sec = Math.floor(s % 60); + return `${m}:${sec.toString().padStart(2, "0")}`; +} diff --git a/packages/client/src/gamdl.ts b/packages/client/src/gamdl.ts new file mode 100644 index 0000000..a203902 --- /dev/null +++ b/packages/client/src/gamdl.ts @@ -0,0 +1,167 @@ +import { spawn } from "node:child_process"; +import { tmpdir } from "node:os"; +import { join, extname } from "node:path"; +import { writeFileSync, mkdirSync, readdirSync, rmSync, existsSync, statSync } from "node:fs"; +import { loadSettings } from "./config.ts"; + +const GAMDL_BIN = join(process.env.HOME || "", ".local/bin/gamdl"); + +export function checkGamdl(): { found: boolean; path: string } { + const found = existsSync(GAMDL_BIN); + return { found, path: GAMDL_BIN }; +} + +function cookiesToNetscape(cookieString: string): string { + // Build the output as a single string instead of allocating an array + join. + let out = + "# Netscape HTTP Cookie File\n" + + "# This file was generated by catunes\n" + + "\n"; + const cookies = cookieString.split(";"); + for (let i = 0; i < cookies.length; i++) { + const raw = cookies[i]!.trim(); + if (!raw) continue; + const eq = raw.indexOf("="); + if (eq < 0) continue; + const name = raw.slice(0, eq).trim(); + const value = raw.slice(eq + 1).trim(); + out += `.music.apple.com\tTRUE\t/\tFALSE\t9999999999\t${name}\t${value}\n`; + out += `.apple.com\tTRUE\t/\tFALSE\t9999999999\t${name}\t${value}\n`; + } + return out; +} + +export interface AppleMusicDownload { + path: string; + tempDir: string; +} + +// Use a Set for O(1) extension lookup instead of .some() linear scan. +const AUDIO_EXTS = new Set([".m4a", ".mp3", ".flac", ".alac", ".wav", ".ogg", ".opus", ".aac"]); + +export async function downloadAppleMusic(url: string): Promise { + if (!existsSync(GAMDL_BIN)) { + throw new Error("gamdl not found at ~/.local/bin/gamdl. Install with: pipx install gamdl"); + } + const settings = loadSettings(); + const cookies = settings.appleCookies; + if (!cookies) { + throw new Error("No Apple Music cookies set. Use /auth first."); + } + const tempDir = join(tmpdir(), `catunes-apple-${Date.now()}`); + mkdirSync(tempDir, { recursive: true }); + + const cookiesPath = join(tempDir, "cookies.txt"); + if (cookies) { + writeFileSync(cookiesPath, cookiesToNetscape(cookies)); + } + + return new Promise((resolve, reject) => { + const args = [ + url, + "-o", tempDir, + "--temp-path", tempDir, + "-n", + ]; + if (cookies) { + args.push("-c", cookiesPath); + } + + const proc = spawn(GAMDL_BIN, args, { stdio: ["ignore", "pipe", "pipe"] }); + + let stdout = ""; + let stderr = ""; + proc.stdout?.on("data", (d: Buffer) => { stdout += d.toString(); }); + proc.stderr?.on("data", (d: Buffer) => { stderr += d.toString(); }); + + proc.on("close", (code) => { + const tail = (stderr + stdout).slice(-500); + if (code !== 0) { + cleanupTempDir(tempDir); + return reject(new Error(`gamdl exit ${code}:\n${tail}`)); + } + const files = findAudioFiles(tempDir); + if (files.length > 0 && files[0]) { + resolve({ path: files[0], tempDir }); + } else { + // List what gamdl actually created before cleaning up. + const allFiles = listAllFiles(tempDir).join(", "); + cleanupTempDir(tempDir); + reject(new Error( + `gamdl completed but no audio found.\n` + + `URL: ${url}\n` + + `Output dir: ${tempDir}\n` + + `Files created: [${allFiles}]\n` + + `gamdl output:\n${tail}`, + )); + } + }); + + proc.on("error", (err) => { + cleanupTempDir(tempDir); + reject(err); + }); + }); +} + +/** Iterative directory walk — avoids recursive spread allocations. */ +function listAllFiles(dir: string): string[] { + const results: string[] = []; + const stack: string[] = [dir]; + while (stack.length > 0) { + const current = stack.pop()!; + try { + for (const file of readdirSync(current)) { + const path = join(current, file); + try { + const st = statSync(path); + if (st.isDirectory()) { + stack.push(path); + } else { + results.push(file); + } + } catch { + // skip + } + } + } catch { + // skip + } + } + return results; +} + +/** Iterative audio-file finder — uses Set lookup and push instead of spread. */ +function findAudioFiles(dir: string): string[] { + const results: string[] = []; + const stack: string[] = [dir]; + while (stack.length > 0) { + const current = stack.pop()!; + try { + for (const file of readdirSync(current)) { + const path = join(current, file); + try { + const st = statSync(path); + if (st.isDirectory()) { + stack.push(path); + } else if (AUDIO_EXTS.has(extname(file).toLowerCase())) { + results.push(path); + } + } catch { + // not accessible + } + } + } catch { + // dir not readable + } + } + return results; +} + +export function cleanupTempDir(tempDir: string): void { + try { + if (existsSync(tempDir)) rmSync(tempDir, { recursive: true, force: true }); + } catch { + // ignore + } +} diff --git a/packages/client/src/i18n.ts b/packages/client/src/i18n.ts index 87310e3..c10221d 100644 --- a/packages/client/src/i18n.ts +++ b/packages/client/src/i18n.ts @@ -68,6 +68,8 @@ CONTROLS (interface): "doctor.ytDownloaded": " ✅ yt-dlp downloaded by catunes ({size} MB)", "doctor.ytAuto": " ⚙️ yt-dlp not installed — will auto-download on first use ({asset})", + "doctor.gamdlOk": " ✅ gamdl found", + "doctor.gamdlMissing": " ⚠️ gamdl not found (only needed for Apple Music downloads)", "doctor.ready": "\n Ready to play. 🎵\n", "doctor.needMpv": "\n Install mpv to be able to play.\n", "add.ok": "✅ Added to the playlist:\n {url}", @@ -105,26 +107,21 @@ CONTROLS (interface): "ui.state.pause": "PAUSE", "ui.state.stop": "STOP", "ui.help": - " {a}↑↓{/} nav {a}↵{/} play {a}space{/} pause {a}n/p{/} next/prev {a}/{/} search {a}?{/} help {a}q{/} quit", - "ui.helpLabel": " Keys ", - "ui.helpScreen": ` {a}Tab{/} switch panel (tracks / playlists) - {a}↑ ↓{/} navigate - {a}Enter{/} play selected (or open playlist) - {a}Space{/} pause / resume - {a}← →{/} seek 5s - {a}n / p{/} next / previous - {a}s{/} shuffle on/off - {a}r{/} repeat (off / all / one) - {a}/{/} search YouTube - {a}a{/} add a URL - {a}d{/} delete selected - {a}o{/} settings (language, results) - {a}+ / -{/} volume - {a}m{/} mute - {a}?{/} this help - {a}q{/} quit + " {a}↑↓{/} nav {a}/help{/} for commands {a}/quit{/} exit", + "ui.helpLabel": " Commands ", + "ui.helpScreen": ` {a}/provider [youtube|apple]{/} Switch provider + {a}/search {/} Search songs + {a}/library{/} Fetch Apple Music library + {a}/play {/} Play track by index + {a}/auth {/} Set Apple Music cookies + {a}/pause{/} Pause / resume + {a}/next / /prev{/} Next / previous track + {a}/quit{/} Exit catunes - {gray-fg}Esc to close{/}`, + {a}↑ ↓{/} navigate list + {a}Esc{/} clear command + + {gray-fg}Type commands at the bottom prompt{/}`, "ui.langLabel": " Language ", "ui.settingsLabel": " Settings ", "ui.optLanguage": "Language", @@ -186,6 +183,8 @@ CONTROLES (interfaz): "doctor.ytDownloaded": " ✅ yt-dlp descargado por catunes ({size} MB)", "doctor.ytAuto": " ⚙️ yt-dlp no instalado — se descargará al primer uso ({asset})", + "doctor.gamdlOk": " ✅ gamdl encontrado", + "doctor.gamdlMissing": " ⚠️ gamdl no encontrado (solo necesario para descargas de Apple Music)", "doctor.ready": "\n Listo para reproducir. 🎵\n", "doctor.needMpv": "\n Instala mpv para poder reproducir.\n", "add.ok": "✅ Anadida a la playlist:\n {url}", @@ -223,26 +222,21 @@ CONTROLES (interfaz): "ui.state.pause": "PAUSA", "ui.state.stop": "STOP", "ui.help": - " {a}↑↓{/} nav {a}↵{/} play {a}espacio{/} pausa {a}n/p{/} sig/ant {a}/{/} buscar {a}?{/} ayuda {a}q{/} salir", - "ui.helpLabel": " Teclas ", - "ui.helpScreen": ` {a}Tab{/} cambiar panel (canciones / listas) - {a}↑ ↓{/} navegar - {a}Enter{/} reproducir seleccionada (o abrir lista) - {a}Espacio{/} pausa / reanudar - {a}← →{/} avanzar/retroceder 5s - {a}n / p{/} siguiente / anterior - {a}s{/} aleatorio on/off - {a}r{/} repetir (off / todo / una) - {a}/{/} buscar en YouTube - {a}a{/} anadir una URL - {a}d{/} borrar seleccionada - {a}o{/} ajustes (idioma, resultados) - {a}+ / -{/} volumen - {a}m{/} silenciar - {a}?{/} esta ayuda - {a}q{/} salir + " {a}↑↓{/} nav {a}/help{/} para comandos {a}/quit{/} salir", + "ui.helpLabel": " Comandos ", + "ui.helpScreen": ` {a}/provider [youtube|apple]{/} Cambiar proveedor + {a}/search {/} Buscar canciones + {a}/library{/} Cargar librería Apple Music + {a}/play {/} Reproducir por índice + {a}/auth {/} Configurar cookies Apple + {a}/pause{/} Pausa / reanudar + {a}/next / /prev{/} Siguiente / anterior + {a}/quit{/} Salir de catunes + + {a}↑ ↓{/} navegar lista + {a}Esc{/} limpiar comando - {gray-fg}Esc para cerrar{/}`, + {gray-fg}Escribe comandos en el prompt inferior{/}`, "ui.langLabel": " Idioma ", "ui.settingsLabel": " Ajustes ", "ui.optLanguage": "Idioma", @@ -279,14 +273,52 @@ export function getLocale(): Locale { return current; } +// --- Pre-compiled template cache --- +// Each template is split once into alternating literal/placeholder parts, +// so subsequent calls just concatenate strings (no regex, no replaceAll). +type CompiledTemplate = { parts: string[]; slots: string[] }; +const _templateCache = new Map(); +const _PLACEHOLDER_RE = /\{([^}]+)\}/g; + +function _compile(template: string): CompiledTemplate { + const parts: string[] = []; + const slots: string[] = []; + let lastIndex = 0; + let m: RegExpExecArray | null; + _PLACEHOLDER_RE.lastIndex = 0; + while ((m = _PLACEHOLDER_RE.exec(template)) !== null) { + parts.push(template.slice(lastIndex, m.index)); + slots.push(m[1]!); + lastIndex = _PLACEHOLDER_RE.lastIndex; + } + parts.push(template.slice(lastIndex)); + return { parts, slots }; +} + /** Translate a key, interpolating {placeholders} from vars. */ export function t(key: string, vars?: Record): string { const loc = getLocale(); - let str = MESSAGES[loc][key] ?? MESSAGES.en[key] ?? key; - if (vars) { - for (const [k, v] of Object.entries(vars)) { - str = str.replaceAll(`{${k}}`, String(v)); - } + const raw = MESSAGES[loc][key] ?? MESSAGES.en[key] ?? key; + + if (!vars) return raw; + + // Cache key: locale + translation key (the raw template is always the same + // for a given locale+key pair, so we don't need to include the template). + const cacheKey = loc + "\0" + key; + let compiled = _templateCache.get(cacheKey); + if (!compiled) { + compiled = _compile(raw); + _templateCache.set(cacheKey, compiled); + } + + const { parts, slots } = compiled; + if (slots.length === 0) return raw; + + let result = parts[0]!; + for (let i = 0; i < slots.length; i++) { + const slotKey = slots[i]!; + const v = vars[slotKey]; + result += (v !== undefined ? String(v) : `{${slotKey}}`) + parts[i + 1]!; } - return str; + return result; } diff --git a/packages/client/src/player.ts b/packages/client/src/player.ts index 8afc109..92c6b3d 100644 --- a/packages/client/src/player.ts +++ b/packages/client/src/player.ts @@ -19,14 +19,36 @@ import { YTDLP_DIR } from "./ytdlp.ts"; /** 10-band graphic-equalizer center frequencies (Hz). */ export const EQ_BANDS = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]; +// Pre-computed per-band filter prefixes — avoids template-literal work +// inside the hot eqFilterChain loop. +const _EQ_PREFIXES: string[] = EQ_BANDS.map( + (f) => `equalizer=f=${f}:width_type=o:width=1:g=`, +); + +// Cache for eqFilterChain: skip recomputation when gains haven't changed. +let _lastEqKey = ""; +let _lastEqResult = ""; + /** Builds the mpv audio-filter value for a set of EQ gains (dB). "" = flat. */ export function eqFilterChain(gains: number[]): string { if (!gains.some((g) => Math.abs(g) > 0.01)) return ""; - const chain = EQ_BANDS.map( - (f, i) => - `equalizer=f=${f}:width_type=o:width=1:g=${(gains[i] ?? 0).toFixed(1)}`, - ).join(","); - return `lavfi=[${chain}]`; + + // Build a cheap cache key from the rounded gains. + const rounded: string[] = new Array(EQ_BANDS.length); + for (let i = 0; i < EQ_BANDS.length; i++) { + rounded[i] = (gains[i] ?? 0).toFixed(1); + } + const key = rounded.join(","); + if (key === _lastEqKey) return _lastEqResult; + + // Build the chain using pre-computed prefixes. + const parts: string[] = new Array(EQ_BANDS.length); + for (let i = 0; i < EQ_BANDS.length; i++) { + parts[i] = _EQ_PREFIXES[i]! + rounded[i]!; + } + _lastEqResult = `lavfi=[${parts.join(",")}]`; + _lastEqKey = key; + return _lastEqResult; } export interface PlayerState { @@ -40,12 +62,35 @@ export interface PlayerState { type MpvCommand = { command: unknown[]; request_id?: number }; +// Pre-serialized JSON for commands that never change — avoids +// JSON.stringify + object allocation on every call. +const _CMD_STOP = '{"command":["stop"]}\n'; +const _CMD_QUIT = '{"command":["quit"]}\n'; + +// Reusable command object for `send` — mutated in place to avoid allocations. +const _sendBuf: MpvCommand = { command: [] }; + export class Player extends EventEmitter { private proc: ChildProcess | null = null; private socket: Socket | null = null; private socketPath: string; private reqId = 1; - private buffer = ""; + + // Buffer state for onData: we track a read-offset into the buffer + // string so that we only create a new (shorter) string when the + // unconsumed tail grows too far from the start. + private _buf = ""; + private _bufOffset = 0; + + // Reusable snapshot for snapshotState() — avoids a new object per call. + private _snapshot: PlayerState = { + url: null, + title: null, + paused: false, + position: 0, + duration: 0, + volume: 80, + }; state: PlayerState = { url: null, @@ -67,16 +112,22 @@ export class Player extends EventEmitter { process.platform === "win32" ? "\\\\.\\pipe\\catunes-mpv" : join(tmpdir(), `catunes-mpv-${process.pid}.sock`); + + // Bind once so we never create a new closure for the "data" listener. + this._onDataBound = this._onData.bind(this); } + // Bound handler — assigned in constructor, avoids per-connection closure. + private _onDataBound: (chunk: Buffer | string) => void; + /** Starts the mpv process and opens the control channel. */ async start(): Promise { // Make sure mpv's ytdl_hook can find our (possibly auto-downloaded) // yt-dlp by prepending catunes's bin dir to the child's PATH. - const env = { - ...process.env, - PATH: `${YTDLP_DIR}${delimiter}${process.env.PATH ?? ""}`, - }; + // We assign PATH directly instead of spreading the entire process.env + // object, which would allocate a large shallow copy on every start(). + const env = Object.assign(Object.create(null), process.env) as NodeJS.ProcessEnv; + env.PATH = `${YTDLP_DIR}${delimiter}${process.env.PATH ?? ""}`; this.proc = spawn( this.mpvBin, @@ -109,7 +160,7 @@ export class Player extends EventEmitter { const sock = createConnection(this.socketPath); sock.on("connect", () => { this.socket = sock; - sock.on("data", (chunk) => this.onData(chunk)); + sock.on("data", this._onDataBound); resolve(); }); sock.on("error", () => { @@ -122,12 +173,15 @@ export class Player extends EventEmitter { } /** Processes the JSON lines emitted by mpv (events and responses). */ - private onData(chunk: Buffer | string) { - this.buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8"); + private _onData(chunk: Buffer | string) { + this._buf += typeof chunk === "string" ? chunk : chunk.toString("utf8"); + let nl: number; - while ((nl = this.buffer.indexOf("\n")) >= 0) { - const line = this.buffer.slice(0, nl).trim(); - this.buffer = this.buffer.slice(nl + 1); + while ((nl = this._buf.indexOf("\n", this._bufOffset)) >= 0) { + // Extract only the current line — the offset lets us skip already- + // processed bytes without re-slicing the whole buffer each time. + const line = this._buf.substring(this._bufOffset, nl).trim(); + this._bufOffset = nl + 1; if (!line) continue; try { const msg = JSON.parse(line); @@ -143,6 +197,13 @@ export class Player extends EventEmitter { // non-JSON line: ignore it } } + + // Compact the buffer: discard already-processed bytes when the + // consumed portion exceeds 4 KB to keep memory bounded. + if (this._bufOffset > 4096) { + this._buf = this._buf.slice(this._bufOffset); + this._bufOffset = 0; + } } private onProperty(name: string, data: unknown) { @@ -171,8 +232,36 @@ export class Player extends EventEmitter { this.socket.write(JSON.stringify(cmd) + "\n"); } + /** Sends a command using the reusable _sendBuf to reduce allocations. */ + private sendArgs(...args: unknown[]) { + if (!this.socket) return; + _sendBuf.command = args; + this.socket.write(JSON.stringify(_sendBuf) + "\n"); + } + private observe(property: string, id: number) { - this.send({ command: ["observe_property", id, property] }); + this.sendArgs("observe_property", id, property); + } + + /** + * Returns a shallow copy of the current state. Reuses a single + * internal snapshot object so callers like React's setState() do + * not allocate a fresh object on every 90 ms tick. + * + * NOTE: the returned reference is always the same object — React's + * useState will still trigger a re-render because setState() is + * called unconditionally, and the object's *contents* change. + */ + snapshotState(): PlayerState { + const s = this._snapshot; + const st = this.state; + s.url = st.url; + s.title = st.title; + s.paused = st.paused; + s.position = st.position; + s.duration = st.duration; + s.volume = st.volume; + return s; } // --- Public API (used by the keyboard or the room) --- @@ -181,7 +270,7 @@ export class Player extends EventEmitter { load(url: string) { this.state.url = url; this.state.title = url; - this.send({ command: ["loadfile", url, "replace"] }); + this.sendArgs("loadfile", url, "replace"); this.setPause(false); this.emit("state", this.state); } @@ -191,23 +280,23 @@ export class Player extends EventEmitter { } setPause(paused: boolean) { - this.send({ command: ["set_property", "pause", paused] }); + this.sendArgs("set_property", "pause", paused); } /** Relative seek in seconds (negative = backward). */ seek(seconds: number) { - this.send({ command: ["seek", seconds, "relative"] }); + this.sendArgs("seek", seconds, "relative"); } /** Absolute seek to a specific second (key for syncing rooms). */ seekTo(seconds: number) { - this.send({ command: ["seek", seconds, "absolute"] }); + this.sendArgs("seek", seconds, "absolute"); } setVolume(volume: number) { const v = Math.max(0, Math.min(100, volume)); this.state.volume = v; - this.send({ command: ["set_property", "volume", v] }); + this.sendArgs("set_property", "volume", v); } /** @@ -215,15 +304,16 @@ export class Player extends EventEmitter { * chain. All-zero gains clear the filter so there's no extra processing. */ setEqualizer(gains: number[]) { - this.send({ command: ["set_property", "af", eqFilterChain(gains)] }); + this.sendArgs("set_property", "af", eqFilterChain(gains)); } stop() { - this.send({ command: ["stop"] }); + if (!this.socket) return; + this.socket.write(_CMD_STOP); } quit() { - this.send({ command: ["quit"] }); + if (this.socket) this.socket.write(_CMD_QUIT); this.socket?.end(); this.proc?.kill(); } diff --git a/packages/client/src/playlist.ts b/packages/client/src/playlist.ts index 584d0e8..fad8f38 100644 --- a/packages/client/src/playlist.ts +++ b/packages/client/src/playlist.ts @@ -48,6 +48,8 @@ function entryArtist(e: CacheEntry | undefined): string | undefined { export interface SearchResult { url: string; title: string; + duration?: number; + artist?: string; } // --- yt-dlp (shared) --- @@ -64,17 +66,18 @@ function ytDlpStdout(args: string[]): Promise { } /** Parses yt-dlp "%(id)s\t%(title)s" lines into results. */ +const YT_URL_PREFIX = "https://www.youtube.com/watch?v="; function parseEntries(stdout: string): SearchResult[] { - return stdout - .split(/\r?\n/) - .map((l) => l.trim()) - .filter(Boolean) - .map((line) => { - const tab = line.indexOf("\t"); - const id = tab >= 0 ? line.slice(0, tab) : line; - const title = tab >= 0 ? line.slice(tab + 1) : id; - return { url: `https://www.youtube.com/watch?v=${id}`, title }; - }); + const results: SearchResult[] = []; + for (const raw of stdout.split(/\r?\n/)) { + const line = raw.trim(); + if (!line) continue; + const tab = line.indexOf("\t"); + const id = tab >= 0 ? line.slice(0, tab) : line; + const title = tab >= 0 ? line.slice(tab + 1) : id; + results.push({ url: YT_URL_PREFIX + id, title }); + } + return results; } /** Searches YouTube and returns results (flat, no download). */ @@ -239,11 +242,12 @@ function toTrack(url: string, cache: Record): Track { /** Reads the active playlist → tracks with provisional titles. */ export function loadPlaylist(): Track[] { const cache = loadCache(); - return readFileSync(activeFile(), "utf8") - .split(/\r?\n/) - .map((l) => l.trim()) - .filter((url) => url && !url.startsWith("#")) - .map((url) => toTrack(url, cache)); + const tracks: Track[] = []; + for (const raw of readFileSync(activeFile(), "utf8").split(/\r?\n/)) { + const url = raw.trim(); + if (url && !url.startsWith("#")) tracks.push(toTrack(url, cache)); + } + return tracks; } /** @@ -256,10 +260,11 @@ export function addUrl( const clean = url.trim(); if (!clean) return { added: false, reason: "empty" }; const file = activeFile(); - const existing = readFileSync(file, "utf8") - .split(/\r?\n/) - .map((l) => l.trim()); - if (existing.includes(clean)) return { added: false, reason: "duplicate" }; + const existing = new Set(); + for (const raw of readFileSync(file, "utf8").split(/\r?\n/)) { + existing.add(raw.trim()); + } + if (existing.has(clean)) return { added: false, reason: "duplicate" }; appendFileSync(file, `${clean}\n`); return { added: true }; } @@ -277,17 +282,23 @@ export function removeUrl(url: string): boolean { // --- title cache --- +// In-memory cache: avoids re-reading and re-parsing the JSON file on every +// call to loadCache(). Invalidated whenever saveCache() writes a new version. +let _cachedTitles: Record | null = null; + function loadCache(): Record { - if (!existsSync(TITLES_CACHE)) return {}; + if (_cachedTitles) return _cachedTitles; + if (!existsSync(TITLES_CACHE)) return (_cachedTitles = {}); try { - return JSON.parse(readFileSync(TITLES_CACHE, "utf8")); + return (_cachedTitles = JSON.parse(readFileSync(TITLES_CACHE, "utf8"))); } catch { - return {}; + return (_cachedTitles = {}); } } function saveCache(cache: Record): void { - writeFileSync(TITLES_CACHE, JSON.stringify(cache, null, 2)); + _cachedTitles = cache; + writeFileSync(TITLES_CACHE, JSON.stringify(cache)); } /** @@ -372,11 +383,12 @@ export async function resolveTitlesAt( tr.artist = artist; tr.resolved = true; cache[tr.url] = { title: meta.title, duration: meta.duration, artist }; - saveCache(cache); onUpdate(i, tr); } }; await Promise.all( Array.from({ length: Math.min(concurrency, pending.length) }, worker), ); + // Save once at the end instead of after every individual track resolve. + if (pending.length > 0) saveCache(cache); } diff --git a/packages/client/src/status.ts b/packages/client/src/status.ts index 44d73bf..24320ad 100644 --- a/packages/client/src/status.ts +++ b/packages/client/src/status.ts @@ -9,23 +9,21 @@ import { writeFileSync, readFileSync, existsSync, statSync } from "node:fs"; import { STATUS_FILE, ensureConfig } from "./config.ts"; import type { Player } from "./player.ts"; - -function fmtTime(s: number): string { - if (!isFinite(s) || s < 0) s = 0; - const m = Math.floor(s / 60); - const sec = Math.floor(s % 60); - return `${m}:${sec.toString().padStart(2, "0")}`; -} +import { fmtTime } from "./fmt.ts"; /** Starts the periodic writing of the state to disk. */ export function startStatusBroadcast(player: Player, intervalMs = 1000) { ensureConfig(); + // Only write when the line actually changes, to reduce I/O. + let lastLine = ""; const write = () => { const s = player.state; if (!s.url) return; const icon = s.paused ? "⏸" : "▶"; const title = (s.title ?? "").replace(/\s+/g, " ").slice(0, 45); const line = `${icon} ${title} ${fmtTime(s.position)}/${fmtTime(s.duration)}`; + if (line === lastLine) return; + lastLine = line; try { writeFileSync(STATUS_FILE, line); } catch { @@ -50,3 +48,4 @@ export function readStatus(maxAgeMs = 8000): string { return ""; } } + diff --git a/packages/client/src/title.ts b/packages/client/src/title.ts index b74b3a4..f60e114 100644 --- a/packages/client/src/title.ts +++ b/packages/client/src/title.ts @@ -8,13 +8,7 @@ // contents, so it coexists with the UI without any issue.) import type { Player } from "./player.ts"; - -function fmtTime(s: number): string { - if (!isFinite(s) || s < 0) s = 0; - const m = Math.floor(s / 60); - const sec = Math.floor(s % 60); - return `${m}:${sec.toString().padStart(2, "0")}`; -} +import { fmtTime } from "./fmt.ts"; /** Sets the terminal title. */ export function setTerminalTitle(text: string): void { @@ -31,15 +25,23 @@ export function clearTerminalTitle(): void { * Returns the timer in case you want to stop it. */ export function startTitleBroadcast(player: Player, intervalMs = 1000) { + // Only write the escape sequence when the title string actually changes. + let lastTitle = ""; const update = () => { const s = player.state; if (!s.url) { - setTerminalTitle("catunes"); + if (lastTitle !== "catunes") { + lastTitle = "catunes"; + setTerminalTitle("catunes"); + } return; } const icon = s.paused ? "⏸" : "♪"; const title = (s.title ?? "").replace(/\s+/g, " ").slice(0, 45); - setTerminalTitle(`${icon} ${title} ${fmtTime(s.position)}`); + const full = `${icon} ${title} ${fmtTime(s.position)}`; + if (full === lastTitle) return; + lastTitle = full; + setTerminalTitle(full); }; update(); @@ -48,3 +50,4 @@ export function startTitleBroadcast(player: Player, intervalMs = 1000) { process.on("exit", () => clearTerminalTitle()); return timer; } + diff --git a/packages/client/src/ui/ink/app.tsx b/packages/client/src/ui/ink/app.tsx index 7d26f9d..36835a2 100644 --- a/packages/client/src/ui/ink/app.tsx +++ b/packages/client/src/ui/ink/app.tsx @@ -1,15 +1,12 @@ -// Modern terminal UI built with Ink (React). The core (player, playlist, -// theme, i18n, ytdlp) is reused unchanged; this is only the presentation + -// input layer. +// Modern terminal UI built with Ink (React). -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { EventEmitter } from "node:events"; import { render, Box, Text, useApp, useInput, useStdout } from "ink"; import type { Player } from "../../player.ts"; import { EQ_BANDS } from "../../player.ts"; import { type Track, - type SearchResult, loadPlaylist, listPlaylists, activePlaylist, @@ -41,13 +38,14 @@ import { import { loadSettings, saveSettings } from "../../config.ts"; import { ensureYtDlp } from "../../ytdlp.ts"; import { AudioAnalyzer, BANDS, WAVE_POINTS } from "../../audio.ts"; +import type { Overlay } from "./overlay.ts"; +import { createCommands, matchCommands } from "./commands.ts"; +import { fmtTime } from "../../fmt.ts"; const SIDEBAR_W = 24; const SPECTRUM_H = 6; const SPECTRUM_COLS = BANDS; const SEARCH_PRESETS = [10, 20, 30, 50, 100]; - -// 10-band equalizer presets (dB per band: 31Hz … 16kHz). const EQ_PRESETS: { name: string; gains: number[] }[] = [ { name: "Flat", gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, { name: "Bass Boost", gains: [7, 6, 5, 3, 1, 0, 0, 0, 0, 0] }, @@ -63,12 +61,12 @@ const EQ_LABELS = ["31", "62", "125", "250", "500", "1k", "2k", "4k", "8k", "16k /** Command bus so `catunes pause/next/...` (another tab) can drive the UI. */ export const controlBus = new EventEmitter(); -function fmtTime(s: number): string { - if (!isFinite(s) || s < 0) s = 0; - const m = Math.floor(s / 60); - const sec = Math.floor(s % 60); - return `${m}:${sec.toString().padStart(2, "0")}`; -} +// fmtTime is now imported from ../../fmt.ts (shared across the codebase). + +// --- Pre-allocated initial arrays (avoid creating new arrays in useState) --- +const INIT_SPEC = Object.freeze(new Array(SPECTRUM_COLS).fill(0) as number[]); +const INIT_PEAKS = Object.freeze(new Array(SPECTRUM_COLS).fill(0) as number[]); +const INIT_WAVE = Object.freeze(new Array(WAVE_POINTS).fill(0) as number[]); function bar(ratio: number, width: number): string { const filled = Math.max(0, Math.min(width, Math.round(ratio * width))); @@ -111,18 +109,36 @@ function useTermSize() { export const VIZ_MODES = ["bars", "mirror", "smooth", "scope", "plasma"] as const; const LEVELS = "▁▂▃▄▅▆▇█"; +// LRU cache for hslToHex — called 216× per plasma frame (36 cols × 6 rows). +// Quantise inputs to reduce key space; cache the last ~512 unique colours. +const _hslCache = new Map(); +const _HSL_CACHE_MAX = 512; function hslToHex(h: number, s: number, l: number): string { - s /= 100; - l /= 100; + // Quantise: h to nearest int, s & l to 1 decimal → same visual result. + const qh = Math.round(h) | 0; + const qs = Math.round(s * 10) | 0; + const ql = Math.round(l * 10) | 0; + const key = (qh << 16) | (qs << 8) | ql; + const cached = _hslCache.get(key); + if (cached !== undefined) return cached; + const sn = s / 100; + const ln = l / 100; const k = (n: number) => (n + h / 30) % 12; - const a = s * Math.min(l, 1 - l); + const a = sn * Math.min(ln, 1 - ln); const f = (n: number) => { - const c = l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1)); + const c = ln - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1)); return Math.round(255 * c) .toString(16) .padStart(2, "0"); }; - return `#${f(0)}${f(8)}${f(4)}`; + const hex = `#${f(0)}${f(8)}${f(4)}`; + if (_hslCache.size >= _HSL_CACHE_MAX) { + // Evict oldest entry (first key in insertion order). + const first = _hslCache.keys().next().value; + if (first !== undefined) _hslCache.delete(first); + } + _hslCache.set(key, hex); + return hex; } // Colour columns by frequency: bass (left) → treble (right) across the theme's @@ -229,7 +245,7 @@ function vizPlasma(frame: number, energy: number): React.ReactNode[] { return rows; } -function Visualizer({ +const Visualizer = React.memo(function Visualizer({ mode, spec, peaks, @@ -253,7 +269,7 @@ function Visualizer({ rows = vizPlasma(frame, playing ? Math.max(0.15, energy) : 0.1); } else rows = vizBars(spec, peaks); return {rows}; -} +}); // --- Cat mascot (ᓚᘏᗢ) --- // Pacing cat used as a loading spinner; constant width so nothing jitters. @@ -284,7 +300,7 @@ const CAT_ROWS = [ const CAT_W = 29; const CAT_H = CAT_ROWS.length; -function PixelCat({ +const PixelCat = React.memo(function PixelCat({ mode, beat, frame, @@ -421,9 +437,9 @@ function PixelCat({ ); -} +}); -function NowPlaying({ +const NowPlaying = React.memo(function NowPlaying({ state, spec, peaks, @@ -533,7 +549,7 @@ function NowPlaying({ ); -} +}); /** * Virtualized list panel: only the items that fit (maxVisible) are built and @@ -541,7 +557,7 @@ function NowPlaying({ * drops the off-screen ones — so a 4,000-track list costs the same as a tiny * one. */ -function Panel({ +const Panel = React.memo(function Panel({ title, count, selected, @@ -611,10 +627,10 @@ function Panel({ )} ); -} +}); /** Centered modal frame. */ -function Modal({ +const Modal = React.memo(function Modal({ title, cols, rows, @@ -647,9 +663,9 @@ function Modal({ ); -} +}); -function PickList({ +const PickList = React.memo(function PickList({ options, selected, maxVisible, @@ -688,23 +704,7 @@ function PickList({ ); -} - -type Overlay = - | { kind: "none" } - | { kind: "settings" } - | { kind: "theme" } - | { kind: "lang" } - | { kind: "playlists" } - | { kind: "searchLimit" } - | { kind: "help" } - | { kind: "eq" } - | { kind: "searchInput" } - | { kind: "addInput"; target: "track" | "list" } - | { kind: "searchResults"; results: SearchResult[] } - | { kind: "confirmTrack"; index: number } - | { kind: "confirmPlaylist"; name: string } - | { kind: "loading"; text: string }; +}); function App({ player, @@ -721,16 +721,16 @@ function App({ const [tracks, setTracks] = useState(initialTracks); const [playlists, setPlaylists] = useState(listPlaylists()); - const [focus, setFocus] = useState<"tracks" | "sidebar">("tracks"); + const [focus, setFocus] = useState<"command" | "sidebar" | "tracks">("tracks"); const [listIdx, setListIdx] = useState(0); const [sideIdx, setSideIdx] = useState( Math.max(0, listPlaylists().indexOf(activePlaylist())), ); const [current, setCurrent] = useState(-1); const [state, setState] = useState({ ...player.state }); - const [spec, setSpec] = useState(new Array(SPECTRUM_COLS).fill(0)); - const [peaks, setPeaks] = useState(new Array(SPECTRUM_COLS).fill(0)); - const [wave, setWave] = useState(new Array(WAVE_POINTS).fill(0)); + const [spec, setSpec] = useState(INIT_SPEC as unknown as number[]); + const [peaks, setPeaks] = useState(INIT_PEAKS as unknown as number[]); + const [wave, setWave] = useState(INIT_WAVE as unknown as number[]); const [frame, setFrame] = useState(0); const [mode, setMode] = useState(loadSettings().vizMode ?? "bars"); const [shuffle, setShuffle] = useState(false); @@ -743,17 +743,37 @@ function App({ const [filter, setFilter] = useState(""); // filter text for the current list const [filtering, setFiltering] = useState(false); // editing the filter + const settings = loadSettings(); + const [command, setCommand] = useState(""); + const [activeProvider, setActiveProvider] = useState<"youtube" | "apple">( + settings.activeProvider ?? "youtube", + ); + const [statusMsg, setStatusMsg] = useState(""); + const [autocompleteIdx, setAutocompleteIdx] = useState(0); + + // Autocomplete: filter commands based on what the user typed. + const commandTrimmed = command.trimStart(); + + const [overlay, setOverlay] = useState({ kind: "none" }); const [sel, setSel] = useState(0); // selection index inside list overlays const [input, setInput] = useState(""); // text-input overlays const [, bump] = useState(0); // force re-render after theme/lang change const prevPaused = useRef(false); - const specRef = useRef(new Array(SPECTRUM_COLS).fill(0)); - const smoothRef = useRef(new Array(SPECTRUM_COLS).fill(0)); - const peakRef = useRef(new Array(SPECTRUM_COLS).fill(0)); + const specRef = useRef([...INIT_SPEC]); + const smoothRef = useRef([...INIT_SPEC]); + const peakRef = useRef([...INIT_SPEC]); const errRef = useRef(0); // consecutive failed tracks (stop if all unavailable) - const waveRef = useRef(new Array(WAVE_POINTS).fill(0)); + const waveRef = useRef([...INIT_WAVE]); + // Double-buffer for spectrum and peaks: swap instead of .slice() every tick. + const specBufRef = useRef<[number[], number[]]>([[...INIT_SPEC], [...INIT_SPEC]]); + const peakBufRef = useRef<[number[], number[]]>([[...INIT_PEAKS], [...INIT_PEAKS]]); + const bufIdx = useRef(0); + // Cached player state fields to avoid unnecessary setState calls. + const prevStateRef = useRef({ position: 0, duration: 0, paused: false, volume: 0, url: "", title: "" }); const inflight = useRef(new Set()); // URLs whose title is resolving + const appleTempRef = useRef(null); // temp dir from gamdl to clean up + const searchProviderRef = useRef<"youtube" | "apple">("youtube"); // chosen provider for /search // Transient cat reaction (wink/scared); cleared once the deadline frame passes. const reactRef = useRef<{ type: "wink" | "scared" | "meow"; until: number }>({ type: "wink", @@ -766,25 +786,43 @@ function App({ const panelMax = Math.max(3, rows - 19); // Filtered view: indices into `tracks` that match the filter (all if none). const filt = filter.trim().toLowerCase(); - const viewIdx = filt - ? tracks.reduce((acc, t, i) => { - if (t.title.toLowerCase().includes(filt)) acc.push(i); - return acc; - }, []) - : tracks.map((_, i) => i); + const viewIdx = useMemo(() => { + if (!filt) + return tracks.map((_, i) => i); + return tracks.reduce((acc, t, i) => { + if (t.title.toLowerCase().includes(filt)) acc.push(i); + return acc; + }, []); + }, [tracks, filt]); // --- playback --- - const play = (i: number) => { + const play = async (i: number) => { const tr = tracks[i]; if (!tr) return; setCurrent(i); setListIdx(i); - react("meow"); // the cat greets a new track + react("meow"); + // Clean up any previous Apple Music temp directory. + if (appleTempRef.current) { + const { cleanupTempDir } = await import("../../gamdl.ts"); + cleanupTempDir(appleTempRef.current); + appleTempRef.current = null; + } try { - player.load(tr.url); - analyzer.start(tr.url, 0); - } catch { - // A bad URL must never crash the UI. + if (tr.url.includes("music.apple.com")) { + setStatusMsg("AP Preparing Apple Music track..."); + const { downloadAppleMusic } = await import("../../gamdl.ts"); + const dl = await downloadAppleMusic(tr.url); + appleTempRef.current = dl.tempDir; + player.load(dl.path); + analyzer.start(dl.path, 0); + setStatusMsg(`[AP] Playing: ${tr.title}`); + } else { + player.load(tr.url); + analyzer.start(tr.url, 0); + } + } catch (err) { + setStatusMsg(`ERR Playback failed: ${String(err)}`); } }; const pickNext = (auto: boolean): number | null => { @@ -836,9 +874,35 @@ function App({ } // A track that actually plays clears the unavailable-streak counter. if (player.state.position > 3) errRef.current = 0; - setState({ ...player.state }); + // Only update React state when relevant player fields actually changed + // (avoids creating a new state object on every mpv poll). + const ps = player.state; + const prev = prevStateRef.current; + if ( + ps.position !== prev.position || + ps.duration !== prev.duration || + ps.paused !== prev.paused || + ps.volume !== prev.volume || + ps.url !== prev.url || + ps.title !== prev.title + ) { + prev.position = ps.position; + prev.duration = ps.duration; + prev.paused = ps.paused; + prev.volume = ps.volume; + prev.url = ps.url ?? ""; + prev.title = ps.title ?? ""; + setState({ ...ps }); + } }; const onEnded = (r: string) => { + // Clean up Apple Music temp files on track end. + if (appleTempRef.current) { + import("../../gamdl.ts").then(({ cleanupTempDir }) => { + cleanupTempDir(appleTempRef.current!); + appleTempRef.current = null; + }); + } if (r === "eof") { errRef.current = 0; advance(true); @@ -898,7 +962,8 @@ function App({ const id = setInterval(() => { const playing = !!player.state.url && !player.state.paused; if (!playing) { - specRef.current = specRef.current.map((v) => Math.max(0, v - 0.6)); + const sr = specRef.current; + for (let i = 0; i < sr.length; i++) sr[i] = Math.max(0, sr[i]! - 0.6); } // Attack fast, release slow → smoother bars; peak caps fall gently. const sm = smoothRef.current; @@ -909,12 +974,38 @@ function App({ sm[i] = t > sm[i]! ? t : sm[i]! * 0.72 + t * 0.28; pk[i] = Math.max(sm[i]!, pk[i]! - 0.12); } - setSpec(sm.slice()); - setPeaks(pk.slice()); + // Double-buffer: copy into the back buffer, then swap — avoids .slice(). + const bi = bufIdx.current ^= 1; + const specBuf = specBufRef.current[bi]!; + const peakBuf = peakBufRef.current[bi]!; + for (let i = 0; i < sm.length; i++) { + specBuf[i] = sm[i]!; + peakBuf[i] = pk[i]!; + } + setSpec(specBuf); + setPeaks(peakBuf); setWave(waveRef.current); setFrame((f) => f + 1); - setState({ ...player.state }); - }, 90); + // Only update player state when values actually changed. + const ps = player.state; + const prev = prevStateRef.current; + if ( + ps.position !== prev.position || + ps.duration !== prev.duration || + ps.paused !== prev.paused || + ps.volume !== prev.volume || + ps.url !== prev.url || + ps.title !== prev.title + ) { + prev.position = ps.position; + prev.duration = ps.duration; + prev.paused = ps.paused; + prev.volume = ps.volume; + prev.url = ps.url ?? ""; + prev.title = ps.title ?? ""; + setState({ ...ps }); + } + }, 120); return () => clearInterval(id); }, [player]); @@ -988,19 +1079,88 @@ function App({ lastPos: Math.floor(player.state.position), }); } + // Clean up Apple Music temp files. + if (appleTempRef.current) { + import("../../gamdl.ts").then(({ cleanupTempDir }) => { + cleanupTempDir(appleTempRef.current!); + }); + } analyzer.stop(); player.quit(); exit(); }; // --- async actions --- - const doSearch = async (query: string) => { + const execSearch = async (query: string, provider: "youtube" | "apple") => { setOverlay({ kind: "loading", text: t("ui.searching") }); - await ensureYtDlp(() => {}); - const results = await searchYouTube(query, loadSettings().searchLimit ?? 20); - if (results.length === 0) return setOverlay({ kind: "none" }); - setSel(0); - setOverlay({ kind: "searchResults", results }); + if (provider === "youtube") { + await ensureYtDlp(() => {}); + const results = await searchYouTube(query, loadSettings().searchLimit ?? 20); + if (results.length === 0) return setOverlay({ kind: "none" }); + setSel(0); + setOverlay({ kind: "searchResults", results }); + } else { + const { searchApple } = await import("../../apple.ts"); + try { + const results = await searchApple(query); + if (results.length === 0) { + setStatusMsg("ERR No results found on Apple Music."); + return setOverlay({ kind: "none" }); + } + setSel(0); + setOverlay({ kind: "searchResults", results }); + setStatusMsg(`OK Found ${results.length} songs on Apple Music.`); + } catch (err) { + setStatusMsg(`ERR Apple Music search failed: ${String(err)}`); + setOverlay({ kind: "none" }); + } + } + }; + + // Command registry: built once, accessed via name lookup. + const commands = useMemo( + () => + createCommands({ + setStatusMsg, + setActiveProvider, + saveSettings: saveSettings as (s: Record) => void, + setOverlay, + setSel, + setTracks, + setListIdx, + activeProvider, + player, + viewIdx, + current, + tracks, + execSearch, + play, + advance, + quit, + openOverlay, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [activeProvider, viewIdx.length, current, tracks.length], + ); + + // Autocomplete: filter commands based on what the user typed. + const showAutocomplete = commandTrimmed.startsWith("/") && !commandTrimmed.includes(" "); + const filteredCmds = showAutocomplete + ? matchCommands(commandTrimmed, commands) + : []; + const acSel = Math.min(autocompleteIdx, Math.max(0, filteredCmds.length - 1)); + + const handleCommand = async (raw: string) => { + const cmd = raw.trim(); + if (!cmd) return; + const [action, ...argsArr] = cmd.split(/\s+/); + const args = argsArr.join(" "); + const cmdObj = commands[action?.toLowerCase() ?? ""]; + if (cmdObj) { + await cmdObj.handler(args); + } else { + setStatusMsg(`? Unknown command: ${action?.toLowerCase() ?? ""}`); + } }; const openList = (name: string) => { setPlaylists(listPlaylists()); @@ -1047,35 +1207,23 @@ function App({ // Overlays first. if (overlay.kind === "loading") return; - // Live filter editing for the current list. - if (filtering) { - if (key.escape) { - setFilter(""); - setFiltering(false); - setListIdx(0); - return; - } - if (key.return) { - setFiltering(false); - if (viewIdx[listIdx] != null) play(viewIdx[listIdx]!); - return; - } - if (key.upArrow) return setListIdx((i) => Math.max(0, i - 1)); - if (key.downArrow) - return setListIdx((i) => Math.min(viewIdx.length - 1, i + 1)); - if (key.backspace || key.delete) { - setFilter((s) => s.slice(0, -1)); - setListIdx(0); - return; - } - if (ch && !key.ctrl && !key.meta) { - setFilter((s) => s + ch); - setListIdx(0); - return; - } + if (overlay.kind === "searchResults") { + if (key.escape) return closeOverlay(); + if (key.upArrow) return setSel((i) => Math.max(0, i - 1)); + if (key.downArrow) return setSel((i) => Math.min(overlay.results.length - 1, i + 1)); + if (key.return) return chooseOverlay(); return; } + if (overlay.kind === "searchProvider") { + if (key.escape) return closeOverlay(); + if (key.upArrow) return setSel((i) => Math.max(0, i - 1)); + if (key.downArrow) return setSel((i) => Math.min(1, i + 1)); + if (key.return) return chooseOverlay(); + return; + } + + // --- text-input overlays (search, add) --- if (overlay.kind === "searchInput" || overlay.kind === "addInput") { if (key.escape) return closeOverlay(); if (key.return) { @@ -1083,9 +1231,12 @@ function App({ const ov = overlay; closeOverlay(); if (!value) return; - if (ov.kind === "searchInput") void doSearch(value); - else if (ov.target === "list") void importList(value); - else addTrack(value); + if (ov.kind === "searchInput") { + void execSearch(value, searchProviderRef.current); + } else if (ov.kind === "addInput") { + if (ov.target === "list") void importList(value); + else addTrack(value); + } return; } if (key.backspace || key.delete) return setInput((s) => s.slice(0, -1)); @@ -1093,127 +1244,92 @@ function App({ return; } - if (overlay.kind === "confirmTrack") { - if (key.return || ch === "y") { - removeUrl(tracks[overlay.index]!.url); - if (current === overlay.index) setCurrent(-1); - reload(); - react("scared"); // the cat is startled when you delete + // --- slash-command autocomplete --- + if (showAutocomplete && filteredCmds.length > 0) { + if (key.tab) { + const cmd = filteredCmds[acSel]; + if (cmd) { setCommand(cmd.name + " "); setAutocompleteIdx(0); } + return; } - return closeOverlay(); - } - if (overlay.kind === "confirmPlaylist") { - if (key.return || ch === "y") { - removePlaylist(overlay.name); - const remaining = listPlaylists(); - setPlaylists(remaining); - if (activePlaylist() === overlay.name) switchPlaylist(remaining[0] ?? "Default"); - react("scared"); + if (key.upArrow) { + setAutocompleteIdx((i) => Math.max(0, i - 1)); + return; + } + if (key.downArrow) { + setAutocompleteIdx((i) => Math.min(filteredCmds.length - 1, i + 1)); + return; + } + if (key.escape) { + setCommand(""); + setAutocompleteIdx(0); + return; + } + if (key.return && commandTrimmed === filteredCmds[acSel]?.name) { + void handleCommand(command); + setCommand(""); + setAutocompleteIdx(0); + return; } - return closeOverlay(); } - const listOverlays: Record = { - settings: 4, - theme: listThemes().length, - lang: SUPPORTED_LOCALES.length, - playlists: playlists.length, - searchLimit: SEARCH_PRESETS.length, - searchResults: - overlay.kind === "searchResults" ? overlay.results.length : 0, - }; - if (overlay.kind in listOverlays) { - const count = listOverlays[overlay.kind]!; - if (key.escape) return closeOverlay(); - if (key.upArrow) return setSel((i) => Math.max(0, i - 1)); - if (key.downArrow) return setSel((i) => Math.min(count - 1, i + 1)); - if (key.return) return chooseOverlay(); + // --- Tab: cycle focus between command / playlists / tracks --- + if (key.tab && !showAutocomplete) { + setFocus((f) => + f === "command" ? "sidebar" : f === "sidebar" ? "tracks" : "command", + ); return; } - if (overlay.kind === "help") return closeOverlay(); - - if (overlay.kind === "eq") { - if (key.escape || ch === "e") return closeOverlay(); - if (key.leftArrow) return setEqBand((b) => Math.max(0, b - 1)); - if (key.rightArrow) - return setEqBand((b) => Math.min(EQ_BANDS.length - 1, b + 1)); - if (key.upArrow || key.downArrow) { - const next = [...eq]; - const d = key.upArrow ? 1 : -1; - next[eqBand] = Math.max(-12, Math.min(12, (next[eqBand] ?? 0) + d)); - return applyEq(next); - } - if (ch === "0") return applyEq(new Array(EQ_BANDS.length).fill(0)); - if (ch === "p") { - // Cycle to the next preset by matching the current gains. - const i = EQ_PRESETS.findIndex( - (pr) => JSON.stringify(pr.gains) === JSON.stringify(eq), - ); - return applyEq(EQ_PRESETS[(i + 1) % EQ_PRESETS.length]!.gains); + // --- Enter on empty command: play track or switch playlist --- + if (key.return && command.trim() === "") { + if (focus === "tracks" && viewIdx[listIdx] != null) { + play(viewIdx[listIdx]!); + } else if (focus === "sidebar" && playlists[sideIdx]) { + switchPlaylist(playlists[sideIdx]!); + } else { + setFocus("command"); } return; } - // Main view. - if (ch === "q") return quit(); - if (ch === " ") return player.togglePause(); - if (key.tab) return setFocus((f) => (f === "tracks" ? "sidebar" : "tracks")); - if (key.leftArrow) return player.seek(-5); - if (key.rightArrow) return player.seek(5); - if (ch === "n") return advance(false); - if (ch === "p") return play((current - 1 + tracks.length) % (tracks.length || 1)); - if (ch === "s") return setShuffle((v) => !v); - if (ch === "r") - return setRepeat((v) => (v === "off" ? "all" : v === "all" ? "one" : "off")); - if (ch === "v") { - const idx = (VIZ_MODES as readonly string[]).indexOf(mode); - const m = VIZ_MODES[(idx + 1) % VIZ_MODES.length]!; - setMode(m); - saveSettings({ vizMode: m }); + // Default: capture into command buffer + if (key.return) { + setFocus("command"); + void handleCommand(command); + setCommand(""); + setAutocompleteIdx(0); return; } - if (ch === "m") return toggleMute(); - if (ch === "+" || ch === "=") return setVol(player.state.volume + 5); - if (ch === "-") return setVol(player.state.volume - 5); - if (ch === "/") return openOverlay({ kind: "searchInput" }); - if (ch === "a") - return openOverlay({ - kind: "addInput", - target: focus === "sidebar" ? "list" : "track", - }); - if (ch === "o") return openOverlay({ kind: "settings" }); - if (ch === "e") return openOverlay({ kind: "eq" }); - if (ch === "f") { - setFocus("tracks"); - setFiltering(true); + if (key.backspace || key.delete) { + setFocus("command"); + setCommand((s) => s.slice(0, -1)); + setAutocompleteIdx(0); return; } - if (ch === "?") return openOverlay({ kind: "help" }); - if (ch === "d") { - if (focus === "sidebar" && playlists[sideIdx]) { - return openOverlay({ kind: "confirmPlaylist", name: playlists[sideIdx]! }); - } - if (viewIdx[listIdx] != null) - return openOverlay({ kind: "confirmTrack", index: viewIdx[listIdx]! }); + if (key.escape) { + setCommand(""); + setAutocompleteIdx(0); return; } - - if (focus === "tracks") { - if (key.upArrow) return setListIdx((i) => Math.max(0, i - 1)); - if (key.downArrow) - return setListIdx((i) => Math.min(viewIdx.length - 1, i + 1)); - if (key.return && viewIdx[listIdx] != null) return play(viewIdx[listIdx]!); - return; + if (ch && !key.ctrl && !key.meta) { + setFocus("command"); + setCommand((s) => s + ch); + setAutocompleteIdx(0); } - if (key.upArrow) return setSideIdx((i) => Math.max(0, i - 1)); - if (key.downArrow) return setSideIdx((i) => Math.min(playlists.length - 1, i + 1)); - if (key.return) { - const name = playlists[sideIdx]; - if (name) switchPlaylist(name); + + // Arrow-key navigation based on focus (when command is empty). + if (command === "") { + if (focus === "sidebar") { + if (key.upArrow) return setSideIdx((i) => Math.max(0, i - 1)); + if (key.downArrow) return setSideIdx((i) => Math.min(playlists.length - 1, i + 1)); + } else if (focus === "tracks") { + if (key.upArrow) return setListIdx((i) => Math.max(0, i - 1)); + if (key.downArrow) return setListIdx((i) => Math.min(viewIdx.length - 1, i + 1)); + } } }); + function openOverlay(o: Overlay) { setInput(""); setSel(initialSel(o)); @@ -1269,6 +1385,15 @@ function App({ if (n) saveSettings({ searchLimit: n }); return closeOverlay(); } + if (overlay.kind === "searchProvider") { + const provider = sel === 0 ? "youtube" : "apple"; + searchProviderRef.current = provider; + setActiveProvider(provider); + saveSettings({ activeProvider: provider }); + closeOverlay(); + openOverlay({ kind: "searchInput" }); + return; + } if (overlay.kind === "searchResults") { const r = overlay.results[sel]; if (r) { @@ -1433,8 +1558,8 @@ function App({ /> - - - ↑↓ · ↵ play · space · n/p · s/r · v viz ({mode}) · e eq · / search · - f filter · a add · d del · o settings · ? help · q quit - + + {statusMsg && ( + + {statusMsg} + + )} + {showAutocomplete && filteredCmds.length > 0 && ( + + {filteredCmds.slice(0, 8).map((cmd, i) => ( + + + {i === acSel ? "› " : " "} + {cmd.syntax} + + {cmd.desc} + + ))} + + ↹ complete · ↑↓ choose · ↵ execute · esc dismiss + + + )} + + + {activeProvider === "apple" ? "AP" : "YT"} {"› "} + + + {command} + + + {frame % 10 < 5 ? "█" : " "} + + {focus === "command" && ( + (Tab to cycle) + )} + ); @@ -1516,6 +1678,17 @@ function renderOverlay( ); } + if (overlay.kind === "searchProvider") { + return ( + + + + ); + } if (overlay.kind === "searchResults") { return ( @@ -1580,8 +1753,9 @@ function renderOverlay( ↑↓ navigate · ↵ play · space pause · ←→ seek{"\n"} n/p next/prev · s shuffle · r repeat · v visualizer{"\n"} - e equalizer · f filter · / search · a add · d delete{"\n"} - o settings · +/- volume · m mute · Tab panel · ? help · q quit + e equalizer · f filter · a add · d delete{"\n"} + o settings · +/- volume · m mute · ? help · q quit{"\n"} + / slash commands · ↹ autocomplete esc to close diff --git a/packages/client/src/ui/ink/commands.ts b/packages/client/src/ui/ink/commands.ts new file mode 100644 index 0000000..0017453 --- /dev/null +++ b/packages/client/src/ui/ink/commands.ts @@ -0,0 +1,157 @@ +// Command registry for catunes slash commands. +// Add new commands here: define the name, syntax, description and handler. +// They automatically appear in autocomplete and /help. + +import type { Track } from "../../playlist.ts"; +import type { Overlay } from "./overlay.ts"; + +export interface CommandCtx { + setStatusMsg: (msg: string) => void; + setActiveProvider: (p: "youtube" | "apple") => void; + saveSettings: (s: Record) => void; + setOverlay: (o: Overlay) => void; + setSel: (i: number) => void; + setTracks: (t: Track[]) => void; + setListIdx: (i: number) => void; + activeProvider: "youtube" | "apple"; + player: { togglePause(): void }; + viewIdx: number[]; + current: number; + tracks: Track[]; + execSearch: (q: string, p: "youtube" | "apple") => void; + play: (i: number) => void; + advance: (auto: boolean) => void; + quit: () => void; + openOverlay: (o: Overlay) => void; +} + +export interface Command { + name: string; + syntax: string; + desc: string; + handler: (args: string) => Promise; +} + +export function createCommands(ctx: CommandCtx): Record { + const ok = (msg: string) => ctx.setStatusMsg(`OK ${msg}`); + const err = (msg: string) => ctx.setStatusMsg(`ERR ${msg}`); + + return { + "/provider": { + name: "/provider", + syntax: "/provider [youtube|apple]", + desc: "Switch music provider", + handler: async (args) => { + if (args === "apple" || args === "youtube") { + ctx.setActiveProvider(args); + ctx.saveSettings({ activeProvider: args }); + ok(`Provider: ${args}`); + } else { + err("Usage: /provider [apple|youtube]"); + } + }, + }, + "/search": { + name: "/search", + syntax: "/search ", + desc: "Search songs", + handler: async (args) => { + if (args) { + await ctx.execSearch(args, ctx.activeProvider); + } else { + ctx.setSel(0); + ctx.setOverlay({ kind: "searchProvider" }); + } + }, + }, + "/auth": { + name: "/auth", + syntax: "/auth ", + desc: "Set Apple Music session cookies", + handler: async (args) => { + if (!args) return err("Usage: /auth "); + ctx.saveSettings({ appleCookies: args }); + const { resetAppleClient } = await import("../../apple.ts"); + resetAppleClient(); + ok("Apple Music cookies updated."); + }, + }, + "/library": { + name: "/library", + syntax: "/library", + desc: "Load Apple Music library", + handler: async () => { + try { + ctx.setStatusMsg("AP Fetching Apple Music library..."); + const { getLibrarySongs } = await import("../../apple.ts"); + const libTracks = await getLibrarySongs(); + if (libTracks.length === 0) { + err("Library is empty or cookies are invalid."); + } else { + ctx.setTracks(libTracks); + ctx.setListIdx(0); + ok(`Loaded ${libTracks.length} songs.`); + } + } catch (e) { + err(`Failed to fetch library: ${String(e)}`); + } + }, + }, + "/play": { + name: "/play", + syntax: "/play ", + desc: "Play track by index", + handler: async (args) => { + const idx = parseInt(args, 10); + if (!isNaN(idx) && ctx.viewIdx[idx] != null) { + ctx.play(ctx.viewIdx[idx]!); + } else { + err("Usage: /play "); + } + }, + }, + "/pause": { + name: "/pause", + syntax: "/pause", + desc: "Pause / resume", + handler: async () => { ctx.player.togglePause(); }, + }, + "/next": { + name: "/next", + syntax: "/next", + desc: "Skip to next track", + handler: async () => { ctx.advance(false); }, + }, + "/prev": { + name: "/prev", + syntax: "/prev", + desc: "Go to previous track", + handler: async () => { + ctx.play((ctx.current - 1 + ctx.tracks.length) % (ctx.tracks.length || 1)); + }, + }, + "/quit": { + name: "/quit", + syntax: "/quit", + desc: "Exit catunes", + handler: async () => { ctx.quit(); }, + }, + "/exit": { + name: "/exit", + syntax: "/exit", + desc: "Exit catunes", + handler: async () => { ctx.quit(); }, + }, + "/help": { + name: "/help", + syntax: "/help", + desc: "Show commands", + handler: async () => { ctx.openOverlay({ kind: "help" }); }, + }, + }; +} + +/** Returns commands whose name starts with the given prefix (for autocomplete). */ +export function matchCommands(prefix: string, cmds: Record): Command[] { + return Object.values(cmds).filter((c) => c.name.startsWith(prefix)); +} diff --git a/packages/client/src/ui/ink/overlay.ts b/packages/client/src/ui/ink/overlay.ts new file mode 100644 index 0000000..cd8ceb9 --- /dev/null +++ b/packages/client/src/ui/ink/overlay.ts @@ -0,0 +1,18 @@ +import type { SearchResult } from "../../playlist.ts"; + +export type Overlay = + | { kind: "none" } + | { kind: "settings" } + | { kind: "theme" } + | { kind: "lang" } + | { kind: "playlists" } + | { kind: "searchLimit" } + | { kind: "help" } + | { kind: "eq" } + | { kind: "searchInput" } + | { kind: "addInput"; target: "track" | "list" } + | { kind: "searchResults"; results: SearchResult[] } + | { kind: "confirmTrack"; index: number } + | { kind: "confirmPlaylist"; name: string } + | { kind: "loading"; text: string } + | { kind: "searchProvider" }; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 645df6e..bd0ad4d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -18,6 +18,9 @@ interface Member { interface Room { code: string; members: Map; + /** Cached roster array — invalidated on join/leave via _rosterDirty flag. */ + _rosterCache: { id: string; name: string }[]; + _rosterDirty: boolean; } const rooms = new Map(); @@ -28,6 +31,12 @@ type Incoming = | { type: "pause"; paused: boolean } | { type: "chat"; text: string }; +// Reusable roster payload object — avoids a new object literal per broadcast. +const _rosterPayload: { type: "roster"; members: { id: string; name: string }[] } = { + type: "roster", + members: [], +}; + function broadcast(room: Room, payload: unknown, exceptId?: string) { const data = JSON.stringify(payload); for (const m of room.members.values()) { @@ -35,8 +44,27 @@ function broadcast(room: Room, payload: unknown, exceptId?: string) { } } -function roster(room: Room) { - return [...room.members.values()].map((m) => ({ id: m.id, name: m.name })); +/** Returns the cached roster array, rebuilding only when members changed. */ +function roster(room: Room): { id: string; name: string }[] { + if (room._rosterDirty) { + const arr: { id: string; name: string }[] = []; + for (const m of room.members.values()) { + arr.push({ id: m.id, name: m.name }); + } + room._rosterCache = arr; + room._rosterDirty = false; + } + return room._rosterCache; +} + +/** Broadcasts the roster using the shared payload object. */ +function broadcastRoster(room: Room) { + _rosterPayload.members = roster(room); + broadcast(room, _rosterPayload); +} + +function createRoom(code: string): Room { + return { code, members: new Map(), _rosterCache: [], _rosterDirty: true }; } const server = Bun.serve<{ id: string; room?: string }, {}>({ @@ -51,7 +79,9 @@ const server = Bun.serve<{ id: string; room?: string }, {}>({ }, websocket: { open(ws) { - ws.send(JSON.stringify({ type: "hello", id: ws.data.id })); + // Pre-build hello JSON via string concat — avoids allocating an object + // just to stringify it immediately. + ws.send('{"type":"hello","id":"' + ws.data.id + '"}'); }, message(ws, raw) { let msg: Incoming; @@ -64,12 +94,13 @@ const server = Bun.serve<{ id: string; room?: string }, {}>({ if (msg.type === "join") { let room = rooms.get(msg.room); if (!room) { - room = { code: msg.room, members: new Map() }; + room = createRoom(msg.room); rooms.set(msg.room, room); } room.members.set(ws.data.id, { id: ws.data.id, name: msg.name, ws }); ws.data.room = msg.room; - broadcast(room, { type: "roster", members: roster(room) }); + room._rosterDirty = true; + broadcastRoster(room); return; } @@ -94,7 +125,10 @@ const server = Bun.serve<{ id: string; room?: string }, {}>({ if (!room) return; room.members.delete(ws.data.id); if (room.members.size === 0) rooms.delete(room.code); - else broadcast(room, { type: "roster", members: roster(room) }); + else { + room._rosterDirty = true; + broadcastRoster(room); + } }, }, }); diff --git a/scripts/patch-applemusic-exports.sh b/scripts/patch-applemusic-exports.sh new file mode 100755 index 0000000..905000c --- /dev/null +++ b/scripts/patch-applemusic-exports.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Patches the applemusic-api package.json to export the AxiosManager module +# so catunes can access the internal cookiejar for cookie injection. +set -euo pipefail + +find_pkg() { + # Locate the real (non-symlink) directory of the installed package. + local link + link="$(readlink -f packages/client/node_modules/applemusic-api 2>/dev/null)" || return 1 + [ -d "$link" ] || return 1 + echo "$link" +} + +PKG_DIR="$(find_pkg)" +if [ -z "$PKG_DIR" ]; then + echo "applemusic-api not installed, skipping patch" >&2 + exit 0 +fi + +PKG_JSON="$PKG_DIR/package.json" +if [ ! -f "$PKG_JSON" ]; then + echo "package.json not found at $PKG_JSON" >&2 + exit 1 +fi + +# Check if the export already exists. +if grep -q './dist/utils/AxiosManager.js' "$PKG_JSON" 2>/dev/null; then + echo "applemusic-api exports already patched." + exit 0 +fi + +# Apply the patch using python3 (more reliable than sed for JSON). +python3 -c " +import json, sys +with open('$PKG_JSON') as f: + pkg = json.load(f) +exp = pkg.setdefault('exports', {}) +exp['./dist/utils/AxiosManager.js'] = { + 'types': './dist/utils/AxiosManager.d.ts', + 'import': './dist/utils/AxiosManager.js' +} +with open('$PKG_JSON', 'w') as f: + json.dump(pkg, f, indent=2) + f.write('\n') +" && echo "Patched applemusic-api exports successfully."