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; }