File indexing completed on 2023-05-30 11:40:29

0001 /*
0002     Now playing
0003     Copyright (C) 2017  James D. Smith <smithjd15@gmail.com>
0004     Copyright (C) 2011  Martin Klapetek <martin.klapetek@gmail.com>
0005 
0006     This library is free software; you can redistribute it and/or
0007     modify it under the terms of the GNU Lesser General Public
0008     License as published by the Free Software Foundation; either
0009     version 2.1 of the License, or (at your option) any later version.
0010 
0011     This library is distributed in the hope that it will be useful,
0012     but WITHOUT ANY WARRANTY; without even the implied warranty of
0013     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0014     Lesser General Public License for more details.
0015 
0016     You should have received a copy of the GNU Lesser General Public
0017     License along with this library; if not, write to the Free Software
0018     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
0019 */
0020 
0021 #include "telepathy-mpris.h"
0022 #include "ktp_kded_debug.h"
0023 
0024 #include <KConfig>
0025 #include <KConfigGroup>
0026 #include <KSharedConfig>
0027 
0028 #include <QTimer>
0029 #include <QDBusConnectionInterface>
0030 #include <QDBusInterface>
0031 #include <QDBusReply>
0032 
0033 static const QLatin1String dbusInterfaceProperties("org.freedesktop.DBus.Properties");
0034 
0035 static const QLatin1String mprisPath("/org/mpris/MediaPlayer2");
0036 static const QLatin1String mprisServicePrefix("org.mpris.MediaPlayer2");
0037 static const QLatin1String mprisInterfaceName("org.mpris.MediaPlayer2.Player");
0038 
0039 TelepathyMPRIS::TelepathyMPRIS(QObject* parent)
0040     : QObject(parent),
0041     m_activationTimer(new QTimer()),
0042     m_activePlayer(new Player())
0043 {
0044     connect(this, &TelepathyMPRIS::playerChange, &m_initLoop, &QEventLoop::quit);
0045 
0046     m_activationTimer->setSingleShot(true);
0047     m_activationTimer->setInterval(400);
0048 }
0049 
0050 TelepathyMPRIS::~TelepathyMPRIS()
0051 {
0052 }
0053 
0054 void TelepathyMPRIS::enable(bool enable)
0055 {
0056     if (enable && !m_timerConnection) {
0057         //player changed
0058         m_timerConnection = QObject::connect(m_activationTimer, &QTimer::timeout, m_activationTimer, [=] {
0059             auto getPlayers = [this] (Service state) {
0060                 QList<Player*> players;
0061                 for (Player *watchedPlayer : m_players.values()) {
0062                     if (watchedPlayer->playState == state) {
0063                         players << watchedPlayer;
0064                     }
0065                 }
0066 
0067                 return players;
0068             };
0069 
0070             if (m_activePlayer->playState < Paused) {
0071                 QList<Player*> players = QList<Player*>() << getPlayers(Playing) << getPlayers(Paused);
0072 
0073                 //if the players list is empty create a new default active player
0074                 if (players.isEmpty()) {
0075                     m_activePlayer = new Player();
0076                 } else {
0077                     m_activePlayer = players.at(0);
0078                 }
0079 
0080                 qCDebug(KTP_KDED_MODULE) << "Active player changed:" << m_players.key(m_activePlayer);
0081             }
0082 
0083             if (!m_initLoop.isRunning()) {
0084                 Q_EMIT playerChange();
0085             } else {
0086                 m_initLoop.quit();
0087             }
0088         });
0089 
0090         //get registered service names asynchronously
0091         QDBusPendingCall async = QDBusConnection::sessionBus().interface()->asyncCall(QLatin1String("ListNames"));
0092         QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
0093         connect(callWatcher, &QDBusPendingCallWatcher::finished, callWatcher, [=] {
0094             bool foundPlayers = false;
0095             QDBusPendingReply<QStringList> reply = *callWatcher;
0096             if (reply.isError()) {
0097                 qCDebug(KTP_KDED_MODULE) << reply.error();
0098                 return;
0099             }
0100 
0101             for (const QString &serviceName : reply.value()) {
0102                 if (serviceName.startsWith(mprisServicePrefix)) {
0103                     requestPlaybackStatus(serviceName, QDBusConnection::sessionBus().interface()->serviceOwner(serviceName));
0104                     foundPlayers = true;
0105                 }
0106             }
0107 
0108             if (!foundPlayers) {
0109                 m_initLoop.quit();
0110             }
0111 
0112             callWatcher->deleteLater();
0113         });
0114 
0115         //watch for mpris2-enabled players appearing and disappearing
0116         connect(QDBusConnection::sessionBus().interface(),
0117                     &QDBusConnectionInterface::serviceOwnerChanged,
0118                     this,
0119                     &TelepathyMPRIS::serviceOwnerChanged);
0120 
0121         m_initLoop.exec();
0122     } else if (!enable) {
0123         disconnect(m_timerConnection);
0124         disconnect(QDBusConnection::sessionBus().interface(),
0125                     &QDBusConnectionInterface::serviceOwnerChanged,
0126                     this,
0127                     &TelepathyMPRIS::serviceOwnerChanged);
0128 
0129         QHash<QString, QString> serviceNameByOwner = m_serviceNameByOwner;
0130         for (const QString &serviceName : serviceNameByOwner) {
0131             serviceOwnerChanged(serviceName, serviceNameByOwner.key(serviceName), QString());
0132         }
0133     }
0134 }
0135 
0136 void TelepathyMPRIS::onPlayerSignalReceived(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties)
0137 {
0138     Q_UNUSED(invalidatedProperties)
0139 
0140     //this is not the correct property interface, ignore
0141     if (interface != mprisInterfaceName) {
0142         return;
0143     }
0144 
0145     const QString &serviceName = m_serviceNameByOwner[QDBusConnection::sessionBus().interface()->serviceOwner(message().service())];
0146     sortPlayerReply(changedProperties, serviceName);
0147 }
0148 
0149 void TelepathyMPRIS::requestPlaybackStatus(const QString &serviceName, const QString &owner)
0150 {
0151     auto playerConnected = [=] () {
0152         if (m_players.keys().contains(serviceName)) {
0153             return true;
0154         }
0155 
0156         bool connected = QDBusConnection::sessionBus().connect(serviceName,
0157                                       mprisPath,
0158                                       dbusInterfaceProperties,
0159                                       QLatin1String("PropertiesChanged"),
0160                                       this,
0161                                       SLOT(onPlayerSignalReceived(QString,QVariantMap,QStringList)));
0162 
0163         if (connected) {
0164             qCDebug(KTP_KDED_MODULE) << "Found player" << serviceName;
0165             Player *player = new Player();
0166             m_players.insert(serviceName, player);
0167             m_serviceNameByOwner.insert(owner, serviceName);
0168         }
0169 
0170         return connected;
0171     };
0172 
0173     QDBusMessage mprisMsg = QDBusMessage::createMethodCall(serviceName, mprisPath, dbusInterfaceProperties, QLatin1String("GetAll"));
0174     mprisMsg.setArguments(QList<QVariant>() << mprisInterfaceName);
0175 
0176     QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(mprisMsg);
0177     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
0178     connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [=] {
0179         QDBusPendingReply<QVariantMap> reply = *watcher;
0180 
0181         if (reply.isError()) {
0182             qCWarning(KTP_KDED_MODULE) << "Received error reply from DBus" << reply.error() << "service" << serviceName;
0183         } else if (playerConnected()) {
0184             sortPlayerReply(reply.value(), serviceName);
0185         }
0186 
0187         watcher->deleteLater();
0188     });
0189 }
0190 
0191 void TelepathyMPRIS::serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
0192 {
0193     auto playerVanished = [=] (const QString &serviceName, const QString &oldOwner) {
0194         if (m_players.keys().contains(serviceName)) {
0195             QDBusConnection::sessionBus().disconnect(serviceName,
0196                                                      mprisPath,
0197                                                      dbusInterfaceProperties,
0198                                                      QLatin1String("PropertiesChanged"),
0199                                                      this,
0200                                                      SLOT(onPlayerSignalReceived(QString,QVariantMap,QStringList)));
0201 
0202             m_players[serviceName]->playState = Service::Unknown;
0203             if (m_players[serviceName] == m_activePlayer) {
0204                 m_activationTimer->start();
0205             }
0206             m_players.remove(serviceName);
0207             m_serviceNameByOwner.remove(oldOwner);
0208             qCDebug(KTP_KDED_MODULE) << "Player" << serviceName << "is no longer available";
0209         }
0210     };
0211 
0212     if (serviceName.startsWith(mprisServicePrefix)) {
0213         qCDebug(KTP_KDED_MODULE) << "DBus service name change:" << serviceName << "once owned by" << oldOwner << "is now owned by" << newOwner;
0214         if (oldOwner.isEmpty()) {
0215             //if we have no oldOwner, we have new player registered at dbus
0216             requestPlaybackStatus(serviceName, newOwner);
0217         } else if (newOwner.isEmpty()) {
0218             //if there's no newOwner, the player quit
0219             playerVanished(serviceName, oldOwner);
0220         } else if (!oldOwner.isEmpty() && !newOwner.isEmpty()) {
0221             //otherwise the service name owner changed
0222             m_serviceNameByOwner.remove(oldOwner);
0223             m_serviceNameByOwner.insert(newOwner, serviceName);
0224             requestPlaybackStatus(serviceName, newOwner);
0225         }
0226     }
0227 }
0228 
0229 void TelepathyMPRIS::sortPlayerReply(const QVariantMap &serviceInfo, const QString &serviceName)
0230 {
0231     bool playerChanged = false;
0232 
0233     //sort and store incoming metadata
0234     if (serviceInfo.keys().contains(QLatin1String("Metadata"))) {
0235         QVariantMap metadata = qdbus_cast<QVariantMap>(serviceInfo.value(QLatin1String("Metadata")));
0236         if (m_players[serviceName]->metadata != metadata) {
0237             m_players[serviceName]->metadata = metadata;
0238             playerChanged = true;
0239         }
0240     }
0241 
0242     //sort and store incoming playback information
0243     if (serviceInfo.keys().contains(QLatin1String("PlaybackStatus"))) {
0244         QVariant playbackStatus = serviceInfo.value(QLatin1String("PlaybackStatus"));
0245         auto playState = [] (const QVariant &playbackStatus) {
0246             TelepathyMPRIS::Service playState;
0247             if (playbackStatus == QLatin1String("Playing")) {
0248                 playState = Service::Playing;
0249             } else if (playbackStatus == QLatin1String("Paused")) {
0250                 playState = Service::Paused;
0251             } else if (playbackStatus == QLatin1String("Stopped")) {
0252                 playState = Service::Stopped;
0253             } else {
0254                 playState = Service::Unknown;
0255             }
0256 
0257             return playState;
0258         };
0259 
0260         if (m_players[serviceName]->playState != playState(playbackStatus)) {
0261             if ((m_players[serviceName]->playState == Unknown) && (playState(playbackStatus) < Playing)) {
0262                 playerChanged = false;
0263             } else {
0264                 playerChanged = true;
0265             }
0266 
0267             m_players[serviceName]->playState = playState(serviceInfo.value(QLatin1String("PlaybackStatus")));
0268         }
0269     }
0270 
0271     qCDebug(KTP_KDED_MODULE) << "Player" << serviceName << m_players[serviceName]->playState;
0272 
0273     if (playerChanged || m_initLoop.isRunning()) {
0274         m_activationTimer->start();
0275     }
0276 }