255 lines
10 KiB
Markdown
255 lines
10 KiB
Markdown
# Beefy Player
|
||
|
||

|
||
|
||
A flashy terminal player for **internet radio** — a live spectrum analyser,
|
||
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.
|
||
|
||
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.
|
||
|
||
## Features
|
||
|
||
- **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
|
||
|
||
### 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
|
||
```
|
||
|
||
## Usage
|
||
|
||
```bash
|
||
beefy-player
|
||
```
|
||
|
||
The UI wants roughly **84×24**; it also works smaller, just less pretty.
|
||
|
||
| 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:
|
||
|
||
```bash
|
||
playerctl -l
|
||
playerctl metadata --format '{{artist}} — {{title}} ({{album}})'
|
||
playerctl play-pause
|
||
```
|
||
|
||
If there is no session bus (SSH without `dbus-run-session`, containers, …)
|
||
MPRIS is skipped and the status bar shows `mpris off`.
|
||
|
||
**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.
|
||
|
||
> macOS shows one Now Playing app at a time. If Music, Safari or another mpv is
|
||
> playing, the media keys go there instead.
|
||
|
||
## Configuration
|
||
|
||
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/`.
|
||
|
||
These stay in the usual XDG locations on every platform, macOS included;
|
||
`XDG_CONFIG_HOME` and `XDG_CACHE_HOME` are honoured when set.
|
||
|
||
## Troubleshooting
|
||
|
||
- **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.
|
||
- **macOS: spectrum stays `FFT`?** Grant your terminal **System Audio
|
||
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
|
||
|
||
```bash
|
||
uv sync # create the environment
|
||
uv run pytest # test suite
|
||
uvx ruff check . # lint
|
||
```
|
||
|
||
Each catalogue is a self-contained provider module under
|
||
`beefy_player/providers/`; `base.py` documents the interface. `tools/ansi_art.py`
|
||
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.
|
||
|
||
## Licence
|
||
|
||
WTFPL — Do What The Fuck You Want To Public License, Version 2. See
|
||
[LICENSE](LICENSE). SomaFM and its channel names are trademarks of
|
||
SomaFM.com, LLC. tilderadio belongs to the tildeverse community.
|
||
|
||
Terminal image rendering uses [Pillow](https://python-pillow.org/).
|