Findet das Backend die von Navidrome gemeldete Datei nicht am angegebenen Pfad
(Subsonic liefert oft einen aus Tags gebauten virtuellen Pfad), sucht es ueber
Dateiname bzw. Kuenstler+Titel weiter. Liegt derselbe Song mehrfach auf der
Platte, konnte das die falsche Kopie treffen: die wurde getaggt, der bearbeitete
Song blieb unveraendert — die Aenderung "sprang zurueck". Das erklaert, warum es
bei Songs ohne Dublette immer ging und bei anderen nie.
Der ueber Aehnlichkeit gefundene Kandidat wird jetzt per ffprobe gegen die Tags
geprueft, die Navidrome fuer diesen Song meldet, und bei Widerspruch mit 409 und
erklaerender Meldung abgebrochen statt geraten. Zusaetzlich melden uebersprungene
Umbenennungen und Verschiebungen sich als Warnung, statt stumm zu bleiben; die
Antwort nennt den tatsaechlich bearbeiteten Pfad, der Editor zeigt beides an.
Ausserdem: -dontwarn org.slf4j.** — der Release-APK-Build brach seit laengerem an
R8 ab (Ktors Logging referenziert den slf4j-1.x-Binder, den es auf Android nicht
gibt). Vorbestehend, mit HEAD~1 verifiziert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Backend serves a staged APK so co-users self-serve the latest build:
- GET /api/apk streams APK_PATH (default /data/novasonic.apk) with the
android package content-type + attachment disposition; 404 if absent.
- GET /api/apk/info reports availability, size, mtime, and an optional
version from /data/novasonic.version.txt.
- Settings gets a "🤖 Android App" section that appears only when an APK is
present and shows version/size/date.
Maintainer updates the APK with:
docker cp app-release.apk novasonic-backend:/data/novasonic.apk
Android source is already tracked; only build artifacts stay out of git.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds GET /scrobble/users (distinct users for the partner picker) and
GET /scrobble/compare?with= to the scrobble service. Taste match = cosine
of the two users' artist-play vectors (0..100); also returns shared artists
(mirrored bars), shared tracks, and each side's unique top artists. Reuses
the existing per-user play-log — no new data capture.
Web UI src/ui/compare.js (overlay from Settings "🎧 Musik-Match"): partner
picker fed by /scrobble/users, match-score ring, per-user totals, shared +
unique lists with clickable artist/album rows. Cosine + shared logic verified
against a hand computation on an in-memory DB. Android pending (parity: 🔜).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deezer's /search/artist is a ranked, incomplete fuzzy match — low-profile
same-name artists (e.g. Nana id 80239872) never appear in results. Add a
"paste Deezer link or ID" field to the pin picker that looks the artist up
directly via /artist/{id} (new deezerLookupArtist) and pins it. Same reliable
route as the importer direct link vs yt-dlp search.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes same-name collisions where NovaSonic guessed the wrong Deezer artist (e.g.
'Nana' → a random DJ): fans, top songs, missing albums and the hero image were all
off. A '⚙ Falscher Künstler?' button on the artist page opens a picker (same-name
Deezer candidates with image + fan count); the chosen mapping is stored SERVER-SIDE
(shared across users, like the romaji overrides) in /data/deezer-artist-overrides.json.
Once pinned, the artist page short-circuits the name search to the pinned Deezer id
(drives albums/top-songs/fans) and overrides the hero image with the pinned artist's
Deezer picture (NovaSonic-side, so it works even for tag-merged same-name artists —
Navidrome can't disambiguate them without an MBID). New endpoints
GET/POST/DELETE /api/artist-overrides; api.js load/get/set/remove; loaded at startup.
SW cache -> ns-v74. Needs on-device runtime test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Play-log now stores each play's genre (from the song tags; web scrobbleLog +
android PlayLogClient capture song.genre). The stats endpoint returns topGenres
(GROUP BY lower(genre), case-insensitive, blanks ignored); web + android render a
Top-Genres section. Schema migrates via ALTER TABLE. Old plays have no genre
(fills going forward, like cover art). SW cache -> ns-v72.
Android compileDebugKotlin + web build green; genre grouping + null-ignore unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Player: opening the PWA no longer auto-plays when the user had paused. Pausing via
the media notification while backgrounded routes through togglePlay, which now sets
S._userPaused; _onForeground only auto-resumes an INVOLUNTARY background stop
(_wasPlayingBeforeHide && !_userPaused). playCurrent clears the flag on a new track.
Involuntary-stop/freeze recovery on foreground is preserved.
Stats modal: no more small→big flash when switching period/tab — the body keeps a
stable min-height and retains its current content until the new data arrives (only
the first open shows 'Lade…'); a per-request token drops out-of-order responses.
SW cache -> ns-v71.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Navigator can't step before logging began: the endpoint returns firstPlayedAt
(MIN(played_at) over all the user's plays, unfiltered by window); web + android
disable the back arrow once the shown year/month is at/before that first play.
- Top artists/tracks/albums are now clickable → open the artist / album page (web
navigates and closes the overlay; android navigates via nav routes). Tracks open
their album.
- Android: moved the 'Meine Statistiken' button to the top of Settings (was at the
bottom near logout). SW cache -> ns-v70.
Android compileDebugKotlin + web build green; firstPlayedAt verified server-side.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navigator (web + android): browse Alles / Jahr / Monat with a ‹ › stepper — see
top plays for a specific month or year (e.g. March 2024). Steps into the future
are disabled. Uses the existing /scrobble/stats endpoint with computed from/to +
bucket (month for year view, day for month view). All windows verified.
Cover art fix: the app resolves covers from the Subsonic **coverArt token**, not
the raw entity id — so stats covers (which used albumId/songId) never loaded.
Now the play-log stores each play's coverArt (web + android capture), the stats
endpoint returns it for top tracks/albums, and both clients render coverUrl(coverArt)
with an id fallback. Old plays lack coverArt (blank until re-listened); artist
images still use artistId (may need getArtistInfo2 later if blank). Schema migrates
existing DBs via ALTER TABLE. SW cache -> ns-v69.
Android compileDebugKotlin + web build green; server windows + cover storage unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 4 (part B): Android statistics screen consuming GET /scrobble/stats —
tiles (plays + listening time), day/month/year play series, top artists/tracks/
albums, 24h listening clock, period tabs; opened from a Settings button
("Meine Statistiken"). New StatsViewModel + StatsScreen + ScrobbleModels;
PlayLogClient.fetchStats() (tolerant JSON decode, null on any failure);
repo.fetchScrobbleStats(); nav route + SettingsScreen(onOpenStats).
Cover art everywhere: top artists show a circular artist image, tracks/albums
a square cover — resolved client-side from Navidrome via artistId/albumId/songId
(no coverArt knowledge in the scrobble service). Endpoint now also returns
albumId for top tracks so track covers resolve reliably. Web stats list gets the
same thumbnails. SW cache -> ns-v67.
Android compileDebugKotlin + web build green. Needs on-device runtime test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opens from Settings ("Meine Statistiken") as a self-contained modal overlay
(no router changes). Fetches GET /scrobble/stats and renders: totals tiles
(plays + listening time), a per-bucket play series, top artists/tracks/albums
with mini bars, and a 24h listening clock. Period tabs (all / this year /
last 30 days) switch from/to + bucket. Vanilla inline SVG-free bars, no chart
lib. Reuses .modal-overlay.open + design tokens; sends local tz offset for
day bucketing. Graceful empty/error/unconfigured states. SW cache -> ns-v65.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The finished-download notification resolved the album by name via search3, then fell back to
a same-named album by a DIFFERENT artist (or the first result) when the exact artist match
wasn't found — e.g. right after download, before Navidrome rescans. Many artists share an album
title ("I LIKE IT!"), so the notification navigated to a completely different song.
- Web (notify.js) + Android (AppNavigation): require the artist to match when known (fuzzy for
multi-artist / album-artist drift); drop the name-only and first-result fallbacks. If nothing
matches, show "wird evtl. gerade gescannt" instead of opening a wrong album.
The download itself was correct all along (verified: it grabbed UPSAHL's official "I LIKE IT!"
visualizer) — only the notification's navigation was wrong. SW ns-v63.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix (the reported bug):
- Deezer's /artist/{id}/albums omits the artist object, so discography album cards had an
empty artist name → the download modal fell back to the literal "Unknown Artist", which
poisoned the yt-dlp search ("Unknown Artist <album>") and its artist/channel filter, failing
with "Nicht eindeutig gefunden".
- deezerLookupArtistAlbums now backfills the known artist name (we're on that artist's page).
- openDlModal now defaults album cards to an ALBUM download (was always Track), so a one-click
download no longer grabs an album as a single track named after the album.
Rename (requested cleanup, dependency-checked so nothing breaks):
- All frontend itunes* identifiers → deezer* (functions, the ItunesItem-shape field keys
itunesData/itunesArtistId/itunesFanCount/itunesId, the itunes-album-detail route + DOM ids,
the badge-itunes CSS class). The backend reads none of these fields, so the boundary is safe.
- Verified: build passes; route/DOM-id/CSS producer↔consumer all consistent; only the factual
"Apple's iTunes /search 403" comments keep the historical name. SW ns-v62.
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>
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>
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>
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>
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 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>
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>
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>
- 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>
- downloads.js: use durationMs (Spotify) before trackTimeMillis (iTunes)
- server: read durationMs from top-level body, not only from tracks[0]
- server: also grab durationMs + trackNum from iTunes in the Spotify path
Without durationMs the duration filter never ran, allowing a 58s fan video
to pass all text filters and be downloaded instead of the actual song.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After saving sort tags via the metadata editor, invalidate _artistSortMap
and the apiCached getArtists entry so the updated Navidrome sortName values
are picked up on the next page visit without a hard refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dispAlbumName now takes the full album object and reads album.sortName
directly — no kuromoji fallback, no backend translation.
dispAlbum (song context) also drops the kuromoji fallback, keeping only
the sortAlbum/albumSort file tag.
Override list → Navidrome sort tag → raw Japanese name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hero now only waits for getArtist (fast local Navidrome) before revealing.
getArtistImage (Last.fm lookup) was the 2-4s bottleneck when uncached —
it now resolves async and patches in the artist photo after the hero is
already visible.
Album card, album detail header, and home hero now display the raw album
name from Navidrome instead of converting to romaji when showRomaji is on.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
notify.js statically imported navigate from views/views.js, creating a new
circular chain: navigation.js → ui/ui.js → settings.js → downloads.js →
notify.js → views/views.js → navigation.js. This chain did not exist in the
old monolithic ui.js and caused module initialisation failures in some browsers.
Fix: make navigate a lazy dynamic import inside _notifNavigate() body, which
is the only call site. Also replace unnecessary dynamic import('../utils.js')
calls with the already-statically-imported toast function.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings dropdown: 15s (default), 30s, 1/3 of duration, 2/3 of duration, song end.
Duration-based modes use s.duration from Navidrome metadata (always available).
'end' mode skips the timer and fires the scrobble in onEnded (web) / onQueueEnded (native).
Skipping a song never scrobbles in any mode — _cancelScrobble() still runs on skip/pause.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move player.js + nowplaying.js → src/player/
- Move views.js + spotify/soundcloud/youtube/abs → src/views/
- Move ui.js + components.js → src/ui/
- Update all import paths (static + lazy) across every file
- MusicService: wrap ExoPlayer in ForwardingPlayer that always advertises
next/prev commands so the notification skip buttons stay visible in radio
mode even when the JS queue has only one native track
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>