// Thin wrapper around the Radio-Browser community API. // 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' ]; 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(); } 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); } export async function byUuid(uuid) { 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'; } 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 }] }; }