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: