Commit Graph
760 Commits
Author SHA1 Message Date
HoneyandClaude Opus 4.8 93ee958434 feat: artist-page scroll/search memory (android) + move APK button to bottom (web)
- Android: the artist page's main list and the "Alle anzeigen" album overlay
  (grid scroll + search query) are hoisted into the Artist/Explore ViewModels,
  so drilling into an album/artist and back keeps position + query. Covers the
  library artist and Deezer (Explore) artist screens.
- Web: the "Android App" download section now sits at the very bottom of
  Settings, below the Navidrome-Server (account) section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:42:24 +02:00
HoneyandClaude Opus 4.8 a780f6622b chore(android): versionCode 2 (first apk-latest release upload)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:27:06 +02:00
HoneyandClaude Opus 4.8 6da40d9e63 docs(scripts): add .env.release.example template for publish-apk
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
apk-latest
2026-09-02 00:52:49 +02:00
HoneyandClaude Opus 4.8 2fd4d978d0 feat(android): in-app updater (backend/Gitea APK), auto-bump versionCode
The app checks {backend}/api/apk/info on launch and, if a newer versionCode
than BuildConfig.VERSION_CODE is offered, shows an update dialog that
downloads {backend}/api/apk into a single wiped-each-time cache file and
hands it to the system installer (FileProvider + REQUEST_INSTALL_PACKAGES).
No APK hoarding; in-place install preserves data (stable signing key needed).

- backend /api/apk/info now returns versionCode (parsed from the release body).
- publish-apk.mjs auto-increments versionCode, sets versionName, and writes
  versionCode/versionName into the Gitea release body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 00:46:11 +02:00
HoneyandClaude Opus 4.8 845f125d76 feat: remember scroll + search position on Back (web + android)
Going into an album/artist and back now restores the scroll position and
keeps the typed query, instead of resetting to the top.

- Web: navigation.js snapshots each history entry's #main-content scroll and
  any open local (header) search, restoring them on Back/Forward; a genuine
  new navigation still starts at the top.
- Android: the Alben/Künstler lists (LibraryViewModel) and the global/local
  search-results lists (ExploreViewModel/SearchViewModel) hoist their
  LazyListState/LazyGridState, so scroll survives a drill-in/back for the
  tab's lifetime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 00:06:46 +02:00
