Commit Graph

12 Commits

Author SHA1 Message Date
bvandeusen 27766ae063 feat(web/player): self-heal a stale system-playlist / radio queue on total failure
test-web / test (push) Successful in 39s
When the whole queue proves unplayable (e.g. a tab left open across the
daily system-playlist rebuild — the exact stale-snapshot case), the player
now re-pulls the fresh snapshot and resumes instead of dead-ending on
"Try again". The seeder hands the store an opaque refetch closure so the
store stays decoupled from the playlist API and the per-artist
(songs_like_artist) identity problem: single-instance variants re-pull via
systemShuffle, per-artist mixes via getPlaylist(id), radio re-seeds from
its track. Bounded to one self-heal per exhaustion (reset on the next
successful play) so a still-broken refresh can't loop; "Try again" stays
the genuine last resort. Wired from PlaylistCard, the playlist detail page,
and playRadio. Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:51:47 -04:00
bvandeusen 2a8de82a17 fix(web/player): auto-skip a failed track instead of dead-ending on "Try again"
test-web / test (push) Successful in 39s
A track that fails to load (e.g. a stale system-playlist snapshot pointing
at a rebuilt/removed file) hard-set the player to the 'error' state and
stranded the user on a "Try again" button that just re-queued the same
failing track. Now a load error advances to the next track; the error
state only surfaces once the whole queue has proven unplayable — every
track failed, or we reached the end. A failure streak capped at queue
length stops a fully-broken queue from cycling, and resets on the next
successful play.

Next (Track B cont.): self-heal a stale system-playlist / radio queue by
re-pulling the fresh snapshot on total failure, plus the Android
equivalent. Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:40:08 -04:00
bvandeusen c466e6c317 feat(web): crossfade between tracks (Tier C11)
test-web / test (push) Successful in 31s
test-go / test (pull_request) Successful in 34s
test-web / test (pull_request) Successful in 42s
android / Build + lint + test (pull_request) Successful in 5m4s
android / Build signed release APK (pull_request) Has been skipped
test-go / integration (pull_request) Successful in 11m1s
Operator-tunable 0-12s crossfade in Settings → Playback. Default 0
(off). Most albums sound best at 0 — gapless masters, classical, and
live recordings all suffer noticeable crossfades.

Implementation: pure-function position-derived volume scalar.
`deriveFadeScalar(position, duration, crossfadeSec)` ramps from 0 to
1 over the leading X seconds, 1 to 0 over the trailing X seconds,
and stays at 1 in between. Tracks shorter than 2X don't fade.

The layout's audio-volume effect now multiplies player.volume by
the fade scalar — no timers, no AudioContext, no element swapping.
Re-renders at the ~4Hz `timeupdate` cadence give 16 discrete steps
over a 4s fade, audibly close to smooth. Smoothing to per-frame
ramps via rAF is a follow-up if needed.

Setting persists to localStorage as `minstrel.crossfade` (matches
the existing volume-storage convention).

Tests cover the pure derivation (off, too-short track, fade-in,
fade-out) + clamp/persist on setCrossfade + invalid-input recovery
on readStoredCrossfade.
2026-06-01 14:22:30 -04:00
bvandeusen 9b4f907db6 refactor(web): sweep 24 test files to use makeTrack/makeTracks fixtures (#375)
Replaces inline TrackRef literal redefinitions with calls to the
test-utils helper. Tests that asserted on default field values
(e.g. track titles, artist names rendered in the DOM) keep explicit
overrides; tests that only need a stub for shape now use makeTrack()
with no overrides.

PlaylistCard.test.ts, PlaylistTrackRow.test.ts, and playlist.test.ts
SKIPPED — they use PlaylistTrack (with track_id/added_at), not TrackRef.
ArtistCard.svelte and +page.svelte route files (matched by initial grep)
SKIPPED — live code, not test files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:58:14 -04:00
bvandeusen 472b9d9e84 fix(web/m7-364): untrack _position in queue immediate-write effect
Wrap the _position read inside untrack() in the immediate-write $effect
so it no longer registers as a reactive dependency, preventing the effect
from firing on every 4Hz position tick. Also adds three missing
restoreQueue unit tests with vi.mock for the auth store.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:37:55 -04:00
bvandeusen bde017dad3 fix(web/m7-364): pause on last-track remove + reset drawer in test setup 2026-05-03 20:37:33 -04:00
bvandeusen 6f9b5d0059 feat(web/m7-364): queue mutations + drawer state on player store
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 20:29:48 -04:00
bvandeusen f43ea6af5b feat(player): playNext for M7 #372 track-actions menu
Inserts at _queue[_index + 1] so the next-up slot is overwritten with
the chosen track. Empty-queue seeding mirrors enqueueTrack's behavior.
Clears _radioSeedId since user-driven enqueue invalidates the M4c
auto-refresh trigger.
2026-05-02 22:45:13 -04:00
bvandeusen d8b955ff0c feat(web): radio queue auto-refreshes at 80% via ?exclude= param
Adds _radioSeedId state and a module-level $effect.root that triggers a
/api/radio?seed_track=…&exclude=… fetch when queue consumption reaches
80%, appending new tracks without resetting the radio seed. Clears seed
on any non-radio enqueue (playQueue/enqueueTrack/enqueueTracks). Five
new vitest cases cover the trigger threshold, append logic, in-flight
guard, and manual-enqueue reset path.
2026-04-29 08:50:24 -04:00
bvandeusen 7dd77a4723 feat(web): add enqueueTrack, enqueueTracks, playRadio to player store
enqueueTrack/enqueueTracks append to the queue without disturbing
playback state (no auto-play on append, no state mutation when queue
was non-empty). playRadio fetches /api/radio?seed_track=<id> and
feeds the response into playQueue. Empty response leaves queue alone.
2026-04-25 15:22:03 -04:00
bvandeusen 1476d5081b feat(web): add shuffle, repeat, and audio-event reporting to player store
toggleShuffle (Fisher-Yates over queue[index+1..end] with injectable
RNG for test determinism), cycleRepeat (off → all → one → off),
reportStateFromAudio with full repeat-mode semantics on 'ended',
plus simple state mirroring for playing/paused/waiting/error. Extends
skipNext to wrap when repeat='all'.
2026-04-24 17:56:00 -04:00
bvandeusen 431f333e41 feat(web): add player rune store with basic actions
Core state (queue, index, state, position, duration, volume, shuffle,
repeat, error) plus actions: playQueue, togglePlay, linear skipNext,
skipPrev 3-second rule, seekTo, setVolume (localStorage-persisted),
registerAudioEl, reportTimeUpdate, reportDuration. Shuffle/repeat
logic and audio-event reporting land in follow-up commits.

Also install an in-memory Storage polyfill in vitest.setup.ts: Node 25
exposes a partial localStorage global that lacks getItem/setItem/clear
(requires --localstorage-file with a path), and jsdom defers to the
Node global when present, leaving tests unable to exercise storage.
2026-04-24 17:54:07 -04:00