Preview bar:
- 30s previews used to keep playing with no control once you left the album/artist/Explore
screen they started on. Moved the preview player from ExploreViewModel to an app-level
PreviewController, and added a global PreviewBar (above the mini player, on every screen)
to pause/resume/stop it. Starting real library playback cancels the preview.
Artist fan count (parity with web):
- ItunesItem carries fanCount (Deezer nb_fan); shown on search artist cards and the artist
page ("559.542 Fans" / "1,2 Mio. Fans"). Combined with the existing nb_fan sort this makes
same-name collisions (5x "Joji") resolve to the right artist on Android too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deezer's artist search doesn't rank by popularity, so a name collision (5 artists named
"Joji") returned a niche one first (62 fans) instead of the real Joji (559k) — the artist
page then loaded the wrong discography and owned songs didn't show as downloaded.
- api.js: _dzArtist carries fanCount (nb_fan); artist search sorts by fanCount desc.
- search.js: artist cards show the fan count as subtitle; fan count passed to the artist page.
- detail.js: pick the most-popular name match (not the first); for local Navidrome artists
with a real collision (>=2 same-name candidates), tie-break by SONG OVERLAP — prefer the
Deezer candidate whose top tracks actually overlap the library (each artist has different
songs, so this nails the right person). Costs a few calls only on a collision.
- Artist hero shows "<n> Fans" (immediately for global-search artists, after match for local).
- Android: DzArtist.nb_fan + /search/artist sorted by nb_fan so the right artist is first.
- Also: web global-search "unavailable" message now says Deezer, not Apple. SW ns-v52.
Verified live against Deezer: both fan-sort and overlap select the real Joji (id 5078097).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apple's iTunes /search returns 403 from server/datacenter IPs, which had broken global
search, charts, album/artist/single downloads and the metadata-editor autofill in
production. Everything now sources metadata from Deezer (free, no key, more requests,
returns explicit versions + real artist images + durations). Web + Android share one
source, so an artist resolves to one consistent name everywhere. Deezer results are
mapped to the legacy "ItunesItem" shape, so the UI + download flow are unchanged.
Backend:
- New backend/lib/deezer.js: lookupTrackMeta, fetchAlbumTracks, searchAlbumTracks,
fetchArtistAlbums (+ pure pickTrackMatch/mapAlbumTrack for tests).
- metadata.js drops the iTunes lookups; novasonic-server download paths (track, album,
artist, playlist enrich, spotify artwork) + autofill now use Deezer.
- Artist download uses Deezer discography (real albums only), not the iTunes album search.
- Editor autofill endpoint /api/itunes/lookup -> /api/meta/lookup; removed the dead
iTunes /search proxy and the Apple charts RSS proxy.
- backend/test/deezer.test.js: unit tests for numbering, mixed-artist album-artist
stability, explicit flags, the Japanese-title match guard (npm test; RUN_LIVE=1 for live).
Web:
- api.js search/lookups already on Deezer; charts now use Deezer /chart. Artist cards
show the real Deezer photo. Visible "iTunes"/"Apple Music" labels -> "Deezer"
(search badge, charts badge, preview bar, editor button, artist heading). SW ns-v51.
Android:
- New DeezerModels.kt with Dz* models + mappers to ItunesItem/ItunesRssEntry.
- ItunesClient is Deezer-backed (search = tracks+albums+artists in parallel; album/artist
lookups; /chart charts), same method signatures. Visible strings -> Deezer.
Docs: README, CLAUDE.md and CHANGELOG (1.3) updated; Platform Parity Matrix reflects the
shared Deezer source + Android non-local artist page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Playing a song from search results queued the whole result list. Now it plays only
the clicked song; Radio mode, if on, fills the rest — closer to Spotify. Both
platforms already trigger a radio top-up on the first track (web playQueue →
fillRadioQueue; Android onMediaItemTransition → checkRadioRefill), so no extra wiring
is needed. Album/playlist play still queues the full list. Bump SW cache to ns-v44.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reordered the NowPlaying track info to title → album → artists (was title → artists
→ album), so the Android layout matches the PC/web player.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tab switches slid in a fixed direction (and sometimes flipped between push/pop feel,
so the direction felt random). Now the slide follows the target tab's position in the
bottom bar: tapping a tab to the right slides in from the right, a tab to the left
slides in from the left — applied across all four enter/exit/popEnter/popExit
transitions so it's consistent regardless of how Compose classifies the move. Detail
screen push/pop transitions are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The root-level overlay back callback was keyed only on the overlay state, so after an
album drill-down or tab switch the NavHost could re-register its own back callback
above ours and regain priority — one back press then popped Erkunden → Home instead
of dismissing the overlay ("sometimes" bug). Key the DisposableEffect on
currentBackEntry as well, so the callback is removed and re-added (bumped to the top
of the OnBackPressedDispatcher) after every navigation while an overlay is open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Since the offline-download button was added, four buttons shared one row while Play
and Shuffle also carried text labels — the labels got squeezed out and the buttons
looked compressed. All four are now equal-width and icon-only (play, shuffle, edit,
download); Play stays filled as the primary action, the rest tint accent when
active/done. Icons carry contentDescriptions for accessibility.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- On detail screens (album/artist/playlist) the MiniPlayer was the bottom-most bar
with no NavigationBar to absorb the system-navigation inset, so it sat behind the
phone's gesture/nav bar. Pad the bar with navigationBarsPadding() when the tab bar
is absent.
- The Erkunden (iTunes) artist "show all albums" view used local remember state, so
drilling into an album and back reset it to the main artist view. Hoist the open
section into ExploreViewModel.artistAllAlbums so it survives both nav-route and
overlay round-trips, and wire its back into the root overlay-back handler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The iTunes artist / album detail in the Erkunden tab are in-screen overlays on the
NavHost "erkunden" destination, not real routes. Their BackHandler lost back priority
to the NavHost, so one back press popped Erkunden → Home instead of dismissing the
overlay (artist→search, album→artist).
Handle the overlay's back at the app root via OnBackPressedCallback (the same proven
mechanism as the NowPlaying overlay): ExploreScreen reports its current overlay close
action up through onOverlayBackChange, and AppNavigation registers a root-level back
callback for it. Also covers an album opened straight from search (no artist), which
had no back handling at all. Removes the temporary nav-debug strip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shows the live NavController back stack at the top of the screen so we can see
whether detail screens are being pushed correctly or the back button isn't being
handled. To be reverted once the back-navigation issue is diagnosed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MiniPlayer now shows on every screen (album/artist/playlist detail), not just
top-level tabs; the tab bar stays top-level-only.
- Local library song search results get the 3-dot context menu (add to playlist,
favorite, queue, download, …) via onShowContextMenu.
- Offline tab is fed by the local Room DB and no longer waits on the online library:
it stays usable while the library is loading or the server is unreachable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iTunes artist entities carry no artwork, so search results rendered as bare grey
circles. Mirror the web PWA: after showing results, fetch one album per artist and
borrow its cover (entity=album, limit=1), applied only if the user is still viewing
the same artist set. Also give ArtistCard a Person-icon fallback instead of an empty
grey dot, and fall back to the artist id for Subsonic (library) artist covers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "was herunterladen" sheet (track/album/artist + Herunterladen) opened only
half-expanded, hiding the download button behind the system nav bar until you
dragged it up. skipPartiallyExpanded=true opens it fully (button already clears
the nav bar via navigationBarsPadding).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Song context sheet now opens fully expanded (skipPartiallyExpanded) so the
"Herunterladen" option at the bottom is reachable without an extra drag-up.
- The "new playlist" field in the playlist picker now matches the app's search
bars: rounded 12dp, filled surface, leading icon, NsAccent/NsOnSurface3 borders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The picker listed playlists with a generic list icon; now each row shows the
playlist's cover art (falling back to the icon when there's none), so you can
tell them apart at a glance when adding a song.
- web (modals.js): 34px cover thumbnail per row.
- android (PlaylistPickerSheet): 40dp AsyncImage cover, coverUrl passed from AppNavigation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Artists like ".diedlonely" (leading dot, †, ✝, _ …) weren't surfaced by iTunes,
so the download's metadata lookup fell through to a titleOnly match — grabbing a
same-titled track by another artist (e.g. the "In the Bleak Midwinter" Christmas
carol: Holiday genre, 176s single) and steering the YouTube search to the wrong
(slowed) version.
- backend/metadata.js: strip leading non-alphanumeric from the search artist and
the first-word comparison so the real artist matches (and the right album/genre/
duration are used).
- android ExploreViewModel: strip leading symbols from the search term so the
Explore tab finds these artists.
Keeps the original name for display/storage — only the search/compare is trimmed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Android: MusicRepository.coverArtUrl/streamUrl built a fresh SubsonicClient
per call — each spins up (and never closes) a Ktor HttpClient. That ran for
every cover on every recomposition, causing heavy GC/alloc churn (and a leak)
that showed up as scroll/tab lag. Reuse the cached ClientHolder client for URL
building instead.
Web: the home scroll-row arrows called getBoundingClientRect on every scroll
event (layout thrashing). Split it: only toggle arrow visibility on scroll
(rAF-throttled); do the getBoundingClientRect vertical-centering only on
resize/content change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every other favorite toggle uses Favorite/FavoriteBorder (heart); Now Playing
was the odd one out with Star/StarBorder. Make it a heart everywhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping the star only fired the API and never updated any UI state, so the
icon didn't change (no feedback). Add a global reactive StarState with
optimistic overrides: the star flips instantly across every screen showing
that song (SongRow lists, Now Playing, context sheet), the API runs in the
background, and the override reverts if the call fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The artist screen's "show all albums" overlay state was plain remember(), so
navigating into an album and pressing back reset it — dumping you on the
collapsed artist page instead of the list you came from. Persist which section
is open via rememberSaveable (a String? title) so it survives the round-trip,
and re-derive the overlay entries from the (surviving) categorized albums.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rapid skips + lyrics prefetch fired the full lrclib cascade many times over,
so lrclib throttled the IP and requests timed out (canceled) on both web and
Android. Reduce the request volume:
- Web: de-dupe concurrent fetches per song (prefetch + tab-open now share one
cascade); bump the per-request timeout 6s → 8s.
- Android: per-song Mutex so prefetch + on-open load don't both run the cascade;
send a proper User-Agent (lrclib asks for one and treats it better); debounce
the prefetch (1.5s, cancelled on the next skip) instead of firing immediately
on every track change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Warm the lrclib cache the moment a track starts (Android PlayerController
onMediaItemTransition; web onNpSongChange) so opening the Lyrics tab is
instant. Extract a DOM-free fetch helper on web for reuse.
Rewrite Android lyrics autoscroll to center on the active line's measured
position in a LazyColumn instead of a fixed line-height estimate — removes
stutter and the downward drift on wrapped lines; active line sits at a
~40%-from-top reading anchor.
CHANGELOG: fold into 1.0; drop the now-fixed lyrics known-issue note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- icon.svg / icon-192/512.png: blue→purple equalizer gradient design
- nginx.conf: add /app/ alias so reverse-proxy prefix resolves assets
- vite.config.js: base /app/ + %BASE_URL% in favicon links so path
resolves correctly whether app is served at / or /app/
- ic_launcher.xml: same equalizer design for Android app icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android: camera hero button removed; cover upload moved into edit dialog.
Web: trash icon removed from cover actions in edit modal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Playlist: create (FAB + dialog), rename, delete, remove songs, cover upload/remove
- Library Albums tab: sort chips (Neueste/Älteste/A-Z/Künstler) matching StarredScreen
- ABS notification: shows current chapter title; updates live without re-buffering
- Home hero: pool now matches web (frequent(5) ∪ recentlyPlayed(5)); label MEISTGESPIELT / ZULETZT GEHÖRT
- Home: added Zuletzt gehört section (type=recent) matching web layout
- Notifications: fixed delivery bug — polling moved to NotificationStore app-lifetime scope so notifications arrive even after user navigates away from ImporterScreen
- NotificationStore.startPoll() extended with navType/navAlbum/navArtist/navId params
- Removed defunct AbsPlayerService.kt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'drift to next line' animation moved the text while the user was
still reading/singing the current line, making it feel out of sync.
Now matches the web behavior: smooth-scroll to center the active line
when it changes, nothing more.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Notification sheet: add 'Zum Album →' / 'Öffnen →' button; tapping searches
Navidrome and navigates to the matching album/playlist
- AppNotification: add navType/navAlbum/navArtist/navId fields (mirrors web notify.js)
- ImporterViewModel: store resolved album/artist/type in notification after download
- ABS Android: search bar with 400ms debounce, server-side search via /search endpoint
- AbsClient: add searchLibraryItems() for library-specific search
- AbsScreen/abs.js: strip HTML tags from book descriptions (<p> tags no longer visible)
- NowPlayingScreen: vibrant palette swatch + lighter scrim (0.15) + wider gradient
for more visible dominant color background
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ABS: new AbsBooksScreen loads books directly on tab open; library pills shown only when >1 library — no more two-step library→books navigation
- Nav: shorten "Hörbücher"→"Bücher", "Einstellungen"→"Einst.", reduce label font to 9sp with maxLines=1 to prevent label wrapping
- NowPlaying: increase top padding so the tab bar sits lower on screen (statusBarTop + 16dp, min 52dp)
- NowPlaying: replace blurred-cover background with dominant-color gradient extracted via Palette from cover art; animates on track change; falls back to dark color
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AbsClient: add session API (startSession, closeSession, getProgress, saveProgress, getItemChapters) and data classes (AbsChapter, AbsAudioTrack, AbsPlaySession, AbsProgress)
- AbsPlayerController (new): standalone ExoPlayer controller for ABS audio; tracks global time across multi-track audiobooks, syncs progress to ABS every 30 s, chapter navigation, speed control, sleep timer
- AbsScreen: book detail screen with cover/meta/progress/chapter list/play button; AbsNowPlayingScreen overlay (progress bar, prev/+30s/play/-30s/next controls, speed picker, sleep timer, chapter list with auto-scroll)
- AppNavigation: create AbsPlayerController, wire up book detail and player overlay, hardware back button handling for all ABS sub-screens
- PrefsStore + SettingsViewModel + SettingsScreen: absChapterTrackMode toggle — progress bar shows current chapter length instead of full book
- README: update ABS section to cover both web and Android features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings:
- Reordered to match web: Navidrome → Bibliothek → Theme → Audiobookshelf → Discover APIs
- Save button now appears at top AND bottom for easy access
- Audiobookshelf section: Server URL + API Key (masked) + green/red status dot with Test button
- NovaSonic Backend section: same green/red conn dot + Test button
- Connection status uses ConnStatus sealed class (Idle/Checking/Ok/Err)
Audiobookshelf:
- New AbsClient.kt: Ktor HTTP client, proxies through NovaSonic backend or direct with Bearer token
- New AbsScreen.kt: library list → book list with covers, author, duration
- Hörbücher tab appears in bottom nav only when ABS URL + API key are both set
- absUrl + absApiKey persisted in DataStore
Last played song:
- PlayerController saves song to DataStore on every media item transition
- MiniPlayer shows last song (dimmed, single play button) when no active queue
- Tapping play fetches the song from Navidrome and resumes
- LastPlayedSong stored in DataStore (id, title, artist, coverArt)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Android: show nothing while DataStore loads credentials (null initial
value) to prevent login screen flash on startup when already logged in
- Web: dispAlbumName now only uses sortName as romaji for Japanese titles;
English albums with lowercase ALBUMSORT tags now display correct casing
- lrclib: add no-duration fallback exact match to find songs where local
file duration differs from lrclib entry (e.g. Sanctuary Opening 5:58 vs 4:24)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NetEase returned wrong/unsynced results. lrclib with exact + fuzzy
search is sufficient; no third-party Chinese platform needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 simultaneous requests were hitting lrclib rate limiter.
Now: exact match alone → if no content, 2 parallel searches.
Also check response.status === 200 before parsing to avoid treating
404/429 error bodies as valid empty lyrics responses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Android + web: fire exact/artist-search/title-search in parallel so
total wait = slowest single request instead of sum of all three
- Android: HashMap cache keyed by artist|title, cleared on app restart
- Web: Map cache (session lifetime), cache hit is instant re-open
- Web: extract _showLyricsResult helper, safeJson wrapper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Android: add 6s HttpTimeout to LrcLibClient, wrap each step in withTimeoutOrNull
- Both: add title-only lrclib search (step 3) so "Sanctuary (Opening)" is found
even when artist tag differs ("Utada" vs "Hikaru Utada")
- Web: reduce all timeouts 8s → 5s, extract bestMatch helper
Search order: lrclib exact → lrclib artist+title → lrclib title-only → NetEase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Search order: lrclib exact → lrclib fuzzy search → NetEase.
NetEase has strong coverage for Asian/Japanese music that lrclib misses.
Duration-matched candidate selection on all three steps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Navidrome lyrics plugin does not implement getLyricsBySongId for synced
lyrics — removed all plugin calls. Both Android and web now query
lrclib.net/api/get (exact match) with /api/search fallback for metadata
mismatches (e.g. artist tagged differently in Navidrome vs lrclib).
Also fixes Android lyrics scroll tween<Int> type inference error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace AnnotatedString/ClickableText (offset-based click was unreliable)
with a horizontally-scrollable Row of individual Text composables. Each
artist has its own explicit clickable modifier bound directly to its ID.
ID de-duplication: if Navidrome copies the primary artistId to all entries
in the artists array, only the first keeps the ID; others render as
non-navigable text (dimmed NsAccent) instead of silently going to wrong page.
Fallback: split song.artist by ';' when artists array is absent/single,
matching the web app's parseArtists() logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Song model: add SongArtist{id,name} + artists: List<SongArtist>
Navidrome sends this via OpenSubsonic extension — previously ignored
- NowPlaying: if song.artists has >1 entry, render each name as an
inline AnnotatedString with ClickableText, separated by ' · ';
every artist navigates to their own page with their real ID
Falls back to single artist/artistId entry when array is absent
- AppNavigation: onArtistClick now receives artistId: String instead
of a fixed closure, enabling per-artist navigation from the sheet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>