Add player functionality with HLS support and API integration
- Implemented a new Player class in player.js to handle audio playback, including HLS support using hls.js. - Created a shared API module in api.js for making HTTP requests with proper error handling. - Added DOM utility functions in dom.js for creating and clearing elements. - Introduced WebSocket connection handling in ws.js for real-time updates. - Developed a comprehensive CSS stylesheet for styling the application, including a high-contrast theme.
This commit is contained in:
73
server/scripts/check-images.js
Normal file
73
server/scripts/check-images.js
Normal file
@@ -0,0 +1,73 @@
|
||||
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)'}`);
|
||||
Reference in New Issue
Block a user