1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-01 19:18:28 -04:00

fix(media-player): update Mpris stable metadata handling

- Restores Brave browsers
port 1.5

(cherry picked from commit fe1a783ec2)
This commit is contained in:
purian23
2026-07-15 20:57:05 -04:00
committed by dms-ci[bot]
parent 3d0ce17a8c
commit a55177f1d2
6 changed files with 181 additions and 123 deletions
+3 -30
View File
@@ -13,33 +13,6 @@ BasePill {
readonly property bool _hoverPreview: MprisController.isFirefoxYoutubeHoverPreview(activePlayer)
readonly property bool _isPlaying: !!activePlayer && activePlayer.playbackState === 1 && !_hoverPreview
property string _stableTitle: ""
property string _stableArtist: ""
Connections {
target: root.activePlayer
function onTrackTitleChanged() {
root._syncMeta();
}
function onTrackArtistChanged() {
root._syncMeta();
}
}
onActivePlayerChanged: _syncMeta()
function _syncMeta() {
if (!activePlayer) {
_stableTitle = "";
_stableArtist = "";
return;
}
if (MprisController.isFirefoxYoutubeHoverPreview(activePlayer))
return;
_stableTitle = activePlayer.trackTitle || "";
_stableArtist = activePlayer.trackArtist || "";
}
readonly property bool __isChromeBrowser: {
if (!activePlayer?.identity)
return false;
@@ -310,10 +283,10 @@ BasePill {
readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari")
property string displayText: {
if (!activePlayer || !root._stableTitle)
if (!activePlayer || !MprisController.stableTitle)
return "";
const title = isWebMedia ? root._stableTitle : (root._stableTitle || "Unknown Track");
const subtitle = isWebMedia ? (root._stableArtist || cachedIdentity) : (root._stableArtist || "");
const title = MprisController.stableTitle;
const subtitle = isWebMedia ? (MprisController.stableArtist || cachedIdentity) : MprisController.stableArtist;
return subtitle.length > 0 ? title + " • " + subtitle : title;
}
@@ -14,6 +14,8 @@ Item {
property int dropdownType: 0
property var activePlayer: null
property var allPlayers: []
// Chromium keeps dead MPRIS services registered w/empty metadata; avoid listing them
readonly property var selectablePlayers: (allPlayers || []).filter(p => p && !MprisController.isIdle(p))
property point anchorPos: Qt.point(0, 0)
property bool isRightEdge: false
property var targetWindow: null
@@ -64,6 +66,22 @@ Item {
panelExited();
}
property real _wheelAccum: 0
function volumeWheel(wheelEvent) {
if (!volumeAvailable)
return;
wheelEvent.accepted = true;
_wheelAccum += wheelEvent.angleDelta.y;
const notches = _wheelAccum > 0 ? Math.floor(_wheelAccum / 120) : Math.ceil(_wheelAccum / 120);
if (notches === 0)
return;
_wheelAccum -= notches * 120;
SessionData.suppressOSDTemporarily();
const next = currentVolume + notches * AudioService.wheelVolumeStep / 100;
root.volumeChanged(Math.max(0, Math.min(1, next)));
}
readonly property Item __activePanel: {
switch (dropdownType) {
case 1:
@@ -144,6 +162,7 @@ Item {
hoverEnabled: true
onEntered: panelAreaEntered()
onExited: panelAreaExited()
onWheel: wheelEvent => root.volumeWheel(wheelEvent)
}
Item {
@@ -203,6 +222,7 @@ Item {
onEntered: panelAreaEntered()
onExited: panelAreaExited()
onWheel: wheelEvent => root.volumeWheel(wheelEvent)
onPressed: mouse => updateVolume(mouse)
onPositionChanged: mouse => {
if (pressed)
@@ -428,7 +448,7 @@ Item {
id: playersPanel
visible: dropdownType === 3
width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
height: Math.max(180, Math.min(240, (root.selectablePlayers?.length || 0) * 50 + 80))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
@@ -485,7 +505,7 @@ Item {
anchors.margins: Theme.spacingM
StyledText {
text: I18n.tr("Media Players (") + (allPlayers?.length || 0) + ")"
text: I18n.tr("Media Players (") + (root.selectablePlayers?.length || 0) + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
@@ -506,7 +526,7 @@ Item {
spacing: Theme.spacingS
Repeater {
model: allPlayers || []
model: root.selectablePlayers || []
delegate: Rectangle {
required property var modelData
required property int index
@@ -582,5 +602,4 @@ Item {
}
}
}
}
+63 -68
View File
@@ -47,8 +47,6 @@ Item {
playersExpanded = false;
}
readonly property bool isRightEdge: {
if (barPosition === SettingsData.Position.Right)
return true;
@@ -342,54 +340,6 @@ Item {
root.maybeFinishSwitch();
}
component BgBlurLayer: ClippingRectangle {
id: layer
property alias art: layerImg.source
readonly property bool ready: layerImg.status === Image.Ready && layerImg.source != ""
property bool front: false
signal loaded
anchors.fill: parent
radius: Theme.cornerRadius
color: "transparent"
antialiasing: true
opacity: front ? 0.7 : 0
Behavior on opacity {
NumberAnimation {
duration: 350
easing.type: Easing.InOutQuad
}
}
Image {
id: layerImg
anchors.centerIn: parent
width: Math.max(parent.width, parent.height) * 1.1
height: width
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: false
onStatusChanged: {
if (status === Image.Ready && source != "")
layer.loaded();
}
}
MultiEffect {
anchors.centerIn: parent
width: layerImg.width
height: layerImg.height
source: layerImg
blurEnabled: true
blurMax: 64
blur: 0.8
saturation: -0.2
brightness: -0.25
}
}
BgBlurLayer {
id: layerA
front: bgContainer._showA
@@ -410,6 +360,54 @@ Item {
}
}
component BgBlurLayer: ClippingRectangle {
id: layer
property alias art: layerImg.source
readonly property bool ready: layerImg.status === Image.Ready && layerImg.source != ""
property bool front: false
signal loaded
anchors.fill: parent
radius: Theme.cornerRadius
color: "transparent"
antialiasing: true
opacity: front ? 0.7 : 0
Behavior on opacity {
NumberAnimation {
duration: 350
easing.type: Easing.InOutQuad
}
}
Image {
id: layerImg
anchors.centerIn: parent
width: Math.max(parent.width, parent.height) * 1.1
height: width
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: false
onStatusChanged: {
if (status === Image.Ready && source != "")
layer.loaded();
}
}
MultiEffect {
anchors.centerIn: parent
width: layerImg.width
height: layerImg.height
source: layerImg
blurEnabled: true
blurMax: 64
blur: 0.8
saturation: -0.2
brightness: -0.25
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
@@ -469,7 +467,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: activePlayer?.trackTitle || I18n.tr("Unknown Track")
text: MprisController.stableTitle || I18n.tr("Unknown Track")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
@@ -481,7 +479,7 @@ Item {
}
StyledText {
text: activePlayer?.trackArtist || I18n.tr("Unknown Artist")
text: MprisController.stableArtist || I18n.tr("Unknown Artist")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
@@ -799,16 +797,17 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: {
if (playersExpanded) {
if (allPlayers && allPlayers.length > 1) {
const players = (root.allPlayers || []).filter(p => p && !MprisController.isIdle(p));
if (players.length > 1) {
let currentIndex = -1;
for (let i = 0; i < allPlayers.length; i++) {
if (allPlayers[i] === activePlayer) {
for (let i = 0; i < players.length; i++) {
if (players[i] === root.activePlayer) {
currentIndex = i;
break;
}
}
const nextIndex = (currentIndex + 1) % allPlayers.length;
MprisController.setActivePlayer(allPlayers[nextIndex]);
const nextIndex = (currentIndex + 1) % players.length;
MprisController.setActivePlayer(players[nextIndex]);
}
return;
}
@@ -885,19 +884,15 @@ Item {
onClicked: {
toggleMute();
}
property real wheelAccum: 0
onWheel: wheelEvent => {
SessionData.suppressOSDTemporarily();
const delta = wheelEvent.angleDelta.y;
const current = (currentVolume * 100) || 0;
const maxVol = usePlayerVolume ? 100 : AudioService.sinkMaxVolume;
const newVolume = delta > 0 ? Math.min(maxVol, current + 5) : Math.max(0, current - 5);
if (usePlayerVolume) {
activePlayer.volume = newVolume / 100;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = newVolume / 100;
}
wheelEvent.accepted = true;
wheelAccum += wheelEvent.angleDelta.y;
const notches = wheelAccum > 0 ? Math.floor(wheelAccum / 120) : Math.ceil(wheelAccum / 120);
if (notches === 0)
return;
wheelAccum -= notches * 120;
root.adjustVolume(notches * AudioService.wheelVolumeStep);
}
}
}
@@ -88,7 +88,7 @@ Card {
topPadding: Theme.spacingL
StyledText {
text: activePlayer?.trackTitle || I18n.tr("Unknown")
text: MprisController.stableTitle || I18n.tr("Unknown")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
@@ -99,7 +99,7 @@ Card {
}
StyledText {
text: activePlayer?.trackArtist || I18n.tr("Unknown Artist")
text: MprisController.stableArtist || I18n.tr("Unknown Artist")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
+82 -10
View File
@@ -19,7 +19,8 @@ Singleton {
const desktopEntry = ("desktopEntry" in p && p.desktopEntry) ? String(p.desktopEntry).toLowerCase() : "";
return !excluded.some(ex => {
const exLower = String(ex).toLowerCase().trim();
if (!exLower) return false;
if (!exLower)
return false;
// 1. Substring match
if (identity.includes(exLower) || desktopEntry.includes(exLower))
@@ -43,43 +44,114 @@ Singleton {
}
property MprisPlayer activePlayer: null
property real activePlayerStableLength: 0
// Chromium can report blank metadata between tracks
property string stableTitle: ""
property string stableArtist: ""
Connections {
target: root.activePlayer
function onTrackTitleChanged() {
root.activePlayerStableLength = (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) ? root.activePlayer.length : 0;
root._syncStableMeta();
root._checkIdle();
}
function onTrackArtistChanged() {
root._syncStableMeta();
root._checkIdle();
}
function onLengthChanged() {
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
root.activePlayerStableLength = root.activePlayer.length;
}
}
function onPlaybackStateChanged() {
root._syncStableMeta();
root._checkIdle();
}
}
onActivePlayerChanged: {
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
stableTitle = activePlayer?.trackTitle || "";
stableArtist = activePlayer?.trackArtist || "";
_checkIdle();
}
function _syncStableMeta(): void {
const p = activePlayer;
if (!p) {
stableTitle = "";
stableArtist = "";
return;
}
if (isFirefoxYoutubeHoverPreview(p))
return;
if (p.trackTitle)
stableTitle = p.trackTitle;
if (p.trackArtist)
stableArtist = p.trackArtist;
}
// Chromium reports stopped media w/blank metadata, resolve by checking idle status
Timer {
id: _idleGraceTimer
interval: 1223
onTriggered: {
if (!root.isIdle(root.activePlayer))
return;
root.stableTitle = "";
root.stableArtist = "";
root._resolveActivePlayer();
}
}
function _checkIdle(): void {
if (!isIdle(activePlayer)) {
_idleGraceTimer.stop();
return;
}
if (!_idleGraceTimer.running)
_idleGraceTimer.start();
}
onAvailablePlayersChanged: _resolveActivePlayer()
Component.onCompleted: _resolveActivePlayer()
Instantiator {
model: root.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
function onIsPlayingChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
}
}
}
function isIdle(player: MprisPlayer): bool {
return player
&& player.playbackState === MprisPlaybackState.Stopped
&& !player.trackTitle
&& !player.trackArtist;
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
}
function _resolveActivePlayer(): void {
// Keep the selected player stable across transient metadata changes.
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0)
return;
// A playing player always wins; otherwise keep the selection stable w/idle
const playing = availablePlayers.find(p => p.isPlaying);
if (playing) {
activePlayer = playing;
_persistIdentity(playing.identity);
if (activePlayer !== playing) {
activePlayer = playing;
_persistIdentity(playing.identity);
}
return;
}
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0 && (!isIdle(activePlayer) || _idleGraceTimer.running))
return;
if (activePlayer && availablePlayers.indexOf(activePlayer) < 0) {
const successor = availablePlayers.find(p => p.identity === activePlayer.identity);
if (successor) {
activePlayer = successor;
return;
}
}
const savedId = SessionData.lastPlayerIdentity;
if (savedId) {
const match = availablePlayers.find(p => p.identity === savedId);
+8 -9
View File
@@ -29,19 +29,17 @@ Singleton {
function getArtworkUrl(player) {
if (!player) return "";
// 1. If native trackArtUrl is present and valid
let artUrl = player.trackArtUrl || "";
if (artUrl !== "") {
return artUrl;
}
// 2. Fallback to raw metadata mpris:artUrl if present
if (player.metadata && player.metadata["mpris:artUrl"]) {
artUrl = player.metadata["mpris:artUrl"].toString();
if (artUrl !== "") return artUrl;
}
// 3. Fallback for YouTube from xesam:url
// YouTube publishes no artUrl; derive the thumbnail from the video id.
if (player.metadata && player.metadata["xesam:url"]) {
const url = player.metadata["xesam:url"].toString();
if (url.includes("youtube.com") || url.includes("youtu.be")) {
@@ -71,9 +69,11 @@ Singleton {
return;
}
_clearDebounce.stop();
if (url === _lastArtUrl)
// Same url must re-issue under a new serial; the bump cancelled the in-flight load.
if (url === _lastArtUrl && requestSerial === _lastIssuedSerial)
return;
_lastArtUrl = url;
_lastIssuedSerial = requestSerial;
if (url.startsWith("http://") || url.startsWith("https://")) {
loading = true;
@@ -83,7 +83,6 @@ Singleton {
const filePath = cacheDir + "/remote_" + hash;
const localFileUrl = "file://" + filePath;
// 1. First, check if the file already exists locally
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
return;
@@ -94,7 +93,7 @@ Singleton {
} else {
const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
// 2. Check if this is a YouTube URL to do high quality 16:9 fallback
// YouTube: try the 16:9 maxres thumbnail before falling back.
if (targetUrl.includes("img.youtube.com/vi/")) {
const videoId = targetUrl.split("/vi/")[1].split("/")[0];
const maxresUrl = "https://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg";
@@ -119,7 +118,6 @@ Singleton {
}
}, 50, 15000);
} else {
// Standard curl download for other remote URLs (e.g. SoundCloud)
const tmpPath = filePath + ".tmp";
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => {
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
@@ -168,6 +166,7 @@ Singleton {
property string _pendingArtKey: ""
property string _committedSrcUrl: ""
property int _requestSerial: 0
property int _lastIssuedSerial: -1
onActivePlayerChanged: _updateArtUrl()
@@ -183,8 +182,8 @@ Singleton {
const p = activePlayer;
if (!p)
return "";
// Scope artwork ownership to the MPRIS object and track.
const playerId = p.uniqueId || p.identity || "";
// dbusName is constant per player; uniqueId is per-track and would churn the key.
const playerId = p.dbusName || p.identity || "";
const tid = p.metadata && p.metadata["mpris:trackid"] ? p.metadata["mpris:trackid"].toString() : "";
return playerId + " " + tid + " " + (p.trackTitle || "") + " " + (p.trackArtist || "");
}