HoneyandClaude Opus 4.8 3ceba8a365 feat(android): partial-album detail gets Play/Shuffle + track 3-dot menu
You can still enjoy an incomplete album: the downloaded-tracks section now has
Abspielen + Zufall buttons (play/shuffle the downloaded songs) and each
downloaded row gets the 3-dot context menu, threaded from both artist screens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 23:58:24 +02:00
HoneyandClaude Opus 4.8 3000dc1622 feat(scripts): one-command APK build + upload to the fixed Gitea release
`npm run publish-apk` builds the APK (assembleDebug by default — the release
build type is unsigned) and uploads it to the apk-latest Gitea release,
replacing the old .apk asset, so the web download button always serves the
latest build. Config via env or a gitignored scripts/.env.release
(GITEA_URL/REPO/TOKEN, APK_GRADLE_TASK). Release title tracks versionName.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 18:23:27 +02:00
HoneyandClaude Opus 4.8 33d24c6ed4 feat(backend): APK download proxies a fixed Gitea release (local fallback)
The /api/apk + /api/apk/info routes now fetch a fixed Gitea release
("apk-latest") server-side over the LAN and stream its .apk asset, falling
back to /data/novasonic.apk. Gitea can stay internal-only — the backend holds
the token, the browser only ever talks to the backend. Follows redirects,
constant ~30 MB (overwrite the one release's asset each build).

env: GITEA_URL, GITEA_REPO, GITEA_TOKEN (private repo only), GITEA_APK_TAG.
docker-compose documents them; unset = local-file fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 18:16:13 +02:00
HoneyandClaude Opus 4.8 908b50bcd9 feat(web): download the Android APK from Settings
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>
2026-08-25 18:00:36 +02:00
HoneyandClaude Opus 4.8 7996971ba0 fix(web): search results panel spans full content width
The header search overlay was anchored to the search bar's width, leaving a
dead strip on the right where scrolling hit the background page. Anchor it to
the sticky #mobile-header instead (which already sits right of the sidebar), so
it spans the full main-content width — sidebar stays visible, no dead strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-23 12:46:50 +02:00
HoneyandClaude Opus 4.8 026b392657 fix: Japanese/CJK albums vanished from artist discography (web + android)
The album-name normaliser stripped everything but a-z0-9, so any JP/CJK
title collapsed to "". All of an artist's Japanese singles then normalised
identically, de-duplicated into one entry and "matched" each other — e.g.
inabakumori (稲葉曇) showed only 2 of 9 releases. Normalisers now keep
Unicode letters/numbers (\p{L}\p{N}) in:
- web _normAlbum (detail.js)
- android normAlbum (categorizeAlbums), normTrack (partial-album split),
  the enrichPartialCounts helpers, and the Deezer artist-name match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 18:49:59 +02:00
HoneyandClaude Opus 4.8 ed641e999c fix(web): artist hero name no longer clips last letter; fold 2.3 into 2.2
- .artist-hero-name: add padding-right so the heavy/negative-letter-spacing
  final glyph isn't cut by overflow:hidden.
- CHANGELOG: merge the 2.3 batch into a single 2.2 (2026-08-22) section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 00:48:33 +02:00
HoneyandClaude Opus 4.8 67642f8ab8 fix(web): header search dropdown no longer covers the sidebar
Anchor #header-search-overlay to the search bar (position:absolute inside a
relative .header-search-wrap) instead of position:fixed full-viewport width,
so the results drop down at the bar's width and the left sidebar stays visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 00:41:29 +02:00
HoneyandClaude Opus 4.8 b7c96451d8 feat(android): download notifications everywhere + swipe tabs (NowPlaying, Library)
- Downloads started from global search / Bibliothek now poll via the
  app-lifetime NotificationStore, so they show success/failure like the
  importer and artist page did (was a silent toast only).
- Swipe between tabs, synced with the tab bar, matching Erkunden:
  * Now Playing: Player <-> Lyrics <-> Queue (cover's skip-track swipe
    still wins as a child gesture).
  * Bibliothek: Playlists <-> Alben <-> Künstler <-> Offline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 23:19:20 +02:00
HoneyandClaude Opus 4.8 d11de8a69e feat(android): importer 30s preview + partial-album detection
- Importer: each resolved track gets a play/stop button that plays its
  Deezer/Spotify preview via the app-level preview bar (pauses library
  playback, highlights the row). Parity with the web importer.
- Artist "Unvollständig" now works: Deezer's /artist/{id}/albums omits
  nb_tracks, so owned albums never registered as partial. The artist VM
  now resolves the real total for matched albums via a per-album lookup
  (like web), so the partial section + downloaded/missing split populate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 23:10:28 +02:00
HoneyandClaude Opus 4.8 b584159568 fix: reliable concerts (backend events cache) + web importer preview layout
- Backend now caches Ticketmaster events per artist and prefers the larger
  recent result, so a flaky partial/empty response can't shrink a good list;
  every client (web/Android) stays consistent (1h TTL self-heals).
- Web SoundCloud importer preview: add the missing .gtrack-num grid cell so
  the 4-column row no longer squeezes the cover/title (title was wrapping one
  char per line).
- CHANGELOG [2.3] opened for this batch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 23:02:56 +02:00
HoneyandClaude Opus 4.8 f56d47800c fix(android): even concert rows — fixed calendar-style date block
The "16. Sep. 2026" date wrapped inside the 52dp column, so rows with/without
a time ended up different heights and the divider/venue misaligned. The date
block is now a fixed 56dp calendar block: big day number, small month/year and
time, each single-line — every row is the same height and lines up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 20:00:49 +02:00
HoneyandClaude Opus 4.8 2208ce77b6 fix(events): show the venue-local concert time consistently
Backend sent the UTC `dateTime` (a 20:00 local show read as 08:00Z), and
Android's ZonedDateTime.parse choked on zone-less values → some rows had a
wrong time, others none at all.

