/** * Robust iTunes Refresh Script * * Usage: * ADMIN_PASSWORD='your_password' node scripts/slow-refresh-itunes.js * * Options: * --force Overwrite existing release years */ const API_URL = process.env.API_URL || 'http://localhost:3010'; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; if (!ADMIN_PASSWORD) { console.error('❌ Error: ADMIN_PASSWORD environment variable is required.'); process.exit(1); } const FORCE_UPDATE = process.argv.includes('--force'); const 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'; // Helper for delays const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // Helper to clean search terms function cleanSearchTerm(text) { return text .replace(/_Unplugged/gi, '') .replace(/_Remastered/gi, '') .replace(/_Live/gi, '') .replace(/_Acoustic/gi, '') .replace(/_Radio Edit/gi, '') .replace(/_Extended/gi, '') .replace(/_/g, ' ') .trim(); } async function main() { console.log(`🎵 Starting iTunes Refresh Script`); console.log(` Target: ${API_URL}`); console.log(` Force Update: ${FORCE_UPDATE}`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); try { // 1. Authenticate console.log('🔑 Authenticating...'); const loginRes = await fetch(`${API_URL}/api/admin/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: ADMIN_PASSWORD }) }); if (!loginRes.ok) { throw new Error(`Login failed: ${loginRes.status} ${loginRes.statusText}`); } // We need to manually manage the cookie/header if the API uses cookies, // but the Admin UI uses a custom header 'x-admin-auth'. // Let's verify if the login endpoint returns a token or if we just use the password/flag. // Looking at the code, the client sets 'x-admin-auth' to 'authenticated' in localStorage. // The API middleware likely checks a cookie or just this header? // Let's check lib/auth.ts... actually, let's just assume we need to send the header. // Wait, the frontend sets 'x-admin-auth' to 'authenticated' after successful login. // The middleware likely checks the session cookie set by the login route. // Let's get the cookie from the login response const cookie = loginRes.headers.get('set-cookie'); const headers = { 'Content-Type': 'application/json', 'Cookie': cookie || '', 'x-admin-auth': 'authenticated' // Just in case }; // 2. Fetch Songs console.log('📥 Fetching song list...'); const songsRes = await fetch(`${API_URL}/api/songs`, { headers }); if (!songsRes.ok) throw new Error(`Failed to fetch songs: ${songsRes.status}`); const songs = await songsRes.json(); console.log(`📊 Found ${songs.length} songs.`); let processed = 0; let updated = 0; let skipped = 0; let failed = 0; for (const song of songs) { processed++; const progress = `[${processed}/${songs.length}]`; // Skip if year exists and not forcing if (song.releaseYear && !FORCE_UPDATE) { // console.log(`${progress} Skipping "${song.title}" (Year: ${song.releaseYear})`); skipped++; continue; } console.log(`${progress} Processing: "${song.title}" by "${song.artist}"`); const cleanArtist = cleanSearchTerm(song.artist); const cleanTitle = cleanSearchTerm(song.title); console.log(` → Searching: "${cleanTitle}" by "${cleanArtist}"`); // 3. Query iTunes with Retry Logic let year = null; let retries = 0; const MAX_RETRIES = 3; while (retries < MAX_RETRIES) { try { const term = encodeURIComponent(`${cleanArtist} ${cleanTitle}`); const itunesUrl = `https://itunes.apple.com/search?term=${term}&entity=song&limit=5`; const res = await fetch(itunesUrl, { headers: { 'User-Agent': USER_AGENT, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9' } }); if (res.status === 403 || res.status === 429) { console.warn(` ⚠️ iTunes Rate Limit (${res.status}). Pausing for 60s...`); await sleep(60000); retries++; continue; } if (!res.ok) { console.error(` ❌ iTunes Error: ${res.status}`); break; } const data = await res.json(); if (data.resultCount > 0) { // Simple extraction logic (same as lib/itunes.ts) let earliestYear = null; const normalizedTitle = song.title.toLowerCase().replace(/[^\w\s]/g, ''); const normalizedArtist = song.artist.toLowerCase().replace(/[^\w\s]/g, ''); for (const result of data.results) { const resTitle = result.trackName.toLowerCase().replace(/[^\w\s]/g, ''); const resArtist = result.artistName.toLowerCase().replace(/[^\w\s]/g, ''); if (resTitle.includes(normalizedTitle) && resArtist.includes(normalizedArtist)) { if (result.releaseDate) { const y = new Date(result.releaseDate).getFullYear(); if (!isNaN(y) && (earliestYear === null || y < earliestYear)) { earliestYear = y; } } } } year = earliestYear; } break; // Success } catch (e) { console.error(` ❌ Network Error: ${e.message}`); retries++; await sleep(5000); } } if (year) { if (year !== song.releaseYear) { console.log(` ✅ Found Year: ${year} (Old: ${song.releaseYear})`); // 4. Update Song const updateRes = await fetch(`${API_URL}/api/songs`, { method: 'PUT', headers, body: JSON.stringify({ id: song.id, title: song.title, artist: song.artist, releaseYear: year }) }); if (updateRes.ok) { updated++; } else { console.error(` ❌ Failed to update API: ${updateRes.status}`); failed++; } } else { console.log(` Create (No Change): ${year}`); skipped++; } } else { console.log(` ⚠️ No year found.`); failed++; } // Rate Limit Delay (15s = 4 req/min) await sleep(15000); } console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log('✅ Done!'); console.log(`Updated: ${updated} | Skipped: ${skipped} | Failed: ${failed}`); } catch (error) { console.error('❌ Fatal Error:', error); } } main();