import { PrismaClient } from '@prisma/client'; import { writeFile } from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const prisma = new PrismaClient(); // --- MusicBrainz Logic (Embedded to avoid TS import issues in Docker) --- 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 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function fetchWithRetry(url, maxRetries = 5) { let lastError = null; for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT, 'Accept': 'application/json' } }); if (response.status === 503) { const waitTime = Math.pow(2, attempt) * 1000; console.log(`Rate limited (503), waiting ${waitTime}ms before retry ${attempt + 1}/${maxRetries}...`); await sleep(waitTime); continue; } return response; } catch (error) { lastError = 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'); } async function getReleaseYear(artist, title) { try { const query = `artist:"${artist}" AND recording:"${title}"`; const url = `${MUSICBRAINZ_API_BASE}/recording?query=${encodeURIComponent(query)}&fmt=json&limit=10`; 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; } let earliestYear = null; for (const recording of data.recordings) { if (recording.releases && recording.releases.length > 0) { for (const release of recording.releases) { if (release.date) { const year = parseInt(release.date.split('-')[0]); if (!isNaN(year) && (earliestYear === null || year < earliestYear)) { earliestYear = year; } } } } if (recording['first-release-date']) { const year = parseInt(recording['first-release-date'].split('-')[0]); if (!isNaN(year) && (earliestYear === null || year < earliestYear)) { earliestYear = year; } } } return earliestYear; } catch (error) { console.error(`Error fetching release year for "${title}" by "${artist}":`, error.message); return null; } } // --- Migration Logic --- async function migrate() { console.log('šŸŽµ Starting release year migration...'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); try { // Find songs without release year const songs = await prisma.song.findMany({ where: { releaseYear: null }, orderBy: { id: 'asc' } }); console.log(`šŸ“Š Found ${songs.length} songs without release year.\n`); if (songs.length === 0) { console.log('āœ… All songs already have release years!'); await createFlagFile(); return; } let processed = 0; let successful = 0; let failed = 0; const startTime = Date.now(); for (const song of songs) { processed++; // const progress = `[${processed}/${songs.length}]`; try { // console.log(`${progress} Processing: "${song.title}" by "${song.artist}"`); const releaseYear = await getReleaseYear(song.artist, song.title); if (releaseYear) { await prisma.song.update({ where: { id: song.id }, data: { releaseYear } }); successful++; // console.log(` āœ… Updated with year: ${releaseYear}`); } else { failed++; // console.log(` āš ļø No release year found`); } } catch (error) { failed++; console.error(` āŒ Error processing song:`, error instanceof Error ? error.message : error); } // Progress update every 10 songs (less verbose) if (processed % 10 === 0 || processed === songs.length) { const elapsed = Math.round((Date.now() - startTime) / 1000); const rate = processed / (elapsed || 1); const remaining = songs.length - processed; const eta = Math.round(remaining / rate); process.stdout.write(`\ršŸ“ˆ Progress: ${processed}/${songs.length} | Success: ${successful} | Failed: ${failed} | ETA: ${eta}s`); } } const totalTime = Math.round((Date.now() - startTime) / 1000); console.log('\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log('āœ… Migration completed!'); console.log(`šŸ“Š Total: ${processed} | Success: ${successful} | Failed: ${failed}`); console.log(`ā±ļø Time: ${totalTime}s (${(processed / (totalTime || 1)).toFixed(2)} songs/s)`); await createFlagFile(); } catch (error) { console.error('āŒ Migration failed:', error); throw error; } finally { await prisma.$disconnect(); } } async function createFlagFile() { const flagPath = path.join(process.cwd(), '.release-years-migrated'); await writeFile(flagPath, new Date().toISOString()); console.log(`\nšŸ Created flag file: ${flagPath}`); } migrate();