451 lines
8.9 KiB
Markdown
451 lines
8.9 KiB
Markdown
# 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:
|
|
|
|
- `stationuuid`
|
|
- `name`
|
|
- `url`
|
|
- `url_resolved`
|
|
- `homepage`
|
|
- `favicon`
|
|
- `country`
|
|
- `countrycode`
|
|
- `language`
|
|
- `tags`
|
|
- `codec`
|
|
- `bitrate`
|
|
- `votes`
|
|
- `clickcount`
|
|
|
|
For our app:
|
|
|
|
- `url_resolved` should be preferred over `url`
|
|
- `favicon` should 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:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
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:
|
|
|
|
* `id` must use `stationuuid`
|
|
* `sourceStationUuid` must also store `stationuuid`
|
|
* `streamUrl` must use `url_resolved || url`
|
|
* `logoUrl` must use `favicon || null`
|
|
* `tags` must 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:
|
|
|
|
```txt
|
|
https://de1.api.radio-browser.info/json/stations/search
|
|
```
|
|
|
|
Use these query params:
|
|
|
|
```txt
|
|
countrycode={COUNTRY_CODE}
|
|
hidebroken=true
|
|
is_https=true
|
|
order=clickcount
|
|
reverse=true
|
|
limit=100
|
|
```
|
|
|
|
Example:
|
|
|
|
```txt
|
|
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:
|
|
|
|
1. Loops through all configured countries.
|
|
2. Fetches up to 100 stations per country.
|
|
3. Filters invalid stations.
|
|
4. Normalizes station data.
|
|
5. Deduplicates stations.
|
|
6. Sorts stations by country, then clickcount descending.
|
|
7. Saves the final dataset as JSON.
|
|
8. Does not fail the whole import if one country fails.
|
|
9. Logs a summary after import.
|
|
|
|
Expected output file:
|
|
|
|
```txt
|
|
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:
|
|
|
|
```txt
|
|
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:
|
|
|
|
```ts
|
|
export function normalizeRadioBrowserStation(
|
|
station: RadioBrowserStation,
|
|
countryName: string
|
|
): RadioStation | null
|
|
```
|
|
|
|
The function must:
|
|
|
|
* return `null` if station has no `stationuuid`
|
|
* return `null` if station has no valid name
|
|
* return `null` if 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 `favicon` to `logoUrl`
|
|
* preserve vote/click metadata
|
|
|
|
---
|
|
|
|
## Logo Handling
|
|
|
|
Station logos come from Radio Browser field:
|
|
|
|
```txt
|
|
favicon
|
|
```
|
|
|
|
Use it as:
|
|
|
|
```ts
|
|
logoUrl: station.favicon || null
|
|
```
|
|
|
|
Frontend must support fallback logo behavior:
|
|
|
|
```tsx
|
|
<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:
|
|
|
|
```txt
|
|
public/images/radio-placeholder.svg
|
|
```
|
|
|
|
The placeholder should be simple and lightweight.
|
|
|
|
---
|
|
|
|
## Frontend Integration
|
|
|
|
Update the RadioPlayer so it can load stations from:
|
|
|
|
```txt
|
|
/data/radio-stations.json
|
|
```
|
|
|
|
Add or update a loader function:
|
|
|
|
```ts
|
|
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`:
|
|
|
|
```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:
|
|
|
|
```txt
|
|
[radio-import] Failed to import Germany (DE): {error message}
|
|
```
|
|
|
|
At the end, log:
|
|
|
|
```txt
|
|
[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:
|
|
|
|
* `stationuuid` is 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:import` or 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.
|