Files
NovaSonic/CHANGELOG.md
T

103 KiB
Raw Blame History

Changelog

All notable changes to NovaSonic are documented here. Format follows Keep a Changelog. 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 ✅ ✅
Künstler folgen / Album merken ✅ ✅
Neu von deinen Künstlern ✅ ✅
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.) ✅ ✅

[3.1] — 2026-09-22

Android: unverändert bei 1.15 (versionCode 18) — beide Änderungen betreffen nur das Web.

Changed

  • Der Rollbalken ist greifbar (Web) — er war 4 px breit. Sichtbar reichte das, mit der Maus war er kaum zu treffen: in einer langen Playlist kam man nicht ans Ende, ohne zu scrollen. Jetzt 12 px Greiffläche mit einem schmalen sichtbaren Balken darin (durchsichtiger Rand plus background-clip:content-box), heller beim Überfahren und beim Ziehen — wie in Spotify.
    • min-height:44px hält ihn auch bei sehr langen Listen greifbar; bei 1900 Titeln schrumpfte er sonst auf wenige Pixel, also genau dort, wo man ihn am nötigsten hat.
    • Firefox bekommt nur die Farbe. Dessen Standardbalken ist ohnehin breit genug; ein scrollbar-width:thin hätte ihn wieder verschmälert. Die Ausnahmen, die bewusst keinen Balken zeigen (waagerechte Kartenreihen, Lyrics, Chips), bleiben unangetastet.
  • „Neu von deinen Künstlern" sitzt jetzt in den Favoriten (Web) — es stand links unter Entdecken, in Android dagegen als Knopf „✨ Neu" in den Favoriten. Zwei Plattformen, zwei Orte für dieselbe Sache. Das Web folgt jetzt Android: der Eintrag in der Seitenleiste ist weg, in der Kopfzeile der Favoriten steht der Knopf — dort, wo auch die gefolgten Künstler stehen.
    • Der Knopf erscheint nur, wenn man überhaupt jemandem folgt, genau wie in Android.
    • Die Seite hat dafür einen Zurück-Knopf bekommen (vorher unnötig, weil sie über die Seitenleiste erreichbar war) und zählt für die Seitenleiste jetzt zur Bibliothek statt zu Entdecken.

[3.0] — 2026-09-19

Android: 1.13 (versionCode 16) → 1.15 (versionCode 18). 1.13 brachte Folgen, Zeitleiste und den Gefolgt-Filter, 1.14 die begrenzte Reihe in den Favoriten, 1.15 die Knopfleiste im Album.

Erster Sprung auf eine neue Hauptversion seit 2.0: „Künstlern folgen" ist kein weiteres Fenster, sondern ein neuer Umgang mit der Bibliothek — die App erzählt jetzt, was draußen neu ist, statt nur zu zeigen, was drinsteht.

