fmtCount now shows the full grouped number below a million (toLocaleString de-DE) and only
compacts millions ("1,2 Mio."). The old "Tsd." form read awkwardly on the artist hero. SW ns-v53.
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>
- Search artist cards now use the Deezer artist picture directly (Deezer artists carry
a photo, unlike iTunes entities) instead of a null image + an extra album lookup;
the lookup stays only as a fallback when no picture is present.
- Non-local (Deezer) artist pages: fetch the Deezer artist picture (/artist/{id}) for
the hero photo, which was blank because getArtistImage only runs for Navidrome artists.
- Label "iTunes Künstler" → "Deezer Künstler".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`app.get('/api/deezer/*', …)` — a bare "*" — throws "Missing parameter name" in
Express 5's path-to-regexp at startup, so the server never came up and EVERY endpoint
(abs-proxy, romaji-overrides, deezer, …) failed with a CORS/ERR_FAILED in the browser.
Use a RegExp route instead, which registers cleanly on both Express 4 and 5. Verified
against express 5.2.1: bare "*" crashes, the regex is OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apple's iTunes /search returns 403 from server IPs, killing the Erkunden search. Deezer
is free, needs no key, and is actually better: it returns explicit versions, artist
images, track durations and 30s previews.
- Backend: /api/deezer/* proxy (CORS-safe); album download accepts a `tracks` array in
the request body (Deezer albums send their tracks directly, pre-mapped).
- api.js: itunesSearch / itunesLookupAlbum / itunesLookupArtistSongs|Albums now hit
Deezer and map results to the existing ItunesItem shape, so the UI + download flow are
unchanged. Function names kept.
- downloads.js: album downloads fetch the Deezer album's tracks and send them in the body.
iTunes /lookup (by ID) and the charts RSS proxy stay. Known follow-ups: artist-type
download still calls iTunes /search server-side; the metadata-editor iTunes autofill;
and the Android client — all still on iTunes for now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Confirmed via curl from a clean IP: Apple's iTunes /search returns 403 (blocked
server-side, likely datacenter-IP anti-scraping) while /lookup?id= still returns 200.
So the Erkunden Apple search fails through no fault of ours.
- Charts: the "most-played" RSS (rss.applemarketingtools.com) was fetched directly
from the browser and hit CORS; proxy it via /api/itunes-charts/:kind.
- Error visibility: itunesSearch records the last outcome (HTTP status / network);
the global-search view now shows "Apple-Suche gerade nicht verfügbar" with the
reason (403 = Apple blocks the API, not you) instead of a blank "no results".
- iTunes proxy now forwards the upstream status code instead of always 200.
Deezer (free, no key, ~50 req/5s) is the planned search fallback; MusicBrainz is too
rate-limited (1/s) for interactive search.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Search (web api.js) was JP-first, the download lookups were JP-first, and the Android
client was already US — so the same artist resolved inconsistently: sometimes kanji,
sometimes katakana, sometimes romaji, depending on which path and which store matched.
Switch everything to the US store:
- api.js itunesSearch → country=us (one fetch, matches the Android client)
- metadata.js lookupItunesTrackMeta → US only (drops the JP fetch: consistent names +
halves the download's iTunes calls, which also eases the rate limit)
- searchItunesAlbumTracks → country=us
- remove the JP-katakana guards in the album/artist download paths (redundant now, and
they wrongly romanized genuine Japanese artists like 羽岡佳 → "Kei Haneoka")
Titles still come from the source, so a Japanese title the user picked stays Japanese;
only iTunes-derived fields (album_artist, genre, year, artwork) follow the US store.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deletes left the removed item visible because the list loaders are cached (getPlaylists
~3 min): deletePlaylist navigated to a still-cached list, and deleteSong only reloaded
playlists. Add refreshCurrentView() — drops the API cache and re-runs the active nav
entry's loader (playlists, playlist/album/artist detail, starred, home, …). deleteSong
calls it after the delete+rescan delay; deletePlaylist invalidates getPlaylists before
navigating. Bump SW cache to ns-v46.
Also record the post-1.2 work in the 1.2 changelog (importer Enter, search single-song
+ radio, NowPlaying order, official-audio artist-metadata match, JP-katakana album
artist fix, year back-fill, delete refresh).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
embedMetadata already writes date/track/totaltracks/disc, but the single-track path's
"album already known" branch back-filled genre/total/disc/duration/track# from iTunes
and omitted the year — so a source track without a year got no `date` tag written.
Add !meta.year to the fetch trigger and merge genreMeta.year, so the release year is
set at download time like the other fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The iTunes lookup is JP-first (for correct Japanese titles/artwork), but the Japanese
store returns katakana-localized names for Western artists — so DJ Snake's single got
album_artist / folder "DJスネイク • アルーナジョージ" while the title/artist stayed English.
- lookupItunesTrackMeta: take the NAMES (album, album-artist, track, artist) from the US
store when the search isn't Japanese; keep JP names for actual Japanese songs.
- album & artist download paths: if iTunes' collectionArtistName is Japanese but the
request artist isn't, keep the request's English name for the tag and folder.
Already-downloaded files aren't rewritten — re-download or fix via the editor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web Apple-Music search proxies through /api/itunes (browsers can't call
itunes.apple.com — no CORS). The proxy forwarded every request uncached, so heavy
download activity (each download does several album/track lookups) plus client
searches blew past Apple's ~20 req/min/IP limit and the search started returning
nothing. Add a 30-min in-memory cache keyed by the target URL; only genuine 200
responses (with the resultCount signature) are cached, never throttle/error pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The strict cascade steps required the YouTube channel name to contain the artist, so
official uploads on label channels (BANGTANTV, HYBE LABELS, …) were rejected and the
no-channel fallback grabbed a fan reupload instead (observed: BTS "Pied Piper").
Add yt-dlp's music `artist` field as an OR alternative to the channel check in the
strict steps (two --match-filter args = OR). A metadata dump of "BTS Pied Piper"
confirmed only the official upload has artist=BTS (fan/live/lyric reuploads: NA), and
it's the #1 result — so it now passes strict step 2. channel_is_verified was rejected
as a signal: lyric videos and fan MVs also run on verified channels.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pressing Enter in the importer link input now triggers resolveImporterLink()
(same as the Importieren button). Bump SW cache to ns-v45.
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>
CHANGELOG: 1.2 entry for the post-1.1 work — web forward/back navigation, album
detail album-artist, metadata autofill "&" normalization, Android Erkunden back
re-assert + directional tab transitions, and the backend mixed-artist / album_artist
download fixes.
CLAUDE.md: album/artist download album_artist comes from iTunes collectionArtistName
(not the request/lead-single artist); client-side History router applies nsIdx on
popstate for both directions and must never pushState in the back path.
README left unchanged — the post-1.1 changes are fixes/polish, not new features.
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 favorite star lived inside .np-title-row, so it sat pinned to the title line
rather than centered against the title + album + artist block. Move it out to be a
flex sibling of .np-text inside .np-info (align-items:center), so it centers on the
full text block. Bump SW cache to ns-v43.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply the same collectionArtistName derivation to the per-album loop of the artist
download, so a collaboration/soundtrack in an artist's discography is not tagged with
the searched artist. Each track still gets its own iTunes artistName; album_artist
stays the iTunes album-level artist (consistent, no Navidrome splitting).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- 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>
Duplicate check matches exact computed path only, so iTunes/romaji/trackNum/
album drift between runs re-downloads existing tracks. The no-duration cascade
branch lacks Mittelweg protection and grabs wrong covers (Yorushika 車窓 →
Piano Echoes Shasou).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iTunes/MusicBrainz autofill don't fill the Album-Artist field; multi-artist
lookup splitting is inconsistent across the two endpoints (neither handles
/, feat., ft.).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Duplicate detection (filename-only), missing download size cap (52MB wrong
video), and uncaptured iTunes album artist (collectionArtistName) affecting
Navidrome grouping/sorting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captures the rules and hard-won details from recent work: no mocked frontend
data, two-part versioning, yt-dlp-only download cascade + Mittelweg rule, file
naming, the YTM-search limitation for obscure tracks, and edit/delete file
resolution. Loaded automatically by Claude Code at session start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Promote 0.7.1 → 0.8 with this session's work (Mittelweg cascade, web_music
client, clean filename, duration-filter + metadata-editor fixes, squid removal)
- Drop the patch component from all historical versions (0.7.0 → 0.7, etc.)
- Add Metadata editor and Song delete rows to the parity matrix (Web only)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a track's duration is known but nothing matches the duration window, the
cascade no longer falls back to blind title-only/no-filter grabs (which pulled
same-titled videos by other uploaders, e.g. a "recognize" fan clip). Instead it
relaxes only the duration constraint while still REQUIRING the artist channel +
title. If nothing matches, the download produces no file and the job now fails
cleanly with "Nicht eindeutig gefunden — bitte Direktlink im Importer nutzen"
(previously it falsely reported success with no file).
Single, album and playlist paths all surface the not-found state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-add the ¶ms= Songs-shelf filter (the documented way to song-search
YouTube Music) so results exclude videos/playlists/clips. Verified neutral
for the ambiguous-artist case (yt-dlp's search can't surface that track
regardless) but cleaner for normal tracks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The filename used the raw title, so "recognize - Single" leaked into the
filename ("01 - MINA - recognize - Single.mp3"). Now the cleaned title drives
both the filename and the embedded title tag, giving "01 - MINA - recognize.mp3"
(Track - Artist - Title). The album folder keeps the full album name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After the embedded-title change (recognize vs "recognize - Single"),
Navidrome's title-derived stem can drop the "- Single"/"- EP" suffix the
on-disk filename still carries, so _findFileByStem matched neither exact nor
suffix and every fallback failed -> "File not found on disk" on edit/delete.
Now also match on the structured prefix in both directions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The delete-song endpoint returned a bare "File not found on disk" with no
context, making path-resolution mismatches impossible to diagnose. Now
reports the Navidrome path, music root, and searched filename — matching the
detail the edit-metadata endpoint already provides.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ¶ms= Songs-tab filter re-ranked YTM results toward a same-named
artist's discography and excluded the official track (qNffDItRUeo) entirely,
even logged-out. The default blended search (what the browser shows) ranks
the exact track first. Removing the param so yt-dlp gets the same result set.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"MINA - recognize" skewed YTM ranking toward a same-named artist's full
discography, so the actual track (qNffDItRUeo) never appeared in results.
"MINA recognize" — matching how users type in the YTM search box — ranks
the exact official track first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The YTM search (getYtmVideoUrls) ran without the web_music client, so yt-dlp
parsed the search page as a generic YouTube search and returned unrelated
videos instead of the native Songs shelf — the actual artist track (e.g.
qNffDItRUeo for MINA - recognize) never appeared in results. Apply the same
player_client=web_music,default arg to the search step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
yt-dlp's android_vr player client returns "This video is not available" for
many YouTube Music Art Track / topic uploads (e.g. MINA - recognize) that
play fine in the browser. Switch to player_client=web_music with default
fallback so these tracks become downloadable, fixing the cascade falling
through to wrong YouTube videos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
searchTitle (e.g. "recognize") is now passed to embedMetadata instead of
the raw title ("recognize - Single"), so the ID3 title tag matches the
actual song name. Album tag stays unchanged ("recognize - Single").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>