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"