From 1f149a25a30d127209fd0a066f0d90d604aa6b8d Mon Sep 17 00:00:00 2001 From: The Wobbler Date: Fri, 18 Apr 2025 19:35:13 +0200 Subject: [PATCH] MPRIS: Got everything necessary working. (I think.) --- wobuzz/mpris/dbus_introspectable.py | 22 ++++++ wobuzz/mpris/dbus_properties.py | 21 +++--- wobuzz/mpris/introspection.xml | 76 ++++++++++++++++++++ wobuzz/mpris/mpris_player.py | 106 +++++++++++++++++++++++++--- wobuzz/mpris/mpris_root.py | 46 +++++++++++- wobuzz/mpris/server.py | 31 +++++++- wobuzz/mpris/utils.py | 44 +++++++----- wobuzz/player/player.py | 25 ++++++- wobuzz/ui/track_control.py | 1 - wobuzz/ui/track_progress_slider.py | 19 +---- 10 files changed, 333 insertions(+), 58 deletions(-) create mode 100644 wobuzz/mpris/dbus_introspectable.py create mode 100644 wobuzz/mpris/introspection.xml diff --git a/wobuzz/mpris/dbus_introspectable.py b/wobuzz/mpris/dbus_introspectable.py new file mode 100644 index 0000000..3853071 --- /dev/null +++ b/wobuzz/mpris/dbus_introspectable.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 + +from .utils import * + + +class DBUSIntrospectable(DBusInterface): + def __init__(self, server, interface: str): + self.server = server + self.interface = interface + + file = open(server.app.utils.wobuzz_location + "/mpris/introspection.xml") + self.introspection_xml = file.read() + file.close() + + def get_all(self): + body = ({},) + return body + + # ======== Methods ======== + + def Introspect(self, msg): + return new_method_return(msg, "s", (self.introspection_xml,)) diff --git a/wobuzz/mpris/dbus_properties.py b/wobuzz/mpris/dbus_properties.py index 51665f0..d8eefec 100644 --- a/wobuzz/mpris/dbus_properties.py +++ b/wobuzz/mpris/dbus_properties.py @@ -1,9 +1,6 @@ #!/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 jeepney import new_signal from .utils import * @@ -13,25 +10,31 @@ class DBusProperties(DBusInterface): body = ({},) return body - def properties_changed(self, interface: str): + def properties_changed(self, interface: str, prop_name: str): body = None if interface == MPRIS_ROOT_INTERFACE: - body = (MPRIS_ROOT_INTERFACE,) + self.server.root_interface.get_all() + prop = getattr(self.server.root_interface, prop_name)(None) + + body = (MPRIS_ROOT_INTERFACE,) + ({prop_name: prop}, []) elif interface == MPRIS_PLAYER_INTERFACE: - body = (MPRIS_PLAYER_INTERFACE,) + self.server.player_interface.get_all() + prop = getattr(self.server.player_interface, prop_name)(None) - signature = "" if body is None else "sa{sv}" + body = (MPRIS_PLAYER_INTERFACE,) + ({prop_name: prop}, []) + + signature = "" if body is None else "sa{sv}as" msg = new_signal( - self.server.bus_address.with_interface("org.freedesktop.DBus"), + self.server.bus_address.with_interface(PROPERTIES_INTERFACE), "PropertiesChanged", signature, body ) self.server.bus.send(msg) + # ======== Methods ======== + def Get(self, msg: Message): interface_name = msg.body[0] diff --git a/wobuzz/mpris/introspection.xml b/wobuzz/mpris/introspection.xml new file mode 100644 index 0000000..1fa719c --- /dev/null +++ b/wobuzz/mpris/introspection.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wobuzz/mpris/mpris_player.py b/wobuzz/mpris/mpris_player.py index 67255b2..594722f 100644 --- a/wobuzz/mpris/mpris_player.py +++ b/wobuzz/mpris/mpris_player.py @@ -1,7 +1,5 @@ #!/usr/bin/python3 -from jeepney import new_method_return - from .utils import * @@ -9,26 +7,116 @@ class MPRISPlayer(DBusInterface): def __init__(self, server, interface): super().__init__(server, interface) + self.playback_status = "Stopped" + self.loop_status = "None" + self.shuffle = False self.metadata = { - "mpris:trackid": ("o", "/org/mpris/MediaPlayer2/murx"), # random junk, no functionality + "mpris:trackid": ("o", "/org/bla/gubber"), # random junk, no functionality + "mpris:length": ("x", 0), "xesam:title": ("s", "Huggenburgl") } + self.volume = 1.0 def get_all(self): body = ({ + "PlaybackStatus": ("s", self.playback_status), + "LoopStatus": ("s", self.loop_status), + "Rate": ("d", 1.0), + "Shuffle": ("b", self.shuffle), + "Metadata": ("a{sv}", self.metadata), + "Volume": ("d", self.volume), + "Position": ("x", self.server.app.player.get_progress() * 1000), # milliseconds to microseconds + "MinimumRate": ("d", 1.0), + "MaximumRate": ("d", 1.0), + "CanGoNext": ("b", True), + "CanGoPrevious": ("b", True), "CanPlay": ("b", True), - "Metadata": ("a{sv}", self.metadata) + "CanPause": ("b", True), + "CanSeek": ("b", True), + "CanControl": ("b", True) },) return body - def Play(self, msg: Message): - print("Play!") + # ======== Methods ======== + + def Next(self, msg: Message): + self.server.app.player.next_track() + + def Previous(self, msg: Message): + self.server.app.player.previous_track() + + def Pause(self, msg: Message): + return lambda: self.server.app.player.toggle_playing() def PlayPause(self, msg: Message): - print("Play/Pause!") + return lambda: self.server.app.player.toggle_playing() + + def Stop(self, msg: Message): + self.server.app.player.stop() + + def Play(self, msg: Message): + return lambda: self.server.app.player.toggle_playing() + + def Seek(self, msg: Message): + seek_forward = msg.body[0] // 1000 # microseconds to milliseconds + new_position = self.server.app.player.get_progress() + seek_forward + self.server.app.player.seek(new_position) + + def SetPosition(self, msg: Message): + trackid = msg.body[0] + position = msg.body[1] // 1000 # microseconds to milliseconds + + self.server.app.player.seek(position) + + def OpenUri(self, msg: Message): + pass + + # ======== Properties ======== + + def PlaybackStatus(self, msg: Message): + return "s", self.playback_status + + def LoopStatus(self, msg: Message): + return "s", self.loop_status + + def Rate(self, msg: Message): + return "d", 1.0 + + def Shuffle(self, msg: Message): + return "b", self.shuffle def Metadata(self, msg: Message): - body = (self.metadata,) + return "a{sv}", self.metadata + + def Volume(self, msg: Message): + return "d", self.volume + + def Position(self, msg: Message): + return "x", self.server.app.player.get_progress() * 1000 # milliseconds to microseconds + + def MinimumRate(self, msg: Message): + return "d", 1.0 + + def MaximumRate(self, msg: Message): + return "d", 1.0 + + def CanGoNext(self, msg: Message): + return "b", True + + def CanGoPrevious(self, msg: Message): + return "b", True + + def CanPlay(self, msg: Message): + return "b", True + + def CanPause(self, msg: Message): + return "b", True + + def CanSeek(self, msg: Message): + return "b", True + + def CanControl(self, msg: Message): + return "b", True + - return new_method_return(msg, "a{sv}", body) diff --git a/wobuzz/mpris/mpris_root.py b/wobuzz/mpris/mpris_root.py index d344e0b..2d33dc8 100644 --- a/wobuzz/mpris/mpris_root.py +++ b/wobuzz/mpris/mpris_root.py @@ -7,10 +7,52 @@ class MPRISRoot(DBusInterface): def get_all(self): body = ({ "CanQuit": ("b", True), - "CanRaise": ("b", True) + "Fullscreen": ("b", False), + "CanSetFullscreen": ("b", False), + "CanRaise": ("b", True), + "HasTrackList": ("b", False), + "Identity": ("s", "Wobuzz"), + "DesktopEntry": ("s", "wobuzz"), + "SupportedUriSchemes": ("as", ["file"]), + "SupportedMimeTypes": ("as", ["audio/mpeg"]) },) return body + # ======== Methods ======== + def Raise(self, msg): - print("Raise!") + self.server.app.gui.window.activateWindow() + self.server.app.gui.window.showMaximized() + + def Quit(self, msg): + self.server.app.gui.window.close() + + # ======== Properties ======== + + def CanQuit(self, msg: Message): + return "b", True + + def Fullscreen(self, msg: Message): + return "b", False + + def CanSetFullscreen(self, msg: Message): + return "b", False + + def CanRaise(self, msg: Message): + return "b", True + + def HasTrackList(self, msg: Message): + return "b", False + + def Identity(self, msg: Message): + return "s", "Wobuzz" + + def DesktopEntry(self, msg: Message): + return "s", "wobuzz" + + def SupportedUriSchemes(self, msg: Message): + return "as", ["file"] + + def SupportedMimeTypes(self, msg: Message): + return "as", ["audio/mpeg"] diff --git a/wobuzz/mpris/server.py b/wobuzz/mpris/server.py index 7ab753f..9067282 100644 --- a/wobuzz/mpris/server.py +++ b/wobuzz/mpris/server.py @@ -1,11 +1,13 @@ #!/usr/bin/python3 from threading import Thread +from jeepney import DBusAddress 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 .dbus_introspectable import DBUSIntrospectable from .mpris_root import MPRISRoot from .mpris_player import MPRISPlayer @@ -15,6 +17,7 @@ class MPRISServer: self.app = app self.properties_interface = DBusProperties(self, PROPERTIES_INTERFACE) + self.introspection_interface = DBUSIntrospectable(self, INTROSPECTION_INTERFACE) self.root_interface = MPRISRoot(self, MPRIS_ROOT_INTERFACE) self.player_interface = MPRISPlayer(self, MPRIS_PLAYER_INTERFACE) @@ -51,6 +54,9 @@ class MPRISServer: if interface == PROPERTIES_INTERFACE: self.properties_interface.handle_message(msg) + elif interface == INTROSPECTION_INTERFACE: + self.introspection_interface.handle_message(msg) + elif interface == MPRIS_ROOT_INTERFACE: self.root_interface.handle_message(msg) @@ -58,11 +64,30 @@ class MPRISServer: self.player_interface.handle_message(msg) def on_playstate_update(self): - current_playlist = self.app.player.current_playlist + player = self.app.player + current_playlist = 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) + art_path = self.app.utils.tmp_path + "/cover_cache/" + current_track.metadata.path.split("/")[-1][:-4] - self.properties_interface.properties_changed(MPRIS_PLAYER_INTERFACE) + # metadata milli to microseconds --↓ + self.player_interface.metadata["mpris:length"] = ("x", current_track.duration * 1000) + self.player_interface.metadata["mpris:artUrl"] = ("s", "file://" + art_path) + self.player_interface.metadata["xesam:title"] = ("s", current_track.metadata.title) + self.properties_interface.properties_changed(MPRIS_PLAYER_INTERFACE, "Metadata") + + if player.playing: + if player.paused: + playback_status = "Paused" + + else: + playback_status = "Playing" + + else: + playback_status = "Stopped" + + self.player_interface.playback_status = playback_status + + self.properties_interface.properties_changed(MPRIS_PLAYER_INTERFACE, "PlaybackStatus") diff --git a/wobuzz/mpris/utils.py b/wobuzz/mpris/utils.py index f5a4a55..6d82d44 100644 --- a/wobuzz/mpris/utils.py +++ b/wobuzz/mpris/utils.py @@ -1,11 +1,11 @@ #!/usr/bin/python3 -from jeepney import DBusAddress, Message, MessageType, HeaderFields, new_error, new_signal - +from jeepney import Message, MessageType, HeaderFields, new_error, new_method_return SERVICE_NAME = "org.mpris.MediaPlayer2.wobuzz" OBJECT_PATH = "/org/mpris/MediaPlayer2" PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties" +INTROSPECTION_INTERFACE = "org.freedesktop.DBus.Introspectable" MPRIS_ROOT_INTERFACE = "org.mpris.MediaPlayer2" MPRIS_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player" @@ -46,18 +46,9 @@ class DBusInterface: 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 + post_action = None # function that gets called after return_msg was set match msg.header.message_type: case MessageType.method_call: @@ -67,7 +58,17 @@ class DBusInterface: method = getattr(self, msg.header.fields[HeaderFields.member]) if callable(method): - return_msg = method(msg) + method_data = method(msg) + + if isinstance(method_data, tuple): + post_action, return_msg = method_data + + else: + if callable(method_data): + post_action = method_data + + else: + return_msg = method_data else: return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) @@ -75,8 +76,13 @@ class DBusInterface: else: return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) - if return_msg is not None: - self.server.bus.send_message(return_msg) + if return_msg is None: + return_msg = new_method_return(msg) + + self.server.bus.send_message(return_msg) + + if not post_action is None: + post_action() def get(self, msg: Message): prop_name: str = msg.body[1] @@ -85,7 +91,13 @@ class DBusInterface: prop = getattr(self, prop_name) if callable(prop): - return prop() + prop_value = prop(msg) + signature = prop_value[0] + value = prop_value[1] + + return_msg = new_method_return(msg, "v", (prop_value,)) + + return return_msg return new_error(msg, *DBusErrors.unknownProperty(prop_name)) diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py index 452ed72..4747cb4 100644 --- a/wobuzz/player/player.py +++ b/wobuzz/player/player.py @@ -35,10 +35,10 @@ class Player: self.playing = True self.paused = False - self.app.gui.on_playstate_update() - self.export_cover_art_tmp() + self.app.gui.on_playstate_update() + # cache next track so it immediately starts when the current track finishes self.cache_next_track() @@ -234,3 +234,24 @@ class Player: file = open(art_path, "wb") file.write(metadata.images.any.data) file.close() + + def get_progress(self) -> int: + """ + Gets the progress of the current track in milliseconds. + (Also when paused.) + :return: Progress in milliseconds + """ + + if self.playing: + remaining = self.track_progress.timer.remainingTime() + + if remaining == -1: + remaining = self.track_progress.remaining_time + + track_duration = self.current_playlist.current_track.duration + + progress = track_duration - remaining + + return progress + + return 0 diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py index d159edc..7130789 100644 --- a/wobuzz/ui/track_control.py +++ b/wobuzz/ui/track_control.py @@ -1,6 +1,5 @@ #!/usr/bin/python3 -from PyQt6.QtCore import pyqtSignal from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QToolBar, QLabel diff --git a/wobuzz/ui/track_progress_slider.py b/wobuzz/ui/track_progress_slider.py index 028339c..6a35ade 100644 --- a/wobuzz/ui/track_progress_slider.py +++ b/wobuzz/ui/track_progress_slider.py @@ -55,22 +55,9 @@ class TrackProgressSlider(QSlider): def update_progress(self): if not self.dragged: - if self.app.player.playing: - remaining = self.app.player.track_progress.timer.remainingTime() + progress = self.app.player.get_progress() - if remaining == -1: - remaining = self.app.player.track_progress.remaining_time + self.track_control.progress_indicator.setText(self.app.utils.format_time(progress)) - track_duration = self.app.player.current_playlist.current_track.duration - - progress = track_duration - remaining - - self.track_control.progress_indicator.setText(self.app.utils.format_time(progress)) - - self.setValue(progress) - - else: - self.track_control.progress_indicator.setText(self.app.utils.format_time(0)) - - self.setValue(0) + self.setValue(progress)