feat: mpris

This commit is contained in:
kossLAN 2024-05-19 21:09:16 -04:00
parent 908ba3eef5
commit 9c2d276902
13 changed files with 909 additions and 1 deletions

View file

@ -19,6 +19,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}")
@ -32,6 +33,7 @@ endif ()
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}")
@ -87,7 +89,7 @@ if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient)
endif()
if (SERVICE_STATUS_NOTIFIER)
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS)
set(DBUS ON)
endif()

View file

@ -5,3 +5,7 @@ endif()
if (SERVICE_PIPEWIRE)
add_subdirectory(pipewire)
endif()
if (SERVICE_MPRIS)
add_subdirectory(mpris)
endif()

View file

@ -0,0 +1,49 @@
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
#[[ INCLUDE dbus_item_types.hpp ]]
)
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_item_types.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)

View file

@ -0,0 +1,121 @@
#include "dbus_item_types.hpp"
#include <qdbusargument.h>
#include <qdbusextratypes.h>
#include <qdebug.h>
#include <qendian.h>
#include <qimage.h>
#include <qlogging.h>
#include <qmetatype.h>
#include <qsysinfo.h>
#include <qtypes.h>
QImage DBusMpIconPixmap::createImage() const {
// fix byte order if on a little endian machine
if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
auto* newbuf = new quint32[this->data.size()];
const auto* oldbuf = reinterpret_cast<const quint32*>(this->data.data()); // NOLINT
for (uint i = 0; i < this->data.size() / sizeof(quint32); ++i) {
newbuf[i] = qFromBigEndian(oldbuf[i]); // NOLINT
}
return QImage(
reinterpret_cast<const uchar*>(newbuf), // NOLINT
this->width,
this->height,
QImage::Format_ARGB32,
[](void* ptr) { delete reinterpret_cast<quint32*>(ptr); }, // NOLINT
newbuf
);
} else {
return QImage(
reinterpret_cast<const uchar*>(this->data.data()), // NOLINT
this->width,
this->height,
QImage::Format_ARGB32
);
}
}
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMpIconPixmap& pixmap) {
argument.beginStructure();
argument >> pixmap.width;
argument >> pixmap.height;
argument >> pixmap.data;
argument.endStructure();
return argument;
}
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMpIconPixmap& pixmap) {
argument.beginStructure();
argument << pixmap.width;
argument << pixmap.height;
argument << pixmap.data;
argument.endStructure();
return argument;
}
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMpIconPixmapList& pixmaps) {
argument.beginArray();
pixmaps.clear();
while (!argument.atEnd()) {
pixmaps.append(qdbus_cast<DBusMpIconPixmap>(argument));
}
argument.endArray();
return argument;
}
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMpIconPixmapList& pixmaps) {
argument.beginArray(qMetaTypeId<DBusMpIconPixmap>());
for (const auto& pixmap: pixmaps) {
argument << pixmap;
}
argument.endArray();
return argument;
}
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMpTooltip& tooltip) {
argument.beginStructure();
argument >> tooltip.icon;
argument >> tooltip.iconPixmaps;
argument >> tooltip.title;
argument >> tooltip.description;
argument.endStructure();
return argument;
}
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMpTooltip& tooltip) {
argument.beginStructure();
argument << tooltip.icon;
argument << tooltip.iconPixmaps;
argument << tooltip.title;
argument << tooltip.description;
argument.endStructure();
return argument;
}
QDebug operator<<(QDebug debug, const DBusMpIconPixmap& pixmap) {
debug.nospace() << "DBusMpIconPixmap(width=" << pixmap.width << ", height=" << pixmap.height
<< ")";
return debug;
}
QDebug operator<<(QDebug debug, const DBusMpTooltip& tooltip) {
debug.nospace() << "DBusMpTooltip(title=" << tooltip.title
<< ", description=" << tooltip.description << ", icon=" << tooltip.icon
<< ", iconPixmaps=" << tooltip.iconPixmaps << ")";
return debug;
}
QDebug operator<<(QDebug debug, const QDBusObjectPath& path) {
debug.nospace() << "QDBusObjectPath(" << path.path() << ")";
return debug;
}

View file

@ -0,0 +1,35 @@
#pragma once
#include <qdbusargument.h>
#include <qdbusextratypes.h>
#include <qdebug.h>
#include <qlist.h>
struct DBusMpIconPixmap {
qint32 width = 0;
qint32 height = 0;
QByteArray data;
// valid only for the lifetime of the pixmap
[[nodiscard]] QImage createImage() const;
};
using DBusMpIconPixmapList = QList<DBusMpIconPixmap>;
struct DBusMpTooltip {
QString icon;
DBusMpIconPixmapList iconPixmaps;
QString title;
QString description;
};
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMpIconPixmap& pixmap);
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMpIconPixmap& pixmap);
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMpIconPixmapList& pixmaps);
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMpIconPixmapList& pixmaps);
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMpTooltip& tooltip);
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMpTooltip& tooltip);
QDebug operator<<(QDebug debug, const DBusMpIconPixmap& pixmap);
QDebug operator<<(QDebug debug, const DBusMpTooltip& tooltip);
QDebug operator<<(QDebug debug, const QDBusObjectPath& path);

View file

