feat: Add slow-refresh-itunes.js for robust iTunes year updates and remove migrate-covers.mjs from docker-compose.
This commit is contained in:
194
scripts/slow-refresh-itunes.js
Normal file
194
scripts/slow-refresh-itunes.js
Normal file
@@ -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();
|
||||
Reference in New Issue
Block a user