// Play-time resolution: Bandcamp tracklists and YouTube playlist tracklists, // their parsers and their routes. All network access is stubbed. import { get, html, stubNetwork } from "./helpers.ts"; import { assert, assertEquals, assertFalse } from "@std/assert"; import { parseTralbum, type Tralbum } from "../services/bandcamp-tralbum.ts"; import { parsePlaylistPage, type YouTubePlaylist, } from "../services/youtube-playlist-service.ts"; function escapeAttr(s: string): string { return s.replaceAll("&", "&").replaceAll('"', """); } function tralbumPage(data: unknown): string { return `
`; } const STREAM = "https://t4.bcbits.com/stream/abc/mp3-128/1?p=0&ts=1&t=x"; const ALBUM = { item_type: "album", artist: "Artist", art_id: 12345, current: { title: 'The "Album" & more' }, trackinfo: [ { track_num: 1, title: "One", duration: 180.5, streaming: 1, file: { "mp3-128": STREAM }, }, { track_num: 2, title: "Preorder", streaming: 1, unreleased_track: true, file: null, }, { track_num: 3, title: "", streaming: 1, file: { "mp3-128": "https://evil.test/x.mp3" }, }, { track_num: 4, title: "Capped", streaming: 1, is_capped: true, file: { "mp3-128": STREAM }, }, ], }; Deno.test("parseTralbum: tracks, streamability and art", () => { const album = parseTralbum( tralbumPage(ALBUM), "https://a.bandcamp.com/album/x", ); assert(album); assertEquals(album.itemType, "album"); assertEquals(album.artist, "Artist"); // A title containing a literal & survives the single-pass decode. assertEquals(album.title, 'The "Album" & more'); assertEquals(album.artworkUrl, "https://f4.bcbits.com/img/a12345_16.jpg"); assertEquals(album.tracks.map((t) => t.streamable), [ true, false, false, true, ]); assertEquals(album.tracks[0].streamUrl, STREAM); assertEquals(album.tracks[0].duration, 180.5); // A non-bcbits stream host is never relayed. assertEquals(album.tracks[2].streamUrl, null); assertEquals(album.tracks[2].title, "Track 3"); assert(album.tracks[3].capped); }); Deno.test("parseTralbum: pages it can't use", () => { assertEquals(parseTralbum("", "u"), null); assertEquals(parseTralbum(``, "u"), null); assertEquals( parseTralbum(tralbumPage({ ...ALBUM, hasAudio: false }), "u"), null, ); assertEquals( parseTralbum( tralbumPage({ ...ALBUM, trackinfo: [ALBUM.trackinfo[1]] }), "u", ), null, ); }); function ytPage(data: unknown): string { return ``; } const YT_DATA = { metadata: { playlistMetadataRenderer: { title: "My mix" } }, contents: { list: [ { lockupViewModel: { contentType: "LOCKUP_CONTENT_TYPE_VIDEO", contentId: "vid1", metadata: { lockupMetadataViewModel: { title: { content: "First" } }, }, contentImage: { overlays: [{ thumbnailBadgeViewModel: { text: "1:02:03" } }], }, }, }, { lockupViewModel: { contentType: "LOCKUP_CONTENT_TYPE_PLAYLIST", contentId: "ignored", }, }, { playlistVideoRenderer: { videoId: "vid2", title: { runs: [{ text: "Second" }] }, lengthSeconds: "235", }, }, ], }, }; Deno.test("parsePlaylistPage: current and legacy rows", () => { assertEquals(parsePlaylistPage(ytPage(YT_DATA), "PLx"), { listId: "PLx", title: "My mix", tracks: [ { videoId: "vid1", title: "First", duration: 3723 }, { videoId: "vid2", title: "Second", duration: 235 }, ], }); assertEquals(parsePlaylistPage("", "PLx"), null); assertEquals(parsePlaylistPage(ytPage({ contents: {} }), "PLx"), null); }); Deno.test("GET /api/bandcamp/tracks: resolves, caches, and forces a refetch", async () => { using net = stubNetwork(() => html(tralbumPage(ALBUM))); const url = "https://cached.bandcamp.com/album/x"; const res = await get