# Changelog All notable changes to NovaSonic are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features and fixes together). Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche das waren, steht in einer **Android:**-Zeile direkt unter der Release-Überschrift — die Einträge darunter beschreiben nur noch, *was* sich geändert hat, nicht in welchem APK. --- ## Platform Parity Matrix | Feature | Web (PWA) | Android | |----------------------------------|:---------:|:-------:| | Music playback (Navidrome) | ✅ | ✅ | | Seek bar / scrubbing | ✅ | ✅ | | Queue management | ✅ | ✅ | | Shuffle / Repeat | ✅ | ✅ | | Radio mode (similar songs) | ✅ | ✅ | | Now Playing overlay | ✅ | ✅ | | Dominant color background | ✅ | ✅ | | Cover art scale animation | ❌ | ✅ | | Synced lyrics (LRCLIB) | ✅ | ✅ | | Lyrics autoscroll | ✅ | ✅ | | Star / favorite song | ✅ | ✅ | | Multi-artist navigation | ✅ | ✅ | | Album detail page | ✅ | ✅ | | Artist page (Navidrome) | ✅ | ✅ | | Artist page (Deezer fallback) | ✅ | ✅ | | Global search source | Deezer | Deezer | | Same-name artist disambiguation | ✅ | ✅ | | Artist fan count shown | ✅ | ✅ | | 30s preview control bar | ✅ | ✅ | | Concert events (Ticketmaster) | ✅ | ✅ | | Genre browsing | ✅ | ✅ | | Global search | ✅ | ✅ | | Playlist browsing | ✅ | ✅ | | Playlist create/edit | ✅ | ✅ | | Playlist cover upload | ✅ | ✅ | | Besitzer geteilter Playlists | ✅ | ✅ | | Spotify / YouTube importer | ✅ | ✅ | | Musik-Duell (Anzeigetafel) | ✅ | ✅ | | Wiedergabe fortsetzen | ✅ | ✅ | | Teilen per Link | ✅ | ✅ | | Album → Künstler (Deezer-Suche) | ✅ | ✅ | | Playlist aus Import anlegen | ✅ | ✅ | | Song von Festplatte löschen | ✅ | ✅ | | Metadata editor (tags + cover) | ✅ | ❌ | | Song delete (from disk) | ✅ | ❌ | | Import notification | ✅ | ✅ | | Import notification → nav | ✅ | ✅ | | ABS (Audiobookshelf) playback | ✅ | ✅ | | ABS chapter navigation | ✅ | ✅ | | ABS sleep timer | ✅ | ✅ | | ABS playback speed | ✅ | ✅ | | ABS library search | ✅ | ✅ | | ABS book descriptions (HTML) | ✅ | ✅ | | Media notification | ✅ | ✅ | | ABS notification (modern style) | ❌ | ✅ | | Android Auto | — | ✅ | | Offline download (albums/playlists) | ❌ | ✅ | | Downloads nur über WLAN | — | ✅ | | Offline download (single songs) | ❌ | ✅ | | Offline playback | ❌ | ✅ | | Offline detail / manage downloads | ❌ | ✅ | | Player bar on all screens | ✅ | ✅ | | Timestamped play-log (own scrobble) | ✅ | ✅ | | Listening statistics view | ✅ | ✅ | | Stats cover art (artist/album) | ✅ | ✅ | | Musik-Match (Head-to-Head) | ✅ | ✅ | | Yearly Wrapped | 🔜 | 🔜 | | Romaji / Japanese sort | ✅ | ❌ | | Settings (Navidrome, ABS, etc.) | ✅ | ✅ | --- ## [2.6] — 2026-09-12 **Android:** 1.9 (versionCode 12) → 1.11 (versionCode 14). 1.9 brachte den Besitzer geteilter Playlists und die Offline-Korrekturen, 1.10 den Zählfehler im Downloader, 1.11 den WLAN-Schalter. ### Fixed - **Zurück und Vorwärts gingen im installierten Fenster am PC gar nicht (Web)** — im Browser-Tab funktionierten sie, im PWA-Fenster nicht. Das Manifest setzt `"display": "standalone"`: dort gibt es keine Zurück-/Vorwärts-Knöpfe, und die Seitentasten der Maus werden nicht auf den Verlauf umgelenkt. Die App übernimmt die Maustasten X1/X2 jetzt selbst — **nur** im Standalone-Fenster, denn im Tab macht das der Browser und wir würden doppelt navigieren (beides mitgetestet). Zurück geht über denselben Weg wie der In-App-Knopf, schließt also erst ein offenes Now-Playing-Fenster. `Alt`+`←`/`→` bleibt dem Browser überlassen, das kann er auch im Fenster. - **Zwei Node-Dienste hießen in Navidromes Player-Liste fast gleich** — das Backend meldete sich als `novasonic`, der Scrobble-Dienst als `NovaSonic`; da Navidrome den Namen als `client [User-Agent]` bildet, standen dort zwei Zeilen `novasonic [node]` und `NovaSonic [node]`, unterscheidbar nur an einem Großbuchstaben. Genau die Zeile, an der „Report Real Path" hängt, war so nicht auffindbar. Jetzt `NovaSonic-Backend` und `NovaSonic-Scrobble`. Navidrome legt dafür **neue** Player-Zeilen an; die alten können weg. Eine Player-Zeile hält nur Client, User-Agent, IP, Transcoding, Bitratengrenze und die beiden Schalter — keine Wiedergabezahlen, keine Historie. Löschen ist also gefahrlos, und die neuen Zeilen erben `ND_SUBSONIC_DEFAULTREPORTREALPATH`. - **„Pfad (Navidrome)" im Info-Fenster wieder zu „Pfad"** — der Zusatz war doppelt gemoppelt: der Wert kommt ohnehin aus der Subsonic-API. Was er bedeutet, steht als Kommentar im Code und in den Known Bugs, nicht als Etikett im Fenster. - **Der Zurück-Knopf tat oft nichts (Web)** — `navigate()` prüfte zwar, ob die Ansicht sich überhaupt geändert hat, und ließ den Verlaufseintrag dann weg — **rief `history.pushState` aber trotzdem**. Zweimal auf denselben Tab geklickt hieß also zwei Browser-Einträge auf dieselbe Ansicht, und jeder davon kostete einen Zurück-Druck, bei dem sichtbar nichts passierte. Gemessen im Test: dreimal „Alben" erzeugte **drei** Browser-Einträge bei **einem** Stack-Eintrag, danach bewegten vier Zurück-Drücke die Ansicht genau einmal. Jetzt bekommt nur eine wirklich neue Ansicht einen Verlaufseintrag, sonst wird der bestehende ersetzt. Der allererste Aufruf ersetzt ebenfalls, weil `main.js` die schon offene Seite bereits auf `nsIdx 0` gesetzt hatte — auch das war ein toter Zurück-Druck gleich zu Beginn. Vorwärts funktioniert unverändert (mitgetestet). ### Fixed - **Halb geladene Gruppen meldeten sich als „Vollständig" (Android)** — der Worker startete seinen Zähler bei `group.downloadedTracks` und zählte dieselben fertigen Titel beim Überspringen **gleich noch einmal**: nach einem Fortsetzen mit 412 fertigen Titeln meldete die Gruppe 824. Überschreitet der Wert `totalTracks`, gilt sie als vollständig — der Fortschrittsbalken verschwindet und der Herunterladen-Knopf wird unantastbar, obwohl Titel fehlen. Der Fehler steckte schon länger drin, fiel aber kaum auf, solange Fortsetzen nur von Hand passierte; mit der automatischen Wiederholung hätte er bei jedem Lauf zugeschlagen. Der Zähler beginnt jetzt bei 0 und zählt jeden fertigen Titel genau einmal. - Nebenbei: beim Überspringen wurde der Fortschritt in die Datenbank geschrieben, obwohl er sich nicht ändert — 1900 überflüssige Schreibzugriffe je Lauf. Geschrieben wird jetzt nur nach einem tatsächlichen Versuch und einmal beim Abbruch. - **Große Offline-Downloads liefen nie zu Ende (Android)** — WorkManager räumt einem Hintergrund-Worker rund **10 Minuten** ein und stoppt ihn dann. Ein Vordergrunddienst ist keine Option, `FOREGROUND_SERVICE_TYPE_DATA_SYNC` bringt die App unter Android 15 zum Absturz. Der Downloader prüfte aber nie `isStopped`, lief nach dem Stoppsignal blind weiter bis zum harten Abbruch, und meldete am Ende `Result.failure()` statt `Result.retry()` — ohne Backoff. WorkManager startete deshalb **nichts** von allein neu: bei 1900 Titeln musste man immer wieder von Hand auf Herunterladen tippen. Jetzt: - die Schleife steigt bei `isStopped` sauber aus und fordert den nächsten Lauf an (`Result.retry()`, linearer Backoff 30 s). Fertige Titel überspringt sie ohnehin, es wird also fortgesetzt statt neu geladen. - bleiben danach noch Titel offen, fordert der Worker selbst weitere Läufe an — begrenzt auf 20 Versuche, damit dauerhaft kaputte Titel (auf dem Server gelöscht, immer 404) keine Endlosschleife auslösen. Danach steht in der Gruppenzeile weiter `x/y Titel`, und ein Tipp startet die Kette neu. - ein einzelner Netzfehler beendete bisher die **ganze** Kette endgültig (`Result.failure()` im äußeren `catch`); jetzt wird auch das wiederholt. - beim App-Start werden unvollständige Gruppen weitergeführt (`setzeUnvollstaendigeFort()`), falls die Kette doch einmal reißt — Geräteneustart, aufgebrauchte Versuche. - Nebenbei: die Zahl der offenen Titel kam aus einer Abfrage **pro Titel** am Ende jedes Laufs (1900 Stück); jetzt eine einzige `COUNT`-Abfrage mit JOIN. - **Große Offline-Playlists ließen sich nicht abspielen (Android)** — bei ~100 Titeln ging alles, bei 1900 nicht. Ursache war nicht die Größe an sich: die Warteschlange wurde aus **allen** Titeln der Gruppe gebaut, ohne Rücksicht auf den Download-Zustand. Die Karte, aus der die lokale Datei kommt, kennt dagegen nur fertige (`WHERE state = 'DONE'`) — für wartende oder fehlgeschlagene Titel lieferte `streamUrl()` deshalb die **Netz-Adresse**, und die tut offline nichts. Bei 100 Titeln ist der Download längst durch, bei 1900 praktisch nie; liegt so ein Titel vorn, scheitert die Wiedergabe sofort und sieht aus, als ginge die ganze Playlist nicht. Play und Shuffle nehmen jetzt nur fertige Titel, ein Hinweis nennt die Zahl der übersprungenen. Die Liste zeigt weiterhin alle, damit man sieht, was noch fehlt; ein Tipp auf einen unfertigen Titel sagt das auch. - Verworfen: die naheliegende Vermutung, Media3 stoße bei 1900 Einträgen an die 1-MB-Grenze einer Binder-Transaktion. Tut es nicht — `MediaControllerImplBase` reicht die Liste als `BundleListRetriever` durch, den die Session seitenweise abholt. Die Größe ist unschuldig. ### Added - **„Nur über WLAN herunterladen" (Android)** — die Downloadarbeit lief bisher mit `NetworkType.CONNECTED`, also über **jede** Verbindung. Das war selbstbegrenzend, solange nach zehn Minuten Schluss war; mit der automatischen Wiederholung hätte sich eine Playlist unterwegs ununterbrochen weitergeladen — bei 1900 Titeln grob 9–10 GB über Mobilfunk. Neuer Schalter in den Einstellungen unter **📥 Offline-Downloads**, **standardmäßig an**: dann gilt `UNMETERED`, der Download pausiert ohne WLAN und läuft von selbst weiter, sobald es zurück ist. Abschalten geht nur über eine Rückfrage, die das Datenvolumen ausdrücklich benennt. - Beim Speichern werden laufende Gruppen neu eingereiht (`REPLACE`): WorkManager ändert die Bedingungen einer bereits eingereihten Arbeit nicht mehr, der Schalter hätte sonst erst beim nächsten App-Start gewirkt. Unbedenklich, weil der Worker fortsetzt statt neu zu laden. - **Bei geteilten Playlists steht jetzt dabei, von wem sie sind (Web + Android)** — Navidrome liefert `owner` in `getPlaylists`/`getPlaylist` mit, im Web wurde das Feld nie benutzt. Übersicht und Detailseite zeigen es als `· von `, und zwar **nur bei fremden** Listen — bei den eigenen wäre „von mir" nur Rauschen. Android zeigte den Besitzer auf der Detailseite bisher bei **jeder** Liste und unbeschriftet, sodass der eigene Name wie ein Teil des Titels aussah; jetzt verhalten sich beide Plattformen gleich, und die Playlist-Übersicht in der Bibliothek zeigt es ebenfalls. --- ## [2.5] — 2026-09-10 **Android:** 1.8 (versionCode 11) — Sprung vom Album zum Künstler. ### Added - **Vom Album zum Künstler springen (Web + Android)** — in der globalen Suche führte ein Album bisher in eine Sackgasse: der Künstlername darunter war reiner Text. Wer von dort zum Künstler wollte, musste zurück und neu suchen. Der Name ist jetzt anklickbar und öffnet dieselbe Deezer-Künstlerseite wie eine Künstler-Kachel. Betrifft im Web die Inline-Albumansicht in der globalen Suche und die Vollbild-Albumseite, in Android die Album-Detailansicht in Erkunden — dort stand vorher **gar kein** Künstler, nur der Albumtitel. - **Kostet keinen zusätzlichen API-Aufruf.** Deezer legt `/search/album` und `/album/{id}` das vollständige `artist`-Objekt samt `id` bei (in 120 von 120 Treffern über zwölf Suchen geprüft); beide Mappings (`_dzAlbum` im Web, `DzAlbum.toItunesItem()` in Android) übernahmen die id schon, sie wurde nur nirgends benutzt. - **Ausnahme mit Absicht:** `/artist/{id}/albums` lässt das `artist`-Objekt weg. Alben aus dieser Liste haben keine id, dort bleibt der Name deshalb reiner Text statt ins Leere zu zeigen — man kommt ja gerade von der Künstlerseite. Ein Ersatz-Lookup wäre ein Aufruf für einen Sprung, den niemand macht. - Deezer nennt nur den **Haupt**künstler: bei „A feat. B" landet man auf A. - Android lädt Bild und Fanzahl nach, wenn der Künstler aus einem Album konstruiert wurde — sonst sähe die Kopfzeile anders aus als beim Weg über die Künstlersuche. - **Der Künstler in den Albumansichten ist überhaupt erst zu sehen (Web)** — er stand in `--text2` (`#8888aa`) und 14px unter einem 28px starken Albumtitel und ging darin schlicht unter; dass man ihn anklicken kann, war nirgends erkennbar. Jetzt hell (`--text`), 15px und halbfett, und wo wirklich ein Ziel dahintersteht, zusätzlich dezent unterstrichen (beim Überfahren in Akzentfarbe). Gilt für die lokale Albumseite, die Deezer-Albumseite und die Inline-Albumansicht in der globalen Suche. Die Teil-Album-Seite bleibt bewusst ohne Link — dorthin kommt man nur von der Künstlerseite. - Nebenbei behoben: `.album-artist-name` trug `cursor:pointer` unabhängig davon, ob der Name irgendwohin führte — der Zeiger versprach auf der Deezer- und der Teil-Album-Seite einen Klick, den es nicht gab. Jetzt nur noch dort, wo wirklich ein Ziel dahintersteht. --- ## [2.4] — 2026-09-06 **Android:** 1.4 (versionCode 7) → 1.7 (versionCode 10). 1.4 brachte Warteschlangen-Wiederherstellung und die Streitpunkte im Duell, 1.6 machte das Wiederherstellen rein lokal und damit sofort, 1.7 leert den Cache nach einem Update selbst. ### Added - **Cache räumt sich nach einem Update selbst auf (Android)** — im `cacheDir` liegen Coils Bildspeicher (Cover) und heruntergeladene APK-Updates. Blieb dort nach einer neuen Version etwas Altes liegen, half nur „Cache leeren" von Hand in den Systemeinstellungen — nichts, das man einem normalen Nutzer erklären möchte. Beim ersten Start nach einem Versionswechsel wird der Cache jetzt einmalig geleert; im laufenden Betrieb passiert nichts, sonst wären die Cover bei jedem Start neu zu laden. **Offline-Downloads sind nicht betroffen**, die liegen unter `filesDir/offline` und damit außerhalb des Caches. - **Service-Worker-Cache versioniert sich beim Bauen selbst** — die Cache-Version stand handgepflegt als `ns-v78` in `sw.js` und wurde beim Ausliefern zuverlässig vergessen. Da die Aktivierung nur Caches mit *abweichendem* Namen löscht, blieb der alte bestehen und Nutzer mussten den Browsercache von Hand leeren. Ein Vite-Plugin setzt die Version jetzt aus der Bauzeit (`ns-JJJJMMTThhmmss`), der alte Cache räumt sich beim ersten Start selbst ab. Die Quelldatei bleibt unverändert. - **Zuletzt Gehörtes und Warteschlange überleben den Neustart (Web + Android)** — Android sicherte bisher nur den letzten Song, ohne Warteschlange: die Now-Playing-Leiste blieb nach dem Öffnen **leer, bis man einmal auf Play drückte**. Jetzt merken sich beide Plattformen Warteschlange und aktuellen Titel und stellen sie beim Start wieder her — Android **vorbereitet, aber nicht abgespielt**, damit einen die App beim Öffnen nicht überfällt; läuft schon etwas, passiert nichts. Der Titel beginnt von vorn: die Sekunde im Titel wird bewusst **nicht** gespeichert, das spart dauerndes Schreiben während der Wiedergabe. - Der erste Anlauf (1.4) sicherte nur Song-Ids und lud sie beim Start **einzeln nacheinander über das Netz** nach: das dauerte Sekunden, klappte mal und mal nicht und ohne Verbindung gar nicht. Seit 1.6 liegen die vollständigen Songs als JSON im Gerätespeicher, das Wiederherstellen ist ein rein lokaler Lesevorgang wie im Web. Preis: zwischenzeitlich geänderte Titel oder Cover zeigen bis zum nächsten Abspielen den alten Stand. - `MusicService.onTaskRemoved` sichert die Warteschlange auch, wenn die App aus den letzten Apps gewischt wird — dabei feuert `onIsPlayingChanged` nicht zuverlässig, genau dieser Fall ging vorher verloren. Protokoll unter dem Tag `NsQueue` (`adb logcat -s NsQueue`). - Hinweis fürs Testen: der gespeicherte Zustand entsteht erst ab dieser Version, direkt nach der Installation ist die Leiste also noch leer — einmal abspielen, App schließen, wieder öffnen. - **Streitpunkte im Musik-Duell (Web + Android)** — gemeinsame Künstler, sortiert nach dem größten **Ungleichgewicht** statt nach der Summe: wo einer 120 Plays hat und der andere 3. Ein Balken zeigt das Kräfteverhältnis. Mindestens vier gemeinsame Plays und 50 % Unwucht, damit ein einzelner Play gegen null nicht als Streit gilt. Beide Plattformen nutzen dieselben Schwellen. --- ## [2.3] — 2026-09-04 **Android:** versionCode 3 → 1.3 (versionCode 6). versionCode 3 lieferte den Alben-Tab-Fix aus und erstmals das Scroll- und Suchgedächtnis der Künstlerseite (2.2 enthielt den Code, das damalige APK war älter); 1.1 brachte Teilen-Link, Songlöschen, Playlist aus dem Import und die Duell-Anzeigetafel; 1.2 und 1.3 nahmen die Adressen aus den Einstellungen in den Build. Alle mit demselben Debug-Schlüssel signiert, Updates laufen also in-place und App-Daten bleiben. ### Fixed - **YT-Music-Suche war praktisch tot** — `getYtmVideoUrls` verwarf die komplette Trefferliste, sobald irgendwo ein Kanal-Eintrag (`browse/UC…`) auftauchte, samt der bereits gesammelten Song-URLs. An acht echten Suchen gemessen passierte das in **sieben** Fällen: Schritt 1 der Kaskade — die Stufe mit den offiziellen Art Tracks — lieferte nie etwas, faktisch lief jeder Download über den YouTube-Fallback. Das erklärt eine ganze Serie falscher Treffer (u. a. „Easy" als Mixtape- statt Albumversion). Kanal- und Album-Einträge werden jetzt übersprungen statt abzubrechen; über 20 000 Zufallsausgaben geprüft ist die neue Auswahl eine Obermenge der alten, kann also nur Kandidaten hinzufügen. - **Zurück im Album führte in die Albenliste** — statt dorthin, wo man das Album angeklickt hatte. Der Knopf navigierte vorwärts, legte dabei einen History-Eintrag an und brachte so auch den Browser-Zurück-Knopf durcheinander. Jetzt geht er einen Schritt im Verlauf zurück wie jede andere Detailseite. - **Metadaten bearbeiten zerlegte die Hörstatistik** — Top-Titel gruppierten nach `song_id`, und beim Bearbeiten umbenannte Dateien bekommen von Navidrome eine neue ID: der Titel erschien doppelt mit geteilten Zählern, Künstler ebenso bei geänderter Schreibweise. Gruppiert wird jetzt über einen normalisierten Schlüssel (Titel + Künstler), Anzeigenamen kommen aus dem jüngsten Play. Playlists waren nie betroffen, die werden beim Bearbeiten ohnehin auf die neue ID übertragen. - **Downloader grabbed a same-shaped title with a different number** — searching DAT ADAM's "700 Main Street" saved **"777 Main Street" by an unrelated artist**. `buildTitleKeywords` required only the two longest words (Street, Main); "700" was dropped for being short, and the cascade step that widens to the top 10 no longer checks the artist channel, so nothing else caught it. Numbers are now always required keywords. In the same pass, a small abbreviation map (street/st, saint/st, mount/mt, doctor/dr, versus/vs, number/no) lets a candidate spelled "700 Main St" match a release titled "700 Main Street" — that mismatch had rejected the correct official Art Track in every step, which is why the search fell through to the wrong video in the first place. A re-release year next to a qualifier ("… - 2020 Remaster") is exempt from the number rule: it appears on the release but never in the YouTube title. Measured against a 29-title sample, 24 titles keep their previous behaviour exactly. - **Downloader saved a concert recording as the album track** — searching DAT ADAM's "UFO" stored "Dat Adam - Ufo Video Days 2015 Live". It passed every gate legitimately: right channel, 209 s inside the duration window, "Ufo" in the title, and `!is_live` rejects only *running streams*, not live recordings. The variant blocklist (remix/cover/acoustic/…) now includes `live`. As with the other rules it switches itself off when the searched title says live, so "Live Forever" and deliberately requested live versions are unaffected. - **Album-Position im Web wurde doch nicht wiederhergestellt** — die gespeicherte Scrollposition wurde in genau einem `requestAnimationFrame` gesetzt. Solange Cover und Listeneinträge noch nachluden, war `scrollHeight` kleiner als das Ziel, und der Browser kappte den Wert stillschweigend: man landete wieder oben. Der Wert wird jetzt über bis zu ~0,5 s erneut gesetzt, bis er sitzt. - **Metadaten-Änderungen „sprangen zurück" — bei manchen Songs immer, bei anderen nie.** Findet das Backend die von Navidrome gemeldete Datei nicht am angegebenen Pfad (Navidrome liefert oft einen aus Tags gebauten *virtuellen* Pfad), sucht es über Dateiname bzw. Künstler+Titel weiter. Liegt derselbe Song mehrfach auf der Platte — etwa ein Fehlgriff des Downloaders neben der richtigen Datei —, konnte das die **falsche Kopie** treffen: die wurde getaggt, der bearbeitete Song blieb unverändert. Genau deshalb ging es bei Songs ohne Dublette immer und bei anderen nie. Der über Ähnlichkeit gefundene Kandidat wird jetzt gegen die Tags geprüft, die Navidrome für den Song meldet, und bei Widerspruch mit einer erklärenden Meldung abgebrochen, statt zu raten. - **Dieselbe Playlist zweimal geladen ⇒ Songs doppelt auf der Platte.** Die Dublettenprüfung verglich Künstlernamen zeichengenau, sodass „Cécile Corbel" und „Cecile Corbel" als zwei verschiedene Personen galten — welche Schreibweise ankommt, hängt davon ab, ob die Quelle Deezer oder Spotify war. Der Vergleich ignoriert jetzt Akzente (nur der **Vergleich** — Tags und Dateinamen behalten die Originalschreibweise) und ebenso typografische Apostrophe, an denen `Arrietty’s Song` und `Arrietty's Song` bisher auseinanderfielen. - **Soundtracks landeten doppelt** — die Prüfung durchsucht nur den Ordner des Album-Künstlers. Bei Soundtracks und Compilations weicht der vom Titel-Künstler ab: als Album geladen landet der Song unter `Arcane/`, einzeln geladen unter `Bea Miller/` — die Prüfung sah jeweils im falschen Ordner nach. Weicht der Titel-Künstler ab, wird jetzt auch dessen Ordner geprüft. Gemessen an 28.801 Dateien kostet der zweite Durchlauf ~0,6 ms, gegenüber 3–9 s pro Download. - **Stumm übersprungene Umbenennungen** — lag am Zielnamen schon eine Datei, blieb der Song unter altem Namen bzw. im alten Ordner liegen, ohne Hinweis. Solche Fälle kommen jetzt als Warnung zurück; der Editor zeigt sie samt bearbeitetem Dateipfad an und bleibt offen. ### Added - **Musik-Match wird zum Duell (Web + Android)** — statt eines Prozentrings zeigt die Ansicht jetzt eine Anzeigetafel mit fünf Kategorien, die je einen Sieger haben: Hörzeit, Vielfalt, Tiefe (Plays je Künstler), Nachteule (Anteil vor 5 Uhr) und Entdeckerdrang (Anteil exklusiver Künstler). Bewusst Kennzahlen zur *Hörweise* statt zur schieren Menge, sonst gewinnt immer, wer länger dabei ist. Unentschieden zählt für niemanden. - **Vergleichsansicht im Web zerfiel am Desktop** — sie lebte ausschließlich von Inline-Stilen mit festen Flex-Verhältnissen. Jetzt ein echtes Raster (eine Spalte am Handy, zwei ab 780 px) mit eigenen CSS-Klassen; das Fenster ist von 720 auf 940 px verbreitert, sonst hätte die zweite Spalte nie ausgelöst. - **Gemeinsame Songs wurden über `song_id` abgeglichen** — hatten beide denselben Song aus verschiedenen Dateien gehört oder wurde eine Datei zwischendurch umbenannt, galt er als nicht gemeinsam. Jetzt derselbe normalisierte Schlüssel wie in der Statistik. - **Titelwechsel erreichte den Linux-Desktop nicht** — die Media-Session-Metadaten wurden feldweise mutiert, um unter Windows das Neuaufbauen der Medienkachel zu vermeiden. Über MPRIS kam davon nichts an: Plasma-Widgets zeigten dauerhaft den zuerst gespielten Titel. Bei echtem Titelwechsel wird jetzt ein neues `MediaMetadata` zugewiesen, sonst weiter in-place (Cover-Nachladen). - **Keine Adressen mehr eintippen (Android)** — Navidrome-, Backend- und Scrobble-Adresse kommen als Vorgabe aus dem Build (`-PnovasonicNavidrome=` / `-PnovasonicBackend=` / `-PnovasonicScrobble=`, gepflegt in `scripts/.env.release`). Sie greifen, wenn im Gerätespeicher nichts steht; die Felder in den Einstellungen bleiben sichtbar und überschreibbar, damit ein Umzug am Gerät korrigierbar ist statt per neuem APK. Beim ersten Start ist nur noch **Benutzer und Passwort** einzugeben — Zugangsdaten werden bewusst *nicht* eingebacken, die gehören pro Person. - **Die Web-Adresse ist keine Einstellung mehr (Android)** — sie kommt aus `BuildConfig.SHARE_HOST`, also aus **derselben** Angabe (`-PnovasonicHost=…`), die ohnehin für die App-Links ins Manifest wandert. Damit gibt es keine vierte Adresse zu pflegen, „Teilen" ist immer sichtbar, und ein- und ausgehende Links können nicht mehr auseinanderlaufen. - **Falle beim Veröffentlichen** — der In-App-Updater vergleicht den `versionCode` aus der Gitea-Release-Beschreibung mit dem der installierten App. Wird die Version nicht angehoben **oder** die Beschreibung nicht mitgepflegt (etwa beim Hochladen von Hand statt über `npm run publish-apk`), meldet die App korrekt „kein Update" — obwohl ein neues APK bereitliegt. - **Teilen per Link (Web + Android)** — ein Song oder Album lässt sich per `https://…?song=` an jemanden auf derselben Instanz schicken. Der Link trägt **nur die Id, keine Zugangsdaten**: der Empfänger sieht den Inhalt mit seinem eigenen Navidrome-Konto. Web teilt über `navigator.share()` und fällt sonst auf die Zwischenablage zurück; beim Öffnen wird das Album angezeigt, der Song gestartet und der Parameter aus der Adresszeile entfernt, damit ein Neuladen nicht erneut losspielt. Als installierte PWA landet der Link dank `launch_handler` im bereits offenen Fenster. Android teilt über den Systemdialog (`ACTION_SEND`) und empfängt Links per App-Link-Intent-Filter; der Host kommt beim Bauen aus `-PnovasonicHost=…`, die Web-Adresse für ausgehende Links aus derselben Angabe (seit 1.2, vorher eine eigene Einstellung). Für das Öffnen ohne Nachfrage muss der SHA-256 des Release-Schlüssels in `public/.well-known/assetlinks.json` stehen — Anleitung in `docs/share-links.md`. Ohne diesen Schritt öffnet der Link die Web-App im Browser. - **Android: Songs von der Festplatte löschen** — im Song-Kontextmenü, unten abgesetzt und rot, mit derselben Sicherheitsabfrage wie im Web. Nutzt denselben Backend-Endpunkt (`/api/delete-song`) und erscheint nur, wenn ein Backend konfiguriert ist — Navidrome allein kann keine Dateien entfernen. - **Android: Playlist direkt aus dem Import anlegen** — der Importer hat jetzt wie das Web ein Häkchen „Playlist daraus anlegen" samt Namensfeld (ab zwei Titeln). Nach dem Download stößt er Navidromes Scan an, sucht jeden Titel und legt die Playlist aus den gefundenen an; nicht gefundene werden gezählt statt die Erstellung scheitern zu lassen. Zusätzlich zum Auslieferungsteil dieser Version: production **pulls pre-built images** instead of compiling on the server, the **APK download works again** (it had been broken by Gitea's own link host), and the scroll memory from 2.2 finally behaves on the Alben tab. ### Added (Auslieferung) - **Pull-based production stack** — `docker-compose.yml` now references `${REGISTRY}/novasonic-frontend|-backend|-scrobble:${TAG:-latest}` and builds nothing on the host: `docker compose pull && docker compose up -d`. Building locally from the Dockerfiles moved to `docker-compose.dev.yml` (no registry needed there). Images are pushed from the dev machine with `npm run push-images`. A `.dockerignore` keeps the frontend build context to what `vite build` needs, so `android/`, `backend/` and `.git` no longer travel into the image. ### Fixed - **APK download failed with "Keine APK hinterlegt"** — Gitea builds `browser_download_url` from its own `ROOT_URL`, which pointed at a host the backend cannot reach (a leftover from an earlier network). The API call succeeded, so the download button appeared, but fetching the asset ran into the dead address and fell back silently to a local file that does not exist. Asset and redirect URLs are now rewritten onto `GITEA_URL`; if the Gitea path still fails, a warning log and the 404 body name both sources that were tried instead of claiming no APK exists. - **Alben tab jumped back to the top on Back (Android)** — `LaunchedEffect(state.albumSort)` scrolled to item 0. That effect runs on first composition too, not only on a sort change, so returning from an album discarded the position the ViewModel had correctly kept. Scrolling to the top now happens in `LibraryViewModel.setAlbumSort()`, on a real sort change only. --- ## [2.2] — 2026-08-22 A cleanup-and-parity release: the **Song Importer moves into the Global Search field** (one field, like Android), concert events become **reliable across web/Android**, several Android gaps close, and **YouTube downloads are recovered** after they broke on two independent fronts. ### Added - **In-app updater (Android)** — on launch the app asks the backend (`/api/apk/info`) whether a newer **versionCode** than this build is available and, if so, offers "Laden & installieren": it downloads the APK from `/api/apk` (which proxies the fixed Gitea release) into a single cache file, then hands it to the system installer. No APK pile-up (the cache file is wiped before each download); an update keeps app data / offline downloads (same package + signature = in-place install). The user still confirms the system install and grants "unknown apps" once — Android requires that for sideloading. `publish-apk` now **auto-increments `versionCode`** each build and writes it into the release so the check is a reliable integer compare (commit the bumped `build.gradle.kts`). - **Android APK download in Settings (Web)** — a "🤖 Android App" section in Settings offers the latest APK so co-users self-serve instead of the maintainer hand-delivering builds. The button appears only when an APK is available and shows version/size/date. The backend serves it (`GET /api/apk`, metadata at `/api/apk/info`) from one of two sources, in order: 1. **A fixed Gitea release** (tag `apk-latest`) whose `.apk` asset you overwrite each build — constant ~30 MB, uploadable from anywhere. The backend fetches it **server-side** over the LAN (`GITEA_URL`/`GITEA_REPO`/`GITEA_TOKEN`/`GITEA_APK_TAG` env), so Gitea can stay internal-only and the token never reaches the browser. Release *assets* live in Gitea's attachment store, not the git history, so `clone`/`pull` stay small. 2. **Local fallback** — `docker cp app-release.apk novasonic-backend:/data/novasonic.apk` (optional `/data/novasonic.version.txt`). The Android *source* is already in the repo; only build artifacts stay out of git. - **Unified Global Search field (Web)** — the separate Song-Importer page/sidebar entry is gone. The one search field now auto-detects: a **link** (Spotify/SoundCloud/YouTube) opens the importer preview, plain **text** runs a Deezer search. Cleaner left nav, same behaviour as Android. - **30s preview in the importer (Android)** — each resolved track now has a ▶/⏹ button that plays its Deezer/Spotify preview clip through the same app-level preview bar as Explore (library playback pauses; the row highlights while playing). Parity with the web importer. - **Scroll & search position remembered on Back (Web + Android)** — going into an album/artist and back now returns you to the same scroll spot with your typed query still there, instead of jumping to the top. Web: each history entry's scroll (and the open local/header search) is snapshotted and restored on Back/Forward; a genuine new navigation still starts at the top. Android: the Alben / Künstler lists and the global/local search-results scroll are hoisted into their ViewModels, so they survive a drill-in/back for as long as you're in that tab. - **Partial album — Play / Shuffle + track menu (Android)** — an incomplete album's downloaded tracks now have **Abspielen** and **Zufall** buttons and a per-row 3-dot menu, so you can still play/shuffle what you have while the rest downloads. - **Swipe between tabs (Android)** — a left/right swipe now switches tabs in **Now Playing** (Player ↔ Lyrics ↔ Queue) and in the **Bibliothek** (Playlists ↔ Alben ↔ Künstler ↔ Offline), two-way synced with the tab bar, matching the existing Erkunden swipe. On Now Playing the cover's own swipe (skip track) still wins, since it's a child gesture. ### Fixed - **Concert events reliability (Web + Android)** — Ticketmaster's free `keyword` search intermittently returns a partial subset (or none), so back-to-back calls disagreed (web showed 7, a later live call got only 2, Android sometimes showed 0). The web had a private 30-min cache masking this; the cache now lives **on the backend** and **prefers the larger recent result**, so a flaky response can't shrink a good list and every client stays consistent (1 h TTL self-heals). - **Concert date/time (Web + Android)** — shows the **venue-local** time (Ticketmaster's `localDate`/ `localTime`) instead of the UTC instant, so a 20:00 show no longer displayed as 08:00. Android parses the naive local datetime (was crashing on zone-less values → missing times). - **Concert rows uneven (Android)** — rebuilt each row with a fixed-width calendar-style date block (day big, month/year + time small), so every row is the same height and the divider/venue align. - **Concert load failures were silent (Android)** — the fetch swallowed errors and showed an empty list. Errors now surface ("Konzerte konnten nicht geladen werden" + reason), with a request timeout and non-2xx treated as a real failure. - **Web importer preview layout** — the SoundCloud track row was missing its grid number-cell, so the 4-column `.gtrack-item` grid squeezed the cover into 28px and the title into 52px (title wrapped one character per line). Added the missing cell. - **Header search dropdown covered the sidebar (Web)** — `#header-search-overlay` was `position:fixed; left:0; right:0` (full viewport), overlapping the left sidebar. It's now anchored to the search bar (`position:absolute` inside the wrap), so it drops down at the bar's width and the sidebar stays visible. - **Android artist albums incomplete** — `lookupArtistAlbums` filtered to `record_type == "album"`, dropping EPs and singles that Deezer (and the web app) show. Now returns all, sorted album → EP → single. - **Japanese/CJK albums vanished from the artist discography (Web + Android)** — the album-name normaliser stripped everything except `a-z0-9`, so any Japanese/CJK title collapsed to an empty string. All of an artist's JP singles then normalised identically (`""`), so they de-duplicated into one entry and even "matched" each other — e.g. inabakumori (稲葉曇) showed only 2 of 9 releases, the rest hidden behind one album. The normaliser now keeps Unicode letters/numbers (`\p{L}\p{N}`), so JP titles stay distinct and the full discography (In Library / Unvollständig / Noch nicht heruntergeladen) shows correctly. Same fix applied to the partial-album track match and the Deezer-artist name match. - **Android "Unvollständig" (partial albums) never showed** — Deezer's `/artist/{id}/albums` omits `nb_tracks`, so an owned album's real total was unknown (0) and it never got flagged incomplete; every partly-downloaded album looked complete. The artist VM now resolves the real track count for **matched (owned)** albums via a per-album lookup (like web), so the "Unvollständig" section — with the downloaded/total badge and the downloaded-vs-missing split screen — populates correctly on both the library and Deezer artist pages. - **"Falscher Künstler?" pin button minimised (Web + Android)** — reduced to a small ⚙ icon-only pill in the artist hero's corner instead of a full-width labelled button. - **Downloads from search/Bibliothek were silent (Android)** — a download started from the global search / Bibliothek-Global tab fired only a transient toast and then registered no success/failure notification (only the importer and artist page did). It now goes through the same app-lifetime `NotificationStore` poll, so it notifies on completion even after you leave the screen. - **YouTube downloads (HTTP 403 + "Signature solving failed")** — two things had broken downloads: 1. Recent yt-dlp needs an **external JavaScript runtime** to solve YouTube's signature/nsig challenge — without it, formats were skipped and the media 403'd. The backend now installs **Deno**, which yt-dlp uses automatically. 2. A bug in the current yt-dlp **stable** 403'd the `android_vr` client on *every* video ([yt-dlp#17456](https://github.com/yt-dlp/yt-dlp/issues/17456), fixed in nightly). The backend now pulls yt-dlp from the **nightly** channel (`pip install --pre 'yt-dlp[default]'`). No cookies, PO-token service, or proxy needed — the earlier 403 that looked like an IP block was this stable bug. --- ## [2.1] — 2026-08-14 Builds on the 2.0 listening archive with a **head-to-head "Musik-Match"**, brings the **artist pin** to Android at parity with web, and fixes a batch of same-name-artist and player rough edges. ### Added - **Musik-Match (Head-to-Head) — Web + Android** — compare two users' listening taste from the shared play-log. New scrobble routes `GET /scrobble/users` (partner picker) and `GET /scrobble/compare?with=` — the match score is the **cosine of the two users' artist-play vectors** (0–100). Shows shared artists (mirrored bars), shared tracks, and each side's unique top artists. Opened from Settings ("🎧 Musik-Match"). Only aggregates are exposed, never one user's raw history. - **Artist pin on Android (parity with web)** — read AND write the shared server-side pins (`/api/artist-overrides`) that fix same-name-artist collisions. A "⚙ Falscher Künstler?/Gepinnt" pill on the artist hero opens a picker (Deezer search + a **paste-a-Deezer-link/id** route, the reliable way when `/search/artist` can't surface a low-profile same-name artist); when pinned, the page enriches straight from the pinned Deezer id (albums / top tracks / fans / image). Pins set on either platform apply on both. - **Swipe between Erkunden sub-tabs (Android)** — a left/right swipe switches Bibliothek ↔ Global, two-way synced with the tab bar (HorizontalPager). - **Similar Artists with profile pictures (Web)** — the artist page's "Ähnliche Künstler" now render as avatar chips, matching the Android app (previously plain text). ### Fixed - **A failed direct-link download reported success** — the direct-URL path returned `success` even when no audio was produced (e.g. a YouTube 403), so the job showed "Fertig!" and the UI (web + Android) said the track was added. It now reports failure with a clear message. - **Artist pin — search & similar artists (Web + Android)** — a pinned artist now shows the correct Deezer photo in library search (not the wrong same-name person's), and its "Similar Artists" use Deezer's **related** for the pinned artist instead of Last.fm's list (which was keyed to the merged/wrong Navidrome artist and went "weird"). - **Android player — seek broke after seamless track transitions** — skipping through already-buffered tracks left `durationMs` stuck at 0 (the player stayed READY, so `syncState()` wasn't re-called and `ctrl.duration` was still `TIME_UNSET`), so the seek bar snapped playback to the start (or to a stale offset). `durationMs` now self-heals via the ticker + a fallback to the song's known duration. No app restart needed. - **Erkunden swipe polish (Android)** — pre-compose the neighbour page so the first swipe doesn't hitch, drive the tab indicator off the pager so it moves with the swipe, and keep the Top-Alben charts anchored to the top (were vertically centered). - **Web** — the pin picker crashed (`esc()` on a numeric Deezer id) and the pin button moved to the banner's top-right; pinning by a pasted Deezer link/id was added. - **Android** — Settings spacing: the gap now sits below "Musik-Match", grouping it with "Meine Statistiken". --- ## [2.0] — 2026-08-07 Big change: **NovaSonic gets its own self-hosted, multi-user listening archive.** Navidrome only stores a play *count* and the *last* play date — no timestamped history — so per-day/month/year stats and a yearly "Wrapped" were impossible. Koito and Spindle (the off-the-shelf options) are effectively single-user. Since NovaSonic *is* the player, it now records every counted play into an **isolated scrobble service** with its own database, and shows the results in a **statistics view on web and Android**. This runs **in addition to** the existing Navidrome/Last.fm scrobble — it never replaces it. ### Added - **New `novasonic-scrobble` service** — a small, isolated container (`node:22-slim` + built-in `node:sqlite`, own volume) that owns the play-log. Kept separate from the download backend on purpose: the backend is heavy (yt-dlp/ffmpeg) and restarts regularly, while an archive needs uptime + data integrity. Routes: - `POST /scrobble/plays` — batch ingest, **idempotent by uuid** (safe offline-retry) - `GET /scrobble/history` — raw chronological log (the archive) - `GET /scrobble/stats` — totals, top artists/albums/tracks, day/month/year series, 24h listening clock (on-the-fly `GROUP BY strftime`, local-tz bucketing) - `DELETE /scrobble/plays?user=` — purge one user's rows (test cleanup) - `GET /scrobble/health` — liveness - **Multi-user**: every request carries the same Subsonic token creds the app already uses; the service verifies them via Navidrome `ping.view` (cached), so the stored `user` is real, not spoofable. No new login. - **Play capture (Web + Android)** — fires at the *existing* counted-play moment (respects the user's `scrobbleMode`, consistent with Last.fm), with an **offline-safe queue** (localStorage on web, a JSON file on Android) flushed as an idempotent batch on start and when connectivity returns. - **Statistics view (Web + Android)** — opened from Settings ("📊 Meine Statistiken"): totals tiles (**Plays** + listening time), a day/month/year play series, top artists/tracks/albums, and a 24h listening clock, with period tabs (Alles / Dieses Jahr / Letzte 30 Tage). - **Cover art in stats (Web + Android)** — top artists show a round artist image, tracks/albums a square cover, resolved client-side from Navidrome (the scrobble service stays cover-agnostic). - **`SCROBBLE_URL`** environment variable (frontend) + a **Scrobble URL** field with a test button in Settings on both platforms. ### Changed - **Backend** — base image bumped `node:20-slim` → **`node:22-slim`** (Node 20 reaches end-of-life April 2026; enables the built-in `node:sqlite`). ### Fixed - **Downloads** — remix / live / acoustic / instrumental tracks were skipped as duplicates of the base track: duplicate detection stripped *all* bracketed content, collapsing "Song (Remix)" → "Song". Version-marker brackets are now preserved, so a distinct recording downloads (noise brackets like `(feat. X)` / `(Official Video)` are still stripped). Verified against Deezer + unit-tested both directions. - **UI** — dragging the mouse near a field edge (Global search, importer) started selecting the surrounding titles/labels instead of feeling like an app. `user-select` is now disabled on the `#app` shell; real text fields and lyrics stay selectable. ### Notes - The scrobble subdomain **must be excluded from any SSO / forward-auth gateway** (Authelia/Authentik/etc.), exactly like the download backend — it self-authenticates via `ping.view`, and a gateway in front 302-redirects the browser's API calls to the login portal (breaks `fetch`). - Deploy: add `SCROBBLE_URL` to `.env`, `docker compose up -d novasonic-scrobble`, and route port **3710** through your reverse proxy. - Planned next: a yearly **Wrapped** (`/scrobble/wrapped` + story page) and an optional **ListenBrainz relay** — not in this release. --- ## [1.3] — 2026-07-31 Big change: **the entire metadata/search stack moved from Apple's iTunes to Deezer** — on web, backend downloads *and* Android. Apple's iTunes `/search` returns **403 from server/datacenter IPs**, which had broken global search, charts, artist/album/single downloads and the metadata-editor autofill in production. Deezer is free, needs no key, allows more requests, and returns explicit versions + real artist images + track durations. Web and Android now share the **same** source, so an artist resolves to one consistent name everywhere. ### Changed - **Web + Android + Backend** — Global search, album/artist lookups, artist top tracks and the "Neu & Beliebt" charts all run on **Deezer** (`api.deezer.com`). Results are mapped to the existing item shape, so the UI is unchanged; artist cards now show real photos (iTunes artist entities had none) - **Backend (download)** — Single-track, album and **artist** downloads, plus the metadata-editor autofill, now enrich from Deezer: - `backend/lib/deezer.js` — new module: `lookupTrackMeta`, `fetchAlbumTracks`, `searchAlbumTracks`, `fetchArtistAlbums` - Album/artist `album_artist` still comes from the album-level artist (Deezer `album.artist`), keeping soundtracks from splitting - Genre comes from the Deezer album (one album-level genre); per-track artist is preserved for mixed-artist albums - The autofill endpoint moved `/api/itunes/lookup` → **`/api/meta/lookup`** - **Web + Android** — All user-visible "iTunes" / "Apple Music" labels are now "Deezer" (search badge, charts badge, preview bar, editor autofill button, artist "populär" heading) - **Backend** — Removed the dead iTunes `/search` proxy and the Apple charts RSS proxy; the Deezer proxy (`/api/deezer/*`) now also serves charts and the editor lookup - **Web** — Renamed the leftover `itunes*` frontend identifiers to `deezer*` (`itunesSearch` → `deezerSearch`, `itunesLookup*` → `deezerLookup*`, `itunesData`/`itunesArtistId` → `deezerData`/`deezerArtistId`, the `itunes-album-detail` route/DOM ids, the `badge-itunes` CSS class, …). Purely internal — no behaviour change; only the factual "Apple's iTunes /search 403" comments keep the old name ### Added - **Web + Android** — **Same-name artist disambiguation**: Deezer's artist search isn't ranked by popularity, so a name collision (5 artists named "Joji") returned a niche one first — the artist page then loaded the wrong discography and owned songs didn't show as downloaded. Artists are now sorted by Deezer fan count, and on a local artist page (web) with a real collision the pick is tie-broken by **song overlap** with your library (each artist has different songs). The **fan count is shown** on search artist cards and on both the Deezer *and* local Navidrome artist pages (web + Android) - **Android** — **Global 30s-preview control bar**: a preview started on an album/artist/Explore screen used to keep playing with no way to stop it once you navigated away (the local artist page had its *own* uncontrollable preview too). All previews now run through one app-level player with a bar (above the mini player, on every screen) to pause/resume/stop it; starting real playback cancels the preview - **Backend** — Unit tests for the Deezer mapping/matching logic (`backend/test/deezer.test.js`, run with `npm test`), incl. per-track numbering, mixed-artist album-artist stability, explicit flags and the Japanese-title match guard. A live smoke test runs with `RUN_LIVE=1` ### Fixed - **Android** — After **tabbing out and back into the app** with NowPlaying (or an ABS player) open, pressing back first popped the NavHost **invisibly to Home** and only *then* dismissed the overlay — the focus was lost. Returning from the background re-registers the NavController's own back callback on top of the dispatcher, stealing back-priority from the overlays. The overlay back-callbacks are now re-asserted on `ON_RESUME` (and on navigation), so back closes the overlay first again - **Web + Android** — A finished-download **notification led to the wrong song**: it resolved the album by name via `search3`, and when the exact artist match wasn't found (e.g. the new album isn't scanned yet), it fell back to a same-named album by a **different** artist (or the first result). Several artists share an album title ("I LIKE IT!"), so tapping the notification opened a completely different song. Now the artist must match when known (fuzzy for multi-artist/album-artist drift); otherwise it shows "wird evtl. gerade gescannt" instead of opening a wrong album - **Web** — Downloading a not-yet-owned album from an artist page searched YouTube for "**Unknown Artist** <album>" and failed. Deezer's `/artist/{id}/albums` omits the `artist` object, so the discography cards had an empty artist name → the modal fell back to the literal `"Unknown Artist"`. Fixed by backfilling the known artist name in `deezerLookupArtistAlbums` and defaulting the download modal to **Album** for album cards (was always **Track**) - **Web + Android** — Owned albums were wrongly shown as **"Unvollständig 1 / 0 Tracks"** on the artist page. Deezer's `/artist/{id}/albums` list omits `nb_tracks`, so the total read as 0 and every matched album looked incomplete. Web now resolves the real track count from the Deezer album endpoint for **matched (owned) albums only** and treats an unknown total as complete; Android treats an unknown total as complete (never a false partial) ### Notes - Deploy: `git pull`, then rebuild the frontend (`docker compose up -d --build novasonic-frontend`) and restart the backend (`docker compose restart novasonic-backend`). Service worker bumped to `ns-v54` - Titles a user picked stay as chosen; only derived fields (album, album-artist, genre, year, artwork) follow Deezer --- ## [1.2] — 2026-07-25 Follow-up to 1.1: web history navigation, correct download tagging for mixed-artist albums, and UI polish. ### Added - **Android** — Directional bottom-nav tab transitions: the slide follows the tapped tab's position (a tab to the right slides in from the right, one to the left from the left) instead of a fixed, sometimes-random-feeling direction. Applied across all four enter/exit/pop transitions; detail-screen push/pop is unchanged - **Web** — Pressing **Enter** in the importer link field starts the import (same as the Importieren button) ### Fixed - **Web** — Browser / mouse **Forward** navigation now works (previously only Back did): `popstate` applies the target history entry (`nsIdx`) for both directions, and the back handler no longer `pushState()`s over the browser's forward stack. In-app back buttons route through `history.back()` so the browser and app stacks stay in sync - **Web** — Deleting a song or playlist refreshes the current view immediately (drops the cached list) instead of leaving the removed item visible for up to ~3 minutes - **Web** — Album detail header showed the **first track's** artist (e.g. "TWICE" on a soundtrack) instead of the album artist; it now reads the album's own album-artist fields from `getAlbum` (matches the Android app) - **Web** — Metadata autofill (iTunes / MusicBrainz) normalizes the trailing `" & "` in a multi-artist credit ("A, B & C" → "A; B; C"), but only when the value is already a list, so band names like "Simon & Garfunkel" stay intact - **Android** — Erkunden (Apple) overlay back no longer *sometimes* jumped to Home: the root-level back callback is re-asserted on every navigation, so it can't lose back-priority to the NavHost after an album drill-down or tab switch - **Backend (download)** — Mixed-artist albums search **per track**: the YTM/YouTube query and channel filter use each track's own performer, so soundtrack tracks by other artists (TWICE's "Strategy", MeloMance's "Love, Maybe") are found instead of failing with "Nicht eindeutig gefunden" - **Backend (download)** — `album_artist` is derived from iTunes' album-level `collectionArtistName` (identical across the album) instead of the request's artist — which could be the lead single's performer, so a whole KPop soundtrack was tagged `album_artist = TWICE`. Applies to album and artist downloads; each track still gets its own per-track artist tag - **Backend (download)** — Official audio is matched by yt-dlp's `artist` metadata, not just the channel name, so uploads on label channels (BANGTANTV, HYBE, "… - Topic") win over fan reuploads — e.g. BTS "Pied Piper" now grabs the official track. (`channel_is_verified` was rejected as a signal: lyric videos and fan MVs are also on verified channels) - **Backend (download)** — Western artists are no longer tagged with the Japanese store's katakana album-artist ("DJスネイク" for DJ Snake): the JP-first iTunes lookup now keeps Japanese names only for Japanese songs and uses the US/English names otherwise (affects the `album_artist` tag and the on-disk folder) - **Backend (download)** — Release year is back-filled from iTunes on single-track downloads where the source omitted it (the year tag was previously left empty) ### Changed - **Web + Android** — Playing a song from search results queues only that song; Radio mode (if on) fills the rest — closer to Spotify. Album / playlist play still queues the whole list - **Android** — NowPlaying track info is ordered title → album → artists, matching the web player - **Web** — NowPlaying favorite star is vertically centered with the whole title / album / artist block instead of pinned to the title line --- ## [1.1] — 2026-07-19 Polish release: Android navigation and offline resilience, download accuracy for artists with stylised names, and a fix for the lyrics outage caused by lrclib rate-limiting. ### Added - **Android** — Mini player is now visible on every screen (album, artist, playlist detail), not only the top-level tabs; the tab bar stays top-level-only - **Android** — Song search results (local library) got the 3-dot context menu: play, insert next / queue, add to playlist, favorite, go to album, song info, download - **Android** — Artist images in the Apple-Music (Erkunden) search: iTunes artist entities carry no artwork at all, so each artist now borrows the cover of one of their albums; a person icon replaces the empty grey circle where none is found - **Web + Android** — Playlist cover art is shown in the "add to playlist" picker, so playlists are identifiable at a glance - **Web** — "Now playing" scrobble is sent at track start (`scrobble?submission=false`), so Navidrome shows the current track while it plays instead of only on completion ### Fixed - **Web + Android** — Lyrics stopped loading entirely: a burst of requests (prefetch + the 4-request cascade + fast skipping) got the client IP throttled by lrclib, so every lookup hung until it timed out. Fixed with per-song in-flight de-duplication, a prefetch debounced to 1.5 s after track start, a descriptive User-Agent, and a longer 8 s timeout - **Android** — App lag and stutter while scrolling and switching tabs: `coverArtUrl()` and `streamUrl()` built a fresh Ktor `HttpClient` for every cover-art request and never closed it — heavy allocation and GC churn. Both now reuse the cached client - **Android** — Star / favorite gave no visual feedback: a global `StarState` now flips the icon optimistically the moment it's tapped (everywhere it appears) and rolls back if the API call fails - **Android** — Back from an Erkunden (Apple) artist or album jumped straight to Home. Those screens are in-screen overlays on the `erkunden` destination and their `BackHandler` lost back priority to the NavHost, which popped the whole tab. Back is handled at the app root now: album → artist, artist → search - **Android** — The Erkunden artist's "show all albums" view was lost when drilling into an album and back (it lived in local `remember` state); the open section now lives in the ViewModel and survives both route and overlay round-trips - **Android** — Album back-navigation from a local artist returned to the collapsed artist page instead of the "all albums" view it was opened from - **Android** — The mini player sat behind the phone's gesture/navigation bar on detail screens, where there is no tab bar to absorb the system inset - **Android** — The download sheet for new songs and the song context sheet opened only half-expanded, hiding the download button behind the system navigation bar until dragged up; both open fully now - **Android** — The Offline tab was blocked by the online library's loading spinner, so downloads were unreachable while the library loaded or the server was unreachable. It reads from the local Room DB and now loads independently - **Backend (download)** — Artist lookups ignored leading aesthetic punctuation (e.g. `.diedlonely`), so the iTunes match missed, fell through to a title-only match, and grabbed an unrelated same-titled track — whose wrong duration then also misled the YouTube search filter. Leading non-alphanumeric characters are trimmed before lookup - **Backend (metadata)** — The metadata editor showed "[Unknown Album]" where the library shows "Singles": tracks downloaded without an album had no album tag written at all. The album tag now always gets a value (defaulting to `Singles`), and file resolution falls back to `findExistingTrack` for stale or rebased paths - **Web** — Now Playing showed a stale cover after rapid skips - **Web** — Home row scroll arrows caused layout thrash while scrolling: arrow visibility is a rAF-throttled class toggle now, with `getBoundingClientRect()` only on resize / content change ### Changed - **Android** — Favorite is a heart everywhere; the Now Playing screen still used a star - **Android** — Playlist detail actions are uniform, equal-width and icon-only (play / shuffle / edit / download). Since the offline-download button was added, four buttons shared one row and the Play/Shuffle labels were being clipped away - **Android** — The "add to playlist" search field matches the styling of the other search bars - **Web** — Track switching uses a gapless hand-off between two `