[OpenDocString] kdeconnect-kde (cpp)
mpriscontrolplugin.cpp
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);
}
Constructs a new MprisPlayer object with a given serviceName and dbus object path. The object path is the base path of the dbus object, and the bus connection is the connection to the dbus server.
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"));
    }
}
This constructor builds a KdeConnectPlugin object and its d-pointer. It sets up a QDBusServiceWatcher, connects the service owner change signal to the internal DBus connection and sets up connections for the service owner change events. It also connects the service owner changed signal to the internal DBus interface so that the string is empty and non-empty.
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);
    }
}
If the service name is not a prefix of org.mpris.MediaPlayer2.kdeconnect.... then it means that the player is not going offline. Then it adds the service to the list of players that owns it.
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();
}
This adds a player to the list of players. It creates a new player object, adds it to the list, and sets up connections for the propertiesChanged and seeked interfaces.
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);
}
This sends a network packet to the player that seeked to the given position.
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(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 (properties.contains(QStringLiteral("CanSeek"))) {
        np.set(QStringLiteral("canSeek"), properties[QStringLiteral("CanSeek")].toBool());
        somethingToSend = true;
    }

    if (somethingToSend) {
        np.set(QStringLiteral("player"), playerName);
        // Always also update the position
        if (mediaPlayer2PlayerInterface->canSeek()) {
            long long pos = mediaPlayer2PlayerInterface->position();
            np.set(QStringLiteral("pos"), pos / 1000); // Send milis instead of nanos
        }
        sendPacket(np);
    }
}
This sends a network packet to the media player that changed its properties.
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();
}
This removes the player of the given serviceName, and sends the player list to the end of the list.
bool MprisControlPlugin::sendAlbumArt(const NetworkPacket &np)
{
    const QString player = np.get(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(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 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;
}
This sends an album art to the player of the current list. It first checks if the list of players is valid, and if it is the last player in the list, it gets the mpris information from the player list, and if it belongs to the current mpris player, and if it is the current player's mpris interface, and if it is the requested album art url, the mpris interface is used. Then it creates a network packet that sent to the player with the album art url and size. If the requested album art url is not a valid url, the method returns false.
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(QStringLiteral("player"));
    auto it = playerList.find(player);
    bool valid_player = (it != playerList.end());
    if (!valid_player || np.get(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(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(QStringLiteral("setLoopStatus"));
        qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting loopStatus" << loopStatus << "to" << serviceName;
        mprisInterface.setLoopStatus(loopStatus);
    }
    if (np.has(QStringLiteral("setShuffle"))) {
        bool shuffle = np.get(QStringLiteral("setShuffle"));
        qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting shuffle" << shuffle << "to" << serviceName;
        mprisInterface.setShuffle(shuffle);
    }
    if (np.has(QStringLiteral("setVolume"))) {
        double volume = np.get(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(QStringLiteral("Seek"));
        // qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Seeking" << offset << "to" << serviceName;
        mprisInterface.Seek(offset);
    }

    if (np.has(QStringLiteral("SetPosition"))) {
        qlonglong position = np.get(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(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(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;
}
This sends a network packet to the mpris player. It returns true if the packet is OK. It returns false if the packet is not a mpris control.
void MprisControlPlugin::sendPlayerList()
{
    NetworkPacket np(PACKET_TYPE_MPRIS);
    np.set(QStringLiteral("playerList"), playerList.keys());
    np.set(QStringLiteral("supportAlbumArtPayload"), true);
    sendPacket(np);
}
This sends a network packet that lists the players in the internal list.
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);
        }
    }

    QString nowPlaying = title;

    if (!artist.isEmpty()) {
        nowPlaying = artist + QStringLiteral(" - ") + title;
    }

    np.set(QStringLiteral("title"), title);
    np.set(QStringLiteral("artist"), artist);
    np.set(QStringLiteral("album"), album);
    np.set(QStringLiteral("albumArtUrl"), albumArtUrl);
    np.set(QStringLiteral("nowPlaying"), nowPlaying);

    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);
}
This creates a network packet from current play state. It takes the title, artist, album art url and file url. It creates some fields of the network packet based on the current play state.