MPRIS: Switching from python-sdbus to jeepney, no functionality.

This commit is contained in:
The Wobbler 2025-04-16 16:57:01 +02:00
parent a236370d47
commit f23530628c
14 changed files with 301 additions and 125 deletions

View file

@ -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()

View file

@ -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()

4
wobuzz/mpris/__init__.py Normal file
View file

@ -0,0 +1,4 @@
#!/usr/bin/python3
from .server import MPRISServer

View file

@ -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)

View file

@ -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)

View file

@ -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!")

68
wobuzz/mpris/server.py Normal file
View file

@ -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)

96
wobuzz/mpris/utils.py Normal file
View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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)