mymusics

retro MySpace-style music player
Log | Files | Refs | README

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 }