8.9 KiB
RadioPlayer Stations Importer v1
Goal
Build a production-ready radio station importer for our web RadioPlayer.
The importer must use the public Radio Browser API as the source for radio stations and generate a clean local station dataset that the RadioPlayer can use.
We need stations for these countries:
- Austria
- Croatia
- Serbia
- Montenegro
- Bosnia & Herzegovina
- Germany
- United Kingdom
- Italy
- France
- Spain
- USA
- Canada
- Australia
- Luxembourg
- Netherlands
- Sweden
- Switzerland
- Hungary
- Czechia
- Poland
The importer must also include station logos when available.
Important Context
Radio Browser station objects can contain:
stationuuidnameurlurl_resolvedhomepagefaviconcountrycountrycodelanguagetagscodecbitratevotesclickcount
For our app:
url_resolvedshould be preferred overurlfaviconshould be used as the station logo- broken/missing logos must not break the UI
- HTTP streams should be avoided for browser compatibility
- HTTPS streams should be preferred
- broken streams should be filtered out where possible
Countries
Create a shared country list:
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;
Required Output Format
Normalize every imported station to this structure:
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;
};
Rules:
idmust usestationuuidsourceStationUuidmust also storestationuuidstreamUrlmust useurl_resolved || urllogoUrlmust usefavicon || nulltagsmust be converted from comma-separated string to string array- empty strings must become
null - invalid stations must be skipped
- duplicate stations must be removed by
stationuuid - duplicate stream URLs should also be avoided where possible
API Endpoint
Use this endpoint pattern:
https://de1.api.radio-browser.info/json/stations/search
Use these query params:
countrycode={COUNTRY_CODE}
hidebroken=true
is_https=true
order=clickcount
reverse=true
limit=100
Example:
https://de1.api.radio-browser.info/json/stations/search?countrycode=DE&hidebroken=true&is_https=true&order=clickcount&reverse=true&limit=100
Import Requirements
Create an importer script that:
- Loops through all configured countries.
- Fetches up to 100 stations per country.
- Filters invalid stations.
- Normalizes station data.
- Deduplicates stations.
- Sorts stations by country, then clickcount descending.
- Saves the final dataset as JSON.
- Does not fail the whole import if one country fails.
- Logs a summary after import.
Expected output file:
public/data/radio-stations.json
If the project uses a different structure, place it in the closest appropriate public/static data directory and document the chosen path.
Recommended File Structure
Create or adapt these files depending on the current project structure:
src/radio/radioCountries.ts
src/radio/radioTypes.ts
src/radio/radioStationNormalizer.ts
scripts/import-radio-stations.ts
public/data/radio-stations.json
If this is a Vite/React project, this structure is preferred.
If the project already has a different convention, follow the existing convention.
Normalizer Requirements
Create a normalizer function:
export function normalizeRadioBrowserStation(
station: RadioBrowserStation,
countryName: string
): RadioStation | null
The function must:
- return
nullif station has nostationuuid - return
nullif station has no valid name - return
nullif station has no valid stream URL - trim all string values
- convert empty values to
null - parse tags safely
- limit tags to maximum 12 tags per station
- prefer
url_resolved - map
favicontologoUrl - preserve vote/click metadata
Logo Handling
Station logos come from Radio Browser field:
favicon
Use it as:
logoUrl: station.favicon || null
Frontend must support fallback logo behavior:
<img
src={station.logoUrl || "/images/radio-placeholder.svg"}
alt={station.name}
loading="lazy"
onError={(event) => {
event.currentTarget.src = "/images/radio-placeholder.svg";
}}
/>
Create a fallback placeholder if one does not exist:
public/images/radio-placeholder.svg
The placeholder should be simple and lightweight.
Frontend Integration
Update the RadioPlayer so it can load stations from:
/data/radio-stations.json
Add or update a loader function:
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}`);
}
return response.json();
}
The UI should support:
- country filter
- station search by name
- station logo
- station name
- country
- tags
- bitrate/codec where available
- graceful empty state
- graceful loading state
- graceful error state
Player Requirements
When user clicks a station:
- use
streamUrl - display station name
- display logo fallback if logo fails
- show country
- show codec/bitrate if available
- do not crash if playback fails
- display a user-friendly playback error
Optional But Recommended
Add a script command to package.json:
{
"scripts": {
"radio:import": "tsx scripts/import-radio-stations.ts"
}
}
If tsx is not installed and the project uses TypeScript scripts, add it as a dev dependency.
If the project does not use tsx, use the existing project script runner.
Error Handling
The importer must handle:
- network errors
- invalid JSON
- empty responses
- country-specific failures
- missing favicon
- missing homepage
- missing language
- invalid station URLs
- duplicated stations
- duplicated stream URLs
Do not stop the whole import because one country fails.
Log errors like:
[radio-import] Failed to import Germany (DE): {error message}
At the end, log:
[radio-import] Imported {total} stations from {successfulCountries}/{totalCountries} countries.
[radio-import] Failed countries: DE, FR
[radio-import] Output: public/data/radio-stations.json
Data Quality Rules
Skip stations when:
stationuuidis missing- name is missing
- stream URL is missing
- stream URL is not HTTPS
- codec is obviously unsupported by browsers
Preferred codecs:
- MP3
- AAC
- OGG
Do not hard fail on unknown codec, but keep codec in the dataset.
Browser Compatibility
Since this is a web RadioPlayer:
- prefer HTTPS streams
- avoid HTTP streams
- keep fallback image local
- do not assume every logo loads
- do not assume every stream can play in every browser
- do not autoplay without user interaction
Acceptance Criteria
The task is complete when:
- country list exists
- importer script exists
- normalized station type exists
- radio-stations.json is generated
- logos are included through
logoUrl - UI can load the local JSON file
- UI shows logo fallback on broken/missing logos
- player can play a selected station
- import failure for one country does not fail the whole script
npm run radio:importor equivalent command works- TypeScript build passes
- lint passes if configured
- existing app behavior is not broken
Testing Checklist
Manually verify:
- Import script runs successfully
- JSON file is generated
- Germany has stations
- Austria has stations
- Croatia has stations
- USA has stations
- United Kingdom has stations
- stations have
streamUrl - many stations have
logoUrl - missing logos show fallback
- clicking a station starts playback
- broken streams show a friendly error
- country filtering works
- search works
- no console crash happens when logo or stream fails
Notes
Do not manually hardcode hundreds of station stream URLs.
Use Radio Browser as the source of truth for imported stations.
Keep a curated featured station list separate later if needed.