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:
59
server/db/index.js
Normal file
59
server/db/index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
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');
|
||||
}
|
||||
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)');
|
||||
}
|
||||
|
||||
80
server/db/schema.sql
Normal file
80
server/db/schema.sql
Normal file
@@ -0,0 +1,80 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('admin','user')),
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
display_name TEXT,
|
||||
theme TEXT DEFAULT 'dark',
|
||||
default_volume REAL DEFAULT 0.7
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
homepage TEXT,
|
||||
country TEXT,
|
||||
genres TEXT, -- JSON array
|
||||
description TEXT,
|
||||
image_url TEXT,
|
||||
source TEXT NOT NULL CHECK (source IN ('seed','radiobrowser','manual')),
|
||||
source_ref TEXT,
|
||||
category TEXT,
|
||||
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_stations_enabled ON stations(enabled);
|
||||
CREATE INDEX IF NOT EXISTS idx_stations_source ON stations(source);
|
||||
CREATE INDEX IF NOT EXISTS idx_stations_category ON stations(category);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_stations_uuid ON stations(uuid);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS streams (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT UNIQUE,
|
||||
station_id INTEGER NOT NULL REFERENCES stations(id) ON DELETE CASCADE,
|
||||
url TEXT NOT NULL,
|
||||
format TEXT NOT NULL CHECK (format IN ('mp3','aac','hls','m3u','pls','ogg','unknown')),
|
||||
bitrate INTEGER,
|
||||
label TEXT,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
last_checked_at TEXT,
|
||||
last_status TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_streams_station ON streams(station_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_streams_uuid ON streams(uuid);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
station_id INTEGER NOT NULL REFERENCES stations(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, station_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS play_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
station_id INTEGER NOT NULL REFERENCES stations(id) ON DELETE CASCADE,
|
||||
stream_id INTEGER REFERENCES streams(id) ON DELETE SET NULL,
|
||||
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_history_user ON play_history(user_id, started_at DESC);
|
||||
Reference in New Issue
Block a user