From 3b6d1c3bd874619eb7e6186a03338ee38e7fa593 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Sun, 19 May 2024 21:09:16 -0400 Subject: [PATCH] feat: mpris --- CMakeLists.txt | 4 +- src/services/CMakeLists.txt | 4 + src/services/mpris/CMakeLists.txt | 47 +++++ .../mpris/org.mpris.MediaPlayer2.Player.xml | 33 ++++ src/services/mpris/org.mpris.MprisWatcher.xml | 16 ++ src/services/mpris/player.cpp | 87 +++++++++ src/services/mpris/player.hpp | 67 +++++++ src/services/mpris/qml.cpp | 180 ++++++++++++++++++ src/services/mpris/qml.hpp | 113 +++++++++++ src/services/mpris/watcher.cpp | 146 ++++++++++++++ src/services/mpris/watcher.hpp | 53 ++++++ 11 files changed, 749 insertions(+), 1 deletion(-) create mode 100644 src/services/mpris/CMakeLists.txt create mode 100644 src/services/mpris/org.mpris.MediaPlayer2.Player.xml create mode 100644 src/services/mpris/org.mpris.MprisWatcher.xml create mode 100644 src/services/mpris/player.cpp create mode 100644 src/services/mpris/player.hpp create mode 100644 src/services/mpris/qml.cpp create mode 100644 src/services/mpris/qml.hpp create mode 100644 src/services/mpris/watcher.cpp create mode 100644 src/services/mpris/watcher.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bf20ab..2e5dffb 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 091a7ec..4915762 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 0000000..ffe6d0a --- /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 0000000..846c539 --- /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 0000000..ab63155 --- /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 0000000..b8676ba --- /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 0000000..168006a --- /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 0000000..4e99a9f --- /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 0000000..4e7896b --- /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 0000000..2a735f7 --- /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 0000000..b3e0bde --- /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