mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-07 14:08:29 -04:00
refactor(mpris): enrich metadata across equivalent players (#2928)
* fix(mpris): enrich metadata across equivalent players * fix(mpris): address review feedback on source stealing and title stripping - _bestPlayingPlayer: when the active player is playing but not controllable, only hand ownership to a controllable peer that passes isSameTrack; unrelated players can no longer steal the active source. - displayTrackTitle: keep the full trackTitle for display. Suffix stripping now only applies to the isSameTrack matching key, and only for a known app-name allowlist (YouTube, SoundCloud, browsers, etc.) so legitimate titles containing ' | ' are never mangled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Port 1.5
This commit is contained in:
@@ -493,7 +493,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: activePlayer?.trackAlbum || ""
|
text: MprisController.stableAlbum
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextSecondary
|
color: Theme.surfaceTextSecondary
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ Singleton {
|
|||||||
// Chromium can report blank metadata between tracks
|
// Chromium can report blank metadata between tracks
|
||||||
property string stableTitle: ""
|
property string stableTitle: ""
|
||||||
property string stableArtist: ""
|
property string stableArtist: ""
|
||||||
|
property string stableAlbum: ""
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.activePlayer
|
target: root.activePlayer
|
||||||
@@ -59,6 +60,9 @@ Singleton {
|
|||||||
root._syncStableMeta();
|
root._syncStableMeta();
|
||||||
root._checkIdle();
|
root._checkIdle();
|
||||||
}
|
}
|
||||||
|
function onTrackAlbumChanged() {
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
function onLengthChanged() {
|
function onLengthChanged() {
|
||||||
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
|
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
|
||||||
root.activePlayerStableLength = root.activePlayer.length;
|
root.activePlayerStableLength = root.activePlayer.length;
|
||||||
@@ -72,8 +76,10 @@ Singleton {
|
|||||||
|
|
||||||
onActivePlayerChanged: {
|
onActivePlayerChanged: {
|
||||||
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
|
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
|
||||||
stableTitle = activePlayer?.trackTitle || "";
|
stableTitle = "";
|
||||||
stableArtist = activePlayer?.trackArtist || "";
|
stableArtist = "";
|
||||||
|
stableAlbum = "";
|
||||||
|
_syncStableMeta();
|
||||||
_checkIdle();
|
_checkIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,14 +88,24 @@ Singleton {
|
|||||||
if (!p) {
|
if (!p) {
|
||||||
stableTitle = "";
|
stableTitle = "";
|
||||||
stableArtist = "";
|
stableArtist = "";
|
||||||
|
stableAlbum = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isFirefoxYoutubeHoverPreview(p))
|
if (isFirefoxYoutubeHoverPreview(p))
|
||||||
return;
|
return;
|
||||||
if (p.trackTitle)
|
const metadataPlayer = _bestMetadataPlayer(p);
|
||||||
stableTitle = p.trackTitle;
|
const nextTitle = displayTrackTitle(metadataPlayer);
|
||||||
if (p.trackArtist)
|
const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
|
||||||
stableArtist = p.trackArtist;
|
if (trackChanged) {
|
||||||
|
stableArtist = "";
|
||||||
|
stableAlbum = "";
|
||||||
|
}
|
||||||
|
if (nextTitle)
|
||||||
|
stableTitle = nextTitle;
|
||||||
|
if (metadataPlayer.trackArtist)
|
||||||
|
stableArtist = metadataPlayer.trackArtist;
|
||||||
|
if (metadataPlayer.trackAlbum)
|
||||||
|
stableAlbum = metadataPlayer.trackAlbum;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chromium reports stopped media w/blank metadata, resolve by checking idle status
|
// Chromium reports stopped media w/blank metadata, resolve by checking idle status
|
||||||
@@ -101,6 +117,7 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
root.stableTitle = "";
|
root.stableTitle = "";
|
||||||
root.stableArtist = "";
|
root.stableArtist = "";
|
||||||
|
root.stableAlbum = "";
|
||||||
root._resolveActivePlayer();
|
root._resolveActivePlayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,9 +139,30 @@ Singleton {
|
|||||||
delegate: Connections {
|
delegate: Connections {
|
||||||
required property MprisPlayer modelData
|
required property MprisPlayer modelData
|
||||||
target: modelData
|
target: modelData
|
||||||
|
ignoreUnknownSignals: true
|
||||||
function onIsPlayingChanged() {
|
function onIsPlayingChanged() {
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onTrackTitleChanged() {
|
||||||
if (modelData.isPlaying)
|
if (modelData.isPlaying)
|
||||||
root._resolveActivePlayer();
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onTrackArtistChanged() {
|
||||||
|
if (modelData.isPlaying)
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onTrackAlbumChanged() {
|
||||||
|
if (modelData.isPlaying)
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onMetadataChanged() {
|
||||||
|
if (modelData.isPlaying)
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,9 +171,90 @@ Singleton {
|
|||||||
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App-name suffixes that browsers/integrations append as "<title> | <App>".
|
||||||
|
// Only these are stripped for track matching; display always keeps the full title.
|
||||||
|
readonly property var _appTitleSuffixes: ["youtube", "youtube music", "soundcloud", "spotify", "chrome", "chromium", "firefox", "brave", "vivaldi", "twitch"]
|
||||||
|
|
||||||
|
function _stripAppTitleSuffix(title: string): string {
|
||||||
|
const idx = title.lastIndexOf(" | ");
|
||||||
|
if (idx <= 0)
|
||||||
|
return title;
|
||||||
|
const suffix = title.substring(idx + 3).trim().toLowerCase();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedTrackArtist(player: MprisPlayer): string {
|
||||||
|
return (player?.trackArtist || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameTrack(first: MprisPlayer, second: MprisPlayer): bool {
|
||||||
|
const firstTitle = normalizedTrackTitle(first);
|
||||||
|
const secondTitle = normalizedTrackTitle(second);
|
||||||
|
if (!firstTitle || firstTitle !== secondTitle)
|
||||||
|
return false;
|
||||||
|
const firstArtist = normalizedTrackArtist(first);
|
||||||
|
const secondArtist = normalizedTrackArtist(second);
|
||||||
|
return !firstArtist || !secondArtist || firstArtist === secondArtist;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metadataQuality(player: MprisPlayer): int {
|
||||||
|
if (!player)
|
||||||
|
return -1;
|
||||||
|
let quality = player.trackArtist ? 100 : 0;
|
||||||
|
quality += player.trackTitle ? 40 : 0;
|
||||||
|
quality += player.trackAlbum ? 20 : 0;
|
||||||
|
quality += player.trackArtUrl || player.metadata?.["mpris:artUrl"] ? 10 : 0;
|
||||||
|
quality += player.metadata?.["xesam:url"] ? 5 : 0;
|
||||||
|
return quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
|
||||||
|
const equivalents = availablePlayers.filter(candidate => {
|
||||||
|
return candidate.playbackState !== MprisPlaybackState.Stopped && isSameTrack(player, candidate);
|
||||||
|
});
|
||||||
|
if (equivalents.length === 0)
|
||||||
|
return player;
|
||||||
|
return equivalents.reduce((best, candidate) => {
|
||||||
|
return metadataQuality(candidate) > metadataQuality(best) ? candidate : best;
|
||||||
|
}, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bestPlayingPlayer(): MprisPlayer {
|
||||||
|
const playing = availablePlayers.filter(player => player.isPlaying);
|
||||||
|
if (playing.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const controllable = playing.filter(player => player.canControl);
|
||||||
|
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.
|
||||||
|
const mirror = controllable.find(player => isSameTrack(activePlayer, player));
|
||||||
|
return mirror || activePlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePlayer?.canControl && activePlayer.playbackState === MprisPlaybackState.Paused) {
|
||||||
|
const onlyEquivalentMirrors = playing.every(player => isSameTrack(activePlayer, player));
|
||||||
|
if (onlyEquivalentMirrors)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return controllable[0] || playing[0];
|
||||||
|
}
|
||||||
|
|
||||||
function _resolveActivePlayer(): void {
|
function _resolveActivePlayer(): void {
|
||||||
// A playing player always wins; otherwise keep the selection stable w/idle
|
const playing = _bestPlayingPlayer();
|
||||||
const playing = availablePlayers.find(p => p.isPlaying);
|
|
||||||
if (playing) {
|
if (playing) {
|
||||||
if (activePlayer !== playing) {
|
if (activePlayer !== playing) {
|
||||||
activePlayer = playing;
|
activePlayer = playing;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Singleton {
|
|||||||
return hash.toString(16).padStart(8, '0');
|
return hash.toString(16).padStart(8, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArtworkUrl(player) {
|
function _directArtworkUrl(player) {
|
||||||
if (!player) return "";
|
if (!player) return "";
|
||||||
|
|
||||||
let artUrl = player.trackArtUrl || "";
|
let artUrl = player.trackArtUrl || "";
|
||||||
@@ -54,6 +54,20 @@ Singleton {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getArtworkUrl(player) {
|
||||||
|
const directUrl = _directArtworkUrl(player);
|
||||||
|
if (directUrl !== "")
|
||||||
|
return directUrl;
|
||||||
|
|
||||||
|
const equivalent = MprisController.availablePlayers.find(candidate => {
|
||||||
|
return candidate !== player
|
||||||
|
&& candidate.playbackState !== MprisPlaybackState.Stopped
|
||||||
|
&& MprisController.isSameTrack(player, candidate)
|
||||||
|
&& _directArtworkUrl(candidate) !== "";
|
||||||
|
});
|
||||||
|
return _directArtworkUrl(equivalent);
|
||||||
|
}
|
||||||
|
|
||||||
function _commit(u, artKey, srcUrl) {
|
function _commit(u, artKey, srcUrl) {
|
||||||
resolvedArtUrl = u;
|
resolvedArtUrl = u;
|
||||||
_committedArtKey = u !== "" ? artKey : "";
|
_committedArtKey = u !== "" ? artKey : "";
|
||||||
@@ -171,11 +185,25 @@ Singleton {
|
|||||||
onActivePlayerChanged: _updateArtUrl()
|
onActivePlayerChanged: _updateArtUrl()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.activePlayer
|
target: MprisController
|
||||||
ignoreUnknownSignals: true
|
function onAvailablePlayersChanged() {
|
||||||
function onTrackTitleChanged() { root._updateArtUrl(); }
|
root._updateArtUrl();
|
||||||
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
}
|
||||||
function onMetadataChanged() { root._updateArtUrl(); }
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: MprisController.availablePlayers
|
||||||
|
delegate: Connections {
|
||||||
|
required property MprisPlayer modelData
|
||||||
|
target: modelData
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onIsPlayingChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackTitleChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackArtistChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackAlbumChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
||||||
|
function onMetadataChanged() { root._updateArtUrl(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _trackKey() {
|
function _trackKey() {
|
||||||
@@ -201,8 +229,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
_pendingArtKey = key;
|
_pendingArtKey = key;
|
||||||
const url = getArtworkUrl(activePlayer);
|
const url = getArtworkUrl(activePlayer);
|
||||||
// Ignore Chrome's same-track thumbnail size updates.
|
// Ignore duplicate notifications, but allow a richer peer to replace
|
||||||
if (key !== "" && key === _committedArtKey)
|
// the artwork for the same canonical track.
|
||||||
|
if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
|
||||||
return;
|
return;
|
||||||
if (key !== "" && url !== "" && url === _committedSrcUrl) {
|
if (key !== "" && url !== "" && url === _committedSrcUrl) {
|
||||||
// Chrome can publish track metadata before its new artwork URL.
|
// Chrome can publish track metadata before its new artwork URL.
|
||||||
|
|||||||
Reference in New Issue
Block a user