Commit Graph
779 Commits
Author SHA1 Message Date
HoneyandClaude Opus 4.8 6f455b565f fix(android): smoother Erkunden swipe + top charts no longer vertically centered
- Pre-compose the neighbour page (beyondViewportPageCount = 1) so the first
  swipe doesn't hitch while the heavy LazyColumn builds.
- Drive the TabRow indicator off pagerState.currentPage so it moves with the
  swipe (at the half-way point) instead of only on settle — the lag read as
  "not smooth".
- Wrap each pager page in Box(fillMaxSize) so short content (the Top-Alben
  charts row) sits at the top again, not vertically centered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 22:01:34 +02:00
HoneyandClaude Opus 4.8 7f5d25a03b feat(android): swipe between Erkunden sub-tabs (Bibliothek ↔ Global)
Wrap the Erkunden content in a HorizontalPager so a left/right swipe switches
between the Bibliothek and Global search tabs, two-way synced with the TabRow: a
chip tap animates the pager; a settled swipe updates the selected tab (and stops
any preview, like a tap). Removes the idea from the backlog now that it's built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 21:52:11 +02:00
HoneyandClaude Opus 4.8 87ee8cd097 fix(android player): seek broke after seamless track transitions (durationMs stuck at 0)
After skipping through already-buffered tracks the player stays READY, so
onPlaybackStateChanged never fires and syncState() isn't re-called; ctrl.duration
was TIME_UNSET(→0) at the transition, leaving state.durationMs stuck at 0. The
seek bar then computed fraction*0 = 0 and snapped playback to the start every time
(or to a stale value → "only 10s"). Restarting the app re-read the real duration,
which is why it recovered.

Fix: durationMs now self-heals — the 500ms ticker refreshes it too, and
resolveDurationMs() falls back to the current song's known duration while
ctrl.duration is still unset. Also record the Erkunden swipe-between-tabs idea.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 21:47:58 +02:00
HoneyandClaude Opus 4.8 803331e807 feat(android): pinned similar artists tap through to library; note Following view idea
Pinned similar artists (Deezer related) were non-clickable on Android because
Deezer artists carry no Navidrome id. On tap, resolve the name against the
library (exact case-insensitive match) and navigate to that Navidrome artist if
present, else a short "not in your library" toast.

Also: extend the CHANGELOG "Artist following" backlog idea with a dedicated
"Following" view (grid of followed artists) + follow/unfollow toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 01:12:47 +02:00
HoneyandClaude Opus 4.8 26de85a140 fix(artist pin): apply pins in search + fix similar artists (web + android)
Two gaps the pin left, on both platforms:

1. Search still showed the wrong same-name artist's photo. The library search
   now prefers the pin's Deezer picture for a pinned artist (web: search.js
   artist cards; android: LibrarySearchContent via SearchViewModel overrides).

2. Similar Artists were "weird" — they come from Last.fm keyed to the (merged/
   wrong) Navidrome artist. When pinned, use Deezer "related" of the pinned
   artist instead (new deezerRelatedArtists / ItunesClient.lookupRelatedArtists).

Also: web Similar Artists now render with profile pictures (avatar chips), like
the Android app, instead of plain text chips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 18:42:15 +02:00
HoneyandClaude Opus 4.8 1ee85482f3 feat(android): artist pin (same-name disambiguation) — parity with web
Android now reads AND writes the shared server-side artist overrides
(/api/artist-overrides), so pins set on web apply here and can be set/removed
in-app — closing the web-only gap.

- ArtistOverride model + ArtistOverrideClient (GET/POST/DELETE on the backend);
  MusicRepository caches the map and exposes get/set/remove.
- ArtistViewModel resolves the pin up front; when pinned it enriches the artist
  page straight from the pinned Deezer id (albums/top-tracks/fans/image) instead
  of the name-guess. Picker: Deezer search candidates + a "paste Deezer link/id"
  route (reliable when /search/artist can't surface a low-profile same-name
  artist), plus clear-pin.
- ArtistDetailScreen: "⚙ Falscher Künstler? / Gepinnt" pill top-right of the hero,
  pinned Deezer picture overrides the hero image, ModalBottomSheet picker.
