From f23530628cd2caeebbef5db650c0555073e29277 Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Wed, 16 Apr 2025 16:57:01 +0200 Subject: [PATCH] MPRIS: Switching from python-sdbus to jeepney, no functionality. --- requirements.txt | 5 +- wobuzz/gui.py | 4 +- wobuzz/main.py | 4 ++ wobuzz/mpris/__init__.py | 4 ++ wobuzz/mpris/dbus_properties.py | 67 +++++++++++++++++++ wobuzz/mpris/mpris_player.py | 34 ++++++++++ wobuzz/mpris/mpris_root.py | 16 +++++ wobuzz/mpris/server.py | 68 +++++++++++++++++++ wobuzz/mpris/utils.py | 96 +++++++++++++++++++++++++++ wobuzz/player/mpris.py | 102 ----------------------------- wobuzz/player/player.py | 6 -- wobuzz/ui/main_window.py | 6 +- wobuzz/ui/track_control.py | 10 --- wobuzz/ui/track_progress_slider.py | 4 +- 14 files changed, 301 insertions(+), 125 deletions(-) create mode 100644 wobuzz/mpris/__init__.py create mode 100644 wobuzz/mpris/dbus_properties.py create mode 100644 wobuzz/mpris/mpris_player.py create mode 100644 wobuzz/mpris/mpris_root.py create mode 100644 wobuzz/mpris/server.py create mode 100644 wobuzz/mpris/utils.py delete mode 100644 wobuzz/player/mpris.py diff --git a/requirements.txt b/requirements.txt index 3fbe474..7981831 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,6 @@ pygame~=2.6.1 tinytag~=2.1.0 pydub~=0.25.1 wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools -sdbus~=0.14.0 -setuptools~=68.1.2 \ No newline at end of file +setuptools~=78.1.0 +Wobuzz~=0.1a3 +jeepney~=0.8.0 \ No newline at end of file diff --git a/wobuzz/gui.py b/wobuzz/gui.py index c23dc49..a56082a 100644 --- a/wobuzz/gui.py +++ b/wobuzz/gui.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 -from PyQt6.QtCore import Qt, QTimer -from PyQt6.QtWidgets import QDockWidget, QFileDialog +from PyQt6.QtCore import QTimer from .ui.main_window import MainWindow from .ui.popups import Popups @@ -76,6 +75,7 @@ class GUI: def on_playstate_update(self): self.track_control.on_playstate_update() self.track_info.update_info() + self.app.mpris_server.on_playstate_update() def update_gui(self): self.track_control.track_progress_slider.update_progress() diff --git a/wobuzz/main.py b/wobuzz/main.py index 6a089bc..14331a8 100644 --- a/wobuzz/main.py +++ b/wobuzz/main.py @@ -8,6 +8,7 @@ from .utils import Utils from .player import Player from .library.library import Library from .gui import GUI +from .mpris import MPRISServer class Wobuzz: @@ -22,6 +23,9 @@ class Wobuzz: self.library = Library(self) self.player = Player(self) self.gui = GUI(self) + self.mpris_server = MPRISServer(self) + self.gui.window.mpris_signal.connect(self.mpris_server.handle_event) + self.mpris_server.start() self.late_init() diff --git a/wobuzz/mpris/__init__.py b/wobuzz/mpris/__init__.py new file mode 100644 index 0000000..a5a4d67 --- /dev/null +++ b/wobuzz/mpris/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/python3 + +from .server import MPRISServer + diff --git a/wobuzz/mpris/dbus_properties.py b/wobuzz/mpris/dbus_properties.py new file mode 100644 index 0000000..51665f0 --- /dev/null +++ b/wobuzz/mpris/dbus_properties.py @@ -0,0 +1,67 @@ +#!/usr/bin/python3 + +from jeepney import Header, MessageType, Endianness, MessageFlag, new_method_return +from jeepney.bus_messages import message_bus +from jeepney.io.blocking import open_dbus_connection +from jeepney.wrappers import new_header + +from .utils import * + + +class DBusProperties(DBusInterface): + def get_all(self): + body = ({},) + return body + + def properties_changed(self, interface: str): + body = None + + if interface == MPRIS_ROOT_INTERFACE: + body = (MPRIS_ROOT_INTERFACE,) + self.server.root_interface.get_all() + + elif interface == MPRIS_PLAYER_INTERFACE: + body = (MPRIS_PLAYER_INTERFACE,) + self.server.player_interface.get_all() + + signature = "" if body is None else "sa{sv}" + + msg = new_signal( + self.server.bus_address.with_interface("org.freedesktop.DBus"), + "PropertiesChanged", + signature, + body + ) + self.server.bus.send(msg) + + def Get(self, msg: Message): + interface_name = msg.body[0] + + return_msg = None + + if interface_name == PROPERTIES_INTERFACE: + return self.get(msg) + + elif interface_name == MPRIS_ROOT_INTERFACE: + return self.server.root_interface.get(msg) + + elif interface_name == MPRIS_PLAYER_INTERFACE: + return self.server.player_interface.get(msg) + + else: + return new_error(msg, *DBusErrors.invalidArgs(interface=interface_name)) + + def GetAll(self, msg: Message): + interface = msg.body[0] + + if interface == PROPERTIES_INTERFACE: + body = self.get_all() + + elif interface == MPRIS_ROOT_INTERFACE: + body = self.server.root_interface.get_all() + + elif interface == MPRIS_PLAYER_INTERFACE: + body = self.server.player_interface.get_all() + + else: + return new_error(msg, *DBusErrors.invalidArgs(interface=interface)) + + return new_method_return(msg, "a{sv}", body) diff --git a/wobuzz/mpris/mpris_player.py b/wobuzz/mpris/mpris_player.py new file mode 100644 index 0000000..67255b2 --- /dev/null +++ b/wobuzz/mpris/mpris_player.py @@ -0,0 +1,34 @@ +#!/usr/bin/python3 + +from jeepney import new_method_return + +from .utils import * + + +class MPRISPlayer(DBusInterface): + def __init__(self, server, interface): + super().__init__(server, interface) + + self.metadata = { + "mpris:trackid": ("o", "/org/mpris/MediaPlayer2/murx"), # random junk, no functionality + "xesam:title": ("s", "Huggenburgl") + } + + def get_all(self): + body = ({ + "CanPlay": ("b", True), + "Metadata": ("a{sv}", self.metadata) + },) + + return body + + def Play(self, msg: Message): + print("Play!") + + def PlayPause(self, msg: Message): + print("Play/Pause!") + + def Metadata(self, msg: Message): + body = (self.metadata,) + + return new_method_return(msg, "a{sv}", body) diff --git a/wobuzz/mpris/mpris_root.py b/wobuzz/mpris/mpris_root.py new file mode 100644 index 0000000..d344e0b --- /dev/null +++ b/wobuzz/mpris/mpris_root.py @@ -0,0 +1,16 @@ +#!/usr/bin/python3 + +from .utils import * + + +class MPRISRoot(DBusInterface): + def get_all(self): + body = ({ + "CanQuit": ("b", True), + "CanRaise": ("b", True) + },) + + return body + + def Raise(self, msg): + print("Raise!") diff --git a/wobuzz/mpris/server.py b/wobuzz/mpris/server.py new file mode 100644 index 0000000..7ab753f --- /dev/null +++ b/wobuzz/mpris/server.py @@ -0,0 +1,68 @@ +#!/usr/bin/python3 + +from threading import Thread +from jeepney.bus_messages import message_bus +from jeepney.io.blocking import DBusConnection, open_dbus_connection + +from .utils import * +from .dbus_properties import DBusProperties +from .mpris_root import MPRISRoot +from .mpris_player import MPRISPlayer + + +class MPRISServer: + def __init__(self, app): + self.app = app + + self.properties_interface = DBusProperties(self, PROPERTIES_INTERFACE) + self.root_interface = MPRISRoot(self, MPRIS_ROOT_INTERFACE) + self.player_interface = MPRISPlayer(self, MPRIS_PLAYER_INTERFACE) + + self.bus_address = DBusAddress(OBJECT_PATH, SERVICE_NAME, PROPERTIES_INTERFACE) + self.bus: DBusConnection = None + + def start(self): + self.bus = open_dbus_connection() + + reply = self.bus.send_and_get_reply(message_bus.RequestName(SERVICE_NAME)) + if reply.body[0] == 1: + print("MPRIS Server connected to DBus: ", SERVICE_NAME) + + Thread(target=self.run_event_loop, daemon=True).start() + + def run_event_loop(self): + while True: + msg = self.bus.receive() + self.app.gui.window.mpris_signal.emit(msg) # queue message in the PyQt event loop + + def handle_event(self, event): # called by app.gui.window.mpris_signal + self.handle_message(event) + + def handle_message(self, msg: Message): + object_path = msg.header.fields[HeaderFields.path] + + if not object_path == OBJECT_PATH: # only accept messages for "/org/mpris/MediaPlayer2" + self.bus.send_message(new_error(msg, *DBusErrors.unknownObject(object_path))) + return + + interface = msg.header.fields[HeaderFields.interface] + + # let the corresponding interface handle the message + if interface == PROPERTIES_INTERFACE: + self.properties_interface.handle_message(msg) + + elif interface == MPRIS_ROOT_INTERFACE: + self.root_interface.handle_message(msg) + + elif interface == MPRIS_PLAYER_INTERFACE: + self.player_interface.handle_message(msg) + + def on_playstate_update(self): + current_playlist = self.app.player.current_playlist + + if current_playlist is not None and current_playlist.current_track is not None: + current_track = current_playlist.current_track + + self.player_interface.metadata["xesam:title"] = ("s", current_track.metadata.title) + + self.properties_interface.properties_changed(MPRIS_PLAYER_INTERFACE) diff --git a/wobuzz/mpris/utils.py b/wobuzz/mpris/utils.py new file mode 100644 index 0000000..f5a4a55 --- /dev/null +++ b/wobuzz/mpris/utils.py @@ -0,0 +1,96 @@ +#!/usr/bin/python3 + +from jeepney import DBusAddress, Message, MessageType, HeaderFields, new_error, new_signal + + +SERVICE_NAME = "org.mpris.MediaPlayer2.wobuzz" +OBJECT_PATH = "/org/mpris/MediaPlayer2" +PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties" +MPRIS_ROOT_INTERFACE = "org.mpris.MediaPlayer2" +MPRIS_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player" + + +class DBusErrors: + @classmethod + def unknownMethod(cls, method: str) -> tuple[str, str, tuple[str]]: + return "org.freedesktop.DBus.Error.UnknownMethod", "s", (f"No such method '{method}'.",) + + @classmethod + def unknownProperty(cls, prop: str) -> tuple[str, str, tuple[str]]: + return "org.freedesktop.DBus.Error.InvalidArgs", "s", (f"No such property '{prop}'",) + + @classmethod + def invalidArgs(cls, prop: str=None, interface: str=None): + if prop is None and interface is None: + return "org.freedesktop.DBus.Error.InvalidArgs" + + if interface is None: + return "org.freedesktop.DBus.Error.InvalidArgs", "s", (f"No such property '{prop}'.",) + + if prop is None: + return "org.freedesktop.DBus.Error.InvalidArgs", "s", (f"No such interface '{interface}'.",) + + return ( + "org.freedesktop.DBus.Error.InvalidArgs", + "s", + (f"No such property '{prop}' on interface '{interface}'.",) + ) + + @classmethod + def unknownObject(cls, path: str): + return "org.freedesktop.DBus.Error.UnknownObject", "s", (f"No such object on path '{path}'.",) + + +class DBusInterface: + def __init__(self, server, interface: str): + self.server = server + self.interface = interface + + def __setattr__(self, key: str, value) -> None: + super().__setattr__(key, value) + + if not key[0].isupper() and not callable(value) and hasattr(self, key.title()): + getter = getattr(self, key.title()) + + if callable(getter): + if hasattr(self.server, "bus"): + self.server.properties_interface.properties_changed(self.interface) + + def handle_message(self, msg: Message): + return_msg = None + + match msg.header.message_type: + case MessageType.method_call: + method_name: str = msg.header.fields[HeaderFields.member] + + if hasattr(self, method_name) and method_name[0].isupper(): + method = getattr(self, msg.header.fields[HeaderFields.member]) + + if callable(method): + return_msg = method(msg) + + else: + return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) + + else: + return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) + + if return_msg is not None: + self.server.bus.send_message(return_msg) + + def get(self, msg: Message): + prop_name: str = msg.body[1] + + if prop_name[0].isupper() and hasattr(self, prop_name): + prop = getattr(self, prop_name) + + if callable(prop): + return prop() + + return new_error(msg, *DBusErrors.unknownProperty(prop_name)) + + else: + return new_error(msg, *DBusErrors.unknownProperty(prop_name)) + + def get_all(self) -> tuple[dict[str: tuple[str: any]]]: + raise NotImplementedError diff --git a/wobuzz/player/mpris.py b/wobuzz/player/mpris.py deleted file mode 100644 index fdf7bf4..0000000 --- a/wobuzz/player/mpris.py +++ /dev/null @@ -1,102 +0,0 @@ -from sdbus import ( - dbus_method_async, - dbus_property_async, - DbusInterfaceCommonAsync, - request_default_bus_name_async -) -import asyncio - -from wobuzz.player.track import TrackMetadata - -SERVICE_NAME = "org.mpris.MediaPlayer2.wobuzz" -OBJECT_PATH = "/org/mpris/MediaPlayer2" -MPRIS_ROOT_INTERFACE = "org.mpris.MediaPlayer2" -MPRIS_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player" - - -class MPRISRoot(DbusInterfaceCommonAsync, interface_name=MPRIS_ROOT_INTERFACE): - @dbus_method_async() - async def Raise(self): - print("Raise, maybe?") - - @dbus_property_async("s") - def Identity(self): - return "Wobuzz" - - -class MPRISPlayer(DbusInterfaceCommonAsync, interface_name=MPRIS_PLAYER_INTERFACE): - def __init__(self): - super().__init__() - - self._metadata = {} - - @dbus_property_async("b") - def CanSeek(self): - return False - - @dbus_method_async() - async def PlayPause(self): - self.app.gui.track_control.toggle_playing_signal.emit() - - @dbus_method_async() - async def Next(self): - self.app.gui.track_control.next_signal.emit() - - @dbus_method_async() - async def Previous(self): - self.app.gui.track_control.previous_signal.emit() - - @dbus_method_async() - async def Stop(self): - self.app.gui.track_control.stop_signal.emit() - - @dbus_property_async("a{sv}") - def Metadata(self): - return self._metadata - - @Metadata.setter # noqa - def Metadata_setter(self, metadata: dict) -> None: - self._metadata = metadata - - async def set_metadata(self, metadata: TrackMetadata): - await self.Metadata.set_async(self.to_xesam(metadata)) - - def to_xesam(self, metadata: "TrackMetadata") -> dict: - # cache name by filename without extension - art_path = self.app.utils.tmp_path + "/cover_cache/" + metadata.path.split("/")[-1][:-4] - - xesam_metadata = { - "mpris:trackid": ("s", "kjuztuktg"), # nonsense, no functionality - "mpris:artUrl": ("s", "file://" + art_path), - "xesam:title": ("s", metadata.title), - "xesam:artist": ("as", [metadata.artist]) - } - - return xesam_metadata - - -class MPRISServer(MPRISRoot, MPRISPlayer): - def __init__(self, app): - super().__init__() - - self.app = app - - self.loop = None - - async def setup_bus(self) -> None: - await request_default_bus_name_async(SERVICE_NAME) - self.export_to_dbus(OBJECT_PATH) - - def start(self): - self.loop = asyncio.new_event_loop() - self.loop.run_until_complete(self.setup_bus()) - self.loop.run_forever() - - def exec_async(self, function): - """ - This stupid function somehow allows us to execute an async function from the main thread. - If someone ha a better solution, please improve this. I have no idea of how asyncio works. - """ - - loop = asyncio.new_event_loop() - loop.run_until_complete(function) diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 6946667..452ed72 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -8,7 +8,6 @@ import pygame.event from .playlist import Playlist from .track_progress_timer import TrackProgress -from .mpris import MPRISServer class Player: @@ -24,10 +23,6 @@ class Player: self.history = Playlist(self.app, "History") self.current_playlist = None - self.mpris_server = MPRISServer(self.app) - # start mpris server in a thread (daemon = exit with main thread) - threading.Thread(target=self.mpris_server.start, daemon=True).start() - self.playing = False self.paused = False @@ -43,7 +38,6 @@ class Player: self.app.gui.on_playstate_update() self.export_cover_art_tmp() - self.mpris_server.exec_async(self.mpris_server.set_metadata(self.current_playlist.current_track.metadata)) # cache next track so it immediately starts when the current track finishes self.cache_next_track() diff --git a/wobuzz/ui/main_window.py b/wobuzz/ui/main_window.py index 35c9e6b..9705b71 100644 --- a/wobuzz/ui/main_window.py +++ b/wobuzz/ui/main_window.py @@ -1,8 +1,10 @@ #!/usr/bin/python3 -from PyQt6.QtCore import Qt +from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QIcon, QShortcut from PyQt6.QtWidgets import QMainWindow, QMenu +from jeepney import Message + from .track_control import TrackControl from .settings import Settings from .process.process_dock import ProcessDock @@ -10,6 +12,8 @@ from .track_info import TrackInfo class MainWindow(QMainWindow): + mpris_signal = pyqtSignal(Message) + def __init__(self, app, gui, parent=None): super().__init__(parent) diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index 6b41796..d159edc 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -8,11 +8,6 @@ from .track_progress_slider import TrackProgressSlider class TrackControl(QToolBar): - toggle_playing_signal = pyqtSignal() # signals for MPRIS - next_signal = pyqtSignal() - previous_signal = pyqtSignal() - stop_signal = pyqtSignal() - def __init__(self, app, parent=None): super().__init__(parent) @@ -52,13 +47,9 @@ class TrackControl(QToolBar): def connect(self): self.previous_button.triggered.connect(self.previous_track) - self.previous_signal.connect(self.previous_track) self.toggle_play_button.triggered.connect(self.app.player.toggle_playing) - self.toggle_playing_signal.connect(self.app.player.toggle_playing) self.stop_button.triggered.connect(self.app.player.stop) - self.stop_signal.connect(self.app.player.stop) self.next_button.triggered.connect(self.next_track) - self.next_signal.connect(self.next_track) def previous_track(self): if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): @@ -94,4 +85,3 @@ class TrackControl(QToolBar): else: self.toggle_play_button.setIcon(self.play_icon) - diff --git a/wobuzz/ui/track_progress_slider.py b/wobuzz/ui/track_progress_slider.py index cf7a11b..028339c 100644 --- a/wobuzz/ui/track_progress_slider.py +++ b/wobuzz/ui/track_progress_slider.py @@ -67,10 +67,10 @@ class TrackProgressSlider(QSlider): self.track_control.progress_indicator.setText(self.app.utils.format_time(progress)) - self.track_control.track_progress_slider.setValue(progress) + self.setValue(progress) else: self.track_control.progress_indicator.setText(self.app.utils.format_time(0)) - self.track_control.track_progress_slider.setValue(0) + self.setValue(0)