fix(web): songs-like play overlay + scrubber smoothing
test-web / test (push) Failing after 45s

PlaylistCard: per-artist system variants (songs_like_artist) all share
one variant tag but exist as one playlist per seed artist; routing
their play handler through systemShuffle(variant) hit the wrong
playlist (or 404). Detect via seed_artist_id != null and fall through
to getPlaylist(playlist.id) for those; still tag the queue with the
variant so source attribution stays correct.

smoothPosition.svelte.ts: new useSmoothPosition() hook mirrors
Android's rememberSmoothPositionMs. player.position only updates
~4 Hz (HTML audio timeupdate), so the seek thumb stepped visibly;
$effect resets on each canonical tick / seek / track-change and a
rAF loop extrapolates at playback rate between ticks.

Wired into both PlayerBar.svelte (mini + expanded seek rows) and
now-playing/+page.svelte. Seek input handler still reads the raw
range value (not smoothed.value) so user drags stay authoritative.
This commit is contained in:
2026-06-01 22:39:24 -04:00
parent d4c6bb3f2d
commit 74bae74f9f
4 changed files with 86 additions and 10 deletions
+15 -6
View File
@@ -14,11 +14,17 @@
import { formatDuration } from '$lib/media/duration';
import { FALLBACK_COVER, coverUrl } from '$lib/media/covers';
import { dominantColorFromUrl, rgbToCssString } from '$lib/media/dominantColor';
import { useSmoothPosition } from '$lib/player/smoothPosition.svelte';
import LikeButton from './LikeButton.svelte';
import TrackMenu from './TrackMenu.svelte';
const current = $derived(player.current);
// Per-frame interpolated playhead so the scrubber bar moves
// smoothly between server ticks instead of stepping every ~250ms.
// Reset on track/play-state change via the helper's $effect.
const smoothed = useSmoothPosition();
const skipPrevDisabled = $derived(player.index === 0 && player.position < 3);
const skipNextDisabled = $derived(
player.index === player.queue.length - 1 && player.repeat === 'off'
@@ -226,10 +232,12 @@
</div>
</div>
<!-- Bottom row: full-width seek slider + time labels -->
<!-- Bottom row: full-width seek slider + time labels.
Reads the smoothed position so the slider thumb glides
between server ticks at playback rate. -->
<div class="flex items-center gap-2">
<span class="w-9 shrink-0 text-right text-[10px] tabular-nums text-text-secondary">
{formatDuration(player.position)}
{formatDuration(smoothed.value)}
</span>
<input
type="range"
@@ -237,7 +245,7 @@
min="0"
max={player.duration || 0}
step="0.1"
value={player.position}
value={smoothed.value}
oninput={onSeekInput}
class="flex-1 accent-accent"
/>
@@ -346,10 +354,11 @@
<SkipForward size={20} strokeWidth={1.5} fill="currentColor" />
</button>
</div>
<!-- Seek row -->
<!-- Seek row — reads smoothed position so the thumb glides
between server ticks. See useSmoothPosition. -->
<div class="flex items-center gap-3">
<span class="w-12 shrink-0 text-right text-xs tabular-nums text-text-secondary">
{formatDuration(player.position)}
{formatDuration(smoothed.value)}
</span>
<input
type="range"
@@ -357,7 +366,7 @@
min="0"
max={player.duration || 0}
step="0.1"
value={player.position}
value={smoothed.value}
oninput={onSeekInput}
class="flex-1 accent-accent"
/>
+13 -2
View File
@@ -64,7 +64,15 @@
starting = true;
try {
const variant = playlist.system_variant;
if (variant != null) {
// systemShuffle keys by variant alone, which only works for
// SINGLE-instance variants (for_you, discover, deep_cuts, …).
// songs_like_artist has one playlist PER seed artist; using
// the variant alone would return the wrong one (or fail).
// For those, fall through to getPlaylist so the play handler
// hits the exact playlist the user clicked on. Detect via
// seed_artist_id which is non-null only for per-artist mixes.
const isPerArtist = playlist.seed_artist_id != null;
if (variant != null && !isPerArtist) {
// #415: server returns the rotation-aware order (unplayed
// this rotation first). Play it as-is — no client shuffle —
// and tag the queue so play_started carries `source` and
@@ -78,7 +86,10 @@
const detail = await getPlaylist(playlist.id);
const refs = toTrackRefs(detail.tracks);
if (refs.length > 0) {
playQueue(refs, 0);
// Tag the queue with the variant when one exists so play
// events still carry the correct source attribution even
// though we went through the per-id path.
playQueue(refs, 0, variant != null ? { source: variant } : undefined);
}
}
} finally {