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. normal users only have read rights on the original repository because registration is disabled on the server.
Issues and pull-requests are not synced. Issues and pull-requests are not synced.
### Features ### Features (Implemented & Planned)
| Feature | Description | State | | 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 | | 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 | | 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 | | MPRIS Integration | Basic MPRIS integration (Partial Metadata, Play, Pause, Stop, Next, Previous) | <input type="checkbox" disabled /> Partially 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 | | 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 |
| 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 | | 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 |
| 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 | | 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 ### Performance

View file

@ -1,5 +1,7 @@
PyQt6 PyQt6~=6.8.0
pygame pygame~=2.6.1
tinytag tinytag~=2.1.0
pydub pydub~=0.25.1
wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools 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( setuptools.setup(
name="Wobuzz", name="Wobuzz",
version="0.1a2", version="0.1a3",
description="An audio player made by The Wobbler", description="An audio player made by The Wobbler",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@ -30,11 +30,12 @@ setuptools.setup(
packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]), packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]),
package_data={"": ["*.txt", "*.svg"]}, package_data={"": ["*.txt", "*.svg"]},
install_requires=[ install_requires=[
"PyQt6", "PyQt6~=6.8.0",
"tinytag", "tinytag~=2.1.0",
"pydub", "pydub~=0.25.1",
"pygame", "pygame~=2.6.1",
"wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools" "wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools",
"sdbus~=0.14.0"
], ],
entry_points={ entry_points={
"console_scripts": ["wobuzz=wobuzz.command_line:main"], "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 threading
import pygame.mixer import pygame.mixer
import pygame.event import pygame.event
from .playlist import Playlist from .playlist import Playlist
from .track_progress_timer import TrackProgress from .track_progress_timer import TrackProgress
from .mpris import MPRISServer
class Player: class Player:
@ -21,6 +23,10 @@ class Player:
self.history = Playlist(self.app, "History") self.history = Playlist(self.app, "History")
self.current_playlist = None 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.playing = False
self.paused = False self.paused = False
@ -34,6 +40,7 @@ class Player:
self.paused = False self.paused = False
self.app.gui.on_playstate_update() 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 # cache next track so it immediately starts when the current track finishes
self.cache_next_track() self.cache_next_track()
@ -191,3 +198,22 @@ class Player:
self.current_playlist = playlist self.current_playlist = playlist
self.start_playing() 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 #!/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
from .track_progress_slider import TrackProgressSlider from .track_progress_slider import TrackProgressSlider
class TrackControl(QToolBar): 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): def __init__(self, app, parent=None):
super().__init__(parent) super().__init__(parent)
@ -45,9 +52,13 @@ class TrackControl(QToolBar):
def connect(self): def connect(self):
self.previous_button.triggered.connect(self.previous_track) 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_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_button.triggered.connect(self.next_track)
self.next_signal.connect(self.next_track)
def previous_track(self): def previous_track(self):
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): 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() 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): def on_playstate_update(self):
if self.app.player.playing: if self.app.player.playing:
if self.app.player.paused: if self.app.player.paused: