Download albums/playlists to private device storage (Room + WorkManager, HttpURLConnection streaming). New Offline tab lists groups with progress; tapping a group opens a detail screen (cover-art rows, play/shuffle, per-song delete from device). Downloaded tracks play from disk on every screen. Album/playlist cards show a downloaded badge; download button resumes missing songs. Song storage is dedup'd by ID and reference-counted on delete. Fixes: Android 15 download crash (dropped banned DATA_SYNC foreground service); offline detail flash-back (initial empty state no longer mistaken for deleted). Docs: CHANGELOG 0.10 + parity matrix; condensed README Features. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.2 KiB
6.2 KiB
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. SeeCHANGELOG.md. - Keep the
CHANGELOG.mdPlatform Parity Matrix accurate when adding cross-platform features.
Layout
src/— web PWA (vanilla ES modules). State insrc/state.js(theSobject); Subsonic/iTunes API insrc/api.js; views insrc/views/, UI insrc/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
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.extwith 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_musicplayer 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 30Mon 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
(
sourceUrlpath) is the reliable route. - File resolution for edit/delete: Navidrome's stored path may use a different mount root
or a stale stem.
metadata.jsrebases 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_artisttag. All tracks in a download share the samemainArtistvalue (= iTunescollectionArtistNamewhen available, elsealbumArtistparam, else track artist). Never userawArtistforalbum_artist.
Android offline system
- Storage:
context.filesDir/offline/audio/{songId}.{ext}and.../covers/{coverArtId}.jpg— private internal storage, not accessible via file manager. - Database: Room DB
novasonic_offline.db(version 1) with three tables:downloaded_tracks(track metadata + local paths + state),download_groups(album/playlist/song group + progress),group_track_cross_refs(many-to-many). See OfflineEntities.kt and OfflineDao.kt. - OfflineRepository (OfflineRepository.kt): holds hot
StateFlowmaps (_audioMap,_coverMap) keyed by songId/coverArtId for O(1) lookups inMusicRepository.streamUrl(). CallenqueueGroup()to schedule a download. - DownloadWorker (DownloadWorker.kt):
CoroutineWorkerusing plainHttpURLConnectionstreaming to.tmp→ rename. NosetExpedited()and nosetForeground()—FOREGROUND_SERVICE_TYPE_DATA_SYNCis banned on Android 15 (targetSdk=35) and crashes the app; downloads are silent background work. - Playback:
MusicRepository.streamUrl()checksofflineRepo.localAudioPath(id)first; if the file exists it returns afile://URI. This applies to every play path — album, playlist, Offline tab — no special offline mode needed. - Offline tab: Library → tab 2 (OFFLINE). Driven by
OfflineViewModel. Play button on a completed group callsofflineVm.songsForGroup()(JOIN query →DownloadedTrackEntity.toSong()) thenplayer.playSongs().
UI / references
When referencing files in chat, use clickable markdown links, not backticks:
[ytdlp.js](backend/lib/ytdlp.js).