/** * iTunes Search API integration for fetching release years * API Documentation: https://performance-partners.apple.com/search-api */ interface ItunesResult { wrapperType: string; kind: string; artistName: string; collectionName: string; trackName: string; releaseDate: string; primaryGenreName: string; } interface ItunesResponse { resultCount: number; results: ItunesResult[]; } // Rate limiting state let lastRequestTime = 0; let blockedUntil = 0; const MIN_INTERVAL = 2000; // 2 seconds = 30 requests per minute const BLOCK_DURATION = 60000; // 60 seconds pause after 403 // Mutex for serializing requests let requestQueue = Promise.resolve(null); /** * Get the earliest release year for a song from iTunes * @param artist Artist name * @param title Song title * @returns Release year or null if not found */ export async function getReleaseYearFromItunes(artist: string, title: string): Promise { // Queue the request to ensure sequential execution and rate limiting const result = requestQueue.then(() => executeRequest(artist, title)); // Update queue to wait for this request requestQueue = result.catch(() => null); return result; } async function executeRequest(artist: string, title: string): Promise { try { // Check if blocked const now = Date.now(); if (now < blockedUntil) { const waitTime = blockedUntil - now; console.log(`iTunes API blocked (403/429). Waiting ${Math.ceil(waitTime / 1000)}s before next request...`); await new Promise(resolve => setTimeout(resolve, waitTime)); } // Enforce rate limit (min interval) const timeSinceLast = Date.now() - lastRequestTime; if (timeSinceLast < MIN_INTERVAL) { const delay = MIN_INTERVAL - timeSinceLast; await new Promise(resolve => setTimeout(resolve, delay)); } // Construct search URL const term = encodeURIComponent(`${artist} ${title}`); const url = `https://itunes.apple.com/search?term=${term}&entity=song&limit=10`; const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9' } }); lastRequestTime = Date.now(); if (response.status === 403 || response.status === 429) { console.warn(`iTunes API rate limit hit (${response.status}). Pausing for 60s.`); blockedUntil = Date.now() + BLOCK_DURATION; return null; } if (!response.ok) { console.error(`iTunes API error: ${response.status} ${response.statusText}`); return null; } const data: ItunesResponse = await response.json(); if (data.resultCount === 0) { return null; } // Filter for exact(ish) matches to avoid wrong songs // and find the earliest release date let earliestYear: number | null = null; const normalizedTitle = title.toLowerCase().replace(/[^\w\s]/g, ''); const normalizedArtist = artist.toLowerCase().replace(/[^\w\s]/g, ''); for (const result of data.results) { // Basic validation that it's the right song const resTitle = result.trackName.toLowerCase().replace(/[^\w\s]/g, ''); const resArtist = result.artistName.toLowerCase().replace(/[^\w\s]/g, ''); // Check if title and artist are contained in the result (fuzzy match) if (resTitle.includes(normalizedTitle) && resArtist.includes(normalizedArtist)) { if (result.releaseDate) { const year = new Date(result.releaseDate).getFullYear(); if (!isNaN(year)) { if (earliestYear === null || year < earliestYear) { earliestYear = year; } } } } } return earliestYear; } catch (error) { console.error(`Error fetching release year from iTunes for "${title}" by "${artist}":`, error); return null; } }