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:
@@ -21,16 +21,19 @@ export interface PlayableRichContent {
|
||||
* before. Bandcamp additionally honours GERBEUR_BANDCAMP_PLAYER: in "native"
|
||||
* mode the page is resolved to its mp3 streams and played as a queue, which is
|
||||
* what makes it autoplay and lets albums play through. Any failure — offline,
|
||||
* preorder-only release, Bandcamp changing its markup — silently falls back to
|
||||
* the embed, so this can never leave a card unplayable.
|
||||
* preorder-only release, Bandcamp changing its markup — falls back to the embed.
|
||||
*
|
||||
* Returns whether playback actually started. A native-mode page with no stored
|
||||
* embedUrl (an artist root, a /music index, a preorder) has nothing to fall
|
||||
* back to, and the caller has to be told so the click isn't swallowed.
|
||||
*/
|
||||
export function usePlayRichContent() {
|
||||
const { play, playQueue } = useContext(PlayerContext);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const playEmbed = useCallback(
|
||||
(rc: PlayableRichContent, dumpHref?: string) => {
|
||||
if (!rc.embedUrl) return;
|
||||
(rc: PlayableRichContent, dumpHref?: string): boolean => {
|
||||
if (!rc.embedUrl) return false;
|
||||
play({
|
||||
kind: "embed",
|
||||
embedUrl: rc.embedUrl,
|
||||
@@ -40,17 +43,15 @@ export function usePlayRichContent() {
|
||||
artworkUrl: rc.thumbnailUrl,
|
||||
subtitle: rc.siteName,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
[play],
|
||||
);
|
||||
|
||||
const playRichContent = useCallback(
|
||||
async (rc: PlayableRichContent, dumpHref?: string) => {
|
||||
async (rc: PlayableRichContent, dumpHref?: string): Promise<boolean> => {
|
||||
const native = rc.type === "bandcamp" && BANDCAMP_PLAYER === "native";
|
||||
if (!native) {
|
||||
playEmbed(rc, dumpHref);
|
||||
return;
|
||||
}
|
||||
if (!native) return playEmbed(rc, dumpHref);
|
||||
|
||||
setPending(true);
|
||||
try {
|
||||
@@ -62,9 +63,10 @@ export function usePlayRichContent() {
|
||||
fallbackArtworkUrl: rc.thumbnailUrl,
|
||||
});
|
||||
playQueue(items, 0);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn("bandcamp: native playback unavailable, using embed", err);
|
||||
playEmbed(rc, dumpHref);
|
||||
return playEmbed(rc, dumpHref);
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user