/** * MusicBrainz API integration for fetching release years * API Documentation: https://musicbrainz.org/doc/MusicBrainz_API * Rate Limiting: 50 requests per second for meaningful User-Agent strings */ const MUSICBRAINZ_API_BASE = 'https://musicbrainz.org/ws/2'; const USER_AGENT = 'hoerdle/0.1.0 ( elpatron@mailbox.org )'; const RATE_LIMIT_DELAY = 25; // 25ms between requests = ~40 req/s (safe margin) /** * Sleep utility for rate limiting */ function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Fetch with retry logic for HTTP 503 (rate limit exceeded) */ async function fetchWithRetry(url: string, maxRetries = 5): Promise { let lastError: Error | null = null; for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT, 'Accept': 'application/json' } }); // If rate limited (503), wait with exponential backoff if (response.status === 503) { const waitTime = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s console.log(`Rate limited (503), waiting ${waitTime}ms before retry ${attempt + 1}/${maxRetries}...`); await sleep(waitTime); continue; } return response; } catch (error) { lastError = error as Error; if (attempt < maxRetries - 1) { const waitTime = Math.pow(2, attempt) * 1000; console.log(`Network error, waiting ${waitTime}ms before retry ${attempt + 1}/${maxRetries}...`); await sleep(waitTime); } } } throw lastError || new Error('Max retries exceeded'); } /** * Get the earliest release year for a song from MusicBrainz * @param artist Artist name * @param title Song title * @returns Release year or null if not found */ export async function getReleaseYear(artist: string, title: string): Promise { try { // Build search query using Lucene syntax const query = `artist:"${artist}" AND recording:"${title}"`; const url = `${MUSICBRAINZ_API_BASE}/recording?query=${encodeURIComponent(query)}&fmt=json&limit=10`; // Add rate limiting delay await sleep(RATE_LIMIT_DELAY); const response = await fetchWithRetry(url); if (!response.ok) { console.error(`MusicBrainz API error: ${response.status} ${response.statusText}`); return null; } const data = await response.json(); if (!data.recordings || data.recordings.length === 0) { console.log(`No recordings found for "${title}" by "${artist}"`); return null; } // Find the earliest release year from all recordings let earliestYear: number | null = null; for (const recording of data.recordings) { // Check if recording has releases if (recording.releases && recording.releases.length > 0) { for (const release of recording.releases) { if (release.date) { // Extract year from date (format: YYYY-MM-DD or YYYY) const year = parseInt(release.date.split('-')[0]); if (!isNaN(year) && (earliestYear === null || year < earliestYear)) { earliestYear = year; } } } } // Also check first-release-date on the recording itself if (recording['first-release-date']) { const year = parseInt(recording['first-release-date'].split('-')[0]); if (!isNaN(year) && (earliestYear === null || year < earliestYear)) { earliestYear = year; } } } if (earliestYear) { console.log(`Found release year ${earliestYear} for "${title}" by "${artist}"`); } else { console.log(`No release year found for "${title}" by "${artist}"`); } return earliestYear; } catch (error) { console.error(`Error fetching release year for "${title}" by "${artist}":`, error); return null; } }