210 lines
8.1 KiB
JavaScript
210 lines
8.1 KiB
JavaScript
|
|
/**
|
|
* 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}"`);
|
|
|
|
// 3. Query iTunes with Retry Logic
|
|
let year = null;
|
|
let retries = 0;
|
|
const MAX_RETRIES = 3;
|
|
|
|
while (retries < MAX_RETRIES) {
|
|
try {
|
|
const cleanArtist = cleanSearchTerm(song.artist);
|
|
const cleanTitle = cleanSearchTerm(song.title);
|
|
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();
|