fix(cover): await bounded cover fetch on cold-cache play

Prefetch alone can't cover every first play (any surface, feeds outside
the FeedPage focus window, quick plays) — the live session showed
cover-art-files empty with zero albumart tracks on a cold-cache play.
play() now serves the disk cache synchronously and, on a miss, awaits the
single-flight fetch with a 1.2s cap (covers fetch in ~300ms typically);
past the cap it plays bare and warms the cache. Verified: cold-cache play
-> cover present at load -> albumart track.
This commit is contained in:
2026-08-12 10:44:20 -04:00
parent bd7d988741
commit ed75c2fff7

View File

@@ -16,7 +16,6 @@ import { onCleanup } from "solid-js";
import { import {
cachedCoverPath, cachedCoverPath,
fetchCoverArt, fetchCoverArt,
prefetchCoverArt,
} from "../utils/cover-art"; } from "../utils/cover-art";
import { import {
createAudioBackend, createAudioBackend,
@@ -316,13 +315,21 @@ async function play(episode: Episode): Promise<void> {
const feedStore = useFeedStore(); const feedStore = useFeedStore();
const feed = feedStore.feeds().find((f) => f.podcast.id === episode.podcastId); const feed = feedStore.feeds().find((f) => f.podcast.id === episode.podcastId);
const podcastTitle = feed?.customName || feed?.podcast.title || ""; const podcastTitle = feed?.customName || feed?.podcast.title || "";
// Cover art must NEVER gate playback (it was a curl subprocess blocking // Cover art only applies at file LOAD (the runtime video-add fallback
// play() by up to 8s). Serve the disk-cached file synchronously when it // never becomes an albumart track), so a cold-cache play must wait for
// exists; on a miss, play bare and warm the cache for the next play — // the fetch or play artless. Serve the disk cache synchronously; on a
// runtime video-add can't become an albumart track, so late art only // miss, await the single-flight fetch with a 1.2s cap (covers fetch in
// works via the load-time cover-art-files property. // ~300ms typically) — past the cap, play bare and let the fetch warm
// the cache for next time.
const coverUrl = feed?.podcast.coverUrl; const coverUrl = feed?.podcast.coverUrl;
const coverArtPath = coverUrl ? cachedCoverPath(coverUrl) : null; let coverArtPath = coverUrl ? cachedCoverPath(coverUrl) : null;
if (coverUrl && !coverArtPath) {
const path = await Promise.race([
fetchCoverArt(coverUrl),
new Promise<null>((resolve) => setTimeout(() => resolve(null), 1200)),
]);
if (path) coverArtPath = path;
}
// Resume from saved progress if available and not completed // Resume from saved progress if available and not completed
const savedProgress = progressStore.get(episode.id); const savedProgress = progressStore.get(episode.id);
@@ -339,10 +346,6 @@ async function play(episode: Episode): Promise<void> {
coverArtPath: coverArtPath ?? undefined, coverArtPath: coverArtPath ?? undefined,
}); });
if (coverUrl && !coverArtPath) {
prefetchCoverArt(coverUrl);
}
setCurrentEpisode(episode); setCurrentEpisode(episode);
setIsPlaying(true); setIsPlaying(true);
setPosition(startPos); setPosition(startPos);