File indexing completed on 2024-05-19 05:38:36

0001 /*
0002     SPDX-FileCopyrightText: 2007-2012 Alex Merry <alex.merry@kdemail.net>
0003     SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-or-later
0006 */
0007 
0008 #include "mpris2sourcemodel.h"
0009 
0010 #include <QDBusConnection>
0011 #include <QDBusConnectionInterface>
0012 #include <QDBusPendingCallWatcher>
0013 #include <QDBusPendingReply>
0014 #include <QDBusServiceWatcher>
0015 #include <QStringList>
0016 
0017 #include "libkmpris_debug.h"
0018 #include "playercontainer.h"
0019 
0020 std::shared_ptr<Mpris2SourceModel> Mpris2SourceModel::self()
0021 {
0022     static std::weak_ptr<Mpris2SourceModel> s_model;
0023     if (s_model.expired()) {
0024         std::shared_ptr<Mpris2SourceModel> ptr{new Mpris2SourceModel};
0025         s_model = ptr;
0026         return ptr;
0027     }
0028 
0029     return s_model.lock();
0030 }
0031 
0032 Mpris2SourceModel::Mpris2SourceModel(QObject *parent)
0033     : QAbstractListModel(parent)
0034 {
0035     auto watcher =
0036         new QDBusServiceWatcher(QStringLiteral("org.mpris.MediaPlayer2*"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this);
0037     connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &Mpris2SourceModel::onServiceOwnerChanged);
0038 
0039     fetchServiceNames();
0040 }
0041 
0042 Mpris2SourceModel::~Mpris2SourceModel()
0043 {
0044 }
0045 
0046 QVariant Mpris2SourceModel::dataFromPlayer(PlayerContainer *container, int role)
0047 {
0048     switch (role) {
0049     case TrackRole:
0050         return container->track();
0051     case CanControlRole:
0052         return container->canControl();
0053     case CanGoNextRole:
0054         return container->canGoNext();
0055     case CanGoPreviousRole:
0056         return container->canGoPrevious();
0057     case CanPlayRole:
0058         return container->canPlay();
0059     case CanPauseRole:
0060         return container->canPause();
0061     case CanStopRole:
0062         return container->canStop();
0063     case CanSeekRole:
0064         return container->canSeek();
0065     case LoopStatusRole:
0066         return container->loopStatus();
0067     case PlaybackStatusRole:
0068         return container->playbackStatus();
0069     case PositionRole:
0070         return container->position();
0071     case RateRole:
0072         return container->rate();
0073     case ShuffleRole:
0074         return container->shuffle();
0075     case VolumeRole:
0076         return container->volume();
0077     case ArtUrlRole:
0078         return container->artUrl();
0079     case ArtistRole:
0080         return container->artist();
0081     case AlbumRole:
0082         return container->album();
0083     case LengthRole:
0084         return container->length();
0085     case CanQuitRole:
0086         return container->canQuit();
0087     case CanRaiseRole:
0088         return container->canRaise();
0089     case CanSetFullscreenRole:
0090         return container->canSetFullscreen();
0091     case DesktopEntryRole:
0092         return container->desktopEntry();
0093     case IdentityRole:
0094         return container->identity();
0095     case IconNameRole:
0096         return container->iconName();
0097     case InstancePidRole:
0098         return container->instancePid();
0099     case KDEPidRole:
0100         return container->kdePid();
0101     case ContainerRole:
0102         return QVariant::fromValue(container);
0103     default:
0104         return {};
0105     }
0106 }
0107 
0108 QVariant Mpris2SourceModel::data(const QModelIndex &index, int role) const
0109 {
0110     if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
0111         return {};
0112     }
0113 
0114     return dataFromPlayer(m_containers.at(index.row()), role);
0115 }
0116 
0117 bool Mpris2SourceModel::setData(const QModelIndex &index, const QVariant &value, int role)
0118 {
0119     if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
0120         return {};
0121     }
0122 
0123     bool ok = false;
0124     switch (PlayerContainer *const container = m_containers.at(index.row()); role) {
0125     case LoopStatusRole:
0126         Q_ASSERT_X(value.toUInt() <= qToUnderlying(LoopStatus::Track), Q_FUNC_INFO, qUtf8Printable(QString::number(value.toUInt())));
0127         if (value.toUInt() <= qToUnderlying(LoopStatus::Track)) {
0128             container->setLoopStatus(static_cast<LoopStatus::Status>(value.toUInt(&ok)));
0129             return ok;
0130         }
0131         break;
0132     case PlaybackStatusRole:
0133         Q_ASSERT_X(value.toUInt() <= qToUnderlying(PlaybackStatus::Paused), Q_FUNC_INFO, qUtf8Printable(QString::number(value.toUInt())));
0134         if (value.toUInt() <= qToUnderlying(PlaybackStatus::Paused)) {
0135             container->setPlaybackStatus(static_cast<PlaybackStatus::Status>(value.toUInt(&ok)));
0136             return ok;
0137         }
0138         break;
0139     case PositionRole:
0140         container->setPosition(value.toLongLong(&ok));
0141         return ok;
0142     case ShuffleRole:
0143         Q_ASSERT_X(value.toUInt() <= qToUnderlying(ShuffleStatus::On), Q_FUNC_INFO, qUtf8Printable(QString::number(value.toUInt())));
0144         if (value.toUInt() <= qToUnderlying(ShuffleStatus::On)) {
0145             container->setShuffle(static_cast<ShuffleStatus::Status>(value.toUInt(&ok)));
0146             return ok;
0147         }
0148         break;
0149     case VolumeRole:
0150         Q_ASSERT_X(value.toDouble() >= 0.0 && value.toDouble() <= 1.0, Q_FUNC_INFO, qUtf8Printable(QString::number(value.toDouble())));
0151         if (value.toDouble() >= 0.0 && value.toDouble() <= 1.0) {
0152             container->setVolume(value.toDouble(&ok));
0153             return ok;
0154         }
0155         break;
0156     default:
0157         break;
0158     }
0159 
0160     return false;
0161 }
0162 
0163 int Mpris2SourceModel::rowCount(const QModelIndex &parent) const
0164 {
0165     return parent.isValid() ? 0 : m_containers.size();
0166 }
0167 
0168 QHash<int, QByteArray> Mpris2SourceModel::roleNames() const
0169 {
0170     return {
0171         {TrackRole, QByteArrayLiteral("track")},
0172         {CanControlRole, QByteArrayLiteral("canControl")},
0173         {CanGoNextRole, QByteArrayLiteral("canGoNext")},
0174         {CanGoPreviousRole, QByteArrayLiteral("canGoPrevious")},
0175         {CanPlayRole, QByteArrayLiteral("canPlay")},
0176         {CanPauseRole, QByteArrayLiteral("canPause")},
0177         {CanStopRole, QByteArrayLiteral("canStop")},
0178         {CanSeekRole, QByteArrayLiteral("canSeek")},
0179         {LoopStatusRole, QByteArrayLiteral("loopStatus")},
0180         {PlaybackStatusRole, QByteArrayLiteral("playbackStatus")},
0181         {PositionRole, QByteArrayLiteral("position")},
0182         {RateRole, QByteArrayLiteral("rate")},
0183         {ShuffleRole, QByteArrayLiteral("shuffle")},
0184         {VolumeRole, QByteArrayLiteral("volume")},
0185         {ArtUrlRole, QByteArrayLiteral("artUrl")},
0186         {ArtistRole, QByteArrayLiteral("artist")},
0187         {AlbumRole, QByteArrayLiteral("album")},
0188         {LengthRole, QByteArrayLiteral("length")},
0189         {CanQuitRole, QByteArrayLiteral("canQuit")},
0190         {CanRaiseRole, QByteArrayLiteral("canRaise")},
0191         {CanSetFullscreenRole, QByteArrayLiteral("canSetFullscreen")},
0192         {DesktopEntryRole, QByteArrayLiteral("desktopEntry")},
0193         {IdentityRole, QByteArrayLiteral("identity")},
0194         {IconNameRole, QByteArrayLiteral("iconName")},
0195         {KDEPidRole, QByteArrayLiteral("kdePid")},
0196         {ContainerRole, QByteArrayLiteral("container")},
0197     };
0198 }
0199 
0200 void Mpris2SourceModel::onServiceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
0201 {
0202     if (!serviceName.startsWith(MPRIS2_PREFIX)) {
0203         return;
0204     }
0205 
0206     const QString sourceName = serviceName.sliced(MPRIS2_PATH.size());
0207 
0208     if (!oldOwner.isEmpty()) {
0209         qCDebug(MPRIS2) << "MPRIS service" << serviceName << "just went offline";
0210         removeMediaPlayer(sourceName);
0211     }
0212 
0213     if (!newOwner.isEmpty()) {
0214         qCDebug(MPRIS2) << "MPRIS service" << serviceName << "just came online";
0215         addMediaPlayer(serviceName, sourceName);
0216     }
0217 }
0218 
0219 void Mpris2SourceModel::onServiceNamesFetched(QDBusPendingCallWatcher *watcher)
0220 {
0221     QDBusPendingReply<QStringList> propsReply = *watcher;
0222     watcher->deleteLater();
0223 
0224     if (propsReply.isError()) {
0225         qCWarning(MPRIS2) << "Could not get list of available D-Bus services";
0226     } else {
0227         for (const QStringList names = propsReply.value(); const QString &serviceName : names) {
0228             if (!serviceName.startsWith(MPRIS2_PREFIX)) {
0229                 continue;
0230             }
0231 
0232             qCDebug(MPRIS2) << "Found MPRIS2 service" << serviceName;
0233             // watch out for race conditions; the media player could
0234             // have appeared between starting the service watcher and
0235             // this call being dealt with
0236             // NB: _disappearing_ between sending this call and doing
0237             // this processing is fine
0238             const QString sourceName = serviceName.sliced(MPRIS2_PREFIX.size());
0239             const bool exist = std::any_of(m_containers.cbegin(), m_containers.cend(), [&sourceName](PlayerContainer *c) {
0240                 return c->objectName() == sourceName;
0241             });
0242             if (!exist && !m_pendingContainers.contains(sourceName)) {
0243                 qCDebug(MPRIS2) << "Haven't already seen" << serviceName;
0244                 addMediaPlayer(serviceName, sourceName);
0245             }
0246         }
0247     }
0248 }
0249 
0250 void Mpris2SourceModel::onInitialFetchFinished(PlayerContainer *container)
0251 {
0252     qCDebug(MPRIS2) << "Props fetch for" << container->objectName() << "finished; adding";
0253     const auto erased = m_pendingContainers.erase(container->objectName());
0254     Q_ASSERT_X(erased == 1, Q_FUNC_INFO, qUtf8Printable(container->objectName()));
0255 
0256     // don't let future refreshes trigger this
0257     disconnect(container, &PlayerContainer::initialFetchFinished, this, &Mpris2SourceModel::onInitialFetchFinished);
0258     disconnect(container, &PlayerContainer::initialFetchFailed, this, &Mpris2SourceModel::onInitialFetchFailed);
0259 
0260     // Check if the player follows the specification dutifully.
0261     if (container->identity().isEmpty()) {
0262         qCDebug(MPRIS2) << "MPRIS2 service" << container->objectName() << "isn't standard-compliant, ignoring";
0263         return;
0264     }
0265 
0266     const int row = m_containers.size();
0267     beginInsertRows(QModelIndex(), row, row);
0268 
0269     m_containers.push_back(container);
0270 
0271     endInsertRows();
0272 
0273     // Property bindings
0274     connect(container, &AbstractPlayerContainer::canControlChanged, this, [this] {
0275         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0276         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanControlRole});
0277     });
0278     connect(container, &AbstractPlayerContainer::trackChanged, this, [this] {
0279         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0280         Q_EMIT dataChanged(index(row, 0), index(row, 0), {TrackRole});
0281     });
0282     connect(container, &AbstractPlayerContainer::canGoNextChanged, this, [this] {
0283         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0284         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanGoNextRole});
0285     });
0286     connect(container, &AbstractPlayerContainer::canGoPreviousChanged, this, [this] {
0287         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0288         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanGoPreviousRole});
0289     });
0290     connect(container, &AbstractPlayerContainer::canPlayChanged, this, [this] {
0291         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0292         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanPlayRole});
0293     });
0294     connect(container, &AbstractPlayerContainer::canPauseChanged, this, [this] {
0295         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0296         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanPauseRole});
0297     });
0298     connect(container, &AbstractPlayerContainer::canStopChanged, this, [this] {
0299         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0300         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanStopRole});
0301     });
0302     connect(container, &AbstractPlayerContainer::canSeekChanged, this, [this] {
0303         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0304         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanSeekRole});
0305     });
0306     connect(container, &AbstractPlayerContainer::loopStatusChanged, this, [this] {
0307         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0308         Q_EMIT dataChanged(index(row, 0), index(row, 0), {LoopStatusRole});
0309     });
0310     connect(container, &AbstractPlayerContainer::playbackStatusChanged, this, [this] {
0311         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0312         Q_EMIT dataChanged(index(row, 0), index(row, 0), {PlaybackStatusRole});
0313     });
0314     connect(container, &AbstractPlayerContainer::positionChanged, this, [this] {
0315         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0316         Q_EMIT dataChanged(index(row, 0), index(row, 0), {PositionRole});
0317     });
0318     connect(container, &AbstractPlayerContainer::rateChanged, this, [this] {
0319         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0320         Q_EMIT dataChanged(index(row, 0), index(row, 0), {RateRole});
0321     });
0322     connect(container, &AbstractPlayerContainer::shuffleChanged, this, [this] {
0323         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0324         Q_EMIT dataChanged(index(row, 0), index(row, 0), {ShuffleRole});
0325     });
0326     connect(container, &AbstractPlayerContainer::volumeChanged, this, [this] {
0327         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0328         Q_EMIT dataChanged(index(row, 0), index(row, 0), {VolumeRole});
0329     });
0330     connect(container, &AbstractPlayerContainer::artUrlChanged, this, [this] {
0331         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0332         Q_EMIT dataChanged(index(row, 0), index(row, 0), {ArtUrlRole});
0333     });
0334     connect(container, &AbstractPlayerContainer::artistChanged, this, [this] {
0335         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0336         Q_EMIT dataChanged(index(row, 0), index(row, 0), {ArtistRole});
0337     });
0338     connect(container, &AbstractPlayerContainer::albumChanged, this, [this] {
0339         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0340         Q_EMIT dataChanged(index(row, 0), index(row, 0), {AlbumRole});
0341     });
0342     connect(container, &AbstractPlayerContainer::lengthChanged, this, [this] {
0343         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0344         Q_EMIT dataChanged(index(row, 0), index(row, 0), {LengthRole});
0345     });
0346     connect(container, &AbstractPlayerContainer::canQuitChanged, this, [this] {
0347         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0348         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanQuitRole});
0349     });
0350     connect(container, &AbstractPlayerContainer::canRaiseChanged, this, [this] {
0351         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0352         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanRaiseRole});
0353     });
0354     connect(container, &AbstractPlayerContainer::canSetFullscreenChanged, this, [this] {
0355         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0356         Q_EMIT dataChanged(index(row, 0), index(row, 0), {CanSetFullscreenRole});
0357     });
0358     connect(container, &AbstractPlayerContainer::desktopEntryChanged, this, [this] {
0359         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0360         Q_EMIT dataChanged(index(row, 0), index(row, 0), {DesktopEntryRole, IconNameRole});
0361     });
0362     connect(container, &AbstractPlayerContainer::identityChanged, this, [this] {
0363         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0364         Q_EMIT dataChanged(index(row, 0), index(row, 0), {IdentityRole});
0365     });
0366     connect(container, &AbstractPlayerContainer::kdePidChanged, this, [this] {
0367         const int row = std::distance(m_containers.cbegin(), std::find(m_containers.cbegin(), m_containers.cend(), sender()));
0368         Q_EMIT dataChanged(index(row, 0), index(row, 0), {KDEPidRole});
0369     });
0370 }
0371 
0372 void Mpris2SourceModel::onInitialFetchFailed(PlayerContainer *container)
0373 {
0374     qCDebug(MPRIS2) << "Failed to find a working MPRIS2 interface for" << container->dbusAddress();
0375     const auto erased = m_pendingContainers.erase(container->objectName());
0376     Q_ASSERT_X(erased == 1, Q_FUNC_INFO, qUtf8Printable(container->objectName()));
0377     delete container;
0378 }
0379 
0380 void Mpris2SourceModel::fetchServiceNames()
0381 {
0382     QDBusPendingCall call = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames"));
0383     auto watcher = new QDBusPendingCallWatcher(call, this);
0384     connect(watcher, &QDBusPendingCallWatcher::finished, this, &Mpris2SourceModel::onServiceNamesFetched);
0385 }
0386 
0387 void Mpris2SourceModel::addMediaPlayer(const QString &serviceName, const QString &sourceName)
0388 {
0389     Q_ASSERT_X(!m_pendingContainers.contains(sourceName), Q_FUNC_INFO, qUtf8Printable(sourceName));
0390 
0391     PlayerContainer *const container = new PlayerContainer(serviceName, this);
0392     container->setObjectName(sourceName);
0393     m_pendingContainers.emplace(sourceName, container);
0394 
0395     connect(container, &PlayerContainer::initialFetchFinished, this, &Mpris2SourceModel::onInitialFetchFinished);
0396     connect(container, &PlayerContainer::initialFetchFailed, this, &Mpris2SourceModel::onInitialFetchFailed);
0397 }
0398 
0399 void Mpris2SourceModel::removeMediaPlayer(const QString &sourceName)
0400 {
0401     auto it = std::find_if(m_containers.begin(), m_containers.end(), [&sourceName](PlayerContainer *c) {
0402         return c->objectName() == sourceName;
0403     });
0404     if (it == m_containers.end()) {
0405         auto pendingIt = m_pendingContainers.find(sourceName);
0406         if (pendingIt == m_pendingContainers.end()) [[unlikely]] {
0407             // This can happen when a player appears and disappears on DBus before the service list is fetched
0408             // or a player is invalid (like lacking identity)
0409             return;
0410         }
0411         delete pendingIt->second;
0412         m_pendingContainers.erase(pendingIt);
0413         return;
0414     }
0415 
0416     PlayerContainer *container = *it;
0417     disconnect(container, nullptr, this, nullptr);
0418     const int row = std::distance(m_containers.begin(), it);
0419     beginRemoveRows(QModelIndex(), row, row);
0420     m_containers.erase(it);
0421     endRemoveRows();
0422 
0423     delete container;
0424 }
0425 
0426 #include "moc_mpris2sourcemodel.cpp"