diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f2b55e..c2d1137 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,4 +79,4 @@ jobs: run: cmake --build ui/build -j2 - name: Run UI smoke tests - run: ctest --test-dir ui/build --output-on-failure + run: ctest --test-dir ui/build --output-on-failure -V diff --git a/ui/src/MprisController.cpp b/ui/src/MprisController.cpp index 0290e8c..1390a74 100644 --- a/ui/src/MprisController.cpp +++ b/ui/src/MprisController.cpp @@ -623,7 +623,18 @@ QVariantMap MprisController::buildMetadata() const { if (year.isValid() && !year.isNull()) { out.insert(QStringLiteral("xesam:contentCreated"), QString::number(year.toInt())); } - const QString artUrl = m_bridge->currentTrackCoverPath().trimmed(); + // currentTrackCoverPath() may return an internal image://covers/ URL + // (from the async QML image provider). MPRIS consumers need file:// URLs. + QString artUrl = m_bridge->currentTrackCoverPath().trimmed(); + if (artUrl.startsWith(QStringLiteral("image://covers/"))) { + QString localPath = artUrl.mid(QStringLiteral("image://covers/").size()); + // Strip any fragment (nonce for QML cache busting) + const int hashPos = localPath.indexOf(QLatin1Char('#')); + if (hashPos >= 0) { + localPath.truncate(hashPos); + } + artUrl = QUrl::fromLocalFile(localPath).toString(QUrl::FullyEncoded); + } if (!artUrl.isEmpty()) { out.insert(QStringLiteral("mpris:artUrl"), artUrl); } diff --git a/ui/tests/tst_bridge_client.cpp b/ui/tests/tst_bridge_client.cpp index f77a7ed..08a9794 100644 --- a/ui/tests/tst_bridge_client.cpp +++ b/ui/tests/tst_bridge_client.cpp @@ -64,6 +64,7 @@ private slots: void mprisPublishesPlaybackStateOnPlaybackSignal(); void mprisCanPauseOnlyWhilePlaying(); void mprisControllerConstructionDoesNotCrash(); + void mprisArtUrlConvertsImageProviderToFileUrl(); void spectrogramOverlaySettingsApplyFromSnapshot(); void spectrogramOverlaySettingsDecodeFromBinaryPayload(); void testSoloChannelCommandEncoding(); @@ -704,6 +705,38 @@ void BridgeClientTest::mprisControllerConstructionDoesNotCrash() { } +void BridgeClientTest::mprisArtUrlConvertsImageProviderToFileUrl() { + BridgeClient client; + isolateBridgeClient(client); + client.m_queueLength = 1; + client.m_playbackState = QStringLiteral("Playing"); + client.m_currentTrackPath = QStringLiteral("/music/track.flac"); + // Simulate the async image provider URL that coverUrlForPath() produces. + client.m_currentTrackCoverPath = + QStringLiteral("image://covers//music/cover.jpg#w=600&r=1"); + + MprisController controller(&client); + controller.m_serviceRegistered = true; + controller.m_hasPublishedPlayerState = false; + + emit client.trackMetadataChanged(); + + QVERIFY(controller.m_hasPublishedPlayerState); + const QVariantMap meta = controller.m_lastPlayerState.metadata; + const QString artUrl = meta.value(QStringLiteral("mpris:artUrl")).toString(); + // Must be a file:// URL, not an image:// URL. + QVERIFY2(artUrl.startsWith(QStringLiteral("file://")), + qPrintable(QStringLiteral("Expected file:// URL, got: ") + artUrl)); + QVERIFY2(!artUrl.contains(QStringLiteral("image://")), + qPrintable(QStringLiteral("image:// leaked into MPRIS artUrl: ") + artUrl)); + // Fragment (nonce) must be stripped. + QVERIFY2(!artUrl.contains(QLatin1Char('#')), + qPrintable(QStringLiteral("Fragment not stripped: ") + artUrl)); + + controller.m_serviceRegistered = false; + controller.m_objectRegistered = false; +} + void BridgeClientTest::spectrogramOverlaySettingsApplyFromSnapshot() { BridgeClient client; isolateBridgeClient(client);