diff --git a/CMakeLists.txt b/CMakeLists.txt index 30f991e8..12824965 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,7 +33,7 @@ endif () message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") -message(STATUS " Mpris: ${SERVICE_MPRIS}") +message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index ab485c45..b07919ad 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -188,9 +188,9 @@ public: dbus::DBusPropertyGroup properties; dbus::DBusProperty version {this->properties, "Version"}; - dbus::DBusProperty textDirection {this->properties, "TextDirection", "", false}; + dbus::DBusProperty textDirection {this->properties, "TextDirection"}; dbus::DBusProperty status {this->properties, "Status"}; - dbus::DBusProperty iconThemePath {this->properties, "IconThemePath", {}, false}; + dbus::DBusProperty iconThemePath {this->properties, "IconThemePath"}; void prepareToShow(qint32 item, bool sendOpened); void updateLayout(qint32 parent, qint32 depth); diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 7dac84ab..1e5e0bd1 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -112,8 +112,6 @@ void asyncReadPropertyInternal( } void AbstractDBusProperty::tryUpdate(const QVariant& variant) { - this->mExists = true; - auto error = this->read(variant); if (error.isValid()) { qCWarning(logDbusProperties).noquote() @@ -161,44 +159,6 @@ void AbstractDBusProperty::update() { } } -void AbstractDBusProperty::write() { - if (this->group == nullptr) { - qFatal(logDbusProperties) << "Tried to write dbus property" << this->name - << "which is not attached to a group"; - } else { - const QString propStr = this->toString(); - - if (this->group->interface == nullptr) { - qFatal(logDbusProperties).noquote() - << "Tried to write property" << propStr << "of a disconnected interface"; - } - - qCDebug(logDbusProperties).noquote() << "Writing property" << propStr; - - auto pendingCall = this->group->propertyInterface->Set( - this->group->interface->interface(), - this->name, - QDBusVariant(this->serialize()) - ); - - auto* call = new QDBusPendingCallWatcher(pendingCall, this); - - auto responseCallback = [propStr](QDBusPendingCallWatcher* call) { - const QDBusPendingReply<> reply = *call; - - if (reply.isError()) { - qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr; - qCWarning(logDbusProperties) << reply.error(); - } - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); - } -} - -bool AbstractDBusProperty::exists() const { return this->mExists; } - QString AbstractDBusProperty::toString() const { const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString(); return group + ':' + this->name; @@ -272,7 +232,7 @@ void DBusPropertyGroup::updateAllViaGetAll() { } else { qCDebug(logDbusProperties).noquote() << "Received GetAll property set for" << this->toString(); - this->updatePropertySet(reply.value(), true); + this->updatePropertySet(reply.value()); } delete call; @@ -282,7 +242,7 @@ void DBusPropertyGroup::updateAllViaGetAll() { QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); } -void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) { +void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) { for (const auto [name, value]: properties.asKeyValueRange()) { auto prop = std::find_if( this->properties.begin(), @@ -291,21 +251,11 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool co ); if (prop == this->properties.end()) { - qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" - << this->toString(); + qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this; } else { (*prop)->tryUpdate(value); } } - - if (complainMissing) { - for (const auto* prop: this->properties) { - if (prop->required && !properties.contains(prop->name)) { - qCWarning(logDbusProperties) - << prop->name << "missing from property set for" << this->toString(); - } - } - } } QString DBusPropertyGroup::toString() const { @@ -341,7 +291,7 @@ void DBusPropertyGroup::onPropertiesChanged( } } - this->updatePropertySet(changedProperties, false); + this->updatePropertySet(changedProperties); } } // namespace qs::dbus diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index e24d23fb..3aac07f6 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -79,31 +79,22 @@ class AbstractDBusProperty: public QObject { Q_OBJECT; public: - explicit AbstractDBusProperty( - QString name, - const QMetaType& type, - bool required, - QObject* parent = nullptr - ) + explicit AbstractDBusProperty(QString name, const QMetaType& type, QObject* parent = nullptr) : QObject(parent) , name(std::move(name)) - , type(type) - , required(required) {} + , type(type) {} - [[nodiscard]] bool exists() const; [[nodiscard]] QString toString() const; [[nodiscard]] virtual QString valueString() = 0; public slots: void update(); - void write(); signals: void changed(); protected: virtual QDBusError read(const QVariant& variant) = 0; - virtual QVariant serialize() = 0; private: void tryUpdate(const QVariant& variant); @@ -112,8 +103,6 @@ private: QString name; QMetaType type; - bool required; - bool mExists = false; friend class DBusPropertyGroup; }; @@ -144,7 +133,7 @@ private slots: ); private: - void updatePropertySet(const QVariantMap& properties, bool complainMissing); + void updatePropertySet(const QVariantMap& properties); DBusPropertiesInterface* propertyInterface = nullptr; QDBusAbstractInterface* interface = nullptr; @@ -156,23 +145,17 @@ private: template class DBusProperty: public AbstractDBusProperty { public: - explicit DBusProperty( - QString name, - T value = T(), - bool required = true, - QObject* parent = nullptr - ) - : AbstractDBusProperty(std::move(name), QMetaType::fromType(), required, parent) + explicit DBusProperty(QString name, QObject* parent = nullptr, T value = T()) + : AbstractDBusProperty(std::move(name), QMetaType::fromType(), parent) , value(std::move(value)) {} explicit DBusProperty( DBusPropertyGroup& group, QString name, - T value = T(), - bool required = true, - QObject* parent = nullptr + QObject* parent = nullptr, + T value = T() ) - : DBusProperty(std::move(name), std::move(value), required, parent) { + : DBusProperty(std::move(name), parent, std::move(value)) { group.attachProperty(this); } @@ -182,7 +165,7 @@ public: return str; } - [[nodiscard]] const T& get() const { return this->value; } + [[nodiscard]] T get() const { return this->value; } void set(T value) { this->value = std::move(value); @@ -200,8 +183,6 @@ protected: return result.error; } - QVariant serialize() override { return QVariant::fromValue(this->value); } - private: T value; diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt index 3ee96061..ffe6d0ad 100644 --- a/src/services/mpris/CMakeLists.txt +++ b/src/services/mpris/CMakeLists.txt @@ -1,6 +1,13 @@ +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 - NO_NAMESPACE TRUE + CLASSNAME DBusMprisPlayer ) qt_add_dbus_interface(DBUS_INTERFACES @@ -8,19 +15,20 @@ qt_add_dbus_interface(DBUS_INTERFACES dbus_player ) -set_source_files_properties(org.mpris.MediaPlayer2.xml PROPERTIES - CLASSNAME DBusMprisPlayerApp - NO_NAMESPACE TRUE +set_source_files_properties(org.mpris.MprisWatcher.xml PROPERTIES + CLASSNAME DBusMprisWatcher ) qt_add_dbus_interface(DBUS_INTERFACES - org.mpris.MediaPlayer2.xml - dbus_player_app + org.mpris.MprisWatcher.xml + dbus_watcher_interface ) qt_add_library(quickshell-service-mpris STATIC - player.cpp + qml.cpp + watcher.cpp + player.cpp ${DBUS_INTERFACES} ) diff --git a/src/services/mpris/module.md b/src/services/mpris/module.md deleted file mode 100644 index e2256e8c..00000000 --- a/src/services/mpris/module.md +++ /dev/null @@ -1,7 +0,0 @@ -name = "Quickshell.Services.Mpris" -description = "Mpris Service" -headers = [ - "player.hpp", - "watcher.hpp", -] ------ diff --git a/src/services/mpris/org.mpris.MediaPlayer2.Player.xml b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml index a0095231..846c539c 100644 --- a/src/services/mpris/org.mpris.MediaPlayer2.Player.xml +++ b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml @@ -1,24 +1,33 @@ - - - + + + + + + + + + + + + + + + + + + + + - - - + + - - - - - - - - - - - - - + + + + + + diff --git a/src/services/mpris/org.mpris.MediaPlayer2.xml b/src/services/mpris/org.mpris.MediaPlayer2.xml deleted file mode 100644 index fa880d08..00000000 --- a/src/services/mpris/org.mpris.MediaPlayer2.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - 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 index 3b0c7463..b8676bad 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -1,427 +1,87 @@ #include "player.hpp" -#include -#include -#include #include +#include #include #include +#include #include +#include #include -#include #include #include "../../dbus/properties.hpp" #include "dbus_player.h" -#include "dbus_player_app.h" using namespace qs::dbus; -namespace qs::service::mpris { - Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); -QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) { - switch (status) { - case MprisPlaybackState::Stopped: return "Stopped"; - case MprisPlaybackState::Playing: return "Playing"; - case MprisPlaybackState::Paused: return "Paused"; - default: return "Unknown Status"; - } -} +namespace qs::service::mp { -QString MprisLoopState::toString(MprisLoopState::Enum status) { - switch (status) { - case MprisLoopState::None: return "None"; - case MprisLoopState::Track: return "Track"; - case MprisLoopState::Playlist: return "Playlist"; - default: return "Unknown Status"; - } -} +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); -MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(parent) { - this->app = new DBusMprisPlayerApp( - address, - "/org/mpris/MediaPlayer2", - QDBusConnection::sessionBus(), - this - ); + this->player = new DBusMprisPlayer(conn, path, QDBusConnection::sessionBus(), this); - this->player = - new DBusMprisPlayer(address, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this); - - if (!this->player->isValid() || !this->app->isValid()) { - qCWarning(logMprisPlayer) << "Cannot create MprisPlayer for" << address; + if (!this->player->isValid()) { + qCWarning(logMprisPlayer).noquote() << "Cannot create MprisPlayer for" << conn; return; } // clang-format off - QObject::connect(&this->pCanQuit, &AbstractDBusProperty::changed, this, &MprisPlayer::canQuitChanged); - QObject::connect(&this->pCanRaise, &AbstractDBusProperty::changed, this, &MprisPlayer::canRaiseChanged); - QObject::connect(&this->pCanSetFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::canSetFullscreenChanged); - QObject::connect(&this->pIdentity, &AbstractDBusProperty::changed, this, &MprisPlayer::identityChanged); - QObject::connect(&this->pFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::fullscreenChanged); - QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged); - QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged); + 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->pCanControl, &AbstractDBusProperty::changed, this, &MprisPlayer::canControlChanged); - QObject::connect(&this->pCanSeek, &AbstractDBusProperty::changed, this, &MprisPlayer::canSeekChanged); - QObject::connect(&this->pCanGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoNextChanged); - QObject::connect(&this->pCanGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoPreviousChanged); - QObject::connect(&this->pCanPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::canPlayChanged); - QObject::connect(&this->pCanPause, &AbstractDBusProperty::changed, this, &MprisPlayer::canPauseChanged); - QObject::connect(&this->pPosition, &AbstractDBusProperty::changed, this, &MprisPlayer::onPositionChanged); - QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek); - QObject::connect(&this->pVolume, &AbstractDBusProperty::changed, this, &MprisPlayer::volumeChanged); - QObject::connect(&this->pMetadata, &AbstractDBusProperty::changed, this, &MprisPlayer::onMetadataChanged); - QObject::connect(&this->pPlaybackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onPlaybackStatusChanged); - QObject::connect(&this->pLoopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onLoopStatusChanged); - QObject::connect(&this->pRate, &AbstractDBusProperty::changed, this, &MprisPlayer::rateChanged); - QObject::connect(&this->pMinRate, &AbstractDBusProperty::changed, this, &MprisPlayer::minRateChanged); - QObject::connect(&this->pMaxRate, &AbstractDBusProperty::changed, this, &MprisPlayer::maxRateChanged); - QObject::connect(&this->pShuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::shuffleChanged); + 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->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); - - // Ensure user triggered position updates can update length. - QObject::connect(this, &MprisPlayer::positionChanged, this, &MprisPlayer::onExportedPositionChanged); + QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); // clang-format on - this->appProperties.setInterface(this->app); - this->playerProperties.setInterface(this->player); - this->appProperties.updateAllViaGetAll(); - this->playerProperties.updateAllViaGetAll(); -} - -void MprisPlayer::raise() { - if (!this->canRaise()) { - qWarning() << "Cannot call raise() on" << this << "because canRaise is false."; - return; - } - - this->app->Raise(); -} - -void MprisPlayer::quit() { - if (!this->canQuit()) { - qWarning() << "Cannot call quit() on" << this << "because canQuit is false."; - return; - } - - this->app->Quit(); -} - -void MprisPlayer::openUri(const QString& uri) { this->player->OpenUri(uri); } - -void MprisPlayer::next() { - if (!this->canGoNext()) { - qWarning() << "Cannot call next() on" << this << "because canGoNext is false."; - return; - } - - this->player->Next(); -} - -void MprisPlayer::previous() { - if (!this->canGoPrevious()) { - qWarning() << "Cannot call previous() on" << this << "because canGoPrevious is false."; - return; - } - - this->player->Previous(); -} - -void MprisPlayer::seek(qreal offset) { - if (!this->canSeek()) { - qWarning() << "Cannot call seek() on" << this << "because canSeek is false."; - return; - } - - auto target = static_cast(offset * 1000) * 1000; - this->player->Seek(target); + this->properties.setInterface(this->player); + this->properties.updateAllViaGetAll(); } bool MprisPlayer::isValid() const { return this->player->isValid(); } -QString MprisPlayer::address() const { return this->player->service(); } +bool MprisPlayer::isReady() const { return this->mReady; } -bool MprisPlayer::canControl() const { return this->pCanControl.get(); } -bool MprisPlayer::canPlay() const { return this->canControl() && this->pCanPlay.get(); } -bool MprisPlayer::canPause() const { return this->canControl() && this->pCanPause.get(); } -bool MprisPlayer::canSeek() const { return this->canControl() && this->pCanSeek.get(); } -bool MprisPlayer::canGoNext() const { return this->canControl() && this->pCanGoNext.get(); } -bool MprisPlayer::canGoPrevious() const { return this->canControl() && this->pCanGoPrevious.get(); } -bool MprisPlayer::canQuit() const { return this->pCanQuit.get(); } -bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); } -bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); } - -QString MprisPlayer::identity() const { return this->pIdentity.get(); } - -qlonglong MprisPlayer::positionMs() const { - if (!this->positionSupported()) return 0; // unsupported - if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0; - - auto paused = this->mPlaybackState == MprisPlaybackState::Paused; - auto time = paused ? this->pausedTime : QDateTime::currentDateTime(); - auto offset = time - this->lastPositionTimestamp; - auto rateMul = static_cast(this->pRate.get() * 1000); - offset = (offset * rateMul) / 1000; - - return (this->pPosition.get() / 1000) + offset.count(); +void MprisPlayer::setPosition(QDBusObjectPath trackId, qlonglong position) { // NOLINT + this->player->SetPosition(trackId, position); } - -qreal MprisPlayer::position() const { - if (!this->positionSupported()) return 0; // unsupported - if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0; - - return static_cast(this->positionMs()) / 1000.0; // NOLINT -} - -bool MprisPlayer::positionSupported() const { return this->pPosition.exists(); } - -void MprisPlayer::setPosition(qreal position) { - if (this->pPosition.get() == -1) { - qWarning() << "Cannot set position of" << this << "because position is not supported."; - return; - } - - if (!this->canSeek()) { - qWarning() << "Cannot set position of" << this << "because canSeek is false."; - return; - } - - auto target = static_cast(position * 1000) * 1000; - this->pPosition.set(target); - - if (!this->mTrackId.isEmpty()) { - this->player->SetPosition(QDBusObjectPath(this->mTrackId), target); - return; - } else { - auto pos = this->positionMs() * 1000; - this->player->Seek(target - pos); - } -} - -void MprisPlayer::onPositionChanged() { - const bool firstChange = !this->lastPositionTimestamp.isValid(); - this->lastPositionTimestamp = QDateTime::currentDateTimeUtc(); - emit this->positionChanged(); - if (firstChange) emit this->positionSupportedChanged(); -} - -void MprisPlayer::onExportedPositionChanged() { - if (!this->lengthSupported()) emit this->lengthChanged(); -} - -void MprisPlayer::onSeek(qlonglong time) { this->pPosition.set(time); } - -qreal MprisPlayer::length() const { - if (this->mLength == -1) { - return this->position(); // unsupported - } else { - return static_cast(this->mLength / 1000) / 1000; // NOLINT - } -} - -bool MprisPlayer::lengthSupported() const { return this->mLength != -1; } - -qreal MprisPlayer::volume() const { return this->pVolume.get(); } -bool MprisPlayer::volumeSupported() const { return this->pVolume.exists(); } - -void MprisPlayer::setVolume(qreal volume) { - if (!this->canControl()) { - qWarning() << "Cannot set volume of" << this << "because canControl is false."; - return; - } - - if (!this->volumeSupported()) { - qWarning() << "Cannot set volume of" << this << "because volume is not supported."; - return; - } - - this->pVolume.set(volume); - this->pVolume.write(); -} - -QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); } - -void MprisPlayer::onMetadataChanged() { - auto lengthVariant = this->pMetadata.get().value("mpris:length"); - qlonglong length = -1; - if (lengthVariant.isValid() && lengthVariant.canConvert()) { - length = lengthVariant.value(); - } - - if (length != this->mLength) { - this->mLength = length; - emit this->lengthChanged(); - } - - auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); - if (trackidVariant.isValid() && trackidVariant.canConvert()) { - this->mTrackId = trackidVariant.value(); - this->onSeek(0); - } - - emit this->metadataChanged(); -} - -MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } - -void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { - if (playbackState == this->mPlaybackState) return; - - switch (playbackState) { - case MprisPlaybackState::Stopped: - if (!this->canControl()) { - qWarning() << "Cannot set playbackState of" << this - << "to Stopped because canControl is false."; - return; - } - - this->player->Stop(); - break; - case MprisPlaybackState::Playing: - if (!this->canPlay()) { - qWarning() << "Cannot set playbackState of" << this << "to Playing because canPlay is false."; - return; - } - - this->player->Play(); - break; - case MprisPlaybackState::Paused: - if (!this->canPause()) { - qWarning() << "Cannot set playbackState of" << this << "to Paused because canPause is false."; - return; - } - - this->player->Pause(); - break; - default: - qWarning() << "Cannot set playbackState of" << this << "to unknown value" << playbackState; - return; - } -} - -void MprisPlayer::onPlaybackStatusChanged() { - const auto& status = this->pPlaybackStatus.get(); - - if (status == "Playing") { - this->mPlaybackState = MprisPlaybackState::Playing; - } else if (status == "Paused") { - this->mPlaybackState = MprisPlaybackState::Paused; - } else if (status == "Stopped") { - this->mPlaybackState = MprisPlaybackState::Stopped; - } else { - this->mPlaybackState = MprisPlaybackState::Stopped; - qWarning() << "Received unexpected PlaybackStatus for" << this << status; - } - - emit this->playbackStateChanged(); -} - -MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; } -bool MprisPlayer::loopSupported() const { return this->pLoopStatus.exists(); } - -void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) { - if (!this->canControl()) { - qWarning() << "Cannot set loopState of" << this << "because canControl is false."; - return; - } - - if (!this->loopSupported()) { - qWarning() << "Cannot set loopState of" << this << "because loop state is not supported."; - return; - } - - if (loopState == this->mLoopState) return; - - QString loopStatusStr; - switch (loopState) { - case MprisLoopState::None: loopStatusStr = "None"; break; - case MprisLoopState::Track: loopStatusStr = "Track"; break; - case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break; - default: - qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState; - return; - } - - this->pLoopStatus.set(loopStatusStr); - this->pLoopStatus.write(); -} - -void MprisPlayer::onLoopStatusChanged() { - const auto& status = this->pLoopStatus.get(); - - if (status == "None") { - this->mLoopState = MprisLoopState::None; - } else if (status == "Track") { - this->mLoopState = MprisLoopState::Track; - } else if (status == "Playlist") { - this->mLoopState = MprisLoopState::Playlist; - } else { - this->mLoopState = MprisLoopState::None; - qWarning() << "Received unexpected LoopStatus for" << this << status; - } - - emit this->loopStateChanged(); -} - -qreal MprisPlayer::rate() const { return this->pRate.get(); } -qreal MprisPlayer::minRate() const { return this->pMinRate.get(); } -qreal MprisPlayer::maxRate() const { return this->pMaxRate.get(); } - -void MprisPlayer::setRate(qreal rate) { - if (rate == this->pRate.get()) return; - - if (rate < this->pMinRate.get() || rate > this->pMaxRate.get()) { - qWarning() << "Cannot set rate for" << this << "to" << rate - << "which is outside of minRate and maxRate" << this->pMinRate.get() - << this->pMaxRate.get(); - return; - } - - this->pRate.set(rate); - this->pRate.write(); -} - -bool MprisPlayer::shuffle() const { return this->pShuffle.get(); } -bool MprisPlayer::shuffleSupported() const { return this->pShuffle.exists(); } - -void MprisPlayer::setShuffle(bool shuffle) { - if (!this->shuffleSupported()) { - qWarning() << "Cannot set shuffle for" << this << "because shuffle is not supported."; - return; - } - - if (!this->canControl()) { - qWarning() << "Cannot set shuffle state of" << this << "because canControl is false."; - return; - } - - this->pShuffle.set(shuffle); - this->pShuffle.write(); -} - -bool MprisPlayer::fullscreen() const { return this->pFullscreen.get(); } - -void MprisPlayer::setFullscreen(bool fullscreen) { - if (!this->canSetFullscreen()) { - qWarning() << "Cannot set fullscreen for" << this << "because canSetFullscreen is false."; - return; - } - - this->pFullscreen.set(fullscreen); - this->pFullscreen.write(); -} - -QList MprisPlayer::supportedUriSchemes() const { return this->pSupportedUriSchemes.get(); } -QList MprisPlayer::supportedMimeTypes() const { return this->pSupportedMimeTypes.get(); } +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->volumeSupported()) emit this->volumeSupportedChanged(); - if (this->loopSupported()) emit this->loopSupportedChanged(); - if (this->shuffleSupported()) emit this->shuffleSupportedChanged(); + if (this->mReady) return; + this->mReady = true; emit this->ready(); } -} // namespace qs::service::mpris +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 index 0b18d78c..168006a0 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -1,324 +1,67 @@ #pragma once -#include +#include +#include +#include #include -#include -#include #include -#include "../../core/doc.hpp" #include "../../dbus/properties.hpp" #include "dbus_player.h" -#include "dbus_player_app.h" -namespace qs::service::mpris { +Q_DECLARE_LOGGING_CATEGORY(logMprisPlayer); -class MprisPlaybackState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; +namespace qs::service::mp { -public: - enum Enum { - Stopped = 0, - Playing = 1, - Paused = 2, - }; - Q_ENUM(Enum); +class MprisPlayer; - Q_INVOKABLE static QString toString(MprisPlaybackState::Enum status); -}; - -class MprisLoopState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum { - None = 0, - Track = 1, - Playlist = 2, - }; - Q_ENUM(Enum); - - Q_INVOKABLE static QString toString(MprisLoopState::Enum status); -}; - -///! A media player exposed over MPRIS. -/// A media player exposed over MPRIS. -/// -/// > [!WARNING] Support for various functionality and general compliance to -/// > the MPRIS specification varies wildly by player. -/// > Always check the associated `canXyz` and `xyzSupported` properties if available. -/// -/// > [!INFO] The TrackList and Playlist interfaces were not implemented as we could not -/// > find any media players using them to test against. class MprisPlayer: public QObject { Q_OBJECT; - // clang-format off - Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged); - Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged); - Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged); - Q_PROPERTY(bool canSeek READ canSeek NOTIFY canSeekChanged); - Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged); - Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged); - Q_PROPERTY(bool canQuit READ canQuit NOTIFY canQuitChanged); - Q_PROPERTY(bool canRaise READ canRaise NOTIFY canRaiseChanged); - Q_PROPERTY(bool canSetFullscreen READ canSetFullscreen NOTIFY canSetFullscreenChanged); - /// The human readable name of the media player. - Q_PROPERTY(QString identity READ identity NOTIFY identityChanged); - /// The current position in the playing track, as seconds, with millisecond precision, - /// or `0` if `positionSupported` is false. - /// - /// May only be written to if `canSeek` and `positionSupported` are true. - /// - /// > [!WARNING] To avoid excessive property updates wasting CPU while `position` is not - /// > actively monitored, `position` usually will not update reactively, unless a nonlinear - /// > change in position occurs, however reading it will always return the current position. - /// > - /// > If you want to actively monitor the position, the simplest way it to emit the `positionChanged` - /// > signal manually for the duration you are monitoring it, Using a [FrameAnimation] if you need - /// > the value to update smoothly, such as on a slider, or a [Timer] if not, as shown below. - /// > - /// > ```qml {filename="Using a FrameAnimation"} - /// > FrameAnimation { - /// > // only emit the signal when the position is actually changing. - /// > running: player.playbackState == MprisPlaybackState.Playing - /// > // emit the positionChanged signal every frame. - /// > onTriggered: player.positionChanged() - /// > } - /// > ``` - /// > - /// > ```qml {filename="Using a Timer"} - /// > Timer { - /// > // only emit the signal when the position is actually changing. - /// > running: player.playbackState == MprisPlaybackState.Playing - /// > // Make sure the position updates at least once per second. - /// > interval: 1000 - /// > repeat: true - /// > // emit the positionChanged signal every second. - /// > onTriggered: player.positionChanged() - /// > } - /// > ``` - /// - /// [FrameAnimation]: https://doc.qt.io/qt-6/qml-qtquick-frameanimation.html - /// [Timer]: https://doc.qt.io/qt-6/qml-qtqml-timer.html - Q_PROPERTY(qreal position READ position WRITE setPosition NOTIFY positionChanged); - Q_PROPERTY(bool positionSupported READ positionSupported NOTIFY positionSupportedChanged); - /// The length of the playing track, as seconds, with millisecond precision, - /// or the value of `position` if `lengthSupported` is false. - Q_PROPERTY(qreal length READ length NOTIFY lengthChanged); - Q_PROPERTY(bool lengthSupported READ lengthSupported NOTIFY lengthSupportedChanged); - /// The volume of the playing track from 0.0 to 1.0, or 1.0 if `volumeSupported` is false. - /// - /// May only be written to if `canControl` and `volumeSupported` are true. - Q_PROPERTY(qreal volume READ volume WRITE setVolume NOTIFY volumeChanged); - Q_PROPERTY(bool volumeSupported READ volumeSupported NOTIFY volumeSupportedChanged); - /// Metadata of the current track. - /// - /// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata). - /// Do not count on any of them actually being present. - Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); - /// The playback state of the media player. - /// - /// - If `canPlay` is false, you cannot assign the `Playing` state. - /// - If `canPause` is false, you cannot assign the `Paused` state. - /// - If `canControl` is false, you cannot assign the `Stopped` state. - /// (or any of the others, though their repsective properties will also be false) - Q_PROPERTY(MprisPlaybackState::Enum playbackState READ playbackState WRITE setPlaybackState NOTIFY playbackStateChanged); - /// The loop state of the media player, or `None` if `loopSupported` is false. - /// - /// May only be written to if `canControl` and `loopSupported` are true. - Q_PROPERTY(MprisLoopState::Enum loopState READ loopState WRITE setLoopState NOTIFY loopStateChanged); - Q_PROPERTY(bool loopSupported READ loopSupported NOTIFY loopSupportedChanged); - /// The speed the song is playing at, as a multiplier. - /// - /// Only values between `minRate` and `maxRate` (inclusive) may be written to the property. - /// Additionally, It is recommended that you only write common values such as `0.25`, `0.5`, `1.0`, `2.0` - /// to the property, as media players are free to ignore the value, and are more likely to - /// accept common ones. - Q_PROPERTY(qreal rate READ rate WRITE setRate NOTIFY rateChanged); - Q_PROPERTY(qreal minRate READ minRate NOTIFY minRateChanged); - Q_PROPERTY(qreal maxRate READ maxRate NOTIFY maxRateChanged); - /// If the play queue is currently being shuffled, or false if `shuffleSupported` is false. - /// - /// May only be written if `canControl` and `shuffleSupported` are true. - Q_PROPERTY(bool shuffle READ shuffle WRITE setShuffle NOTIFY shuffleChanged); - Q_PROPERTY(bool shuffleSupported READ shuffleSupported NOTIFY shuffleSupportedChanged); - /// If the player is currently shown in fullscreen. - /// - /// May only be written to if `canSetFullscreen` is true. - Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged); - /// Uri schemes supported by `openUri`. - Q_PROPERTY(QList supportedUriSchemes READ supportedUriSchemes NOTIFY supportedUriSchemesChanged); - /// Mime types supported by `openUri`. - Q_PROPERTY(QList supportedMimeTypes READ supportedMimeTypes NOTIFY supportedMimeTypesChanged); - // clang-format on - QML_ELEMENT; - QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris"); public: - explicit MprisPlayer(const QString& address, QObject* parent = nullptr); + explicit MprisPlayer(const QString& address, QObject* parent = nullptr); + QString watcherId; // TODO: maybe can be private CHECK - /// Bring the media player to the front of the window stack. - /// - /// May only be called if `canRaise` is true. - Q_INVOKABLE void raise(); - /// Quit the media player. - /// - /// May only be called if `canQuit` is true. - Q_INVOKABLE void quit(); - /// Open the given URI in the media player. - /// - /// Many players will silently ignore this, especially if the uri - /// does not match `supportedUriSchemes` and `supportedMimeTypes`. - Q_INVOKABLE void openUri(const QString& uri); - /// Play the next song. - /// - /// May only be called if `canGoNext` is true. - Q_INVOKABLE void next(); - /// Play the previous song, or go back to the beginning of the current one. - /// - /// May only be called if `canGoPrevious` is true. - Q_INVOKABLE void previous(); - /// Change `position` by an offset. - /// - /// Even if `positionSupported` is false and you cannot set `position`, - /// this function may work. - /// - /// May only be called if `canSeek` is true. - Q_INVOKABLE void seek(qreal offset); + void setPosition(QDBusObjectPath trackId, qlonglong position); + void next(); + void previous(); + void pause(); + void playPause(); + void stop(); + void play(); [[nodiscard]] bool isValid() const; - [[nodiscard]] QString address() 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" }; - [[nodiscard]] bool canControl() const; - [[nodiscard]] bool canSeek() const; - [[nodiscard]] bool canGoNext() const; - [[nodiscard]] bool canGoPrevious() const; - [[nodiscard]] bool canPlay() const; - [[nodiscard]] bool canPause() const; - [[nodiscard]] bool canQuit() const; - [[nodiscard]] bool canRaise() const; - [[nodiscard]] bool canSetFullscreen() const; - - [[nodiscard]] QString identity() const; - - [[nodiscard]] qlonglong positionMs() const; - [[nodiscard]] qreal position() const; - [[nodiscard]] bool positionSupported() const; - void setPosition(qreal position); - - [[nodiscard]] qreal length() const; - [[nodiscard]] bool lengthSupported() const; - - [[nodiscard]] qreal volume() const; - [[nodiscard]] bool volumeSupported() const; - void setVolume(qreal volume); - - [[nodiscard]] QVariantMap metadata() const; - - [[nodiscard]] MprisPlaybackState::Enum playbackState() const; - void setPlaybackState(MprisPlaybackState::Enum playbackState); - - [[nodiscard]] MprisLoopState::Enum loopState() const; - [[nodiscard]] bool loopSupported() const; - void setLoopState(MprisLoopState::Enum loopState); - - [[nodiscard]] qreal rate() const; - [[nodiscard]] qreal minRate() const; - [[nodiscard]] qreal maxRate() const; - void setRate(qreal rate); - - [[nodiscard]] bool shuffle() const; - [[nodiscard]] bool shuffleSupported() const; - void setShuffle(bool shuffle); - - [[nodiscard]] bool fullscreen() const; - void setFullscreen(bool fullscreen); - - [[nodiscard]] QList supportedUriSchemes() const; - [[nodiscard]] QList supportedMimeTypes() const; + 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: - QSDOC_HIDE void ready(); - void canControlChanged(); - void canPlayChanged(); - void canPauseChanged(); - void canSeekChanged(); - void canGoNextChanged(); - void canGoPreviousChanged(); - void canQuitChanged(); - void canRaiseChanged(); - void canSetFullscreenChanged(); - void identityChanged(); - void positionChanged(); - void positionSupportedChanged(); - void lengthChanged(); - void lengthSupportedChanged(); - void volumeChanged(); - void volumeSupportedChanged(); - void metadataChanged(); - void playbackStateChanged(); - void loopStateChanged(); - void loopSupportedChanged(); - void rateChanged(); - void minRateChanged(); - void maxRateChanged(); - void shuffleChanged(); - void shuffleSupportedChanged(); - void fullscreenChanged(); - void supportedUriSchemesChanged(); - void supportedMimeTypesChanged(); + void ready(); private slots: void onGetAllFinished(); - void onPositionChanged(); - void onExportedPositionChanged(); - void onSeek(qlonglong time); - void onMetadataChanged(); - void onPlaybackStatusChanged(); - void onLoopStatusChanged(); + void updatePlayer(); private: - // clang-format off - dbus::DBusPropertyGroup appProperties; - dbus::DBusProperty pIdentity {this->appProperties, "Identity"}; - dbus::DBusProperty pCanQuit {this->appProperties, "CanQuit"}; - dbus::DBusProperty pCanRaise {this->appProperties, "CanRaise"}; - dbus::DBusProperty pFullscreen {this->appProperties, "Fullscreen", false, false}; - dbus::DBusProperty pCanSetFullscreen {this->appProperties, "CanSetFullscreen", false, false}; - dbus::DBusProperty> pSupportedUriSchemes {this->appProperties, "SupportedUriSchemes"}; - dbus::DBusProperty> pSupportedMimeTypes {this->appProperties, "SupportedMimeTypes"}; - - dbus::DBusPropertyGroup playerProperties; - dbus::DBusProperty pCanControl {this->playerProperties, "CanControl"}; - dbus::DBusProperty pCanPlay {this->playerProperties, "CanPlay"}; - dbus::DBusProperty pCanPause {this->playerProperties, "CanPause"}; - dbus::DBusProperty pCanSeek {this->playerProperties, "CanSeek"}; - dbus::DBusProperty pCanGoNext {this->playerProperties, "CanGoNext"}; - dbus::DBusProperty pCanGoPrevious {this->playerProperties, "CanGoPrevious"}; - dbus::DBusProperty pPosition {this->playerProperties, "Position", 0, false}; // "required" - dbus::DBusProperty pVolume {this->playerProperties, "Volume", 1, false}; // "required" - dbus::DBusProperty pMetadata {this->playerProperties, "Metadata"}; - dbus::DBusProperty pPlaybackStatus {this->playerProperties, "PlaybackStatus"}; - dbus::DBusProperty pLoopStatus {this->playerProperties, "LoopStatus", "", false}; - dbus::DBusProperty pRate {this->playerProperties, "Rate", 1, false}; // "required" - dbus::DBusProperty pMinRate {this->playerProperties, "MinimumRate", 1, false}; // "required" - dbus::DBusProperty pMaxRate {this->playerProperties, "MaximumRate", 1, false}; // "required" - dbus::DBusProperty pShuffle {this->playerProperties, "Shuffle", false, false}; - // clang-format on - - MprisPlaybackState::Enum mPlaybackState = MprisPlaybackState::Stopped; - MprisLoopState::Enum mLoopState = MprisLoopState::None; - QDateTime lastPositionTimestamp; - QDateTime pausedTime; - qlonglong mLength = -1; - - DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; - QString mTrackId; + bool mReady = false; }; -} // namespace qs::service::mpris +} // 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 index 1e107660..2a735f74 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -1,24 +1,22 @@ #include "watcher.hpp" -#include +#include #include #include #include #include #include #include +#include #include -#include -#include -#include -#include "player.hpp" +Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mp.watcher", QtWarningMsg); -namespace qs::service::mpris { - -Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); +namespace qs::service::mp { MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { + new MprisWatcherAdaptor(this); + qCDebug(logMprisWatcher) << "Starting MprisWatcher"; auto bus = QDBusConnection::sessionBus(); @@ -28,99 +26,121 @@ MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { 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.setWatchMode( - QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration - ); - this->serviceWatcher.addWatchedService("org.mpris.MediaPlayer2*"); + this->serviceWatcher.addWatchedService("org.mpris.MprisWatcher"); this->serviceWatcher.setConnection(bus); - this->registerExisting(); + this->tryRegister(); } -void MprisWatcher::registerExisting() { - const QStringList& list = QDBusConnection::sessionBus().interface()->registeredServiceNames(); +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.startsWith("org.mpris.MediaPlayer2")) { + if (service.contains("org.mpris.MediaPlayer2")) { qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service; - this->registerPlayer(service); + RegisterMprisPlayer(service); } } } void MprisWatcher::onServiceRegistered(const QString& service) { - if (service.startsWith("org.mpris.MediaPlayer2")) { + if (service == "org.mpris.MprisWatcher") { + qCDebug(logMprisWatcher) << "MprisWatcher"; + return; + } else if (service.contains("org.mpris.MediaPlayer2")) { qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered."; - this->registerPlayer(service); + RegisterMprisPlayer(service); } else { - qCWarning(logMprisWatcher) << "Got a registration event for untracked service" << service; + 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 (auto* player = this->mPlayers.value(service)) { - player->deleteLater(); - this->mPlayers.remove(service); - qCDebug(logMprisWatcher) << "Unregistered MprisPlayer" << service; + if (service == "org.mpris.MprisWatcher") { + qCDebug(logMprisWatcher) << "Active MprisWatcher unregistered, attempting registration"; + this->tryRegister(); + return; } else { - qCWarning(logMprisWatcher) << "Got service unregister event for untracked service" << service; + 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); } -void MprisWatcher::onPlayerReady() { - auto* player = qobject_cast(this->sender()); - this->readyPlayers.push_back(player); - emit this->playersChanged(); -} +QList MprisWatcher::registeredPlayers() const { return this->players; } -void MprisWatcher::onPlayerDestroyed(QObject* object) { - auto* player = static_cast(object); // NOLINT - - if (this->readyPlayers.removeOne(player)) { - emit this->playersChanged(); - } -} - -QQmlListProperty MprisWatcher::players() { - return QQmlListProperty( - this, - nullptr, - &MprisWatcher::playersCount, - &MprisWatcher::playerAt - ); -} - -qsizetype MprisWatcher::playersCount(QQmlListProperty* property) { - return static_cast(property->object)->readyPlayers.count(); // NOLINT -} - -MprisPlayer* MprisWatcher::playerAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->readyPlayers.at(index); // NOLINT -} - -void MprisWatcher::registerPlayer(const QString& address) { - if (this->mPlayers.contains(address)) { - qCDebug(logMprisWatcher) << "Skipping duplicate registration of MprisPlayer" << address; +void MprisWatcher::RegisterMprisPlayer(const QString& player) { + if (this->players.contains(player)) { + qCDebug(logMprisWatcher).noquote() + << "Skipping duplicate registration of MprisPlayer" << player << "to watcher"; return; } - auto* player = new MprisPlayer(address, this); - if (!player->isValid()) { - qCWarning(logMprisWatcher) << "Ignoring invalid MprisPlayer registration of" << address; - delete player; + if (!QDBusConnection::sessionBus().interface()->serviceOwner(player).isValid()) { + qCWarning(logMprisWatcher).noquote() + << "Ignoring invalid MprisPlayer registration of" << player << "to watcher"; return; } - this->mPlayers.insert(address, player); - QObject::connect(player, &MprisPlayer::ready, this, &MprisWatcher::onPlayerReady); - QObject::connect(player, &QObject::destroyed, this, &MprisWatcher::onPlayerDestroyed); + this->serviceWatcher.addWatchedService(player); + this->players.push_back(player); - qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address; + qCDebug(logMprisWatcher).noquote() << "Registered MprisPlayer" << player << "to watcher"; + + emit this->MprisPlayerRegistered(player); } -} // namespace qs::service::mpris +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 index a1e4df7c..b3e0bdeb 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -3,51 +3,51 @@ #include #include #include -#include #include #include #include -#include -#include -#include #include -#include "player.hpp" +Q_DECLARE_LOGGING_CATEGORY(logMprisWatcher); -namespace qs::service::mpris { +namespace qs::service::mp { -///! Provides access to MprisPlayers. -class MprisWatcher: public QObject { +class MprisWatcher + : public QObject + , protected QDBusContext { Q_OBJECT; - QML_NAMED_ELEMENT(Mpris); - QML_SINGLETON; - /// All connected MPRIS players. - Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); + Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion); + Q_PROPERTY(QList RegisteredMprisPlayers READ registeredPlayers); public: explicit MprisWatcher(QObject* parent = nullptr); - [[nodiscard]] QQmlListProperty players(); + 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: - void playersChanged(); + // NOLINTBEGIN + void MprisWatcherRegistered(); + void MprisPlayerRegistered(const QString& service); + void MprisPlayerUnregistered(const QString& service); + // NOLINTEND private slots: - void onServiceRegistered(const QString& service); + void onServiceRegistered(const QString& service); void onServiceUnregistered(const QString& service); - void onPlayerReady(); - void onPlayerDestroyed(QObject* object); - -private: - static qsizetype playersCount(QQmlListProperty* property); - static MprisPlayer* playerAt(QQmlListProperty* property, qsizetype index); - - void registerExisting(); - void registerPlayer(const QString& address); +private: QDBusServiceWatcher serviceWatcher; - QHash mPlayers; - QList readyPlayers; }; -} // namespace qs::service::mpris +} // namespace qs::service::mp diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index aa411909..9dbf02fd 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -54,14 +54,14 @@ public: dbus::DBusProperty status {this->properties, "Status"}; dbus::DBusProperty category {this->properties, "Category"}; dbus::DBusProperty windowId {this->properties, "WindowId"}; - dbus::DBusProperty iconThemePath {this->properties, "IconThemePath", "", false}; - dbus::DBusProperty iconName {this->properties, "IconName", "", false}; // IconPixmap may be set - dbus::DBusProperty iconPixmaps {this->properties, "IconPixmap", {}, false}; // IconName may be set + dbus::DBusProperty iconThemePath {this->properties, "IconThemePath"}; + dbus::DBusProperty iconName {this->properties, "IconName"}; + dbus::DBusProperty iconPixmaps {this->properties, "IconPixmap"}; dbus::DBusProperty overlayIconName {this->properties, "OverlayIconName"}; dbus::DBusProperty overlayIconPixmaps {this->properties, "OverlayIconPixmap"}; dbus::DBusProperty attentionIconName {this->properties, "AttentionIconName"}; dbus::DBusProperty attentionIconPixmaps {this->properties, "AttentionIconPixmap"}; - dbus::DBusProperty attentionMovieName {this->properties, "AttentionMovieName", "", false}; + dbus::DBusProperty attentionMovieName {this->properties, "AttentionMovieName"}; dbus::DBusProperty tooltip {this->properties, "ToolTip"}; dbus::DBusProperty isMenu {this->properties, "ItemIsMenu"}; dbus::DBusProperty menuPath {this->properties, "Menu"};