Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 12 additions & 1 deletion ui/src/MprisController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
33 changes: 33 additions & 0 deletions ui/tests/tst_bridge_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private slots:
void mprisPublishesPlaybackStateOnPlaybackSignal();
void mprisCanPauseOnlyWhilePlaying();
void mprisControllerConstructionDoesNotCrash();
void mprisArtUrlConvertsImageProviderToFileUrl();
void spectrogramOverlaySettingsApplyFromSnapshot();
void spectrogramOverlaySettingsDecodeFromBinaryPayload();
void testSoloChannelCommandEncoding();
Expand Down Expand Up @@ -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);
Expand Down