The album download tagged album_artist with the request's `artist`, which can be a
single track's performer — the lead single "TAKEDOWN" is by TWICE, so a whole
KPop-Demon-Hunters soundtrack got album_artist=TWICE on every track. Derive the album
artist from the iTunes tracks' collectionArtistName (the album-level artist, identical
across the album) and fall back to the request artist only if iTunes omits it. This is
the documented intent (mainArtist = collectionArtistName when available) and keeps
album_artist consistent so Navidrome doesn't split the album.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
downloadAlbumTracks passed each track's artistName as `artist` but no `rawArtist`,
so in ytdlp.js the YTM/YouTube search query and the channel filter used
`mainArtist = albumArtist || artist` — always the album-artist blob. On mixed-artist
soundtracks that matched nothing: e.g. TWICE's "Strategy" and MeloMance's "Love,
Maybe" were searched as "KPop Demon Hunters Cast, HUNTR/X & Saja Boys <title>" with
`channel ~= KPop`, and failed with "Nicht eindeutig gefunden".
Pass rawArtist = track.artistName so the search + channel filter use the actual
performer ("TWICE Strategy", channel ~= TWICE). The album_artist tag still comes from
`albumArtist`, so album grouping is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
loadAlbumDetail overwrote the header artist with parseArtists(songs[0]) — the first
track's artist. On soundtracks/compilations that is wrong (e.g. "KPop Demon Hunters"
showed "TWICE" from track 1 "TAKEDOWN"). Use r.album (the getAlbum response, which
carries the album's own album-artist tags in .artists / .artist) so it matches the
Android app: "KPop Demon Hunters Cast • HUNTR/X • Saja Boys".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Forces installed PWAs to pull the new build (metadata "&" split + forward
navigation fixes) instead of serving stale cached assets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The popstate listener always called _navGoBack(), but both Back and Forward fire
popstate — so Forward was treated as Back and never restored the page you came from.
_navGoBack also pushState()'d on every back, which wiped the browser's forward stack
entirely.
Route all history through the History API: navigate() pushes a state (nsIdx) only on
genuine new navigation; popstate now applies whichever entry the browser moved to via
_navOnPopState(state.nsIdx), covering Back AND Forward with no pushState in the
handler. In-app back buttons / Backspace go through history.back() so there's a single
path and the browser stack stays in sync. NowPlaying overlays still close first on a
back press.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apple Music / MusicBrainz render multi-artist credits as "A, B, C & D" — the last
artist is joined with " & " instead of the list separator, so it stayed glued to the
previous one (e.g. "REI AMI & KPop Demon Hunters Cast" became a single artist that
Navidrome's ";" split could not break apart).
_fmtArtists now also rewrites " & " to the list separator, but only when the string
is already a list (contains ; , • ·), so real band names like "Simon & Garfunkel"
stay intact. The iTunes and MusicBrainz autofill results are now run through
_fmtArtists too (previously set raw), so the artist field comes out ";"-separated
and matches the Navidrome Tags.artist.Split config.
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>
CLAUDE.md:
- Download pipeline: leading aesthetic characters in artist names must be trimmed
before the iTunes lookup (a missed match cascades into a wrong-duration YouTube
grab), and an album tag is always written (defaulting to Singles) so Navidrome
and the metadata editor agree.
- New Android section covering the traps this release cost the most time on: a
BackHandler inside a NavHost destination loses back priority (overlay back must
be registered at the app root), overlay state must live in the ViewModel rather
than remember/rememberSaveable, bottomBar and ModalBottomSheet inset handling,
reusing the Ktor client, global optimistic StarState, and iTunes artists having
no artwork.
README: offline downloads also cover single songs and work without a server
connection; StarState added to the data/ listing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Polish release covering Android navigation and offline resilience, download
accuracy for stylised artist names, and the lrclib lyrics-throttling fix.
Also updates the parity matrix (single-song offline downloads, player bar on all
screens) and corrects the Windows media-tile known bug — the two-element gapless
handoff was kept for skip speed, not reverted as the entry claimed.
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>
Two-part fix for "[Unknown Album]" vs "Singles" mismatch that broke the
metadata editor (File not found):
- Download: always write an album tag, defaulting to "Singles" so it matches
the on-disk folder (album || 'Singles'). No-album tracks no longer show as
"[Unknown Album]" while living in a "Singles" folder. (Future downloads.)
- Editor resolve: Subsonic getSong returns a *virtual* tag-built path (e.g.
"[Unknown Album]/Title.mp3" with raw "|"), not the physical file ("Singles/
Artist - Title_YTPMV.mp3"). Add findExistingTrack as a final fallback, and
make its normalizer map the sanitized chars (/ \ : * ? " < > | and _) to a
space so a title tag matches the sanitized filename. (Fixes existing tracks.)
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>
Ping Navidrome with scrobble?submission=false when a track starts so
getNowPlaying reflects it instantly (feeds e.g. a Discord rich-presence
plugin / other clients' Now Playing). Independent of the existing delayed
"played" scrobble used for play counts and Last.fm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow artists and get notified on new releases (iTunes/MusicBrainz/Last.fm)
and nearby concerts (reusing the existing Ticketmaster integration), surfaced
in the in-app notification bell, with a download action from the alert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Themed/random discovery mixes (Rock/Charts/Random) that auto-download via the
yt-dlp backend, auto-delete after a configurable retention time, with an option
to keep tracks in the permanent library. Captures sources, retention, keep flow,
and open questions for later implementation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The NP overlay only refreshed its cover while open, so skipping with it closed
left the <img> on the last-opened song; reopening briefly showed that old cover
until the current one loaded. Now the NP cover element is kept pointed at the
current song even while closed (loads in the background), so opening it always
shows the right artwork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reintroduce the two-element handoff, this time as a skip-speed optimization
(not for the OS tile). A second <audio> element pre-buffers the next track;
on a forward skip playCurrent hands playback over to it instead of reloading
the source, so playback starts immediately instead of ~1s. Covers are
pre-warmed in parallel so the new artwork is ready too.
Listeners are bound to both elements once (handlers guard with el !== S.audio).
Safety: handoff only when the standby is buffered (readyState >= 2); otherwise
falls back to the unchanged source-swap path, and a failed handoff play()
recovers with a fresh source load. Web-only; the native bridge path is untouched.
Note: this does not change the OS media-tile flicker (a separate, MSE-only fix).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removing the second <audio> element fixed most of the OS media-tile flicker,
but skips now load the next track fresh → a brief gap where Windows shows the
generic site entry. Prefetch the next track's first ~4MB with fetch() (not a
media element, so no Chromium session flip) to warm the HTTP cache + TLS
connection; the src-swap then starts faster and the gap shrinks. Abortable on
queue change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navidrome's player (react-jinke fork) uses one persistent <audio> element and
does not flicker the OS media tile, so the source-swap itself isn't the cause.
Our one structural difference was a second buffering <audio> (_preAudio) for
preload. A second media element seems to make Chromium hand the OS media
session over on skip → Windows tile flicker. This drops it; we rely on the main
element's preload="auto" + browser HTTP cache. Cover art is still prewarmed.
Trade-off: next-track start may be marginally slower. Easy to revert if it
doesn't fix the flicker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two-element gapless handoff was reverted (switching <audio> elements
still ends/starts the OS media session, so Windows rebuilds the tile).
Correct the changelog claim and add a Known Bugs entry; the real fix is MSE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a second <audio> element; S.audio always points at the active one and
listeners are bound to both once (handlers guard with el !== S.audio).
preloadNext buffers the next track into the standby element, and playCurrent
hands playback over to it on skip instead of swapping the source — so the
active element never empties and Windows keeps the media tile (also makes
forward skips instant/gapless).
Safety: handoff only when the standby is buffered (readyState >= 2); falls
back to the unchanged source-swap path otherwise, if there's no second
element, or if handoff play() rejects. Web-only — the native bridge path is
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
prefetchLyrics now fires ~1.5s after a song change instead of immediately,
so the lrclib requests no longer compete with audio-stream buffering when a
new track starts. Timer is cleared on rapid song changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Home rows get hover left/right arrows that only show for the scrollable
direction (enhanceScrollRow + initScrollRows, called from startApp);
visibility recomputed on scroll/resize/content-load.
Fix the OS media tile flicker when skipping on desktop PWA: mutate the
existing MediaMetadata in place instead of assigning a new one each track
(the replacement made Windows tear down and rebuild the tile). Action
handlers are now set once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Space = play/pause, ←/→ = seek 5s, Shift+←/→ = prev/next, ↑/↓ = volume.
Handler lives in player.js (where _pos/_dur/seekToMs are in scope), wired
from startApp via initKeyboardShortcuts(). Ignored while typing in inputs,
and yields Space to focused buttons/links so it never double-fires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add PNG icons (192/512) for a crisp taskbar/Start-menu icon, drop the
portrait orientation lock so the window resizes freely on desktop, and add
a stable app id. Installs as a standalone desktop window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
loadHomeData now takes a force flag; navigating to Home via a click
(not back/forward) invalidates the home rows' API cache so they re-fetch
and the random row re-randomizes — matching the Android re-tap behavior.
CHANGELOG: note the Home-refresh fix under 1.0.
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>
Promote the 0.10 changelog entry to 1.0; feature-complete across web and
Android. Android versionName was already 1.0.
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>
- Sidebar: replace ♫ emoji with new equalizer SVG icon (icon.svg)
- Mobile header: remove duplicate logo+name next to search bar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep nginx /app/ alias for favicon resolution only.
CSS/JS stay at root-relative paths via location /.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>