Added

  • Künstlern folgen und Alben merken (Web + Android) — Grundlage für die geplanten Meldungen zu neuen Veröffentlichungen und Konzerten. „Folgen" ist kein eigener Speicher, sondern Navidromes Stern auf dem Künstler: star&id=<ndArtistId>. Damit ist der Zustand sofort auf allen Geräten da, überlebt Neuinstallationen und taucht sogar in Navidromes eigener Oberfläche auf.

    • Möglich ist das, weil Navidrome id, albumId und artistId beim Markieren in eine Liste wirft und gleich behandelt (media_annotation.go) — unser vorhandener Aufruf trug also ohne Änderung. Den Zustand liefert starred aus getArtist/getAlbum.
    • Das Herz sitzt im Banner des Künstlers, direkt neben dem ⚙ für den Pin — auf beiden Plattformen gleich. Die Aktionsleiste darunter ist „was ich jetzt tue" (abspielen, mischen, Konzerte); Folgen ist dagegen ein Zustand und gehört an den Künstler selbst. Beim Album bleibt das Herz in der Aktionsleiste, dort gibt es kein Banner.
    • Übersicht, wem man folgt: die Favoritenseite zeigt jetzt Abschnitte Künstler, denen du folgst, Gemerkte Alben und Songs. Das kostet keinen zusätzlichen Aufruf — getStarred2 lieferte Künstler und Alben die ganze Zeit im selben Paket mit, wir haben sie nur weggeworfen.
    • Beide Plattformen schalten optimistisch um und fallen bei einem Fehlschlag zurück — Android über den vorhandenen StarState, der ohnehin über IDs arbeitet und deshalb unverändert taugt.
    • Wer ohne Künstler-Pin folgt, bekommt einen Hinweis: ohne Pinnung läuft die Deezer-Zuordnung über den Namen, und bei gleichnamigen Künstlern kämen dauerhaft die Neuheiten des Falschen.
  • Filter „❤ Gefolgt" auf der Künstlerseite (Web) — die Künstlerseite hat jetzt zwei Chips (Alle / ❤ Gefolgt (n)) und ein Suchfeld. Damit ist die Liste, wem man folgt, dort, wo man Künstler ohnehin sucht — statt nur als waagerechte Reihe unter den Favoriten. getArtists liefert kein starred mit, die Markierungen kommen deshalb aus getStarred2 und werden 30 s gehalten, damit das Umschalten der Chips nicht jedes Mal nachfragt. Ein Herz-Klick verwirft diesen Zwischen- speicher sofort — sonst zeigte der Filter nach dem Entfolgen noch eine halbe Minute den alten Stand.

  • Die Reihe der gefolgten Künstler ist begrenzt, mit „Alle anzeigen" (Web + Android) — vorher landeten alle gefolgten Künstler in der waagerechten Reihe der Favoriten. Bei 200 Gefolgten sind das 200 Karten samt Bildern, von denen man vier sieht — und zum Durchsehen taugt eine endlose Reihe ohnehin nicht. Am Ende steht jetzt eine Karte „Alle anzeigen (+n weitere)", die in die Künstlerliste mit aktivem Gefolgt-Filter springt.

    • Im Web richtet sich die Anzahl nach der Fensterbreite statt nach einer festen Zahl: es wird gerechnet, wie viele Karten nebeneinander passen (Kartenbreite plus Abstand), minus eine, damit „Alle anzeigen" nicht angeschnitten am Rand klebt. Untergrenze zwei. Bei einer Größenänderung wird neu aufgeteilt — sonst bliebe die Zahl auf dem Stand des ersten Aufbaus.
    • Android zeigt zehn und dann den Knopf; die LazyRow baut ohnehin nur Sichtbares, dort ging es allein um die Durchsehbarkeit. Weil das LibraryViewModel am Navigationseintrag hängt und einen Zielwechsel nicht überlebt, reicht LibraryIntent die Absicht einmalig durch.
  • Die Knopfleiste im Album sieht aus wie die der Playlist (Android) — im Album trugen Play und Shuffle noch Beschriftungen und teilten sich die Breite, während Herz und Download auf feste 46 dp gesetzt waren. Vier Knöpfe in einer Reihe lassen dafür keinen Platz: die Schrift wurde abgeschnitten, und die Reihe war ungleich geteilt. Jetzt vier gleich breite reine Symbolknöpfe wie im Playlist-Detail — Play gefüllt als Hauptaktion, der Rest umrandet und erst accent gefärbt, wenn er aktiv ist (gemerkt, vollständig geladen). Die Symbole tragen jetzt Beschriftungen für die Vorlesefunktion, vorher stand dort bei Play und Shuffle null.

  • Zeitfenster-Chips der Zeitleiste beginnen mit 90 Tagen (Android) — das ist die Vorgabe, also steht der aktive Chip jetzt vorn statt in der Mitte. Web war schon so.

  • Filter „❤ Gefolgt" im Künstler-Reiter (Android) — zwei Chips über der Liste, gleiche Stelle wie im Web. Bewusst ein Filter und kein eigener Reiter: die Bibliotheksleiste trägt schon Playlists, Alben, Künstler und Offline. Die markierten IDs werden bei jedem Einschalten frisch geholt, damit ein eben entfolgter Künstler nicht noch dasteht.

  • Zeitleiste „Neu von deinen Künstlern" (Android) — erreichbar über „✨ Neu" in den Favoriten, dort, wo auch die gefolgten Künstler stehen. Gleiche Darstellung wie im Web: nach Monat gruppiert, neueste zuerst, Vorhandenes mit Haken statt Download-Knopf, Künstlername anklickbar. Der Download-Knopf hängt am bekannten Weg samt Benachrichtigung über den Fortschritt.

  • Zeitleiste „Neu von deinen Künstlern" (Web) — eigener Punkt in der Seitenleiste. Zeigt die jüngsten Veröffentlichungen aller Künstler, denen du folgst: nach Monat gruppiert, neueste zuerst, pro Eintrag Cover, Künstler, Art (Album/Single/EP), Titelzahl und Datum. Umschaltbar auf 30 Tage, 90 Tage oder ein Jahr.

    • Was du schon hast, ist markiert. Verglichen wird über Künstler + Albumname (akzent- und zeichenunabhängig) — Deezers Album-Id hat mit Navidromes nichts zu tun. Alles andere bekommt direkt einen Laden-Knopf, der den vertrauten Download-Dialog öffnet.
    • Neuer Backend-Endpunkt POST /api/artist-releases (backend/lib/releases.js). Die Deezer-Zuordnung passiert dort, weil die Künstler-Pins ohnehin serverseitig liegen: der Client schickt nur seine Navidrome-Künstler und muss über Deezer nichts wissen. Gepinnte Künstler nutzen die feste ID, ungepinnte werden über den Namen gesucht — sortiert nach Fan-Zahl, weil Deezers Suche nicht nach Bekanntheit ordnet. Künstler ohne Treffer kommen als ohneZuordnung zurück und die Oberfläche rät zum Pinnen, statt still die Neuheiten des Falschen anzuzeigen.
    • Der Cache liegt im Backend, nicht im Client. Bei 30 gefolgten Künstlern wären das sonst 30 Deezer-Aufrufe pro Gerät und App-Start. 6 h für Alben, 24 h für die Namenszuordnung, höchstens vier Anfragen gleichzeitig; bei einem Deezer-Fehler wird der abgelaufene Stand ausgeliefert statt einer leeren Liste.

