refactor!: rename somafm-tui to beefy-player

The app is now a general internet-radio player rather than a SomaFM client, so the package, console script, MPRIS identity, mpv namespaces, macOS tap names, XDG directories and docs all move to beefy-player.

Environment variables are now BEEFY_* (the old SOMAFM_* spellings are still honoured) and an existing ~/.config/somafm-tui/config.json is migrated on first run, so favourites and volume survive the rename.

The visible interface is untouched: same layout, same logo, same keys.
This commit is contained in:
beefcarver committed 2026-11-03 16:10:25 -05:00
1 parent 4bed90b489
commit e30ca09627
44 files changed
+329 -192

No files matched your search

+6 -6
View File
@@ -3,11 +3,11 @@ from __future__ import annotations
import pytest
from PIL import Image
from somafm_tui import artwork
from somafm_tui.app import SomaFMApp
from somafm_tui.widgets import art as art_widgets
from somafm_tui.widgets.art import ArtPane, ITerm2Image, KittyImage
from somafm_tui.widgets.stage import StagePanel
from beefy_player import artwork
from beefy_player.app import BeefyPlayerApp
from beefy_player.widgets import art as art_widgets
from beefy_player.widgets.art import ArtPane, ITerm2Image, KittyImage
from beefy_player.widgets.stage import StagePanel
async def _no_fetch(client, url: str) -> None:
@@ -27,7 +27,7 @@ async def test_art_pane_switches_between_image_and_mosaic(monkeypatch) -> None:
monkeypatch.setattr(artwork, "load_cached", lambda url: None)
monkeypatch.setattr(artwork, "fetch", _no_fetch)
app = SomaFMApp(autoplay=False, offline=True, audio=False)
app = BeefyPlayerApp(autoplay=False, offline=True, audio=False)
async with app.run_test(size=(110, 40)) as pilot:
await pilot.pause()
pane = app.query_one(ArtPane)
+6 -4
View File
@@ -6,7 +6,7 @@ import httpx
import pytest
from PIL import Image
from somafm_tui import artwork
from beefy_player import artwork
def png_bytes(size=(400, 200)) -> bytes:
@@ -28,9 +28,11 @@ def test_protocol_detection() -> None:
assert artwork.detect_protocol({"ZELLIJ": "1", "TERM_PROGRAM": "WezTerm"}) is None
assert artwork.detect_protocol({"KITTY_PID": "1"}) == "tgp"
assert artwork.detect_protocol({"NO_COLOR": "", "KITTY_WINDOW_ID": "1"}) is None
assert artwork.detect_protocol({"SOMAFM_IMAGES": "0", "KITTY_WINDOW_ID": "1"}) is None
assert artwork.detect_protocol({"SOMAFM_IMAGES": "tgp"}) == "tgp"
assert artwork.detect_protocol({"SOMAFM_IMAGES": "iterm2"}) == "iterm2"
assert artwork.detect_protocol({"BEEFY_IMAGES": "0", "KITTY_WINDOW_ID": "1"}) is None
assert artwork.detect_protocol({"BEEFY_IMAGES": "tgp"}) == "tgp"
assert artwork.detect_protocol({"BEEFY_IMAGES": "iterm2"}) == "iterm2"
# the pre-rename variable still works
assert artwork.detect_protocol({"SOMAFM_IMAGES": "0"}) is None
def test_fit_image_crops_to_the_cell_box() -> None:
+1 -1
View File
@@ -5,7 +5,7 @@ import json
import httpx
import pytest
from somafm_tui import bandcamp
from beefy_player import bandcamp
TRACK_PAYLOAD = {
"auto": {
+1 -1
View File
@@ -6,7 +6,7 @@ import types
import pytest
from rich.cells import cell_len
from somafm_tui.widgets.header import HeaderBar
from beefy_player.widgets.header import HeaderBar
TIME = re.compile(r"\d\d:\d\d:\d\d$")
DATE = re.compile(r"[A-Z][a-z]{2} \d\d [A-Z][a-z]{2}$")
+1 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from somafm_tui import macos_tap
from beefy_player import macos_tap
def test_osstatus_decodes_four_character_codes() -> None:
+3 -3
View File
@@ -3,8 +3,8 @@ from __future__ import annotations
import queue
import sys
from somafm_tui import mediasession
from somafm_tui.mediasession import (
from beefy_player import mediasession
from beefy_player.mediasession import (
MediaSession,
NowPlayingSession,
create_media_session,
@@ -12,7 +12,7 @@ from somafm_tui.mediasession import (
def test_linux_uses_mpris() -> None:
from somafm_tui.mpris import MprisService
from beefy_player.mpris import MprisService
session = create_media_session(queue.Queue(), enabled=False, platform="linux")
assert isinstance(session, MprisService)
+4 -4
View File
@@ -4,7 +4,7 @@ import queue
import pytest
from somafm_tui import mpris
from beefy_player import mpris
def test_build_metadata() -> None:
@@ -17,16 +17,16 @@ def test_build_metadata() -> None:
stream_url="https://ice2.somafm.com/groovesalad-256-mp3",
track_key="groovesalad|Jens Buchert|Rauschebart|Gravity",
)
assert metadata["mpris:trackid"].value.startswith("/org/somafm/track/")
assert metadata["mpris:trackid"].value.startswith("/org/beefyplayer/track/")
assert metadata["xesam:title"].value == "Rauschebart"
assert metadata["xesam:artist"].value == ["Jens Buchert"]
assert metadata["xesam:album"].value == "Gravity"
assert metadata["mpris:artUrl"].value == "https://example.com/art.jpg"
assert metadata["xesam:url"].value.startswith("https://ice2.")
assert metadata["xesam:comment"].value == ["SomaFM · Groove Salad"]
assert metadata["xesam:comment"].value == ["Beefy Player · Groove Salad"]
minimal = mpris.build_metadata(station="X", title="", artist="")
assert minimal["xesam:artist"].value == ["SomaFM"]
assert minimal["xesam:artist"].value == ["Beefy Player"]
assert "xesam:album" not in minimal
assert "mpris:artUrl" not in minimal
+11 -11
View File
@@ -5,7 +5,7 @@ import queue
import time
from pathlib import Path
from somafm_tui.player import Player
from beefy_player.player import Player
FAKE_MPV = '''#!/usr/bin/env python3
import json, os, socket, sys, time
@@ -171,27 +171,27 @@ def test_backend_detection_without_tools(tmp_path, monkeypatch) -> None:
def test_media_key_messages_become_player_events() -> None:
player = Player()
player._handle_mpv_message(
{"event": "client-message", "args": ["somafm", "next"]}
{"event": "client-message", "args": ["beefy", "next"]}
)
event = player.events.get_nowait()
assert event.kind == "media"
assert event.detail == "next"
player._handle_mpv_message(
{"event": "client-message", "args": ["somafm", "toggle"]}
{"event": "client-message", "args": ["beefy", "toggle"]}
)
assert player.events.get_nowait().detail == "toggle"
# Foreign namespaces, unknown commands and malformed payloads are ignored.
player._handle_mpv_message({"event": "client-message", "args": ["other", "next"]})
player._handle_mpv_message({"event": "client-message", "args": ["somafm", "seek"]})
player._handle_mpv_message({"event": "client-message", "args": ["beefy", "seek"]})
player._handle_mpv_message({"event": "client-message", "args": None})
player._handle_mpv_message({"event": "client-message"})
assert player.events.empty()
def test_platform_options_never_leak_to_linux(tmp_path, monkeypatch) -> None:
import somafm_tui.player as player_module
import beefy_player.player as player_module
monkeypatch.setattr(player_module, "RUNTIME_DIR", tmp_path)
assert Player()._platform_options("linux") == []
@@ -203,12 +203,12 @@ def test_platform_options_never_leak_to_linux(tmp_path, monkeypatch) -> None:
assert conf.endswith("mpv-input.conf")
bindings = (tmp_path / "mpv-input.conf").read_text()
for line in (
"PLAY script-message somafm toggle",
"PLAYONLY script-message somafm play",
"PAUSEONLY script-message somafm pause",
"STOP script-message somafm stop",
"NEXT script-message somafm next",
"PREV script-message somafm prev",
"PLAY script-message beefy toggle",
"PLAYONLY script-message beefy play",
"PAUSEONLY script-message beefy pause",
"STOP script-message beefy stop",
"NEXT script-message beefy next",
"PREV script-message beefy prev",
):
assert line in bindings
+3 -3
View File
@@ -5,8 +5,8 @@ from pathlib import Path
import numpy as np
from somafm_tui import spectrum as spectrum_module
from somafm_tui.spectrum import Frame, SpectrumEngine
from beefy_player import spectrum as spectrum_module
from beefy_player.spectrum import Frame, SpectrumEngine
PCM_SCRIPT = """#!/usr/bin/env python3
import math, struct, sys, time
@@ -42,7 +42,7 @@ def tap_pcm(
class FakeTap:
"""Stand-in for ``somafm_tui.macos_tap.SystemAudioTap``."""
"""Stand-in for ``beefy_player.macos_tap.SystemAudioTap``."""
def __init__(
self,
+3 -3
View File
@@ -4,9 +4,9 @@ from __future__ import annotations
import math
from somafm_tui import config
from somafm_tui.spectrum import Frame, SpectrumEngine
from somafm_tui.widgets.spectrum import (
from beefy_player import config
from beefy_player.spectrum import Frame, SpectrumEngine
from beefy_player.widgets.spectrum import (
VISUALIZER_LABELS,
WATERFALL_HISTORY,
SpectrumPanel,
+9 -9
View File
@@ -5,15 +5,15 @@ from pathlib import Path
import pytest
from somafm_tui.app import SomaFMApp
from somafm_tui.widgets.header import HeaderBar
from somafm_tui.widgets.search import SearchInput
from somafm_tui.widgets.spectrum import SpectrumPanel
from somafm_tui.widgets.stage import StagePanel
from somafm_tui.widgets.station_list import StationList
from somafm_tui.widgets.status import StatusBar
from beefy_player.app import BeefyPlayerApp
from beefy_player.widgets.header import HeaderBar
from beefy_player.widgets.search import SearchInput
from beefy_player.widgets.spectrum import SpectrumPanel
from beefy_player.widgets.stage import StagePanel
from beefy_player.widgets.station_list import StationList
from beefy_player.widgets.status import StatusBar
PACKAGE = Path(__file__).resolve().parents[1] / "somafm_tui"
PACKAGE = Path(__file__).resolve().parents[1] / "beefy_player"
def test_no_rgb_colors_in_source() -> None:
@@ -29,7 +29,7 @@ def test_no_rgb_colors_in_source() -> None:
@pytest.mark.asyncio
async def test_backgrounds_use_terminal_default_and_borders_are_ansi() -> None:
app = SomaFMApp(autoplay=False, offline=True, audio=False)
app = BeefyPlayerApp(autoplay=False, offline=True, audio=False)
async with app.run_test(size=(100, 30)) as pilot:
await pilot.pause()
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from somafm_tui.widgets import common
from beefy_player.widgets import common
BERLIN = Path("/usr/share/zoneinfo/Europe/Berlin")
UTC_FILE = Path("/usr/share/zoneinfo/UTC")