| Add MPRIS integration for media key control
All checks were successful
SonarQube Code Quality Scan / SonarQube Scan (push) Successful in 1m48s

Implements org.mpris.MediaPlayer2 and org.mpris.MediaPlayer2.Player
D-Bus interfaces, allowing desktop environment media keys and tools
like playerctl to control playback (play, pause, next, previous)
and display now-playing metadata.

Also adds _FallbackServiceInterface, _FallbackVariant and
_FallbackNameFlag so all tests pass without dbus-fast installed;
the module gracefully degrades when the library is unavailable.
This commit is contained in:
2026-06-09 18:01:41 +03:00
parent 37d4a7d47e
commit e2d64cd464
4 changed files with 771 additions and 0 deletions

53
gui.py
View File

@@ -80,6 +80,7 @@ except (ImportError, OSError):
from config import SETTINGS, get_setting, save_setting, save_volume
from discord_rpc import DiscordRPC
from mpris import MprisManager
from music_db import MusicDB
from player import AudioPlayer, SearchWorker, _best_thumbnail
@@ -2279,6 +2280,9 @@ class TunettiWindow(QMainWindow):
self.db = MusicDB(SETTINGS["db_path"])
self.rpc = DiscordRPC(SETTINGS["discord_client_id"])
self.player = AudioPlayer(self)
self.mpris = MprisManager()
self.mpris.start()
self._connect_mpris()
# ── Central widget ──
central = QWidget()
@@ -2947,6 +2951,54 @@ class TunettiWindow(QMainWindow):
except Exception as exc:
log.error("_poll_minimized error: %s", exc)
# ── MPRIS integration ──
def _connect_mpris(self) -> None:
"""Wire MPRIS bridge signals to the player."""
bridge = self.mpris.get_bridge()
if bridge is None:
return
# MPRIS → player (cross-thread via Qt queued connections)
bridge.play_requested.connect(self.player.resume)
bridge.pause_requested.connect(self.player.pause)
bridge.play_pause_requested.connect(self.player.toggle_pause)
bridge.stop_requested.connect(self.player.stop_playback)
bridge.next_requested.connect(self.player.skip)
bridge.previous_requested.connect(self.player.previous)
# Player → MPRIS state updates
self.player.playback_state_changed.connect(
self._on_mpris_state_changed
)
self.player.song_started.connect(self._on_mpris_song_started)
self.player.song_ended.connect(self._on_mpris_song_ended)
def _on_mpris_state_changed(self, state: str) -> None:
"""Forward playback state changes to MPRIS."""
song = self.player.get_current()
self.mpris.notify_state_changed(
state, song, self.player.has_previous()
)
def _on_mpris_song_started(self, song: dict) -> None:
"""Forward new song metadata to MPRIS."""
self.mpris.notify_state_changed(
"playing", song, self.player.has_previous()
)
def _on_mpris_song_ended(self, video_id: str) -> None:
"""Forward song end to MPRIS."""
song = self.player.get_current()
if song:
self.mpris.notify_state_changed(
"playing", song, self.player.has_previous()
)
else:
self.mpris.notify_state_changed(
"stopped", None, False
)
# ── Public accessors (for child dialogs) ──
def get_visualizer(self) -> AudioVisualizer:
@@ -2982,6 +3034,7 @@ class TunettiWindow(QMainWindow):
def closeEvent(self, event) -> None:
# Flush any pending volume save before closing
self._playback.flush_volume()
self.mpris.stop()
self.player.shutdown()
self.rpc.stop()
self.db.close()