feat(providers): add nine catalogues and the Radio Browser directory

This commit is contained in:
beefcarver committed 2026-11-03 20:24:53 -05:00
1 parent a3ad693132
commit 49f7efe0d2
27 files changed
+1695 -22

No files matched your search

+54 -15
View File
@@ -7,11 +7,17 @@ song history, and full desktop media integration, drawn with your terminal's
own ANSI palette on the terminal's default background so it fits any theme
(including transparent terminals).
The station catalogue is pluggable. Beefy Player ships with
**[SomaFM](https://somafm.com)** (≈50 curated channels),
**[tilderadio](https://tilderadio.org)** (the tildeverse's single community
stream) and **[Bassdrive](https://bassdrive.com)** (worldwide drum & bass), and
the UI never talks to a station catalogue directly: it asks a
The station catalogue is pluggable. Beefy Player ships with thirteen catalogues
— **[SomaFM](https://somafm.com)** (≈50 curated channels),
**[Radio Paradise](https://radioparadise.com)** (five mixes), the community
stations **[tilderadio](https://tilderadio.org)**,
**[Bassdrive](https://bassdrive.com)**, **[dublab](https://dublab.com)**,
**[Rinse FM](https://rinse.fm)**, **[Refuge Worldwide](https://refugeworldwide.com)**,
**[LYL Radio](https://lyl.live)**, **[The Lot Radio](https://thelotradio.com)**,
**[Cashmere Radio](https://cashmereradio.com)**,
**[Datafruits](https://datafruits.fm)** and **[aNONradio](https://anonradio.net)**,
plus the **[Radio Browser](https://www.radio-browser.info)** directory
(300 stations) — and the UI never talks to a station catalogue directly: it asks a
[provider](#providers) for stations, song history and stream URLs. Press `p` to
switch catalogue without restarting. Adding another internet-radio catalogue is
a self-contained module, not a rewrite.
@@ -55,7 +61,9 @@ implemented here from public endpoints and open protocols.
- **Last played** — the full recent history, newest first.
- **Every station** — with SomaFM that is the whole `channels.json` roster
(≈50 channels), live listener counts and genre search; tilderadio is a
single-station catalogue, and the list simply holds one.
single-station catalogue, and the list simply holds one. Long lists get a grey
thumb down the right edge (no rail) and a `12/51` position in the pane title;
`pgup`/`pgdn` move a screen at a time.
- **Favourites** that float to the top, remembered between runs.
- **Pluggable catalogues** — one `providers` module per radio network, with a
`p` picker in the UI, `--provider` on the command line, and the choice
@@ -197,23 +205,46 @@ layout and the guessed icecast mirrors all live in
`beefy_player/providers/somafm/`. Nothing outside that package imports it; the
app only ever holds a `RadioProvider`.
Three catalogues ship today:
Thirteen catalogues ship today:
| slug | catalogue | station list | history | streams |
| slug | catalogue | stations | what is on | relays |
| --- | --- | --- | --- | --- |
| `somafm` | [SomaFM](https://somafm.com) | `channels.json` (≈50) | `songs/<station>.json` | `.pls` → icecast mirrors |
| `tilderadio` | [tilderadio](https://tilderadio.org) | `/api/now/` (1) | `now_playing` + `history[]` | AzuraCast mounts (mp3/ogg, 320k) |
| `bassdrive` | [Bassdrive](https://bassdrive.com) | icecast status (1) | rolling `display-title` | three mounts + mirrors |
| `radioparadise` | [Radio Paradise](https://radioparadise.com) | five mixes | `api/now_playing` (artist, album, cover) | FLAC / AAC / MP3 icecast |
| `tilderadio` | [tilderadio](https://tilderadio.org) | 1 + schedule | `/api/now/` | AzuraCast mounts |
| `bassdrive` | [Bassdrive](https://bassdrive.com) | 1 + schedule | icecast `display-title` (the show) | three mounts + mirrors |
| `dublab` | [dublab](https://dublab.com) | 1 + schedule | dublab's calendar | Airtime mp3 |
| `rinse` | [Rinse FM](https://rinse.fm) | 3 | — | proxy mounts (aac+) |
| `refuge` | [Refuge Worldwide](https://refugeworldwide.com) | 1 | `public.radio.co` status | radio.co mp3 |
| `lyl` | [LYL Radio](https://lyl.live) | 1 | — | icecast mp3 |
| `lot` | [The Lot Radio](https://thelotradio.com) | 1 | — | HLS |
| `cashmere` | [Cashmere Radio](https://cashmereradio.com) | 1 | — | Airtime ogg/mp3 |
| `datafruits` | [Datafruits](https://datafruits.fm) | 1 | streampusher icecast | relay mp3 |
| `anonradio` | [aNONradio](https://anonradio.net) | 1 | icecast title | icecast mp3 |
| `radiobrowser` | [Radio Browser](https://www.radio-browser.info) | 300 (directory) | — | whatever each station publishes |
They are deliberately different shapes: fifty channels with PLS playlists, a
single AzuraCast stream that carries its own history, and an Icecast relay whose
"now playing" is the *show* rather than the track. That is the seam working.
The single-stream catalogues also use the optional capabilities: their left
pane is the broadcast schedule rather than a list containing one station, and
they opt out of Bandcamp lookups. Bassdrive is the extreme case — Icecast
reports one title and keeps no history, so the provider maps the title to
`Station.dj`/`presenter` and keeps a rolling record of the shows it has heard.
The single-stream catalogues also use the optional capabilities: where a
schedule exists (`tilderadio`, `bassdrive`, `dublab`) the left pane is that
schedule rather than a list containing one station, and none of them do
Bandcamp lookups. Bassdrive is the extreme case — Icecast reports one title and
keeps no history, so the provider maps the title to `Station.dj`/`presenter`
and keeps a rolling record of the shows it has heard.
The community stations share one base class,
`providers/community/base.py`, which knows how to read an Icecast
`status-json.xsl`, a `public.radio.co` status, a scheduled show, or nothing at
all; each station is then ten lines of data (relays, wordmark, tagline). Where
a relay publishes no metadata (Rinse, LYL, The Lot, Cashmere) the app simply
plays it and says so, rather than inventing a track.
`radiobrowser` is the directory case: three hundred of the most-clicked working
stations from the community database, keyless and public-domain, with no
per-station history at all — breadth rather than depth, and the one catalogue
where ON AIR legitimately has nothing to say.
### Header wordmarks
@@ -306,6 +337,7 @@ The UI wants roughly **84×24**; it also works smaller, just less pretty.
| key | action |
| -------------- | --------------------------------------------- |
| `↑` / `↓`, `j` / `k` | move through stations |
| `pgup` / `pgdn` | page through stations |
| `enter` | tune in to the selected station |
| `space` | play / pause |
| `s` | stop |
@@ -363,8 +395,11 @@ beefy_player/
base.py RadioProvider interface + HTTP/playlist toolkit
wordmark.py the half-block font every hero is drawn with
somafm/ the SomaFM catalogue (API, hero, bundled fallback list)
radiobrowser/ the community directory (300 stations, no metadata)
radioparadise/ five curated mixes with real track metadata
tilderadio/ the tilderadio catalogue (one station, hero, schedule)
bassdrive/ Bassdrive (Icecast status, show schedule, rolling history)
community/ one module per community station, on a shared base
player.py mpv over a Unix-socket JSON IPC channel
spectrum.py analyser (parec / Core Audio tap / ffmpeg / sim)
macos_tap.py macOS output capture (ctypes + Core Audio)
@@ -404,6 +439,10 @@ tools/
values. A memoised fetch and a lock keep a cold start to one homepage
download; the display-title's show is matched against the running slot for
"live from <city>".
- `beefy_player/providers/community/` — the community stations: `base.py` reads
Icecast or radio.co status, keeps the rolling history and turns a running
schedule slot into "what is on"; each station module is data. `rinse.py`
shows the multi-station case and `dublab.py` the schedule case.
- `beefy_player/providers/wordmark.py` — the half-block capitals behind every
hero, with per-character style spans.
- `beefy_player/widgets/schedule.py` — the left pane for single-stream