"use strict"; const { getDb } = require("./db"); const { naturalCompare } = require("./util"); /* ==================== songs ==================== */ function getSongByFile(file) { return getDb().prepare("SELECT * FROM songs WHERE file = ?").get(file); } function getSongById(id) { return getDb().prepare("SELECT * FROM songs WHERE id = ?").get(id); } function getAllSongs() { const rows = getDb().prepare("SELECT * FROM songs").all(); return rows.sort((a, b) => naturalCompare(a.file, b.file)); } function upsertSong(s) { const d = getDb(); const ex = getSongByFile(s.file); if (ex) { d.prepare( `UPDATE songs SET name=?, artist=?, cover=?, duration=?, file_size=? WHERE file=?` ).run( s.name != null ? s.name : ex.name, s.artist != null ? s.artist : ex.artist, s.cover != null ? s.cover : ex.cover, s.duration != null ? s.duration : ex.duration, s.file_size != null ? s.file_size : ex.file_size, s.file ); return getSongByFile(s.file); } d.prepare( `INSERT INTO songs (file, name, artist, cover, duration, file_size, heat, cache_status, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, 'pending', ?)` ).run( s.file, s.name || "", s.artist || "", s.cover || "", s.duration != null ? s.duration : null, s.file_size || 0, Date.now() ); return getSongByFile(s.file); } function updateSong(file, fields) { const keys = Object.keys(fields); if (!keys.length) return; const set = keys.map((k) => `${k}=?`).join(", "); getDb() .prepare(`UPDATE songs SET ${set} WHERE file=?`) .run(...keys.map((k) => fields[k]), file); } function deleteSongByFile(file) { return getDb().prepare("DELETE FROM songs WHERE file = ?").run(file); } function setCacheStatus(file, status) { getDb().prepare("UPDATE songs SET cache_status=? WHERE file=?").run(status, file); } function incrementHeat(file) { getDb().prepare("UPDATE songs SET heat = heat + 1 WHERE file=?").run(file); return getSongByFile(file)?.heat || 0; } /* ==================== streams ==================== */ function createStream(id, name) { getDb() .prepare( "INSERT INTO streams (id, name, enabled, current_pos, media_seq, created_at) VALUES (?, ?, 1, 0, 0, ?)" ) .run(id, name || id, Date.now()); return getStream(id); } function listStreams() { return getDb().prepare("SELECT * FROM streams ORDER BY created_at ASC").all(); } function getStream(id) { return getDb().prepare("SELECT * FROM streams WHERE id = ?").get(id); } function deleteStream(id) { return getDb().prepare("DELETE FROM streams WHERE id = ?").run(id); } function setStreamState(id, fields) { const keys = Object.keys(fields); if (!keys.length) return; const set = keys.map((k) => `${k}=?`).join(", "); getDb() .prepare(`UPDATE streams SET ${set} WHERE id=?`) .run(...keys.map((k) => fields[k]), id); } function setStreamEnabled(id, enabled) { setStreamState(id, { enabled: enabled ? 1 : 0 }); } /* ==================== stream_playlist ==================== */ function getPlaylistRows(streamId) { return getDb() .prepare( `SELECT sp.id AS entry_id, sp.position, s.* FROM stream_playlist sp JOIN songs s ON s.id = sp.song_id WHERE sp.stream_id = ? ORDER BY sp.position ASC` ) .all(streamId); } function playlistCount(streamId) { return getDb() .prepare("SELECT COUNT(*) c FROM stream_playlist WHERE stream_id=?") .get(streamId).c; } function addToPlaylist(streamId, songId) { const d = getDb(); const max = d.prepare("SELECT COALESCE(MAX(position),-1) m FROM stream_playlist WHERE stream_id=?") .get(streamId).m + 1; d.prepare( "INSERT INTO stream_playlist (stream_id, song_id, position) VALUES (?, ?, ?)" ).run(streamId, songId, max); } function removePlaylistEntry(streamId, entryId) { const d = getDb(); const row = d.prepare("SELECT position FROM stream_playlist WHERE id=? AND stream_id=?").get(entryId, streamId); if (!row) return false; d.prepare("DELETE FROM stream_playlist WHERE id=?").run(entryId); d.prepare("UPDATE stream_playlist SET position = position - 1 WHERE stream_id=? AND position > ?") .run(streamId, row.position); return true; } function setPlaylistOrder(streamId, entryIds) { const d = getDb(); const stmt = d.prepare("UPDATE stream_playlist SET position=? WHERE id=? AND stream_id=?"); const tx = d.transaction((ids) => { ids.forEach((id, i) => stmt.run(i, id, streamId)); }); tx(entryIds); } function clearPlaylist(streamId) { getDb().prepare("DELETE FROM stream_playlist WHERE stream_id=?").run(streamId); } /* ==================== sessions ==================== */ function createSession(token, expiresAt) { getDb() .prepare("INSERT INTO sessions (token, created_at, expires_at) VALUES (?, ?, ?)") .run(token, Date.now(), expiresAt); } function getSession(token) { const s = getDb().prepare("SELECT * FROM sessions WHERE token=?").get(token); if (!s) return null; if (s.expires_at < Date.now()) { deleteSession(token); return null; } return s; } function deleteSession(token) { getDb().prepare("DELETE FROM sessions WHERE token=?").run(token); } function purgeExpiredSessions() { return getDb().prepare("DELETE FROM sessions WHERE expires_at < ?").run(Date.now()).changes; } /* ==================== settings ==================== */ function getSetting(key, def = null) { const r = getDb().prepare("SELECT value FROM settings WHERE key=?").get(key); if (!r) return def; try { return JSON.parse(r.value); } catch (_) { return r.value; } } function setSetting(key, value) { getDb() .prepare( "INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value" ) .run(key, JSON.stringify(value)); } module.exports = { getSongByFile, getSongById, getAllSongs, upsertSong, updateSong, deleteSongByFile, setCacheStatus, incrementHeat, createStream, listStreams, getStream, deleteStream, setStreamState, setStreamEnabled, getPlaylistRows, playlistCount, addToPlaylist, removePlaylistEntry, setPlaylistOrder, clearPlaylist, createSession, getSession, deleteSession, purgeExpiredSessions, getSetting, setSetting, };