Finalize scoring system, release year integration, and fix song deletion
This commit is contained in:
190
scripts/migrate-release-years.mjs
Normal file
190
scripts/migrate-release-years.mjs
Normal file
@@ -0,0 +1,190 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user