2026-11-03 16:10:25 -05:00
|
|
|
|
# Beefy Player
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
2026-11-03 16:10:25 -05:00
|
|
|
|

|
2026-10-30 04:40:12 +00:00
|
|
|
|
|
2026-11-03 16:10:25 -05:00
|
|
|
|
A flashy terminal player for **internet radio** — a live spectrum analyser,
|
2026-11-04 15:11:49 -05:00
|
|
|
|
song history and album art, with desktop media integration on Linux and macOS.
|
|
|
|
|
|
It draws with your terminal's own ANSI palette on the terminal's default
|
|
|
|
|
|
background, so it fits any theme, including transparent terminals.
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
Thirteen station catalogues ship today, from SomaFM's curated channels to the
|
|
|
|
|
|
62,000-station Radio Browser directory, loaded as you scroll and searched live
|
|
|
|
|
|
as you type. Press `p` to switch catalogue without restarting.
|
2026-11-03 17:51:27 -05:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
## Features
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
- **Live spectrum analyser** — 72 log-spaced bands with peak caps, a palette
|
|
|
|
|
|
gradient and a mirrored spectrum. Six modes (`v`): mirrored bars, plain bars,
|
|
|
|
|
|
oscilloscope, scrolling waterfall, radial spectrum and a high-resolution
|
|
|
|
|
|
braille oscilloscope.
|
|
|
|
|
|
- **Synced to what you hear** — the analyser listens to your actual audio
|
|
|
|
|
|
output by default, so the bars follow the music. If that is unavailable it
|
|
|
|
|
|
falls back to decoding the stream, then to a simulated display.
|
|
|
|
|
|
- **Now playing and history** — artist, title, album, DJ, listeners, bitrate
|
|
|
|
|
|
and "played N minutes ago", plus the full recent history. A clickable
|
|
|
|
|
|
**Bandcamp ↗** link appears when the current track can be found there.
|
|
|
|
|
|
- **Real album art** — native images in Kitty, iTerm2 and WezTerm; an
|
|
|
|
|
|
ANSI-shaded mosaic in every other terminal.
|
|
|
|
|
|
- **Thirteen station catalogues** — SomaFM (≈50 channels), Radio Paradise,
|
|
|
|
|
|
tilderadio, Bassdrive, dublab, Rinse FM, Refuge Worldwide, LYL Radio, The Lot
|
|
|
|
|
|
Radio, Cashmere Radio, Datafruits, aNONradio and the Radio Browser directory
|
|
|
|
|
|
(loaded as you scroll in popularity order, with live search across all
|
|
|
|
|
|
62,000).
|
|
|
|
|
|
- **Favourites, search and schedules** — favourites float to the top and are
|
|
|
|
|
|
remembered between runs, including directory stations outside the loaded
|
|
|
|
|
|
page; search by title, genre or description, with live server-side search
|
|
|
|
|
|
across the Radio Browser directory; single-stream stations show their
|
|
|
|
|
|
broadcast schedule instead of a one-item list.
|
|
|
|
|
|
- **Desktop media integration** — MPRIS on Linux (GNOME/KDE widgets, lock
|
|
|
|
|
|
screens, `playerctl` and the media keys) and Now Playing on macOS (Control
|
|
|
|
|
|
Center and the media keys).
|
|
|
|
|
|
- **Sixteen palettes, theme-aware** — every colour is native ANSI against the
|
|
|
|
|
|
terminal's default background, so transparent terminals stay transparent and
|
|
|
|
|
|
the TUI follows your colour scheme. Press `c` to cycle palettes.
|
|
|
|
|
|
- **Graceful offline** — cached stations and schedules, a bundled fallback
|
|
|
|
|
|
list, automatic mirror failover and `ffplay` when `mpv` is missing.
|
|
|
|
|
|
|
|
|
|
|
|
## Installation
|
|
|
|
|
|
|
2026-11-04 16:20:38 -05:00
|
|
|
|
### Nix
|
|
|
|
|
|
|
|
|
|
|
|
The flake builds the package, runs the test suite and wraps everything the app
|
|
|
|
|
|
calls at runtime (`mpv`, `ffmpeg`, and PulseAudio's `parec`/`pactl` — PipeWire
|
|
|
|
|
|
speaks the same protocol):
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
nix run git+https://tildeforge.dev/beefcarver/beefy-player # try it
|
|
|
|
|
|
nix profile install git+https://tildeforge.dev/beefcarver/beefy-player # install it
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
In a NixOS configuration:
|
|
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
|
{
|
|
|
|
|
|
inputs.beefy-player.url = "git+https://tildeforge.dev/beefcarver/beefy-player";
|
|
|
|
|
|
|
|
|
|
|
|
outputs = { nixpkgs, beefy-player, ... }: {
|
|
|
|
|
|
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
|
|
|
|
|
modules = [
|
|
|
|
|
|
beefy-player.nixosModules.default
|
|
|
|
|
|
{
|
|
|
|
|
|
programs.beefy-player.enable = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Or, for a single user with Home Manager:
|
|
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
|
{
|
|
|
|
|
|
inputs.beefy-player.url = "git+https://tildeforge.dev/beefcarver/beefy-player";
|
|
|
|
|
|
|
|
|
|
|
|
outputs = { home-manager, beefy-player, ... }: {
|
|
|
|
|
|
homeConfigurations.me = home-manager.lib.homeManagerConfiguration {
|
|
|
|
|
|
modules = [
|
|
|
|
|
|
beefy-player.homeModules.default
|
|
|
|
|
|
{
|
|
|
|
|
|
programs.beefy-player.enable = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Both modules add one option, `programs.beefy-player.package`, so a pinned or
|
|
|
|
|
|
overridden build can be substituted for the flake's own. The raw package
|
|
|
|
|
|
(`packages.${system}.default`) and an overlay (`overlays.default`) are exported
|
|
|
|
|
|
too. From a checkout, `nix develop` gives you `uv`, `ruff` and the audio tools
|
|
|
|
|
|
for the usual `uv sync` / `uv run pytest` workflow; `nix build` builds and
|
|
|
|
|
|
tests the package.
|
|
|
|
|
|
|
|
|
|
|
|
### From source
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
uv sync
|
|
|
|
|
|
uv run beefy-player
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
## Usage
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-11-04 15:11:49 -05:00
|
|
|
|
beefy-player
|
2026-10-30 04:17:55 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
The UI wants roughly **84×24**; it also works smaller, just less pretty.
|
|
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
| flag | what it does |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| `--provider <slug>` | pick a station catalogue |
|
|
|
|
|
|
| `--station <id>` | pick a station (see `--list`) |
|
|
|
|
|
|
| `--play` | tune the last station right away |
|
|
|
|
|
|
| `--list` | print station ids for the current catalogue |
|
|
|
|
|
|
| `--volume <0-100>` | set the initial volume |
|
|
|
|
|
|
| `--offline` | no network; use cached and bundled stations |
|
|
|
|
|
|
| `--no-audio` | UI demo without `mpv` / `ffmpeg` |
|
|
|
|
|
|
| `--no-mpris` | no MPRIS / Now Playing, no media keys |
|
|
|
|
|
|
| `--spectrum-source monitor\|stream` | force synced output capture or the stream tap |
|
|
|
|
|
|
|
|
|
|
|
|
Environment variables are prefixed `BEEFY_`: `BEEFY_IMAGES` (`tgp`, `iterm2`
|
|
|
|
|
|
or `0`), `BEEFY_MPV_ARGS` and `BEEFY_SPECTRUM_DEVICE`.
|
|
|
|
|
|
|
|
|
|
|
|
### Keys
|
|
|
|
|
|
|
|
|
|
|
|
| key | action |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| `↑` / `↓`, `j` / `k` | move through stations |
|
|
|
|
|
|
| `pgup` / `pgdn` | page 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` | close the search box; press again to clear the filter |
|
|
|
|
|
|
| `v` | visualiser: mirror → bars → wave → waterfall → radial → braille |
|
|
|
|
|
|
| `c` | colour palette (16 of them, remembered) |
|
|
|
|
|
|
| `r` | refresh stations, schedule and song history |
|
|
|
|
|
|
| `p` | switch station catalogue |
|
|
|
|
|
|
| `g` / `G` | top / bottom |
|
|
|
|
|
|
| `?` | help |
|
|
|
|
|
|
| `q` | quit |
|
|
|
|
|
|
|
|
|
|
|
|
## Station catalogues
|
|
|
|
|
|
|
|
|
|
|
|
| catalogue | slug | stations |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| [SomaFM](https://somafm.com) | `somafm` | ≈50 curated channels |
|
|
|
|
|
|
| [Radio Paradise](https://radioparadise.com) | `radioparadise` | 5 mixes |
|
|
|
|
|
|
| [tilderadio](https://tilderadio.org) | `tilderadio` | 1 + schedule |
|
|
|
|
|
|
| [Bassdrive](https://bassdrive.com) | `bassdrive` | 1 + schedule |
|
|
|
|
|
|
| [dublab](https://dublab.com) | `dublab` | 1 + schedule |
|
|
|
|
|
|
| [Rinse FM](https://rinse.fm) | `rinse` | 3 |
|
|
|
|
|
|
| [Refuge Worldwide](https://refugeworldwide.com) | `refuge` | 1 |
|
|
|
|
|
|
| [LYL Radio](https://lyl.live) | `lyl` | 1 |
|
|
|
|
|
|
| [The Lot Radio](https://thelotradio.com) | `lot` | 1 |
|
|
|
|
|
|
| [Cashmere Radio](https://cashmereradio.com) | `cashmere` | 1 |
|
|
|
|
|
|
| [Datafruits](https://datafruits.fm) | `datafruits` | 1 |
|
|
|
|
|
|
| [aNONradio](https://anonradio.net) | `anonradio` | 1 |
|
|
|
|
|
|
| [Radio Browser](https://www.radio-browser.info) | `radiobrowser` | 62k · paged + live search |
|
|
|
|
|
|
|
|
|
|
|
|
Pick one with `p` in the UI, `--provider <slug>` on the command line, or by
|
|
|
|
|
|
setting `"provider"` in the config file.
|
|
|
|
|
|
|
|
|
|
|
|
## Desktop media integration
|
|
|
|
|
|
|
|
|
|
|
|
**Linux** — while playing, the app exposes an
|
|
|
|
|
|
`org.mpris.MediaPlayer2.beefyplayer.instance<pid>` player on the session bus,
|
|
|
|
|
|
so GNOME/KDE widgets, lock screens, `playerctl` and the media keys pick it up:
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
playerctl -l
|
|
|
|
|
|
playerctl metadata --format '{{artist}} — {{title}} ({{album}})'
|
|
|
|
|
|
playerctl play-pause
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
If there is no session bus (SSH without `dbus-run-session`, containers, …)
|
|
|
|
|
|
MPRIS is skipped and the status bar shows `mpris off`.
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
**macOS** — mpv publishes to the system Now Playing session, so Control Center,
|
|
|
|
|
|
the menu bar and the media keys drive the app while it plays. The status bar
|
|
|
|
|
|
shows `nowplaying on`. `--no-mpris` (also spelled `--no-media`) turns the media
|
|
|
|
|
|
keys off.
|
2026-10-30 18:43:26 -04:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
> macOS shows one Now Playing app at a time. If Music, Safari or another mpv is
|
|
|
|
|
|
> playing, the media keys go there instead.
|
2026-10-30 18:43:26 -04:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
## Configuration
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
Settings live in `~/.config/beefy-player/config.json` (favourites, volume,
|
|
|
|
|
|
provider and the last station tuned in each catalogue). Cached stations,
|
|
|
|
|
|
schedules and artwork live in `~/.cache/beefy-player/`.
|
2026-11-03 16:10:25 -05:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
These stay in the usual XDG locations on every platform, macOS included;
|
|
|
|
|
|
`XDG_CONFIG_HOME` and `XDG_CACHE_HOME` are honoured when set.
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
## Troubleshooting
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
- **Artwork shows the mosaic?** Real images are auto-enabled for Kitty, iTerm2
|
|
|
|
|
|
and WezTerm. Force a protocol with `BEEFY_IMAGES=tgp` (Kitty) or
|
|
|
|
|
|
`BEEFY_IMAGES=iterm2` (iTerm2/WezTerm); `BEEFY_IMAGES=0` forces the mosaic.
|
|
|
|
|
|
`--diagnose-images` prints what was detected.
|
|
|
|
|
|
- **No audio?** `mpv` is preferred, `ffplay` works as a fallback. Without
|
|
|
|
|
|
either, the UI still runs and tells you. Extra mpv arguments can be injected
|
|
|
|
|
|
with `BEEFY_MPV_ARGS`, e.g. `BEEFY_MPV_ARGS='--audio-device=...'`.
|
|
|
|
|
|
- **Spectrum says `FFT` instead of `SYNC`?** Output capture is unavailable or
|
|
|
|
|
|
failed, so the analyser is decoding the stream instead; the bars are still
|
|
|
|
|
|
real audio but can drift. Force sync with `--spectrum-source monitor`, or
|
|
|
|
|
|
pick a specific monitor with `BEEFY_SPECTRUM_DEVICE` on Linux. `SIM` means the
|
|
|
|
|
|
bars are synthesized.
|
2026-10-30 22:53:29 -04:00
|
|
|
|
- **macOS: spectrum stays `FFT`?** Grant your terminal **System Audio
|
2026-11-04 15:11:49 -05:00
|
|
|
|
Recording** permission under *System Settings → Privacy & Security → System
|
|
|
|
|
|
Audio Recording*, then restart the app. Also check that mpv is playing to the
|
|
|
|
|
|
default output device, since that is what the analyser records.
|
|
|
|
|
|
- **macOS: media keys do nothing?** Another app owns the Now Playing session,
|
|
|
|
|
|
the app was started with `--no-mpris`, or audio fell back to `ffplay` (mpv is
|
|
|
|
|
|
required for Now Playing).
|
|
|
|
|
|
- **MPRIS says off?** There is no session bus. Run from your desktop session,
|
|
|
|
|
|
or use `dbus-run-session` for a private bus. macOS does not use MPRIS.
|
|
|
|
|
|
|
|
|
|
|
|
## Development
|
2026-10-30 04:24:53 +00:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-11-04 15:11:49 -05:00
|
|
|
|
uv sync # create the environment
|
|
|
|
|
|
uv run pytest # test suite
|
|
|
|
|
|
uvx ruff check . # lint
|
2026-10-30 04:24:53 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-11-04 15:11:49 -05:00
|
|
|
|
Each catalogue is a self-contained provider module under
|
|
|
|
|
|
`beefy_player/providers/`; `base.py` documents the interface. `tools/ansi_art.py`
|
2026-11-04 16:20:38 -05:00
|
|
|
|
converts a PNG logo into half-block ANSI art. On NixOS, `nix develop` (or the
|
|
|
|
|
|
older `shell.nix`) provides the same `uv sync` / `uv run` workflow.
|
2026-10-30 04:24:53 +00:00
|
|
|
|
|
2026-10-30 04:17:55 +00:00
|
|
|
|
## Licence
|
|
|
|
|
|
|
|
|
|
|
|
WTFPL — Do What The Fuck You Want To Public License, Version 2. See
|
|
|
|
|
|
[LICENSE](LICENSE). SomaFM and its channel names are trademarks of
|
2026-11-03 17:51:27 -05:00
|
|
|
|
SomaFM.com, LLC. tilderadio belongs to the tildeverse community.
|
2026-10-30 04:17:55 +00:00
|
|
|
|
|
|
|
|
|
|
Terminal image rendering uses [Pillow](https://python-pillow.org/).
|