/** * Turns a Bandcamp page into a queue of streams for the site's own player. * `usePlayRichContent` calls it when a Bandcamp link is played in native mode, * and `streamSources.ts` again when the streams' signed URLs expire. * * @module */ import { API_URL } from "../../config/api.ts"; import type { PlayerItem } from "../../contexts/player/PlayerContext.ts"; import type { Tralbum, TralbumTrack } from "../../../shared/player.ts"; /** * Passed to {@link resolveBandcampQueue} by its callers and copied onto every * item of the queue, since the server's tracklist knows nothing of the dump. */ export interface BandcampContext { dumpHref?: string; /** Lets PlayerProvider fall back to Bandcamp's iframe if the streams fail. */ embedUrl?: string; /** Used only if the page carries no album art of its own. */ fallbackArtworkUrl?: string; } /** * Resolves the Bandcamp page `pageUrl` into one `stream` item per streamable * track, through `GET /api/bandcamp/tracks`, each carrying `ctx`. `force` * bypasses the server's cache, for re-resolving once the signed URLs expire. * * @throws {Error} When the request fails or the page has no streamable track. * Callers then fall back to the embed. */ export async function resolveBandcampQueue( pageUrl: string, ctx: BandcampContext = {}, { force = false }: { force?: boolean } = {}, ): Promise { const res = await fetch( `${API_URL}/api/bandcamp/tracks?url=${encodeURIComponent(pageUrl)}${ force ? "&force=1" : "" }`, ); if (!res.ok) throw new Error(`resolve failed: ${res.status}`); const body = await res.json() as { success: boolean; data?: Tralbum }; const album = body.data; if (!body.success || !album) throw new Error("resolve returned no data"); const items = album.tracks .filter((t): t is TralbumTrack & { streamUrl: string } => t.streamable && typeof t.streamUrl === "string" ) .map((t): PlayerItem => ({ kind: "stream", streamUrl: t.streamUrl, type: "bandcamp", // The header and the tracklist name the track; the artist goes below. title: t.title, subtitle: album.artist, duration: t.duration, trackNum: t.trackNum, artworkUrl: album.artworkUrl ?? ctx.fallbackArtworkUrl, dumpHref: ctx.dumpHref, resolveUrl: album.sourceUrl, resolveIndex: t.index, resolvedAt: album.resolvedAt, embedUrl: ctx.embedUrl, })); if (items.length === 0) throw new Error("no streamable tracks"); return items; }