From ed75c2fff7ca77dd138fd7cb526feade70644bf3 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 12 Aug 2026 10:44:20 -0400 Subject: [PATCH] fix(cover): await bounded cover fetch on cold-cache play MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/hooks/useAudio.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/hooks/useAudio.ts b/src/hooks/useAudio.ts index 297d656..bac31d0 100644 --- a/src/hooks/useAudio.ts +++ b/src/hooks/useAudio.ts @@ -16,7 +16,6 @@ import { onCleanup } from "solid-js"; import { cachedCoverPath, fetchCoverArt, - prefetchCoverArt, } from "../utils/cover-art"; import { createAudioBackend, @@ -316,13 +315,21 @@ async function play(episode: Episode): Promise { const feedStore = useFeedStore(); const feed = feedStore.feeds().find((f) => f.podcast.id === episode.podcastId); const podcastTitle = feed?.customName || feed?.podcast.title || ""; - // Cover art must NEVER gate playback (it was a curl subprocess blocking - // play() by up to 8s). Serve the disk-cached file synchronously when it - // exists; on a miss, play bare and warm the cache for the next play — - // runtime video-add can't become an albumart track, so late art only - // works via the load-time cover-art-files property. + // Cover art only applies at file LOAD (the runtime video-add fallback + // never becomes an albumart track), so a cold-cache play must wait for + // the fetch or play artless. Serve the disk cache synchronously; on a + // miss, await the single-flight fetch with a 1.2s cap (covers fetch in + // ~300ms typically) — past the cap, play bare and let the fetch warm + // the cache for next time. 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((resolve) => setTimeout(() => resolve(null), 1200)), + ]); + if (path) coverArtPath = path; + } // Resume from saved progress if available and not completed const savedProgress = progressStore.get(episode.id); @@ -339,10 +346,6 @@ async function play(episode: Episode): Promise { coverArtPath: coverArtPath ?? undefined, }); - if (coverUrl && !coverArtPath) { - prefetchCoverArt(coverUrl); - } - setCurrentEpisode(episode); setIsPlaying(true); setPosition(startPos);