Implemented basic MPRIS integration.
This commit is contained in:
parent
e845c41ca3
commit
9416ac6737
6 changed files with 160 additions and 40 deletions
19
README.md
19
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.
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
13
setup.py
13
setup.py
|
@ -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
98
wobuzz/player/mpris.py
Normal 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)
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue