Implemented basic MPRIS integration.

This commit is contained in:
The Wobbler 2025-04-12 22:00:29 +02:00
parent e845c41ca3
commit 9416ac6737
6 changed files with 160 additions and 40 deletions

View file

@ -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. | <input type="checkbox" disabled checked /> Implemented |
| Background Job Monitor | A QDockWidget where background processes are listed. | <input type="checkbox" disabled checked /> 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. | <input type="checkbox" disabled /> Not Implemented |
| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | <input type="checkbox" disabled /> 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. | <input type="checkbox" disabled /> 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. | <input type="checkbox" disabled /> Not Implemented |
| Feature | Description | State |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | <input type="checkbox" disabled checked /> Implemented |
| Background Job Monitor | A QDockWidget where background processes are listed. | <input type="checkbox" disabled checked /> Implemented |
| MPRIS Integration | Basic MPRIS integration (Partial Metadata, Play, Pause, Stop, Next, Previous) | <input type="checkbox" disabled /> 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. | <input type="checkbox" disabled /> Not Implemented |
| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | <input type="checkbox" disabled /> 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. | <input type="checkbox" disabled /> 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. | <input type="checkbox" disabled /> Not Implemented |
### Performance

View file

@ -1,5 +1,7 @@
PyQt6
pygame
tinytag
pydub
wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools
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

View file

@ -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"],

98
wobuzz/player/mpris.py Normal file
View file

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

View file

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

View file

@ -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: