File indexing completed on 2024-05-05 05:35:45

0001 /*
0002     SPDX-FileCopyrightText: 2017 Kai Uwe Broulik <kde@privat.broulik.de>
0003     SPDX-FileCopyrightText: 2017 David Edmundson <davidedmundson@kde.org>
0004 
0005     SPDX-License-Identifier: MIT
0006 */
0007 
0008 #include "mprisplugin.h"
0009 
0010 #include <QCoreApplication>
0011 #include <QDBusConnection>
0012 #include <QDBusObjectPath>
0013 #include <QGuiApplication>
0014 #include <QImageReader>
0015 
0016 #include "mprisplayer.h"
0017 #include "mprisroot.h"
0018 
0019 #include <unistd.h> // getppid
0020 
0021 static const QString s_serviceName = QStringLiteral("org.mpris.MediaPlayer2.plasma-browser-integration");
0022 
0023 static QString sanitizedString(const QString &str)
0024 {
0025     return str.left(200);
0026 }
0027 
0028 static QString sanitizedUrlDisplayString(const QUrl &url)
0029 {
0030     // No blob, no file, please.
0031     if (url.scheme() != QLatin1String("https") && url.scheme() != QLatin1String("http")) {
0032         return QString();
0033     }
0034 
0035     return url.toDisplayString().left(2083); // Supposed limit of what Chrome allows
0036 }
0037 
0038 MPrisPlugin::MPrisPlugin(QObject *parent)
0039     : AbstractBrowserPlugin(QStringLiteral("mpris"), 1, parent)
0040     , m_root(new MPrisRoot(this))
0041     , m_player(new MPrisPlayer(this))
0042     , m_playbackStatus(QStringLiteral("Stopped"))
0043     , m_loopStatus(QStringLiteral("None"))
0044 {
0045     if (!QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/mpris/MediaPlayer2"), this)) {
0046         qWarning() << "Failed to register MPris object";
0047         return;
0048     }
0049 
0050     m_propertyChangeSignalTimer.setInterval(200);
0051     m_propertyChangeSignalTimer.setSingleShot(true);
0052     connect(&m_propertyChangeSignalTimer, &QTimer::timeout, this, &MPrisPlugin::sendPropertyChanges);
0053 
0054     m_possibleLoopStatus = {
0055         {QStringLiteral("None"), false},
0056         {QStringLiteral("Track"), true},
0057         {QStringLiteral("Playlist"), true},
0058     };
0059 }
0060 
0061 bool MPrisPlugin::onUnload()
0062 {
0063     unregisterService();
0064     return true;
0065 }
0066 
0067 // TODO this can surely be done in a much beter way with introspection and what not
0068 void MPrisPlugin::emitPropertyChange(const QDBusAbstractAdaptor *interface, const char *propertyName)
0069 {
0070     // TODO don't assume it's index 0
0071     // TODO figure out encoding encoding
0072     const QString interfaceName = QString::fromUtf8(interface->metaObject()->classInfo(0).value());
0073 
0074     const QMetaProperty prop = metaObject()->property(metaObject()->indexOfProperty(propertyName));
0075 
0076     const QString propertyNameString = QString::fromUtf8(prop.name());
0077     const QVariant value = prop.read(this);
0078 
0079     m_pendingPropertyChanges[interfaceName][propertyNameString] = value;
0080 
0081     if (!m_propertyChangeSignalTimer.isActive()) {
0082         m_propertyChangeSignalTimer.start();
0083     }
0084 }
0085 
0086 void MPrisPlugin::sendPropertyChanges()
0087 {
0088     for (auto it = m_pendingPropertyChanges.constBegin(), end = m_pendingPropertyChanges.constEnd(); it != end; ++it) {
0089         const QString &interfaceName = it.key();
0090 
0091         const QVariantMap &changes = it.value();
0092         if (changes.isEmpty()) {
0093             continue;
0094         }
0095 
0096         QDBusMessage signal = QDBusMessage::createSignal(QStringLiteral("/org/mpris/MediaPlayer2"),
0097                                                          QStringLiteral("org.freedesktop.DBus.Properties"),
0098                                                          QStringLiteral("PropertiesChanged"));
0099 
0100         signal.setArguments({
0101             interfaceName,
0102             changes,
0103             QStringList(), // invalidated
0104         });
0105 
0106         QDBusConnection::sessionBus().send(signal);
0107     }
0108 
0109     m_pendingPropertyChanges.clear();
0110 }
0111 
0112 bool MPrisPlugin::registerService()
0113 {
0114     QString serviceName = s_serviceName;
0115 
0116     if (QDBusConnection::sessionBus().registerService(serviceName)) {
0117         m_serviceName = serviceName;
0118         return true;
0119     }
0120 
0121     // now try appending PID in case multiple hosts are running
0122     serviceName.append(QLatin1String("-")).append(QString::number(QCoreApplication::applicationPid()));
0123 
0124     if (QDBusConnection::sessionBus().registerService(serviceName)) {
0125         m_serviceName = serviceName;
0126         return true;
0127     }
0128 
0129     m_serviceName.clear();
0130     return false;
0131 }
0132 
0133 bool MPrisPlugin::unregisterService()
0134 {
0135     if (m_serviceName.isEmpty()) {
0136         return false;
0137     }
0138     return QDBusConnection::sessionBus().unregisterService(m_serviceName);
0139 }
0140 
0141 void MPrisPlugin::handleData(const QString &event, const QJsonObject &data)
0142 {
0143     if (event == QLatin1String("gone")) {
0144         unregisterService();
0145         setPlaybackStatus(QStringLiteral("Stopped")); // just in case
0146         m_canGoNext = false;
0147         m_canGoPrevious = false;
0148         m_pageTitle.clear();
0149         m_tabTitle.clear();
0150         m_url.clear();
0151         m_mediaSrc.clear();
0152         m_title.clear();
0153         m_artist.clear();
0154         m_artworkUrl.clear();
0155         m_volume = 1.0;
0156         m_muted = false;
0157         m_length = 0;
0158         m_position = 0;
0159     } else if (event == QLatin1String("playing")) {
0160         setPlaybackStatus(QStringLiteral("Playing"));
0161 
0162         m_pageTitle = data.value(QStringLiteral("pageTitle")).toString();
0163         m_tabTitle = data.value(QStringLiteral("tabTitle")).toString();
0164 
0165         m_url = QUrl(data.value(QStringLiteral("url")).toString());
0166         m_mediaSrc = QUrl(data.value(QStringLiteral("mediaSrc")).toString());
0167 
0168         const QUrl posterUrl = QUrl(data.value(QStringLiteral("poster")).toString());
0169         if (m_posterUrl != posterUrl) {
0170             m_posterUrl = posterUrl;
0171             emitPropertyChange(m_player, "Metadata");
0172         }
0173 
0174         const qreal oldVolume = volume();
0175 
0176         m_volume = data.value(QStringLiteral("volume")).toDouble(1);
0177         m_muted = data.value(QStringLiteral("muted")).toBool();
0178 
0179         if (volume() != oldVolume) {
0180             emitPropertyChange(m_player, "Volume");
0181         }
0182 
0183         const qreal length = data.value(QStringLiteral("duration")).toDouble();
0184         // <video> duration is in seconds, mpris uses microseconds
0185         setLength(length * 1000 * 1000);
0186 
0187         const qreal position = data.value(QStringLiteral("currentTime")).toDouble();
0188         setPosition(position * 1000 * 1000);
0189 
0190         const qreal playbackRate = data.value(QStringLiteral("playbackRate")).toDouble(1);
0191         if (m_playbackRate != playbackRate) {
0192             m_playbackRate = playbackRate;
0193             emitPropertyChange(m_player, "Rate");
0194         }
0195 
0196         // check if we're already looping, that keeps us from forcefully
0197         // overwriting Playlist loop with Track loop
0198         const bool oldLoop = m_possibleLoopStatus.value(m_loopStatus);
0199         const bool loop = data.value(QStringLiteral("loop")).toBool();
0200 
0201         if (loop != oldLoop) {
0202             setLoopStatus(loop ? QStringLiteral("Track") : QStringLiteral("None"));
0203         }
0204 
0205         const bool fullscreen = data.value(QStringLiteral("fullscreen")).toBool();
0206         if (m_fullscreen != fullscreen) {
0207             m_fullscreen = fullscreen;
0208             emitPropertyChange(m_root, "Fullscreen");
0209         }
0210 
0211         const bool canSetFullscreen = data.value(QStringLiteral("canSetFullscreen")).toBool();
0212         if (m_canSetFullscreen != canSetFullscreen) {
0213             m_canSetFullscreen = canSetFullscreen;
0214             emitPropertyChange(m_root, "CanSetFullscreen");
0215         }
0216 
0217         processMetadata(data.value(QStringLiteral("metadata")).toObject()); // also emits metadataChanged signal
0218         processCallbacks(data.value(QStringLiteral("callbacks")).toArray());
0219 
0220         registerService();
0221     } else if (event == QLatin1String("paused")) {
0222         setPlaybackStatus(QStringLiteral("Paused"));
0223     } else if (event == QLatin1String("stopped")) {
0224         setPlaybackStatus(QStringLiteral("Stopped"));
0225     } else if (event == QLatin1String("waiting")) {
0226         // unfortunately MPris doesn't have a "Buffering" playback state
0227         // set it to Paused when waiting for the player to avoid the seek slider
0228         // moving while we're not actually playing something
0229         // (we don't get an explicit "paused" signal)
0230         setPlaybackStatus(QStringLiteral("Paused"));
0231     } else if (event == QLatin1String("canplay")) {
0232         // opposite of "waiting", only forwarded by our extension when
0233         // canplay is emitted with the player *not* paused
0234         setPlaybackStatus(QStringLiteral("Playing"));
0235     } else if (event == QLatin1String("duration")) {
0236         const qreal length = data.value(QStringLiteral("duration")).toDouble();
0237 
0238         // <video> duration is in seconds, mpris uses microseconds
0239         setLength(length * 1000 * 1000);
0240     } else if (event == QLatin1String("timeupdate")) {
0241         // not signalling to avoid excess dbus traffic
0242         // media controller asks for this property once when it opens
0243         m_position = data.value(QStringLiteral("currentTime")).toDouble() * 1000 * 1000;
0244     } else if (event == QLatin1String("ratechange")) {
0245         m_playbackRate = data.value(QStringLiteral("playbackRate")).toDouble(1);
0246         emitPropertyChange(m_player, "Rate");
0247     } else if (event == QLatin1String("seeking") || event == QLatin1String("seeked")) {
0248         // seeked is explicit user interaction, signal a change on dbus
0249         const qreal position = data.value(QStringLiteral("currentTime")).toDouble();
0250         // FIXME actually invoke "Seeked" signal
0251         setPosition(position * 1000 * 1000);
0252     } else if (event == QLatin1String("volumechange")) {
0253         m_volume = data.value(QStringLiteral("volume")).toDouble(1);
0254         m_muted = data.value(QStringLiteral("muted")).toBool();
0255         emitPropertyChange(m_player, "Volume");
0256     } else if (event == QLatin1String("metadata")) {
0257         processMetadata(data.value(QStringLiteral("metadata")).toObject());
0258     } else if (event == QLatin1String("callbacks")) {
0259         processCallbacks(data.value(QStringLiteral("callbacks")).toArray());
0260     } else if (event == QLatin1String("titlechange")) {
0261         const QString oldTitle = effectiveTitle();
0262         m_pageTitle = data.value(QStringLiteral("pageTitle")).toString();
0263 
0264         if (oldTitle != effectiveTitle()) {
0265             emitPropertyChange(m_player, "Metadata");
0266         }
0267     } else if (event == QLatin1String("fullscreenchange")) {
0268         const bool fullscreen = data.value(QStringLiteral("fullscreen")).toBool();
0269         if (m_fullscreen != fullscreen) {
0270             m_fullscreen = fullscreen;
0271             emitPropertyChange(m_root, "Fullscreen");
0272         }
0273     } else {
0274         qWarning() << "Don't know how to handle mpris event" << event;
0275     }
0276 }
0277 
0278 QString MPrisPlugin::identity() const
0279 {
0280     return QGuiApplication::applicationDisplayName();
0281 }
0282 
0283 QString MPrisPlugin::desktopEntry() const
0284 {
0285     return QGuiApplication::desktopFileName();
0286 }
0287 
0288 bool MPrisPlugin::canRaise() const
0289 {
0290     return true; // really?
0291 }
0292 
0293 bool MPrisPlugin::fullscreen() const
0294 {
0295     return m_fullscreen;
0296 }
0297 
0298 void MPrisPlugin::setFullscreen(bool fullscreen)
0299 {
0300     sendData(QStringLiteral("setFullscreen"),
0301              {
0302                  {QStringLiteral("fullscreen"), fullscreen},
0303              });
0304 }
0305 
0306 bool MPrisPlugin::canSetFullscreen() const
0307 {
0308     return m_canSetFullscreen;
0309 }
0310 
0311 bool MPrisPlugin::canGoNext() const
0312 {
0313     return m_canGoNext;
0314 }
0315 
0316 bool MPrisPlugin::canGoPrevious() const
0317 {
0318     return m_canGoPrevious;
0319 }
0320 
0321 bool MPrisPlugin::canControl() const
0322 {
0323     return true; // really?
0324 }
0325 
0326 bool MPrisPlugin::canPause() const
0327 {
0328     return canControl();
0329 }
0330 
0331 bool MPrisPlugin::canPlay() const
0332 {
0333     return canControl();
0334 }
0335 
0336 bool MPrisPlugin::canSeek() const
0337 {
0338     // TODO use player.seekable for determining whether we can seek?
0339     return m_length > 0;
0340 }
0341 
0342 qreal MPrisPlugin::volume() const
0343 {
0344     if (m_muted) {
0345         return 0.0;
0346     }
0347     return m_volume;
0348 }
0349 
0350 void MPrisPlugin::setVolume(qreal volume)
0351 {
0352     if (volume < 0) {
0353         volume = 0.0;
0354     }
0355 
0356     sendData(QStringLiteral("setVolume"),
0357              {
0358                  {QStringLiteral("volume"), volume},
0359              });
0360 }
0361 
0362 qlonglong MPrisPlugin::position() const
0363 {
0364     return m_position;
0365 }
0366 
0367 double MPrisPlugin::playbackRate() const
0368 {
0369     return m_playbackRate;
0370 }
0371 
0372 void MPrisPlugin::setPlaybackRate(double playbackRate)
0373 {
0374     if (playbackRate < minimumRate() || playbackRate > maximumRate()) {
0375         return;
0376     }
0377 
0378     sendData(QStringLiteral("setPlaybackRate"),
0379              {
0380                  {QStringLiteral("playbackRate"), playbackRate},
0381              });
0382 }
0383 
0384 double MPrisPlugin::minimumRate() const
0385 {
0386     return 0.01; // don't let it stop
0387 }
0388 
0389 double MPrisPlugin::maximumRate() const
0390 {
0391     return 32; // random
0392 }
0393 
0394 QString MPrisPlugin::playbackStatus() const
0395 {
0396     return m_playbackStatus;
0397 }
0398 
0399 QString MPrisPlugin::loopStatus() const
0400 {
0401     return m_loopStatus;
0402 }
0403 
0404 void MPrisPlugin::setLoopStatus(const QString &loopStatus)
0405 {
0406     if (!m_possibleLoopStatus.contains(loopStatus)) {
0407         return;
0408     }
0409 
0410     sendData(QStringLiteral("setLoop"),
0411              {
0412                  {QStringLiteral("loop"), m_possibleLoopStatus.value(loopStatus)},
0413              });
0414 
0415     m_loopStatus = loopStatus;
0416     emitPropertyChange(m_player, "LoopStatus");
0417 }
0418 
0419 QString MPrisPlugin::effectiveTitle() const
0420 {
0421     if (!m_title.isEmpty()) {
0422         return m_title;
0423     }
0424     if (!m_pageTitle.isEmpty()) {
0425         return m_pageTitle;
0426     }
0427     return m_tabTitle;
0428 }
0429 
0430 QVariantMap MPrisPlugin::metadata() const
0431 {
0432     QVariantMap metadata;
0433 
0434     // HACK this is needed or else SetPosition won't do anything
0435     // TODO use something more sensible, e.g. at least have the tab id with the player in there or so
0436     metadata.insert(QStringLiteral("mpris:trackid"), QVariant::fromValue(QDBusObjectPath(QStringLiteral("/org/kde/plasma/browser_integration/1337"))));
0437 
0438     // Task Manager matches the window to the player's PID but in our case
0439     // the browser window isn't owned by us
0440     metadata.insert(QStringLiteral("kde:pid"), getppid());
0441 
0442     const QString title = effectiveTitle();
0443     if (!title.isEmpty()) {
0444         metadata.insert(QStringLiteral("xesam:title"), sanitizedString(title));
0445     }
0446 
0447     if (const QString urlString = sanitizedUrlDisplayString(m_url); !urlString.isEmpty()) {
0448         metadata.insert(QStringLiteral("xesam:url"), urlString);
0449     }
0450     if (const QString mediaSrc = sanitizedUrlDisplayString(m_mediaSrc); !mediaSrc.isEmpty()) {
0451         metadata.insert(QStringLiteral("kde:mediaSrc"), mediaSrc);
0452     }
0453     if (m_length > 0) {
0454         metadata.insert(QStringLiteral("mpris:length"), m_length);
0455     }
0456     // See https://searchfox.org/mozilla-central/rev/a3c18883ef9875ba4bb0cc2e7d6ba5a198aaf9bd/dom/media/mediasession/MediaMetadata.h#34
0457     // and
0458     // https://source.chromium.org/chromium/chromium/src/+/main:services/media_session/public/cpp/media_metadata.h;l=46;drc=098756533733ea50b2dcb1c40d9a9e18d49febbe
0459     // MediaMetadata.artist is of string type, but "xesam:artist" is of stringlist type
0460     if (!m_artist.isEmpty()) {
0461         metadata.insert(QStringLiteral("xesam:artist"), QStringList{sanitizedString(m_artist)});
0462     }
0463 
0464     QUrl artUrl = m_artworkUrl;
0465     if (!artUrl.isValid()) {
0466         artUrl = m_posterUrl;
0467     }
0468     if (const QString artUrlStr = sanitizedUrlDisplayString(artUrl); !artUrlStr.isEmpty()) {
0469         metadata.insert(QStringLiteral("mpris:artUrl"), artUrlStr);
0470     }
0471 
0472     if (!m_album.isEmpty()) {
0473         metadata.insert(QStringLiteral("xesam:album"), sanitizedString(m_album));
0474         // when we don't have artist information use the scheme+domain as "album" (that's what Chrome on Android does)
0475     } else if (m_artist.isEmpty() && m_url.isValid()) {
0476         metadata.insert(QStringLiteral("xesam:album"), sanitizedString(m_url.toDisplayString(QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment)));
0477     }
0478 
0479     return metadata;
0480 }
0481 
0482 void MPrisPlugin::setPlaybackStatus(const QString &playbackStatus)
0483 {
0484     if (m_playbackStatus != playbackStatus) {
0485         m_playbackStatus = playbackStatus;
0486         // emit playbackStatusChanged();
0487 
0488         emitPropertyChange(m_player, "PlaybackStatus");
0489         // these depend on playback status, so signal a change for these, too
0490         emitPropertyChange(m_player, "CanPlay");
0491         emitPropertyChange(m_player, "CanPause");
0492     }
0493 }
0494 
0495 void MPrisPlugin::setLength(qlonglong length)
0496 {
0497     if (m_length != length) {
0498         const bool oldCanSeek = canSeek();
0499 
0500         m_length = length;
0501         emitPropertyChange(m_player, "Metadata");
0502 
0503         if (oldCanSeek != canSeek()) {
0504             emitPropertyChange(m_player, "CanSeek");
0505         }
0506     }
0507 }
0508 
0509 void MPrisPlugin::setPosition(qlonglong position)
0510 {
0511     if (m_position != position) {
0512         m_position = position;
0513 
0514         emitPropertyChange(m_player, "Position");
0515     }
0516 }
0517 
0518 void MPrisPlugin::processMetadata(const QJsonObject &data)
0519 {
0520     m_title = data.value(QStringLiteral("title")).toString();
0521     m_artist = data.value(QStringLiteral("artist")).toString();
0522     m_album = data.value(QStringLiteral("album")).toString();
0523 
0524     // for simplicity we just use the biggest artwork it offers, perhaps we could limit it to some extent
0525     // TODO download/cache artwork somewhere
0526     QSize biggest;
0527     QUrl artworkUrl;
0528     const QJsonArray &artwork = data.value(QStringLiteral("artwork")).toArray();
0529 
0530     const auto &supportedImageMimes = QImageReader::supportedMimeTypes();
0531 
0532     for (auto it = artwork.constBegin(), end = artwork.constEnd(); it != end; ++it) {
0533         const QJsonObject &item = it->toObject();
0534 
0535         const QUrl url = QUrl(item.value(QStringLiteral("src")).toString());
0536         if (!url.isValid()) {
0537             continue;
0538         }
0539 
0540         // why is this named "sizes" when it's just a string and the examples don't mention how one could specify multiple?
0541         // also, how on Earth could a single image src have multiple sizes? ...
0542         // spec says this is a space-separated list of sizes for ... some reason
0543         const QString sizeString = item.value(QStringLiteral("sizes")).toString();
0544         QSize actualSize;
0545 
0546         // now parse the size...
0547         const auto &sizeParts = QStringView(sizeString).split(QLatin1Char('x'));
0548         if (sizeParts.count() == 2) {
0549             const int width = sizeParts.first().toInt();
0550             const int height = sizeParts.last().toInt();
0551             actualSize = QSize(width, height);
0552         }
0553 
0554         const QString type = item.value(QStringLiteral("type")).toString();
0555         if (!type.isEmpty() && !supportedImageMimes.contains(type.toUtf8())) {
0556             continue;
0557         }
0558 
0559         if (biggest.isEmpty() || (actualSize.width() >= biggest.width() && actualSize.height() >= biggest.height())) {
0560             artworkUrl = url;
0561             biggest = actualSize;
0562         }
0563     }
0564 
0565     m_artworkUrl = artworkUrl;
0566 
0567     emitPropertyChange(m_player, "Metadata");
0568 }
0569 
0570 void MPrisPlugin::processCallbacks(const QJsonArray &data)
0571 {
0572     const bool canGoNext = data.contains(QLatin1String("nexttrack"));
0573     if (m_canGoNext != canGoNext) {
0574         m_canGoNext = canGoNext;
0575         emitPropertyChange(m_player, "CanGoNext");
0576     }
0577 
0578     const bool canGoPrevious = data.contains(QLatin1String("previoustrack"));
0579     if (m_canGoPrevious != canGoPrevious) {
0580         m_canGoPrevious = canGoPrevious;
0581         emitPropertyChange(m_player, "CanGoPrevious");
0582     }
0583 }
0584 
0585 void MPrisPlugin::Raise()
0586 {
0587     sendData(QStringLiteral("raise"));
0588 }
0589 
0590 void MPrisPlugin::Quit()
0591 {
0592 }
0593 
0594 void MPrisPlugin::Next()
0595 {
0596     if (!m_canGoNext) {
0597         return;
0598     }
0599     sendData(QStringLiteral("next"));
0600 }
0601 
0602 void MPrisPlugin::Previous()
0603 {
0604     if (!m_canGoPrevious) {
0605         return;
0606     }
0607     sendData(QStringLiteral("previous"));
0608 }
0609 
0610 void MPrisPlugin::Pause()
0611 {
0612     sendData(QStringLiteral("pause"));
0613 }
0614 
0615 void MPrisPlugin::PlayPause()
0616 {
0617     sendData(QStringLiteral("playPause"));
0618 }
0619 
0620 void MPrisPlugin::Stop()
0621 {
0622     sendData(QStringLiteral("stop"));
0623 }
0624 
0625 void MPrisPlugin::Play()
0626 {
0627     sendData(QStringLiteral("play"));
0628 }
0629 
0630 void MPrisPlugin::Seek(qlonglong offset)
0631 {
0632     auto newPosition = position() + offset;
0633     if (newPosition >= m_length) {
0634         Next();
0635         return;
0636     }
0637 
0638     if (newPosition < 0) {
0639         newPosition = 0;
0640     }
0641     SetPosition(QDBusObjectPath() /*unused*/, newPosition);
0642 }
0643 
0644 void MPrisPlugin::SetPosition(const QDBusObjectPath &path, qlonglong position)
0645 {
0646     Q_UNUSED(path); // TODO use?
0647 
0648     if (position < 0 || position >= m_length) {
0649         return;
0650     }
0651 
0652     sendData(QStringLiteral("setPosition"), {{QStringLiteral("position"), position / 1000.0 / 1000.0}});
0653 }
0654 
0655 void MPrisPlugin::OpenUri(const QString &uri)
0656 {
0657     Q_UNUSED(uri);
0658 }
0659 
0660 #include "moc_mprisplugin.cpp"