- ItunesClient.lookupArtist(id) (Deezer-backed) for the link-paste flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 18:28:29 +02:00
HoneyandClaude Opus 4.8 a5e5d414b4 docs: record ideas in the bottom Planned/Backlog, mention Musik-Match in README
Move the share-links / Wrapped / household-leaderboard / ListenBrainz ideas
into the existing "Planned / Backlog" section at the bottom of the CHANGELOG
(not a new top section), and add a short Musik-Match clause to the README
listening-stats feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 01:18:12 +02:00
HoneyandClaude Opus 4.8 e34a934460 docs(changelog): add Ideas / Backlog section (share-links, Wrapped, leaderboard, ListenBrainz)
The CHANGELOG is the home for feature ideas going forward. Records the
share-by-link deep-link idea (PWA on PC / Android app on phone via App Links +
assetlinks.json), plus the already-tracked Wrapped, household leaderboard, and
optional ListenBrainz relay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 01:16:09 +02:00
HoneyandClaude Opus 4.8 19f82ad076 fix(android settings): move gap below Musik-Match, not above it
The Column already spaces children 12dp; the extra Spacer(8dp) between
"Meine Statistiken" and "Musik-Match" stacked to ~20dp, making that gap look
too big while Musik-Match↔Speichern stayed tight. Group the two stat buttons
together (12dp) and add the separating space (4dp) before the Save button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 01:08:19 +02:00
HoneyandClaude Opus 4.8 c933847201 feat(android): Head-to-Head "Musik-Match" screen (parity with web)
Android CompareScreen mirroring the web feature: partner picker fed by
GET /scrobble/users, then GET /scrobble/compare?with= → a match-score ring
(Canvas arc), per-user totals, mirrored shared-artist bars, shared tracks,
and each side's unique top artists. Rows navigate to artist/album. Reuses the
one PlayLogClient HttpClient (fetchUsers/fetchCompare). Opened from Settings
"🎧 Musik-Match". Parity matrix updated (Android ✅).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 00:52:33 +02:00
HoneyandClaude Opus 4.8 cbedbba6fc feat(scrobble): Head-to-Head "Musik-Match" — compare two users' listening
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>
2026-08-13 00:46:29 +02:00
HoneyandClaude Opus 4.8 ba2306656e feat(artist pin): pin by Deezer link/ID when search can't surface the artist
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>
2026-08-13 00:38:33 +02:00
HoneyandClaude Opus 4.8 5f251d555f fix(artist pin): esc() crashed on numeric Deezer id; move pin button to banner top-right
- esc() now coerces to string first (String(s ?? '')), so passing a number (e.g. the
  Deezer artist id in the pin picker) no longer throws 'replace is not a function'.
- The '⚙ Falscher Künstler?' button moved from under the fan count to the top-right
  of the artist banner (absolute, on the hero). SW cache -> ns-v75.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 00:16:14 +02:00
HoneyandClaude Opus 4.8 02960cf886 feat(artist): pin a local artist to a specific Deezer artist (same-name fix, shared)
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>
2026-08-12 21:38:42 +02:00
HoneyandClaude Opus 4.8 b788fedec3 fix(download): dedup missed re-downloads when the title contains ' - ' (KH triple/quadruple)
findExistingTrack split the filename at the first ' - ' assuming 'Artist - Title',
so a title with internal dashes ('Kingdom Hearts 2 - Dearly Beloved - Main Theme')
or a garbage/absent artist prefix ('01-01 - …') was mis-parsed → the duplicate went
undetected and got re-downloaded (3-4x). Now match the query title against BOTH the
full stem AND the part after the first ' - ', so titles with dashes and prefix-only
filenames are recognized. Normal 'Artist - Title' files still match; no false
positives; remix-vs-base distinction preserved. Unit-tested on the exact KH case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 17:06:14 +02:00
HoneyandClaude Opus 4.8 75dceba97b fix(editor): 'Deezer' autofill button is Deezer-only again (MusicBrainz has its own button)
Stufe B routed /api/meta/lookup through the combined resolver, so the editor's
'Deezer' button could silently return MusicBrainz data while the status said
'von Deezer'. The editor already has a dedicated 'MusicBrainz' button, so keep the
two explicit and honest: Deezer button = Deezer only. The combined
Deezer→MusicBrainz→clean resolver stays on the DOWNLOAD paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 16:53:25 +02:00
HoneyandClaude Opus 4.8 e61d57425e docs(CLAUDE): document the single-track metadata resolver (Deezer→MusicBrainz→clean fallback)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 01:51:54 +02:00
HoneyandClaude Opus 4.8 6d084a7b7a fix(download): stop wrong album/cover from same-titled tracks; add MusicBrainz fallback
Root cause: deezer pickTrackMatch fell back to a blind title-only match, grabbing a
same-titled song by a completely different artist (e.g. 'Memories' by Rab Noakes for
'☆NoAki☆') and applying its album/cover/year. The download's clean fallback existed
but was never reached because the lookup almost always returned *something*.

