Add Radio Browser station importer
This commit is contained in:
88
scripts/import-radio-stations.ts
Normal file
88
scripts/import-radio-stations.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { radioCountries } from '../src/radio/radioCountries.js';
|
||||
import { normalizeRadioBrowserStation } from '../src/radio/radioStationNormalizer.js';
|
||||
import type { RadioBrowserStation, RadioStation } from '../src/radio/radioTypes.js';
|
||||
|
||||
const API_ENDPOINT = 'https://de1.api.radio-browser.info/json/stations/search';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const outputPath = path.join(repoRoot, 'public', 'data', 'radio-stations.json');
|
||||
|
||||
const collectedStations: RadioStation[] = [];
|
||||
const seenStationIds = new Set<string>();
|
||||
const seenStreamUrls = new Set<string>();
|
||||
const failedCountries: string[] = [];
|
||||
let successfulCountries = 0;
|
||||
|
||||
for (const country of radioCountries) {
|
||||
try {
|
||||
const fetchedStations = await fetchCountryStations(country.code);
|
||||
const normalizedStations = fetchedStations
|
||||
.map((station) => normalizeRadioBrowserStation(station, country.name))
|
||||
.filter((station): station is RadioStation => station !== null);
|
||||
|
||||
for (const station of normalizedStations) {
|
||||
if (seenStationIds.has(station.id)) continue;
|
||||
if (seenStreamUrls.has(station.streamUrl)) continue;
|
||||
|
||||
seenStationIds.add(station.id);
|
||||
seenStreamUrls.add(station.streamUrl);
|
||||
collectedStations.push(station);
|
||||
}
|
||||
|
||||
successfulCountries += 1;
|
||||
} catch (error) {
|
||||
failedCountries.push(country.code);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[radio-import] Failed to import ${country.name} (${country.code}): ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
collectedStations.sort((left, right) => {
|
||||
const countryOrder = left.country.localeCompare(right.country, undefined, { sensitivity: 'base' });
|
||||
if (countryOrder !== 0) return countryOrder;
|
||||
if (right.clickcount !== left.clickcount) return right.clickcount - left.clickcount;
|
||||
return left.name.localeCompare(right.name, undefined, { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, `${JSON.stringify(collectedStations, null, 2)}\n`, 'utf8');
|
||||
|
||||
console.log(`[radio-import] Imported ${collectedStations.length} stations from ${successfulCountries}/${radioCountries.length} countries.`);
|
||||
console.log(`[radio-import] Failed countries: ${failedCountries.length > 0 ? failedCountries.join(', ') : 'None'}`);
|
||||
console.log('[radio-import] Output: public/data/radio-stations.json');
|
||||
|
||||
async function fetchCountryStations(countryCode: string): Promise<RadioBrowserStation[]> {
|
||||
const url = new URL(API_ENDPOINT);
|
||||
url.search = new URLSearchParams({
|
||||
countrycode: countryCode,
|
||||
hidebroken: 'true',
|
||||
is_https: 'true',
|
||||
order: 'clickcount',
|
||||
reverse: 'true',
|
||||
limit: '100',
|
||||
}).toString();
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'user-agent': 'RadioPlayerImporter/1.0',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as unknown;
|
||||
if (!Array.isArray(payload)) {
|
||||
throw new Error('Expected an array response from Radio Browser.');
|
||||
}
|
||||
|
||||
return payload as RadioBrowserStation[];
|
||||
}
|
||||
Reference in New Issue
Block a user