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

@ -3,5 +3,6 @@ pygame~=2.6.1
tinytag~=2.1.0 tinytag~=2.1.0
pydub~=0.25.1 pydub~=0.25.1
wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools
sdbus~=0.14.0 setuptools~=78.1.0
setuptools~=68.1.2 Wobuzz~=0.1a3
jeepney~=0.8.0

View file

@ -1,7 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
from PyQt6.QtCore import Qt, QTimer from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QDockWidget, QFileDialog
from .ui.main_window import MainWindow from .ui.main_window import MainWindow
from .ui.popups import Popups from .ui.popups import Popups
@ -76,6 +75,7 @@ class GUI:
def on_playstate_update(self): def on_playstate_update(self):
self.track_control.on_playstate_update() self.track_control.on_playstate_update()
self.track_info.update_info() self.track_info.update_info()
self.app.mpris_server.on_playstate_update()
def update_gui(self): def update_gui(self):
self.track_control.track_progress_slider.update_progress() self.track_control.track_progress_slider.update_progress()

View file

@ -8,6 +8,7 @@ from .utils import Utils
from .player import Player from .player import Player
from .library.library import Library from .library.library import Library
from .gui import GUI from .gui import GUI
from .mpris import MPRISServer
class Wobuzz: class Wobuzz:
@ -22,6 +23,9 @@ class Wobuzz:
self.library = Library(self) self.library = Library(self)
self.player = Player(self) self.player = Player(self)
self.gui = GUI(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() 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 .playlist import Playlist
from .track_progress_timer import TrackProgress from .track_progress_timer import TrackProgress
from .mpris import MPRISServer
class Player: class Player:
@ -24,10 +23,6 @@ class Player:
self.history = Playlist(self.app, "History") self.history = Playlist(self.app, "History")
self.current_playlist = None 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.playing = False
self.paused = False self.paused = False
@ -43,7 +38,6 @@ class Player:
self.app.gui.on_playstate_update() self.app.gui.on_playstate_update()
self.export_cover_art_tmp() 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 # cache next track so it immediately starts when the current track finishes
self.cache_next_track() self.cache_next_track()

View file

@ -1,8 +1,10 @@
#!/usr/bin/python3 #!/usr/bin/python3
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QIcon, QShortcut from PyQt6.QtGui import QIcon, QShortcut
from PyQt6.QtWidgets import QMainWindow, QMenu from PyQt6.QtWidgets import QMainWindow, QMenu
from jeepney import Message
from .track_control import TrackControl from .track_control import TrackControl
from .settings import Settings from .settings import Settings
from .process.process_dock import ProcessDock from .process.process_dock import ProcessDock
@ -10,6 +12,8 @@ from .track_info import TrackInfo
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
mpris_signal = pyqtSignal(Message)
def __init__(self, app, gui, parent=None): def __init__(self, app, gui, parent=None):
super().__init__(parent) super().__init__(parent)

View file

@ -8,11 +8,6 @@ from .track_progress_slider import TrackProgressSlider
class TrackControl(QToolBar): 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): def __init__(self, app, parent=None):
super().__init__(parent) super().__init__(parent)
@ -52,13 +47,9 @@ class TrackControl(QToolBar):
def connect(self): def connect(self):
self.previous_button.triggered.connect(self.previous_track) 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_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_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_button.triggered.connect(self.next_track)
self.next_signal.connect(self.next_track)
def previous_track(self): def previous_track(self):
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): 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: else:
self.toggle_play_button.setIcon(self.play_icon) 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.progress_indicator.setText(self.app.utils.format_time(progress))
self.track_control.track_progress_slider.setValue(progress) self.setValue(progress)
else: else:
self.track_control.progress_indicator.setText(self.app.utils.format_time(0)) self.track_control.progress_indicator.setText(self.app.utils.format_time(0))
self.track_control.track_progress_slider.setValue(0) self.setValue(0)