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>
- 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>
- styles.css: remove overflow:hidden from .player-info-wrap — it was
clipping the player-status-row and shifting the shuffle badge
- index.html: use absolute /icon.svg path for favicon; add PNG fallback
for browsers without SVG favicon support
- icon-192.png / icon-512.png: regenerated from new equalizer SVG design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap player-info + star in .player-info-wrap (flex, align-items:center)
so the star is immediately to the right of the text block and centered
vertically with it, instead of floating to the far end of the 1fr column.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- icon.svg: replaced music-note with 5-bar equalizer waveform (gold
gradient on dark bg) — works well as favicon and app icon at all sizes
- index.html: add <link rel="icon"> so browsers pick up the favicon;
move #btn-star-current out of the title row and into player-track so
player-track's align-items:center centers it with the full info block
- styles.css: add flex:1 to .player-info base rule; remove stale mobile
overrides that assumed the star was inside player-info (position:absolute,
padding-right hack)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Artist name from Spotify/source is stored directly in folder, filename,
and tags without being replaced by the iTunes artistName. iTunes is used
only for supplemental metadata (genre, year, cover, duration, trackNum,
albumArtist/collectionArtistName). Sort tags still receive romaji via
romajiForStorage() in ytdlp.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consistent with point 7: original (kanji) names in folder, filename,
and main tags — romaji only in ARTISTSORT/ALBUMARTISTSORT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Japanese Art Tracks on YTM/YouTube are often romanized ("Souiunatsu"
for "そういう夏") — the Japanese keyword filter in --match-filter always
rejected the correct video. Channel + duration are sufficient quality
gates for JP tracks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Album words ("KAGUYA Music", "OST" etc.) appended to the search query
caused wrong or no results — "Kei Haneoka 確信 KAGUYA Music" would miss
tracks the plain "Kei Haneoka 確信" query finds immediately.
Artist + cleanTitle is sufficient for both JP and Western tracks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Japanese tracks stored with original names in folder/filename/tags; romaji only in sort tags
- findExistingTrack: case-insensitive, searches all album folders under artist, strips release suffix
- --max-filesize 30M + 2x duration cap on all yt-dlp calls
- iTunes collectionArtistName captured as album_artist; fixes compilation album splitting
- album_artist tag uses mainArtist (not rawArtist) so all tracks in one album are consistent
- No-duration cascade: steps 5-8 (title-only/no-filter) removed — fail cleanly like with-duration
- Autofill iTunes: fills title, artist, albumArtist, discNum (was album/genre/year/track/total only)
- Autofill MusicBrainz: fills albumArtist, discNum (was missing both)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both iTunes/MusicBrainz autofill buttons currently fill only partial form
fields; they should populate title, artist, album-artist, album, genre, year,
track, disc, total, cover — backend endpoints must return those fields too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Artists are stored ;-separated (feat./ft. normalized away at download), and
both lookups already use the first artist. The real gap is the strict match
failing for multi-artist tracks (no fallback) plus iTunes autofill not filling
the artist/album-artist fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>