Add Radio Browser station importer

This commit is contained in:
2026-04-26 14:33:55 +02:00
parent 7e256a669e
commit 972164bba7
14 changed files with 36414 additions and 112 deletions

View File

@@ -0,0 +1,12 @@
import type { RadioStation } from './radioTypes.js';
export async function loadRadioStations(): Promise<RadioStation[]> {
const response = await fetch('/data/radio-stations.json');
if (!response.ok) {
throw new Error(`Failed to load radio stations: ${response.status}`);
}
const stations = (await response.json()) as RadioStation[];
return Array.isArray(stations) ? stations : [];
}

View File

@@ -0,0 +1,24 @@
export const radioCountries = [
{ name: 'Austria', code: 'AT' },
{ name: 'Croatia', code: 'HR' },
{ name: 'Serbia', code: 'RS' },
{ name: 'Montenegro', code: 'ME' },
{ name: 'Bosnia & Herzegovina', code: 'BA' },
{ name: 'Germany', code: 'DE' },
{ name: 'United Kingdom', code: 'GB' },
{ name: 'Italy', code: 'IT' },
{ name: 'France', code: 'FR' },
{ name: 'Spain', code: 'ES' },
{ name: 'USA', code: 'US' },
{ name: 'Canada', code: 'CA' },
{ name: 'Australia', code: 'AU' },
{ name: 'Luxembourg', code: 'LU' },
{ name: 'Netherlands', code: 'NL' },
{ name: 'Sweden', code: 'SE' },
{ name: 'Switzerland', code: 'CH' },
{ name: 'Hungary', code: 'HU' },
{ name: 'Czechia', code: 'CZ' },
{ name: 'Poland', code: 'PL' },
] as const;
export type RadioCountry = (typeof radioCountries)[number];

View File

@@ -0,0 +1,96 @@
import type { RadioBrowserStation, RadioStation } from './radioTypes.js';
const MAX_TAGS = 12;
const OBVIOUSLY_UNSUPPORTED_CODECS = new Set([
'wma',
'wmav2',
'asf',
'ra',
'rm',
'ape',
'alac',
'amr',
]);
function trimToNull(value: string | null | undefined): string | null {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toNumber(value: number | string | null | undefined): number {
const parsed = typeof value === 'number' ? value : Number(value ?? 0);
return Number.isFinite(parsed) ? parsed : 0;
}
function toNullableNumber(value: number | string | null | undefined): number | null {
const parsed = typeof value === 'number' ? value : Number(value ?? NaN);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
}
function parseTags(tags: string | null | undefined): string[] {
if (typeof tags !== 'string' || tags.trim().length === 0) return [];
const seen = new Set<string>();
const parsed: string[] = [];
for (const rawTag of tags.split(',')) {
const tag = rawTag.trim();
if (!tag) continue;
const normalizedTag = tag.toLowerCase();
if (seen.has(normalizedTag)) continue;
seen.add(normalizedTag);
parsed.push(tag);
if (parsed.length >= MAX_TAGS) break;
}
return parsed;
}
function isHttpsUrl(value: string | null): value is string {
if (!value) return false;
try {
const url = new URL(value);
return url.protocol === 'https:';
} catch {
return false;
}
}
export function normalizeRadioBrowserStation(
station: RadioBrowserStation,
countryName: string,
): RadioStation | null {
const stationUuid = trimToNull(station.stationuuid);
if (!stationUuid) return null;
const name = trimToNull(station.name);
if (!name) return null;
const streamUrl = trimToNull(station.url_resolved) ?? trimToNull(station.url);
if (!isHttpsUrl(streamUrl)) return null;
const codec = trimToNull(station.codec);
if (codec && OBVIOUSLY_UNSUPPORTED_CODECS.has(codec.toLowerCase())) {
return null;
}
return {
id: stationUuid,
name,
country: trimToNull(station.country) ?? countryName,
countryCode: trimToNull(station.countrycode) ?? '',
language: trimToNull(station.language),
tags: parseTags(station.tags),
codec,
bitrate: toNullableNumber(station.bitrate),
streamUrl,
homepage: trimToNull(station.homepage),
logoUrl: trimToNull(station.favicon),
votes: toNumber(station.votes),
clickcount: toNumber(station.clickcount),
source: 'radio-browser',
sourceStationUuid: stationUuid,
};
}

34
src/radio/radioTypes.ts Normal file
View File

@@ -0,0 +1,34 @@
export type RadioBrowserStation = {
stationuuid?: string | null;
name?: string | null;
url?: string | null;
url_resolved?: string | null;
homepage?: string | null;
favicon?: string | null;
country?: string | null;
countrycode?: string | null;
language?: string | null;
tags?: string | null;
codec?: string | null;
bitrate?: number | string | null;
votes?: number | string | null;
clickcount?: number | string | null;
};
export type RadioStation = {
id: string;
name: string;
country: string;
countryCode: string;
language: string | null;
tags: string[];
codec: string | null;
bitrate: number | null;
streamUrl: string;
homepage: string | null;
logoUrl: string | null;
votes: number;
clickcount: number;
source: 'radio-browser';
sourceStationUuid: string;
};