- Implemented a new API endpoint for retrieving and managing user favorites in /api/users. - Added functionality for admins to edit the shared "main" user's favorites. - Created a one-shot DB smoke test script for verifying multi-user kiosk migrations. - Introduced a RoomClock class for synchronizing server time across clients using WebSocket.
96 lines
4.0 KiB
JavaScript
96 lines
4.0 KiB
JavaScript
import Database from 'better-sqlite3';
|
|
import { readFileSync, mkdirSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
let db;
|
|
|
|
export function initDb(dbPath) {
|
|
const abs = resolve(dbPath);
|
|
mkdirSync(dirname(abs), { recursive: true });
|
|
db = new Database(abs);
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('foreign_keys = ON');
|
|
const schema = readFileSync(resolve(__dirname, 'schema.sql'), 'utf8');
|
|
db.exec(schema);
|
|
runMigrations(db);
|
|
return db;
|
|
}
|
|
|
|
export function getDb() {
|
|
if (!db) throw new Error('DB not initialized');
|
|
return db;
|
|
}
|
|
|
|
// Idempotent migrations for upgrading older DBs that pre-date a column.
|
|
function runMigrations(db) {
|
|
const stationCols = new Set(db.prepare("PRAGMA table_info(stations)").all().map((c) => c.name));
|
|
if (!stationCols.has('uuid')) {
|
|
db.exec('ALTER TABLE stations ADD COLUMN uuid TEXT');
|
|
}
|
|
if (!stationCols.has('category')) {
|
|
db.exec('ALTER TABLE stations ADD COLUMN category TEXT');
|
|
}
|
|
if (!stationCols.has('image_path')) {
|
|
db.exec('ALTER TABLE stations ADD COLUMN image_path TEXT');
|
|
}
|
|
if (!stationCols.has('image_source')) {
|
|
db.exec('ALTER TABLE stations ADD COLUMN image_source TEXT');
|
|
}
|
|
const streamCols = new Set(db.prepare("PRAGMA table_info(streams)").all().map((c) => c.name));
|
|
if (!streamCols.has('uuid')) {
|
|
db.exec('ALTER TABLE streams ADD COLUMN uuid TEXT');
|
|
}
|
|
|
|
// Backfill UUIDs. For RB stations, prefer the existing source_ref so the
|
|
// public UUID matches the upstream Radio-Browser stationuuid.
|
|
const setStationUuid = db.prepare('UPDATE stations SET uuid = ? WHERE id = ?');
|
|
for (const row of db.prepare("SELECT id, source, source_ref FROM stations WHERE uuid IS NULL OR uuid = ''").all()) {
|
|
const u = (row.source === 'radiobrowser' && row.source_ref) ? row.source_ref : randomUUID();
|
|
setStationUuid.run(u, row.id);
|
|
}
|
|
|
|
const setStreamUuid = db.prepare('UPDATE streams SET uuid = ? WHERE id = ?');
|
|
for (const row of db.prepare("SELECT id FROM streams WHERE uuid IS NULL OR uuid = ''").all()) {
|
|
setStreamUuid.run(randomUUID(), row.id);
|
|
}
|
|
|
|
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_stations_uuid ON stations(uuid)');
|
|
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_streams_uuid ON streams(uuid)');
|
|
db.exec('CREATE INDEX IF NOT EXISTS idx_stations_category ON stations(category)');
|
|
|
|
// station_plays gained session/listen-time aggregates so the leaderboard
|
|
// can rank by actual playtime, not just play-button taps.
|
|
const playCols = new Set(db.prepare("PRAGMA table_info(station_plays)").all().map((c) => c.name));
|
|
if (!playCols.has('sessions')) {
|
|
db.exec('ALTER TABLE station_plays ADD COLUMN sessions INTEGER NOT NULL DEFAULT 0');
|
|
}
|
|
if (!playCols.has('total_play_ms')) {
|
|
db.exec('ALTER TABLE station_plays ADD COLUMN total_play_ms INTEGER NOT NULL DEFAULT 0');
|
|
}
|
|
|
|
// Multi-user kiosk: per-user metadata + designation of "main" identity.
|
|
const userCols = new Set(db.prepare("PRAGMA table_info(users)").all().map((c) => c.name));
|
|
if (!userCols.has('is_main')) {
|
|
db.exec('ALTER TABLE users ADD COLUMN is_main INTEGER NOT NULL DEFAULT 0');
|
|
}
|
|
if (!userCols.has('avatar_color')) {
|
|
db.exec('ALTER TABLE users ADD COLUMN avatar_color TEXT');
|
|
}
|
|
if (!userCols.has('avatar_emoji')) {
|
|
db.exec('ALTER TABLE users ADD COLUMN avatar_emoji TEXT');
|
|
}
|
|
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_only_one_main ON users(is_main) WHERE is_main = 1');
|
|
|
|
// Cross-client stream sync: when a room enters 'play', record the wall-clock
|
|
// moment so every client can target the same playhead.
|
|
const stateCols = new Set(db.prepare("PRAGMA table_info(room_state)").all().map((c) => c.name));
|
|
if (!stateCols.has('started_at')) {
|
|
db.exec('ALTER TABLE room_state ADD COLUMN started_at INTEGER');
|
|
}
|
|
}
|
|
|