metadata.ts (2817B)
1 import fs from "node:fs"; 2 3 /** Internet Archive item containing the Dragon Hoard ZIPs. */ 4 export const IA_DRAGON_HOARD_ID = "myspace_dragon_hoard_2010"; 5 6 export interface TrackMeta { 7 id: string; 8 title: string; 9 artist: string; 10 /** Basename, e.g. std_xxx.mp3 */ 11 fileKey: string; 12 /** Original MySpace CDN URL from the TSV (last column). */ 13 cdnUrl: string; 14 /** Direct Internet Archive download URL (file inside the collection ZIP). */ 15 archiveUrl: string; 16 } 17 18 function basenameFromUrl(url: string): string | null { 19 const u = url.trim(); 20 if (!u) return null; 21 const last = u.replace(/\/+$/, "").split("/").pop() ?? ""; 22 return last.toLowerCase().endsWith(".mp3") ? last : null; 23 } 24 25 /** 26 * Build an archive.org download URL for an MP3 inside a collection ZIP, matching 27 * the Hobbit / ia-myspace-music-search player logic. 28 * @see https://github.com/jbaicoianu/ia-myspace-music-search/blob/master/src/viewer.js 29 */ 30 export function buildArchiveDownloadUrl( 31 cdnUrl: string, 32 itemId: string = IA_DRAGON_HOARD_ID, 33 ): string | null { 34 let parsed: URL; 35 try { 36 parsed = new URL(cdnUrl.trim()); 37 } catch { 38 return null; 39 } 40 if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null; 41 const pathParts = parsed.pathname.split("/").filter(Boolean); 42 if (pathParts.length < 2) return null; 43 const collection = pathParts[0]!; 44 const fname = pathParts[pathParts.length - 1]!; 45 if (!fname.toLowerCase().endsWith(".mp3")) return null; 46 return `https://archive.org/download/${itemId}/${collection}.zip/${encodeURIComponent(`${collection}/${fname}`)}`; 47 } 48 49 /** Parse one TSV line into track metadata, or null if invalid. */ 50 export function parseTrackLine(line: string, itemId: string = IA_DRAGON_HOARD_ID): TrackMeta | null { 51 if (!line.trim()) return null; 52 const parts = line.split("\t"); 53 if (parts.length < 4) return null; 54 const id = parts[0]!; 55 const title = parts[1]!; 56 const artist = parts[3]!; 57 const cdnUrl = parts[parts.length - 1]!; 58 const archiveUrl = buildArchiveDownloadUrl(cdnUrl, itemId); 59 if (!archiveUrl) return null; 60 const bn = basenameFromUrl(cdnUrl); 61 if (!bn) return null; 62 return { 63 id, 64 title, 65 artist, 66 fileKey: bn, 67 cdnUrl, 68 archiveUrl, 69 }; 70 } 71 72 /** 73 * Read metadata.tsv and return every track that maps to a valid Internet Archive URL. 74 * Does not require local MP3 files. 75 */ 76 export function loadTracksFromTsv(tsvPath: string, itemId: string = IA_DRAGON_HOARD_ID): TrackMeta[] { 77 if (!fs.existsSync(tsvPath)) { 78 throw new Error(`METADATA_TSV not found: ${tsvPath}`); 79 } 80 81 const out: TrackMeta[] = []; 82 const raw = fs.readFileSync(tsvPath, "utf-8"); 83 const lines = raw.split(/\r?\n/); 84 for (const line of lines) { 85 const track = parseTrackLine(line, itemId); 86 if (track) out.push(track); 87 } 88 return out; 89 }