# RDJ Art Manager 🎡 A production-ready, open-source web app to find, review, and apply **missing album artwork** for a [RadioDJ](https://www.radiodj.ro/) MySQL database. - πŸ” **Free artwork lookup** β€” iTunes Search API first, MusicBrainz + Cover Art Archive fallback. No API keys, no credit cards. - πŸ—‚ **Multi-user with roles** β€” embedded SQLite (better-sqlite3) stores accounts and per-user RadioDJ connection profiles. First registered user becomes the **admin**. - πŸ” **Secure by default** β€” scrypt password hashing, AES-256-GCM encrypted DB passwords, HttpOnly session cookies, rate-limited auth endpoints. - πŸŽ› **Spreadsheet-style review grid** β€” preview artwork candidates per track, pick the right cover, filter, auto-search. - πŸ“¦ **ZIP export** β€” images renamed to the base audio filename (`01 Track.mp3` β†’ `01 Track.jpg`), plus an `export-report.json` manifest. - πŸ—„ **Optional DB update** β€” sets `songs.image` in the RadioDJ database to the new file name. - 🐳 **Docker** β€” one `docker compose up` with `./data` persisted. ## Quick start ### Local (Node.js β‰₯ 20) ```bash npm install npm start # open http://localhost:3000 ``` ### Docker ```bash docker compose up -d --build # open http://localhost:3000 ``` SQLite data is persisted in `./data` (mapped to `/app/data` in the container). ## Configuration | Env var | Default | Description | | --------------- | ------------ | --------------------------------------------------------------------------- | | `PORT` | `3000` | HTTP port. | | `DATA_DIR` | `./data` | Where the SQLite database lives. | | `APP_SECRET` | *(auto)* | **Set this in production** (min 16 chars). Encrypts stored RadioDJ DB passwords. If unset, a random secret is generated at `data/.app-secret`. Changing it makes stored DB passwords unreadable. | | `COOKIE_SECURE` | `0` | Set to `1` when serving over HTTPS so session cookies get the `Secure` flag. | ## Usage 1. **Register** β€” the first account becomes the `admin`. 2. Open **Profiles** and add a RadioDJ MySQL connection (host, port, database, user, password). Use **Test** to verify connectivity β€” it also reports how many tracks are missing artwork. 3. Pick the station in the header dropdown and click **Load tracks missing artwork**. The query matches tracks where `song_type = 0` and `image` is `NULL`, empty, **or a RadioDJ default placeholder** (`%no_cover%`, `%no_image%`, `%no-cover%` β€” case-insensitive). Placeholder rows are flagged in the grid with an amber β€œplaceholder art” badge. 4. Pick a **primary art source** in the header (Auto chains iTunes β†’ Deezer β†’ MusicBrainz, or pin a single API), use the grid **pagination bar** (page size 10/25/30/50/all, prev/next, numbered pages), then click **Search artwork** per row (or **Fetch Art for Current Page** for the whole page) to get artwork candidates. Hover a thumbnail to see its source (native tooltip), click it to open the **full-size preview overlay** (title, artist, source, β€œView Original Image”, and **βœ“ Confirm & Select This Art** β€” close with backdrop click or `Esc`). A row only becomes exportable once you tick its checkbox; fetching art never checks the box for you. 5. Click **Download ZIP & Update Database**: - Uncheck **Update RadioDJ DB** to only download the ZIP. - When checked, the app runs `UPDATE songs SET image = '' WHERE id = ` for every exported item. 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. ### Admin The admin header button opens the administration panel: - List all users and change roles (`user` ⇄ `admin`). You can't change your own role or demote the last admin. - Copy (clone) all station profiles from one user to another β€” encrypted passwords carry over. ## API overview | Method | Endpoint | Description | | ------ | --------------------------------- | ------------------------------------------------------- | | POST | `/api/auth/register` | Register (first user becomes admin). | | POST | `/api/auth/login` / `logout` | Session auth (HttpOnly cookie). | | GET | `/api/auth/me` | Current session user. | | GET/POST | `/api/profiles` | List / create RadioDJ connection profiles. | | PUT/DELETE | `/api/profiles/:id` | Update (blank password keeps stored one) / delete. | | 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. | | 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. | | GET | `/api/health` | Health check (used by Docker HEALTHCHECK). | ## Project layout ``` β”œβ”€β”€ server.js # Express backend: auth, profiles, MySQL pools, artwork search, ZIP export β”œβ”€β”€ public/index.html # Single-page Tailwind dashboard (vanilla JS) β”œβ”€β”€ data/ # SQLite storage (git-ignored, volume-mapped in Docker) β”œβ”€β”€ Dockerfile β”œβ”€β”€ docker-compose.yml β”œβ”€β”€ smoke-test.ps1 # Optional API smoke test (run while the server is up) └── package.json ``` ## Security notes - Passwords: scrypt (N=16384) + constant-time comparison. - RadioDJ DB passwords: AES-256-GCM encrypted at rest; never returned by the API. - Sessions: random 256-bit tokens in SQLite, HttpOnly + SameSite=Lax cookies, 7-day expiry. - Auth endpoints are rate-limited; all API routes require authentication; admin routes require the `admin` role. - SQL access is parameterized; the RadioDJ connection user only needs `SELECT`/`UPDATE` on the `songs` table. ## License MIT β€” see [LICENSE](LICENSE).