kdeconnect-kde/plugins/mpriscontrol/mpriscontrolplugin.cpp
Alexander Lohnau 2e67f95017 Add explicit moc includes to cpp files
The rationale is explained in https://planet.kde.org/friedrich-kossebau-2023-06-28-include-also-moc-files-of-headers/

In case of KDEConnect, it impressively speeds up compilation. Before it
took 390 seconds on a clean build and with this change it took 330 seconds.
This is due to the mocs_compilation having to include the header files
and thus all their headers. Due to the lots of small plugins we have,
this means that the same headers must be compiled plenty of times.
When we include the moc files directly in the C++ file, they are already
available.
2023-07-30 07:27:45 +00:00

404 lines
16 KiB
C++

/**
* SPDX-FileCopyrightText: 2013 Albert Vaca <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "mpriscontrolplugin.h"
#include <QDBusArgument>
#include <QDBusMessage>
#include <QDBusReply>
#include <QDBusServiceWatcher>
#include <qdbusconnectioninterface.h>
#include <KPluginFactory>
#include <core/device.h>
#include <dbushelper.h>
#include "generated/systeminterfaces/dbusproperties.h"
#include "generated/systeminterfaces/mprisplayer.h"
#include "generated/systeminterfaces/mprisroot.h"
#include "plugin_mpris_debug.h"
K_PLUGIN_CLASS_WITH_JSON(MprisControlPlugin, "kdeconnect_mpriscontrol.json")
MprisPlayer::MprisPlayer(const QString &serviceName, const QString &dbusObjectPath, const QDBusConnection &busConnection)
: m_serviceName(serviceName)
, m_propertiesInterface(new OrgFreedesktopDBusPropertiesInterface(serviceName, dbusObjectPath, busConnection))
, m_mediaPlayer2PlayerInterface(new OrgMprisMediaPlayer2PlayerInterface(serviceName, dbusObjectPath, busConnection))
{
m_mediaPlayer2PlayerInterface->setTimeout(500);
}
MprisControlPlugin::MprisControlPlugin(QObject *parent, const QVariantList &args)
: KdeConnectPlugin(parent, args)
, prevVolume(-1)
{
m_watcher = new QDBusServiceWatcher(QString(), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this);
// TODO: QDBusConnectionInterface::serviceOwnerChanged is deprecated, maybe query org.freedesktop.DBus directly?
connect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &MprisControlPlugin::serviceOwnerChanged);
// Add existing interfaces
const QStringList services = QDBusConnection::sessionBus().interface()->registeredServiceNames().value();
for (const QString &service : services) {
// The string doesn't matter, it just needs to be empty/non-empty
serviceOwnerChanged(service, QLatin1String(""), QStringLiteral("1"));
}
}
// Copied from the mpris2 dataengine in the plasma-workspace repository
void MprisControlPlugin::serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
{
if (!serviceName.startsWith(QStringLiteral("org.mpris.MediaPlayer2.")))
return;
if (serviceName.startsWith(QStringLiteral("org.mpris.MediaPlayer2.kdeconnect.")))
return;
// playerctld is a only a proxy to other media players, and can thus safely be ignored
if (serviceName == QStringLiteral("org.mpris.MediaPlayer2.playerctld"))
return;
if (!oldOwner.isEmpty()) {
qCDebug(KDECONNECT_PLUGIN_MPRIS) << "MPRIS service" << serviceName << "just went offline";
removePlayer(serviceName);
}
if (!newOwner.isEmpty()) {
qCDebug(KDECONNECT_PLUGIN_MPRIS) << "MPRIS service" << serviceName << "just came online";
addPlayer(serviceName);
}
}
void MprisControlPlugin::addPlayer(const QString &service)
{
const QString mediaPlayerObjectPath = QStringLiteral("/org/mpris/MediaPlayer2");
OrgMprisMediaPlayer2Interface iface(service, mediaPlayerObjectPath, QDBusConnection::sessionBus());
QString identity = iface.identity();
if (identity.isEmpty()) {
identity = service.mid(sizeof("org.mpris.MediaPlayer2"));
}
QString uniqueName = identity;
for (int i = 2; playerList.contains(uniqueName); ++i) {
uniqueName = identity + QLatin1String(" [") + QString::number(i) + QLatin1Char(']');
}
MprisPlayer player(service, mediaPlayerObjectPath, QDBusConnection::sessionBus());
playerList.insert(uniqueName, player);
connect(player.propertiesInterface(), &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &MprisControlPlugin::propertiesChanged);
connect(player.mediaPlayer2PlayerInterface(), &OrgMprisMediaPlayer2PlayerInterface::Seeked, this, &MprisControlPlugin::seeked);
qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Mpris addPlayer" << service << "->" << uniqueName;
sendPlayerList();
}
void MprisControlPlugin::seeked(qlonglong position)
{
// qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Seeked in player";
OrgMprisMediaPlayer2PlayerInterface *mediaPlayer2PlayerInterface = (OrgMprisMediaPlayer2PlayerInterface *)sender();
const auto end = playerList.constEnd();
const auto it = std::find_if(playerList.constBegin(), end, [mediaPlayer2PlayerInterface](const MprisPlayer &player) {
return (player.mediaPlayer2PlayerInterface() == mediaPlayer2PlayerInterface);
});
if (it == end) {
qCWarning(KDECONNECT_PLUGIN_MPRIS) << "Seeked signal received for no longer tracked service" << mediaPlayer2PlayerInterface->service();
return;
}
const QString &playerName = it.key();
NetworkPacket np(PACKET_TYPE_MPRIS,
{{QStringLiteral("pos"), position / 1000}, // Send milis instead of nanos
{QStringLiteral("player"), playerName}});
sendPacket(np);
}
void MprisControlPlugin::propertiesChanged(const QString &propertyInterface, const QVariantMap &properties)
{
Q_UNUSED(propertyInterface);
OrgFreedesktopDBusPropertiesInterface *propertiesInterface = (OrgFreedesktopDBusPropertiesInterface *)sender();
const auto end = playerList.constEnd();
const auto it = std::find_if(playerList.constBegin(), end, [propertiesInterface](const MprisPlayer &player) {
return (player.propertiesInterface() == propertiesInterface);
});
if (it == end) {
qCWarning(KDECONNECT_PLUGIN_MPRIS) << "PropertiesChanged signal received for no longer tracked service" << propertiesInterface->service();
return;
}
OrgMprisMediaPlayer2PlayerInterface *const mediaPlayer2PlayerInterface = it.value().mediaPlayer2PlayerInterface();
const QString &playerName = it.key();
NetworkPacket np(PACKET_TYPE_MPRIS);
bool somethingToSend = false;
if (properties.contains(QStringLiteral("Volume"))) {
int volume = (int)(properties[QStringLiteral("Volume")].toDouble() * 100);
if (volume != prevVolume) {
np.set(QStringLiteral("volume"), volume);
prevVolume = volume;
somethingToSend = true;
}
}
if (properties.contains(QStringLiteral("Metadata"))) {
QDBusArgument aux = qvariant_cast<QDBusArgument>(properties[QStringLiteral("Metadata")]);
QVariantMap nowPlayingMap;
aux >> nowPlayingMap;
mprisPlayerMetadataToNetworkPacket(np, nowPlayingMap);
somethingToSend = true;
}
if (properties.contains(QStringLiteral("PlaybackStatus"))) {
bool playing = (properties[QStringLiteral("PlaybackStatus")].toString() == QLatin1String("Playing"));
np.set(QStringLiteral("isPlaying"), playing);
somethingToSend = true;
}
if (properties.contains(QStringLiteral("LoopStatus"))) {
np.set(QStringLiteral("loopStatus"), properties[QStringLiteral("LoopStatus")]);
somethingToSend = true;
}
if (properties.contains(QStringLiteral("Shuffle"))) {
np.set(QStringLiteral("shuffle"), properties[QStringLiteral("Shuffle")].toBool());
somethingToSend = true;
}
if (properties.contains(QStringLiteral("CanPause"))) {
np.set(QStringLiteral("canPause"), properties[QStringLiteral("CanPause")].toBool());
somethingToSend = true;
}
if (properties.contains(QStringLiteral("CanPlay"))) {
np.set(QStringLiteral("canPlay"), properties[QStringLiteral("CanPlay")].toBool());
somethingToSend = true;
}
if (properties.contains(QStringLiteral("CanGoNext"))) {
np.set(QStringLiteral("canGoNext"), properties[QStringLiteral("CanGoNext")].toBool());
somethingToSend = true;
}
if (properties.contains(QStringLiteral("CanGoPrevious"))) {
np.set(QStringLiteral("canGoPrevious"), properties[QStringLiteral("CanGoPrevious")].toBool());
somethingToSend = true;
}
if (somethingToSend) {
np.set(QStringLiteral("player"), playerName);
// Always also update the position if can seek
bool canSeek = mediaPlayer2PlayerInterface->canSeek();
np.set(QStringLiteral("canSeek"), canSeek);
if (canSeek) {
long long pos = mediaPlayer2PlayerInterface->position();
np.set(QStringLiteral("pos"), pos / 1000); // Send milis instead of nanos
}
sendPacket(np);
}
}
void MprisControlPlugin::removePlayer(const QString &serviceName)
{
const auto end = playerList.end();
const auto it = std::find_if(playerList.begin(), end, [serviceName](const MprisPlayer &player) {
return (player.serviceName() == serviceName);
});
if (it == end) {
qCWarning(KDECONNECT_PLUGIN_MPRIS) << "Could not find player for serviceName" << serviceName;
return;
}
const QString &playerName = it.key();
qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Mpris removePlayer" << serviceName << "->" << playerName;
playerList.erase(it);
sendPlayerList();
}
bool MprisControlPlugin::sendAlbumArt(const NetworkPacket &np)
{
const QString player = np.get<QString>(QStringLiteral("player"));
auto it = playerList.find(player);
bool valid_player = (it != playerList.end());
if (!valid_player) {
return false;
}
// Get mpris information
auto &mprisInterface = *it.value().mediaPlayer2PlayerInterface();
QVariantMap nowPlayingMap = mprisInterface.metadata();
// Check if the supplied album art url indeed belongs to this mpris player
QUrl playerAlbumArtUrl{nowPlayingMap[QStringLiteral("mpris:artUrl")].toString()};
QString requestedAlbumArtUrl = np.get<QString>(QStringLiteral("albumArtUrl"));
if (!playerAlbumArtUrl.isValid() || playerAlbumArtUrl != QUrl(requestedAlbumArtUrl)) {
return false;
}
// Only support sending local files
if (playerAlbumArtUrl.scheme() != QStringLiteral("file")) {
return false;
}
// Open the file to send
QSharedPointer<QFile> art{new QFile(playerAlbumArtUrl.toLocalFile())};
// Send the album art as payload
NetworkPacket answer(PACKET_TYPE_MPRIS);
answer.set(QStringLiteral("transferringAlbumArt"), true);
answer.set(QStringLiteral("player"), player);
answer.set(QStringLiteral("albumArtUrl"), requestedAlbumArtUrl);
answer.setPayload(art, art->size());
sendPacket(answer);
return true;
}
bool MprisControlPlugin::receivePacket(const NetworkPacket &np)
{
if (np.has(QStringLiteral("playerList"))) {
return false; // Whoever sent this is an mpris client and not an mpris control!
}
if (np.has(QStringLiteral("albumArtUrl"))) {
return sendAlbumArt(np);
}
// Send the player list
const QString player = np.get<QString>(QStringLiteral("player"));
auto it = playerList.find(player);
bool valid_player = (it != playerList.end());
if (!valid_player || np.get<bool>(QStringLiteral("requestPlayerList"))) {
sendPlayerList();
if (!valid_player) {
return true;
}
}
// Do something to the mpris interface
const QString &serviceName = it.value().serviceName();
// turn from pointer to reference to keep the patch diff small,
// actual patch would change all "mprisInterface." into "mprisInterface->"
auto &mprisInterface = *it.value().mediaPlayer2PlayerInterface();
if (np.has(QStringLiteral("action"))) {
const QString &action = np.get<QString>(QStringLiteral("action"));
// qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Calling action" << action << "in" << serviceName;
// TODO: Check for valid actions, currently we trust anything the other end sends us
mprisInterface.call(action);
}
if (np.has(QStringLiteral("setLoopStatus"))) {
const QString &loopStatus = np.get<QString>(QStringLiteral("setLoopStatus"));
qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting loopStatus" << loopStatus << "to" << serviceName;
mprisInterface.setLoopStatus(loopStatus);
}
if (np.has(QStringLiteral("setShuffle"))) {
bool shuffle = np.get<bool>(QStringLiteral("setShuffle"));
qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting shuffle" << shuffle << "to" << serviceName;
mprisInterface.setShuffle(shuffle);
}
if (np.has(QStringLiteral("setVolume"))) {
double volume = np.get<int>(QStringLiteral("setVolume")) / 100.f;
qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting volume" << volume << "to" << serviceName;
mprisInterface.setVolume(volume);
}
if (np.has(QStringLiteral("Seek"))) {
int offset = np.get<int>(QStringLiteral("Seek"));
// qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Seeking" << offset << "to" << serviceName;
mprisInterface.Seek(offset);
}
if (np.has(QStringLiteral("SetPosition"))) {
qlonglong position = np.get<qlonglong>(QStringLiteral("SetPosition"), 0) * 1000;
qlonglong seek = position - mprisInterface.position();
// qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting position by seeking" << seek << "to" << serviceName;
mprisInterface.Seek(seek);
}
// Send something read from the mpris interface
NetworkPacket answer(PACKET_TYPE_MPRIS);
bool somethingToSend = false;
if (np.get<bool>(QStringLiteral("requestNowPlaying"))) {
QVariantMap nowPlayingMap = mprisInterface.metadata();
mprisPlayerMetadataToNetworkPacket(answer, nowPlayingMap);
qlonglong pos = mprisInterface.position();
answer.set(QStringLiteral("pos"), pos / 1000);
bool playing = (mprisInterface.playbackStatus() == QLatin1String("Playing"));
answer.set(QStringLiteral("isPlaying"), playing);
answer.set(QStringLiteral("canPause"), mprisInterface.canPause());
answer.set(QStringLiteral("canPlay"), mprisInterface.canPlay());
answer.set(QStringLiteral("canGoNext"), mprisInterface.canGoNext());
answer.set(QStringLiteral("canGoPrevious"), mprisInterface.canGoPrevious());
answer.set(QStringLiteral("canSeek"), mprisInterface.canSeek());
// LoopStatus is an optional field
if (mprisInterface.property("LoopStatus").isValid()) {
const QString &loopStatus = mprisInterface.loopStatus();
answer.set(QStringLiteral("loopStatus"), loopStatus);
}
// Shuffle is an optional field
if (mprisInterface.property("Shuffle").isValid()) {
bool shuffle = mprisInterface.shuffle();
answer.set(QStringLiteral("shuffle"), shuffle);
}
somethingToSend = true;
}
if (np.get<bool>(QStringLiteral("requestVolume"))) {
int volume = (int)(mprisInterface.volume() * 100);
answer.set(QStringLiteral("volume"), volume);
somethingToSend = true;
}
if (somethingToSend) {
answer.set(QStringLiteral("player"), player);
sendPacket(answer);
}
return true;
}
void MprisControlPlugin::sendPlayerList()
{
NetworkPacket np(PACKET_TYPE_MPRIS);
np.set(QStringLiteral("playerList"), playerList.keys());
np.set(QStringLiteral("supportAlbumArtPayload"), true);
sendPacket(np);
}
void MprisControlPlugin::mprisPlayerMetadataToNetworkPacket(NetworkPacket &np, const QVariantMap &nowPlayingMap) const
{
QString title = nowPlayingMap[QStringLiteral("xesam:title")].toString();
QString artist = nowPlayingMap[QStringLiteral("xesam:artist")].toStringList().join(QLatin1String(", "));
QString album = nowPlayingMap[QStringLiteral("xesam:album")].toString();
QString albumArtUrl = nowPlayingMap[QStringLiteral("mpris:artUrl")].toString();
QUrl fileUrl = nowPlayingMap[QStringLiteral("xesam:url")].toUrl();
if (title.isEmpty() && artist.isEmpty() && fileUrl.isLocalFile()) {
title = fileUrl.fileName();
QStringList splitUrl = fileUrl.path().split(QDir::separator());
if (album.isEmpty() && splitUrl.size() > 1) {
album = splitUrl.at(splitUrl.size() - 2);
}
}
np.set(QStringLiteral("title"), title);
np.set(QStringLiteral("artist"), artist);
np.set(QStringLiteral("album"), album);
np.set(QStringLiteral("albumArtUrl"), albumArtUrl);
bool hasLength = false;
long long length = nowPlayingMap[QStringLiteral("mpris:length")].toLongLong(&hasLength) / 1000; // nanoseconds to milliseconds
if (!hasLength) {
length = -1;
}
np.set(QStringLiteral("length"), length);
np.set(QStringLiteral("url"), fileUrl);
}
#include "moc_mpriscontrolplugin.cpp"
#include "mpriscontrolplugin.moc"