From 3866f0277c63923972c55a403b6674a5320c73a8 Mon Sep 17 00:00:00 2001 From: beefcarver Date: Fri, 30 Oct 2026 04:17:55 +0000 Subject: [PATCH] docs(readme): document installation, keybindings and features --- README.md | 247 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..9be9c6f --- /dev/null +++ b/README.md @@ -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/.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` 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` 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 + - `.pls` → resolved direct icecast URLs (cached 8h) + - `songs/.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/.json song history snapshots +~/.cache/somafm-tui/artwork/ 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=` + 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/).