Add API documentation and underground station importer

- Introduced a new HTML documentation page for the oradio API, including a JavaScript file to handle dynamic content and API requests.
- Added a CSS file for styling the documentation page.
- Implemented an underground station importer script that fetches data from Radio-Browser and writes it to a JSON file.
- Created a stats module to compute and manage vote and play statistics for radio stations.
- Added a polyfill for modulepreload to ensure compatibility with older browsers.
This commit is contained in:
Marco Mooren
2026-05-11 02:06:48 +02:00
parent e0a60f7b64
commit 00246389bc
52 changed files with 6280 additions and 2475 deletions

View File

@@ -10,64 +10,64 @@ export const router = Router();
router.use(requireAdmin);
router.post('/health-check', async (_req, res) => {
const n = await runHealthCheck();
res.json({ checked: n });
const n = await runHealthCheck();
res.json({ checked: n });
});
router.post('/reseed', (_req, res) => {
res.json(applySeedIfEmpty());
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())
});
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 });
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 });
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) });
}
}
} 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);
await Promise.all(Array.from({ length: concurrency }, worker));
res.json(results);
});