diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0bf20ab4..2e5dffb7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,6 +20,7 @@ option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
option(SERVICE_PIPEWIRE "PipeWire service" ON)
+option(SERVICE_MPRIS "Mpris service" ON)
message(STATUS "Quickshell configuration")
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
@@ -34,6 +35,7 @@ message(STATUS " X11: ${X11}")
message(STATUS " Services")
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
+message(STATUS " Mpris: ${SERVICE_MPRIS}")
message(STATUS " Hyprland: ${HYPRLAND}")
if (HYPRLAND)
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
@@ -89,7 +91,7 @@ if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient)
endif()
-if (SERVICE_STATUS_NOTIFIER)
+if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS)
set(DBUS ON)
endif()
diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt
index 091a7ec6..4915762c 100644
--- a/src/services/CMakeLists.txt
+++ b/src/services/CMakeLists.txt
@@ -5,3 +5,7 @@ endif()
if (SERVICE_PIPEWIRE)
add_subdirectory(pipewire)
endif()
+
+if (SERVICE_MPRIS)
+ add_subdirectory(mpris)
+endif()
diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt
new file mode 100644
index 00000000..ffe6d0ad
--- /dev/null
+++ b/src/services/mpris/CMakeLists.txt
@@ -0,0 +1,47 @@
+qt_add_dbus_adaptor(DBUS_INTERFACES
+ org.mpris.MprisWatcher.xml
+ watcher.hpp
+ qs::service::mp::MprisWatcher
+ dbus_watcher
+ MprisWatcherAdaptor
+)
+
+set_source_files_properties(org.mpris.MediaPlayer2.Player.xml PROPERTIES
+ CLASSNAME DBusMprisPlayer
+)
+
+qt_add_dbus_interface(DBUS_INTERFACES
+ org.mpris.MediaPlayer2.Player.xml
+ dbus_player
+)
+
+set_source_files_properties(org.mpris.MprisWatcher.xml PROPERTIES
+ CLASSNAME DBusMprisWatcher
+)
+
+qt_add_dbus_interface(DBUS_INTERFACES
+ org.mpris.MprisWatcher.xml
+ dbus_watcher_interface
+)
+
+qt_add_library(quickshell-service-mpris STATIC
+ qml.cpp
+
+ watcher.cpp
+ player.cpp
+ ${DBUS_INTERFACES}
+)
+
+# dbus headers
+target_include_directories(quickshell-service-mpris PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+
+qt_add_qml_module(quickshell-service-mpris
+ URI Quickshell.Services.Mpris
+ VERSION 0.1
+)
+
+target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus)
+target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin)
+
+qs_pch(quickshell-service-mpris)
+qs_pch(quickshell-service-mprisplugin)
diff --git a/src/services/mpris/org.mpris.MediaPlayer2.Player.xml b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml
new file mode 100644
index 00000000..846c539c
--- /dev/null
+++ b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/services/mpris/org.mpris.MprisWatcher.xml b/src/services/mpris/org.mpris.MprisWatcher.xml
new file mode 100644
index 00000000..ab631558
--- /dev/null
+++ b/src/services/mpris/org.mpris.MprisWatcher.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp
new file mode 100644
index 00000000..b8676bad
--- /dev/null
+++ b/src/services/mpris/player.cpp
@@ -0,0 +1,87 @@
+#include "player.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "../../dbus/properties.hpp"
+#include "dbus_player.h"
+
+using namespace qs::dbus;
+
+Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg);
+
+namespace qs::service::mp {
+
+MprisPlayer::MprisPlayer(const QString& address, QObject* parent)
+ : QObject(parent)
+ , watcherId(address) {
+ // qDBusRegisterMetaType();
+ // qDBusRegisterMetaType();
+ // qDBusRegisterMetaType();
+ // spec is unclear about what exactly an item address is, so account for both
+ auto splitIdx = address.indexOf('/');
+ auto conn = splitIdx == -1 ? address : address.sliced(0, splitIdx);
+ auto path = splitIdx == -1 ? "/org/mpris/MediaPlayer2" : address.sliced(splitIdx);
+
+ this->player = new DBusMprisPlayer(conn, path, QDBusConnection::sessionBus(), this);
+
+ if (!this->player->isValid()) {
+ qCWarning(logMprisPlayer).noquote() << "Cannot create MprisPlayer for" << conn;
+ return;
+ }
+
+ // clang-format off
+ QObject::connect(&this->canControl, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->canGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->canGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->canPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->canPause, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->metadata, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->playbackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->position, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->minimumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->maximumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+
+ QObject::connect(&this->loopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->rate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->shuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+ QObject::connect(&this->volume, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer);
+
+ QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished);
+ // clang-format on
+
+ this->properties.setInterface(this->player);
+ this->properties.updateAllViaGetAll();
+}
+
+bool MprisPlayer::isValid() const { return this->player->isValid(); }
+bool MprisPlayer::isReady() const { return this->mReady; }
+
+void MprisPlayer::setPosition(QDBusObjectPath trackId, qlonglong position) { // NOLINT
+ this->player->SetPosition(trackId, position);
+}
+void MprisPlayer::next() { this->player->Next(); }
+void MprisPlayer::previous() { this->player->Previous(); }
+void MprisPlayer::pause() { this->player->Pause(); }
+void MprisPlayer::playPause() { this->player->PlayPause(); }
+void MprisPlayer::stop() { this->player->Stop(); }
+void MprisPlayer::play() { this->player->Play(); }
+
+void MprisPlayer::onGetAllFinished() {
+ if (this->mReady) return;
+ this->mReady = true;
+ emit this->ready();
+}
+
+void MprisPlayer::updatePlayer() { // NOLINT
+ // TODO: emit signal here
+}
+
+} // namespace qs::service::mp
diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp
new file mode 100644
index 00000000..168006a0
--- /dev/null
+++ b/src/services/mpris/player.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "../../dbus/properties.hpp"
+#include "dbus_player.h"
+
+Q_DECLARE_LOGGING_CATEGORY(logMprisPlayer);
+
+namespace qs::service::mp {
+
+class MprisPlayer;
+
+class MprisPlayer: public QObject {
+ Q_OBJECT;
+
+public:
+ explicit MprisPlayer(const QString& address, QObject* parent = nullptr);
+ QString watcherId; // TODO: maybe can be private CHECK
+
+ void setPosition(QDBusObjectPath trackId, qlonglong position);
+ void next();
+ void previous();
+ void pause();
+ void playPause();
+ void stop();
+ void play();
+
+ [[nodiscard]] bool isValid() const;
+ [[nodiscard]] bool isReady() const;
+
+ // clang-format off
+ dbus::DBusPropertyGroup properties;
+ dbus::DBusProperty canControl {this->properties, "CanControl" };
+ dbus::DBusProperty canGoNext {this->properties, "CanGoNext" };
+ dbus::DBusProperty canGoPrevious {this->properties, "CanGoPrevious" };
+ dbus::DBusProperty canPlay {this->properties, "CanPlay" };
+ dbus::DBusProperty canPause {this->properties, "CanPause" };
+ dbus::DBusProperty metadata {this->properties, "Metadata"};
+ dbus::DBusProperty playbackStatus {this->properties, "PlaybackStatus" };
+ dbus::DBusProperty position {this->properties, "Position" };
+ dbus::DBusProperty minimumRate {this->properties, "MinimumRate" };
+ dbus::DBusProperty maximumRate {this->properties, "MaximumRate" };
+
+ dbus::DBusProperty loopStatus {this->properties, "LoopStatus" };
+ dbus::DBusProperty rate {this->properties, "Rate" };
+ dbus::DBusProperty shuffle {this->properties, "Shuffle" };
+ dbus::DBusProperty volume {this->properties, "Volume" };
+ // clang-format on
+
+signals:
+ void ready();
+
+private slots:
+ void onGetAllFinished();
+ void updatePlayer();
+
+private:
+ DBusMprisPlayer* player = nullptr;
+ bool mReady = false;
+};
+
+} // namespace qs::service::mp
diff --git a/src/services/mpris/qml.cpp b/src/services/mpris/qml.cpp
new file mode 100644
index 00000000..4e99a9f2
--- /dev/null
+++ b/src/services/mpris/qml.cpp
@@ -0,0 +1,180 @@
+#include "qml.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "../../dbus/properties.hpp"
+#include "player.hpp"
+#include "watcher.hpp"
+
+using namespace qs::dbus;
+using namespace qs::service::mp;
+
+Player::Player(qs::service::mp::MprisPlayer* player, QObject* parent)
+ : QObject(parent)
+ , player(player) {
+
+ // clang-format off
+ QObject::connect(&this->player->canControl, &AbstractDBusProperty::changed, this, &Player::canControlChanged);
+ QObject::connect(&this->player->canGoNext, &AbstractDBusProperty::changed, this, &Player::canGoNextChanged);
+ QObject::connect(&this->player->canGoPrevious, &AbstractDBusProperty::changed, this, &Player::canGoPreviousChanged);
+ QObject::connect(&this->player->canPlay, &AbstractDBusProperty::changed, this, &Player::canPlayChanged);
+ QObject::connect(&this->player->canPause, &AbstractDBusProperty::changed, this, &Player::canPauseChanged);
+ QObject::connect(&this->player->metadata, &AbstractDBusProperty::changed, this, &Player::metadataChanged);
+ QObject::connect(&this->player->playbackStatus, &AbstractDBusProperty::changed, this, &Player::playbackStatusChanged);
+ QObject::connect(&this->player->position, &AbstractDBusProperty::changed, this, &Player::positionChanged);
+ QObject::connect(&this->player->minimumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged);
+ QObject::connect(&this->player->maximumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged);
+
+ QObject::connect(&this->player->loopStatus, &AbstractDBusProperty::changed, this, &Player::loopStatusChanged);
+ QObject::connect(&this->player->rate, &AbstractDBusProperty::changed, this, &Player::rateChanged);
+ QObject::connect(&this->player->shuffle, &AbstractDBusProperty::changed, this, &Player::shuffleChanged);
+ QObject::connect(&this->player->volume, &AbstractDBusProperty::changed, this, &Player::volumeChanged);
+ // clang-format on
+}
+
+bool Player::canControl() const {
+ if (this->player == nullptr) return false;
+ return this->player->canControl.get();
+}
+
+bool Player::canGoNext() const {
+ if (this->player == nullptr) return false;
+ return this->player->canGoNext.get();
+}
+
+bool Player::canGoPrevious() const {
+ if (this->player == nullptr) return false;
+ return this->player->canGoPrevious.get();
+}
+
+bool Player::canPlay() const {
+ if (this->player == nullptr) return false;
+ return this->player->canPlay.get();
+}
+
+bool Player::canPause() const {
+ if (this->player == nullptr) return false;
+ return this->player->canPause.get();
+}
+
+QVariantMap Player::metadata() const {
+ if (this->player == nullptr) return {};
+ return this->player->metadata.get();
+}
+
+QString Player::playbackStatus() const {
+ if (this->player == nullptr) return "";
+
+ if (this->player->playbackStatus.get().isEmpty()) return "Unsupported";
+ return this->player->playbackStatus.get();
+}
+
+qlonglong Player::position() const {
+ if (this->player == nullptr) return 0;
+ return this->player->position.get();
+}
+
+double Player::minimumRate() const {
+ if (this->player == nullptr) return 0.0;
+ return this->player->minimumRate.get();
+}
+
+double Player::maximumRate() const {
+ if (this->player == nullptr) return 0.0;
+ return this->player->maximumRate.get();
+}
+
+QString Player::loopStatus() const {
+ if (this->player == nullptr) return "";
+
+ if (this->player->loopStatus.get().isEmpty()) return "Unsupported";
+ return this->player->loopStatus.get();
+}
+
+double Player::rate() const {
+ if (this->player == nullptr) return 0.0;
+ return this->player->rate.get();
+}
+
+bool Player::shuffle() const {
+ if (this->player == nullptr) return false;
+ return this->player->shuffle.get();
+}
+
+double Player::volume() const {
+ if (this->player == nullptr) return 0.0;
+ return this->player->volume.get();
+}
+
+// NOLINTBEGIN
+void Player::setPosition(QDBusObjectPath trackId, qlonglong position) const {
+ this->player->setPosition(trackId, position);
+}
+void Player::next() const { this->player->next(); }
+void Player::previous() const { this->player->previous(); }
+void Player::pause() const { this->player->pause(); }
+void Player::playPause() const { this->player->playPause(); }
+void Player::stop() const { this->player->stop(); }
+void Player::play() const { this->player->play(); }
+// NOLINTEND
+
+Mpris::Mpris(QObject* parent): QObject(parent) {
+ auto* watcher = MprisWatcher::instance();
+
+ // clang-format off
+ QObject::connect(watcher, &MprisWatcher::MprisPlayerRegistered, this, &Mpris::onPlayerRegistered);
+ QObject::connect(watcher, &MprisWatcher::MprisPlayerUnregistered, this, &Mpris::onPlayerUnregistered);
+ // clang-format on
+
+ for (QString& player: watcher->players) {
+ this->mPlayers.push_back(new Player(new MprisPlayer(player), this));
+ }
+}
+
+void Mpris::onPlayerRegistered(const QString& service) {
+ this->mPlayers.push_back(new Player(new MprisPlayer(service), this));
+ emit this->playersChanged();
+}
+
+void Mpris::onPlayerUnregistered(const QString& service) {
+ Player* mprisPlayer = nullptr;
+ MprisPlayer* player = playerWithAddress(players(), service)->player;
+
+ this->mPlayers.removeIf([player, &mprisPlayer](Player* testPlayer) {
+ if (testPlayer->player == player) {
+ mprisPlayer = testPlayer;
+ return true;
+ } else return false;
+ });
+
+ emit this->playersChanged();
+
+ delete mprisPlayer->player;
+ delete mprisPlayer;
+}
+
+QQmlListProperty Mpris::players() {
+ return QQmlListProperty(this, nullptr, &Mpris::playersCount, &Mpris::playerAt);
+}
+
+qsizetype Mpris::playersCount(QQmlListProperty* property) {
+ return reinterpret_cast(property->object)->mPlayers.count(); // NOLINT
+}
+
+Player* Mpris::playerAt(QQmlListProperty* property, qsizetype index) {
+ return reinterpret_cast(property->object)->mPlayers.at(index); // NOLINT
+}
+
+Player* Mpris::playerWithAddress(QQmlListProperty property, const QString& address) {
+ for (Player* player: reinterpret_cast(property.object)->mPlayers) { // NOLINT
+ if (player->player->watcherId == address) {
+ return player;
+ }
+ }
+
+ return nullptr;
+}
diff --git a/src/services/mpris/qml.hpp b/src/services/mpris/qml.hpp
new file mode 100644
index 00000000..4e7896b0
--- /dev/null
+++ b/src/services/mpris/qml.hpp
@@ -0,0 +1,113 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "player.hpp"
+
+
+
+///! Mpris implementation for quickshell
+/// mpris service, get useful information from apps that implement media player fucntionality [mpris spec]
+/// (Beware of misuse of spec, it is just a suggestion for most)
+///
+///
+/// [mpris spec]: https://specifications.freedesktop.org/mpris-spec
+class Player: public QObject {
+ Q_OBJECT;
+ // READ-ONLY
+ Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged);
+ Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged);
+ Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged);
+ Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged);
+ Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged);
+ Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged);
+ Q_PROPERTY(QString playbackStatus READ playbackStatus NOTIFY playbackStatusChanged);
+ Q_PROPERTY(qlonglong position READ position NOTIFY positionChanged);
+ Q_PROPERTY(double minimumRate READ minimumRate NOTIFY minimumRateChanged);
+ Q_PROPERTY(double maximumRate READ maximumRate NOTIFY maximumRateChanged);
+
+ // READ/WRITE - Write isn't implemented thus this need to fixed when that happens.
+ Q_PROPERTY(QString loopStatus READ loopStatus NOTIFY loopStatusChanged);
+ Q_PROPERTY(double rate READ rate NOTIFY rateChanged);
+ Q_PROPERTY(bool shuffle READ shuffle NOTIFY shuffleChanged);
+ Q_PROPERTY(double volume READ volume NOTIFY volumeChanged);
+
+ QML_ELEMENT;
+ QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris");
+
+public:
+ explicit Player(qs::service::mp::MprisPlayer* player, QObject* parent = nullptr);
+
+ // These are all self-explanatory.
+ Q_INVOKABLE void setPosition(QDBusObjectPath trackId, qlonglong position) const;
+ Q_INVOKABLE void next() const;
+ Q_INVOKABLE void previous() const;
+ Q_INVOKABLE void pause() const;
+ Q_INVOKABLE void playPause() const;
+ Q_INVOKABLE void stop() const;
+ Q_INVOKABLE void play() const;
+
+ [[nodiscard]] bool canControl() const;
+ [[nodiscard]] bool canGoNext() const;
+ [[nodiscard]] bool canGoPrevious() const;
+ [[nodiscard]] bool canPlay() const;
+ [[nodiscard]] bool canPause() const;
+ [[nodiscard]] QVariantMap metadata() const;
+ [[nodiscard]] QString playbackStatus() const;
+ [[nodiscard]] qlonglong position() const;
+ [[nodiscard]] double minimumRate() const;
+ [[nodiscard]] double maximumRate() const;
+
+ [[nodiscard]] QString loopStatus() const;
+ [[nodiscard]] double rate() const;
+ [[nodiscard]] bool shuffle() const;
+ [[nodiscard]] double volume() const;
+
+ qs::service::mp::MprisPlayer* player = nullptr;
+
+signals:
+ void canControlChanged();
+ void canGoNextChanged();
+ void canGoPreviousChanged();
+ void canPlayChanged();
+ void canPauseChanged();
+ void metadataChanged();
+ void playbackStatusChanged();
+ void positionChanged();
+ void minimumRateChanged();
+ void maximumRateChanged();
+
+ void loopStatusChanged();
+ void rateChanged();
+ void shuffleChanged();
+ void volumeChanged();
+};
+
+class Mpris: public QObject {
+ Q_OBJECT;
+ Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged);
+ QML_ELEMENT;
+ QML_SINGLETON;
+
+public:
+ explicit Mpris(QObject* parent = nullptr);
+
+ [[nodiscard]] QQmlListProperty players();
+
+signals:
+ void playersChanged();
+
+private slots:
+ void onPlayerRegistered(const QString& service);
+ void onPlayerUnregistered(const QString& service);
+
+private:
+ static qsizetype playersCount(QQmlListProperty* property);
+ static Player* playerAt(QQmlListProperty* property, qsizetype index);
+ static Player* playerWithAddress(QQmlListProperty property, const QString& address);
+
+ QList mPlayers;
+};
diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp
new file mode 100644
index 00000000..2a735f74
--- /dev/null
+++ b/src/services/mpris/watcher.cpp
@@ -0,0 +1,146 @@
+#include "watcher.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mp.watcher", QtWarningMsg);
+
+namespace qs::service::mp {
+
+MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) {
+ new MprisWatcherAdaptor(this);
+
+ qCDebug(logMprisWatcher) << "Starting MprisWatcher";
+
+ auto bus = QDBusConnection::sessionBus();
+
+ if (!bus.isConnected()) {
+ qCWarning(logMprisWatcher) << "Could not connect to DBus. Mpris service will not work.";
+ return;
+ }
+
+ if (!bus.registerObject("/MprisWatcher", this)) {
+ qCWarning(logMprisWatcher) << "Could not register MprisWatcher object with "
+ "DBus. Mpris service will not work.";
+ return;
+ }
+
+ // clang-format off
+ QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &MprisWatcher::onServiceRegistered);
+ QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MprisWatcher::onServiceUnregistered);
+
+ this->serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration);
+ // clang-format on
+
+ this->serviceWatcher.addWatchedService("org.mpris.MediaPlayer2*");
+ this->serviceWatcher.addWatchedService("org.mpris.MprisWatcher");
+ this->serviceWatcher.setConnection(bus);
+
+ this->tryRegister();
+}
+
+void MprisWatcher::tryRegister() { // NOLINT
+ auto bus = QDBusConnection::sessionBus();
+ auto success = bus.registerService("org.mpris.MprisWatcher");
+
+ if (success) {
+ qCDebug(logMprisWatcher) << "Registered watcher at org.mpris.MprisWatcher";
+ emit this->MprisWatcherRegistered();
+ registerExisting(bus); // Register services that already existed before creation.
+ } else {
+ qCDebug(logMprisWatcher) << "Could not register watcher at "
+ "org.mpris.MprisWatcher, presumably because one is "
+ "already registered.";
+ qCDebug(logMprisWatcher
+ ) << "Registration will be attempted again if the active service is unregistered.";
+ }
+}
+
+void MprisWatcher::registerExisting(const QDBusConnection& connection) {
+ QStringList list = connection.interface()->registeredServiceNames();
+ for (const QString& service: list) {
+ if (service.contains("org.mpris.MediaPlayer2")) {
+ qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service;
+ RegisterMprisPlayer(service);
+ }
+ }
+}
+
+void MprisWatcher::onServiceRegistered(const QString& service) {
+ if (service == "org.mpris.MprisWatcher") {
+ qCDebug(logMprisWatcher) << "MprisWatcher";
+ return;
+ } else if (service.contains("org.mpris.MediaPlayer2")) {
+ qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered.";
+ RegisterMprisPlayer(service);
+ } else {
+ qCWarning(logMprisWatcher) << "Got a registration event for a untracked service";
+ }
+}
+
+// TODO: This is getting triggered twice on unregistration, investigate.
+void MprisWatcher::onServiceUnregistered(const QString& service) {
+ if (service == "org.mpris.MprisWatcher") {
+ qCDebug(logMprisWatcher) << "Active MprisWatcher unregistered, attempting registration";
+ this->tryRegister();
+ return;
+ } else {
+ QString qualifiedPlayer;
+ this->players.removeIf([&](const QString& player) {
+ if (QString::compare(player, service) == 0) {
+ qualifiedPlayer = player;
+ return true;
+ } else return false;
+ });
+
+ if (!qualifiedPlayer.isEmpty()) {
+ qCDebug(logMprisWatcher).noquote()
+ << "Unregistered MprisPlayer" << qualifiedPlayer << "from watcher";
+
+ emit this->MprisPlayerUnregistered(qualifiedPlayer);
+ } else {
+ qCWarning(logMprisWatcher).noquote()
+ << "Got service unregister event for untracked service" << service;
+ }
+ }
+
+ this->serviceWatcher.removeWatchedService(service);
+}
+
+QList MprisWatcher::registeredPlayers() const { return this->players; }
+
+void MprisWatcher::RegisterMprisPlayer(const QString& player) {
+ if (this->players.contains(player)) {
+ qCDebug(logMprisWatcher).noquote()
+ << "Skipping duplicate registration of MprisPlayer" << player << "to watcher";
+ return;
+ }
+
+ if (!QDBusConnection::sessionBus().interface()->serviceOwner(player).isValid()) {
+ qCWarning(logMprisWatcher).noquote()
+ << "Ignoring invalid MprisPlayer registration of" << player << "to watcher";
+ return;
+ }
+
+ this->serviceWatcher.addWatchedService(player);
+ this->players.push_back(player);
+
+ qCDebug(logMprisWatcher).noquote() << "Registered MprisPlayer" << player << "to watcher";
+
+ emit this->MprisPlayerRegistered(player);
+}
+
+MprisWatcher* MprisWatcher::instance() {
+ static MprisWatcher* instance = nullptr; // NOLINT
+ if (instance == nullptr) instance = new MprisWatcher();
+ return instance;
+}
+
+} // namespace qs::service::mp
diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp
new file mode 100644
index 00000000..b3e0bdeb
--- /dev/null
+++ b/src/services/mpris/watcher.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(logMprisWatcher);
+
+namespace qs::service::mp {
+
+class MprisWatcher
+ : public QObject
+ , protected QDBusContext {
+ Q_OBJECT;
+ Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion);
+ Q_PROPERTY(QList RegisteredMprisPlayers READ registeredPlayers);
+
+public:
+ explicit MprisWatcher(QObject* parent = nullptr);
+
+ void tryRegister();
+ void registerExisting(const QDBusConnection &connection);
+
+ [[nodiscard]] qint32 protocolVersion() const { return 0; } // NOLINT
+ [[nodiscard]] QList registeredPlayers() const;
+
+ // NOLINTBEGIN
+ void RegisterMprisPlayer(const QString& player);
+ // NOLINTEND
+
+ static MprisWatcher* instance();
+ QList players;
+
+signals:
+ // NOLINTBEGIN
+ void MprisWatcherRegistered();
+ void MprisPlayerRegistered(const QString& service);
+ void MprisPlayerUnregistered(const QString& service);
+ // NOLINTEND
+
+private slots:
+ void onServiceRegistered(const QString& service);
+ void onServiceUnregistered(const QString& service);
+
+private:
+ QDBusServiceWatcher serviceWatcher;
+};
+
+} // namespace qs::service::mp