A tiny command-line player for deep-focus internet radio. No UI, no
daemon config, no library to maintain — a single bash script around
mpv. Start a station, get your terminal back, work.
$ focusradio play drone 50m
▶ drone (for 50m, fading out at the end)
$ focusradio now
▶ drone — Chronotope Project - Mind's I
- 14 curated focus stations — SomaFM ambient classics (Drone Zone, Groove Salad, Deep Space One…), beat-free drone (Cryosleep, Ambient Sleeping Pill, 9128.live), NTS Slow Focus, freeCodeCamp Code Radio. Every stream verified working; the list is one obvious block at the top of the script.
- Discover more —
focusradio discover <tag>searches the Radio Browser community database (by tag or keyword, most-played first), skips stations you already have, plays each pick through mpv to confirm it actually works, and saves the keepers to your own list. The built-in stations stay untouched. - Gentle start and stop — playback swells in from silence and eases
out again on
focusradio stop(and when switching stations), instead of cutting in and out abruptly. A short 3-second fade by default; setFOCUSRADIO_FADEto change it (FOCUSRADIO_FADE=0disables it entirely). - Timed sessions with a fade-out —
focusradio play drone 50mplays for 50 minutes, then fades to silence over the last stretch and stops itself. No alarm, no notification: the fading music is the signal that the session is over. - Play log and favorites — everything you hear is scrobbled to a
local TSV file. Heard something great?
focusradio favsaves it;focusradio favsandfocusradio historyread your collection back. - Offline fallback — if the stream dies mid-session (network blip, station outage), focusradio switches to locally generated brown noise instead of yanking you out of flow with silence, then returns to the station automatically once it is reachable again.
mpv, socat, jq, curl — all standard packages:
# Debian/Ubuntu
sudo apt install mpv socat jq curl
# macOS (Homebrew)
brew install mpv socat jq curl
git clone https://github.com/kprzybys/focusradio.git && cd focusradio
ln -sf "$(pwd)/focusradio" ~/.local/bin/focusradio
Make sure ~/.local/bin is on your PATH (most distros add it for you);
then focusradio list works from anywhere. To uninstall, remove the
symlink (rm ~/.local/bin/focusradio) and, if you like, your data
(rm -r ~/.local/share/focusradio).
Optional shell completion for subcommands and station names — the
station list is pulled live from the script, so it stays in sync when
you edit STATIONS. Files live in completions/. All three shells
need focusradio on your PATH (the symlink above) so completion can
query the stations.
bash (needs the bash-completion package and bash 4+ — note stock
macOS ships bash 3.2, so brew install bash if you want it there):
mkdir -p ~/.local/share/bash-completion/completions
ln -sf "$(pwd)/completions/focusradio.bash" \
~/.local/share/bash-completion/completions/focusradio
Open a new shell, then focusradio play <Tab> lists the stations.
zsh — put _focusradio on your $fpath:
mkdir -p ~/.zsh/completions
ln -sf "$(pwd)/completions/_focusradio" ~/.zsh/completions/_focusradio
then, before compinit in ~/.zshrc:
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit
fish — loaded automatically:
mkdir -p ~/.config/fish/completions
ln -sf "$(pwd)/completions/focusradio.fish" \
~/.config/fish/completions/focusradio.fish
focusradio list # available stations (▶ = playing now)
focusradio discover lofi # search Radio Browser by tag/keyword and add stations
focusradio play drone # play; play again to switch stations
focusradio play drone 50m # timed session: fade-out + auto-stop at the end
focusradio now # what's playing
focusradio stop # stop
focusradio fav # save the current track to favorites
focusradio favs # show favorites
focusradio history [n] # last n scrobbled tracks (default 20)
Durations: 50m, 1h, 90s, or a bare number (minutes). The
end-of-session fade takes one third of the session, capped at two
minutes.
The gentle start/stop fade is 3 seconds by default. Override it per
invocation with FOCUSRADIO_FADE (seconds), e.g. FOCUSRADIO_FADE=5 focusradio play drone, or FOCUSRADIO_FADE=0 for the old instant
on/off.
- Play log and favorites:
~/.local/share/focusradio/(played.log,favs.log— plain TSV, yours to grep). - Discovered stations:
~/.local/share/focusradio/stations.local(name|url|description, one per line — edit a line to rename a station, delete a line to remove it). - Runtime state (mpv IPC socket, pidfiles):
$XDG_RUNTIME_DIR/focusradio/, gone after reboot.
focusradio play starts mpv detached with a JSON IPC socket and a
small watcher process alongside it. The watcher polls the socket every
few seconds: it scrobbles title changes, runs the end-of-session fade,
and — if mpv dies while the state says "playing" — starts a brown-noise
generator (lavfi anoisesrc, no files needed) and probes the station
URL every 30 s until it can switch back. focusradio stop tears the
whole thing down.
Edit the STATIONS block at the top of the script — one
name|URL|description line per station. Anything mpv can play works
(.pls playlists, direct stream URLs).
Tests use bats:
bats test/ # everything
bats test/focusradio.bats # fast unit tests only
test/focusradio.bats— unit tests for the pure-logic helpers (duration parsing, station lookup, slug generation, URL validation, run-dir hardening). Theysourcethe script — itsmainruns only when the file is executed, not when sourced.test/e2e.bats— end-to-end tests that drive the real binary and spawn mpv against hermetic local resources: anav://lavfisource for playback, and a throwaway HTTP server standing in for both Radio Browser and the stream host, sodiscoverruns without the network. Needsffmpegandpython3on top of the runtime deps; slower.
Both use throwaway data and runtime dirs, so they never touch your real stations or a running session.
MIT — see LICENSE.