From dc854580be648448a7e25fe2d3c2b00db1f91a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuomas=20Sepp=C3=A4nen?= Date: Sat, 11 Apr 2026 10:53:19 +0300 Subject: [PATCH] fix: convert image:// cover URLs to file:// for MPRIS art metadata PR #9 routed cover art through an async QML image provider, changing cover paths from raw file paths to image://covers/ URLs. MPRIS consumers (KDE Plasma, etc.) cannot resolve Qt-internal image:// URLs, so album art stopped appearing in KDE window previews and the media player tray widget. Strip the image://covers/ prefix and any cache-busting fragment, then emit a proper file:// URL in mpris:artUrl. Also add -V (verbose) to CI ctest invocation to match local test runner output. --- .github/workflows/ci.yml | 2 +- ui/src/MprisController.cpp | 13 ++++++++++++- ui/tests/tst_bridge_client.cpp | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) 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);