Fixed

  • Favoriten überleben ein Umbenennen (Web) — Umbenennen und Verschieben geben dem Song beim nächsten Scan eine neue Navidrome-ID. Für Playlists wurde die Zugehörigkeit längst umgehängt, für den Stern nicht: wer einen markierten Song bearbeitete, verlor die Markierung stillschweigend. Der Zustand wird jetzt vor dem Schreiben gelesen (starred aus getSong) und nach dem Wiederfinden auf die neue ID gesetzt. Schlägt das fehl, steht es als Warnung im Fenster statt unbemerkt zu verschwinden. Grenze: der Stern liegt in Navidrome pro Nutzer. Übertragen lässt sich nur der des bearbeitenden Nutzers — fremde Markierungen sind über die Subsonic-API weder lesbar noch setzbar.

  • Dubletten lassen sich im Editor selbst auflösen (Web) — wollte das Speichern die Datei in den Ordner der neuen Tags verschieben und lag dort schon eine gleichnamige, blieb sie liegen und der Hinweis schickte einen auf die Konsole. Derselbe Song lag danach doppelt in der Bibliothek — genau das, was die ganze Dateizuordnung durcheinanderbringt.

    • Das Fenster stellt jetzt beide Kopien gegenüber (Pfad, Künstler, Titel, Länge, Größe) und sagt, ob es dieselbe Aufnahme ist: gleiche Länge (± 2 s) und gleicher Titel-Tag. Darunter steht ein Knopf, der die überzählige löscht — mit derselben Rückfrage wie beim Song-Löschen.
    • Neuer Endpunkt POST /api/resolve-duplicate. Er nimmt Pfade entgegen und prüft deshalb strenger als das Löschen per ID: beide müssen im Musikordner liegen, Audiodateien sein und existieren, und dieselbe Länge und denselben Titel tragen. Sonst 409 und es wird nichts gelöscht. Geprüft mit echten Dateien: sechs Fälle, von „fehlende Felder" über einen Pfad außerhalb des Musikordners bis zum anderen Song mit ähnlichem Namen — nur die echte Dublette fiel.
  • Ein „/" im Künstler- oder Albumnamen führte den Metadaten-Editor auf die falsche Datei — beim Bearbeiten von „K/DA – VILLAIN" meldete das Backend, die gefundene Datei enthalte „Bad Omens – Like A Villain". Zwei Fehler trafen aufeinander:

    • Der gemeldete Pfad kann es gar nicht geben. Navidrome baut ihn aus den Tags (fakePath()): {AlbumArtist}/{Album}/{DD}-{TT} - {Titel}. Steht ein Schrägstrich im Namen, wird daraus eine zusätzliche Verzeichnisebene — „K/DA/ALL OUT/…" statt des Ordners K_DA, den unser sanitize() beim Herunterladen angelegt hat. Dasselbe bei „POP/STARS" → POP_STARS. Der Pfad zeigt also ins Leere, und die Suche fällt auf Ähnlichkeit zurück.
    • Die Ähnlichkeitssuche war zu großzügig. Sie nahm jede Datei, deren Stamm auf den Titel endet — und „bad omens - like a villain" endet nun einmal auf „villain". Getroffen wird jetzt nur noch ein vollständiges Feld des Dateinamens: der ganze Stamm, oder der Teil hinter einem „ - ". Die Endsilben-Regel ist ersatzlos weg.
    • Neu davor: der Ordner wird aus den Tags nachgebaut, genau so wie der Downloader ihn angelegt hätte (Schrägstrich → _, erster Künstler), und zuerst dort gesucht. Das ist gezielt statt geraten und macht die Ähnlichkeitssuche für diesen Fall überflüssig.
    • Dass der Fehler überhaupt sichtbar wurde, lag am Tag-Abgleich aus 2.3: er hat den Fehlgriff erkannt und abgebrochen, statt die falsche Datei zu bearbeiten.
  • Das Löschen hatte diesen Abgleich nicht — es nahm den Treffer der Ähnlichkeitssuche ohne Gegenprobe und löschte. Bei „VILLAIN" hätte es die Datei von Bad Omens erwischt, unwiderruflich und ohne Hinweis. Jetzt prüft auch /api/delete-song die Tags der gefundenen Datei gegen das, was Navidrome für diesen Song meldet, und bricht im Zweifel mit 409 ab. Als letzten Suchschritt bekommt es zusätzlich findExistingTrack — der Weg, der bei „K/DA" von sich aus richtig liegt, weil er / und _ beim Vergleich gleich behandelt.

  • Die Suchhelfer liegen jetzt in metadata.js statt im Server, damit sie geprüft werden können: sieben neue Fälle in backend/test/metadata.test.js (49 Prüfungen grün).

Changed

  • Nur noch ein Künstler im Dateinamen (Backend + Editor) — bisher stand dort die vollständige Gästeliste: 01 - K_DA,Madison Beer,i-dle,Lexie Liu,Jaira Burns,Seraphine,League of Legends - MORE.mp3. Üblich ist das nirgends — Picard, beets und Lidarr schreiben höchstens einen Künstler in den Namen —, und es hatte zwei handfeste Nachteile: sanitize() schneidet bei 100 Zeichen hart ab, also mitten im letzten Namen, und mit dem Dateisystemlimit von 255 Byte wird es bei mehrbytigen Namen eng, was Umbenennen und Verschieben scheitern lässt. Jetzt 01 - K_DA - MORE.mp3.

    • Genommen wird der erste Künstler des Titels, nicht der des Albums — so trägt ein Soundtrack-Titel im Namen weiterhin seinen eigenen Interpreten, auch wenn der Ordner am Album-Künstler hängt. Die vollständige Liste steht unverändert im artist-Tag, und nur den liest Navidrome.
    • Gilt für neue Downloads und für Dateien, die bearbeitet werden; Bestehendes bleibt liegen. Beide Namensformen stehen also eine Weile nebeneinander. Die Dublettenprüfung erkennt beide — eigener Testfall, damit die alten Dateien nicht ein zweites Mal geladen werden.
  • Künstlerbilder laden endlich zügig (Web) — im Künstlerraster lief pro Künstler ein eigener getArtistInfo2-Aufruf, nur um die Bildadresse zu erfahren. Bei 200 Künstlern sind das 200 API-Aufrufe auf einen Schlag, die der Browser zu sechst abarbeitet (mehr Verbindungen erlaubt er pro Host nicht) — und jeder davon konnte Navidrome zu einer externen Abfrage bei Last.fm zwingen. Dass manche Bilder gar nicht kamen, lag an derselben Quelle: Last.fm liefert für viele Künstler nur noch einen Platzhalter, den der Code als „kein Bild" erkennt. Dabei liefert getArtists das Bild längst mit: toArtistID3 setzt coverArt, und Navidrome löst Künstlerbilder selbst auf (ArtistArtPriority = artist.*, album/artist.*, external) und legt sie im eigenen Cache ab. Jetzt geht pro Künstler nur noch das Bild selbst über die Leitung, kein API-Aufruf davor; ist coverArt leer, hat der Künstler kein Bild und wir sparen uns den Versuch. Gemessen im Test: 0 statt 120 Zusatzaufrufe bei 120 Künstlern. Gleiches gilt jetzt für die Künstlerkarten in der lokalen Suche und die gefolgten Künstler in den Favoriten. Android war nie betroffen — dort lief es immer schon über coverArt.

  • Top Songs fragen jetzt über die Künstler-ID (Web + Android) — Navidrome 0.64 nimmt bei getTopSongs zusätzlich id. Bisher lief die Abfrage nur über den Namen, und der Namensweg sucht mit LIKE und Max: 1 (core/external/provider.go): bei zwei gleichnamigen Künstlern in der Bibliothek gewann schlicht der erste Treffer. Wir schicken jetzt id und artist — ältere Navidrome-Versionen ignorieren id, neuere fallen bei unbekannter ID von selbst auf den Namen zurück. In Erkunden bleibt es bewusst beim Namen: die dortige artistId ist Deezers, nicht Navidromes.


