#!/usr/bin/env bun import { join } from "path"; import { $ } from "bun"; import { albums } from "../src/lib/list"; // Configuration const API_BASE = "https://api.song.link/v1-alpha.1/links"; const RATE_LIMIT_DELAY = 6000; // 6 seconds (10 requests per minute) const USER_COUNTRY = "DE"; // Germany const DATA_DIR = join(import.meta.dir, "../data"); const CACHE_FILE = join(DATA_DIR, "fetched-songs.json"); const OUTPUT_TS_FILE = join(import.meta.dir, "../src/lib/songLinks.ts"); const OUTPUT_JSON_FILE = join(DATA_DIR, "song-links.json"); interface SongLinks { spotify?: string; youtube?: string; youtubeMusic?: string; appleMusic?: string; } interface SongLinksData { [songId: string]: SongLinks; } interface CachedSongs { [songId: string]: boolean; } // Delay function for rate limiting const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // Load cached songs async function loadCache(): Promise { const file = Bun.file(CACHE_FILE); const exists = await file.exists(); if (!exists) { console.log("No cache file found, starting fresh..."); return {}; } try { return await file.json(); } catch { console.log("Invalid cache file, starting fresh..."); return {}; } } // Load existing song links async function loadExistingSongLinks(): Promise { const file = Bun.file(OUTPUT_JSON_FILE); const exists = await file.exists(); if (!exists) { console.log("No existing song links found, starting fresh..."); return {}; } try { return await file.json(); } catch { console.log("Invalid song links file, starting fresh..."); return {}; } } // Ensure data directory exists async function ensureDataDir(): Promise { await Bun.write(join(DATA_DIR, ".gitkeep"), ""); } // Save cache async function saveCache(cache: CachedSongs): Promise { await ensureDataDir(); await Bun.write(CACHE_FILE, JSON.stringify(cache, null, 2)); } // Save song links to JSON async function saveSongLinksJSON(songLinks: SongLinksData): Promise { await ensureDataDir(); await Bun.write(OUTPUT_JSON_FILE, JSON.stringify(songLinks, null, 2)); } // Save song links to TypeScript file async function saveSongLinksTS(songLinks: SongLinksData): Promise { const tsContent = `// This file is auto-generated by scripts/fetch-song-links.ts // Do not edit manually // Last updated: ${new Date().toISOString()} // Run: bun run fetch-links export interface SongLinks { spotify?: string; youtube?: string; youtubeMusic?: string; appleMusic?: string; } export interface SongLinksData { [songId: string]: SongLinks; } export function getSongLink(songId: string): SongLinks { return songLinks[songId]; } export const songLinks: SongLinksData = ${JSON.stringify(songLinks, null, 2)}; `; await Bun.write(OUTPUT_TS_FILE, tsContent); } // Fetch song links from API async function fetchSongLinks(spotifyUrl: string, songName: string): Promise { try { // Encode the Spotify URL const encodedUrl = encodeURIComponent(spotifyUrl); // Use the url parameter with songIfSingle for better matching const url = `${API_BASE}?url=${encodedUrl}&userCountry=${USER_COUNTRY}&songIfSingle=true`; console.log(` Fetching: ${songName}`); console.log(` Spotify URL: ${spotifyUrl}`); const response = await fetch(url); if (!response.ok) { const errorText = await response.text(); console.error(` āŒ Failed to fetch (${response.status}): ${errorText}`); return null; } const data = await response.json(); // Extract links from the response const links: SongLinks = { spotify: data.linksByPlatform?.spotify?.url || undefined, youtube: data.linksByPlatform?.youtube?.url || undefined, youtubeMusic: data.linksByPlatform?.youtubeMusic?.url || undefined, appleMusic: data.linksByPlatform?.appleMusic?.url || undefined, }; console.log(` āœ“ Success!`); return links; } catch (error) { console.error(` āŒ Error:`, error); return null; } } // Main function async function main() { console.log("šŸŽµ Starting song link fetcher...\n"); // Load existing data const cache = await loadCache(); const songLinks = await loadExistingSongLinks(); let requestCount = 0; let successCount = 0; let skipCount = 0; let failCount = 0; // Process all albums and tracks for (const album of albums) { console.log(`\nšŸ“€ Album: ${album.label}`); for (const track of album.tracks) { const songId = track.id; const songName = track.label; const spotifyUrl = track.__SPOTIFY_URL__; // Skip if already fetched if (cache[songId]) { console.log(` ā­ļø Skipping: ${songName} (already fetched)`); skipCount++; continue; } // Skip if no Spotify URL if (!spotifyUrl) { console.log(` ā­ļø Skipping: ${songName} (no Spotify URL)`); skipCount++; continue; } // Add delay before request (except for first request) if (requestCount > 0) { console.log(` ā³ Waiting ${RATE_LIMIT_DELAY / 1000}s (rate limit)...`); await delay(RATE_LIMIT_DELAY); } // Fetch links const links = await fetchSongLinks(spotifyUrl, songName); if (links) { songLinks[songId] = links; cache[songId] = true; successCount++; // Save progress after each successful fetch await saveSongLinksJSON(songLinks); await saveCache(cache); } else { failCount++; } requestCount++; } } // Save final TypeScript file console.log("\nšŸ’¾ Generating TypeScript file..."); await saveSongLinksTS(songLinks); // Summary console.log("\n" + "=".repeat(50)); console.log("šŸ“Š Summary:"); console.log(` Total requests: ${requestCount}`); console.log(` Successful: ${successCount}`); console.log(` Skipped: ${skipCount}`); console.log(` Failed: ${failCount}`); console.log("=".repeat(50)); // Format files with prettier console.log("\nšŸŽØ Formatting files with Prettier..."); try { await $`bunx prettier --write ${OUTPUT_TS_FILE} ${OUTPUT_JSON_FILE} ${CACHE_FILE}`; console.log(` āœ“ Formatted all files`); } catch (error) { console.error(" āš ļø Prettier formatting failed:", error); } console.log("\n✨ Done!"); console.log(`\nšŸ“ Output files:`); console.log(` - ${OUTPUT_TS_FILE}`); console.log(` - ${OUTPUT_JSON_FILE}`); console.log(` - ${CACHE_FILE}`); } // Run the script main().catch((error) => { console.error("āŒ Fatal error:", error); process.exit(1); });