diff --git a/CLAUDE.md b/CLAUDE.md index 19f09ed..43f0dbf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,6 +56,15 @@ Backend runs in Docker (see `docker-compose.yml`); on the deploy host it lives a 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. +- **Leading aesthetic characters:** artists often style their name with leading punctuation + (`.diedlonely`, `†…`). `metadata.js` strips leading non-alphanumerics (`stripLeadSym`) before the + iTunes lookup and in `firstWord`. Without it the lookup misses, falls through to a title-only + match, and returns an unrelated same-titled track — whose wrong duration then *also* misleads the + YouTube duration filter (observed: a 96 s track resolved to a 176 s slowed version). The same trim + runs client-side in Android's `ExploreViewModel.search()`. +- **Always write an `album` tag.** `ytdlp.js` defaults it to `Singles` when no album is known. + Previously no tag was written at all, so Navidrome showed its own "Singles" fallback while the + metadata editor showed "[Unknown Album]" — the two never matched and file resolution failed. - **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. @@ -79,6 +88,32 @@ Backend runs in Docker (see `docker-compose.yml`); on the deploy host it lives a - **Playback:** `MusicRepository.streamUrl()` checks `offlineRepo.localAudioPath(id)` first; if the file exists it returns a `file://` 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 calls `offlineVm.songsForGroup()` (JOIN query → `DownloadedTrackEntity.toSong()`) then `player.playSongs()`. +## Android navigation, state & insets (hard-won) + +- **A `BackHandler` inside a NavHost destination loses back priority to the NavHost.** The Erkunden + (Apple) artist/album are in-screen *overlays* on the `erkunden` destination, not routes — their + `BackHandler` never fired, so one back press popped the whole tab to Home. Fix: the overlay reports + its close action upward via `onOverlayBackChange`, and `AppNavigation` registers a **root-level** + `OnBackPressedCallback` for it (the same mechanism the NowPlaying overlay always used successfully). + Real nav routes (`album/{id}`, `artist/{id}`) are unaffected — `popBackStack()` is correct there. +- **Overlay state belongs in the ViewModel, not `remember`.** Anything an overlay shows (e.g. the + artist's "all albums" section) is destroyed when the user drills into an album, because the overlay + leaves composition. `rememberSaveable` does **not** save you — it only survives for real nav + destinations. Hoist it into the ViewModel (see `ExploreViewModel.artistAllAlbums`). +- **`bottomBar` insets:** the tab `NavigationBar` absorbs the system-navigation inset. Detail screens + have no tab bar, so the MiniPlayer needs `navigationBarsPadding()` itself — otherwise it sits behind + the phone's gesture/navigation bar. +- **`ModalBottomSheet`:** use `rememberModalBottomSheetState(skipPartiallyExpanded = true)` *and* + `navigationBarsPadding()` on the content, or the bottom-most action (e.g. "Herunterladen") opens + hidden behind the navigation bar and needs an extra drag-up. +- **Reuse the Ktor client.** `MusicRepository.coverArtUrl()/streamUrl()` must go through + `ClientHolder.get(creds)`. Constructing a `SubsonicClient` per call spins up (and never closes) an + `HttpClient` for *every cover art request* — this was the cause of visible scroll/tab-switch lag. +- **Star/favorite is optimistic and global** via `data/StarState.kt`. Toggle through it so every icon + (song rows, NowPlaying, context sheet) flips instantly and rolls back if the API call fails. +- **iTunes artists have no artwork.** The Search API returns no `artworkUrl100` for artist entities; + borrow the cover of one of their albums (`entity=album&limit=1`) — otherwise you get grey circles. + ## UI / references When referencing files in chat, use clickable markdown links, not backticks: diff --git a/README.md b/README.md index 3e003a4..12935ae 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Built with Vite + vanilla ES modules (web) and Jetpack Compose + Media3/ExoPlaye - **Playlists** — Create, rename, delete, add/remove tracks, and upload a cover (Web + Android). - **Importer & downloads** — Paste a Spotify / YouTube / YouTube Music / SoundCloud link to download via yt-dlp, with iTunes metadata enrichment and duplicate detection. - **Metadata editor** (web) — Edit tags + cover art; files are renamed/moved and Navidrome is rescanned on save. -- **Offline** (Android) — Download albums/playlists to private storage; an Offline tab to play and manage them; downloaded tracks play from disk on every screen. +- **Offline** (Android) — Download albums, playlists, or single songs to private storage; an Offline tab to play and manage them (works without a server connection); downloaded tracks play from disk on every screen. - **Audiobookshelf** — Browse and stream audiobooks with chapter navigation, speed control, sleep timer, and progress sync. Web is an installable PWA (themes, German UI); Android is native Jetpack Compose + Media3/ExoPlayer with a mini player, media/ABS notifications, and Android Auto. See [CHANGELOG.md](CHANGELOG.md) for the full web/Android parity matrix. @@ -65,7 +65,7 @@ NovaSonic/ ├── android/ # Native Android app (Jetpack Compose + Media3) │ └── app/src/main/kotlin/com/novasonic/app/ │ ├── api/ # SubsonicClient, AbsClient, LrcLibClient, ItunesClient -│ ├── data/ # PrefsStore (DataStore), MusicRepository +│ ├── data/ # PrefsStore (DataStore), MusicRepository, StarState │ ├── offline/ # Room DB, DAO, OfflineRepository, DownloadWorker (offline downloads) │ ├── player/ # PlayerController, MusicService (Media3) │ ├── ui/