"""MPRIS on the session bus: media keys, the desktop's media controls, and KDE Connect on the phone.""" import hashlib from PySide6.QtCore import ClassInfo, Property, QObject, Slot from PySide6.QtDBus import QDBusAbstractAdaptor, QDBusConnection, QDBusMessage, QDBusObjectPath PATH = "/org/mpris/MediaPlayer2" ROOT_IFACE = "org.mpris.MediaPlayer2" PLAYER_IFACE = "org.mpris.MediaPlayer2.Player" @ClassInfo({"D-Bus Interface": ROOT_IFACE}) class _Root(QDBusAbstractAdaptor): def __init__(self, parent, window_raise, quit_): super().__init__(parent) self._raise, self._quit = window_raise, quit_ @Slot() def Raise(self): self._raise() @Slot() def Quit(self): self._quit() CanQuit = Property(bool, lambda self: True) CanRaise = Property(bool, lambda self: True) HasTrackList = Property(bool, lambda self: False) Identity = Property(str, lambda self: "StemRoller Player") DesktopEntry = Property(str, lambda self: "stemroller-player") SupportedUriSchemes = Property("QStringList", lambda self: []) SupportedMimeTypes = Property("QStringList", lambda self: []) @ClassInfo({"D-Bus Interface": PLAYER_IFACE}) class _Player(QDBusAbstractAdaptor): def __init__(self, parent, mpris): super().__init__(parent) self.m = mpris @Slot() def Next(self): self.m.player.next() @Slot() def Previous(self): self.m.player.previous() @Slot() def Pause(self): self.m.engine.pause() @Slot() def PlayPause(self): self.m.player.toggle() @Slot() def Stop(self): self.m.engine.pause() @Slot() def Play(self): if not self.m.engine.playing: self.m.player.toggle() @Slot("qlonglong") def Seek(self, offset_us): self.m.player.seek(self.m.engine.position() + offset_us / 1e6) @Slot(QDBusObjectPath, "qlonglong") def SetPosition(self, track_id, position_us): self.m.player.seek(position_us / 1e6) @Slot(str) def OpenUri(self, uri): pass def _status(self): s = self.m.player._get_status() return "Playing" if s in ("playing", "buffering") or (s == "loading" and self.m.engine.playing) else \ "Paused" if self.m.player.current() else "Stopped" def _volume(self): return float(self.m.engine.volume) def _set_volume(self, v): self.m.engine.setVolume(max(0.0, min(1.0, v))) PlaybackStatus = Property(str, _status) LoopStatus = Property(str, lambda self: {"off": "None", "all": "Playlist", "one": "Track"}[self.m.player.repeat]) Rate = Property(float, lambda self: 1.0) Shuffle = Property(bool, lambda self: self.m.player.shuffle) Metadata = Property("QVariantMap", lambda self: self.m.metadata()) Volume = Property(float, _volume, _set_volume) Position = Property("qlonglong", lambda self: int(self.m.engine.position() * 1e6)) MinimumRate = Property(float, lambda self: 1.0) MaximumRate = Property(float, lambda self: 1.0) CanGoNext = Property(bool, lambda self: bool(self.m.player.queue)) CanGoPrevious = Property(bool, lambda self: bool(self.m.player.queue)) CanPlay = Property(bool, lambda self: bool(self.m.player.queue)) CanPause = Property(bool, lambda self: True) CanSeek = Property(bool, lambda self: True) CanControl = Property(bool, lambda self: True) class Mpris(QObject): def __init__(self, player, engine, window_raise, quit_, parent=None): super().__init__(parent) self.player, self.engine = player, engine self._root = _Root(self, window_raise, quit_) self._adaptor = _Player(self, self) self.bus = QDBusConnection.sessionBus() self.ok = self.bus.isConnected() and self.bus.registerObject(PATH, self) and \ self.bus.registerService("org.mpris.MediaPlayer2.stemroller_player") self._last = {} player.changed.connect(self._changed) player.mixChanged.connect(self._changed) engine.volumeChanged.connect(self._changed) engine.playingChanged.connect(self._changed) def metadata(self): t = self.player.current() if not t: return {"mpris:trackid": QDBusObjectPath("/org/mpris/MediaPlayer2/TrackList/NoTrack")} artist = t["artist"] label = self.player._get_preset_label() if label and label != "Full Mix": artist = f"{artist} ยท {label}" # the mix rides on the artist line, as on the phone md = { "mpris:trackid": QDBusObjectPath("/stemroller/track/t" + hashlib.sha1(t["key"].encode()).hexdigest()[:16]), "mpris:length": int((self.engine.duration or t.get("durationMs", 0) / 1000) * 1e6), "xesam:title": t["title"], "xesam:artist": [artist], "xesam:album": t["album"], "xesam:albumArtist": [t.get("albumArtist") or t["artist"]], } art = self.player.plex.art_url(t.get("thumb"), 600) or t.get("_cover", "") if art: md["mpris:artUrl"] = art if t.get("index"): md["xesam:trackNumber"] = t["index"] return md def _changed(self): if not self.ok: return now = { "PlaybackStatus": self._adaptor._status(), "LoopStatus": self._adaptor.LoopStatus, "Shuffle": self.player.shuffle, "Volume": float(self.engine.volume), "Metadata": self.metadata(), "CanGoNext": bool(self.player.queue), "CanGoPrevious": bool(self.player.queue), "CanPlay": bool(self.player.queue), } diff = {k: v for k, v in now.items() if k == "Metadata" and _md_key(v) != _md_key(self._last.get(k)) or k != "Metadata" and self._last.get(k) != v} self._last = now if not diff: return msg = QDBusMessage.createSignal(PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged") msg.setArguments([PLAYER_IFACE, diff, []]) self.bus.send(msg) def _md_key(md): if not md: return None return tuple((k, str(v.path()) if isinstance(v, QDBusObjectPath) else repr(v)) for k, v in sorted(md.items()))