Lyrics: Schalter "Mitlaufen" im Lyrics-Reiter, Vorgabe AUS. Hervorgehoben wird die
laufende Zeile immer, gescrollt nur bei eingeschaltetem Mitlaufen. Nur bei synchronen
Texten sichtbar. Android speichert den Schluessel einzeln, damit das Speichern im
Einstellungsbildschirm ihn nicht auf die Vorgabe zuruecksetzt.
Mischen (Web): sort(() => Math.random() - 0.5) ist keine Mischung. Gemessen blieb der
erste Titel drei- bis fuenfmal zu oft vorn (18,8 % statt 10 % bei n=10, 0,23 % statt
0,05 % bei n=2000). Ersetzt durch Fisher-Yates in queueMath.mischeListe.
Queue-Ende (Web): ein beendetes <audio> meldet sich als pausiert, die Wiederaufnahme
nach unfreiwilligem Hintergrund-Stop rief play() und spielte den letzten Titel erneut.
Jetzt Abbruch bei audio.ended.
Shuffle (Android): das Mischen macht jetzt der Player (playSongs(liste, -1)), damit die
ungemischte Liste erhalten bleibt — vorher leuchtete das Symbol nicht und "Shuffle aus"
konnte die Albumreihenfolge nicht wiederherstellen.
Radio-Modus (beide): der laufende Titel ist aus der letzten Rueckfallebene ausgenommen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Mitschreiben der Abspielposition war zu viel des Guten: es bedeutete
staendiges Schreiben waehrend der Wiedergabe fuer einen Gewinn, den man kaum
bemerkt. Gesichert werden jetzt nur Warteschlange und aktueller Titel, der dann
von vorn beginnt.
Der eigentliche Gewinn bleibt und war der Ausloeser: unter Android war die
Now-Playing-Leiste nach dem Oeffnen leer, bis man einmal auf Play drueckte —
gesichert war nur der letzte Song, nicht die Warteschlange. Das Wiederherstellen
fuellt den Zustand jetzt ueber syncState(), also ohne Abspielen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wiedergabe fortsetzen:
Im Web ueberlebten Warteschlange und letzter Titel bereits, die POSITION im
Titel aber nicht — beim Neuladen begann der Song von vorn. Android sicherte nur
den letzten Song ohne Warteschlange. Beide merken sich jetzt Warteschlange,
Listenposition und Sekunde.
Web schreibt gedrosselt (alle 5 s) waehrend der Wiedergabe und sicher beim
Pausieren, Wegwischen und Tabwechsel — pagehide/visibilitychange statt
beforeunload, das am Handy unzuverlaessig feuert. Android sichert bei jedem
Titelwechsel und beim Pausieren und stellt nach dem Verbinden wieder her:
vorbereitet, aber NICHT abgespielt, wer die App oeffnet will nicht ueberfallen
werden. Laeuft schon etwas (Android Auto), passiert nichts.
Zwei Feinheiten: lag die Position im letzten Rest des Titels, faengt er von vorn
an, statt zwei Sekunden Abspann zu spielen; und gespeichert werden nur Song-Ids,
weil Titel und Cover sich durch Metadaten-Bearbeitung aendern koennen —
verschwundene Titel werden beim Wiederherstellen uebersprungen.
Streitpunkte im Duell:
Gemeinsame Kuenstler nach groesstem Ungleichgewicht statt nach Summe, mit
Kraefteverhaeltnis-Balken. Mindestens vier gemeinsame Plays und 50 % Unwucht,
sonst gilt ein einzelner Play gegen null schon als Streit. Web und Android mit
identischen Schwellen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Statt eines Prozentrings ("wie aehnlich seid ihr") zeigt die Ansicht jetzt, wer
worin gewinnt: fuenf Kategorien mit je einem Sieger und ein Gesamtstand.
Hoerzeit, Vielfalt, Tiefe (Plays je Kuenstler), Nachteule (Anteil vor 5 Uhr) und
Entdeckerdrang (Anteil exklusiver Kuenstler) — bewusst Kennzahlen zur Hoerweise
statt zur Menge, sonst gewinnt immer, wer laenger dabei ist.
Am laufenden Dienst mit zwei gegensaetzlichen Nutzern geprueft: 60 Plays a 300 s
ergeben 5,0 h und Tiefe 12,0, jeder dritte Play nachts ergibt 33 %, 60 von 62
exklusiven Kuenstlern ergeben 97 % — Endstand 3:2. Das JSON-Feldschema wurde
gegen das Kotlin-Modell abgeglichen.
Dazu drei Fixes im selben Bereich:
- Die Web-Ansicht lebte nur von Inline-Stilen mit festen Flex-Verhaeltnissen und
zerfiel am Desktop. Jetzt ein Raster mit eigenen Klassen, eine Spalte am Handy,
zwei ab 780 px; das Fenster musste dafuer von 720 auf 940 px, sonst haette die
zweite Spalte nie ausgeloest.
- Gemeinsame Songs wurden ueber song_id abgeglichen und fielen auseinander,
sobald eine Datei umbenannt wurde. Jetzt normalisierter Titel+Kuenstler.
- Media-Session: die Metadaten wurden feldweise mutiert (gegen Kachel-Flackern
unter Windows), ueber MPRIS kam davon nichts an — Plasma-Widgets zeigten
dauerhaft den zuerst gespielten Titel. Bei echtem Titelwechsel jetzt ein neues
MediaMetadata, sonst weiter in-place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Player: opening the PWA no longer auto-plays when the user had paused. Pausing via
the media notification while backgrounded routes through togglePlay, which now sets
S._userPaused; _onForeground only auto-resumes an INVOLUNTARY background stop
(_wasPlayingBeforeHide && !_userPaused). playCurrent clears the flag on a new track.
Involuntary-stop/freeze recovery on foreground is preserved.
Stats modal: no more small→big flash when switching period/tab — the body keeps a
stable min-height and retains its current content until the new data arrives (only
the first open shows 'Lade…'); a per-request token drops out-of-order responses.
SW cache -> ns-v71.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1+2 of a self-hosted, multi-user listening archive — the basis for
day/month/year stats and a yearly "Wrapped". Runs IN ADDITION to the existing
Navidrome/Last.fm scrobble; never replaces it.
Service (isolated, own container):
- scrobble/scrobble-server.js — node:sqlite play-log (own volume), routes
POST /scrobble/plays (idempotent by uuid), GET /scrobble/history (archive),
DELETE /scrobble/plays?user= (test cleanup), GET /scrobble/health.
Multi-user auth via Navidrome ping.view (cached) -> user not spoofable.
- docker-compose: novasonic-scrobble service (node:22, port 3710) + volume;
backend bumped node:20->22 (EOL); SCROBBLE_URL plumbing (entrypoint, .env.example).
Capture (both platforms, offline-safe queue, idempotent batch flush):
- Fires at the existing scrobbleMode counted-play moment — consistent with
Last.fm, respects the user's setting (no separate 50%/4min rule).
- Web: src/player/scrobbleLog.js (localStorage queue) + subsonicAuthParams()
in api.js + hooks in player.js + flush init in main.js; S.cfg.scrobbleUrl.
- Android: data/PlayLogClient.kt (filesDir queue, reused Ktor client) +
repo.logPlay/flushPlayLog + PlayerController hook + NovaSonicApp flush +
scrobbleUrl in PrefsStore/SettingsViewModel/SettingsScreen (with test button).
Plan + design decisions documented in docs/scrobble-plan.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ping Navidrome with scrobble?submission=false when a track starts so
getNowPlaying reflects it instantly (feeds e.g. a Discord rich-presence
plugin / other clients' Now Playing). Independent of the existing delayed
"played" scrobble used for play counts and Last.fm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The NP overlay only refreshed its cover while open, so skipping with it closed
left the <img> on the last-opened song; reopening briefly showed that old cover
until the current one loaded. Now the NP cover element is kept pointed at the
current song even while closed (loads in the background), so opening it always
shows the right artwork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reintroduce the two-element handoff, this time as a skip-speed optimization
(not for the OS tile). A second <audio> element pre-buffers the next track;
on a forward skip playCurrent hands playback over to it instead of reloading
the source, so playback starts immediately instead of ~1s. Covers are
pre-warmed in parallel so the new artwork is ready too.
Listeners are bound to both elements once (handlers guard with el !== S.audio).
Safety: handoff only when the standby is buffered (readyState >= 2); otherwise
falls back to the unchanged source-swap path, and a failed handoff play()
recovers with a fresh source load. Web-only; the native bridge path is untouched.
Note: this does not change the OS media-tile flicker (a separate, MSE-only fix).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removing the second <audio> element fixed most of the OS media-tile flicker,
but skips now load the next track fresh → a brief gap where Windows shows the
generic site entry. Prefetch the next track's first ~4MB with fetch() (not a
media element, so no Chromium session flip) to warm the HTTP cache + TLS
connection; the src-swap then starts faster and the gap shrinks. Abortable on
queue change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navidrome's player (react-jinke fork) uses one persistent <audio> element and
does not flicker the OS media tile, so the source-swap itself isn't the cause.
Our one structural difference was a second buffering <audio> (_preAudio) for
preload. A second media element seems to make Chromium hand the OS media
session over on skip → Windows tile flicker. This drops it; we rely on the main
element's preload="auto" + browser HTTP cache. Cover art is still prewarmed.
Trade-off: next-track start may be marginally slower. Easy to revert if it
doesn't fix the flicker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a second <audio> element; S.audio always points at the active one and
listeners are bound to both once (handlers guard with el !== S.audio).
preloadNext buffers the next track into the standby element, and playCurrent
hands playback over to it on skip instead of swapping the source — so the
active element never empties and Windows keeps the media tile (also makes
forward skips instant/gapless).
Safety: handoff only when the standby is buffered (readyState >= 2); falls
back to the unchanged source-swap path otherwise, if there's no second
element, or if handoff play() rejects. Web-only — the native bridge path is
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Home rows get hover left/right arrows that only show for the scrollable
direction (enhanceScrollRow + initScrollRows, called from startApp);
visibility recomputed on scroll/resize/content-load.
Fix the OS media tile flicker when skipping on desktop PWA: mutate the
existing MediaMetadata in place instead of assigning a new one each track
(the replacement made Windows tear down and rebuild the tile). Action
handlers are now set once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Space = play/pause, ←/→ = seek 5s, Shift+←/→ = prev/next, ↑/↓ = volume.
Handler lives in player.js (where _pos/_dur/seekToMs are in scope), wired
from startApp via initKeyboardShortcuts(). Ignored while typing in inputs,
and yields Space to focused buttons/links so it never double-fires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dispAlbumName now takes the full album object and reads album.sortName
directly — no kuromoji fallback, no backend translation.
dispAlbum (song context) also drops the kuromoji fallback, keeping only
the sortAlbum/albumSort file tag.
Override list → Navidrome sort tag → raw Japanese name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The queue serialization was saving artist as a plain string but not the
artists array. On cold start the app restores from localStorage, so
parseArtists had no array to work with and fell back to splitting the
artist string by semicolons — "Maduk Veela" has none, so it returned
one combined entry. PWA always re-fetches fresh API data so the array
was always present. Adding artists:[] to both song and queue entries
fixes artist navigation after cold start on Android.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WebBridge: skip setMediaItems/prepare when the track is already buffered
from the cold-start pre-warm, so resuming the last song is instant (~0s)
instead of re-buffering from scratch (~10s).
Player: show pause icon during buffering (isPlaying=false, isBuffering=true)
so the button doesn't flash back to "play" between tracks. Also optimistically
set _nativePlaying=true and call setPlayIcon immediately on playCurrent so the
icon updates before the native callback round-trip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Call updateMediaSession immediately on track change so skip buttons and
cover art in the OS notification bar update without waiting for audio load
- Replace link[rel=prefetch] (no-op for audio streams) with a shadow Audio
element (preload=auto) that actually buffers the next track
- Start preloading 60s before track end instead of 30s for more buffer margin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings dropdown: 15s (default), 30s, 1/3 of duration, 2/3 of duration, song end.
Duration-based modes use s.duration from Navidrome metadata (always available).
'end' mode skips the timer and fires the scrobble in onEnded (web) / onQueueEnded (native).
Skipping a song never scrobbles in any mode — _cancelScrobble() still runs on skip/pause.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move player.js + nowplaying.js → src/player/
- Move views.js + spotify/soundcloud/youtube/abs → src/views/
- Move ui.js + components.js → src/ui/
- Update all import paths (static + lazy) across every file
- MusicService: wrap ExoPlayer in ForwardingPlayer that always advertises
next/prev commands so the notification skip buttons stay visible in radio
mode even when the JS queue has only one native track
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>