somafm-tui

A flashy terminal radio for SomaFM — 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 and is implemented directly (Kitty TGP + iTerm2 OSC 1337) — no terminal probing.

Run

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:

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:

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:

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.

Licence

WTFPL — Do What The Fuck You Want To Public License, Version 2. See LICENSE. SomaFM and its channel names are trademarks of SomaFM.com, LLC.

Terminal image rendering uses Pillow.

S
Description
A flashy terminal player for internet radio
Readme WTFPL
1.6 MiB
0 Stars 1 Watchers 0 Forks
Languages
Python 97.9%
Nix 1.4%
Ruby 0.4%
Shell 0.3%