@ -0,0 +1,33 @@
<node>
<interface name='org.mpris.MediaPlayer2.Player'>
<property name='CanControl' type='b' access='read' />
<property name='CanGoNext' type='b' access='read' />
<property name='CanGoPrevious' type='b' access='read' />
<property name='CanPlay' type='b' access='read' />
<property name='CanPause' type='b' access='read'/>
<property name='Metadata' type='a{sv}' access='read'>
<annotation name='org.qtproject.QtDBus.QtTypeName' value='QVariantMap'/>
</property>
<property name='PlaybackStatus' type='s' access='read'/>
<property name='Shuffle' type='b' access='readwrite' />
<property name='LoopStatus' type='s' access='readwrite' />
<property name='Rate' type='d' access='readwrite' />
<property name='Volume' type='d' access='readwrite' />
<property name='Position' type='x' access='read' />
<property name='MinimumRate' type='d' access='read' />
<property name='MaximumRate' type='d' access='read' />
<method name='SetPosition'>
<arg direction='in' type='o' name='TrackId'/>
<arg direction='in' type='x' name='Position'/>
</method>
<method name='Seek'>
<arg direction='in' type='x' name='Offset' />
</method>
<method name='PlayPause' />
<method name='Next' />
<method name='Previous' />
<method name='Stop' />
<method name='Play' />
<method name='Pause' />
</interface>
</node>

View file

@ -0,0 +1,16 @@
<node>
<interface name='org.mpris.MprisWatcher'>
<property name='ProtocolVersion' type='i' access='read'/>
<property name='RegisteredMprisPlayers' type='as' access='read'/>
<method name='RegisterMprisPlayer'>
<arg name='service' type='s' direction='in'/>
</method>
<signal name='MprisPlayerRegistered'>
<arg type='s' direction='out' name='service'/>
</signal>
<signal name='MprisPlayerUnregistered'>
<arg type='s' direction='out' name='service'/>
</signal>
</interface>
</node>

View file

@ -0,0 +1,88 @@
#include "player.hpp"
#include <qdbusextratypes.h>
#include <qdbusmetatype.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qsize.h>
#include <qstring.h>
#include <qtypes.h>
#include "../../dbus/properties.hpp"
#include "dbus_item_types.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<DBusSniIconPixmap>();
// qDBusRegisterMetaType<DBusSniIconPixmapList>();
// qDBusRegisterMetaType<DBusSniTooltip>();
// 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

View file

@ -0,0 +1,68 @@
#pragma once
#include <qdbusextratypes.h>
#include <qdbuspendingcall.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtypes.h>
#include "../../dbus/properties.hpp"
#include "dbus_player.h"
#include "dbus_item_types.hpp"
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<bool> canControl {this->properties, "CanControl" };
dbus::DBusProperty<bool> canGoNext {this->properties, "CanGoNext" };
dbus::DBusProperty<bool> canGoPrevious {this->properties, "CanGoPrevious" };
dbus::DBusProperty<bool> canPlay {this->properties, "CanPlay" };
dbus::DBusProperty<bool> canPause {this->properties, "CanPause" };
dbus::DBusProperty<QVariantMap> metadata {this->properties, "Metadata"};
dbus::DBusProperty<QString> playbackStatus {this->properties, "PlaybackStatus" };
dbus::DBusProperty<qlonglong> position {this->properties, "Position" };
dbus::DBusProperty<double> minimumRate {this->properties, "MinimumRate" };
dbus::DBusProperty<double> maximumRate {this->properties, "MaximumRate" };
dbus::DBusProperty<QString> loopStatus {this->properties, "LoopStatus" };
dbus::DBusProperty<double> rate {this->properties, "Rate" };
dbus::DBusProperty<bool> shuffle {this->properties, "Shuffle" };
dbus::DBusProperty<double> 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

180
src/services/mpris/qml.cpp Normal file
View file

@ -0,0 +1,180 @@
#include "qml.hpp"
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtypes.h>
#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<Player> Mpris::players() {
return QQmlListProperty<Player>(this, nullptr, &Mpris::playersCount, &Mpris::playerAt);
}
qsizetype Mpris::playersCount(QQmlListProperty<Player>* property) {
return reinterpret_cast<Mpris*>(property->object)->mPlayers.count(); // NOLINT
}
Player* Mpris::playerAt(QQmlListProperty<Player>* property, qsizetype index) {
return reinterpret_cast<Mpris*>(property->object)->mPlayers.at(index); // NOLINT
}
Player* Mpris::playerWithAddress(QQmlListProperty<Player> property, const QString& address) {
for (Player* player: reinterpret_cast<Mpris*>(property.object)->mPlayers) { // NOLINT
if (player->player->watcherId == address) {
return player;
}
}
return nullptr;
}

113
src/services/mpris/qml.hpp Normal file
View file

@ -0,0 +1,113 @@
#pragma once
#include <qdbusextratypes.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtypes.h>
#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<Player> players READ players NOTIFY playersChanged);
QML_ELEMENT;
QML_SINGLETON;
public:
explicit Mpris(QObject* parent = nullptr);
[[nodiscard]] QQmlListProperty<Player> players();
signals:
void playersChanged();
private slots:
void onPlayerRegistered(const QString& service);
void onPlayerUnregistered(const QString& service);
private:
static qsizetype playersCount(QQmlListProperty<Player>* property);
static Player* playerAt(QQmlListProperty<Player>* property, qsizetype index);
static Player* playerWithAddress(QQmlListProperty<Player> property, const QString& address);
QList<Player*> mPlayers;
};

View file

@ -0,0 +1,146 @@
#include "watcher.hpp"
#include <dbus_watcher.h>
#include <qdbusconnection.h>
#include <qdbusconnectioninterface.h>
#include <qdbusservicewatcher.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
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<QString> 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

View file

@ -0,0 +1,53 @@
#pragma once
#include <qdbuscontext.h>
#include <qdbusinterface.h>
#include <qdbusservicewatcher.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtypes.h>
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<QString> 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<QString> registeredPlayers() const;
// NOLINTBEGIN
void RegisterMprisPlayer(const QString& player);
// NOLINTEND
static MprisWatcher* instance();
QList<QString> 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