[2.6] — 2026-09-12

Android: 1.9 (versionCode 12) → 1.12 (versionCode 15). 1.9 brachte den Besitzer geteilter Playlists und die Offline-Korrekturen, 1.10 den Zählfehler im Downloader, 1.11 den WLAN-Schalter, 1.12 das vollständige Löschen.

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

  • Offline-Downloads löschen ließ Dateien zurück (Android) — „komplett löschen und neu laden" ist der naheliegende Ausweg, wenn eine Gruppe durcheinandergeraten ist; dafür muss Löschen aber wirklich alles erwischen. Es fehlte an drei Stellen:
    • Cover blieben liegen. Entfernt wurde nur die Audiodatei. Bei einer Playlist über viele Alben sammeln sich so hunderte JPEGs an. Jetzt werden sie mitgelöscht — aber nur, wenn kein anderer Titel dasselbe Cover mehr benutzt, denn Cover werden geteilt gespeichert.
    • Unfertige Titel behielten ihre Datei. Der Pfad kam aus der Karte der fertigen Titel; ein abgebrochener oder fehlgeschlagener Download tauchte darin nicht auf und blieb unberührt. Der Pfad kommt jetzt aus der Datenbankzeile selbst, dazu die angefangene .tmp.
    • Laufende Downloadarbeit lief weiter. Nach dem Löschen lud der Worker noch minutenlang ins Leere; jetzt wird die Arbeit der Gruppe abgebrochen.
  • Ein zu hoch gezählter Fortschritt konnte sich nie mehr korrigieren (Android) — Gruppen aus der Zeit vor 1.10 tragen womöglich einen Wert über totalTracks und gelten damit als „Vollständig": der Herunterladen-Knopf ist unantastbar, und das Fortsetzen beim Start sucht nur Gruppen unter dem Sollwert — die Gruppe fiel also durch jedes Raster. Beim App-Start wird der Fortschritt aller Gruppen jetzt einmal aus dem tatsächlichen Zustand der Titel neu berechnet (eine SQL-Anweisung), danach greifen Knopf und Fortsetzen wieder.
  • 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 <Name>, 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 Hauptkü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=<id> 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, 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 <audio> elements, making skips effectively instant (it does not fix the Windows media-tile flicker — see Known Bugs)

[1.0] — 2026-06-25

🎉 First stable release. NovaSonic is feature-complete across web (PWA) and Android: streaming, lyrics, artist pages, playlists, importer/downloads, Audiobookshelf, and — new in this release — full offline downloads on Android. Further work lands as point releases.

Added

  • Android — Offline downloads: download albums and playlists to device storage with a single tap; album download button is in the album hero row (▶ / shuffle / ↓), playlist button next to the edit toggle. Tapping again resumes / re-downloads any missing songs (already-downloaded tracks are skipped)
  • Android — Offline tab in Library (position 2, next to Playlists): lists downloaded groups with cover art, track count, and an in-progress bar; shows total MB used
  • Android — Offline detail screen: tap a group to view its songs (with cover art, like the online playlist), play / shuffle, and remove individual songs from the device via the edit toggle. Works fully offline (reads from Room, no network)
  • Android — Offline playback: tap ▶ on a fully-downloaded group to play locally; downloaded songs are also served from disk automatically when opened from any album or playlist view
  • Android — Downloaded badge: albums and playlists in the Library tabs show a ↓-in-circle badge on the cover when fully downloaded
  • Android — Duplicate-safe storage: a song shared by an album and a playlist is stored once (keyed by song ID) and reference-counted on delete — removing one group keeps the file if another still needs it
  • Web + Android — Lyrics prefetch: lyrics are fetched the moment a track starts playing (not only when the Lyrics tab is opened), so opening Lyrics is instant. The lrclib cache is warmed on every song change
  • Web (PWA) — Desktop install polish: manifest.json now ships PNG icons (192/512) for a crisp taskbar/Start-menu icon, drops the portrait lock so the window resizes freely, and adds a stable app id — installs as a standalone desktop window (no browser chrome)
  • Web — Keyboard shortcuts (desktop): Space = play/pause, ←/→ = seek 5 s, Shift+←/→ = previous/next track, ↑/↓ = volume. Ignored while typing in inputs/search fields
  • Web — Horizontal rows (Home) now have hover scroll arrows; only the scrollable direction is shown (no left arrow at the start, no right arrow at the end)

