From e5d06029efae82efef7d0f26685db7a94b0bcc27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=B6rdle=20Bot?= Date: Mon, 24 Nov 2025 15:27:52 +0100 Subject: [PATCH] feat: Add `slow-refresh-itunes.js` for robust iTunes year updates and remove `migrate-covers.mjs` from docker-compose. --- docker-compose.example.yml | 2 +- scripts/slow-refresh-itunes.js | 194 +++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 scripts/slow-refresh-itunes.js diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 28286d5..5337d37 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -26,4 +26,4 @@ services: start_period: 40s # Run migrations and start server (auto-baseline on first run if needed) command: > - sh -c "npx prisma migrate deploy || (echo 'Baselining existing database...' && sh scripts/baseline-migrations.sh && npx prisma migrate deploy) && node scripts/migrate-covers.mjs && node server.js" + sh -c "npx prisma migrate deploy || (echo 'Baselining existing database...' && sh scripts/baseline-migrations.sh && npx prisma migrate deploy) && node server.js" diff --git a/scripts/slow-refresh-itunes.js b/scripts/slow-refresh-itunes.js new file mode 100644 index 0000000..0ec235f --- /dev/null +++ b/scripts/slow-refresh-itunes.js @@ -0,0 +1,194 @@ + +/** + * 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)); + +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}"`); + + // 3. Query iTunes with Retry Logic + let year = null; + let retries = 0; + const MAX_RETRIES = 3; + + while (retries < MAX_RETRIES) { + try { + const term = encodeURIComponent(`${song.artist} ${song.title}`); + 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();