Android speicherte nur die Song-Ids und lud beim Start jeden Titel EINZELN und
nacheinander ueber das Netz nach. Das dauerte Sekunden, war unzuverlaessig und
ohne Verbindung unmoeglich. Gespeichert werden jetzt die vollstaendigen Songs als
JSON — Wiederherstellen ist ein lokaler Lesevorgang, wie im Web. Der Preis ist
ein veralteter Titel oder Cover bis zum naechsten Abspielen, was die deutlich
kleinere Unschoenheit ist.
Dazu sichert MusicService.onTaskRemoved die Warteschlange beim Wegwischen der
App: dort feuert onIsPlayingChanged nicht zuverlaessig, genau dieser Fall ging
vorher verloren.
Service Worker: die Cache-Version stand handgepflegt als ns-v78 in sw.js und
wurde beim Ausliefern vergessen — die Aktivierung loescht nur Caches mit
abweichendem Namen, also blieb der alte bestehen und Nutzer mussten den
Browsercache von Hand leeren. Ein Vite-Plugin setzt sie jetzt aus der Bauzeit,
die Quelldatei bleibt unveraendert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Erste Android-Version mit Warteschlangen-Wiederherstellung und Streitpunkten.
Das zuvor hochgeladene 1.3 enthielt restoreQueue noch nicht — daher blieb die
Now-Playing-Leiste nach dem Oeffnen leer. Release-Beschreibung mit angehoben,
sonst meldet der In-App-Updater kein Update.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die erste Fassung beschrieb einen anderen Mechanismus (doppelte Tracknummer aus
dem Titel-Tag). Marcs Beispiel zeigt etwas anderes: 01-02 steht an der Stelle des
Kuenstlers, die Tracknummer fehlt. trackPrefix kann diese Form nicht erzeugen,
die Quelle muss also das Kuenstlerfeld so geliefert haben — dieselbe Form ist in
metadata.test.js bereits als "garbage artist prefix" festgehalten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Mitschreiben der Abspielposition war zu viel des Guten: es bedeutete
staendiges Schreiben waehrend der Wiedergabe fuer einen Gewinn, den man kaum
bemerkt. Gesichert werden jetzt nur Warteschlange und aktueller Titel, der dann
von vorn beginnt.
Der eigentliche Gewinn bleibt und war der Ausloeser: unter Android war die
Now-Playing-Leiste nach dem Oeffnen leer, bis man einmal auf Play drueckte —
gesichert war nur der letzte Song, nicht die Warteschlange. Das Wiederherstellen
fuellt den Zustand jetzt ueber syncState(), also ohne Abspielen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wiedergabe fortsetzen:
Im Web ueberlebten Warteschlange und letzter Titel bereits, die POSITION im
Titel aber nicht — beim Neuladen begann der Song von vorn. Android sicherte nur
den letzten Song ohne Warteschlange. Beide merken sich jetzt Warteschlange,
Listenposition und Sekunde.
Web schreibt gedrosselt (alle 5 s) waehrend der Wiedergabe und sicher beim
Pausieren, Wegwischen und Tabwechsel — pagehide/visibilitychange statt
beforeunload, das am Handy unzuverlaessig feuert. Android sichert bei jedem
Titelwechsel und beim Pausieren und stellt nach dem Verbinden wieder her:
vorbereitet, aber NICHT abgespielt, wer die App oeffnet will nicht ueberfallen
werden. Laeuft schon etwas (Android Auto), passiert nichts.
Zwei Feinheiten: lag die Position im letzten Rest des Titels, faengt er von vorn
an, statt zwei Sekunden Abspann zu spielen; und gespeichert werden nur Song-Ids,
weil Titel und Cover sich durch Metadaten-Bearbeitung aendern koennen —
verschwundene Titel werden beim Wiederherstellen uebersprungen.
Streitpunkte im Duell:
Gemeinsame Kuenstler nach groesstem Ungleichgewicht statt nach Summe, mit
Kraefteverhaeltnis-Balken. Mindestens vier gemeinsame Plays und 50 % Unwucht,
sonst gilt ein einzelner Play gegen null schon als Streit. Web und Android mit
identischen Schwellen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ergaenzt Backend und Scrobble um die Navidrome-Adresse: alle drei kommen als
buildConfigField aus dem Build und greifen, wenn im Geraetespeicher nichts steht.
Der Anmeldebildschirm ist damit vorbelegt — beim ersten Start bleiben nur noch
Benutzer und Passwort. Die Felder in den Einstellungen bleiben sichtbar und
ueberschreibbar, ein Umzug ist also ohne neues APK zu korrigieren.
Zugangsdaten bleiben aussen vor. Das Publish-Skript reicht die Werte aus
scripts/.env.release durch, damit kuenftige Builds sie nicht verlieren.
Am erzeugten BuildConfig geprueft, alle vier Werte stehen darin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Host stand ohnehin schon beim Bauen fest, weil die App-Links ihn im Manifest
brauchen. Ihn zusaetzlich als Einstellung zu fuehren hiess: eine vierte Adresse
pflegen, die vom Manifest abweichen kann, ohne dass es jemand merkt — und der
Teilen-Eintrag versteckte sich, solange sie leer war, was beim Suchen schlicht
wie ein fehlendes Feature aussah.
Jetzt kommt beides aus -PnovasonicHost=…: das Manifest fuer eingehende Links,
BuildConfig.SHARE_HOST fuer ausgehende. Die Einstellung samt Speicherfeld ist
entfernt, Teilen ist immer sichtbar. versionCode 5 / 1.2, APK hochgeladen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Statt eines Prozentrings ("wie aehnlich seid ihr") zeigt die Ansicht jetzt, wer
worin gewinnt: fuenf Kategorien mit je einem Sieger und ein Gesamtstand.
Hoerzeit, Vielfalt, Tiefe (Plays je Kuenstler), Nachteule (Anteil vor 5 Uhr) und
Entdeckerdrang (Anteil exklusiver Kuenstler) — bewusst Kennzahlen zur Hoerweise
statt zur Menge, sonst gewinnt immer, wer laenger dabei ist.
Am laufenden Dienst mit zwei gegensaetzlichen Nutzern geprueft: 60 Plays a 300 s
ergeben 5,0 h und Tiefe 12,0, jeder dritte Play nachts ergibt 33 %, 60 von 62
exklusiven Kuenstlern ergeben 97 % — Endstand 3:2. Das JSON-Feldschema wurde
gegen das Kotlin-Modell abgeglichen.
Dazu drei Fixes im selben Bereich:
- Die Web-Ansicht lebte nur von Inline-Stilen mit festen Flex-Verhaeltnissen und
zerfiel am Desktop. Jetzt ein Raster mit eigenen Klassen, eine Spalte am Handy,
zwei ab 780 px; das Fenster musste dafuer von 720 auf 940 px, sonst haette die
zweite Spalte nie ausgeloest.
- Gemeinsame Songs wurden ueber song_id abgeglichen und fielen auseinander,
sobald eine Datei umbenannt wurde. Jetzt normalisierter Titel+Kuenstler.
- Media-Session: die Metadaten wurden feldweise mutiert (gegen Kachel-Flackern
unter Windows), ueber MPRIS kam davon nichts an — Plasma-Widgets zeigten
dauerhaft den zuerst gespielten Titel. Bei echtem Titelwechsel jetzt ein neues
MediaMetadata, sonst weiter in-place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein Song oder Album laesst sich per https://<host>/?song=<id> an jemanden auf
derselben Instanz schicken. Der Link traegt nur die Navidrome-Id, keine
Zugangsdaten — der Empfaenger sieht den Inhalt mit seinem eigenen Konto.
Web: Teilen ueber navigator.share() mit Rueckfall auf die Zwischenablage. Beim
Start wird ?song= / ?album= aufgeloest (nach der Anmeldung, vorher fehlen die
Zugangsdaten), das Album geoeffnet, der Song gestartet und der Parameter aus der
Adresszeile entfernt, damit ein Neuladen nicht erneut losspielt. Das Manifest
bekommt launch_handler=navigate-existing, damit der Link in einem bereits
offenen PWA-Fenster landet statt in einem neuen.
Android: Teilen ueber ACTION_SEND, Empfang ueber einen App-Link-Intent-Filter.
Der Host ist eine Build-Eigenschaft (-PnovasonicHost=…, verifiziert am
zusammengefuehrten Manifest), die Web-Adresse fuer ausgehende Links eine neue
Einstellung. Weil der NavController in der Compose-Hierarchie lebt und die
Activity nicht selbst navigieren kann, reicht DeepLinkState den Link durch;
onNewIntent ist mitbedacht, sonst passierte beim zweiten Link nichts.
assetlinks.json liegt als Vorlage in public/ und wird vom Frontend-Image
ausgeliefert (im Build verifiziert); ohne den Release-Fingerabdruck oeffnet der
Link die Web-App im Browser statt die App. Anleitung: docs/share-links.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Luecken derselben Familie, die dazu fuehrten, dass dieselben Songs ein
zweites Mal geladen wurden:
- Kuenstlernamen wurden zeichengenau verglichen: "Cécile Corbel" und "Cecile
Corbel" galten als zwei Personen. Welche Schreibweise ankommt, haengt an der
Quelle (Deezer vs. Spotify). Der Vergleich normalisiert jetzt Akzente weg —
nur der Vergleich, Tags und Dateinamen behalten das Original.
- Typografische Apostrophe: "Arrietty’s Song" (U+2019) und "Arrietty's Song"
fielen auseinander. Fiel beim Testen auf, betrifft viele Soundtracks.
- findExistingTrack durchsucht nur den Ordner des uebergebenen Kuenstlers. Bei
Soundtracks weicht der Album- vom Titel-Kuenstler ab, der Song landet je nach
Ladeweg unter dem einen oder anderen — geprueft wurde jeweils der falsche.
Weicht der Titel-Kuenstler ab, wird nun auch dessen Ordner geprueft (~0,6 ms
bei 28.801 Dateien, gegenueber 3-9 s pro Download).
Sieben neue Tests zielen gezielt auf Fehlalarme: aehnliche Namen (Cecil Taylor
vs. Cécile Corbel), gleicher Titel bei verschiedenen Kuenstlern, Remix gegen
Basistitel, fremder Titel im Soundtrack-Ordner — alle bleiben getrennt.
Gesamte Suite: 42 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Android holt zwei Sachen nach, die es im Web laengst gibt:
- Song von der Festplatte loeschen, im Kontextmenue unten abgesetzt und rot, mit
derselben Sicherheitsabfrage wie im Web. Laeuft ueber /api/delete-song und
erscheint nur mit konfiguriertem Backend — Navidrome allein loescht nichts.
- Playlist direkt aus dem Import anlegen: Haekchen plus Namensfeld ab zwei
Titeln, danach Scan anstossen, Titel suchen, Playlist aus den gefundenen.
Nicht gefundene werden gezaehlt statt die Erstellung scheitern zu lassen.
Dazu der Web-Fix zur Album-Position: die gespeicherte Scrollposition wurde in
genau einem requestAnimationFrame gesetzt. Solange Cover und Eintraege noch
nachluden, war scrollHeight kleiner als das Ziel und der Browser kappte den Wert
stillschweigend — man landete wieder oben. Jetzt wird er ueber bis zu ~0,5 s
erneut gesetzt, bis er sitzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
is_live meint nur "laeuft gerade als Stream" — eine Konzert-AUFNAHME kam
ungehindert durch und landete als Albumtrack in der Bibliothek ("Dat Adam - Ufo
Video Days 2015 Live" auf dem echten Kuenstlerkanal, Dauer im Fenster). Der
Variantenfilter bekommt daher eine live-Regel. Sie schaltet sich ab, wenn der
gesuchte Titel selbst live enthaelt; die Pruefung nutzt \blive\b statt includes,
sonst wuerde "deliver" die Regel aushebeln. Drei Tests dazu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Folgekorrektur zu 02b5905: "… - 2020 Remaster" steht auf der Veroeffentlichung,
aber nie im YouTube-Titel — als Pflicht-Keyword haette die Zahl den richtigen
Upload abgelehnt. Jahreszahlen neben einem Qualifier (remaster/version/edition/
anniversary/mono/stereo) fallen daher wieder raus; ein Titel, der selbst eine
Jahreszahl ist ("1999", "Berlin 2020"), behaelt sie. Gegen 29 Beispieltitel
gemessen verhalten sich 24 exakt wie vorher.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"700 Main Street" lud "777 Main Street" eines fremden Artists: buildTitleKeywords
behielt nur die zwei laengsten Woerter (Street, Main), die unterscheidende 700
fiel als zu kurz raus — und Schritt 3 der Kaskade prueft den Kuenstler-Channel
nicht mehr. Zahlen werden jetzt immer als \b<n>\b gefordert. Dazu eine kleine
Abkuerzungstabelle (street/st, saint/st, mount/mt, doctor/dr, versus/vs,
number/no), denn der korrekte Art Track heisst "700 Main st" und scheiterte
bisher an jedem Schritt. Tests in backend/test/ytdlp.test.js.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fasst die Aenderungen seit 2.2 als Release zusammen: image-basiertes
docker-compose.yml (Bauen wandert nach .dev.yml), der auf GITEA_URL
umgeschriebene APK-Asset-Download und der Alben-Tab-Fix, dazu das
veroeffentlichte versionCode 3. Am Handy bestaetigt: Update-Dialog,
In-Place-Install und die Scrollpositionen funktionieren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
APK mit dem Debug-Key vom alten Rechner gebaut (Signatur 1039a00e…, passend zur
installierten App) und als Asset im Release apk-latest ersetzt, Body auf
versionCode=3. Dazu das ausfuehrbare Bit auf gradlew, das im Clone fehlte, und
.idea/ ignoriert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LaunchedEffect(state.albumSort) { scrollToItem(0) } feuerte nicht nur beim
Sortierwechsel, sondern auch beim ersten Compose nach der Rueckkehr aus einem
Album — die im ViewModel gehaltene Position wurde also sofort ueberschrieben.
Das Hochscrollen sitzt jetzt in setAlbumSort() und laeuft nur bei echtem
Wechsel; dazu zwei ungenutzte remember*State-Importe entfernt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
- .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>
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>
- 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>
- 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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>