Add master display UI with audio output management and styling
- Implement main.js for the master display functionality, including WebSocket connection, audio output management, and state handling. - Create style.css for the master display's visual design, ensuring a cohesive look and feel with a dark theme and responsive layout. - Integrate device management with a fallback for non-Electron environments, allowing users to select audio outputs. - Add features for managing favorites, including toggling favorites and filtering by genre. - Enhance user experience with a responsive favorites grid and drag-to-scroll functionality.
This commit is contained in:
65
server/scripts/download-images.js
Normal file
65
server/scripts/download-images.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// Backfill cover-art for every station missing a local image.
|
||||
//
|
||||
// node server/scripts/download-images.js # only stations missing a local file
|
||||
// node server/scripts/download-images.js --all # re-download even cached stations
|
||||
// node server/scripts/download-images.js --no-scrape # skip homepage scrape fallback
|
||||
//
|
||||
// Strategy per station:
|
||||
// 1. Try downloading the existing image_url.
|
||||
// 2. On failure (404, HTML, dead host, ...), call scrapeIcon() to find a
|
||||
// fresh icon from Radio-Browser / the station homepage, persist the new
|
||||
// URL, and try again.
|
||||
//
|
||||
// Safe to re-run.
|
||||
|
||||
import 'dotenv/config';
|
||||
import { initDb } from '../db/index.js';
|
||||
import { listStations, updateStation } from '../stations.js';
|
||||
import { saveStationImageFromUrl, ensureImageDirs } from '../media/images.js';
|
||||
import { scrapeIcon } from '../sources/iconScraper.js';
|
||||
|
||||
const FORCE = process.argv.includes('--all');
|
||||
const NO_SCRAPE = process.argv.includes('--no-scrape');
|
||||
const CONCURRENCY = 4;
|
||||
|
||||
initDb(process.env.DB_PATH || './data/db/oradio.sqlite');
|
||||
ensureImageDirs();
|
||||
|
||||
const all = listStations({ enabled: null });
|
||||
const todo = all.filter((s) => FORCE || !s.image_path);
|
||||
console.log(`[images] ${todo.length} of ${all.length} stations to fetch (force=${FORCE}, scrape=${!NO_SCRAPE})`);
|
||||
|
||||
let i = 0, ok = 0, scraped = 0, fail = 0;
|
||||
async function worker() {
|
||||
while (i < todo.length) {
|
||||
const idx = i++;
|
||||
const s = todo[idx];
|
||||
const label = `[${idx + 1}/${todo.length}] ${s.name}`;
|
||||
|
||||
// 1. Try the existing remote URL.
|
||||
let rel = s.image_url
|
||||
? await saveStationImageFromUrl(s.id, s.image_url, { source: s.image_source || 'remote' })
|
||||
: null;
|
||||
|
||||
// 2. Fallback: scrape a fresh icon from the homepage / Radio-Browser.
|
||||
if (!rel && !NO_SCRAPE) {
|
||||
const found = await scrapeIcon(s);
|
||||
if (found && found !== s.image_url) {
|
||||
updateStation(s.id, { image_url: found });
|
||||
rel = await saveStationImageFromUrl(s.id, found, { source: 'scraped' });
|
||||
if (rel) scraped++;
|
||||
}
|
||||
}
|
||||
|
||||
if (rel) {
|
||||
ok++;
|
||||
console.log(` ${label} -> ${rel}`);
|
||||
} else {
|
||||
fail++;
|
||||
console.log(` ${label} ✗ (${s.image_url || 'no url'})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
|
||||
console.log(`[images] done. ok=${ok} (scraped=${scraped}) fail=${fail}`);
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user