- 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.
161 lines
7.0 KiB
SQL
161 lines
7.0 KiB
SQL
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')),
|
|
is_main INTEGER NOT NULL DEFAULT 0, -- exactly one user is the shared "main" identity
|
|
avatar_color TEXT, -- cosmetic, used by avatar picker
|
|
avatar_emoji TEXT, -- cosmetic, used by avatar picker
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
-- Note: the partial unique index on users(is_main) is created by runMigrations()
|
|
-- after legacy DBs have the is_main column added via ALTER TABLE.
|
|
|
|
-- Trusted kiosk/browser devices. A device cookie (random token) is set after
|
|
-- an admin marks the device trusted. Users on the device's whitelist can
|
|
-- fast-switch without re-entering a password.
|
|
CREATE TABLE IF NOT EXISTS kiosk_devices (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
token TEXT NOT NULL UNIQUE,
|
|
label TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
last_seen_at TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS kiosk_device_users (
|
|
device_id INTEGER NOT NULL REFERENCES kiosk_devices(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (device_id, user_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_kiosk_device_users_user ON kiosk_device_users(user_id);
|
|
|
|
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,
|
|
image_path TEXT, -- relative path under data/images, e.g. "stations/12.jpg"
|
|
image_source TEXT, -- 'remote' | 'scraped' | 'upload'
|
|
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);
|
|
CREATE INDEX IF NOT EXISTS idx_history_station ON play_history(station_id);
|
|
|
|
-- One vote per user per station. value is +1 (up) or -1 (down). Row is
|
|
-- deleted entirely when the user clears their vote, so the COUNT is exact.
|
|
CREATE TABLE IF NOT EXISTS station_votes (
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
station_id INTEGER NOT NULL REFERENCES stations(id) ON DELETE CASCADE,
|
|
value INTEGER NOT NULL CHECK (value IN (-1, 1)),
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (user_id, station_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_votes_station ON station_votes(station_id);
|
|
|
|
-- Aggregate play counter. Cheaper than COUNT(*) over play_history every render
|
|
-- and lets anonymous/public listing show play counts without exposing history.
|
|
-- `total_play_ms` and `sessions` accumulate from closed play_history rows so
|
|
-- the leaderboard can rank by actual listen time, not just play-button taps.
|
|
CREATE TABLE IF NOT EXISTS station_plays (
|
|
station_id INTEGER PRIMARY KEY REFERENCES stations(id) ON DELETE CASCADE,
|
|
plays INTEGER NOT NULL DEFAULT 0,
|
|
sessions INTEGER NOT NULL DEFAULT 0,
|
|
total_play_ms INTEGER NOT NULL DEFAULT 0,
|
|
last_played_at TEXT
|
|
);
|
|
|
|
-- Named listening rooms. One "display" client + many controller/panel clients
|
|
-- per room share state (now-playing, volume, votes). A personal room is
|
|
-- auto-provisioned per user so single-user kiosks Just Work.
|
|
CREATE TABLE IF NOT EXISTS rooms (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS room_members (
|
|
room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner','member','guest')),
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (room_id, user_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_room_members_user ON room_members(user_id);
|
|
|
|
-- Last-known playback state per room. Persisted so clients reconnecting see
|
|
-- the same now-playing card immediately, even after a server restart.
|
|
CREATE TABLE IF NOT EXISTS room_state (
|
|
room_id INTEGER PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
|
|
station_id INTEGER REFERENCES stations(id) ON DELETE SET NULL,
|
|
playing INTEGER NOT NULL DEFAULT 0,
|
|
volume REAL NOT NULL DEFAULT 0.7,
|
|
started_at INTEGER, -- epoch ms when playback (or current station) began; anchors cross-client sync
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|