fradio
fradio is a small fish-shell command-line internet radio player I built on top of mpv's JSON IPC socket. One detached mpv process holds the stream; fradio subcommands talk to it over a unix domain socket to switch stations, query metadata, control playback, and adjust volume. The terminal stays free; the prompt knows what's playing; switching stations is instant.
Why this exists
The original "implementation" was a handful of fish functions that each shelled out to mpv in the foreground:
function kalx320
mpv https://stream.kalx.berkeley.edu:8443/kalx.flac
end
That blocks the terminal, has no notion of "currently playing", can't switch stations without Ctrl-C, and can't expose state to a prompt or status bar. fradio fixes all of that with a single persistent mpv backend and a thin fish CLI in front of it.
Features
- Detached playback —
mpvruns in--idlemode in the background. Your shell stays usable. - Instant station switching — switching sends an IPC
loadfileto the samempvprocess, no restart. - Now-playing metadata —
media-titlefrom ICY tags, pulled over IPC. - Volume control —
fradio vol [N|+N|-N]andfradio mute, persisted acrossstop/play. - Tags & filtering — stations carry comma-separated tags;
fradio list -t jazzfilters. - Search & discovery —
fradio searchqueries radio-browser.info, the public crowd-sourced internet radio directory. Multi-term AND, quoted phrases, server-side tag filter, country filter.fradio pick <N>adds a result to your config (with its tags). - Robust streaming — ffmpeg HTTP reconnect flags + a 10s jitter buffer mean long-distance icecast streams survive transient TCP drops.
- Prompt/status integration —
fradio statusprints a one-liner when playing and nothing when stopped, so prompt modules collapse cleanly. ~30ms per call. - Tab completion — fish completions for subcommands, station names, and
-ttag values. - Cross-platform installer — one script handles macOS (Homebrew/MacPorts) and the major Linux distros.
Install
Requirements: mpv, fish, and python3 (the IPC client is plain Python — no nc dependency, which used to be a footgun on Linux distros where gnu-netcat lacks -U).
About mpv
mpv is a free, open-source media player descended from MPlayer/mplayer2 with FFmpeg under the hood. It's headless-friendly, scriptable, and exposes a JSON IPC protocol over a unix domain socket — which is the entire reason fradio works the way it does. fradio doesn't decode audio itself; it spawns one mpv --idle in the background and sends it commands like loadfile, cycle pause, and set_property volume.
mpv is also a great general-purpose player on its own. If you've never used it, mpv <url> will play more or less anything you point it at.
Installing mpv
The fradio installer (./install.sh) calls the right package manager for you. If you'd rather install mpv by hand:
| Platform | Command |
|---|---|
| macOS (Homebrew) | brew install mpv |
| macOS (MacPorts) | sudo port install mpv |
| Arch / Manjaro / EndeavourOS | sudo pacman -S mpv |
| Debian / Ubuntu / Mint / Pop!_OS | sudo apt install mpv |
| Fedora / RHEL / Rocky / Alma | sudo dnf install mpv |
| openSUSE | sudo zypper install mpv |
| Alpine | sudo apk add mpv |
| Void | sudo xbps-install -S mpv |
| Windows | winget / scoop / installer |
Verify with:
command -v mpv python3 fish
mpv --version
All three commands should resolve and mpv --version should print v0.34 or newer (older versions had buggy --input-ipc-server behavior that fradio has not been tested against).
Installing fradio
git clone git@github.com:jtmack6/fradio.git
cd fradio
./install.sh
The installer:
- Installs any missing deps (mpv, fish, python3) via the native package manager (Homebrew, MacPorts, pacman, apt, dnf, zypper, apk, xbps).
- Symlinks
fradio.fishinto~/.config/fish/conf.d/so fish autoloads it on shell startup. (conf.d/rather thanfunctions/because completions need to register at startup, not on first call.) - Copies
starship.tomlinto~/.config/starship.toml(backing up any existing one). Pass--no-starshipto skip if you manage starship config separately.
Useful flags: --dry-run (preview), --yes (unattended), --no-starship, --help.
First run
fradio list # see seeded stations (KALX, KQED, KFJC, KDVS)
fradio kfjc # play one — any unrecognized arg = station name
fradio now # → ♪ kfjc — <track title>
fradio next # cycle to the next station
fradio search smooth jazz # query radio-browser.info
fradio pick 1 # add result #1 to stations.conf
fradio vol 75 # set volume
fradio mute # toggle
fradio stop # quit mpv
On first invocation, fradio creates ~/.config/fradio/stations.conf and seeds it. Subsequent runs leave it alone.
Discovery
fradio search is the part I use most. Examples:
fradio search -t jazz trio # tag + positional terms (AND)
fradio search -t bebop,fusion # multiple tags (AND)
fradio search -c JP -t jazz # Japanese jazz
fradio search -c US,GB,DE jazz # Anglo-European jazz (country OR)
fradio search -l "smooth jazz" # long format with full URL
fradio search --all classical | less # everything, browseable
fradio pick 3 myslug # explicit short name when auto-slug collides
Tag filtering is substring-matched against the station's tags field; country filtering uses ISO 3166-1 alpha-2 codes (US, GB, JP, ...). Multiple -t flags AND together; multiple -c flags OR together (a station has exactly one country).
Prompt integration (the killer feature)
fradio status is the integration seam. The trick is it prints nothing when stopped, so the surrounding pill collapses instead of rendering an empty box.
Starship
[custom.fradio]
description = "Currently playing radio station"
command = "source ~/Projects/Audio/Radio/fradio/fradio.fish; fradio status"
when = "source ~/Projects/Audio/Radio/fradio/fradio.fish; fradio status | string length -q"
shell = ["fish", "--no-config"]
format = "[ $output ](bg:color_band_status fg:color_accent_magenta)"
Then slot ${custom.fradio} into your main format string, e.g. between $battery and $time.
A few non-obvious things in the above:
- No
-cinshell. Starship pipes the command to stdin, not as a-carg. Adding-cmakes fish bail with-c: option requires an argument. Same pattern as starship's bash example:["bash", "--noprofile", "--norc"]. --no-configskips loading your fullconfig.fishon every prompt render so the module stays fast.whenis its own command. Without it the styled pill renders as an empty[ ]whenever playback is stopped.fradio status | string length -qexits 0 only if the input has any non-empty line.- Both
commandandwhensourcefradio.fishexplicitly. Makes the module work regardless of whether yourconfig.fishsources fradio, and works the same in any subshell starship spawns.
tmux
set -g status-right '#(fish -c "fradio status") | %H:%M'
set -g status-interval 5
status-interval controls poll rate — 5s is plenty since ICY metadata only updates when a track changes. tmux invokes via /bin/sh -c, so fish -c works (fish loads config.fish on -c by default).
fish prompt without starship
function fish_right_prompt
set -l line (fradio status)
if test -n "$line"
set_color magenta
echo $line
set_color normal
end
end
Architecture, briefly
One detached mpv --idle process owns the audio stream. fradio subcommands open the IPC socket (/tmp/fradio.sock), send a single JSON command (loadfile, get_property media-title, cycle pause, etc.), and exit. Volume is mirrored to /tmp/fradio.volume so it survives stop/play cycles — the next mpv --idle reads the file and re-applies via --volume=N.
The IPC client is plain python3 using built-in socket.AF_UNIX. Earlier versions used nc -U, which is fine on macOS but ships missing on a lot of Linux distros (gnu-netcat lacks -U). Switching to python killed that whole class of footgun.
Repo
github.com/jtmack6/fradio — fradio.fish (the player), install.sh (the installer), starship.toml (reference config), README and CLAUDE.md (per-repo docs).
This article was drafted by Claude (Anthropic's Claude Code, Opus 4.7) from my own Obsidian notes on the project — the technical content is mine; the prose stitching is Claude's.