From 86ab23a81429b9bbca75385c9600c55d2ec4bb51 Mon Sep 17 00:00:00 2001 From: minster586 <43217359+minster586@users.noreply.github.com> Date: Mon, 2 Nov 2026 05:46:56 -0500 Subject: [PATCH] small tweeks --- Album-Art.png | Bin 0 -> 22143 bytes Dockerfile | 1 + README.md | 13 +- docker-compose.yml | 1 + package-lock.json | 613 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- public/index.html | 237 +++++++++++++++--- server.js | 261 +++++++++++++++++-- 8 files changed, 1076 insertions(+), 54 deletions(-) create mode 100644 Album-Art.png diff --git a/Album-Art.png b/Album-Art.png new file mode 100644 index 0000000000000000000000000000000000000000..c2f8ae43f11ce9fe056e3759b94175c5345b47e7 GIT binary patch literal 22143 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
gdrh4vrsuVCp0#x(BU6 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

vq}orbIP%PzT}N($di;O$_lEPMjB( zxS0O;HBE}Du+`2KMc@TnR3B_Qc;v*vpy>YmnC1mrOwGILQIhzCaN>G4f?0hXIdj;- z(ZS(#&k*}CBg!>ehWW1NM*AY-pe-4H8ecbMK?#+xD^35BTFmcHOg|eTcRrD7^&zW_<^cL$gvJR!eZdNKqG-RdH@LpT_B# z@?W{uA9c931_v*1Y$nDv$^BbAmrM|v)Bo$vWJjqc=)|f)Dx$SU^CCM&e^&MOsmNM) zDQc^7COk1`q~PlvNM`S8RO$eWpI-|LBoMncN#5`HC?Kx+k+vUYn6AzHS8$P=p2sVg zPI+g_#)==}u6t4QjC-r;s)4N^h=(JZoY+h>=g*JmJY?gE=iBC2Ag=>?qGnzz??12h z=iyNxTiU(EEsiSN0=-~^gZT;ze}*u~QXvo}%%$%gX&E~hER>IQ@-3fb2EKLM*XX#Q z2Spg|$nsRTaxf;{;(0#D7wI7(+a4`2{b$}I=q#7`DF%{Q&=A#~-}vQKiwlTm2RiXg z@qkO_pIp@P085j4T5+(bz~aQX0mA-%sh!`orRFLc#vFr;|n^;#v)>S{6!B0`|9u^i?h1LI0WkH5SeVE9Gt;)8S7a_IwW^bp zGw0S2Ms>;xf(#?MEf-#qCQ3Cs*vmv@EpbCe*Tj zeVSf#j}Z^kI@$j2!372XhP&X<3=$`B|vpPd8 z&f?5X4ik#HR?)S`Dw-=PsTX^npQ$}l2c=gnh9Q5PuPq-^{5LSb+%cN^+B7oak0zjM zatn8`mVO`fkV4`po^xm&#VDE9yD~T2y6bQu9E}WpnWF1A_4QQ*!OT!>JIBFdK?yr>E#<_* z7)b3g!^D~OZbLo|i=IfV;{1H>GDJ&b>}

^Naa_rP6C2O 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 for every exported item. + - When checked, the app runs `UPDATE songs SET image = '' WHERE id = ` for every successfully exported item. Exported images are always RadioDJ-compatible `.jpg` or `.png`; WebP sources (including URLs and uploads) are converted to JPEG. 6. Copy the images from the ZIP into the folder configured in RadioDJ under *Options → Options → Album Art*, so RadioDJ picks them up. > **Note:** RadioDJ's `songs.image` column stores just the artwork file name (relative to its album-art folder). That is exactly what this app writes. +> Artwork logs do not include image URLs or file contents. Unsupported formats are reported as export errors rather than added to the ZIP. + +Tracks without selected artwork display the bundled `Album-Art.png` in the web interface. It is only a visual fallback and is not selected, exported, or written to RadioDJ automatically. ### Admin @@ -70,7 +74,7 @@ The admin header button opens the administration panel: | POST | `/api/profiles/:id/test` | Test MySQL connectivity + count missing artwork. | | GET | `/api/tracks?profileId=&page=&limit=` | Tracks missing artwork (`song_type=0` and `image` NULL/empty or a `no_cover`/`no_image`/`no-cover` placeholder); `limit=all` disables paging. Returns `{tracks,totalTracks,page,totalPages}`. | | POST | `/api/artwork/search` | `{artist,title,album,source}` — source `auto` chains iTunes → Deezer → MusicBrainz/CAA; or `itunes`/`deezer`/`musicbrainz` only. | -| POST | `/api/export` | `{profileId,updateDb,items[]}` → ZIP download + optional DB update. | +| POST | `/api/export` | JSON for URL artwork, or multipart form data with a JSON `payload` and uploaded artwork files → ZIP download + optional DB update. | | GET | `/api/admin/users` | (admin) List users. | | PUT | `/api/admin/users/:id/role` | (admin) Change role. | | POST | `/api/admin/clone-profiles` | (admin) Copy all profiles between users. | @@ -81,6 +85,7 @@ The admin header button opens the administration panel: ``` ├── server.js # Express backend: auth, profiles, MySQL pools, artwork search, ZIP export ├── public/index.html # Single-page Tailwind dashboard (vanilla JS) +├── Album-Art.png # Web-interface fallback for tracks without selected artwork ├── data/ # SQLite storage (git-ignored, volume-mapped in Docker) ├── Dockerfile ├── docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml index 8413995..0f2788e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: # stored passwords unreadable. APP_SECRET: ${APP_SECRET:-change-me-to-a-long-random-string} PORT: 3000 + LOG_LEVEL: ${LOG_LEVEL:-info} volumes: - ./data:/app/data restart: unless-stopped diff --git a/package-lock.json b/package-lock.json index de4dee1..b7bdd1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,524 @@ "archiver": "^7.0.1", "better-sqlite3": "^11.10.0", "express": "^4.21.2", - "mysql2": "^3.14.0" + "multer": "^2.5.0", + "mysql2": "^3.14.0", + "sharp": "^0.35.5" }, "engines": { "node": ">=20" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -104,6 +616,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/archiver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", @@ -416,6 +934,17 @@ "node": ">=8.0.0" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1335,6 +1864,24 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.5.0.tgz", + "integrity": "sha512-BsqOzYGVq49JcW2IzoSAxeKYRevb+dGGPPWleOR8Lrlk4igwxu1/9pwSXlByhCb7MVZDrNJFC0IBP5/O5nLYRw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mysql2": { "version": "3.24.4", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.4.tgz", @@ -1745,6 +2292,55 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1919,6 +2515,14 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/streamx": { "version": "2.28.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", @@ -2125,6 +2729,13 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", + "license": "0BSD", + "optional": true + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index d5f49e2..06c4294 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "archiver": "^7.0.1", "better-sqlite3": "^11.10.0", "express": "^4.21.2", - "mysql2": "^3.14.0" + "multer": "^2.5.0", + "mysql2": "^3.14.0", + "sharp": "^0.35.5" }, "overrides": { "qs": "^6.16.0" diff --git a/public/index.html b/public/index.html index 46a2f49..8696221 100644 --- a/public/index.html +++ b/public/index.html @@ -93,6 +93,7 @@ Artwork Title Artist + Album Audio Filename Search & Results @@ -107,7 +108,8 @@

Artwork sources: iTunes Search API, MusicBrainz + Cover Art Archive — 100% free, no API keys. - Hover any thumbnail to see its source; click it to inspect the full-size image. Exported images are + Edit title, artist, or album in the grid to adjust artwork searches. These edits are temporary and do not update RadioDJ metadata. + The last option in each row lets you upload custom artwork or use a direct image URL. Hover any thumbnail to see its source; click it to inspect the full-size image. Exported images are renamed to the audio file’s base name (e.g. 01 Track.mp3 → 01 Track.jpg). Nothing is exported until you tick the track’s checkbox to confirm its artwork.

@@ -132,6 +134,30 @@ + + +