# Radio, Ambient, Fokus The oldest part of diora (it began as a radio player): internet radio with saved stations, history and track titles, the generative Ambient player, and the web page's Fokus timer. Written down 2026-10-24 from the code; it is the reference for the three apps. ## Server: the `radio` app Models (`radio/models.py`): | model | what | |---|---| | `SavedStation` | a user's station: name, url (unique per user), bitrate, country, tags, `favicon_url` (the logo), `is_favorite`, notes | | `StationPlay` | one listening: station, started/ended; hour, weekday, weekend filled on save (for the recommendations) | | `TrackHistory` | a track title heard (user, or `session_key` signed out), `scrobbled` | | `FeaturedStation` | stations picked in the admin for everybody (order, active) | | `FocusSession` | a finished Fokus round | Also: `podcasts.PlayerState` with `kind='radio'` (the player sync between devices) and `accounts.UserProfile.focus_station_url/_name` (the station the Fokus timer plays). Endpoints (`radio/urls.py`): - `GET /` — the page (`templates/radio/player.html`). The saved stations, the history (50) and the featured stations come rendered into it (`#saved-tbody`, `#history-tbody`, `.featured-section`) and as `INITIAL_SAVED` (with `play_count`), `INITIAL_FEATURED`, `USER_FOCUS_STATION`. The apps take these parts from it (see below). - `GET /radio/sse/?url=` — server-sent events with the track title, `data: {"track": …}` when it changes, `{"error": "stream ended"}` at the end. The server opens the stream itself with `Icy-MetaData: 1` and reads `StreamTitle` (`radio/icy.py`). - `POST /radio/record/` — a track into the history; `scrobble: true` sends it to Last.fm when the account has it linked (`radio/lastfm.py`; "Artist - Title", else the station as artist). - `POST /radio/save/` (by url: create or update), `/radio/remove//`, `/radio/favorite//`, `/radio/notes//`; `GET /radio/history/` (last 200), `POST /radio/history//delete/`. - `POST /radio/play/start/` → `play_id`, `/radio/play/stop/`; `GET /radio/recommendations/` — the five stations most played around this hour on this kind of day, with a German label. - `POST /radio/import/` — an `.m3u`/`.m3u8` file into the saved stations (`#EXTINF` names) → `{added, skipped}`. - `POST /radio/focus/record/`, `GET /radio/focus/stats/`. - `GET /radio/affiliate/?track=` — artwork/album from iTunes (an Amazon link only with `AMAZON_AFFILIATE_ENABLED`). - `GET /radio/creamfresh-stream/`, `POST /radio/creamfresh-feedback/` — the user's own station (radio.creamfresh.xyz) through the server, which adds its login; 👍/👎 for the DJ. - `GET /radio/stream-player/` — an old standalone player page; nothing links to it. **Station search** is not on the server: the page asks radio-browser.info directly (`de1.api.radio-browser.info/json/stations/search`, 50, by click count). A pasted `http(s)://` address is offered to play or save as it is. ## Web page (`static/js/app/`) | file | what | |---|---| | `01-core.js` | the globals: `audio` (one `