import 'dotenv/config'; import Database from 'better-sqlite3'; const db = new Database(process.env.DB_PATH || './data/db/oradio.sqlite'); const rows = db.prepare(`SELECT id, name, image_url FROM stations WHERE image_url IS NOT NULL AND image_url != ''`).all(); const TIMEOUT_MS = 8000; const CONCURRENCY = 12; const apply = process.argv.includes('--apply'); async function check(url) { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), TIMEOUT_MS); const headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9' }; try { // Many CDNs don't support HEAD or return wrong content-type for HEAD; use a ranged GET. const res = await fetch(url, { method: 'GET', redirect: 'follow', signal: ctrl.signal, headers: { ...headers, Range: 'bytes=0-1023' } }); if (!res.ok && res.status !== 206) { // Treat 4xx (except 429 rate-limit) as broken; 5xx as transient → keep. if (res.status === 429) return { ok: true, transient: true }; if (res.status >= 500) return { ok: true, transient: true }; return { ok: false, reason: `HTTP ${res.status}` }; } const ct = (res.headers.get('content-type') || '').toLowerCase(); if (ct && !ct.startsWith('image/') && !ct.includes('octet-stream')) { return { ok: false, reason: `bad content-type ${ct}` }; } // Drain a small amount so the body is closed cleanly. try { await res.arrayBuffer(); } catch { } return { ok: true }; } catch (err) { const code = err.cause?.code || err.code || err.name; // DNS / connection failures are definitive. if (['ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ETIMEDOUT'].includes(code)) { return { ok: false, reason: code }; } if (err.name === 'AbortError') return { ok: true, transient: true }; // timeout, keep return { ok: false, reason: code || err.message }; } finally { clearTimeout(t); } } const upd = db.prepare('UPDATE stations SET image_url = NULL WHERE id = ?'); let bad = 0, ok = 0; async function worker(queue) { while (queue.length) { const r = queue.shift(); const res = await check(r.image_url); if (res.ok) { ok++; } else { bad++; console.log(`BAD ${r.name} :: ${res.reason} :: ${r.image_url}`); if (apply) upd.run(r.id); } } } const queue = rows.slice(); console.log(`Checking ${queue.length} image_url values (apply=${apply})...`); await Promise.all(Array.from({ length: CONCURRENCY }, () => worker(queue))); console.log(`Done. ok=${ok} bad=${bad}${apply ? ' (cleared)' : ' (dry run; pass --apply to clear)'}`);