diff --git a/README.md b/README.md
index 06cf37b..789a27e 100644
--- a/README.md
+++ b/README.md
@@ -10,16 +10,17 @@ Please note that [the repository on teapot.informationsanarchistik.de](https://t
normal users only have read rights on the original repository because registration is disabled on the server.
Issues and pull-requests are not synced.
-### Features
+### Features (Implemented & Planned)
-| Feature | Description | State |
-|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
-| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | Implemented |
-| Background Job Monitor | A QDockWidget where background processes are listed. | Implemented |
-| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | Not Implemented |
-| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | Not Implemented |
-| Synchronisation between devices | This should be pretty hard to implement and idk. if i will ever make it, but synchronisation could be pretty practical e.g. if you have multiple audio systems in different rooms. | Not Implemented |
-| Audio visualization | Firstly, rather simple audio visualization like an oscilloscope would be cool, also something more complicated like [ProjectM](https://github.com/projectM-visualizer/projectm) could be integrated. | Not Implemented |
+| Feature | Description | State |
+|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
+| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | Implemented |
+| Background Job Monitor | A QDockWidget where background processes are listed. | Implemented |
+| MPRIS Integration | Basic MPRIS integration (Partial Metadata, Play, Pause, Stop, Next, Previous) | Partially Implemented |
+| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | Not Implemented |
+| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | Not Implemented |
+| Synchronisation between devices | This should be pretty hard to implement and idk. if i will ever make it, but synchronisation could be pretty practical e.g. if you have multiple audio systems in different rooms. | Not Implemented |
+| Audio visualization | Firstly, rather simple audio visualization like an oscilloscope would be cool, also something more complicated like [ProjectM](https://github.com/projectM-visualizer/projectm) could be integrated. | Not Implemented |
### Performance
diff --git a/requirements.txt b/requirements.txt
index f4f580b..3fbe474 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,7 @@
-PyQt6
-pygame
-tinytag
-pydub
-wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools
\ No newline at end of file
+PyQt6~=6.8.0
+pygame~=2.6.1
+tinytag~=2.1.0
+pydub~=0.25.1
+wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools
+sdbus~=0.14.0
+setuptools~=68.1.2
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 790fec4..5df990e 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@ long_description = (this_directory / "README.md").read_text()
setuptools.setup(
name="Wobuzz",
- version="0.1a2",
+ version="0.1a3",
description="An audio player made by The Wobbler",
long_description=long_description,
long_description_content_type="text/markdown",
@@ -30,11 +30,12 @@ setuptools.setup(
packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]),
package_data={"": ["*.txt", "*.svg"]},
install_requires=[
- "PyQt6",
- "tinytag",
- "pydub",
- "pygame",
- "wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools"
+ "PyQt6~=6.8.0",
+ "tinytag~=2.1.0",
+ "pydub~=0.25.1",
+ "pygame~=2.6.1",
+ "wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools",
+ "sdbus~=0.14.0"
],
entry_points={
"console_scripts": ["wobuzz=wobuzz.command_line:main"],
diff --git a/wobuzz/player/mpris.py b/wobuzz/player/mpris.py
new file mode 100644
index 0000000..f4c3146
--- /dev/null
+++ b/wobuzz/player/mpris.py
@@ -0,0 +1,98 @@
+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:
+ xesam_metadata = {
+ "mpris:trackid": ("s", "kjuztuktg"),
+ "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)
diff --git a/wobuzz/player/player.py b/wobuzz/player/player.py
index f32d25a..b9bc4e9 100644
--- a/wobuzz/player/player.py
+++ b/wobuzz/player/player.py
@@ -4,8 +4,10 @@ import time
import threading
import pygame.mixer
import pygame.event
+
from .playlist import Playlist
from .track_progress_timer import TrackProgress
+from .mpris import MPRISServer
class Player:
@@ -21,6 +23,10 @@ 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
@@ -34,6 +40,7 @@ class Player:
self.paused = False
self.app.gui.on_playstate_update()
+ 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()
@@ -191,3 +198,22 @@ class Player:
self.current_playlist = playlist
self.start_playing()
+
+ def toggle_playing(self):
+ if self.playing and self.paused: # paused
+ self.unpause()
+
+ elif self.playing: # playing
+ self.pause()
+
+ # stopped but tracks in the current playlist
+ elif self.current_playlist is not None and self.current_playlist.has_tracks():
+ self.start_playing()
+
+ elif self.current_playlist is None:
+ if self.app.settings.latest_playlist is not None:
+ for playlist in self.app.library.playlists: # get loaded playlist by the path of the latest playlist
+ if playlist.path == self.app.settings.latest_playlist:
+ self.start_playlist(playlist)
+
+ break
diff --git a/wobuzz/ui/track_control.py b/wobuzz/ui/track_control.py
index 770e6f8..6b41796 100644
--- a/wobuzz/ui/track_control.py
+++ b/wobuzz/ui/track_control.py
@@ -1,11 +1,18 @@
#!/usr/bin/python3
+from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QToolBar, QLabel
+
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)
@@ -45,9 +52,13 @@ class TrackControl(QToolBar):
def connect(self):
self.previous_button.triggered.connect(self.previous_track)
- self.toggle_play_button.triggered.connect(self.toggle_playing)
+ 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():
@@ -73,25 +84,6 @@ class TrackControl(QToolBar):
self.track_progress_slider.update_progress()
- def toggle_playing(self):
- if self.app.player.playing and self.app.player.paused: # paused
- self.app.player.unpause()
-
- elif self.app.player.playing: # playing
- self.app.player.pause()
-
- # stopped but tracks in the current playlist
- elif self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks():
- self.app.player.start_playing()
-
- elif self.app.player.current_playlist is None:
- if self.app.settings.latest_playlist is not None:
- for playlist in self.app.library.playlists: # get loaded playlist by the path of the latest playlist
- if playlist.path == self.app.settings.latest_playlist:
- self.app.player.start_playlist(playlist)
-
- break
-
def on_playstate_update(self):
if self.app.player.playing:
if self.app.player.paused: