forked from Wobbl/Wobuzz
MPRIS: Switching from python-sdbus to jeepney, no functionality.
This commit is contained in:
parent
a236370d47
commit
f23530628c
14 changed files with 301 additions and 125 deletions
|
@ -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()
|
||||
|
|
|
@ -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
4
wobuzz/mpris/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from .server import MPRISServer
|
||||
|
67
wobuzz/mpris/dbus_properties.py
Normal file
67
wobuzz/mpris/dbus_properties.py
Normal 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)
|
34
wobuzz/mpris/mpris_player.py
Normal file
34
wobuzz/mpris/mpris_player.py
Normal 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)
|
16
wobuzz/mpris/mpris_root.py
Normal file
16
wobuzz/mpris/mpris_root.py
Normal 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
68
wobuzz/mpris/server.py
Normal 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
96
wobuzz/mpris/utils.py
Normal 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
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue