- 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.
74 lines
2.6 KiB
JavaScript
74 lines
2.6 KiB
JavaScript
import { Router } from 'express';
|
|
import { requireAdmin } from '../auth.js';
|
|
import { runHealthCheck } from '../streams/checker.js';
|
|
import { applySeedIfEmpty } from '../sources/seed.js';
|
|
import { getDb } from '../db/index.js';
|
|
import { scrapeIcon } from '../sources/iconScraper.js';
|
|
import { listStations, getStation, updateStation } from '../stations.js';
|
|
|
|
export const router = Router();
|
|
router.use(requireAdmin);
|
|
|
|
router.post('/health-check', async (_req, res) => {
|
|
const n = await runHealthCheck();
|
|
res.json({ checked: n });
|
|
});
|
|
|
|
router.post('/reseed', (_req, res) => {
|
|
res.json(applySeedIfEmpty());
|
|
});
|
|
|
|
router.get('/system', (_req, res) => {
|
|
const db = getDb();
|
|
res.json({
|
|
stations: db.prepare('SELECT COUNT(*) AS n FROM stations').get().n,
|
|
streams: db.prepare('SELECT COUNT(*) AS n FROM streams').get().n,
|
|
users: db.prepare('SELECT COUNT(*) AS n FROM users').get().n,
|
|
favorites: db.prepare('SELECT COUNT(*) AS n FROM favorites').get().n,
|
|
node: process.version,
|
|
uptime_s: Math.round(process.uptime())
|
|
});
|
|
});
|
|
|
|
// Scrape an icon for a single station.
|
|
router.post('/stations/:id/scrape-icon', async (req, res) => {
|
|
const id = Number(req.params.id);
|
|
const st = getStation(id);
|
|
if (!st) return res.status(404).json({ error: 'not found' });
|
|
const url = await scrapeIcon(st);
|
|
if (!url) return res.status(404).json({ error: 'no icon found' });
|
|
const updated = updateStation(id, { image_url: url });
|
|
res.json({ id, image_url: url, station: updated });
|
|
});
|
|
|
|
// Bulk: scrape icons for every station (optionally only those missing one).
|
|
router.post('/scrape-icons', async (req, res) => {
|
|
const onlyMissing = req.query.all !== '1';
|
|
const stations = listStations({ enabled: null }).filter((s) => !onlyMissing || !s.image_url);
|
|
const results = { total: stations.length, updated: 0, skipped: 0, failed: 0, items: [] };
|
|
// Limit concurrency to avoid hammering hosts.
|
|
const concurrency = 4;
|
|
let i = 0;
|
|
async function worker() {
|
|
while (i < stations.length) {
|
|
const s = stations[i++];
|
|
try {
|
|
const url = await scrapeIcon(s);
|
|
if (url) {
|
|
updateStation(s.id, { image_url: url });
|
|
results.updated++;
|
|
results.items.push({ id: s.id, name: s.name, image_url: url });
|
|
} else {
|
|
results.failed++;
|
|
results.items.push({ id: s.id, name: s.name, image_url: null });
|
|
}
|
|
} catch (err) {
|
|
results.failed++;
|
|
results.items.push({ id: s.id, name: s.name, error: String(err?.message || err) });
|
|
}
|
|
}
|
|
}
|
|
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
res.json(results);
|
|
});
|