Files
RadioPlayerWeb/.vscode/agents/radio-player-stations-importer_v1.md

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.