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:
Marco Mooren
2026-05-10 14:43:00 +02:00
commit e0a60f7b64
51 changed files with 9022 additions and 0 deletions

122
server/sources/seed.js Normal file
View File

@@ -0,0 +1,122 @@
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();
}