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:
Marco Mooren
2026-05-11 02:06:48 +02:00
parent e0a60f7b64
commit 00246389bc
52 changed files with 6280 additions and 2475 deletions

View File

@@ -2,64 +2,64 @@
// Docs: https://api.radio-browser.info/
const SERVERS = [
'https://de1.api.radio-browser.info',
'https://nl1.api.radio-browser.info',
'https://at1.api.radio-browser.info'
'https://de1.api.radio-browser.info',
'https://nl1.api.radio-browser.info',
'https://at1.api.radio-browser.info'
];
let activeServer = SERVERS[0];
async function rb(path, params) {
const url = new URL(path, activeServer);
if (params) for (const [k, v] of Object.entries(params)) {
if (v != null) url.searchParams.set(k, String(v));
}
const res = await fetch(url, { headers: { 'User-Agent': 'OnlineRadioExplorer/0.1' } });
if (!res.ok) throw new Error(`Radio-Browser ${res.status}`);
return res.json();
const url = new URL(path, activeServer);
if (params) for (const [k, v] of Object.entries(params)) {
if (v != null) url.searchParams.set(k, String(v));
}
const res = await fetch(url, { headers: { 'User-Agent': 'OnlineRadioExplorer/0.1' } });
if (!res.ok) throw new Error(`Radio-Browser ${res.status}`);
return res.json();
}
export async function search({ name, country, tag, limit = 30 }) {
const list = await rb('/json/stations/search', {
name, country, tag, limit, hidebroken: true, order: 'votes', reverse: true
});
return list.map(toCanonical);
const list = await rb('/json/stations/search', {
name, country, tag, limit, hidebroken: true, order: 'votes', reverse: true
});
return list.map(toCanonical);
}
export async function byUuid(uuid) {
const list = await rb('/json/stations/byuuid', { uuids: uuid });
return list[0] ? toCanonical(list[0]) : null;
const list = await rb('/json/stations/byuuid', { uuids: uuid });
return list[0] ? toCanonical(list[0]) : null;
}
function detectFormat(codec, url) {
const c = (codec || '').toLowerCase();
if (c.includes('mp3')) return 'mp3';
if (c.includes('aac')) return 'aac';
if (c.includes('ogg') || c.includes('vorbis') || c.includes('opus')) return 'ogg';
if (url?.endsWith('.m3u8')) return 'hls';
if (url?.endsWith('.m3u')) return 'm3u';
if (url?.endsWith('.pls')) return 'pls';
return 'unknown';
const c = (codec || '').toLowerCase();
if (c.includes('mp3')) return 'mp3';
if (c.includes('aac')) return 'aac';
if (c.includes('ogg') || c.includes('vorbis') || c.includes('opus')) return 'ogg';
if (url?.endsWith('.m3u8')) return 'hls';
if (url?.endsWith('.m3u')) return 'm3u';
if (url?.endsWith('.pls')) return 'pls';
return 'unknown';
}
function toCanonical(s) {
return {
uuid: s.stationuuid || undefined,
name: s.name,
slug: `rb-${s.stationuuid}`,
homepage: s.homepage || null,
country: s.countrycode || s.country || null,
genres: (s.tags || '').split(',').map((t) => t.trim()).filter(Boolean),
description: null,
image_url: s.favicon || null,
source: 'radiobrowser',
source_ref: s.stationuuid,
streams: [{
url: s.url_resolved || s.url,
format: detectFormat(s.codec, s.url_resolved || s.url),
bitrate: s.bitrate || null,
label: s.codec ? `${s.codec} ${s.bitrate || ''}`.trim() : null,
priority: 0
}]
};
return {
uuid: s.stationuuid || undefined,
name: s.name,
slug: `rb-${s.stationuuid}`,
homepage: s.homepage || null,
country: s.countrycode || s.country || null,
genres: (s.tags || '').split(',').map((t) => t.trim()).filter(Boolean),
description: null,
image_url: s.favicon || null,
source: 'radiobrowser',
source_ref: s.stationuuid,
streams: [{
url: s.url_resolved || s.url,
format: detectFormat(s.codec, s.url_resolved || s.url),
bitrate: s.bitrate || null,
label: s.codec ? `${s.codec} ${s.bitrate || ''}`.trim() : null,
priority: 0
}]
};
}