#!/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 syncOutputPath = path.join(repoRoot, 'public', 'data', 'radio-stations-sync.json'); const collectedStations: RadioStation[] = []; const seenStationIds = new Set(); const seenStreamUrls = new Set(); 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 }); const serializedStations = `${JSON.stringify(collectedStations, null, 2)}\n`; await Promise.all([ writeFile(outputPath, serializedStations, 'utf8'), writeFile(syncOutputPath, serializedStations, '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'); console.log('[radio-import] Sync output: public/data/radio-stations-sync.json'); async function fetchCountryStations(countryCode: string): Promise { 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[]; }