docs(readme): document installation, keybindings and features

This commit is contained in:
beefcarver committed 2026-10-30 04:17:55 +00:00
1 parent 8eb22ea52c
commit 3866f0277c
1 file changed
+247
+247
View File
@@ -0,0 +1,247 @@
# somafm-tui
A flashy terminal radio for **[SomaFM](https://somafm.com)** — a live spectrum
analyser, song history, and full desktop media integration over MPRIS, drawn
with your terminal's own ANSI palette on the terminal's default background so
it fits any theme (including transparent terminals).
No existing SomaFM client is used: the station list, playlists, song history,
audio control, spectrum visualiser and MPRIS service are all implemented here from
the public SomaFM endpoints and open protocols.
```
▄▀▀▀ ▄▀▀▄ █▄ ▄█ ▄▀▀▄ SOMA FM RADIO 14:32:07
▀▀▀▄ █ █ █ █ █ █▀▀█ LISTENER SUPPORTED Sat 15 Nov
▄▄▄▀ ▀▄▄▀ █ █ █ █ SAN FRANCISCO vol 70%
● PLAYING Groove Salad · Jens Buchert — Rauschebart
┌ STATIONS ───────────────┐┌ ON AIR ────────────────────────────────────┐
│ ★ ▶ Groove Salad 1.9k ││ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ GROOVE SALAD ● PLAYING│
│ ▶ Drone Zone 812 ││ ▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ A nicely chilled plate│
│ Secret Agent 420 ││ │
│ Indie Pop Rocks 901 ││ NOW PLAYING 12s ago │
│ … ││ Jens Buchert │
│ ││ Rauschebart │
│ ││ Gravity dj Rusty Hodge │
│ ││ ♪ 1.9k listeners · mp3 · 128k │
│ ││ │
│ ││ LAST PLAYED │
│ ││ 14:29 Alex Cortiz — Room 505 │
│ ││ 14:24 Tosca — Amber November │
└─────────────────────────┘└────────────────────────────────────────────┘
┌ SPECTRUM · MIRROR · SYNC ─────────────────────────────────────────────┐
│ ▁▃▅▇█▇▅▃▁ ▂▄▆█▆▄▂ │
│ ▁▂▄▆██████████████████▆▄▂▁▂▄▆█████████▆▄▂▁ │
│ ▀▄▂▁▂▄▆██████████████████▆▄▂▁▂▄▆█████████▆▄▂▁ │
└───────────────────────────────────────────────────────────────────────┘
space pause · ↑↓ station · enter tune · +/- vol · f ★ · / filter · ? help
```
## Features
- **Live spectrum analyser** — 72 log-spaced bands with peak caps, a moving
gradient taken from your terminal's ANSI palette and a symmetric mirrored
spectrum; or a full-height oscilloscope. Three modes (`v`).
- **Synced spectrum, not a guess** — by default the analyser records your
actual PulseAudio/PipeWire output monitor with `parec`, so the bars follow
what you hear (`SYNC`). Without a monitor it falls back to a muted second
`ffmpeg` decode of the stream (`FFT`, can drift), then to a synthesized
display (`SIM`).
- **Real artwork where supported** — Kitty uses the kitty graphics protocol;
iTerm2 and WezTerm use their native iTerm2 inline-image protocol. Images
occupy exactly the ON AIR art cell box and fall back to the procedural ANSI
mosaic for other terminals, failed loads, or blocked graphics passthrough.
Images are cached in `~/.cache/somafm-tui/artwork`.
- **Now playing** — artist, title, album, DJ, listeners, format/bitrate and
"played N minutes ago", with procedural ANSI-shaded album art that changes
per track.
- **Last played** — the full recent history from `somafm.com/songs/<id>.json`.
- **Every station** — the whole `channels.json` roster (≈50 channels), live
listener counts, genre search.
- **Favourites** that float to the top, remembered between runs.
- **MPRIS** — an `org.mpris.MediaPlayer2.somafmtui.instance<pid>` player exposes
`PlaybackStatus`, `Metadata` (title/artist/album/artUrl), `Volume`, and
`Play`/`Pause`/`PlayPause`/`Stop`/`Next`/`Previous`. Desktop media widgets and
media keys pick it up automatically.
- **Theme-aware** — every accent is emitted as native ANSI (not RGB), and the
background is the terminal default (`SGR 49`), so transparent terminals stay
transparent and the TUI adopts your shell/terminal colour scheme.
- **Graceful degradation** — works with cached data offline, falls back to a
bundled station list, reconnects across ICE mirrors automatically, and uses
`ffplay` if `mpv` is missing.
## Requirements
Python **3.10+**, plus the tools below. This is a minimum, not a pin: there
is no `.python-version`, and `uv` can install a compatible interpreter if your
distro ships something older. `shell.nix` provides everything inside an
**FHS environment** (needed because uv installs manylinux wheels such as
NumPy, which expect `libstdc++.so.6` in the standard FHS paths):
| tool | why |
| -------- | ------------------------------------------------------- |
| python3 | runtime (CPython 3.10 or newer; no exact version pin) |
| uv | installs **all** Python dependencies (see `pyproject.toml`) |
| mpv | playback + JSON IPC control |
| ffmpeg | muted stream decode (fallback analyser) |
| pulseaudio | `parec`/`pactl` for the synced monitor capture (works with PipeWire) |
| dbus | session bus for MPRIS (`playerctl` included for testing) |
Terminal image rendering uses [Pillow](https://python-pillow.org/) and is
implemented directly (Kitty TGP + iTerm2 OSC 1337) — no terminal probing.
## Run
```bash
nix-shell # enters the FHS dev shell
uv sync
uv run somafm-tui
```
If you used an older `shell.nix` before, recreate the venv once so it picks up
the Nix-provided Python:
```bash
rm -rf .venv && uv sync
```
> FHS shell.nix files do not support `nix-shell --run "..."`. Enter the shell
> interactively and run commands there.
Useful flags:
```bash
uv run somafm-tui --play # start the last station immediately
uv run somafm-tui --station defcon # pick a station
uv run somafm-tui --volume 55
uv run somafm-tui --offline # no network; cached/bundled stations
uv run somafm-tui --no-mpris # don't announce over D-Bus
uv run somafm-tui --list # print station ids
uv run somafm-tui --no-audio # UI demo without mpv/ffmpeg
uv run somafm-tui --spectrum-source monitor # force synced output capture
uv run somafm-tui --spectrum-source stream # force the muted ffmpeg tap
SOMAFM_IMAGES=0 uv run somafm-tui # force the ANSI artwork mosaic
SOMAFM_IMAGES=tgp uv run somafm-tui # force Kitty/TGP artwork
SOMAFM_IMAGES=iterm2 uv run somafm-tui # force iTerm2/WezTerm artwork
```
The UI wants roughly **84×24**; it also works smaller, just less pretty.
## Keys
| key | action |
| -------------- | --------------------------------------------- |
| `↑` / `↓`, `j` / `k` | move through stations |
| `enter` | tune in to the selected station |
| `space` | play / pause |
| `s` | stop |
| `]` / `[` | next / previous station (tunes immediately) |
| `+` / `-` | volume |
| `m` | mute |
| `f` | favourite (favourites sort to the top) |
| `/` | filter by title, genre, description |
| `esc` | clear filter / close search |
| `v` | visualiser: mirrored bars → bars → wave |
| `r` | refresh channels + song history |
| `g` / `G` | top / bottom |
| `?` | help |
| `q` | quit |
## Desktop media widgets (MPRIS)
While playing, the app exports
`org.mpris.MediaPlayer2.somafmtui.instance<pid>` on the **session bus**. Check it:
```bash
playerctl -l
playerctl metadata --format '{{artist}} — {{title}} ({{album}})'
playerctl play-pause
```
`mpris:artUrl` is the track's album art when SomaFM provides it, otherwise the
channel logo. If there is no session bus (SSH without `dbus-run-session`,
containers, …) MPRIS is skipped and the status bar shows `mpris off`.
## How it works
- `somafm_tui/api.py` — async `httpx` client for:
- `channels.json` → stations, genres, listeners, `.pls` playlist URLs
- `<station>.pls` → resolved direct icecast URLs (cached 8h)
- `songs/<station>.json` → last played tracks (cached on disk)
Playlist and channel caching means the UI opens instantly and still works
offline.
- `somafm_tui/player.py` — mpv over a Unix-socket JSON IPC channel. Observes
`pause`/`volume`, handles mirror failover on `end-file`, auto-restarts a dead
engine, and uses `SIGSTOP`/`SIGCONT` when falling back to `ffplay`.
- `somafm_tui/spectrum.py` — 44.1 kHz mono s16le analyser. Prefers capturing
the real sink monitor with `parec` (SYNC); otherwise a muted low-latency
`ffmpeg` decode of the stream (FFT). Hann-windowed 2048-point FFT, log band
grouping, auto-gain, fast attack/decay smoothing, peak hold and waveform
downsampling.
- `somafm_tui/mpris.py` — `dbus-next` MPRIS service in its own thread with a
thread-safe command queue back into the UI.
- `somafm_tui/widgets/` — Textual widgets rendered with native ANSI colours;
backgrounds are `ansi_default` (`SGR 49`), so transparent terminals stay
transparent.
### Files
```
~/.config/somafm-tui/config.json favourites, volume, last station, mode
~/.cache/somafm-tui/channels.json last good station list
~/.cache/somafm-tui/playlists.json resolved stream URLs
~/.cache/somafm-tui/songs/<id>.json song history snapshots
~/.cache/somafm-tui/artwork/<sha1> cached album/channel artwork
```
## Notes / troubleshooting
- **NixOS: `libstdc++.so.6: cannot open shared object file`** — this is NumPy's
manylinux wheel looking for the C++ runtime. `shell.nix` now uses
`pkgs.buildFHSEnv` and installs `stdenv.cc.cc.lib`, so `nix-shell` puts you in
an FHS root where the wheel loads. If the error persists, your `.venv` was
built against a different Python: `rm -rf .venv && uv sync`.
- **`bwrap: No permissions to create a new namespace`** — FHS shells need
unprivileged user namespaces. They are enabled by default on NixOS; inside a
locked-down container you may need to run the app on the host instead.
- **Artwork shows the mosaic?** Real images are auto-enabled for Kitty,
iTerm2 and WezTerm. Force a protocol with `SOMAFM_IMAGES=tgp` (Kitty/TGP) or
`SOMAFM_IMAGES=iterm2` (iTerm2/WezTerm); `SOMAFM_IMAGES=0` forces the ANSI
mosaic. Kitty inside tmux is attempted through tmux passthrough (enable
`set -g allow-passthrough on`); iTerm2/WezTerm inside tmux default to the
mosaic. Images are cached under `~/.cache/somafm-tui/artwork`.
- **Clock shows UTC?** The clock and song history use the system zone from
`/etc/localtime`; a non-UTC `TZ` overrides it. `nix-shell` can inherit a
build-time `TZ=UTC`, which the app deliberately ignores. In a container, bind
the host's `/etc/localtime` or set e.g. `TZ=Europe/Berlin`.
- **MPRIS says off/error?** No session bus. Run from your desktop session
(the shell profile picks up `$XDG_RUNTIME_DIR/bus`). `dbus-run-session -- uv
run somafm-tui` starts a private bus for testing, but desktop widgets won't
see it.
- **No audio?** `somafm-tui` needs `mpv` (preferred) or `ffplay`. Without
either, the UI still runs and tells you. Extra mpv args can be injected with
`SOMAFM_MPV_ARGS` (e.g. `SOMAFM_MPV_ARGS='--audio-device=...'`).
- **Spectrum says `FFT` instead of `SYNC`?** `parec`/`pactl` are unavailable,
the monitor device failed, or you forced the stream tap. The bars are still
real audio, but the second stream can drift relative to playback. Use
`--spectrum-source monitor` to force sync, or `SOMAFM_SPECTRUM_DEVICE=<source>`
to pick a specific monitor/source. `SIM` means the bars are synthesized.
- **Diagnose artwork:** `uv run somafm-tui --diagnose-images` prints the
detected protocol, widget class, terminal env, dependency versions and cache
path. If `protocol` is `None`, force it with `SOMAFM_IMAGES=tgp` (Kitty) or
`SOMAFM_IMAGES=iterm2` (iTerm2/WezTerm).
- **`SYNC` but the bars are flat?** mpv may be playing to a different sink (or
directly to ALSA). Point `SOMAFM_SPECTRUM_DEVICE` at that sink's monitor, or
use `--spectrum-source stream`.
- **Bandwidth** is unchanged in SYNC mode (the monitor capture creates no
second stream). The FFT fallback opens one extra stream; if you are on a
metered link, use `--spectrum-source monitor` (or `--no-audio` for a UI-only
demo).
- SomaFM is listener-supported: if you enjoy it, [donate](https://somafm.com/support/).
## Licence
WTFPL — Do What The Fuck You Want To Public License, Version 2. See
[LICENSE](LICENSE). SomaFM and its channel names are trademarks of
SomaFM.com, LLC.
Terminal image rendering uses [Pillow](https://python-pillow.org/).