- Backend: prefer localDate+localTime (venue-local, naive) over the UTC instant.
- Android: parse a naive LocalDateTime first, fall back to ZonedDateTime for
  older "…Z" values. Web already renders the naive string as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 19:59:26 +02:00
HoneyandClaude Opus 4.8 f27eaf9c82 fix(android): surface concert-fetch errors instead of swallowing them
Events were fetched with runCatching{}.getOrDefault(emptyList()), so any
network/deserialization failure looked identical to "no concerts". The
backend + Ticketmaster work (verified), so the failure is client-side.

- EventsClient: expectSuccess + timeouts so a non-2xx (e.g. an SSO/HTML
  redirect) becomes a real exception instead of an obscure body() failure.
- ArtistViewModel: capture the failure into eventsError (+ Logcat) rather
  than defaulting to empty.
- ConcertsSheet: show "Konzerte konnten nicht geladen werden" + the reason
  when the fetch failed, vs. "Keine Konzerte angekündigt" when genuinely none.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 19:46:16 +02:00
HoneyandClaude Opus 4.8 7b4599c50a feat(artist): minimal icon-only pin button + show all Deezer releases (Android)
- Pin ("Falscher Künstler?") is now a small icon-only ⚙ chip on both web
  and Android — accent-blue when pinned, subtle white otherwise; no more
  text banner over the hero.
- Android artist discography no longer filters to record_type=="album";
  it now shows albums, EPs AND singles (ordered albums→EPs→singles),
  matching Deezer's own artist page. Web already showed all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 19:19:17 +02:00
HoneyandClaude Opus 4.8 934253a036 feat(web): merge Song Importer into Global Suche (one unified field)
The importer no longer has its own sidebar/bottom-nav entry. The Global
Suche field now handles both jobs: plain text runs the Deezer search;
a pasted Spotify/SoundCloud/YouTube link switches into importer mode
(badge + "Importieren" button + preview), matching the Android app and
tidying the left nav.

- New onGlobalInput/tryImporterResolve; importer DOM moved into
  page-discover; old page-spotify-importer removed; routing cleaned up.
- Existing resolvers unchanged (still read the mirrored importer-link-input).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 17:59:44 +02:00
HoneyandClaude Opus 4.8 0a6c81b4a1 docs: tidy CHANGELOG (drop duplicated [0.9] block) + refresh README
- CHANGELOG: remove the accidentally duplicated Changed/Fixed/Investigated
  block that sat orphaned before [0.8]; [0.9] now appears once.
- README: artist-pages bullet now mentions the same-name artist pin
  (Deezer link/id) and that a pinned artist's similar list comes from Deezer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 00:54:19 +02:00
