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:
1 parent
4bed90b489
commit
e30ca09627
44 files changed
+329
-192
No files matched your search
@@ -0,0 +1,160 @@
|
||||
"""Desktop media integration, with a platform-neutral face.
|
||||
|
||||
The rest of the app only ever talks to this small surface::
|
||||
|
||||
session = create_media_session(actions, enabled=True)
|
||||
session.start()
|
||||
session.set_track(station=…, title=…, artist=…, album=…, art_url=…)
|
||||
session.set_status("Playing")
|
||||
session.set_volume(0.7)
|
||||
|
||||
which keeps the platform differences in one place:
|
||||
|
||||
* **Linux / BSD / any D-Bus desktop** — :class:`beefy_player.mpris.MprisService`
|
||||
exports ``org.mpris.MediaPlayer2.beefyplayer.instance<pid>`` on the session
|
||||
bus, so GNOME/KDE widgets, lock screens and ``playerctl`` show the station
|
||||
and can drive playback.
|
||||
* **macOS** — mpv *is* the app the system knows about: its Cocoa backend
|
||||
registers with ``MPRemoteCommandCenter``/``MPNowPlayingInfoCenter``, which
|
||||
is what Control Center, the menu bar and the media keys talk to. So there
|
||||
is nothing to publish here, and the media keys are routed back into the TUI
|
||||
by mpv (see :mod:`beefy_player.player`). :class:`NowPlayingSession` only
|
||||
keeps the state the status bar renders and stays out of the way.
|
||||
|
||||
Anything else (or a disabled/absent backend) degrades to an inert session
|
||||
whose status label is ``off`` — the TUI carries on regardless.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from queue import Queue
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"MediaSession",
|
||||
"NowPlayingSession",
|
||||
"create_media_session",
|
||||
"media_actions_queue",
|
||||
]
|
||||
|
||||
|
||||
def media_actions_queue() -> Queue[str]:
|
||||
"""Queue of commands coming back from the desktop media integration."""
|
||||
return Queue()
|
||||
|
||||
|
||||
class MediaSession:
|
||||
"""Inert media session: keeps state, publishes nothing.
|
||||
|
||||
Used as the base for backends that have no IPC service of their own and as
|
||||
the fallback when desktop integration is unavailable.
|
||||
"""
|
||||
|
||||
#: Short name for the status bar ("mpris", "nowplaying", …).
|
||||
label = "media"
|
||||
|
||||
def __init__(self, actions: Queue[str], *, enabled: bool = True) -> None:
|
||||
self.actions = actions
|
||||
self.enabled = bool(enabled)
|
||||
self.available = False
|
||||
self.error = ""
|
||||
self._status = "Stopped"
|
||||
self._volume = 0.7
|
||||
self._metadata: dict[str, Any] = {}
|
||||
|
||||
# ----- lifecycle ---------------------------------------------------- #
|
||||
|
||||
def start(self) -> None:
|
||||
return None
|
||||
|
||||
def stop(self) -> None:
|
||||
return None
|
||||
|
||||
# ----- state -------------------------------------------------------- #
|
||||
|
||||
@property
|
||||
def status_label(self) -> str:
|
||||
if not self.enabled:
|
||||
return "off"
|
||||
if self.available:
|
||||
return "on"
|
||||
return "error" if self.error else "off"
|
||||
|
||||
def set_status(self, status: str) -> None:
|
||||
if status in ("Playing", "Paused", "Stopped"):
|
||||
self._status = status
|
||||
|
||||
def set_volume(self, ratio: float) -> None:
|
||||
self._volume = max(0.0, min(1.0, float(ratio)))
|
||||
|
||||
def set_track(
|
||||
self,
|
||||
*,
|
||||
station: str,
|
||||
title: str,
|
||||
artist: str,
|
||||
album: str = "",
|
||||
art_url: str = "",
|
||||
stream_url: str = "",
|
||||
track_key: str = "",
|
||||
) -> None:
|
||||
self._metadata = {
|
||||
"station": station,
|
||||
"title": title,
|
||||
"artist": artist,
|
||||
"album": album,
|
||||
"art_url": art_url,
|
||||
"stream_url": stream_url,
|
||||
"track_key": track_key,
|
||||
}
|
||||
|
||||
|
||||
class NowPlayingSession(MediaSession):
|
||||
"""macOS: mpv owns the system Now Playing session, we just mirror state.
|
||||
|
||||
mpv's Cocoa backend publishes the live ICY title (and play/pause state) to
|
||||
Control Center and receives the media keys; :mod:`beefy_player.player` routes
|
||||
those key presses back into this app. ``available`` is false when the audio
|
||||
backend is not mpv, because then nobody publishes anything.
|
||||
"""
|
||||
|
||||
label = "nowplaying"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
actions: Queue[str],
|
||||
*,
|
||||
enabled: bool = True,
|
||||
backend: str | None = "mpv",
|
||||
) -> None:
|
||||
super().__init__(actions, enabled=enabled)
|
||||
self.backend = backend
|
||||
self.available = bool(enabled) and backend == "mpv"
|
||||
|
||||
@property
|
||||
def status_label(self) -> str:
|
||||
if not self.enabled:
|
||||
return "off"
|
||||
return "on" if self.available else "off"
|
||||
|
||||
|
||||
def create_media_session(
|
||||
actions: Queue[str],
|
||||
*,
|
||||
enabled: bool = True,
|
||||
backend: str | None = None,
|
||||
platform: str | None = None,
|
||||
) -> MediaSession:
|
||||
"""Pick the right desktop media backend for this platform.
|
||||
|
||||
``platform``/``backend`` exist so the choice is testable without touching
|
||||
the real machine.
|
||||
"""
|
||||
system = sys.platform if platform is None else platform
|
||||
if system == "darwin":
|
||||
return NowPlayingSession(actions, enabled=enabled, backend=backend)
|
||||
|
||||
from .mpris import MprisService
|
||||
|
||||
return MprisService(actions, enabled=enabled)
|
||||
Reference in new issue
Block a user