v3: fix the queue and re-resolve edge cases in native bandcamp playback
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m59s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m59s
Re-resolving a stream is a network round-trip, and nothing checked that the player still belonged to that item once it landed. Picking another dump — or closing the player — while a resolve was in flight let the stale result swap the queue back; from onError, which resolves with autoplay, it would also start playing over whatever was chosen instead. A generation counter, bumped by playQueue/advanceTo/stop, now drops any result that no longer owns the player, with a separate counter owning the resolving flag so a superseded resolve can't clear a newer one's spinner. A track that stays dead after a re-resolve is one track, not one album: the cooldown path now steps to the next entry and only falls back to the iframe on the last one. A failed resolve still goes straight to the embed, since that's an album-wide failure rather than a single bad URL. And a track that has vanished from the release, or lost its streamable flag to Bandcamp's free-play cap, no longer resumes track 1 at the dead track's offset — the findIndex miss resets the offset instead of seeking past the end of another track. Clicking the playing row rewound it but never resumed: seekTo only moves currentTime, so re-selecting a finished or paused track looked like a no-op. It now resumes as well. Same idea in the compact rich-content card, whose button advertises "Pause" while active but re-played on click — in native mode that meant a fresh /api/bandcamp/tracks and a restart from track 1. It pauses for streams now; embeds have no transport of their own and keep restarting, as before. playRichContent reports whether playback actually started, so a native-mode Bandcamp page with no streams and no stored embedUrl — an artist root, a /music index, a preorder — is no longer a dead click: the journal card navigates to the dump as it used to, and the rich-content card opens the source page. Also: the tralbum fetch releases the response body on its two error paths. The endpoint is unauthenticated, so random /track/<slug> URLs would leak a connection per request while also defeating the cache. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E56F55N7m5GGKKEwoYFJtH
This commit is contained in:
@@ -43,6 +43,13 @@ export function JournalCard(
|
||||
navigate(dumpUrl(dump));
|
||||
}
|
||||
|
||||
// A playable card plays. If playback turns out to be impossible — a Bandcamp
|
||||
// page with no streams and no embed to fall back to — the card must still do
|
||||
// what an unplayable one does rather than swallowing the click.
|
||||
async function handlePlayOrNavigate(rc: NonNullable<typeof playable>) {
|
||||
if (!await playRichContent(rc, dumpUrl(dump))) handleNavigate();
|
||||
}
|
||||
|
||||
// Mirrors FilePreview (the hot/new feeds) so a video shows its generated
|
||||
// still here too, rather than degrading to a text card with a 🎬.
|
||||
const thumbnailUrl = dump.thumbnailMime
|
||||
@@ -151,7 +158,7 @@ export function JournalCard(
|
||||
<li
|
||||
className={className}
|
||||
onClick={playable
|
||||
? () => void playRichContent(playable, dumpUrl(dump))
|
||||
? () => void handlePlayOrNavigate(playable)
|
||||
: handleNavigate}
|
||||
>
|
||||
<div className="journal-card-image">
|
||||
|
||||
@@ -20,12 +20,20 @@ export default function RichContentCard(
|
||||
{ richContent, compact = false, thumbnailOverrideUrl, dumpHref }:
|
||||
RichContentCardProps,
|
||||
) {
|
||||
const { current, playing } = useContext(PlayerContext);
|
||||
const { current, playing, togglePlay } = useContext(PlayerContext);
|
||||
const { playRichContent, pending } = usePlayRichContent();
|
||||
|
||||
const canPlay = canPlayRichContent(richContent);
|
||||
const thumbnailSrc = thumbnailOverrideUrl ?? richContent.thumbnailUrl;
|
||||
// The dump's own thumbnail overrides the provider's, in the player header too.
|
||||
const playable = { ...richContent, thumbnailUrl: thumbnailSrc };
|
||||
// Nothing to play and no embed to fall back to: the card body is a link to
|
||||
// the source, so the thumbnail behaves like one rather than going dead.
|
||||
const playOrOpen = async () => {
|
||||
if (!await playRichContent(playable, dumpHref)) {
|
||||
globalThis.open(richContent.url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
};
|
||||
|
||||
const placeholder = {
|
||||
url: richContent.url,
|
||||
@@ -64,7 +72,12 @@ export default function RichContentCard(
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void playRichContent(playable, dumpHref);
|
||||
// The button says "Pause" once this card is the one playing, so it
|
||||
// has to pause. Re-playing would re-resolve the album and lose the
|
||||
// queue position. Embeds have no transport of their own, so they
|
||||
// keep restarting, as before.
|
||||
if (isActive && current?.kind === "stream") togglePlay();
|
||||
else void playOrOpen();
|
||||
}}
|
||||
aria-label={isPlaying ? "Pause" : "Play"}
|
||||
>
|
||||
@@ -105,7 +118,7 @@ export default function RichContentCard(
|
||||
type="button"
|
||||
className={`rich-content-thumbnail-btn${pending ? " is-pending" : ""}`}
|
||||
disabled={pending}
|
||||
onClick={() => void playRichContent(playable, dumpHref)}
|
||||
onClick={() => void playOrOpen()}
|
||||
aria-label="Play"
|
||||
>
|
||||
{thumbnailImg}
|
||||
|
||||
Reference in New Issue
Block a user