diff --git a/quickshell/Modules/OSD/MediaPlaybackOSD.qml b/quickshell/Modules/OSD/MediaPlaybackOSD.qml
index 10d969dbd..ddce20a66 100644
--- a/quickshell/Modules/OSD/MediaPlaybackOSD.qml
+++ b/quickshell/Modules/OSD/MediaPlaybackOSD.qml
@@ -156,9 +156,10 @@ DankOSD {
if (MprisController.isFirefoxYoutubeHoverPreview(player))
return;
- const newTitle = player.trackTitle || "";
- const newArtist = player.trackArtist || "";
- const newAlbum = player.trackAlbum || "";
+ const metaPlayer = MprisController.bestMetadataPlayer(player);
+ const newTitle = MprisController.displayTrackTitle(metaPlayer);
+ const newArtist = metaPlayer.trackArtist || "";
+ const newAlbum = metaPlayer.trackAlbum || "";
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
root._displayTitle = newTitle;
diff --git a/quickshell/Services/MprisController.qml b/quickshell/Services/MprisController.qml
index b3cc0bdcb..88a6ccb55 100644
--- a/quickshell/Services/MprisController.qml
+++ b/quickshell/Services/MprisController.qml
@@ -93,7 +93,7 @@ Singleton {
}
if (isFirefoxYoutubeHoverPreview(p))
return;
- const metadataPlayer = _bestMetadataPlayer(p);
+ const metadataPlayer = bestMetadataPlayer(p);
const nextTitle = displayTrackTitle(metadataPlayer);
const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
if (trackChanged) {
@@ -171,8 +171,7 @@ Singleton {
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
}
- // App-name suffixes that browsers/integrations append as "
| ".
- // Only these are stripped for track matching; display always keeps the full title.
+ // Known " | " suffixes stripped for matching only; display keeps the full title
readonly property var _appTitleSuffixes: ["youtube", "youtube music", "soundcloud", "spotify", "chrome", "chromium", "firefox", "brave", "vivaldi", "twitch"]
function _stripAppTitleSuffix(title: string): string {
@@ -183,12 +182,10 @@ Singleton {
return _appTitleSuffixes.indexOf(suffix) !== -1 ? title.substring(0, idx).trim() : title;
}
- // Matching key: strip only known app suffixes so equivalent players line up.
function normalizedTrackTitle(player: MprisPlayer): string {
return _stripAppTitleSuffix((player?.trackTitle || "").trim()).toLowerCase();
}
- // Display: never strip — a generic " | " cut would mangle legitimate titles.
function displayTrackTitle(player: MprisPlayer): string {
return (player?.trackTitle || "").trim();
}
@@ -197,14 +194,24 @@ Singleton {
return (player?.trackArtist || "").trim().toLowerCase();
}
+ // Artist missing on either side: fall back to URL, then album, before trusting a title-only match
function isSameTrack(first: MprisPlayer, second: MprisPlayer): bool {
const firstTitle = normalizedTrackTitle(first);
- const secondTitle = normalizedTrackTitle(second);
- if (!firstTitle || firstTitle !== secondTitle)
+ if (!firstTitle || firstTitle !== normalizedTrackTitle(second))
return false;
const firstArtist = normalizedTrackArtist(first);
const secondArtist = normalizedTrackArtist(second);
- return !firstArtist || !secondArtist || firstArtist === secondArtist;
+ if (firstArtist && secondArtist)
+ return firstArtist === secondArtist;
+ const firstUrl = (first?.metadata?.["xesam:url"] || "").toString();
+ const secondUrl = (second?.metadata?.["xesam:url"] || "").toString();
+ if (firstUrl && secondUrl)
+ return firstUrl === secondUrl;
+ const firstAlbum = (first?.trackAlbum || "").trim().toLowerCase();
+ const secondAlbum = (second?.trackAlbum || "").trim().toLowerCase();
+ if (firstAlbum && secondAlbum)
+ return firstAlbum === secondAlbum;
+ return true;
}
function metadataQuality(player: MprisPlayer): int {
@@ -218,10 +225,16 @@ Singleton {
return quality;
}
- function _bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
- const equivalents = availablePlayers.filter(candidate => {
+ function equivalentPlayers(player: MprisPlayer): var {
+ if (!player)
+ return [];
+ return availablePlayers.filter(candidate => {
return candidate.playbackState !== MprisPlaybackState.Stopped && isSameTrack(player, candidate);
});
+ }
+
+ function bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
+ const equivalents = equivalentPlayers(player);
if (equivalents.length === 0)
return player;
return equivalents.reduce((best, candidate) => {
@@ -238,9 +251,7 @@ Singleton {
if (activePlayer?.isPlaying) {
if (activePlayer.canControl || controllable.length === 0)
return activePlayer;
- // Active source is playing but not controllable: only hand ownership to a
- // controllable *equivalent* peer (same track). Never let an unrelated
- // player steal the active source while it is still playing.
+ // Playing but not controllable: only a same-track controllable peer may take over
const mirror = controllable.find(player => isSameTrack(activePlayer, player));
return mirror || activePlayer;
}
diff --git a/quickshell/Services/TrackArtService.qml b/quickshell/Services/TrackArtService.qml
index 98bd3aa4b..e40d6c5ea 100644
--- a/quickshell/Services/TrackArtService.qml
+++ b/quickshell/Services/TrackArtService.qml
@@ -59,11 +59,8 @@ Singleton {
if (directUrl !== "")
return directUrl;
- const equivalent = MprisController.availablePlayers.find(candidate => {
- return candidate !== player
- && candidate.playbackState !== MprisPlaybackState.Stopped
- && MprisController.isSameTrack(player, candidate)
- && _directArtworkUrl(candidate) !== "";
+ const equivalent = MprisController.equivalentPlayers(player).find(candidate => {
+ return candidate !== player && _directArtworkUrl(candidate) !== "";
});
return _directArtworkUrl(equivalent);
}
@@ -229,8 +226,7 @@ Singleton {
}
_pendingArtKey = key;
const url = getArtworkUrl(activePlayer);
- // Ignore duplicate notifications, but allow a richer peer to replace
- // the artwork for the same canonical track.
+ // Ignore duplicate notifications, but let a richer peer replace same-track art
if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
return;
if (key !== "" && url !== "" && url === _committedSrcUrl) {