diora-web/docs/radio.md
mrwnslz 262056b5ee
Some checks failed
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Successful in 10m37s
Test / e2e (push) Failing after 1m28s
Radio: Titel-Abfrage (/radio/sse/) nie ins private Netz; ohne Konto nur Sender aus dem Verzeichnis (kuratiert, Featured, radio-browser per Sender-ID) – eigene Adressen brauchen ein Konto; sw v146
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 16:14:05 +02:00

137 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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=&station=` — 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`). Which addresses it opens
(`radio/streamcheck.py`, since 2026-10-24): never one in a private network (loopback,
10.x, 172.16–31.x, 192.168.x, link-local, the Docker network); **without an account only the
directory's stations** — the page's curated lists (read from `16-stations.js`), the featured
stations, the creamfresh stream, and what radio-browser.info lists (`station=` its station
id, which the page sends for search results; else by the address; kept a day). An address of
one's own is for saving, and saving needs an account: signed out, a pasted address offers
signing in instead of ▶.
- `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/<pk>/`, `/radio/favorite/<pk>/`,
`/radio/notes/<pk>/`; `GET /radio/history/` (last 200), `POST /radio/history/<pk>/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 `<audio>`), `currentStation`, `currentTrack`, `sseSource`, `isPlaying`, `currentPlayId` |
| `10-radio.js` | `playStation(url, name, id)`, `stopPlayback`, `togglePlayStop`, the SSE (`startMetadataSSE` → `recordTrack`), volume (0–255, `diora_volume`), search, saving, favourites, recommendations, the creamfresh votes, Media Session |
| `16-stations.js` | mood chips (they search a tag), curated lists, featured stations, the occasional donation hint, notes |
| `29-m3u.js` | M3U import |
| `80-radio-sidebar.js` | the compact radio player of the sidebar |
| `24-ambient.js` | the Ambient tab (below) and `showRadioTab` |
| `14-focus.js` | Fokus (below) |
Sub-tabs `RADIO_SUB_TABS = ['search', 'saved', 'history']` (`20-tabs.js`), hash `#radio/saved`,
remembered in `diora_active_radio_tab`. On an https page an `http://` stream would be blocked:
the page opens it in a new tab — unless the host plays it (`window.DIORA_PLAYS_HTTP_STREAMS`,
set by the apps). No HLS: what `<audio>` plays.
## Ambient (`static/ambient/index.html`, "Drift")
No sound files: a generative WebAudio synthesizer. Modes (`MODES`): ambient music (chords,
bells, grains), noise colours, nature (rain, storm, sea, wind, stream, fire, crickets),
machines (fan, cabin, train), focus and sleep (binaural delta–beta, singing bowls,
heartbeat); each with two sliders and seeded presets in the URL hash.
The page holds it in an iframe, loaded when first needed (`24-ambient.js`); they talk by
`postMessage` (`{type: 'ambient', cmd: 'start'|'stop'|'mode'|'volume'}` in,
`{state: 'playing'|'stopped'}` out). Mode remembered in `diora_ambient_mode`.
Hooks for the apps: `__dioraAmbientStart/Stop/Cmd/Info/Playing`.
**One sound at a time:** Ambient starting stops the radio (`stopPlayback(false)`) and pauses a
podcast (`__dioraPausePlayer()`; Android: `podcastPause()`, pause only, never toggle); the
radio starting stops Ambient.
## Fokus (web page only)
A Pomodoro timer (25/5 min) in its own section (`nav.js`: `focus`, `webOnly`): it plays the
chosen focus station, records a `FocusSession` after each work phase, pauses the sound for the
break; with a do-not-disturb full-screen mode (`toggleDND`) and a statistics table.
## Android (diora-android)
- **One player** for radio, podcasts and audiobooks: `RadioService.kt`, a Media3
`MediaLibraryService` with one ExoPlayer (15 s connect / 20 s read timeout; diora's own
server gets the login headers — the creamfresh proxy). After a pause of more than 3 s a
station jumps back to live. Android Auto: Saved, Featured, Queue, voice search by name.
Resuming after a restart plays the last station. Radio always streams on mobile data (the
mobile-data rules are for episodes).
- `Radio.kt` — the page's controller: play, stop, `track(title)` (new metadata without
rebuffering), state to the page (`window.__dioraRadio`).
- `RadioPage.kt` — the saved stations, history and featured stations exist only in the
rendered `/`: the app fetches it with its login, cuts those parts out into `radio-page.json`
and splices them into its own page at markers that `tools/build_web.py` set
(`<!--diora:saved|history|featured-->`, `__DIORA_RADIO_*_JSON__`). So they show offline too.
- `LocalServer.kt` passes `/radio/sse/` through as a stream (padded with SSE comments: WebView
hands intercepted data over only when its buffer is full); POSTs go through `native.js`.
- `web-native/static/native-radio.js` — turns the page's `audio` into a remote of the native
player (`radioPlay`/`radioStop`, counters against stale echoes), shows errors ("Sender nicht
erreichbar"), draws the dock/rail (from `nav.js`), the mini player and the radio bar.
- `AmbientService.kt` — Ambient as a foreground media service (notification with Stopp, audio
focus, stops when headphones are pulled); started and stopped from the page
(`ambientState`). Widgets for Ambient and resuming (`CardWidgets.kt`).
- Sleep timer (`SleepTimer.kt`, `SleepRules.kt`): counts only playing time (Ambient too), fades
out over the last 10 s, shaking resets it, can stop after N episodes, can switch itself on at
night (22–6 h); one that counted nothing for 3 h is dropped.
- Data usage: the stream counts as `RADIO_STREAM`, `/radio/` and `/` as `RADIO_DATA`
(`DataUsage.kt`).
## Desktop (diora-desktop)
A copy of `native-radio.js`; `src/web/desktop.js` plays with a hidden `Audio()` of its own
(`DioraNative.radioPlay/Stop/State/Refresh/Track`; media keys: pause = stop) and re-reads `/`
like RadioPage.kt (at most every 20 s). `http://` streams go through the local router
(`/__stream?u=`, `src/main/router.js`) without their ICY data; the SSE is passed through with the
login; `backgroundThrottling: false` keeps the titles coming.
## Known weak spots
- The login for the creamfresh stream is written into `radio/views.py`; it belongs in the
environment like the other secrets.
- Every listener's titles cost the server a second connection to the stream.
- No recording, no alarm; station logos only from radio-browser's favicon.