Files
Modus/services/mpris.py
T
2025-06-13 21:00:45 +05:45

211 lines
6.4 KiB
Python

from gi.repository import Playerctl
from typing import Optional
import gi
import contextlib
from loguru import logger
from fabric.core.service import Service, Signal, Property
from fabric.utils.helpers import get_enum_member_name, snake_case_to_kebab_case
gi.require_version("Playerctl", "2.0")
class MediaPlayer(Service):
"""A service to manage a media player."""
@Signal
def exit(self) -> None: ...
@Signal
def metadata_changed(self) -> None: ...
@Signal
def playback_status_changed(self) -> None: ...
@Property(str, flags="read-write")
def player_name(self) -> str:
return self._player.get_property("player_name")
@Property(int, "read-write", default_value=0)
def track_position(self) -> int:
return self._player.get_property("position")
@track_position.setter
def track_position(self, new_pos: int):
self._player.set_position(new_pos)
@Property(str, flags="read-write")
def album_image_url(self) -> str:
return self._player.get_property("metadata").unpack().get("mpris:artUrl", None)
@Property(int, flags="read-write")
def track_duration(self) -> int:
default_duration = minutes_to_microseconds(5)
metadata = self._player.get_property("metadata").unpack()
duration = metadata.get("mpris:length", default_duration) # Default: 5 minutes
return default_duration if duration == 0 else duration
@Property(str, flags="readable")
def status(self) -> str:
return snake_case_to_kebab_case(
get_enum_member_name(
self._player.get_property("playback-status"), # type: ignore
default="unknown",
)
)
@Property(str, flags="readable")
def track(self) -> str:
return (
f"{self.track_artist} {self.track_title}"
if self.player_name == "spotify"
else self.track_title or "Music"
)
@Property(str, flags="readable")
def track_title(self) -> str:
return self._player.get_title()
@Property(str, flags="readable")
def track_artist(self) -> str:
return self._player.get_artist()
@Property(bool, "readable", default_value=False)
def can_go_next(self) -> bool:
return self._player.get_property("can_go_next")
@Property(bool, "readable", default_value=False)
def can_go_previous(self) -> bool:
return self._player.get_property("can_go_previous")
@Property(bool, "readable", default_value=False)
def can_seek(self) -> bool:
return self._player.get_property("can_seek")
@Property(bool, "readable", default_value=False)
def can_pause(self) -> bool:
return self._player.get_property("can_pause")
def __init__(self, player):
super().__init__()
self._player = player
self._signal_connectors = {}
signals = {
"seeked": lambda *args: self.notify("track_position"),
"playback-status": lambda *args: self._on_playback_status_changed(),
"metadata": lambda *args: self._on_metadata_changed(),
"exit": lambda *args: self._on_exit(),
}
for signal_name, handler in signals.items():
self._signal_connectors[signal_name] = self._player.connect(
signal_name, handler
)
def _on_playback_status_changed(self):
self.notify("status")
self.playback_status_changed.emit()
def _on_metadata_changed(self):
for prop in [
"track_duration",
"track_position",
"track_title",
"track_artist",
"track",
"album_image_url",
]:
self.notify(prop)
self.metadata_changed.emit()
def _on_exit(self):
for signal_id in self._signal_connectors.values():
with contextlib.suppress(Exception):
self._player.disconnect(signal_id)
del self._player
self.exit.emit()
def play_pause(self):
if self.can_pause:
self._player.play_pause()
def next(self):
if self.can_go_next:
self._player.next()
def previous(self):
if self.can_go_previous:
self._player.previous()
def get_position(self):
return self._player.get_position() if self._player else 0
@staticmethod
def minutes_to_microseconds(minutes):
return minutes * 60 * 1000000
class MediaManager(Service):
"""MediaManager Service using Playerctl to control media players"""
@Signal
def player_appeared(self) -> None: ...
@Signal
def player_vanished(self) -> None: ...
@Property(list, "readable")
def player_names(self):
return self._manager.get_property("player_names")
@Property(list[MediaPlayer], "readable")
def players(self) -> list[MediaPlayer]:
return self._players.values()
@Property(MediaPlayer, "readable")
def current_player(self) -> Optional[MediaPlayer]:
current_player = None
for player in self.players:
if player.status == "playing":
return player
current_player = player
return current_player
def __init__(self):
super().__init__()
self._manager = Playerctl.PlayerManager()
self._players: dict[str, MediaPlayer] = {}
self._manager.connect(
"name-appeared", lambda _, player: self._on_player_appeared(player=player)
)
self._manager.connect(
"player-vanished", lambda _, player: self._on_player_vanished(player=player)
)
for player in self._manager.get_property("player-names"):
self._on_player_appeared(player=player)
def _on_player_appeared(self, player):
"""Callback when a new player appears"""
player = Playerctl.Player.new_from_name(player)
player_name = player.get_property("player_name")
logger.info(f"[Media] New player appeared: {player_name}")
self._manager.manage_player(player)
self._players[player_name] = MediaPlayer(player=player)
self.notify("current-player")
self.player_appeared.emit()
def _on_player_vanished(self, player):
"""Callback when a player disappears"""
player_name = player.get_property("player_name") or "Unknown"
logger.info(f"[Media] Player vanished: {player_name}")
self._players.pop(player_name)
self.notify("current-player")
self.player_vanished.emit()