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 playbackmpv runs in --idle mode in the background. Your shell stays usable.
  • Instant station switching — switching sends an IPC loadfile to the same mpv process, no restart.
  • Now-playing metadatamedia-title from ICY tags, pulled over IPC.
  • Volume controlfradio vol [N|+N|-N] and fradio mute, persisted across stop/play.
  • Tags & filtering — stations carry comma-separated tags; fradio list -t jazz filters.
  • Search & discoveryfradio search queries 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 integrationfradio status prints 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 -t tag 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:

PlatformCommand
macOS (Homebrew)brew install mpv
macOS (MacPorts)sudo port install mpv
Arch / Manjaro / EndeavourOSsudo pacman -S mpv
Debian / Ubuntu / Mint / Pop!_OSsudo apt install mpv
Fedora / RHEL / Rocky / Almasudo dnf install mpv
openSUSEsudo zypper install mpv
Alpinesudo apk add mpv
Voidsudo xbps-install -S mpv
Windowswinget / 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:

  1. Installs any missing deps (mpv, fish, python3) via the native package manager (Homebrew, MacPorts, pacman, apt, dnf, zypper, apk, xbps).
  2. Symlinks fradio.fish into ~/.config/fish/conf.d/ so fish autoloads it on shell startup. (conf.d/ rather than functions/ because completions need to register at startup, not on first call.)
  3. Copies starship.toml into ~/.config/starship.toml (backing up any existing one). Pass --no-starship to 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 -c in shell. Starship pipes the command to stdin, not as a -c arg. Adding -c makes fish bail with -c: option requires an argument. Same pattern as starship's bash example: ["bash", "--noprofile", "--norc"].
  • --no-config skips loading your full config.fish on every prompt render so the module stays fast.
  • when is its own command. Without it the styled pill renders as an empty [ ] whenever playback is stopped. fradio status | string length -q exits 0 only if the input has any non-empty line.
  • Both command and when source fradio.fish explicitly. Makes the module work regardless of whether your config.fish sources 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/fradiofradio.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.