MPRIS: Got everything necessary working. (I think.)

This commit is contained in:
The Wobbler 2025-04-18 19:35:13 +02:00
parent f23530628c
commit 1f149a25a3
10 changed files with 333 additions and 58 deletions

View file

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

View file

@ -1,9 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
from jeepney import Header, MessageType, Endianness, MessageFlag, new_method_return from jeepney import new_signal
from jeepney.bus_messages import message_bus
from jeepney.io.blocking import open_dbus_connection
from jeepney.wrappers import new_header
from .utils import * from .utils import *
@ -13,25 +10,31 @@ class DBusProperties(DBusInterface):
body = ({},) body = ({},)
return body return body
def properties_changed(self, interface: str): def properties_changed(self, interface: str, prop_name: str):
body = None body = None
if interface == MPRIS_ROOT_INTERFACE: 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: 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( msg = new_signal(
self.server.bus_address.with_interface("org.freedesktop.DBus"), self.server.bus_address.with_interface(PROPERTIES_INTERFACE),
"PropertiesChanged", "PropertiesChanged",
signature, signature,
body body
) )
self.server.bus.send(msg) self.server.bus.send(msg)
# ======== Methods ========
def Get(self, msg: Message): def Get(self, msg: Message):
interface_name = msg.body[0] interface_name = msg.body[0]

View file

@ -0,0 +1,76 @@
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" type="s" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface_name" type="s" direction="in"/>
<arg name="property_name" type="s" direction="in"/>
<arg name="value" type="v" direction="out"/>
</method>
<method name="GetAll">
<arg name="interface_name" type="s" direction="in"/>
<arg name="properties" type="a{sv}" direction="out"/>
</method>
<!--
<method name="Set">
<arg name="interface_name" type="s" direction="in"/>
<arg name="property_name" type="s" direction="in"/>
<arg name="value" type="v" direction="in"/>
</method>
-->
<signal name="PropertiesChanged">
<arg name="interface_name" type="s"/>
<arg name="changed_properties" type="a{sv}"/>
<arg name="invalidated_properties" type="as"/>
</signal>
</interface>
<interface name="org.mpris.MediaPlayer2">
<method name="Raise"/>
<method name="Quit"/>
<property name="CanQuit" access="read" type="b"/>
<property name="Fullscreen" access="read" type="b"/>
<property name="CanSetFullscreen" access="read" type="b"/>
<property name="CanRaise" access="read" type="b"/>
<property name="HasTrackList" access="read" type="b"/>
<property name="Identity" access="read" type="s"/>
<property name="DesktopEntry" access="read" type="s"/>
<property name="SupportedUriSchemes" access="read" type="as"/>
<property name="SupportedMimeTypes" access="read" type="as"/>
</interface>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="Next"/>
<method name="Previous"/>
<method name="Pause"/>
<method name="PlayPause"/>
<method name="Stop"/>
<method name="Play"/>
<method name="Seek">
<arg name="Offset" type="x" direction="in"/>
</method>
<method name="SetPosition">
<arg name="TrackId" type="o" direction="in"/>
<arg name="Position" type="x" direction="in"/>
</method>
<method name="OpenUri">
<arg name="Uri" type="s" direction="in"/>
</method>
<property name="PlaybackStatus" access="read" type="s"/>
<property name="LoopStatus" access="read" type="s"/>
<property name="Rate" access="read" type="d"/>
<property name="Shuffle" access="read" type="b"/>
<property name="Metadata" access="read" type="a{sv}"/>
<property name="Volume" access="read" type="d"/>
<property name="Position" access="read" type="x"/>
<property name="MinimumRate" access="read" type="d"/>
<property name="MaximumRate" access="read" type="d"/>
<property name="CanGoNext" access="read" type="b"/>
<property name="CanGoPrevious" access="read" type="b"/>
<property name="CanPlay" access="read" type="b"/>
<property name="CanPause" access="read" type="b"/>
<property name="CanSeek" access="read" type="b"/>
<property name="CanControl" access="read" type="b"/>
</interface>
</node>

View file

@ -1,7 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
from jeepney import new_method_return
from .utils import * from .utils import *
@ -9,26 +7,116 @@ class MPRISPlayer(DBusInterface):
def __init__(self, server, interface): def __init__(self, server, interface):
super().__init__(server, interface) super().__init__(server, interface)
self.playback_status = "Stopped"
self.loop_status = "None"
self.shuffle = False
self.metadata = { 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") "xesam:title": ("s", "Huggenburgl")
} }
self.volume = 1.0
def get_all(self): def get_all(self):
body = ({ 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), "CanPlay": ("b", True),
"Metadata": ("a{sv}", self.metadata) "CanPause": ("b", True),
"CanSeek": ("b", True),
"CanControl": ("b", True)
},) },)
return body return body
def Play(self, msg: Message): # ======== Methods ========
print("Play!")
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): 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): 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)

View file

