Commit Graph
78 Commits
Author SHA1 Message Date
HoneyandClaude Sonnet 4.6 d355129134 fix: lock screen audio + notification — audioSession once, deferred metadata, SW artwork cache
- Set navigator.audioSession.type='playback' only once (guarded flag) — repeated
  reassignment was resetting the OS audio session and triggering Doze network cuts
- Defer updateMediaSession() until after play() resolves, not before audio.src swap —
  new MediaMetadata during HAVE_NOTHING state caused Samsung to close/reopen notification
- Fix silent bug: updatePlayerUI(s, true) now actually skips MediaSession (added param)
- 10s safety guards now attempt play() instead of just resetting _trackChanging flag
- SW v40: cache-first for getCoverArt with token-stripped keys — artwork served from
  cache even when Doze blocks network, survives session restarts with new auth tokens
- Pre-warm next track artwork 2s after song starts (earlier than 30s-remaining trigger)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 18:37:23 +02:00
HoneyandClaude Sonnet 4.6 ec13124d99 fix: generation counter prevents stale _reset() from clobbering active transition
When skipping rapidly, old playCurrent() closures had _reset() callbacks
that could fire _trackChanging=false while a newer transition was still
in progress — breaking both notification guard and background recovery.

Each playCurrent() now captures a generation number (++_playGen).
_reset() is a no-op if _playGen has advanced (newer call took over).
canplay and play().catch handlers also check the generation before acting.

This makes all stale callbacks safe regardless of how quickly the user skips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 00:38:55 +02:00
HoneyandClaude Sonnet 4.6 b941672a77 fix: keep _trackChanging=true through entire recovery window (lock screen)
When play() fails on locked screen, Android may revoke audio focus and
fire a spurious pause event. Previously we reset _trackChanging=false
right in the catch, leaving a window for that pause event to set
playbackState='paused' and dismiss the Samsung cover screen notification.

Now _trackChanging stays true for the ENTIRE recovery (initial fail ->
canplay retry -> success/fail), with a 10s Kima-style safety net as the
only exit. This means:
- AbortError: reload src+load(), _trackChanging stays true, canplay retries
- NotAllowed/network: same — _trackChanging stays true, canplay retries
- If canplay fires and play() succeeds: play event resets _trackChanging
- If canplay fires and play() fails: _reset() is called
- If canplay never fires (Doze): 10s timeout resets _trackChanging

Net effect: Samsung lock screen notification never sees playbackState='paused'
during the recovery window, so it stays open instead of dismissing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 00:24:11 +02:00
HoneyandClaude Sonnet 4.6 688348545d fix: clean rewrite of audio recovery matching Kima Hub design
playCurrent():
- Remove canplay listener from before play() — it caused a race where
  both the listener and the initial play() fired simultaneously
- canplay is now only registered in the catch (failure path only),
  eliminating the double-play race entirely
- AbortError: reload src+load() with _trackChanging=true (OS interruption
  recovery); canplay fires once buffered and retries play()
- Other errors: reset _trackChanging, let canplay retry (not throttled
  by Android background timers unlike setTimeout)
