File indexing completed on 2024-04-21 04:56:51

0001 /**
0002  * SPDX-FileCopyrightText: 2013 Albert Vaca <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "mpriscontrolplugin.h"
0008 
0009 #include <QDBusArgument>
0010 #include <QDBusMessage>
0011 #include <QDBusReply>
0012 #include <QDBusServiceWatcher>
0013 #include <qdbusconnectioninterface.h>
0014 
0015 #include <KPluginFactory>
0016 
0017 #include <core/device.h>
0018 #include <dbushelper.h>
0019 
0020 #include "generated/systeminterfaces/dbusproperties.h"
0021 #include "generated/systeminterfaces/mprisplayer.h"
0022 #include "generated/systeminterfaces/mprisroot.h"
0023 #include "plugin_mpriscontrol_debug.h"
0024 
0025 K_PLUGIN_CLASS_WITH_JSON(MprisControlPlugin, "kdeconnect_mpriscontrol.json")
0026 
0027 MprisPlayer::MprisPlayer(const QString &serviceName, const QString &dbusObjectPath, const QDBusConnection &busConnection)
0028     : m_serviceName(serviceName)
0029     , m_propertiesInterface(new OrgFreedesktopDBusPropertiesInterface(serviceName, dbusObjectPath, busConnection))
0030     , m_mediaPlayer2PlayerInterface(new OrgMprisMediaPlayer2PlayerInterface(serviceName, dbusObjectPath, busConnection))
0031 {
0032     m_mediaPlayer2PlayerInterface->setTimeout(500);
0033 }
0034 
0035 MprisControlPlugin::MprisControlPlugin(QObject *parent, const QVariantList &args)
0036     : KdeConnectPlugin(parent, args)
0037     , prevVolume(-1)
0038 {
0039     m_watcher = new QDBusServiceWatcher(QString(), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this);
0040 
0041     // TODO: QDBusConnectionInterface::serviceOwnerChanged is deprecated, maybe query org.freedesktop.DBus directly?
0042     connect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &MprisControlPlugin::serviceOwnerChanged);
0043 
0044     // Add existing interfaces
0045     const QStringList services = QDBusConnection::sessionBus().interface()->registeredServiceNames().value();
0046     for (const QString &service : services) {
0047         // The string doesn't matter, it just needs to be empty/non-empty
0048         serviceOwnerChanged(service, QLatin1String(""), QStringLiteral("1"));
0049     }
0050 }
0051 
0052 // Copied from the mpris2 dataengine in the plasma-workspace repository
0053 void MprisControlPlugin::serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
0054 {
0055     if (!serviceName.startsWith(QStringLiteral("org.mpris.MediaPlayer2.")))
0056         return;
0057     if (serviceName.startsWith(QStringLiteral("org.mpris.MediaPlayer2.kdeconnect.")))
0058         return;
0059     // playerctld is a only a proxy to other media players, and can thus safely be ignored
0060     if (serviceName == QStringLiteral("org.mpris.MediaPlayer2.playerctld"))
0061         return;
0062 
0063     if (!oldOwner.isEmpty()) {
0064         qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << "MPRIS service" << serviceName << "just went offline";
0065         removePlayer(serviceName);
0066     }
0067 
0068     if (!newOwner.isEmpty()) {
0069         qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << "MPRIS service" << serviceName << "just came online";
0070         addPlayer(serviceName);
0071     }
0072 }
0073 
0074 void MprisControlPlugin::addPlayer(const QString &service)
0075 {
0076     const QString mediaPlayerObjectPath = QStringLiteral("/org/mpris/MediaPlayer2");
0077 
0078     OrgMprisMediaPlayer2Interface iface(service, mediaPlayerObjectPath, QDBusConnection::sessionBus());
0079     QString identity = iface.identity();
0080 
0081     if (identity.isEmpty()) {
0082         identity = service.mid(sizeof("org.mpris.MediaPlayer2"));
0083     }
0084 
0085     QString uniqueName = identity;
0086     for (int i = 2; playerList.contains(uniqueName); ++i) {
0087         uniqueName = identity + QLatin1String(" [") + QString::number(i) + QLatin1Char(']');
0088     }
0089 
0090     MprisPlayer player(service, mediaPlayerObjectPath, QDBusConnection::sessionBus());
0091 
0092     playerList.insert(uniqueName, player);
0093 
0094     connect(player.propertiesInterface(), &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &MprisControlPlugin::propertiesChanged);
0095     connect(player.mediaPlayer2PlayerInterface(), &OrgMprisMediaPlayer2PlayerInterface::Seeked, this, &MprisControlPlugin::seeked);
0096 
0097     qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << "Mpris addPlayer" << service << "->" << uniqueName;
0098     sendPlayerList();
0099 }
0100 
0101 void MprisControlPlugin::seeked(qlonglong position)
0102 {
0103     // qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Seeked in player";
0104     OrgMprisMediaPlayer2PlayerInterface *mediaPlayer2PlayerInterface = (OrgMprisMediaPlayer2PlayerInterface *)sender();
0105     const auto end = playerList.constEnd();
0106     const auto it = std::find_if(playerList.constBegin(), end, [mediaPlayer2PlayerInterface](const MprisPlayer &player) {
0107         return (player.mediaPlayer2PlayerInterface() == mediaPlayer2PlayerInterface);
0108     });
0109     if (it == end) {
0110         qCWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Seeked signal received for no longer tracked service" << mediaPlayer2PlayerInterface->service();
0111         return;
0112     }
0113 
0114     const QString &playerName = it.key();
0115 
0116     NetworkPacket np(PACKET_TYPE_MPRIS,
0117                      {{QStringLiteral("pos"), position / 1000}, // Send milis instead of nanos
0118                       {QStringLiteral("player"), playerName}});
0119     sendPacket(np);
0120 }
0121 
0122 void MprisControlPlugin::propertiesChanged(const QString & /*propertyInterface*/, const QVariantMap &properties)
0123 {
0124     OrgFreedesktopDBusPropertiesInterface *propertiesInterface = (OrgFreedesktopDBusPropertiesInterface *)sender();
0125     const auto end = playerList.constEnd();
0126     const auto it = std::find_if(playerList.constBegin(), end, [propertiesInterface](const MprisPlayer &player) {
0127         return (player.propertiesInterface() == propertiesInterface);
0128     });
0129     if (it == end) {
0130         qCWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "PropertiesChanged signal received for no longer tracked service" << propertiesInterface->service();
0131         return;
0132     }
0133 
0134     OrgMprisMediaPlayer2PlayerInterface *const mediaPlayer2PlayerInterface = it.value().mediaPlayer2PlayerInterface();
0135     const QString &playerName = it.key();
0136 
0137     NetworkPacket np(PACKET_TYPE_MPRIS);
0138     bool somethingToSend = false;
0139     if (properties.contains(QStringLiteral("Volume"))) {
0140         int volume = (int)(properties[QStringLiteral("Volume")].toDouble() * 100);
0141         if (volume != prevVolume) {
0142             np.set(QStringLiteral("volume"), volume);
0143             prevVolume = volume;
0144             somethingToSend = true;
0145         }
0146     }
0147     if (properties.contains(QStringLiteral("Metadata"))) {
0148         QDBusArgument aux = qvariant_cast<QDBusArgument>(properties[QStringLiteral("Metadata")]);
0149         QVariantMap nowPlayingMap;
0150         aux >> nowPlayingMap;
0151 
0152         mprisPlayerMetadataToNetworkPacket(np, nowPlayingMap);
0153         somethingToSend = true;
0154     }
0155     if (properties.contains(QStringLiteral("PlaybackStatus"))) {
0156         bool playing = (properties[QStringLiteral("PlaybackStatus")].toString() == QLatin1String("Playing"));
0157         np.set(QStringLiteral("isPlaying"), playing);
0158         somethingToSend = true;
0159     }
0160     if (properties.contains(QStringLiteral("LoopStatus"))) {
0161         np.set(QStringLiteral("loopStatus"), properties[QStringLiteral("LoopStatus")]);
0162         somethingToSend = true;
0163     }
0164     if (properties.contains(QStringLiteral("Shuffle"))) {
0165         np.set(QStringLiteral("shuffle"), properties[QStringLiteral("Shuffle")].toBool());
0166         somethingToSend = true;
0167     }
0168     if (properties.contains(QStringLiteral("CanPause"))) {
0169         np.set(QStringLiteral("canPause"), properties[QStringLiteral("CanPause")].toBool());
0170         somethingToSend = true;
0171     }
0172     if (properties.contains(QStringLiteral("CanPlay"))) {
0173         np.set(QStringLiteral("canPlay"), properties[QStringLiteral("CanPlay")].toBool());
0174         somethingToSend = true;
0175     }
0176     if (properties.contains(QStringLiteral("CanGoNext"))) {
0177         np.set(QStringLiteral("canGoNext"), properties[QStringLiteral("CanGoNext")].toBool());
0178         somethingToSend = true;
0179     }
0180     if (properties.contains(QStringLiteral("CanGoPrevious"))) {
0181         np.set(QStringLiteral("canGoPrevious"), properties[QStringLiteral("CanGoPrevious")].toBool());
0182         somethingToSend = true;
0183     }
0184 
0185     if (somethingToSend) {
0186         np.set(QStringLiteral("player"), playerName);
0187         // Always also update the position if can seek
0188         bool canSeek = mediaPlayer2PlayerInterface->canSeek();
0189         np.set(QStringLiteral("canSeek"), canSeek);
0190         if (canSeek) {
0191             long long pos = mediaPlayer2PlayerInterface->position();
0192             np.set(QStringLiteral("pos"), pos / 1000); // Send milis instead of nanos
0193         }
0194         sendPacket(np);
0195     }
0196 }
0197 
0198 void MprisControlPlugin::removePlayer(const QString &serviceName)
0199 {
0200     const auto end = playerList.end();
0201     const auto it = std::find_if(playerList.begin(), end, [serviceName](const MprisPlayer &player) {
0202         return (player.serviceName() == serviceName);
0203     });
0204     if (it == end) {
0205         qCWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Could not find player for serviceName" << serviceName;
0206         return;
0207     }
0208 
0209     const QString &playerName = it.key();
0210     qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << "Mpris removePlayer" << serviceName << "->" << playerName;
0211 
0212     playerList.erase(it);
0213 
0214     sendPlayerList();
0215 }
0216 
0217 bool MprisControlPlugin::sendAlbumArt(const NetworkPacket &np)
0218 {
0219     const QString player = np.get<QString>(QStringLiteral("player"));
0220     auto it = playerList.find(player);
0221     bool valid_player = (it != playerList.end());
0222     if (!valid_player) {
0223         return false;
0224     }
0225 
0226     // Get mpris information
0227     auto &mprisInterface = *it.value().mediaPlayer2PlayerInterface();
0228     QVariantMap nowPlayingMap = mprisInterface.metadata();
0229 
0230     // Check if the supplied album art url indeed belongs to this mpris player
0231     QUrl playerAlbumArtUrl{nowPlayingMap[QStringLiteral("mpris:artUrl")].toString()};
0232     QString requestedAlbumArtUrl = np.get<QString>(QStringLiteral("albumArtUrl"));
0233     if (!playerAlbumArtUrl.isValid() || playerAlbumArtUrl != QUrl(requestedAlbumArtUrl)) {
0234         return false;
0235     }
0236 
0237     // Only support sending local files
0238     if (playerAlbumArtUrl.scheme() != QStringLiteral("file")) {
0239         return false;
0240     }
0241 
0242     // Open the file to send
0243     QSharedPointer<QFile> art{new QFile(playerAlbumArtUrl.toLocalFile())};
0244 
0245     // Send the album art as payload
0246     NetworkPacket answer(PACKET_TYPE_MPRIS);
0247     answer.set(QStringLiteral("transferringAlbumArt"), true);
0248     answer.set(QStringLiteral("player"), player);
0249     answer.set(QStringLiteral("albumArtUrl"), requestedAlbumArtUrl);
0250     answer.setPayload(art, art->size());
0251     sendPacket(answer);
0252     return true;
0253 }
0254 
0255 void MprisControlPlugin::receivePacket(const NetworkPacket &np)
0256 {
0257     if (np.has(QStringLiteral("playerList"))) {
0258         return; // Whoever sent this is an mpris client and not an mpris control!
0259     }
0260 
0261     if (np.has(QStringLiteral("albumArtUrl"))) {
0262         sendAlbumArt(np);
0263         return;
0264     }
0265 
0266     // Send the player list
0267     const QString player = np.get<QString>(QStringLiteral("player"));
0268     auto it = playerList.find(player);
0269     bool valid_player = (it != playerList.end());
0270     if (!valid_player || np.get<bool>(QStringLiteral("requestPlayerList"))) {
0271         sendPlayerList();
0272         if (!valid_player) {
0273             return;
0274         }
0275     }
0276 
0277     // Do something to the mpris interface
0278     const QString &serviceName = it.value().serviceName();
0279     // turn from pointer to reference to keep the patch diff small,
0280     // actual patch would change all "mprisInterface." into "mprisInterface->"
0281     auto &mprisInterface = *it.value().mediaPlayer2PlayerInterface();
0282     if (np.has(QStringLiteral("action"))) {
0283         const QString &action = np.get<QString>(QStringLiteral("action"));
0284         // qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Calling action" << action << "in" << serviceName;
0285         // TODO: Check for valid actions, currently we trust anything the other end sends us
0286         mprisInterface.call(action);
0287     }
0288     if (np.has(QStringLiteral("setLoopStatus"))) {
0289         const QString &loopStatus = np.get<QString>(QStringLiteral("setLoopStatus"));
0290         qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << "Setting loopStatus" << loopStatus << "to" << serviceName;
0291         mprisInterface.setLoopStatus(loopStatus);
0292     }
0293     if (np.has(QStringLiteral("setShuffle"))) {
0294         bool shuffle = np.get<bool>(QStringLiteral("setShuffle"));
0295         qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << "Setting shuffle" << shuffle << "to" << serviceName;
0296         mprisInterface.setShuffle(shuffle);
0297     }
0298     if (np.has(QStringLiteral("setVolume"))) {
0299         double volume = np.get<int>(QStringLiteral("setVolume")) / 100.f;
0300         qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << "Setting volume" << volume << "to" << serviceName;
0301         mprisInterface.setVolume(volume);
0302     }
0303     if (np.has(QStringLiteral("Seek"))) {
0304         int offset = np.get<int>(QStringLiteral("Seek"));
0305         // qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Seeking" << offset << "to" << serviceName;
0306         mprisInterface.Seek(offset);
0307     }
0308 
0309     if (np.has(QStringLiteral("SetPosition"))) {
0310         qlonglong position = np.get<qlonglong>(QStringLiteral("SetPosition"), 0) * 1000;
0311         qlonglong seek = position - mprisInterface.position();
0312         // qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting position by seeking" << seek << "to" << serviceName;
0313         mprisInterface.Seek(seek);
0314     }
0315 
0316     // Send something read from the mpris interface
0317     NetworkPacket answer(PACKET_TYPE_MPRIS);
0318     bool somethingToSend = false;
0319     if (np.get<bool>(QStringLiteral("requestNowPlaying"))) {
0320         QVariantMap nowPlayingMap = mprisInterface.metadata();
0321         mprisPlayerMetadataToNetworkPacket(answer, nowPlayingMap);
0322 
0323         qlonglong pos = mprisInterface.position();
0324         answer.set(QStringLiteral("pos"), pos / 1000);
0325 
0326         bool playing = (mprisInterface.playbackStatus() == QLatin1String("Playing"));
0327         answer.set(QStringLiteral("isPlaying"), playing);
0328 
0329         answer.set(QStringLiteral("canPause"), mprisInterface.canPause());
0330         answer.set(QStringLiteral("canPlay"), mprisInterface.canPlay());
0331         answer.set(QStringLiteral("canGoNext"), mprisInterface.canGoNext());
0332         answer.set(QStringLiteral("canGoPrevious"), mprisInterface.canGoPrevious());
0333         answer.set(QStringLiteral("canSeek"), mprisInterface.canSeek());
0334 
0335         // LoopStatus is an optional field
0336         if (mprisInterface.property("LoopStatus").isValid()) {
0337             const QString &loopStatus = mprisInterface.loopStatus();
0338             answer.set(QStringLiteral("loopStatus"), loopStatus);
0339         }
0340 
0341         // Shuffle is an optional field
0342         if (mprisInterface.property("Shuffle").isValid()) {
0343             bool shuffle = mprisInterface.shuffle();
0344             answer.set(QStringLiteral("shuffle"), shuffle);
0345         }
0346 
0347         somethingToSend = true;
0348     }
0349     if (np.get<bool>(QStringLiteral("requestVolume"))) {
0350         int volume = (int)(mprisInterface.volume() * 100);
0351         answer.set(QStringLiteral("volume"), volume);
0352         somethingToSend = true;
0353     }
0354 
0355     if (somethingToSend) {
0356         answer.set(QStringLiteral("player"), player);
0357         sendPacket(answer);
0358     }
0359 }
0360 
0361 void MprisControlPlugin::sendPlayerList()
0362 {
0363     NetworkPacket np(PACKET_TYPE_MPRIS);
0364     np.set(QStringLiteral("playerList"), playerList.keys());
0365     np.set(QStringLiteral("supportAlbumArtPayload"), true);
0366     sendPacket(np);
0367 }
0368 
0369 void MprisControlPlugin::mprisPlayerMetadataToNetworkPacket(NetworkPacket &np, const QVariantMap &nowPlayingMap) const
0370 {
0371     QString title = nowPlayingMap[QStringLiteral("xesam:title")].toString();
0372     QString artist = nowPlayingMap[QStringLiteral("xesam:artist")].toStringList().join(QLatin1String(", "));
0373     QString album = nowPlayingMap[QStringLiteral("xesam:album")].toString();
0374     QString albumArtUrl = nowPlayingMap[QStringLiteral("mpris:artUrl")].toString();
0375     QUrl fileUrl = nowPlayingMap[QStringLiteral("xesam:url")].toUrl();
0376 
0377     if (title.isEmpty() && artist.isEmpty() && fileUrl.isLocalFile()) {
0378         title = fileUrl.fileName();
0379 
0380         QStringList splitUrl = fileUrl.path().split(QDir::separator());
0381         if (album.isEmpty() && splitUrl.size() > 1) {
0382             album = splitUrl.at(splitUrl.size() - 2);
0383         }
0384     }
0385 
0386     np.set(QStringLiteral("title"), title);
0387     np.set(QStringLiteral("artist"), artist);
0388     np.set(QStringLiteral("album"), album);
0389     np.set(QStringLiteral("albumArtUrl"), albumArtUrl);
0390 
0391     bool hasLength = false;
0392     long long length = nowPlayingMap[QStringLiteral("mpris:length")].toLongLong(&hasLength) / 1000; // nanoseconds to milliseconds
0393     if (!hasLength) {
0394         length = -1;
0395     }
0396     np.set(QStringLiteral("length"), length);
0397     np.set(QStringLiteral("url"), fileUrl);
0398 }
0399 
0400 #include "moc_mpriscontrolplugin.cpp"
0401 #include "mpriscontrolplugin.moc"