# CLAUDE.md Guidance for Claude Code when working in this repository. ## What this is NovaSonic — a self-hosted music streaming **PWA** (Vite + vanilla ES modules) and **Android app** (Jetpack Compose + Media3/ExoPlayer) for Navidrome / Subsonic servers, plus a **Node.js/Express backend** that downloads tracks via yt-dlp and proxies APIs. ## Critical rules - **Never hardcode or mock frontend data.** All library data comes from the Navidrome (Subsonic) API. No placeholder songs/albums/artists, ever. - **Don't commit or push unless asked.** When asked, push to the Gitea remote. - **Two-part versioning** `MAJOR.MINOR` (e.g. `0.8`) — no patch component. Each release bumps the minor; features and fixes go in the same release. See `CHANGELOG.md`. - Keep the `CHANGELOG.md` Platform Parity Matrix accurate when adding cross-platform features. ## Layout - `src/` — web PWA (vanilla ES modules). State in `src/state.js` (the `S` object); Subsonic/iTunes API in `src/api.js`; views in `src/views/`, UI in `src/ui/`. - `backend/novasonic-server.js` — Express API (downloads, metadata edit/delete, events/ABS proxy). - `backend/lib/` — `ytdlp.js` (download + cascade + ffmpeg tagging), `downloader.js`, `metadata.js` (iTunes lookup, file resolution helpers), `config.js`, `jobs.js`, `romaji.js`. - `android/` — native Kotlin app (separate; backend-dependent web features like the metadata editor are **not** in the Android app). ## Commands ```bash npm run dev # Vite dev server (HMR) npm run build # production build → dist/ npm run preview # preview the production build ``` Backend runs in Docker (see `docker-compose.yml`); on the deploy host it lives at `/opt/navidrome/music`. Restart after backend changes: `docker compose restart novasonic-backend` (this also re-pulls the latest yt-dlp). ## Download pipeline (hard-won knowledge) - Downloads are **yt-dlp only** (the old squid.wtf/Qobuz backend was removed). - **File naming:** `{NN - }Artist - Title.ext` with the release suffix stripped from the title (no `- Single`/`- EP`). The **album folder keeps the full album name**. Original names are used for folder, filename, and all main tags (artist/title/album/album\_artist) — even for Japanese tracks. Romaji goes **only** into sort tags (TITLESORT / ARTISTSORT / etc.). - **Search cascade** (`ytdlp.js`): YTM search first (`web_music` player client, Songs filter param), then regular YouTube; each candidate filtered by duration (±10%, min 10s), title keyword, artist channel, and variant exclusions (remix/cover/sped/…). - **"Mittelweg" rule** (with AND without duration): when nothing matches in the filtered steps, relax only the duration constraint while **still requiring the artist channel**. If nothing matches, download **nothing** and fail cleanly with "Nicht eindeutig gefunden — bitte Direktlink im Importer nutzen". Never fall back to a blind title-only/no-filter grab — it pulled same-titled covers by other uploaders, and wrong Piano Echoes for Japanese tracks. - **Size cap:** `--max-filesize 30M` on every yt-dlp call; Mittelweg steps also cap at 2× expected duration. Prevents saving a 28-min video (52 MB) when the wrong result slips through. - **Duplicate detection** (`metadata.js findExistingTrack`): searches case-insensitively across all album folders under the matching artist folder; compares normalized artist+title ignoring track prefix and release/variant suffix. Handles DJ OKAWARI vs DJ Okawari and "- Single" drift. - **Known limitation:** yt-dlp's YTM/YouTube *search* can't surface very obscure Art Tracks by ambiguously-named artists (e.g. "MINA") — the browser finds them, yt-dlp's API extraction doesn't (confirmed across params/clients/deno). For those, the **direct link in the importer** (`sourceUrl` path) is the reliable route. - **File resolution for edit/delete:** Navidrome's stored path may use a different mount root or a stale stem. `metadata.js` rebases the path and falls back to recursive/stem matching (tolerant of release-suffix mismatch). A ghost Navidrome entry needs a full library rescan. - **Album splitting:** prevented by consistent `album_artist` tag. All tracks in a download share the same `mainArtist` value (= iTunes `collectionArtistName` when available, else `albumArtist` param, else track artist). Never use `rawArtist` for `album_artist`. ## UI / references When referencing files in chat, use clickable markdown links, not backticks: `[ytdlp.js](backend/lib/ytdlp.js)`.