HoneyandClaude Opus 4.8 593c9fdb7b fix(backend): YouTube downloads — install Deno + pull yt-dlp nightly
Downloads were 403ing on two fronts: (1) recent yt-dlp needs a JS runtime to
solve YouTube's signature/nsig challenge (Deno, added in cb7a3d7), and (2) the
current yt-dlp *stable* had a bug that 403'd the android_vr client on every video
(yt-dlp#17456, fixed in nightly). Switch the backend from the stable yt-dlp
binary to nightly via pip (`--pre 'yt-dlp[default]'`). Verified end-to-end incl.
a real frontend download. No cookies / PO-token service / proxy needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 00:40:31 +02:00
HoneyandClaude Opus 4.8 cb7a3d77be fix(backend): install Deno so yt-dlp can solve YouTube's signature/nsig challenge
Recent yt-dlp (which the backend re-pulls latest on every restart) needs an
external JavaScript runtime to solve YouTube's signature/nsig challenge — without
it, "Signature solving failed" → formats get skipped/HTTP 403. Install Deno in the
backend container (+ unzip for its installer) and put it on PATH for the node
process so spawned yt-dlp finds it. No extra container, no account, no proxy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 23:58:22 +02:00
HoneyandClaude Opus 4.8 635ba8d353 revert: drop the YouTube cookies/PO-token/proxy workarounds (back to pre-saga state)
None of it got downloads working on the flagged datacenter IP (web_music media
403, then 429 rate-limiting), so revert the whole lot to keep things clean:
- config.js: remove potProviderUrl + ytdlpProxy
- ytdlp.js: remove potArgs/proxyArgs; player_client back to web_music,default
- novasonic-server.js: remove pot/proxy args from importer resolve
- docker-compose.yml: remove the bgutil-provider service + backend env/plugin/depends_on
- CHANGELOG: drop the [2.2] section

Kept the one genuine fix: a failed direct-link download now reports failure
(moved its note into [2.1]).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 20:24:13 +02:00
HoneyandClaude Opus 4.8 31d4a72e62 fix(backend): download via default client (web_music media 403s); add optional proxy
The PO token provider works (web_music gets a token) but web_music's media URLs
still 403 from a datacenter IP. Prefer the default player clients for the
DOWNLOAD (their URLs go through with a PO token), keeping web_music only as a
fallback for Art-Track extraction; the YTM search still leads with web_music.

Also add an optional YTDLP_PROXY (--proxy) — the durable fix if the IP stays
flagged (403/429) even with a PO token. Empty → unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 17:07:34 +02:00
HoneyandClaude Opus 4.8 4daf87b550 docs(changelog): split download fixes into [2.2] (PO token provider + false-success)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 12:34:15 +02:00
HoneyandClaude Opus 4.8 8fc6a3aaef feat(backend): PO token provider for YouTube downloads; drop the cookies approach
Replace the cookies-file stopgap (needs a throwaway account + manual refresh)
with a self-hosted PO token provider — the cookie-less way past YouTube's
datacenter-IP block (bot check + media 403).

- docker-compose: new bgutil-provider service (brainicism/bgutil-ytdlp-pot-provider)
  + install the bgutil-ytdlp-pot-provider plugin in the backend + POT_PROVIDER_URL
  env + depends_on.
- ytdlp.js / server.js: when POT_PROVIDER_URL is set, pass
  --extractor-args youtubepot-bgutilhttp:base_url=... to the download cascade, the
  YTM search probe and the importer resolve. Empty → omitted (unchanged behaviour).
- Removed the cookiesFile config + --cookies wiring so there's no dead code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 12:28:32 +02:00
HoneyandClaude Opus 4.8 ef019b9b9d fix(backend): direct-link download failure was reported as success
The direct-URL (sourceUrl) path returned { success: true } even when no audio
file was produced (e.g. YouTube 403 / bot check), so the job was marked
"done/Fertig!" and web + Android both said the track was added. It now returns
{ failed: true } → the server sets status 'error' with a clear message, which
both frontends already render as a failure. Search-cascade path was already
correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 12:20:52 +02:00
HoneyandClaude Opus 4.8 0990dfadb5 fix(backend): pass YouTube cookies to yt-dlp to beat the datacenter bot check
YouTube now returns "Sign in to confirm you're not a bot" for yt-dlp from
datacenter IPs, blocking downloads, search and link-resolve. When a cookies file
is present (YTDLP_COOKIES, default /data/youtube-cookies.txt) every yt-dlp call
passes --cookies so it authenticates. Checked at call time (drop the file in, no
code change); absent → unchanged behaviour. Applied to the download cascade, the
YTM search probe, and the importer resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 12:02:26 +02:00
HoneyandClaude Opus 4.8 e82dec7e01 docs(changelog): 2.1 — Musik-Match, Android artist pin, pin/search/similar fixes, player + swipe polish
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 11:45:28 +02:00
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