forked from quickshell/quickshell
		
	service/mpris: finish mpris implementation
This commit is contained in:
		
							parent
							
								
									3b6d1c3bd8
								
							
						
					
					
						commit
						4ee9ac7f7c
					
				
					 16 changed files with 911 additions and 578 deletions
				
			
		| 
						 | 
				
			
			@ -35,7 +35,7 @@ message(STATUS "  X11: ${X11}")
 | 
			
		|||
message(STATUS "  Services")
 | 
			
		||||
message(STATUS "    StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
 | 
			
		||||
message(STATUS "    PipeWire: ${SERVICE_PIPEWIRE}")
 | 
			
		||||
message(STATUS "		Mpris: ${SERVICE_MPRIS}")
 | 
			
		||||
message(STATUS "    Mpris: ${SERVICE_MPRIS}")
 | 
			
		||||
message(STATUS "  Hyprland: ${HYPRLAND}")
 | 
			
		||||
if (HYPRLAND)
 | 
			
		||||
	message(STATUS "    Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -188,9 +188,9 @@ public:
 | 
			
		|||
 | 
			
		||||
	dbus::DBusPropertyGroup properties;
 | 
			
		||||
	dbus::DBusProperty<quint32> version {this->properties, "Version"};
 | 
			
		||||
	dbus::DBusProperty<QString> textDirection {this->properties, "TextDirection"};
 | 
			
		||||
	dbus::DBusProperty<QString> textDirection {this->properties, "TextDirection", "", false};
 | 
			
		||||
	dbus::DBusProperty<QString> status {this->properties, "Status"};
 | 
			
		||||
	dbus::DBusProperty<QStringList> iconThemePath {this->properties, "IconThemePath"};
 | 
			
		||||
	dbus::DBusProperty<QStringList> iconThemePath {this->properties, "IconThemePath", {}, false};
 | 
			
		||||
 | 
			
		||||
	void prepareToShow(qint32 item, bool sendOpened);
 | 
			
		||||
	void updateLayout(qint32 parent, qint32 depth);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -112,6 +112,8 @@ void asyncReadPropertyInternal(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
void AbstractDBusProperty::tryUpdate(const QVariant& variant) {
 | 
			
		||||
	this->mExists = true;
 | 
			
		||||
 | 
			
		||||
	auto error = this->read(variant);
 | 
			
		||||
	if (error.isValid()) {
 | 
			
		||||
		qCWarning(logDbusProperties).noquote()
 | 
			
		||||
| 
						 | 
				
			
			@ -159,6 +161,44 @@ 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;
 | 
			
		||||
| 
						 | 
				
			
			@ -232,7 +272,7 @@ void DBusPropertyGroup::updateAllViaGetAll() {
 | 
			
		|||
		} else {
 | 
			
		||||
			qCDebug(logDbusProperties).noquote()
 | 
			
		||||
			    << "Received GetAll property set for" << this->toString();
 | 
			
		||||
			this->updatePropertySet(reply.value());
 | 
			
		||||
			this->updatePropertySet(reply.value(), true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		delete call;
 | 
			
		||||
| 
						 | 
				
			
			@ -242,7 +282,7 @@ void DBusPropertyGroup::updateAllViaGetAll() {
 | 
			
		|||
	QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) {
 | 
			
		||||
void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) {
 | 
			
		||||
	for (const auto [name, value]: properties.asKeyValueRange()) {
 | 
			
		||||
		auto prop = std::find_if(
 | 
			
		||||
		    this->properties.begin(),
 | 
			
		||||
| 
						 | 
				
			
			@ -251,11 +291,21 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) {
 | 
			
		|||
		);
 | 
			
		||||
 | 
			
		||||
		if (prop == this->properties.end()) {
 | 
			
		||||
			qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this;
 | 
			
		||||
			qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for"
 | 
			
		||||
			                           << this->toString();
 | 
			
		||||
		} 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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -291,7 +341,7 @@ void DBusPropertyGroup::onPropertiesChanged(
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->updatePropertySet(changedProperties);
 | 
			
		||||
	this->updatePropertySet(changedProperties, false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::dbus
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -79,22 +79,31 @@ class AbstractDBusProperty: public QObject {
 | 
			
		|||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit AbstractDBusProperty(QString name, const QMetaType& type, QObject* parent = nullptr)
 | 
			
		||||
	explicit AbstractDBusProperty(
 | 
			
		||||
	    QString name,
 | 
			
		||||
	    const QMetaType& type,
 | 
			
		||||
	    bool required,
 | 
			
		||||
	    QObject* parent = nullptr
 | 
			
		||||
	)
 | 
			
		||||
	    : QObject(parent)
 | 
			
		||||
	    , name(std::move(name))
 | 
			
		||||
	    , type(type) {}
 | 
			
		||||
	    , type(type)
 | 
			
		||||
	    , required(required) {}
 | 
			
		||||
 | 
			
		||||
	[[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);
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +112,8 @@ private:
 | 
			
		|||
 | 
			
		||||
	QString name;
 | 
			
		||||
	QMetaType type;
 | 
			
		||||
	bool required;
 | 
			
		||||
	bool mExists = false;
 | 
			
		||||
 | 
			
		||||
	friend class DBusPropertyGroup;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -133,7 +144,7 @@ private slots:
 | 
			
		|||
	);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	void updatePropertySet(const QVariantMap& properties);
 | 
			
		||||
	void updatePropertySet(const QVariantMap& properties, bool complainMissing);
 | 
			
		||||
 | 
			
		||||
	DBusPropertiesInterface* propertyInterface = nullptr;
 | 
			
		||||
	QDBusAbstractInterface* interface = nullptr;
 | 
			
		||||
| 
						 | 
				
			
			@ -145,17 +156,23 @@ private:
 | 
			
		|||
template <typename T>
 | 
			
		||||
class DBusProperty: public AbstractDBusProperty {
 | 
			
		||||
public:
 | 
			
		||||
	explicit DBusProperty(QString name, QObject* parent = nullptr, T value = T())
 | 
			
		||||
	    : AbstractDBusProperty(std::move(name), QMetaType::fromType<T>(), parent)
 | 
			
		||||
	explicit DBusProperty(
 | 
			
		||||
	    QString name,
 | 
			
		||||
	    T value = T(),
 | 
			
		||||
	    bool required = true,
 | 
			
		||||
	    QObject* parent = nullptr
 | 
			
		||||
	)
 | 
			
		||||
	    : AbstractDBusProperty(std::move(name), QMetaType::fromType<T>(), required, parent)
 | 
			
		||||
	    , value(std::move(value)) {}
 | 
			
		||||
 | 
			
		||||
	explicit DBusProperty(
 | 
			
		||||
	    DBusPropertyGroup& group,
 | 
			
		||||
	    QString name,
 | 
			
		||||
	    QObject* parent = nullptr,
 | 
			
		||||
	    T value = T()
 | 
			
		||||
	    T value = T(),
 | 
			
		||||
	    bool required = true,
 | 
			
		||||
	    QObject* parent = nullptr
 | 
			
		||||
	)
 | 
			
		||||
	    : DBusProperty(std::move(name), parent, std::move(value)) {
 | 
			
		||||
	    : DBusProperty(std::move(name), std::move(value), required, parent) {
 | 
			
		||||
		group.attachProperty(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -165,7 +182,7 @@ public:
 | 
			
		|||
		return str;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] T get() const { return this->value; }
 | 
			
		||||
	[[nodiscard]] const T& get() const { return this->value; }
 | 
			
		||||
 | 
			
		||||
	void set(T value) {
 | 
			
		||||
		this->value = std::move(value);
 | 
			
		||||
| 
						 | 
				
			
			@ -183,6 +200,8 @@ protected:
 | 
			
		|||
		return result.error;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	QVariant serialize() override { return QVariant::fromValue(this->value); }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	T value;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,6 @@
 | 
			
		|||
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
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
qt_add_dbus_interface(DBUS_INTERFACES
 | 
			
		||||
| 
						 | 
				
			
			@ -15,20 +8,19 @@ qt_add_dbus_interface(DBUS_INTERFACES
 | 
			
		|||
	dbus_player
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
set_source_files_properties(org.mpris.MprisWatcher.xml PROPERTIES
 | 
			
		||||
	CLASSNAME DBusMprisWatcher
 | 
			
		||||
set_source_files_properties(org.mpris.MediaPlayer2.xml PROPERTIES
 | 
			
		||||
	CLASSNAME DBusMprisPlayerApp
 | 
			
		||||
	NO_NAMESPACE TRUE
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
qt_add_dbus_interface(DBUS_INTERFACES
 | 
			
		||||
	org.mpris.MprisWatcher.xml
 | 
			
		||||
	dbus_watcher_interface
 | 
			
		||||
	org.mpris.MediaPlayer2.xml
 | 
			
		||||
	dbus_player_app
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
qt_add_library(quickshell-service-mpris STATIC
 | 
			
		||||
	qml.cpp
 | 
			
		||||
 | 
			
		||||
	watcher.cpp
 | 
			
		||||
	player.cpp
 | 
			
		||||
	watcher.cpp
 | 
			
		||||
	${DBUS_INTERFACES}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										7
									
								
								src/services/mpris/module.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/services/mpris/module.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
name = "Quickshell.Services.Mpris"
 | 
			
		||||
description = "Mpris Service"
 | 
			
		||||
headers = [
 | 
			
		||||
	"player.hpp",
 | 
			
		||||
	"watcher.hpp",
 | 
			
		||||
]
 | 
			
		||||
-----
 | 
			
		||||
| 
						 | 
				
			
			@ -1,33 +1,24 @@
 | 
			
		|||
<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'/>
 | 
			
		||||
	<interface name="org.mpris.MediaPlayer2.Player">
 | 
			
		||||
		<method name="OpenUri">
 | 
			
		||||
			<arg direction="in" type="s" name="Uri"/>
 | 
			
		||||
		</method>
 | 
			
		||||
		<method name='Seek'>
 | 
			
		||||
			<arg direction='in' type='x' name='Offset' /> 
 | 
			
		||||
		<method name="SetPosition">
 | 
			
		||||
			<arg direction="in" type="o" name="TrackId"/>
 | 
			
		||||
			<arg direction="in" type="x" name="Position"/>
 | 
			
		||||
		</method>
 | 
			
		||||
		<method name='PlayPause' />
 | 
			
		||||
    <method name='Next' />
 | 
			
		||||
    <method name='Previous' />
 | 
			
		||||
    <method name='Stop' />
 | 
			
		||||
		<method name='Play' />
 | 
			
		||||
		<method name='Pause' />
 | 
			
		||||
		<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"/>
 | 
			
		||||
 | 
			
		||||
		<signal name="Seeked">
 | 
			
		||||
			<arg type="x" name="Position"/>
 | 
			
		||||
		</signal>
 | 
			
		||||
  </interface>
 | 
			
		||||
</node>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								src/services/mpris/org.mpris.MediaPlayer2.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/services/mpris/org.mpris.MediaPlayer2.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
<node>
 | 
			
		||||
	<interface name="org.mpris.MediaPlayer2">
 | 
			
		||||
		<method name="Raise"/>
 | 
			
		||||
		<method name="Quit"/>
 | 
			
		||||
	</interface>
 | 
			
		||||
</node>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
<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>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,87 +1,427 @@
 | 
			
		|||
#include "player.hpp"
 | 
			
		||||
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qdatetime.h>
 | 
			
		||||
#include <qdbusconnection.h>
 | 
			
		||||
#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 <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#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);
 | 
			
		||||
 | 
			
		||||
namespace qs::service::mp {
 | 
			
		||||
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";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
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";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	this->player = new DBusMprisPlayer(conn, path, QDBusConnection::sessionBus(), this);
 | 
			
		||||
MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(parent) {
 | 
			
		||||
	this->app = new DBusMprisPlayerApp(
 | 
			
		||||
	    address,
 | 
			
		||||
	    "/org/mpris/MediaPlayer2",
 | 
			
		||||
	    QDBusConnection::sessionBus(),
 | 
			
		||||
	    this
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	if (!this->player->isValid()) {
 | 
			
		||||
		qCWarning(logMprisPlayer).noquote() << "Cannot create MprisPlayer for" << conn;
 | 
			
		||||
	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;
 | 
			
		||||
		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->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->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->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->properties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished);
 | 
			
		||||
	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);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
 | 
			
		||||
	this->properties.setInterface(this->player);
 | 
			
		||||
	this->properties.updateAllViaGetAll();
 | 
			
		||||
	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<qlonglong>(offset * 1000) * 1000;
 | 
			
		||||
	this->player->Seek(target);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool MprisPlayer::isValid() const { return this->player->isValid(); }
 | 
			
		||||
bool MprisPlayer::isReady() const { return this->mReady; }
 | 
			
		||||
QString MprisPlayer::address() const { return this->player->service(); }
 | 
			
		||||
 | 
			
		||||
void MprisPlayer::setPosition(QDBusObjectPath trackId, qlonglong position) { // NOLINT
 | 
			
		||||
	this->player->SetPosition(trackId, position);
 | 
			
		||||
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<qlonglong>(this->pRate.get() * 1000);
 | 
			
		||||
	offset = (offset * rateMul) / 1000;
 | 
			
		||||
 | 
			
		||||
	return (this->pPosition.get() / 1000) + offset.count();
 | 
			
		||||
}
 | 
			
		||||
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(); }
 | 
			
		||||
 | 
			
		||||
qreal MprisPlayer::position() const {
 | 
			
		||||
	if (!this->positionSupported()) return 0; // unsupported
 | 
			
		||||
	if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0;
 | 
			
		||||
 | 
			
		||||
	return static_cast<qreal>(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<qlonglong>(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<qreal>(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<qlonglong>()) {
 | 
			
		||||
		length = lengthVariant.value<qlonglong>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (length != this->mLength) {
 | 
			
		||||
		this->mLength = length;
 | 
			
		||||
		emit this->lengthChanged();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auto trackidVariant = this->pMetadata.get().value("mpris:trackid");
 | 
			
		||||
	if (trackidVariant.isValid() && trackidVariant.canConvert<QString>()) {
 | 
			
		||||
		this->mTrackId = trackidVariant.value<QString>();
 | 
			
		||||
		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<QString> MprisPlayer::supportedUriSchemes() const { return this->pSupportedUriSchemes.get(); }
 | 
			
		||||
QList<QString> MprisPlayer::supportedMimeTypes() const { return this->pSupportedMimeTypes.get(); }
 | 
			
		||||
 | 
			
		||||
void MprisPlayer::onGetAllFinished() {
 | 
			
		||||
	if (this->mReady) return;
 | 
			
		||||
	this->mReady = true;
 | 
			
		||||
	if (this->volumeSupported()) emit this->volumeSupportedChanged();
 | 
			
		||||
	if (this->loopSupported()) emit this->loopSupportedChanged();
 | 
			
		||||
	if (this->shuffleSupported()) emit this->shuffleSupportedChanged();
 | 
			
		||||
	emit this->ready();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MprisPlayer::updatePlayer() { // NOLINT
 | 
			
		||||
	                                 // TODO: emit signal here
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::mp
 | 
			
		||||
} // namespace qs::service::mpris
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,67 +1,324 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qdbusextratypes.h>
 | 
			
		||||
#include <qdbuspendingcall.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "../../core/doc.hpp"
 | 
			
		||||
#include "../../dbus/properties.hpp"
 | 
			
		||||
#include "dbus_player.h"
 | 
			
		||||
#include "dbus_player_app.h"
 | 
			
		||||
 | 
			
		||||
Q_DECLARE_LOGGING_CATEGORY(logMprisPlayer);
 | 
			
		||||
namespace qs::service::mpris {
 | 
			
		||||
 | 
			
		||||
namespace qs::service::mp {
 | 
			
		||||
class MprisPlaybackState: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
	QML_SINGLETON;
 | 
			
		||||
 | 
			
		||||
class MprisPlayer;
 | 
			
		||||
public:
 | 
			
		||||
	enum Enum {
 | 
			
		||||
		Stopped = 0,
 | 
			
		||||
		Playing = 1,
 | 
			
		||||
		Paused = 2,
 | 
			
		||||
	};
 | 
			
		||||
	Q_ENUM(Enum);
 | 
			
		||||
 | 
			
		||||
	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<QString> supportedUriSchemes READ supportedUriSchemes NOTIFY supportedUriSchemesChanged);
 | 
			
		||||
	/// Mime types supported by `openUri`.
 | 
			
		||||
	Q_PROPERTY(QList<QString> 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);
 | 
			
		||||
	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();
 | 
			
		||||
	/// 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);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isValid() const;
 | 
			
		||||
	[[nodiscard]] bool isReady() const;
 | 
			
		||||
	[[nodiscard]] QString address() 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" };
 | 
			
		||||
	[[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;
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
	[[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<QString> supportedUriSchemes() const;
 | 
			
		||||
	[[nodiscard]] QList<QString> supportedMimeTypes() const;
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void ready();
 | 
			
		||||
	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();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onGetAllFinished();
 | 
			
		||||
	void updatePlayer();
 | 
			
		||||
	void onPositionChanged();
 | 
			
		||||
	void onExportedPositionChanged();
 | 
			
		||||
	void onSeek(qlonglong time);
 | 
			
		||||
	void onMetadataChanged();
 | 
			
		||||
	void onPlaybackStatusChanged();
 | 
			
		||||
	void onLoopStatusChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	dbus::DBusPropertyGroup appProperties;
 | 
			
		||||
	dbus::DBusProperty<QString> pIdentity {this->appProperties, "Identity"};
 | 
			
		||||
	dbus::DBusProperty<bool> pCanQuit {this->appProperties, "CanQuit"};
 | 
			
		||||
	dbus::DBusProperty<bool> pCanRaise {this->appProperties, "CanRaise"};
 | 
			
		||||
	dbus::DBusProperty<bool> pFullscreen {this->appProperties, "Fullscreen", false, false};
 | 
			
		||||
	dbus::DBusProperty<bool> pCanSetFullscreen {this->appProperties, "CanSetFullscreen", false, false};
 | 
			
		||||
	dbus::DBusProperty<QList<QString>> pSupportedUriSchemes {this->appProperties, "SupportedUriSchemes"};
 | 
			
		||||
	dbus::DBusProperty<QList<QString>> pSupportedMimeTypes {this->appProperties, "SupportedMimeTypes"};
 | 
			
		||||
 | 
			
		||||
	dbus::DBusPropertyGroup playerProperties;
 | 
			
		||||
	dbus::DBusProperty<bool> pCanControl {this->playerProperties, "CanControl"};
 | 
			
		||||
	dbus::DBusProperty<bool> pCanPlay {this->playerProperties, "CanPlay"};
 | 
			
		||||
	dbus::DBusProperty<bool> pCanPause {this->playerProperties, "CanPause"};
 | 
			
		||||
	dbus::DBusProperty<bool> pCanSeek {this->playerProperties, "CanSeek"};
 | 
			
		||||
	dbus::DBusProperty<bool> pCanGoNext {this->playerProperties, "CanGoNext"};
 | 
			
		||||
	dbus::DBusProperty<bool> pCanGoPrevious {this->playerProperties, "CanGoPrevious"};
 | 
			
		||||
	dbus::DBusProperty<qlonglong> pPosition {this->playerProperties, "Position", 0, false}; // "required"
 | 
			
		||||
	dbus::DBusProperty<double> pVolume {this->playerProperties, "Volume", 1, false}; // "required"
 | 
			
		||||
	dbus::DBusProperty<QVariantMap> pMetadata {this->playerProperties, "Metadata"};
 | 
			
		||||
	dbus::DBusProperty<QString> pPlaybackStatus {this->playerProperties, "PlaybackStatus"};
 | 
			
		||||
	dbus::DBusProperty<QString> pLoopStatus {this->playerProperties, "LoopStatus", "", false};
 | 
			
		||||
	dbus::DBusProperty<double> pRate {this->playerProperties, "Rate", 1, false}; // "required"
 | 
			
		||||
	dbus::DBusProperty<double> pMinRate {this->playerProperties, "MinimumRate", 1, false}; // "required"
 | 
			
		||||
	dbus::DBusProperty<double> pMaxRate {this->playerProperties, "MaximumRate", 1, false}; // "required"
 | 
			
		||||
	dbus::DBusProperty<bool> 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;
 | 
			
		||||
	bool mReady = false;
 | 
			
		||||
	QString mTrackId;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::mp
 | 
			
		||||
} // namespace qs::service::mpris
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,180 +0,0 @@
 | 
			
		|||
#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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,113 +0,0 @@
 | 
			
		|||
#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;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,22 +1,24 @@
 | 
			
		|||
#include "watcher.hpp"
 | 
			
		||||
 | 
			
		||||
#include <dbus_watcher.h>
 | 
			
		||||
#include <qcontainerfwd.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>
 | 
			
		||||
#include <qqmllist.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mp.watcher", QtWarningMsg);
 | 
			
		||||
#include "player.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::mp {
 | 
			
		||||
namespace qs::service::mpris {
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg);
 | 
			
		||||
 | 
			
		||||
MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) {
 | 
			
		||||
	new MprisWatcherAdaptor(this);
 | 
			
		||||
 | 
			
		||||
	qCDebug(logMprisWatcher) << "Starting MprisWatcher";
 | 
			
		||||
 | 
			
		||||
	auto bus = QDBusConnection::sessionBus();
 | 
			
		||||
| 
						 | 
				
			
			@ -26,121 +28,99 @@ 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->tryRegister();
 | 
			
		||||
	this->registerExisting();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
void MprisWatcher::registerExisting() {
 | 
			
		||||
	const QStringList& list = QDBusConnection::sessionBus().interface()->registeredServiceNames();
 | 
			
		||||
	for (const QString& service: list) {
 | 
			
		||||
		if (service.contains("org.mpris.MediaPlayer2")) {
 | 
			
		||||
		if (service.startsWith("org.mpris.MediaPlayer2")) {
 | 
			
		||||
			qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service;
 | 
			
		||||
			RegisterMprisPlayer(service);
 | 
			
		||||
			this->registerPlayer(service);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MprisWatcher::onServiceRegistered(const QString& service) {
 | 
			
		||||
	if (service == "org.mpris.MprisWatcher") {
 | 
			
		||||
		qCDebug(logMprisWatcher) << "MprisWatcher";
 | 
			
		||||
		return;
 | 
			
		||||
	} else if (service.contains("org.mpris.MediaPlayer2")) {
 | 
			
		||||
	if (service.startsWith("org.mpris.MediaPlayer2")) {
 | 
			
		||||
		qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered.";
 | 
			
		||||
		RegisterMprisPlayer(service);
 | 
			
		||||
		this->registerPlayer(service);
 | 
			
		||||
	} else {
 | 
			
		||||
		qCWarning(logMprisWatcher) << "Got a registration event for a untracked service";
 | 
			
		||||
		qCWarning(logMprisWatcher) << "Got a registration event for untracked service" << 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;
 | 
			
		||||
	if (auto* player = this->mPlayers.value(service)) {
 | 
			
		||||
		player->deleteLater();
 | 
			
		||||
		this->mPlayers.remove(service);
 | 
			
		||||
		qCDebug(logMprisWatcher) << "Unregistered MprisPlayer" << service;
 | 
			
		||||
	} 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;
 | 
			
		||||
		}
 | 
			
		||||
		qCWarning(logMprisWatcher) << "Got service unregister event for untracked service" << service;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->serviceWatcher.removeWatchedService(service);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QList<QString> MprisWatcher::registeredPlayers() const { return this->players; }
 | 
			
		||||
void MprisWatcher::onPlayerReady() {
 | 
			
		||||
	auto* player = qobject_cast<MprisPlayer*>(this->sender());
 | 
			
		||||
	this->readyPlayers.push_back(player);
 | 
			
		||||
	emit this->playersChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MprisWatcher::RegisterMprisPlayer(const QString& player) {
 | 
			
		||||
	if (this->players.contains(player)) {
 | 
			
		||||
		qCDebug(logMprisWatcher).noquote()
 | 
			
		||||
		    << "Skipping duplicate registration of MprisPlayer" << player << "to watcher";
 | 
			
		||||
void MprisWatcher::onPlayerDestroyed(QObject* object) {
 | 
			
		||||
	auto* player = static_cast<MprisPlayer*>(object); // NOLINT
 | 
			
		||||
 | 
			
		||||
	if (this->readyPlayers.removeOne(player)) {
 | 
			
		||||
		emit this->playersChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QQmlListProperty<MprisPlayer> MprisWatcher::players() {
 | 
			
		||||
	return QQmlListProperty<MprisPlayer>(
 | 
			
		||||
	    this,
 | 
			
		||||
	    nullptr,
 | 
			
		||||
	    &MprisWatcher::playersCount,
 | 
			
		||||
	    &MprisWatcher::playerAt
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
qsizetype MprisWatcher::playersCount(QQmlListProperty<MprisPlayer>* property) {
 | 
			
		||||
	return static_cast<MprisWatcher*>(property->object)->readyPlayers.count(); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MprisPlayer* MprisWatcher::playerAt(QQmlListProperty<MprisPlayer>* property, qsizetype index) {
 | 
			
		||||
	return static_cast<MprisWatcher*>(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;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!QDBusConnection::sessionBus().interface()->serviceOwner(player).isValid()) {
 | 
			
		||||
		qCWarning(logMprisWatcher).noquote()
 | 
			
		||||
		    << "Ignoring invalid MprisPlayer registration of" << player << "to watcher";
 | 
			
		||||
	auto* player = new MprisPlayer(address, this);
 | 
			
		||||
	if (!player->isValid()) {
 | 
			
		||||
		qCWarning(logMprisWatcher) << "Ignoring invalid MprisPlayer registration of" << address;
 | 
			
		||||
		delete player;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->serviceWatcher.addWatchedService(player);
 | 
			
		||||
	this->players.push_back(player);
 | 
			
		||||
	this->mPlayers.insert(address, player);
 | 
			
		||||
	QObject::connect(player, &MprisPlayer::ready, this, &MprisWatcher::onPlayerReady);
 | 
			
		||||
	QObject::connect(player, &QObject::destroyed, this, &MprisWatcher::onPlayerDestroyed);
 | 
			
		||||
 | 
			
		||||
	qCDebug(logMprisWatcher).noquote() << "Registered MprisPlayer" << player << "to watcher";
 | 
			
		||||
 | 
			
		||||
	emit this->MprisPlayerRegistered(player);
 | 
			
		||||
	qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MprisWatcher* MprisWatcher::instance() {
 | 
			
		||||
	static MprisWatcher* instance = nullptr; // NOLINT
 | 
			
		||||
	if (instance == nullptr) instance = new MprisWatcher();
 | 
			
		||||
	return instance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::mp
 | 
			
		||||
} // namespace qs::service::mpris
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,51 +3,51 @@
 | 
			
		|||
#include <qdbuscontext.h>
 | 
			
		||||
#include <qdbusinterface.h>
 | 
			
		||||
#include <qdbusservicewatcher.h>
 | 
			
		||||
#include <qhash.h>
 | 
			
		||||
#include <qlist.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qqmllist.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
Q_DECLARE_LOGGING_CATEGORY(logMprisWatcher);
 | 
			
		||||
#include "player.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::mp {
 | 
			
		||||
namespace qs::service::mpris {
 | 
			
		||||
 | 
			
		||||
class MprisWatcher
 | 
			
		||||
    : public QObject
 | 
			
		||||
    , protected QDBusContext {
 | 
			
		||||
///! Provides access to MprisPlayers.
 | 
			
		||||
class MprisWatcher: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion);
 | 
			
		||||
	Q_PROPERTY(QList<QString> RegisteredMprisPlayers READ registeredPlayers);
 | 
			
		||||
	QML_NAMED_ELEMENT(Mpris);
 | 
			
		||||
	QML_SINGLETON;
 | 
			
		||||
	/// All connected MPRIS players.
 | 
			
		||||
	Q_PROPERTY(QQmlListProperty<MprisPlayer> players READ players NOTIFY playersChanged);
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
	[[nodiscard]] QQmlListProperty<MprisPlayer> players();
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	// NOLINTBEGIN
 | 
			
		||||
	void MprisWatcherRegistered();
 | 
			
		||||
	void MprisPlayerRegistered(const QString& service);
 | 
			
		||||
	void MprisPlayerUnregistered(const QString& service);	
 | 
			
		||||
	// NOLINTEND
 | 
			
		||||
	void playersChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onServiceRegistered(const QString& service);
 | 
			
		||||
	void onServiceUnregistered(const QString& service);
 | 
			
		||||
	void onPlayerReady();
 | 
			
		||||
	void onPlayerDestroyed(QObject* object);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static qsizetype playersCount(QQmlListProperty<MprisPlayer>* property);
 | 
			
		||||
	static MprisPlayer* playerAt(QQmlListProperty<MprisPlayer>* property, qsizetype index);
 | 
			
		||||
 | 
			
		||||
	void registerExisting();
 | 
			
		||||
	void registerPlayer(const QString& address);
 | 
			
		||||
 | 
			
		||||
	QDBusServiceWatcher serviceWatcher;
 | 
			
		||||
	QHash<QString, MprisPlayer*> mPlayers;
 | 
			
		||||
	QList<MprisPlayer*> readyPlayers;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::mp
 | 
			
		||||
} // namespace qs::service::mpris
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,14 +54,14 @@ public:
 | 
			
		|||
	dbus::DBusProperty<QString> status {this->properties, "Status"};
 | 
			
		||||
	dbus::DBusProperty<QString> category {this->properties, "Category"};
 | 
			
		||||
	dbus::DBusProperty<quint32> windowId {this->properties, "WindowId"};
 | 
			
		||||
	dbus::DBusProperty<QString> iconThemePath {this->properties, "IconThemePath"};
 | 
			
		||||
	dbus::DBusProperty<QString> iconName {this->properties, "IconName"};
 | 
			
		||||
	dbus::DBusProperty<DBusSniIconPixmapList> iconPixmaps {this->properties, "IconPixmap"};
 | 
			
		||||
	dbus::DBusProperty<QString> iconThemePath {this->properties, "IconThemePath", "", false};
 | 
			
		||||
	dbus::DBusProperty<QString> iconName {this->properties, "IconName", "", false}; // IconPixmap may be set
 | 
			
		||||
	dbus::DBusProperty<DBusSniIconPixmapList> iconPixmaps {this->properties, "IconPixmap", {}, false}; // IconName may be set
 | 
			
		||||
	dbus::DBusProperty<QString> overlayIconName {this->properties, "OverlayIconName"};
 | 
			
		||||
	dbus::DBusProperty<DBusSniIconPixmapList> overlayIconPixmaps {this->properties, "OverlayIconPixmap"};
 | 
			
		||||
	dbus::DBusProperty<QString> attentionIconName {this->properties, "AttentionIconName"};
 | 
			
		||||
	dbus::DBusProperty<DBusSniIconPixmapList> attentionIconPixmaps {this->properties, "AttentionIconPixmap"};
 | 
			
		||||
	dbus::DBusProperty<QString> attentionMovieName {this->properties, "AttentionMovieName"};
 | 
			
		||||
	dbus::DBusProperty<QString> attentionMovieName {this->properties, "AttentionMovieName", "", false};
 | 
			
		||||
	dbus::DBusProperty<DBusSniTooltip> tooltip {this->properties, "ToolTip"};
 | 
			
		||||
	dbus::DBusProperty<bool> isMenu {this->properties, "ItemIsMenu"};
 | 
			
		||||
	dbus::DBusProperty<QDBusObjectPath> menuPath {this->properties, "Menu"};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue