Files
radio-explorer/server/stations.js
Marco Mooren b86dcfbb8d Add master display UI with audio output management and styling
- Implement main.js for the master display functionality, including WebSocket connection, audio output management, and state handling.
- Create style.css for the master display's visual design, ensuring a cohesive look and feel with a dark theme and responsive layout.
- Integrate device management with a fallback for non-Electron environments, allowing users to select audio outputs.
- Add features for managing favorites, including toggling favorites and filtering by genre.
- Enhance user experience with a responsive favorites grid and drag-to-scroll functionality.
2026-05-11 17:55:09 +02:00

150 lines
5.4 KiB
JavaScript

import { randomUUID } from 'node:crypto';
import { getDb } from './db/index.js';
function rowToStation(row) {
if (!row) return null;
const imagePath = row.image_path || null;
const remote = row.image_url || null;
return {
id: row.id,
uuid: row.uuid,
name: row.name,
slug: row.slug,
homepage: row.homepage,
country: row.country,
genres: row.genres ? JSON.parse(row.genres) : [],
description: row.description,
// image_url remains the remote/source URL (what admins edit).
// image_display_url is what UIs should render — prefers the local cache.
image_url: remote,
image_path: imagePath,
image_source: row.image_source || null,
image_display_url: imagePath ? `/media/${imagePath}` : remote,
source: row.source,
source_ref: row.source_ref,
category: row.category,
enabled: !!row.enabled,
created_at: row.created_at,
updated_at: row.updated_at
};
}
export function listStations({ q, source, category, enabled = true } = {}) {
const db = getDb();
const where = [];
const params = [];
if (enabled !== null) { where.push('enabled = ?'); params.push(enabled ? 1 : 0); }
if (source) { where.push('source = ?'); params.push(source); }
if (category) { where.push('category = ?'); params.push(category); }
if (q) { where.push('(name LIKE ? OR genres LIKE ? OR country LIKE ?)'); params.push(`%${q}%`, `%${q}%`, `%${q}%`); }
const sql = `SELECT * FROM stations ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY name COLLATE NOCASE`;
return db.prepare(sql).all(...params).map(rowToStation);
}
export function getStation(id) {
return rowToStation(getDb().prepare('SELECT * FROM stations WHERE id = ?').get(id));
}
export function getStationByUuid(uuid) {
return rowToStation(getDb().prepare('SELECT * FROM stations WHERE uuid = ?').get(uuid));
}
export function getStreamsForStation(stationId) {
return getDb().prepare(
'SELECT * FROM streams WHERE station_id = ? ORDER BY priority ASC, id ASC'
).all(stationId);
}
export function getStreamByUuid(uuid) {
return getDb().prepare('SELECT * FROM streams WHERE uuid = ?').get(uuid);
}
export function slugify(name) {
return name.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80) || `station-${Date.now()}`;
}
export function uniqueSlug(base) {
const db = getDb();
let slug = base, n = 1;
while (db.prepare('SELECT 1 FROM stations WHERE slug = ?').get(slug)) {
n += 1;
slug = `${base}-${n}`;
}
return slug;
}
export function createStation(input, userId) {
const db = getDb();
const slug = input.slug || uniqueSlug(slugify(input.name));
const uuid = input.uuid || randomUUID();
const info = db.prepare(`
INSERT INTO stations (uuid, name, slug, homepage, country, genres, description, image_url, source, source_ref, category, created_by)
VALUES (@uuid, @name, @slug, @homepage, @country, @genres, @description, @image_url, @source, @source_ref, @category, @created_by)
`).run({
uuid,
name: input.name,
slug,
homepage: input.homepage ?? null,
country: input.country ?? null,
genres: JSON.stringify(input.genres ?? []),
description: input.description ?? null,
image_url: input.image_url ?? null,
source: input.source ?? 'manual',
source_ref: input.source_ref ?? null,
category: input.category ?? null,
created_by: userId ?? null
});
const id = info.lastInsertRowid;
for (const s of input.streams ?? []) {
db.prepare(`INSERT INTO streams (uuid, station_id, url, format, bitrate, label, priority)
VALUES (?, ?, ?, ?, ?, ?, ?)`)
.run(s.uuid || randomUUID(), id, s.url, s.format ?? 'unknown', s.bitrate ?? null, s.label ?? null, s.priority ?? 0);
}
return getStation(id);
}
export function updateStation(id, patch) {
const db = getDb();
const cur = getStation(id);
if (!cur) return null;
const next = { ...cur, ...patch };
db.prepare(`
UPDATE stations
SET name=@name, homepage=@homepage, country=@country, genres=@genres,
description=@description, image_url=@image_url, category=@category,
enabled=@enabled, updated_at=datetime('now')
WHERE id=@id
`).run({
id,
name: next.name,
homepage: next.homepage ?? null,
country: next.country ?? null,
genres: JSON.stringify(next.genres ?? []),
description: next.description ?? null,
image_url: next.image_url ?? null,
category: next.category ?? null,
enabled: next.enabled ? 1 : 0
});
return getStation(id);
}
export function deleteStation(id) {
return getDb().prepare('DELETE FROM stations WHERE id = ?').run(id).changes > 0;
}
export function addStream(stationId, s) {
const uuid = s.uuid || randomUUID();
const info = getDb().prepare(`
INSERT INTO streams (uuid, station_id, url, format, bitrate, label, priority)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(uuid, stationId, s.url, s.format ?? 'unknown', s.bitrate ?? null, s.label ?? null, s.priority ?? 0);
return getDb().prepare('SELECT * FROM streams WHERE id = ?').get(info.lastInsertRowid);
}
export function deleteStream(streamId) {
return getDb().prepare('DELETE FROM streams WHERE id = ?').run(streamId).changes > 0;
}