gaming-hub/server/src/plugins/radio/api.ts

105 lines
2.9 KiB
TypeScript
Raw Normal View History

// ── 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<any> {
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<RadioPlace[]> {
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<RadioChannel[]> {
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<SearchHit[]> {
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<string | null> {
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;
}