Fixed

  • Android — Download crash on Android 15 (targetSdk 35): FOREGROUND_SERVICE_TYPE_DATA_SYNC is banned for apps targeting API 35; WorkManager's expedited path used it and the resulting exception killed the app ~1.5 s after tapping download. Fixed by dropping setExpedited() and all foreground-service use from DownloadWorker — downloads run as silent background work with progress shown in the Offline tab
  • Android — Offline detail screen flashed open then immediately returned: its initial empty state (before Room emitted) was mistaken for "deleted". Now shows a brief spinner until loaded, and only leaves when the last song is actually removed
  • Android — Lyrics autoscroll: rewrote centering to use the active line's measured position in a LazyColumn instead of a fixed line-height estimate. Eliminates the stutter and the downward drift on wrapped (multi-line) lyrics — the active line now tracks smoothly and sits at a natural ~40 %-from-top reading anchor
  • Web — Home now refreshes on an explicit Home click (matching Android): the album rows re-fetch and the random row re-randomizes instead of serving the 5-minute cache. Back/forward navigation still keeps the cached state
  • Web (PWA) — Media Session metadata is now mutated in place instead of replaced with a new MediaMetadata each track (reduces churn). Note: this alone does not stop the Windows media tile from briefly closing/reopening on skip — see Known Bugs

Internal

  • Android — Offline system: Room DB (novasonic_offline.db) with downloaded_tracks, download_groups, group_track_cross_refs; OfflineRepository holds hot StateFlow maps for O(1) local-path lookups; DownloadWorker streams audio + cover art via HttpURLConnection to context.filesDir/offline/{audio|covers}/. MusicRepository.streamUrl() checks local files first, so playback uses the file:// URI on every entry point

[0.9] — 2026-06-22