Stufe A — deezer.js: the title-only fallback now REQUIRES an artist correspondence
(equal, or one name contained in the other after normalization). No match → {} →
the existing clean fallback finally runs (album_artist = song artist, album =
'Singles', cover = source thumbnail, year/genre untouched). Test that encoded the old
blind behaviour replaced; NoAki + loose-match regression tests added.

Stufe B — musicbrainz.js: a rate-limited (~1/s, User-Agent) MusicBrainz recording
lookup with the SAME artist discipline + Cover Art Archive cover. trackmeta.js is the
single resolver (Deezer → MusicBrainz → clean fallback); the three single-track call
sites use it (album batch stays Deezer-only to avoid MB rate-limit slowdowns).

Unit-tested (17/17) + live-verified: NoAki→clean fallback, real track→Deezer, MB finds
real albums when Deezer misses. Backend-only; no frontend/Android change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 01:51:29 +02:00
HoneyandClaude Opus 4.8 16f76a4236 fix(web): show non-Japanese names as-is with Romaji on (e.g. ☆NoAki☆ kept its stars)
With 'show Romaji' enabled, dispArtist/dispArtistById/dispArtistParts/dispTitle
substituted the sort tag for the display name unconditionally. For non-Japanese
names the sort tag is just a sort key (leading symbols/articles stripped, so
'☆NoAki☆' → 'NoAki'), which wrongly replaced the real name. Guard the substitution
on the original being Japanese — mirroring what dispAlbumName already did — so
Romaji only applies to Japanese names and everything else shows unchanged.
SW cache -> ns-v73.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 00:48:22 +02:00
HoneyandClaude Opus 4.8 d195518dfb fix(android settings): remove open delay — seed theme instead of a loading gate
The previous flicker fix gated the whole settings form behind a 'loaded' spinner,
which read as a small lag when opening Settings (it waited for DataStore's first
emit even though it's already warm). Instead, expose the applied theme id
(NsColors.themeId, set in applyNsTheme) and seed the ViewModel's initial state with
it, so the theme picker no longer jumps from the default — no gate, no spinner, no
delay. Remaining text fields fill within a frame from the warm DataStore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 00:40:38 +02:00
HoneyandClaude Opus 4.8 711a084686 fix(android): scrobble respects 1/3 & 2/3 again; no settings flicker on open
Scrobble timing: at onMediaItemTransition ctrl.duration is still TIME_UNSET (→0),
so getScrobbleDelayMs's third/two_thirds modes hit the 15s floor and scrobbled way
too early (both Navidrome + our play-log). Use the song's KNOWN duration
(song.duration), falling back to the player only if absent. Web was unaffected
(it already used the song's duration).

Settings flicker: the ViewModel started with default state (theme = Dark Ocean)
and rendered a frame before DataStore emitted the real values, so the theme picker
visibly jumped. Gate the form on a 'loaded' flag set after the first emit; show a
spinner until then.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 00:24:55 +02:00
HoneyandClaude Opus 4.8 0c7132a7f2 feat(scrobble stats): genre ranking (Top-Genres) — web + android
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>
2026-08-11 01:35:00 +02:00
HoneyandClaude Opus 4.8 249b693936 feat(android auto): tapping a song plays its whole list from that song
Browse songs now carry their context in the mediaId (songctx:<contextId>|<songId>).
A new onSetMediaItems override expands a single tapped song into its full list
(Favoriten / album / playlist) and returns it with the tapped index as the start
position, so playback continues through the rest instead of stopping after one song.
onAddMediaItems shares the same resolver; onGetItem resolves context songs too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 01:25:38 +02:00
HoneyandClaude Opus 4.8 d35fb01306 fix(android auto): tapping a song (e.g. in Favoriten) did nothing — re-attach stream URI
Android Auto strips a MediaItem's URI (localConfiguration) when a browse-tree item
crosses the process boundary, so onAddMediaItems received the tapped song without a
playable URI and the else branch passed it through unchanged → no playback. Rebuild
the item with the stream URI resolved from its mediaId.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-10 20:22:09 +02:00
HoneyandClaude Opus 4.8 1d98b273e7 fix(web player + stats modal): no auto-resume after user pause; stable stats modal
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>
2026-08-10 18:06:18 +02:00
HoneyandClaude Opus 4.8 3129333f9a feat(scrobble stats): navigator lower bound, clickable rows, android button to top
- 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>
2026-08-08 21:50:27 +02:00
HoneyandClaude Opus 4.8 86eb893f40 feat(scrobble): time-travel navigator (year/month) + fix stats cover art
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>
2026-08-08 13:11:34 +02:00
HoneyandClaude Opus 4.8 14c0dea7e4 fix(scrobble): 'Alles' (all-time) stats returned nothing — from=0 mapped to now
The client sends from=0 for the all-time range, but the string "0" is truthy, so it
was routed through _toEpochSec which maps 0 -> now, producing an empty window. Parse
'from' directly (0/absent/invalid -> all-time) instead. Affects web + Android.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-08 12:55:29 +02:00
HoneyandClaude Opus 4.8 561014c83f docs: v2.0 — document the scrobble / listening-stats system
- CHANGELOG: new [2.0] entry (own timestamped play-log service, stats view on
  web + android with cover art, remix-dedup fix, user-select UI fix, node 20->22);
  Platform Parity Matrix gains play-log / stats / stats-cover rows (+ Wrapped 🔜).
- README: Listening stats feature, scrobble/ in the project tree, SCROBBLE_URL
  env + scrobble container table, port 3710 + SSO-exclusion note in setup.
- CLAUDE.md: new 'Listening stats / scrobble system' section (isolation rationale,
  node:sqlite gotchas, ping.view multi-user auth, capture point, cover resolution,
  SSO deploy gotcha).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 19:33:46 +02:00
HoneyandClaude Opus 4.8 906b3b48cb ui(scrobble): label the totals tile 'Plays' instead of 'Wiedergaben' (web + android)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 19:29:55 +02:00
HoneyandClaude Opus 4.8 fff6f9e4da feat(scrobble): Android stats screen + cover art in stats (web + android)
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>
2026-08-07 18:41:27 +02:00
HoneyandClaude Opus 4.8 cc1ec1653f fix(ui): stop accidental text-selection of UI chrome when dragging near controls
Dragging the mouse near a field edge (Global Suche, the importer, etc.) started
a text selection of the surrounding page titles/labels instead of feeling like an
app. Disable user-select on the #app shell; re-enable it for real text fields
(input/textarea/contenteditable) and copyable content (lyrics). Setup screen is
outside #app and unaffected. SW cache -> ns-v66.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 17:40:18 +02:00
HoneyandClaude Opus 4.8 26880016e9 feat(scrobble): web statistics view (Phase 3 UI)
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>
2026-08-07 17:17:14 +02:00
HoneyandClaude Opus 4.8 e782b75bd9 feat(scrobble): /scrobble/stats aggregation endpoint (Phase 3 backend)
Per-user listening stats via on-the-fly GROUP BY strftime (no rollup tables):
totals (plays + seconds), top artists/albums/tracks, a day/month/year time
series, and a 24h listening clock. Local-day bucketing via a tz offset.
Params exactly matched per query (node:sqlite throws on extra named params);
from defaults to all-time (avoiding the _toEpochSec(0)->now trap). Unit-verified.

Next: the web statistics view. Wrapped endpoint is Phase 4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 17:09:42 +02:00
HoneyandClaude Opus 4.8 eb55184118 fix(download): remix/live/acoustic/instrumental no longer skipped as duplicate of base track
findExistingTrack normalized titles by stripping ALL bracketed content, so
"Therapy (Remix)" collapsed to "Therapy" and was skipped because the base
track existed. Keep brackets that contain a version marker (remix/live/acoustic/
instrumental/vip/flip/edit/mix/…) so distinct recordings are recognized as new;
still strip noise brackets (feat. X, Official Video). Verified via Deezer that the
remix track carries the marker in its title. Unit-tested both directions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 17:06:00 +02:00
HoneyandClaude Opus 4.8 71190488ca feat(scrobble): own timestamped play-log service (stats/Wrapped foundation)
Phase 1+2 of a self-hosted, multi-user listening archive — the basis for
day/month/year stats and a yearly "Wrapped". Runs IN ADDITION to the existing
Navidrome/Last.fm scrobble; never replaces it.

Service (isolated, own container):
- scrobble/scrobble-server.js — node:sqlite play-log (own volume), routes
  POST /scrobble/plays (idempotent by uuid), GET /scrobble/history (archive),
  DELETE /scrobble/plays?user= (test cleanup), GET /scrobble/health.
  Multi-user auth via Navidrome ping.view (cached) -> user not spoofable.
- docker-compose: novasonic-scrobble service (node:22, port 3710) + volume;
  backend bumped node:20->22 (EOL); SCROBBLE_URL plumbing (entrypoint, .env.example).

Capture (both platforms, offline-safe queue, idempotent batch flush):
- Fires at the existing scrobbleMode counted-play moment — consistent with
  Last.fm, respects the user's setting (no separate 50%/4min rule).
- Web: src/player/scrobbleLog.js (localStorage queue) + subsonicAuthParams()
  in api.js + hooks in player.js + flush init in main.js; S.cfg.scrobbleUrl.
- Android: data/PlayLogClient.kt (filesDir queue, reused Ktor client) +
  repo.logPlay/flushPlayLog + PlayerController hook + NovaSonicApp flush +
  scrobbleUrl in PrefsStore/SettingsViewModel/SettingsScreen (with test button).

Plan + design decisions documented in docs/scrobble-plan.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 22:54:25 +02:00
HoneyandClaude Opus 4.8 e69fbaf75e fix(android): back focus lost after tabbing out — re-assert overlay back callbacks on resume
With NowPlaying (or an ABS player) open, backgrounding and resuming the app made the next back
press pop the NavHost invisibly to Home before dismissing the overlay. Returning from the
background re-registers the NavController's own OnBackPressedCallback on top of the dispatcher,
stealing back-priority from the overlays.

AppNavigation now bumps a resumeTick on Lifecycle.Event.ON_RESUME and keys every overlay
DisposableEffect (NowPlaying, ABS player, ABS item, Erkunden overlay) on resumeTick +
currentBackEntry, so each overlay re-adds its callback above the NavController after a resume or
navigation. Needs on-device verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 10:00:04 +02:00
HoneyandClaude Opus 4.8 17bb527e1d fix: download notification opened the wrong song (same-name album)
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>
2026-08-01 02:30:53 +02:00
HoneyandClaude Opus 4.8 1e206e8df3 fix(web): 'Unknown Artist' download from artist page + rename itunes*→deezer* identifiers
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>
2026-08-01 00:31:09 +02:00
HoneyandClaude Opus 4.8 bc87a04da5 docs(changelog): note the 'Unknown Artist' download failure from artist-page album cards
Empty artist name (Deezer /artist/albums omits the artist object) + openDlModal defaulting to
type=track for album cards + downloads.js 'Unknown Artist' literal → yt-dlp searches
'Unknown Artist <album>' and fails. Documented as a Known Bug; no code change yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 00:19:23 +02:00
HoneyandClaude Opus 4.8 96861bea06 docs(web): unify the artist / album-artist separator hints in the editor
Both fields already accept ; and , (the backend normalizer splits on •·;,). The hints now
read uniformly — "mehrere mit ; oder , trennen" — and explain the actual difference: artist
is split into individual artists, album-artist stays one combined credit. SW ns-v61.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 00:10:04 +02:00
HoneyandClaude Opus 4.8 e4f030c98d fix: artist page resolved "Flay!" to the far more popular "K.FLAY"
The Deezer artist match required only that every query word appear in the candidate name, so
"Flay!" (query word: flay) matched "K.FLAY" (words: k, flay). Sorted by fan count, K.FLAY
(54,822 fans) buried the real "Flay!" (39 fans) — the page showed K.FLAY's fans + top tracks.

Now prefer an EXACT normalized-name match ("flay" == "flay", not "kflay"), falling back to the
loose match only when there's no exact one. Web (detail.js) + Android (ArtistViewModel).
Verified against Deezer: exact match selects the real Flay! (id 12242284, whose top tracks
are actually Flay!). SW ns-v60.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 23:53:42 +02:00
HoneyandClaude Opus 4.8 b3cf01dd3b fix: duplicate rows in artist "Top Songs" when a track exists twice in the library
Navidrome can hold the same track twice (downloaded as a single AND as an album track), so
getTopSongs returned two identical rows ("PIXELATED KISSES" x2). Collapse the top-songs list
by title+album for display (keeps the highest play count) on web + Android. SW ns-v59.

Note: this is a display fix — the underlying duplicate files remain a library-cleanup matter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 17:48:02 +02:00
HoneyandClaude Opus 4.8 fbee695aad fix: album shown 2-3x in "In Library" on the artist page
Deezer returns the same album as several variants (single/EP/album/regional/explicit), and
each variant matched the same Navidrome album, so one owned album rendered 2-3 times
("PIXELATED KISSES" x3).

- Web (detail.js): collapse matched entries to one per owned Navidrome album and one per
  not-owned title before classifying (also avoids duplicate track-count fetches).
- Android (categorizeAlbums): distinctBy(collectionId) missed variants with different ids;
  now also skip an owned album already classified.
SW ns-v58.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 17:41:33 +02:00
HoneyandClaude Opus 4.8 cc4d3b72d2 chore: bump SW to ns-v57 after reverting the artist-image fallback
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 17:21:31 +02:00
Honey e059c358b3 Revert "feat: Deezer artist photo fallback for local artists with no Navidrome image"
This reverts commit 569af968c5.
2026-07-31 17:20:45 +02:00
HoneyandClaude Opus 4.8 569af968c5 feat: Deezer artist photo fallback for local artists with no Navidrome image
Niche local artists (e.g. N!GHT) whose Navidrome metadata agents have no image showed a
blank hero. Now, when Navidrome has no artist image, the hero falls back to the matched
Deezer artist's photo (Navidrome image → Deezer photo → album cover), on web + Android.

- Web (detail.js): the Deezer-photo fallback previously ran only for non-local artists;
  now it runs whenever the Navidrome image is absent, local or not.
- Android: ArtistViewModel captures the matched Deezer artist's photo (deezerImage);
  ArtistDetailScreen uses it as the hero fallback.
SW ns-v56.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 17:07:53 +02:00
HoneyandClaude Opus 4.8 640e475c51 fix: owned albums wrongly shown as "Unvollständig 1/0" on artist page
Deezer's /artist/{id}/albums list omits nb_tracks, so the album total read as 0 and every
owned album (e.g. N!GHT's "Moonlight"/"Yume") was classified as incomplete "1 / 0 Tracks".

- Web (detail.js): resolve the real Deezer track count via the album endpoint for MATCHED
  (owned) albums only — a couple of calls, not the whole discography — and treat an unknown
  total as complete. Genuine partials still work when the true count is known.
- Android (categorizeAlbums): mark partial only when a known total exceeds what you own;
  unknown total counts as complete (never a false "1 / 0").
- CHANGELOG: documented the fix; corrected the Android lyrics known bug (auto-scroll follows
  playback but isn't smooth — it jumps rather than glides). SW ns-v55.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 17:05:14 +02:00
HoneyandClaude Opus 4.8 5528947a93 feat: fans on local Navidrome artists (android) + harmonize web fan layout
- Android local artist page (ArtistDetailScreen/ArtistViewModel) now resolves the Deezer
  artist (fan-sorted, so same-name collisions pick the popular one) and shows "<n> Fans"
  under the name — parity with the web local artist page.
- Also migrated ArtistViewModel's preview to the app-level PreviewController: the local
  artist page had its OWN uncontrollable MediaPlayer preview, which had the same
  "keeps playing after navigating away" bug. Now every preview uses the global bar.
- Web: the artist-hero fan line was laid out in a row next to the name (flex without
  column direction) and used an odd tinted color — now stacked cleanly under the name in
  a muted white. SW ns-v54.
- CHANGELOG: documented the above + added a Known Bug (Android synced-lyrics auto-scroll
  still doesn't follow playback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 16:31:55 +02:00
HoneyandClaude Opus 4.8 9167bfaf0e feat(android): global 30s-preview control bar + artist fan count
Preview bar:
- 30s previews used to keep playing with no control once you left the album/artist/Explore
  screen they started on. Moved the preview player from ExploreViewModel to an app-level
  PreviewController, and added a global PreviewBar (above the mini player, on every screen)
  to pause/resume/stop it. Starting real library playback cancels the preview.

Artist fan count (parity with web):
- ItunesItem carries fanCount (Deezer nb_fan); shown on search artist cards and the artist
  page ("559.542 Fans" / "1,2 Mio. Fans"). Combined with the existing nb_fan sort this makes
  same-name collisions (5x "Joji") resolve to the right artist on Android too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 16:24:46 +02:00