Refine player layout and station data
This commit is contained in:
450
.vscode/agents/radio-player-stations-importer_v1.md
vendored
Normal file
450
.vscode/agents/radio-player-stations-importer_v1.md
vendored
Normal file
@@ -0,0 +1,450 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user