@ -7,10 +7,52 @@ class MPRISRoot(DBusInterface):
def get_all(self): def get_all(self):
body = ({ body = ({
"CanQuit": ("b", True), "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 return body
# ======== Methods ========
def Raise(self, msg): 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"]

View file

@ -1,11 +1,13 @@
#!/usr/bin/python3 #!/usr/bin/python3
from threading import Thread from threading import Thread
from jeepney import DBusAddress
from jeepney.bus_messages import message_bus from jeepney.bus_messages import message_bus
from jeepney.io.blocking import DBusConnection, open_dbus_connection from jeepney.io.blocking import DBusConnection, open_dbus_connection
from .utils import * from .utils import *
from .dbus_properties import DBusProperties from .dbus_properties import DBusProperties
from .dbus_introspectable import DBUSIntrospectable
from .mpris_root import MPRISRoot from .mpris_root import MPRISRoot
from .mpris_player import MPRISPlayer from .mpris_player import MPRISPlayer
@ -15,6 +17,7 @@ class MPRISServer:
self.app = app self.app = app
self.properties_interface = DBusProperties(self, PROPERTIES_INTERFACE) self.properties_interface = DBusProperties(self, PROPERTIES_INTERFACE)
self.introspection_interface = DBUSIntrospectable(self, INTROSPECTION_INTERFACE)
self.root_interface = MPRISRoot(self, MPRIS_ROOT_INTERFACE) self.root_interface = MPRISRoot(self, MPRIS_ROOT_INTERFACE)
self.player_interface = MPRISPlayer(self, MPRIS_PLAYER_INTERFACE) self.player_interface = MPRISPlayer(self, MPRIS_PLAYER_INTERFACE)
@ -51,6 +54,9 @@ class MPRISServer:
if interface == PROPERTIES_INTERFACE: if interface == PROPERTIES_INTERFACE:
self.properties_interface.handle_message(msg) self.properties_interface.handle_message(msg)
elif interface == INTROSPECTION_INTERFACE:
self.introspection_interface.handle_message(msg)
elif interface == MPRIS_ROOT_INTERFACE: elif interface == MPRIS_ROOT_INTERFACE:
self.root_interface.handle_message(msg) self.root_interface.handle_message(msg)
@ -58,11 +64,30 @@ class MPRISServer:
self.player_interface.handle_message(msg) self.player_interface.handle_message(msg)
def on_playstate_update(self): 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: if current_playlist is not None and current_playlist.current_track is not None:
current_track = current_playlist.current_track 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")

View file

@ -1,11 +1,11 @@
#!/usr/bin/python3 #!/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" SERVICE_NAME = "org.mpris.MediaPlayer2.wobuzz"
OBJECT_PATH = "/org/mpris/MediaPlayer2" OBJECT_PATH = "/org/mpris/MediaPlayer2"
PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties" PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties"
INTROSPECTION_INTERFACE = "org.freedesktop.DBus.Introspectable"
MPRIS_ROOT_INTERFACE = "org.mpris.MediaPlayer2" MPRIS_ROOT_INTERFACE = "org.mpris.MediaPlayer2"
MPRIS_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player" MPRIS_PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player"
@ -46,18 +46,9 @@ class DBusInterface:
self.server = server self.server = server
self.interface = interface 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): def handle_message(self, msg: Message):
return_msg = None return_msg = None
post_action = None # function that gets called after return_msg was set
match msg.header.message_type: match msg.header.message_type:
case MessageType.method_call: case MessageType.method_call:
@ -67,7 +58,17 @@ class DBusInterface:
method = getattr(self, msg.header.fields[HeaderFields.member]) method = getattr(self, msg.header.fields[HeaderFields.member])
if callable(method): 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: else:
return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name))
@ -75,9 +76,14 @@ class DBusInterface:
else: else:
return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name)) return_msg = new_error(msg, *DBusErrors.unknownMethod(method_name))
if return_msg is not None: if return_msg is None:
return_msg = new_method_return(msg)
self.server.bus.send_message(return_msg) self.server.bus.send_message(return_msg)
if not post_action is None:
post_action()
def get(self, msg: Message): def get(self, msg: Message):
prop_name: str = msg.body[1] prop_name: str = msg.body[1]
@ -85,7 +91,13 @@ class DBusInterface:
prop = getattr(self, prop_name) prop = getattr(self, prop_name)
if callable(prop): 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)) return new_error(msg, *DBusErrors.unknownProperty(prop_name))

View file

@ -35,10 +35,10 @@ class Player:
self.playing = True self.playing = True
self.paused = False self.paused = False
self.app.gui.on_playstate_update()
self.export_cover_art_tmp() self.export_cover_art_tmp()
self.app.gui.on_playstate_update()
# 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()
@ -234,3 +234,24 @@ class Player:
file = open(art_path, "wb") file = open(art_path, "wb")
file.write(metadata.images.any.data) file.write(metadata.images.any.data)
file.close() 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

View file

@ -1,6 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QToolBar, QLabel from PyQt6.QtWidgets import QToolBar, QLabel

View file

@ -55,22 +55,9 @@ class TrackProgressSlider(QSlider):
def update_progress(self): def update_progress(self):
if not self.dragged: if not self.dragged:
if self.app.player.playing: progress = self.app.player.get_progress()
remaining = self.app.player.track_progress.timer.remainingTime()
if remaining == -1:
remaining = self.app.player.track_progress.remaining_time
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.track_control.progress_indicator.setText(self.app.utils.format_time(progress))
self.setValue(progress) self.setValue(progress)
else:
self.track_control.progress_indicator.setText(self.app.utils.format_time(0))
self.setValue(0)