Compare commits

...
5 Commits
8 changed files with 291 additions and 16 deletions

No files matched your search

+67 -7
View File
@@ -46,6 +46,69 @@ as you type. Press `p` to switch catalogue without restarting.
## Installation ## 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 ## Usage
```bash ```bash
@@ -179,13 +242,10 @@ uvx ruff check . # lint
Each catalogue is a self-contained provider module under Each catalogue is a self-contained provider module under
`beefy_player/providers/`; `base.py` documents the interface. `tools/ansi_art.py` `beefy_player/providers/`; `base.py` documents the interface. `tools/ansi_art.py`
converts a PNG logo into half-block ANSI art. On NixOS, `shell.nix` provides an converts a PNG logo into half-block ANSI art. On NixOS, `nix develop` (or the
FHS shell for the same `uv sync` / `uv run` workflow. older `shell.nix`) provides the same `uv sync` / `uv run` workflow.
## Licence ## Licence
WTFPL — Do What The Fuck You Want To Public License, Version 2. See WTFPL — Do What The Fuck You Want To Public License, Version 2.
[LICENSE](LICENSE). SomaFM and its channel names are trademarks of See [LICENSE](LICENSE).
SomaFM.com, LLC. tilderadio belongs to the tildeverse community.
Terminal image rendering uses [Pillow](https://python-pillow.org/).
+1 -1
View File
@@ -1,3 +1,3 @@
"""Beefy Player — a flashy terminal player for internet radio.""" """Beefy Player — a flashy terminal player for internet radio."""
__version__ = "1.2.1" __version__ = "1.2.2"
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1793726008,
"narHash": "sha256-jQp1wKvbwWdLBASTXkSi/VYmDJ5AbBi1WvcMt9HWJC8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1d060086d60cb76a40cf37109f979d88a5bd6396",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+188
View File
@@ -0,0 +1,188 @@
{
description = "beefy-player — a flashy terminal player for internet radio";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
{ self, nixpkgs }:
let
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
# Things the app shells out to at runtime: mpv for audio, ffmpeg for the
# spectrum capture, and parec/pactl from PulseAudio for the monitor source
# (PipeWire speaks the same protocol).
runtimeDeps =
pkgs:
with pkgs;
[
mpv
ffmpeg
]
++ lib.optionals stdenv.hostPlatform.isLinux [ pulseaudio ];
beefy-player =
pkgs:
let
py = pkgs.python3Packages;
in
py.buildPythonApplication {
pname = "beefy-player";
version = "1.2.2";
pyproject = true;
src = self;
build-system = [ py.hatchling ];
dependencies =
with py;
[
textual
rich
httpx
numpy
pillow
]
++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ dbus-next ];
nativeBuildInputs = [ pkgs.makeWrapper ];
nativeCheckInputs = with py; [
pytestCheckHook
pytest-asyncio
];
# The suite is offline (catalogues are cached fixtures).
doCheck = true;
pythonImportsCheck = [ "beefy_player" ];
makeWrapperArgs = [
"--prefix"
"PATH"
":"
(pkgs.lib.makeBinPath (runtimeDeps pkgs))
];
meta = {
description = "Flashy terminal player for internet radio, with a live spectrum analyser";
homepage = "https://tildeforge.dev/beefcarver/beefy-player";
license = pkgs.lib.licenses.wtfpl;
mainProgram = "beefy-player";
platforms = pkgs.lib.platforms.unix;
};
};
# One module body, two install targets: a NixOS system adds the package
# to environment.systemPackages, Home Manager to home.packages.
mkModule =
install:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.programs.beefy-player;
in
{
options.programs.beefy-player = {
enable = lib.mkEnableOption "beefy-player, a terminal radio player";
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
defaultText = lib.literalMD "the `beefy-player` package from this flake";
description = "The beefy-player package to use.";
};
};
config = lib.mkIf cfg.enable (lib.setAttrByPath install [ cfg.package ]);
};
in
{
packages = forAllSystems (
pkgs:
let
pkg = beefy-player pkgs;
in
{
default = pkg;
beefy-player = pkg;
}
);
apps = forAllSystems (pkgs: {
default = {
type = "app";
program = "${beefy-player pkgs}/bin/beefy-player";
meta.description = "Flashy terminal player for internet radio";
};
});
checks = forAllSystems (pkgs: {
beefy-player = beefy-player pkgs;
});
devShells = forAllSystems (
pkgs:
let
inherit (pkgs) lib;
in
{
# No FHS needed here: `nix develop` puts libstdc++ on LD_LIBRARY_PATH
# so uv's manylinux wheels load, unlike the plain `nix-shell` case in
# shell.nix.
default = pkgs.mkShell {
packages = runtimeDeps pkgs ++ [
pkgs.python3
pkgs.uv
pkgs.ruff
pkgs.git
pkgs.cacert
];
env = {
UV_PYTHON = "${pkgs.python3}/bin/python3";
UV_PYTHON_DOWNLOADS = "never";
UV_PROJECT_ENVIRONMENT = "${toString ./.}/.venv";
UV_LINK_MODE = "copy";
SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
};
LD_LIBRARY_PATH = lib.makeLibraryPath [
pkgs.stdenv.cc.cc.lib
pkgs.zlib
];
shellHook = ''
echo "beefy-player dev shell — uv sync, uv run pytest, uv run beefy-player"
'';
};
}
);
nixosModules.default = mkModule [
"environment"
"systemPackages"
];
nixosModules.beefy-player = self.nixosModules.default;
homeModules.default = mkModule [
"home"
"packages"
];
homeModules.beefy-player = self.homeModules.default;
formatter = forAllSystems (pkgs: pkgs.nixfmt);
overlays.default = final: _prev: {
beefy-player = beefy-player final;
};
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "beefy-player" name = "beefy-player"
version = "1.2.1" version = "1.2.2"
description = "A flashy terminal player for internet radio — live spectrum analyser, MPRIS / macOS Now Playing support, song history and pluggable station catalogues (SomaFM, Radio Paradise, dublab, Rinse FM and more)" description = "A flashy terminal player for internet radio — live spectrum analyser, MPRIS / macOS Now Playing support, song history and pluggable station catalogues (SomaFM, Radio Paradise, dublab, Rinse FM and more)"
readme = "README.md" readme = "README.md"
license = "WTFPL" license = "WTFPL"
+2 -2
View File
@@ -2,13 +2,13 @@ from __future__ import annotations
import os import os
import queue import queue
import sys
import time import time
from pathlib import Path from pathlib import Path
from beefy_player.player import Player from beefy_player.player import Player
FAKE_MPV = '''#!/usr/bin/env python3 FAKE_MPV = f"#!{sys.executable}\n" + '''import json, os, socket, sys, time
import json, os, socket, sys, time
sock_path = None sock_path = None
volume = 70 volume = 70
+4 -4
View File
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import sys
import time import time
from pathlib import Path from pathlib import Path
@@ -8,8 +9,7 @@ import numpy as np
from beefy_player import spectrum as spectrum_module from beefy_player import spectrum as spectrum_module
from beefy_player.spectrum import Frame, SpectrumEngine from beefy_player.spectrum import Frame, SpectrumEngine
PCM_SCRIPT = """#!/usr/bin/env python3 PCM_SCRIPT = f"#!{sys.executable}\n" + """import math, struct, sys, time
import math, struct, sys, time
sr = 44100 sr = 44100
samples = [int(0.6 * 32767 * math.sin(2 * math.pi * 1000 * i / sr)) for i in range(4096)] samples = [int(0.6 * 32767 * math.sin(2 * math.pi * 1000 * i / sr)) for i in range(4096)]
chunk = struct.pack("<%dh" % len(samples), *samples) chunk = struct.pack("<%dh" % len(samples), *samples)
@@ -303,7 +303,7 @@ def test_tap_audio_is_downmixed_and_retuned() -> None:
def test_dead_monitor_falls_back_instead_of_flat_bars(tmp_path) -> None: def test_dead_monitor_falls_back_instead_of_flat_bars(tmp_path) -> None:
script = tmp_path / "silent-parec" script = tmp_path / "silent-parec"
script.write_text( script.write_text(
"#!/usr/bin/env python3\n" f"#!{sys.executable}\n"
"import sys\n" "import sys\n"
"block = b'\\0' * 4096\n" "block = b'\\0' * 4096\n"
"for _ in range(1000):\n" "for _ in range(1000):\n"
@@ -333,7 +333,7 @@ def test_monitor_survives_silence_after_audio(tmp_path, monkeypatch) -> None:
# replaced by the drifting stream tap) for the rest of the session. # replaced by the drifting stream tap) for the rest of the session.
script = tmp_path / "parec" script = tmp_path / "parec"
script.write_text( script.write_text(
"#!/usr/bin/env python3\n" f"#!{sys.executable}\n"
"import math, struct, sys, time\n" "import math, struct, sys, time\n"
"sr = 44100\n" "sr = 44100\n"
"n = 2048\n" "n = 2048\n"
Generated
+1 -1
View File
@@ -32,7 +32,7 @@ wheels = [
[[package]] [[package]]
name = "beefy-player" name = "beefy-player"
version = "1.2.1" version = "1.2.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "dbus-next", marker = "sys_platform == 'linux'" }, { name = "dbus-next", marker = "sys_platform == 'linux'" },