- 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.
123 lines
4.0 KiB
JavaScript
123 lines
4.0 KiB
JavaScript
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { resolve, dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { createHash, randomUUID } from 'node:crypto';
|
|
import { getDb } from '../db/index.js';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const SEED_DIR = resolve(__dirname, '../../data/seed');
|
|
|
|
// Deterministic UUID v5-style derived from slug; stable across DB rebuilds.
|
|
function uuidFromSlug(slug) {
|
|
const h = createHash('sha1').update('oradio:' + slug).digest('hex');
|
|
return [
|
|
h.slice(0, 8),
|
|
h.slice(8, 12),
|
|
'5' + h.slice(13, 16),
|
|
'8' + h.slice(17, 20),
|
|
h.slice(20, 32)
|
|
].join('-');
|
|
}
|
|
|
|
function loadAllSeedFiles() {
|
|
const files = readdirSync(SEED_DIR)
|
|
.filter((f) => f.startsWith('stations') && f.endsWith('.json'))
|
|
.sort();
|
|
const all = [];
|
|
for (const f of files) {
|
|
try {
|
|
const data = JSON.parse(readFileSync(join(SEED_DIR, f), 'utf8'));
|
|
if (Array.isArray(data)) all.push(...data);
|
|
} catch (err) {
|
|
console.warn(`[seed] failed to load ${f}:`, err.message);
|
|
}
|
|
}
|
|
return all;
|
|
}
|
|
|
|
export function loadSeedFile() {
|
|
return loadAllSeedFiles();
|
|
}
|
|
|
|
export function loadCategoriesFile() {
|
|
try {
|
|
const txt = readFileSync(join(SEED_DIR, 'categories.json'), 'utf8');
|
|
return JSON.parse(txt);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge-by-UUID seeder. Inserts stations and streams whose UUIDs are not yet in
|
|
* the database. Existing stations are left untouched (admin edits are preserved).
|
|
*/
|
|
export function applySeed() {
|
|
const db = getDb();
|
|
|
|
const stationByUuid = db.prepare('SELECT id FROM stations WHERE uuid = ?');
|
|
const streamByUuid = db.prepare('SELECT id FROM streams WHERE uuid = ?');
|
|
|
|
const insertStation = db.prepare(`
|
|
INSERT INTO stations (uuid, name, slug, homepage, country, genres, description, image_url, category, source, source_ref)
|
|
VALUES (@uuid, @name, @slug, @homepage, @country, @genres, @description, @image_url, @category, 'seed', @slug)
|
|
`);
|
|
const insertStream = db.prepare(`
|
|
INSERT INTO streams (uuid, station_id, url, format, bitrate, label, priority)
|
|
VALUES (@uuid, @station_id, @url, @format, @bitrate, @label, @priority)
|
|
`);
|
|
|
|
const entries = loadAllSeedFiles();
|
|
let inserted = 0;
|
|
let streamsInserted = 0;
|
|
let skipped = 0;
|
|
|
|
const tx = db.transaction((list) => {
|
|
for (const s of list) {
|
|
const uuid = s.uuid || uuidFromSlug(s.slug);
|
|
const existing = stationByUuid.get(uuid);
|
|
if (existing) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
const info = insertStation.run({
|
|
uuid,
|
|
name: s.name,
|
|
slug: s.slug,
|
|
homepage: s.homepage ?? null,
|
|
country: s.country ?? null,
|
|
genres: JSON.stringify(s.genres ?? []),
|
|
description: s.description ?? null,
|
|
image_url: s.image_url ?? null,
|
|
category: s.category ?? null
|
|
});
|
|
const stationId = info.lastInsertRowid;
|
|
let priority = 0;
|
|
for (const st of s.streams ?? []) {
|
|
const streamUuid = st.uuid || randomUUID();
|
|
if (streamByUuid.get(streamUuid)) continue;
|
|
insertStream.run({
|
|
uuid: streamUuid,
|
|
station_id: stationId,
|
|
url: st.url,
|
|
format: st.format ?? 'unknown',
|
|
bitrate: st.bitrate ?? null,
|
|
label: st.label ?? null,
|
|
priority: st.priority ?? priority
|
|
});
|
|
streamsInserted++;
|
|
priority++;
|
|
}
|
|
inserted++;
|
|
}
|
|
});
|
|
tx(entries);
|
|
|
|
return { inserted, streamsInserted, skipped, total: entries.length };
|
|
}
|
|
|
|
// Back-compat shim: bootstrap and reseed call applySeedIfEmpty(); now always merges.
|
|
export function applySeedIfEmpty() {
|
|
return applySeed();
|
|
}
|