354 lines
14 KiB
Markdown
354 lines
14 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.
|
||
|
||
### Homebrew
|
||
|
||
A tap lives at **[tildeforge.dev/beefcarver/homebrew](https://tildeforge.dev/beefcarver/homebrew)**.
|
||
Homebrew's one-argument `brew tap` assumes GitHub, so point it at the tap URL
|
||
and trust the formula explicitly:
|
||
|
||
```bash
|
||
brew tap beefcarver/homebrew https://tildeforge.dev/beefcarver/homebrew.git
|
||
brew trust --formula beefcarver/homebrew/beefy-player
|
||
brew install beefcarver/homebrew/beefy-player
|
||
```
|
||
|
||
`brew update` refreshes the tap and `brew upgrade` picks up new versions like
|
||
any other formula. The formula builds from source, with `numpy`, `pillow`,
|
||
`mpv` and `ffmpeg` pulled in as Homebrew dependencies.
|
||
|
||
No release is tagged yet, so the formula still carries a placeholder checksum
|
||
and cannot install from a tarball. Until the first `v*` tag is published, use
|
||
`brew install --HEAD beefcarver/homebrew/beefy-player`.
|
||
|
||
The tap is a git submodule at `contrib/homebrew`, so the formula and its
|
||
maintenance notes live in the tap repository itself; after cloning this one,
|
||
run `git submodule update --init` to check it out.
|
||
|
||
### Arch Linux
|
||
|
||
The `contrib/aur/PKGBUILD` here is AUR-ready, but the package is not published
|
||
in the AUR: the AUR has closed new sign-ups, so there is no account to push it
|
||
to. Build and install it from a checkout instead:
|
||
|
||
```bash
|
||
git clone https://tildeforge.dev/beefcarver/beefy-player
|
||
cd beefy-player/contrib/aur
|
||
makepkg -si # needs base-devel; builds, runs the test suite, installs
|
||
```
|
||
|
||
Once the AUR accepts new accounts again, the package can be published and
|
||
`paru -S beefy-player` / `yay -S beefy-player` will work; until then `makepkg`
|
||
is the only route. The `contrib/aur/PKGBUILD` is tracked and kept in sync with
|
||
every release either way.
|
||
|
||
The Python side is pulled in automatically. For audio, the output-synced
|
||
spectrum and image passthrough through tmux, install the optional tools too:
|
||
|
||
```bash
|
||
sudo pacman -S mpv ffmpeg libpulse tmux
|
||
```
|
||
|
||
### Standalone binary (no Nix, no packaging step)
|
||
|
||
`uv` can build the package and drop the executable in `~/.local/bin` — this is
|
||
the `pip install --user`-style install, without touching a system or project
|
||
environment:
|
||
|
||
```bash
|
||
uv tool install git+https://tildeforge.dev/beefcarver/beefy-player
|
||
# or, from a checkout:
|
||
uv tool install .
|
||
```
|
||
|
||
That creates its own isolated environment under `~/.local/share/uv/tools/` and
|
||
symlinks `beefy-player` into `~/.local/bin` (uv's default, which is also where
|
||
`pip install --user` writes on Linux). If uv warns that the directory is not on
|
||
your `PATH`:
|
||
|
||
```bash
|
||
uv tool update-shell # or add ~/.local/bin to PATH by hand
|
||
```
|
||
|
||
Then run `beefy-player` from anywhere. `uv tool upgrade beefy-player`
|
||
re-installs the current source; `uv tool uninstall beefy-player` removes it.
|
||
`pipx install git+https://tildeforge.dev/beefcarver/beefy-player` is the
|
||
`pipx` equivalent and lands in the same place.
|
||
|
||
Only the Python side is installed this way, so `mpv` / `ffmpeg` (and `parec`
|
||
for the synced spectrum) still come from your package manager; without them the
|
||
app runs but has no audio.
|
||
|
||
> On macOS, note that `~/.local/bin` is uv's and pipx's choice, not Python's:
|
||
> a `pip install --user` against Homebrew's Python uses
|
||
> `~/Library/Python/3.x/bin` instead.
|
||
|
||
### From source
|
||
|
||
`uv sync` installs into a project virtualenv, so the executable is
|
||
`.venv/bin/beefy-player` and is not on your `PATH`:
|
||
|
||
```bash
|
||
uv sync
|
||
uv run beefy-player # or: .venv/bin/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
|
||
git submodule update --init # contrib/homebrew, the Homebrew tap
|
||
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. Every catalogue's
|
||
header is a wordmark from the shared half-block font, and the header paints it
|
||
with the active palette gradient (`hero_gradient`, on by default) so all
|
||
thirteen follow the terminal's colours. `tools/ansi_art.py` converts a PNG logo
|
||
into half-block ANSI art that carries colours of its own; a provider using that
|
||
art must set `hero_gradient = False`, because the header would otherwise
|
||
repaint it with the gradient.
|
||
|
||
`uvx ruff check .` is the enforced lint. The tree is not `ruff format`-clean (54
|
||
files would change), so do not reformat it wholesale.
|
||
|
||
Pushes run the test suite (`.forgejo/workflows/tests.yml`); a `v*` tag runs
|
||
`release.yml`, which checks that the tag, `pyproject.toml` and
|
||
`beefy_player.__version__` agree and then publishes the release notes. It does
|
||
not touch packaging, so `contrib/aur/PKGBUILD` and the tap's formula are bumped
|
||
by hand for each release.
|
||
|
||
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).
|