- Add 10s safety net to reset _trackChanging if Doze kills everything
  (matches Kima Hub's reload failsafe)

main.js error handler:
- Skip when _trackChanging=true (playCurrent already recovering) to
  prevent conflicting dual recovery attempts
- _onForeground: reset _trackChanging on play() failure so the flag
  can't get stuck when foreground recovery also fails

index.html:
- Add crossorigin=anonymous: Navidrome and NovaSonic backend both send
  Access-Control-Allow-Origin:* so this is safe, and it enables proper
  cross-context HTTP caching for cover art and audio responses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 23:34:28 +02:00
HoneyandClaude Sonnet 4.6 0344dcdcb6 fix: preload=auto, artwork pre-warm, AbortError reload (matching Kima Hub)
- Add preload="auto" playsinline to <audio> element: browser starts
  buffering as soon as src is set, not only when play() is called —
  critical for background where play() has no data to work with
- Fix AbortError handling: instead of silently returning (was swallowing
  audio element bad-state errors), reload src + load() like Kima Hub
  so the already-registered canplay listener retries play() once buffered
- Pre-warm next song's cover art in _preloadNext() using _cachedCoverUrl
  so the lock screen notification never cold-starts artwork on first skip
- Move _coverCache to top of file so _preloadNext and updateMediaSession
  share the same stable token URLs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 23:09:46 +02:00
HoneyandClaude Sonnet 4.6 a0c6d09c41 fix: stable notification artwork + canplay retry for background audio
- Cache cover art URLs per song so the same auth token is reused across
  metadata updates; Samsung/Chrome can cache the artwork image and the
  notification no longer visually reloads on every skip
- Add canplay listener as background-safe play() retry: the audio
  element fires canplay from its internal state machine (not throttled
  like setTimeout by Android Doze), so play() is retried as soon as
  the browser has buffered enough data
- Set navigator.audioSession.type='playback' on each _tryPlay attempt
  (not just on play event) so the OS audio session claim happens before
  Chrome requests the play grant
- Guard _onForeground playbackState assignment with !_trackChanging to
  prevent visibilitychange/pageshow from stomping 'playing' to 'paused'
  during an in-progress track transition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 22:49:35 +02:00
HoneyandClaude Sonnet 4.6 3f5a5a4c43 fix: Samsung notification stays alive; retry play() on background rejection
- Set playbackState='playing' explicitly before MediaMetadata swap so
  Samsung One UI never sees a gap where state is ambiguous during skip
- Add retry loop (500ms→1s→2s→4s→8s) in playCurrent() catch — handles
  Android Doze rejecting play() in background; stops retrying if the
  song changes mid-flight (AbortError)
- Fix error handler: set _trackChanging=true before audio.load() so
  the load()'s pause event can't slip past the Samsung notification guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 22:29:30 +02:00
HoneyandClaude Sonnet 4.6 33dc07ed5d fix: audioSession playback type, exponential backoff, pageshow recovery
- Set navigator.audioSession.type = 'playback' on first play — tells Android
  OS this is a music app, keeping background network alive through Doze mode
- Replace flat 2s error retry with exponential backoff (1s/2s/4s/8s)
- Add pageshow event alongside visibilitychange for Samsung/iOS PWA reliability
- Attempt audio resume on foreground if OS killed playback while backgrounded
- Switch preload from hidden audio element to link rel=prefetch (lighter,
  avoids triggering two-audio-stream detection on Android)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:34:23 +02:00
HoneyandClaude Sonnet 4.6 502427d02c fix: preload next track 30s early to survive Android Doze network gap
When the current song ends, Android's battery optimization briefly kills
background network access. By starting the next song's stream request while
the current track is still playing (and Chrome holds an active audio session),
the buffer is ready before the gap hits. The same URL is reused by the main
element so Chrome can serve from its in-memory buffer.

Also reset _trackChanging on play() failure to avoid permanently suppressing
pause events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 20:40:25 +02:00
HoneyandClaude Sonnet 4.6 89e169b70a fix: prevent notification close on skip and background audio stall
- playCurrent: set _trackChanging flag and update media session metadata
  before changing src, so the pause event fired by src-swap does not
  set playbackState='paused' and dismiss the Samsung cover screen notification
- Remove stalled->load() handler that silently paused audio with no recovery
- Error handler now retries regardless of paused state (load() causes paused)
- Add .catch() to audio.play() in playCurrent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 20:23:51 +02:00
Honey 7744ae85db fix: Scrub lag fix and Volume control for Audiobooks 2026-05-03 17:20:02 +02:00
Honey 0d8b8b6f5d fix: Bug fixes Scrub lag Chapter track last chapter Mini bar stays over music Resume ABS doesn't stop music 2026-05-03 16:47:17 +02:00
HoneyandClaude Sonnet 4.6 598c108214 fix: detect YTM artist channel expansion in getYtmVideoUrls; revert PWA MediaSession changes
- ytdlp.js: watch for [youtube:tab] in yt-dlp output during flat-playlist step;
  return [] when channel expansion is detected so cascade falls through to ytsearch
- Revert player.js / main.js / state.js PWA _changingTrack changes (not effective)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 01:11:36 +02:00
Honey a5959b19b8 fix: pwa changes 2026-05-02 21:27:57 +02:00
HoneyandClaude Sonnet 4.6 d9b9151f50 feat: persist shuffle and repeat state across reloads/updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:55:32 +02:00
HoneyandClaude Sonnet 4.6 c8c3670ca0 fix: eliminate mobile bg flash — double rAF + will-change:opacity
new Image() onload fires when bytes are downloaded but GPU decode of the
blurred layer hasn't happened yet on mobile. Double rAF defers the opacity
swap until after the browser has actually painted the new background.
will-change:opacity keeps the layers on dedicated compositor layers so
the blur doesn't need to be recomputed on every opacity transition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:52:02 +02:00
HoneyandClaude Sonnet 4.6 af0c2d6eb2 fix: preload bg image before crossfade to eliminate black flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:39:50 +02:00
HoneyandClaude Sonnet 4.6 e0a518fa13 fix: deduplicate NP background crossfade — skip if URL unchanged
_syncNpDirect is called 2-3x per skip from different call sites;
the crossfade now no-ops when the cover URL hasn't changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:36:08 +02:00
HoneyandClaude Sonnet 4.6 97a4cc756d fix: crossfade now-playing background instead of hard flash
Two-layer opacity crossfade replaces the broken background-image transition.
New image loads on the hidden layer, then both opacity values swap smoothly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:29:54 +02:00
HoneyandClaude Sonnet 4.6 e75d90e165 fix: eliminate flash on skip, refresh home on nav
- renderQueue: in-place active-class update when queue unchanged — no DOM rebuild, no image reload
- updatePlayerUI: update img.src directly instead of replacing innerHTML
- _syncNpDirect: same img.src update for now-playing cover
- navigate('home'): always reloads home data so new albums appear without restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:21:12 +02:00
HoneyandClaude Sonnet 4.6 b575e38726 feat: add cover art to queue panel, remove redundant sidebar queue nav item
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:07:07 +02:00
HoneyandClaude Sonnet 4.6 a61436669e fix: per-span hover highlight, use • as artist separator everywhere
- CSS: target #player-artist-name span:hover and #np-artist-name span:hover so only the hovered artist name highlights, not all at once
- change artist separator from / to • in song items, player bar, now playing overlay, album header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 00:51:38 +02:00
HoneyandClaude Sonnet 4.6 a31953d17e fix: split multi-artists in player bar and now playing overlay, fix broken onclick
- player bar: fix onclick broken by JSON.stringify producing double-quoted strings inside double-quoted HTML attribute; use single-quoted JS args instead
- now playing overlay: np-artist-name now splits artists and each is individually clickable (closes NP then navigates to artist)
- remove wrapper onclick from both np-artist-name and player-artist-name in HTML

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 00:45:10 +02:00
HoneyandClaude Sonnet 4.6 e029a8f900 fix: split multi-artists in player bar, each name navigates to own artist profile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 00:39:23 +02:00
HoneyandClaude Sonnet 4.6 16b7faee2b refactor: split views.js and ui.js into focused modules
Extract 3 new files from oversized source files:
- src/components.js (~86 lines): makeCard, makeAlbumCard, makeSongItem,
  makeGlobalTrackItem, mkSection — reusable DOM builders
- src/spotify.js (~187 lines): loadSpotifyImporter, resolveSpotifyLink,
  clearSpotifyPreview, spotifyDlAll, and private render/artwork helpers
- src/nowplaying.js (~255 lines): openNowPlaying, closeNowPlaying,
  syncNowPlaying, switchNpTab, full lyrics engine, renderNpQueue,
  npGoArtist/Album, openNpMenu/closeNpMenu

views.js: 1121 → 864 lines
ui.js:     706 → 462 lines

Update imports in main.js, player.js, views.js accordingly.
Build verified: 15 modules, no errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:20:00 +02:00
HoneyandClaude Sonnet 4.6 e31ddec843 fix(mobile): restore playback state + MediaSession stability
- _restoreLastPlayed: set audio.src + call updateMediaSession so play
  button works immediately and notification shows artwork on app open
- visibilitychange: re-register all MediaSession handlers after screen
  unlock, fold/flip, or background return (fixes vanishing notification)
- Stall watchdog (3s interval): detects frozen audio in background and
  calls play() to recover; also catches missed 'ended' events so
  auto-advance works reliably when screen is off
- stalled/error listeners: attempt audio reload on network hiccup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:53:25 +02:00
HoneyandClaude Sonnet 4.6 fbabdf8269 feat: synced lyrics view in Now Playing overlay
Adds a Lyrics tab to the NP overlay using Navidrome's getLyricsBySongId
endpoint. Synced lyrics auto-scroll and highlight the current line as
the track plays; tapping a line seeks to that position. Falls back to
plain text for unsynced lyrics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 18:42:45 +02:00
HoneyandClaude Sonnet 4.6 9aded56713 Migrate to Vite + ES module architecture
Split monolithic app.js (~1850 lines) into maintainable ES modules:
- src/state.js  — shared mutable state (S object)
- src/utils.js  — md5, esc, formatDur, toast
- src/api.js    — Subsonic API, Navidrome REST, iTunes API
- src/player.js — playback, transport, queue, media session, preview
- src/views.js  — navigation, page loaders, card makers, search
- src/ui.js     — all modals, overlays, sidebar, settings
- src/pwa.js    — service worker registration + update banner
- src/main.js   — app init, connect, window globals

Updated Dockerfile to multi-stage (Node build → nginx serve).
Static assets (sw.js, manifest.json, icon.svg) moved to public/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:01:34 +02:00