docs/radio.md: Radio, Ambient und Fokus – Server, Web, Android, Desktop, bekannte Schwachstellen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e2bb1f9c5d
commit
868e979755
1 changed files with 132 additions and 0 deletions
132
docs/radio.md
Normal file
132
docs/radio.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# 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/<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
|
||||
|
||||
- `/radio/sse/` needs no login and connects to any address it is given — a way to make the
|
||||
server fetch arbitrary URLs. Should require a session (or a saved/featured station).
|
||||
- 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.
|
||||
Loading…
Add table
Reference in a new issue