Files
Modus/services/mpris.py
T
2025-06-26 16:10:17 +05:45

210 lines
6.4 KiB
Python

import contextlib
from typing import Optional
import gi
from fabric.core.service import Property, Service, Signal
from fabric.utils.helpers import get_enum_member_name, snake_case_to_kebab_case
from gi.repository import Playerctl
from loguru import logger
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()