Add API documentation and underground station importer
- Introduced a new HTML documentation page for the oradio API, including a JavaScript file to handle dynamic content and API requests. - Added a CSS file for styling the documentation page. - Implemented an underground station importer script that fetches data from Radio-Browser and writes it to a JSON file. - Created a stats module to compute and manage vote and play statistics for radio stations. - Added a polyfill for modulepreload to ensure compatibility with older browsers.
This commit is contained in:
@@ -2,141 +2,141 @@ import { randomUUID } from 'node:crypto';
|
||||
import { getDb } from './db/index.js';
|
||||
|
||||
function rowToStation(row) {
|
||||
if (!row) return 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: row.image_url,
|
||||
source: row.source,
|
||||
source_ref: row.source_ref,
|
||||
category: row.category,
|
||||
enabled: !!row.enabled,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at
|
||||
};
|
||||
if (!row) return 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: row.image_url,
|
||||
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);
|
||||
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));
|
||||
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));
|
||||
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);
|
||||
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);
|
||||
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()}`;
|
||||
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;
|
||||
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(`
|
||||
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)
|
||||
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);
|
||||
.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(`
|
||||
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);
|
||||
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;
|
||||
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(`
|
||||
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);
|
||||
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;
|
||||
return getDb().prepare('DELETE FROM streams WHERE id = ?').run(streamId).changes > 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user