Files
Modus/services/mpris.py
T

177 lines
6.1 KiB
Python

from gi.repository import Playerctl, GLib
from fabric.utils import bulk_connect
from fabric.core.service import Service, Signal
from fabric import Fabricator
from config.data import ALLOWED_PLAYERS
import gi
gi.require_version("Playerctl", "2.0")
class PlayerService(Service):
@Signal
def shuffle_toggle(self, player: Playerctl.Player, status: bool) -> None: ...
@Signal
def meta_change(self, metadata: GLib.Variant, player: Playerctl.Player) -> None: ...
@Signal
def pause(self) -> None: ...
@Signal
def play(self) -> None: ...
@Signal
def track_position(self, pos: float, dur: float) -> None: ...
def __init__(self, player: Playerctl.Player, **kwargs):
self._player: Playerctl.Player = player
super().__init__(**kwargs)
if player.props.player_name in ALLOWED_PLAYERS:
self._player.connect("playback-status::playing", self.on_play)
self._player.connect("playback-status::paused", self.on_pause)
self._player.connect("shuffle", self.on_shuffle)
self._player.connect("metadata", self.on_metadata)
self._player.connect("seeked", self.on_seeked)
print(type(player))
self.status = self._player.props.playback_status
self.pos_fabricator = Fabricator(
interval=1000, # 1s
poll_from=lambda f, *_: self._player.get_position(),
on_changed=lambda f, *_: self.fabricating(),
)
self.poll_progress()
def cleanup(self):
"""Clean up resources when service is no longer needed"""
try:
if hasattr(self, 'pos_fabricator') and self.pos_fabricator:
self.pos_fabricator.stop()
except Exception as e:
print(f"[DEBUG] Error during PlayerService cleanup: {e}")
def on_seeked(self, player, position):
if self.status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING":
self.pos_fabricator.start()
def set_position(self, pos: float):
print("seeking in the service")
self.pos_fabricator.stop()
micro_pos = int(pos * 1_000_000)
try:
self._player.set_position(micro_pos)
print(f"Set position to {micro_pos}")
except GLib.Error as e:
print(f"Failed to seek: {e}")
def poll_progress(self):
if self.status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING":
self.pos_fabricator.start()
else:
self.pos_fabricator.stop()
def fabricating(self):
try:
# Validate player object
if not self._player or not hasattr(self._player, 'get_position'):
return
# Get position safely
try:
pos = self._player.get_position() / 1_000_000 # seconds
except Exception as e:
print(f"[DEBUG] Error getting player position: {e}")
return
# Get duration safely from metadata
try:
if (hasattr(self._player, 'props') and
hasattr(self._player.props, 'metadata') and
self._player.props.metadata and
"mpris:length" in self._player.props.metadata):
dur = self._player.props.metadata["mpris:length"] / 1_000_000 # seconds
else:
# No duration available, skip this update
return
except Exception as e:
print(f"[DEBUG] Error getting track duration: {e}")
return
# Validate values before emitting signal
if pos is not None and dur is not None and dur > 0:
self.track_position(pos, dur)
except Exception as e:
print(f"[DEBUG] Error in fabricating: {e}")
def on_play(self, player, status):
print("player is playing: {}".format(player.props.player_name))
self.status = player.props.playback_status
self.poll_progress()
self.play()
def on_pause(self, player, status):
print("player is paused: {}".format(player.props.player_name))
self.status = player.props.playback_status
self.poll_progress()
self.pause()
def on_shuffle(self, player, status):
print("suffle status changed for: {}".format(player.props.player_name))
print(type(status), "here is the status type")
self.shuffle_toggle(player, status)
def on_metadata(self, player, metadata):
keys = metadata.keys()
if "xesam:artist" in keys and "xesam:title" in keys:
print(
"{} - {}".format(metadata["xesam:artist"][0], metadata["xesam:title"])
)
self.meta_change(metadata, player)
class PlayerManager(Service):
@Signal
def new_player(self, player: Playerctl.Player) -> Playerctl.Player: ...
@Signal
def player_vanish(self, player: Playerctl.Player) -> None: ...
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._manager = Playerctl.PlayerManager()
self._manager.connect("name-appeared", self._on_name_appeared, self._manager)
self._manager.connect(
"player-vanished", self._on_player_vanished, self._manager
)
print("playerctl init done")
self._players = {}
def init_all_players(self):
# invoked in the UI
for player_obj in self._manager.props.player_names:
name_str = player_obj.name
print(f"{name_str} appeared")
if name_str in ALLOWED_PLAYERS:
player = Playerctl.Player.new_from_name(player_obj)
self._manager.manage_player(player)
self.new_player(player)
def _on_name_appeared(self, sender, name, manager):
name_str = name.name
print(f"{name_str} appeared")
if name_str in ALLOWED_PLAYERS:
player = Playerctl.Player.new_from_name(name)
self._manager.manage_player(player)
self.new_player(player)
def _on_player_vanished(self, sender, player, manager):
print("player has exited: {}".format(player.props.player_name))
print(type(player))
self.player_vanish(player)