// ── Radio Garden API Client ── const BASE = 'https://radio.garden/api'; const UA = 'GamingHub/1.0'; export interface RadioPlace { id: string; geo: [number, number]; title: string; country: string; size: number; } export interface RadioChannel { id: string; title: string; } export interface SearchHit { id: string; type: string; title: string; subtitle: string; url: string; } // ── Cache ── let placesCache: RadioPlace[] = []; let placesCacheTime = 0; const PLACES_TTL = 24 * 60 * 60 * 1000; // 24h // ── Helpers ── async function apiFetch(path: string): Promise { const res = await fetch(`${BASE}${path}`, { headers: { 'User-Agent': UA }, }); if (!res.ok) throw new Error(`Radio Garden API ${res.status}: ${path}`); return res.json(); } // ── Public API ── /** Alle Orte mit Radiosendern weltweit (~30K Einträge, gecached 24h) */ export async function fetchPlaces(): Promise { if (placesCache.length > 0 && Date.now() - placesCacheTime < PLACES_TTL) { return placesCache; } const data = await apiFetch('/ara/content/places'); placesCache = (data?.data?.list ?? []).map((p: any) => ({ id: p.id, geo: p.geo, title: p.title, country: p.country, size: p.size ?? 1, })); placesCacheTime = Date.now(); console.log(`[Radio] Fetched ${placesCache.length} places from Radio Garden`); return placesCache; } /** Sender an einem bestimmten Ort */ export async function fetchPlaceChannels(placeId: string): Promise { const data = await apiFetch(`/ara/content/page/${placeId}/channels`); const channels: RadioChannel[] = []; for (const section of data?.data?.content ?? []) { for (const item of section.items ?? []) { const href: string = item.href ?? item.page?.url ?? ''; const match = href.match(/\/listen\/([^/]+)/); if (match) { channels.push({ id: match[1], title: item.title ?? 'Unbekannt' }); } } } return channels; } /** Sender/Orte/Länder suchen */ export async function searchStations(query: string): Promise { const data = await apiFetch(`/search?q=${encodeURIComponent(query)}`); return (data?.hits?.hits ?? []).map((h: any) => ({ id: h._id ?? '', type: h._source?.type ?? 'unknown', title: h._source?.title ?? '', subtitle: h._source?.subtitle ?? '', url: h._source?.url ?? '', })); } /** Stream-URL auflösen (302 Redirect → tatsächliche Icecast/Shoutcast URL) */ export async function resolveStreamUrl(channelId: string): Promise { try { const res = await fetch(`${BASE}/ara/content/listen/${channelId}/channel.mp3`, { redirect: 'manual', headers: { 'User-Agent': UA }, }); return res.headers.get('location') ?? null; } catch { return null; } } export function getPlacesCount(): number { return placesCache.length; }