Added

  • UI — New NovaSonic app icon: 5-bar equalizer waveform with blue→purple gradient (#7eb8ff → #8b3cf7) on dark background — replaces the generic music-note placeholder
  • UI — Favicon link added to <head> with SVG + PNG fallback (icon-192.png) so browser tabs show the icon
  • UI — Android launcher icon (ic_launcher.xml) updated to match the new equalizer design using the same blue→purple gradient via aapt:attr
  • UI (web) — Sidebar logo updated from ♫ emoji to the new equalizer SVG (icon.svg)
  • nginx — /app/ alias added so reverse-proxy deployments can resolve static assets (icon, manifest) under the /app prefix without Vite base changes

Fixed

  • UI — Player bar star/favorite button now vertically centred with the full track info block (title + album + artist), not pinned to the top of the title line
  • UI — Shuffle status badge no longer shifts when the player bar is active (removed overflow:hidden from .player-info-wrap that was clipping the status row)
  • UI (mobile) — Duplicate NovaSonic logo + name removed from mobile header next to the search bar
  • Backend (download) — Duplicate detection now matches case-insensitively (DJ OKAWARI vs DJ Okawari) and across all album folders under the artist folder, comparing normalized title without track prefix or release/variant suffix — prevents re-downloading songs whose filename drifted between runs
  • Backend (download) — iTunes collectionArtistName (album artist) is now captured and passed through the download pipeline as the album_artist tag; fixes compilation albums showing "Various Artists" for some tracks and the individual track artist for others
  • Metadata editor (autofill) — iTunes button now fills all available fields: title (trackName), artist (artistName), album-artist (collectionArtistName), album, genre, year, track #, disc #, total tracks, cover
  • Metadata editor (autofill) — MusicBrainz button now fills album-artist (from release artist-credit) and disc # in addition to the existing fields

Changed

  • Backend — Japanese tracks now stored with original Japanese names in folders, filenames, and all main tags (title/artist/album/album_artist); Romaji goes only into sort tags (TITLESORT / ARTISTSORT / ALBUMARTISTSORT / ALBUMSORT) for sorting and search in Navidrome. Previously, romaji was applied to filenames and tags too, causing name drift, duplicate misses, and wrong iTunes resolutions (e.g. "Hisaishi Yuzuru" instead of "Joe Hisaishi")
  • Backend — Metadata editor: files and album/artist folders are renamed using the original name typed in the form (may be Japanese), not romaji; sort tags still receive the romaji conversion
  • Backend — fix-artist-sort script now uses the original (kanji) artist name for folder and filename; artistSort (romaji) is only written to ARTISTSORT / ALBUMARTISTSORT sort tags
  • Backend — album_artist tag in downloaded tracks now consistently uses mainArtist (the iTunes-resolved album artist) rather than the raw per-track artist. This prevents Navidrome from splitting one album into multiple when track artists vary (e.g. compilations)
  • Backend (download) — No-duration cascade now ends after step 4 (same Mittelweg philosophy as the with-duration branch): no title-only or filter-free fallback grabs that pull wrong covers (observed: Yorushika 車窓 → Piano Echoes Shasou)
  • Backend (download) — --max-filesize 30M added to all yt-dlp calls; Mittelweg/no-duration steps also cap at 2× expected duration — a 28-min video can no longer be saved as a ~52 MB mp3

Investigated (album splitting root causes)

  1. Missing album_artist tag / inconsistent value across tracks — fixed above (mainArtist + collectionArtistName)
  2. tagAlbumArtist was using rawArtist (per-track Spotify name) instead of mainArtist — fixed
  3. Album name comes from iTunes collectionName — consistent within a collection, no fix needed
  4. Year from iTunes releaseDate — album-level date, consistent within a collection
  5. MBIDs not set — still missing; not the primary splitting cause

[0.8] — 2026-06-19

Removed

  • Backend — squid.wtf (Qobuz) download backend removed; downloads now exclusively use yt-dlp. DOWNLOAD_BACKEND and SQUID_QUALITY env vars are no longer read.

Changed

  • Backend — Download cascade "Mittelweg": when a track's duration is known but no result matches the duration window, the cascade no longer falls back to blind title-only/no-filter grabs (which pulled same-titled videos by other uploaders). It now relaxes only the duration constraint while still requiring the artist channel — and if nothing matches, the job fails cleanly with "Nicht eindeutig gefunden — bitte Direktlink im Importer nutzen" instead of silently saving the wrong song
  • Backend — Duration match margin tightened from ±15% to ±10% (min 10s) to reject near-miss false positives
  • Backend — YTM search uses the web_music player client so YouTube Music "Art Track" / topic uploads are reachable (the previous android_vr client reported many as "video not available")
  • Backend — Search query uses a space between artist and title ("MINA recognize") instead of a dash, matching how the YTM search box ranks the exact track

Fixed

  • Backend — YTM search query: album name was appended for short-title tracks, causing duplicated words (e.g. "MINA - recognize recognize Single"); album is now only appended for Japanese titles, matching spotdl behaviour
  • Backend — Downloaded filename now strips the release suffix → 01 - Artist - Title.mp3 (no - Single/- EP in the filename); the album folder keeps the full album name. The embedded title tag is cleaned the same way
  • Backend — Duration metadata from the iTunes resolver now reaches the yt-dlp duration filter for all importer paths (single, album, playlist, global search), so the right-length track is preferred
  • Backend — Metadata editor / song delete: file resolution now tolerates a release-suffix mismatch between Navidrome's stored stem and the on-disk filename; the delete "file not found" error now reports the actual resolved path for diagnosis
  • Backend — Download no longer aborts the search cascade when a single result is geo-blocked or unavailable (e.g. "This video is not available") — it continues to the next candidate

[0.7] — 2026-06-18

Added

  • Android — Playlist create: "+" FAB on the Playlists tab opens a dialog to create a new playlist
  • Android — Playlist edit dialog (pencil icon): edit name, description, and upload cover — all in one place; matches web edit modal
  • Android — Playlist delete: trash icon in the playlist hero with confirmation dialog; navigates back to library on success
  • Android — Playlist song removal: edit-mode toggle in the playlist action row reveals a "−" button per song; removes via updatePlaylist.view?songIndexToRemove
  • Android — Playlist cover upload: image picker in the edit dialog uploads via Navidrome REST API (POST /api/playlist/{id}/image)
  • Android — ABS notification: current chapter name shown as the main title; book title moves to the artist line; updates automatically on chapter change without interrupting playback
  • Android — Home "Zuletzt gehört" section: recently played albums row matching the web's type=recent row
  • Android — Library Albums: sort chips (Neueste / Älteste / A-Z); Neueste uses type=newest from Navidrome (add-date order); scrolls to top on sort change
  • Android — Artist fast-scroll index bar: draggable letter strip on the right side of the Artists list with letter bubble while dragging
  • Web — Playlist edit modal: "Cover entfernen" button removed

Fixed

  • Android — In-app notification bell: notifications were never delivered after navigating away from the Importer screen (poll ran in viewModelScope, cancelled on navigation); polling now uses NotificationStore.startPoll() with an app-lifetime scope
  • Android — Home hero card: pool now matches web — union of frequent(5) ∪ recentlyPlayed(5) deduped, random pick; label is "MEISTGESPIELT" or "ZULETZT GEHÖRT" (was hardcoded "AKTUELL")
  • Android — Library Albums "Neueste": was incorrectly sorting by release year client-side; now uses type=newest from Navidrome (recently added to library)
  • Android — Artist screen crash: duplicate key in LazyColumn when Navidrome returns the same song ID twice in topSongs
  • Android — "Alle Alben anzeigen" crash: duplicate key when two albums share name + year
  • Android — "Alle Alben anzeigen": albums were left-aligned inside grid cells; now centered
  • Android — "Alle Alben anzeigen" search bar styling matches app design
  • Android — ABS notification button order: Samsung One UI mapping corrected; now shows Kapitel zurück | 30s zurück | Play/Pause | 30s vor | Kapitel vor

[0.6] — 2026-06-17

Added

  • Android — Lyrics preload on NowPlaying open; lyrics load in background while on Player tab, so switching to Lyrics tab is instant

Fixed

  • Android — Home hero card: play button now sits to the right of title/artist, not below
  • Android — ABS media notification now uses Media3's modern media card style (blurred cover art background) instead of a plain notification; identical look to the music notification
  • Android — ABS notification buttons corrected: now shows exactly Kapitel zurück | 30s zurück | Play/Pause | 30s vor | Kapitel vor in both compact and expanded view; previously showed double back-button and 30s-forward was cut off
  • Android — Lyrics autoscroll switches from fixed tween to spring animation (DampingRatioNoBouncy, StiffnessLow); rapid line changes no longer produce choppy stop-and-restart scrolling

Internal

  • Android — ABS notification rebuilt using a custom DefaultMediaNotificationProvider subclass (AbsAwareNotificationProvider) registered via setMediaNotificationProvider(); overrides getMediaButtons() to return exactly 5 session-command buttons when ABS is active, bypassing setCustomLayout() which only appends to (not replaces) the standard skip-prev/play-pause/skip-next buttons
  • Android — Removed abandoned AbsPlayerService.kt (unreferenced, unregistered, referenced missing drawable)

[0.5] — 2026-06

Added

  • Android — ABS full playback: chapter list, playback speed control, sleep timer, progress sync to ABS server
  • Android — ABS 30s skip-back / skip-forward buttons in media notification (compact + expanded view)
  • Android — ABS library search bar in Books tab (server-side search via /api/libraries/{id}/search, 400ms debounce)
  • Android — Notification sheet: shows import/download history with status icons; "Zum Album →" / "Öffnen →" button navigates to album or playlist on tap
  • Web — Notification sheet: "Zum Album →" link button after download completes
  • Android — NowPlaying lyrics: 200ms lookahead for better sync feel (matches web)

Fixed

  • Web + Android — ABS book descriptions: HTML tags (<p>, </p>) now stripped before display
  • Android — NowPlaying background: switched from darkMutedSwatch to vibrantSwatch (55% darken) for more visible color tinting
  • Android — NowPlaying top bar: fixed layout overflow with navigation labels
  • Android — Lyrics scroll: removed Phase 2 "drift" animation; now simply centers active line (matches web scrollTo({ behavior: 'smooth' }))
  • Android — ABS skip: library list skip fixed on first load

[0.4] — 2026-05

Added

  • Android — Full native app rewrite in Kotlin / Jetpack Compose (replaces React Native / Expo attempt)
  • Android — Media3 / ExoPlayer music playback with MusicService (MediaLibraryService)
  • Android — NowPlaying screen: cover art with scale animation (Apple Music style), seek bar with drag scrubbing, queue tab, lyrics tab
  • Android — Dominant color background in NowPlaying (Palette API, animated on song change)
  • Android — Multi-artist display with per-artist tap-to-navigate in NowPlaying
  • Android — Synced lyrics via LRCLIB (parallel requests, in-memory cache)
  • Android — ABS (Audiobookshelf) integration: login, library browse, book detail, chapter playback
  • Android — Android Auto support: browsable tree (Recent, Favorites, Playlists), play-all / shuffle pseudo-items
  • Android — Radio mode: auto-queues similar songs via Navidrome getSimilarSongs
  • Android — Global search (Navidrome)
  • Android — Genre browsing with play button per genre
  • Android — Concert events via Ticketmaster on artist page
  • Android — Spotify / YouTube importer (connects to backend)
  • Android — Notification sheet in-app (import/download job progress)
  • Android — Settings: Navidrome URL/credentials, ABS URL/token, theme, display name

Fixed

  • Android — Login flash on startup (credentials loaded before nav resolves)
  • Android — Album name casing normalization

[0.3] — 2025-Q4

Added

  • Web — Artist page: hero banner, iTunes + Navidrome album categorization, all-songs view
  • Web — Concert events via Ticketmaster (replaces Bandsintown); 30min server-side cache
  • Web — Playlist editing: name, description, public toggle, cover image upload
  • Web — Global search: artist cards with images, explicit badge (E)
  • Web — Romaji display for Japanese artists/albums (Navidrome sortName tag)
  • Web — iTunes album detail page (for tracks not in Navidrome)
  • Web — Mobile search bar in header (tap icon)
  • Web — Smooth progress bar scrubbing with touch drag

Fixed

  • Web — Multi-artist splitting: handles ·, /, ; separators; each name navigates to own artist profile
  • Web — Lyrics: LRCLIB fuzzy fallback, title-only search fallback, parallel requests, in-memory cache
  • Web — Lyrics scrollbar hidden, fade edges on active line
  • Web — Artwork: hi-res cover embedded; consistent album_artist tag for Navidrome grouping
  • Web — feat./ft./featuring converted to / in artist tag for correct Navidrome multi-artist split

[0.2] — 2025-Q3

Added

  • Web — Spotify / YouTube / SoundCloud importer: resolve link → yt-dlp download → optional Navidrome playlist creation
  • Web — iTunes per-track artwork lookup for playlist downloads
  • Web — Synced lyrics via LRCLIB in Now Playing overlay (200ms lookahead)
  • Web — Lyrics embedding in downloaded track metadata
  • Web — Explicit metadata tag + E badge in search
  • Web — Disc number and compilation metadata tags
  • Web — Genre metadata from iTunes primaryGenreName
  • Web — SW update banner (notify user, no force-reload)
  • Web — Duplicate detection before download (shows toast on skip)
  • Web — Player drag feedback; swipe-up player bar opens Now Playing

Fixed

  • Web — Spotify importer: multi-artist parsing, GraphQL embed format, locale URL support, public web-player token
  • Web — Playlist cover auto-refresh after upload
  • Web — Service worker: network-first strategy, does not intercept cross-origin requests, auto-reloads all PWA windows on SW update
  • Web — Bottom navigation: full-width edge-to-edge layout

[0.1] — 2025-Q2

Added

  • Web — Initial PWA music player (Vite + ES modules)
  • Web — Navidrome Subsonic API integration (albums, songs, playlists, search, cover art)
  • Web — Album and playlist browsing
  • Web — Basic player (play/pause, skip, shuffle, repeat)
  • Web — Media Session API (lock screen / notification controls)
  • Web — Service worker with offline support
  • Web — Docker setup: nginx frontend + Node.js backend + Navidrome
  • Web — Home screen: hero card (last played album), genre grid, recently added, most played, discover

Planned / Backlog

Features

  • Web + Android — Yearly Wrapped — a yearly story (/scrobble/wrapped + web story page + Android pager), built on the existing stats aggregations.
  • Web + Android — Household leaderboard (>2 users) — "who listened most this month" + top artists across all users; the broader multi-user version of the already-built 2-user Musik-Match. Needs wider cross-user reads (admin/household mode, or an aggregate endpoint exposing per-user totals only, never raw histories).
  • Backend (optional) — ListenBrainz relay — mirror counted plays to ListenBrainz.
  • Web + Android — Discover Mixes (auto-download, temporary) — themed/random mixes for discovering unknown music, Spotify-style (e.g. "Rock", "Charts", "Random Discovery"). Idea:
    • Generate a mix from a theme/genre/charts (sources: Last.fm / ListenBrainz similar + iTunes charts), then auto-download the tracks via the yt-dlp backend so they play locally.
    • Temporary by default: tracks auto-delete after a configurable time (e.g. 7 days) so the library doesn't fill up with stuff you didn't keep.
    • Keep what you like: per-track (or whole-mix) option to promote into the permanent library instead of letting it expire.
    • Goal: completely random/unfamiliar music you don't already know — like Spotify's curated/discovery playlists, but self-hosted.
    • Open questions: where downloaded-but-temporary tracks live vs. the main library (separate folder + retention job), how Navidrome should/shouldn't index them, and dedupe against tracks you already own.
  • Web + Android — Meldungen zu gefolgten Künstlern — der Rest des Künstler-folgen-Vorhabens. Folgen selbst, die Übersicht „wem folge ich" und die Zeitleiste neuer Veröffentlichungen sind seit 3.0 gebaut (siehe dort); offen sind nur noch die beiden Meldewege:
    • Glocke für neue Veröffentlichungen: die Zeitleiste muss man heute selbst aufschlagen. Die Daten liegen schon vor (POST /api/artist-releases), es fehlt der Vergleich „was ist seit deinem letzten Besuch dazugekommen" und der Eintrag in der vorhandenen Benachrichtigungsglocke (wahlweise Push).
    • Konzert-Meldungen: die vorhandene Ticketmaster-Anbindung wiederverwenden und melden, wenn ein gefolgter Künstler in der Nähe spielt. Bewusst zurückgestellt — erst die Neuheiten.
  • Android — Playlist drag-and-drop / sort order
  • Android — Artist page iTunes fallback (for artists not in Navidrome)
  • Android — Romaji / Japanese sort support
  • Web + Android — Backend settings UI rework (move, reorganize)
  • Web + Android — yt-dlp backend update / maintenance
  • Android - Andorid Auto Sync last played and queue too with Phone then, rn its not really syncing or safin what last played on Android Auto, also check if selecting a song to play on Android Auto works that song is getting played and in a playlist it will play the queue a this point then like it works already in Android and Web

Ideen / Backlog

  • (Der Punkt „Künstler und Alben liken, daraus ‚folgen' bauen" ist mit 3.0 umgesetzt und dort beschrieben.)

Known Bugs

  • Android (lyrics) — Synced-lyrics auto-scroll is not smooth: it follows playback and highlights the right line, but the list jumps to it rather than gliding. Web autoscroll is smooth. Not yet fixed.
  • Web (PWA, desktop) — The Windows media tile (SMTC) briefly closes and reopens when skipping tracks. Swapping the <audio> element's source leaves a momentary gap with no active media output, so Windows tears down and rebuilds the tile. A real fix needs one continuous source (MSE), which requires streaming an MSE-compatible format. The native Android app is unaffected. (The two-element gapless handoff added in 1.1 makes skips effectively instant but does not fix this — switching elements still ends and restarts the OS session.)
  • Backend (download) — Die Dublettenprüfung sucht in den Ordnern von Album- und Titel-Künstler und ignoriert Akzente sowie Apostrophe (2.3). Ein Rest bleibt: steht derselbe Song unter einem deutlich anderen Künstlernamen (etwa vor einer Namenskorrektur), wird er weiterhin nicht erkannt und erneut geladen.
  • Backend (Dateinamen) — NN-NN - {Titel} statt {Tracknummer} - {Künstler} - {Titel} Kein Bug — am 11.9.2026 widerlegt. Der Pfad Rising Insane/DEATHRACE/01-02 - DEATHRACE.mp3 steht so nirgends auf der Platte. Navidrome gibt über die Subsonic-API standardmäßig gar nicht den echten Pfad heraus, sondern baut einen aus den Tags zusammen (fakePath() in server/subsonic/helpers.go): {AlbumArtist}/{Album}/{DD}-{TT} - {Titel}.{Endung} — 01 ist die Disc, 02 die Tracknummer. An der Stelle des Künstlers steht also nichts Kaputtes, dort steht überhaupt kein Künstler. Der Info-Dialog zeigte diesen Pfad nur unbeschriftet als „Pfad" an (s.path aus getSong), was wie ein falscher Dateiname aussah. Folge fürs Backend: dass Navidrome den echten Pfad verschweigt, ist die Wurzel der ganzen Datei-Ratearbeit in metadata.js — und damit auch der „Metadaten springen zurück"-Fehler (2.3), bei dem die Suche nach Ähnlichkeit die falsche Kopie traf. Navidrome hat dafür einen Schalter: Einstellungen → Players → Zeile novasonic → „Report Real Path". Ist er an, liefert getSong den echten absoluten Pfad; das Backend nimmt den bereits ohne Codeänderung direkt (novasonic-server.js:1638), und das Raten entfällt. Es gibt zwei Player-Zeilen, weil Web/Android als NovaSonic und das Backend als novasonic anfragen — entscheidend ist die des Backends.
  • Backend (download) — Der Variantenfilter nutzt \b-Wortgrenzen, und die zählen den Unterstrich als Wortzeichen. Titel wie song_instrumental_cover oder Easy_live rutschen deshalb durch, während Easy (Instrumental) korrekt abgewiesen wird.
  • Backend (metadata/sorting) — MusicBrainz release['artist-credit'] is often absent in the standard recording search response (requires inc=artist-credits which can slow the endpoint). The albumArtist field from MusicBrainz autofill may fall back to the recording artist rather than the release artist.
  • Backend (metadata/sorting) — MBIDs are not embedded in downloaded tracks; Navidrome cannot use them for authoritative album grouping (low priority — album splitting is mostly solved via consistent album_artist).