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
This commit is contained in:
@@ -13,33 +13,6 @@ BasePill {
|
|||||||
readonly property bool _hoverPreview: MprisController.isFirefoxYoutubeHoverPreview(activePlayer)
|
readonly property bool _hoverPreview: MprisController.isFirefoxYoutubeHoverPreview(activePlayer)
|
||||||
readonly property bool _isPlaying: !!activePlayer && activePlayer.playbackState === 1 && !_hoverPreview
|
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: {
|
readonly property bool __isChromeBrowser: {
|
||||||
if (!activePlayer?.identity)
|
if (!activePlayer?.identity)
|
||||||
return false;
|
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")
|
readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari")
|
||||||
|
|
||||||
property string displayText: {
|
property string displayText: {
|
||||||
if (!activePlayer || !root._stableTitle)
|
if (!activePlayer || !MprisController.stableTitle)
|
||||||
return "";
|
return "";
|
||||||
const title = isWebMedia ? root._stableTitle : (root._stableTitle || "Unknown Track");
|
const title = MprisController.stableTitle;
|
||||||
const subtitle = isWebMedia ? (root._stableArtist || cachedIdentity) : (root._stableArtist || "");
|
const subtitle = isWebMedia ? (MprisController.stableArtist || cachedIdentity) : MprisController.stableArtist;
|
||||||
return subtitle.length > 0 ? title + " • " + subtitle : title;
|
return subtitle.length > 0 ? title + " • " + subtitle : title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Item {
|
|||||||
property int dropdownType: 0
|
property int dropdownType: 0
|
||||||
property var activePlayer: null
|
property var activePlayer: null
|
||||||
property var allPlayers: []
|
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 point anchorPos: Qt.point(0, 0)
|
||||||
property bool isRightEdge: false
|
property bool isRightEdge: false
|
||||||
property var targetWindow: null
|
property var targetWindow: null
|
||||||
@@ -64,6 +66,22 @@ Item {
|
|||||||
panelExited();
|
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: {
|
readonly property Item __activePanel: {
|
||||||
switch (dropdownType) {
|
switch (dropdownType) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -144,6 +162,7 @@ Item {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onEntered: panelAreaEntered()
|
onEntered: panelAreaEntered()
|
||||||
onExited: panelAreaExited()
|
onExited: panelAreaExited()
|
||||||
|
onWheel: wheelEvent => root.volumeWheel(wheelEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -203,6 +222,7 @@ Item {
|
|||||||
|
|
||||||
onEntered: panelAreaEntered()
|
onEntered: panelAreaEntered()
|
||||||
onExited: panelAreaExited()
|
onExited: panelAreaExited()
|
||||||
|
onWheel: wheelEvent => root.volumeWheel(wheelEvent)
|
||||||
onPressed: mouse => updateVolume(mouse)
|
onPressed: mouse => updateVolume(mouse)
|
||||||
onPositionChanged: mouse => {
|
onPositionChanged: mouse => {
|
||||||
if (pressed)
|
if (pressed)
|
||||||
@@ -428,7 +448,7 @@ Item {
|
|||||||
id: playersPanel
|
id: playersPanel
|
||||||
visible: dropdownType === 3
|
visible: dropdownType === 3
|
||||||
width: 240
|
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
|
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
||||||
y: anchorPos.y - height / 2
|
y: anchorPos.y - height / 2
|
||||||
radius: Theme.cornerRadius * 2
|
radius: Theme.cornerRadius * 2
|
||||||
@@ -485,7 +505,7 @@ Item {
|
|||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Media Players (") + (allPlayers?.length || 0) + ")"
|
text: I18n.tr("Media Players (") + (root.selectablePlayers?.length || 0) + ")"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
@@ -506,7 +526,7 @@ Item {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: allPlayers || []
|
model: root.selectablePlayers || []
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
@@ -582,5 +602,4 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ Item {
|
|||||||
playersExpanded = false;
|
playersExpanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
readonly property bool isRightEdge: {
|
readonly property bool isRightEdge: {
|
||||||
if (barPosition === SettingsData.Position.Right)
|
if (barPosition === SettingsData.Position.Right)
|
||||||
return true;
|
return true;
|
||||||
@@ -342,54 +340,6 @@ Item {
|
|||||||
root.maybeFinishSwitch();
|
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 {
|
BgBlurLayer {
|
||||||
id: layerA
|
id: layerA
|
||||||
front: bgContainer._showA
|
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 {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
@@ -469,7 +467,7 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: activePlayer?.trackTitle || I18n.tr("Unknown Track")
|
text: MprisController.stableTitle || I18n.tr("Unknown Track")
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
@@ -481,7 +479,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: activePlayer?.trackArtist || I18n.tr("Unknown Artist")
|
text: MprisController.stableArtist || I18n.tr("Unknown Artist")
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -799,16 +797,17 @@ Item {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (playersExpanded) {
|
if (playersExpanded) {
|
||||||
if (allPlayers && allPlayers.length > 1) {
|
const players = (root.allPlayers || []).filter(p => p && !MprisController.isIdle(p));
|
||||||
|
if (players.length > 1) {
|
||||||
let currentIndex = -1;
|
let currentIndex = -1;
|
||||||
for (let i = 0; i < allPlayers.length; i++) {
|
for (let i = 0; i < players.length; i++) {
|
||||||
if (allPlayers[i] === activePlayer) {
|
if (players[i] === root.activePlayer) {
|
||||||
currentIndex = i;
|
currentIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nextIndex = (currentIndex + 1) % allPlayers.length;
|
const nextIndex = (currentIndex + 1) % players.length;
|
||||||
MprisController.setActivePlayer(allPlayers[nextIndex]);
|
MprisController.setActivePlayer(players[nextIndex]);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -885,19 +884,15 @@ Item {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
toggleMute();
|
toggleMute();
|
||||||
}
|
}
|
||||||
|
property real wheelAccum: 0
|
||||||
onWheel: wheelEvent => {
|
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;
|
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
|
topPadding: Theme.spacingL
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: activePlayer?.trackTitle || I18n.tr("Unknown")
|
text: MprisController.stableTitle || I18n.tr("Unknown")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
@@ -99,7 +99,7 @@ Card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: activePlayer?.trackArtist || I18n.tr("Unknown Artist")
|
text: MprisController.stableArtist || I18n.tr("Unknown Artist")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ Singleton {
|
|||||||
const desktopEntry = ("desktopEntry" in p && p.desktopEntry) ? String(p.desktopEntry).toLowerCase() : "";
|
const desktopEntry = ("desktopEntry" in p && p.desktopEntry) ? String(p.desktopEntry).toLowerCase() : "";
|
||||||
return !excluded.some(ex => {
|
return !excluded.some(ex => {
|
||||||
const exLower = String(ex).toLowerCase().trim();
|
const exLower = String(ex).toLowerCase().trim();
|
||||||
if (!exLower) return false;
|
if (!exLower)
|
||||||
|
return false;
|
||||||
|
|
||||||
// 1. Substring match
|
// 1. Substring match
|
||||||
if (identity.includes(exLower) || desktopEntry.includes(exLower))
|
if (identity.includes(exLower) || desktopEntry.includes(exLower))
|
||||||
@@ -43,43 +44,114 @@ Singleton {
|
|||||||
}
|
}
|
||||||
property MprisPlayer activePlayer: null
|
property MprisPlayer activePlayer: null
|
||||||
property real activePlayerStableLength: 0
|
property real activePlayerStableLength: 0
|
||||||
|
// Chromium can report blank metadata between tracks
|
||||||
|
property string stableTitle: ""
|
||||||
|
property string stableArtist: ""
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.activePlayer
|
target: root.activePlayer
|
||||||
function onTrackTitleChanged() {
|
function onTrackTitleChanged() {
|
||||||
root.activePlayerStableLength = (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) ? root.activePlayer.length : 0;
|
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() {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function onPlaybackStateChanged() {
|
||||||
|
root._syncStableMeta();
|
||||||
|
root._checkIdle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 || "";
|
||||||
|
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()
|
onAvailablePlayersChanged: _resolveActivePlayer()
|
||||||
Component.onCompleted: _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 {
|
function isIdle(player: MprisPlayer): bool {
|
||||||
return player
|
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
||||||
&& player.playbackState === MprisPlaybackState.Stopped
|
|
||||||
&& !player.trackTitle
|
|
||||||
&& !player.trackArtist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _resolveActivePlayer(): void {
|
function _resolveActivePlayer(): void {
|
||||||
// Keep the selected player stable across transient metadata changes.
|
// A playing player always wins; otherwise keep the selection stable w/idle
|
||||||
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0)
|
|
||||||
return;
|
|
||||||
const playing = availablePlayers.find(p => p.isPlaying);
|
const playing = availablePlayers.find(p => p.isPlaying);
|
||||||
if (playing) {
|
if (playing) {
|
||||||
activePlayer = playing;
|
if (activePlayer !== playing) {
|
||||||
_persistIdentity(playing.identity);
|
activePlayer = playing;
|
||||||
|
_persistIdentity(playing.identity);
|
||||||
|
}
|
||||||
return;
|
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;
|
const savedId = SessionData.lastPlayerIdentity;
|
||||||
if (savedId) {
|
if (savedId) {
|
||||||
const match = availablePlayers.find(p => p.identity === savedId);
|
const match = availablePlayers.find(p => p.identity === savedId);
|
||||||
|
|||||||
@@ -29,19 +29,17 @@ Singleton {
|
|||||||
function getArtworkUrl(player) {
|
function getArtworkUrl(player) {
|
||||||
if (!player) return "";
|
if (!player) return "";
|
||||||
|
|
||||||
// 1. If native trackArtUrl is present and valid
|
|
||||||
let artUrl = player.trackArtUrl || "";
|
let artUrl = player.trackArtUrl || "";
|
||||||
if (artUrl !== "") {
|
if (artUrl !== "") {
|
||||||
return artUrl;
|
return artUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback to raw metadata mpris:artUrl if present
|
|
||||||
if (player.metadata && player.metadata["mpris:artUrl"]) {
|
if (player.metadata && player.metadata["mpris:artUrl"]) {
|
||||||
artUrl = player.metadata["mpris:artUrl"].toString();
|
artUrl = player.metadata["mpris:artUrl"].toString();
|
||||||
if (artUrl !== "") return artUrl;
|
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"]) {
|
if (player.metadata && player.metadata["xesam:url"]) {
|
||||||
const url = player.metadata["xesam:url"].toString();
|
const url = player.metadata["xesam:url"].toString();
|
||||||
if (url.includes("youtube.com") || url.includes("youtu.be")) {
|
if (url.includes("youtube.com") || url.includes("youtu.be")) {
|
||||||
@@ -71,9 +69,11 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_clearDebounce.stop();
|
_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;
|
return;
|
||||||
_lastArtUrl = url;
|
_lastArtUrl = url;
|
||||||
|
_lastIssuedSerial = requestSerial;
|
||||||
|
|
||||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -83,7 +83,6 @@ Singleton {
|
|||||||
const filePath = cacheDir + "/remote_" + hash;
|
const filePath = cacheDir + "/remote_" + hash;
|
||||||
const localFileUrl = "file://" + filePath;
|
const localFileUrl = "file://" + filePath;
|
||||||
|
|
||||||
// 1. First, check if the file already exists locally
|
|
||||||
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
|
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
|
||||||
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
|
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
|
||||||
return;
|
return;
|
||||||
@@ -94,7 +93,7 @@ Singleton {
|
|||||||
} else {
|
} else {
|
||||||
const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
|
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/")) {
|
if (targetUrl.includes("img.youtube.com/vi/")) {
|
||||||
const videoId = targetUrl.split("/vi/")[1].split("/")[0];
|
const videoId = targetUrl.split("/vi/")[1].split("/")[0];
|
||||||
const maxresUrl = "https://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg";
|
const maxresUrl = "https://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg";
|
||||||
@@ -119,7 +118,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}, 50, 15000);
|
}, 50, 15000);
|
||||||
} else {
|
} else {
|
||||||
// Standard curl download for other remote URLs (e.g. SoundCloud)
|
|
||||||
const tmpPath = filePath + ".tmp";
|
const tmpPath = filePath + ".tmp";
|
||||||
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => {
|
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => {
|
||||||
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
|
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
|
||||||
@@ -168,6 +166,7 @@ Singleton {
|
|||||||
property string _pendingArtKey: ""
|
property string _pendingArtKey: ""
|
||||||
property string _committedSrcUrl: ""
|
property string _committedSrcUrl: ""
|
||||||
property int _requestSerial: 0
|
property int _requestSerial: 0
|
||||||
|
property int _lastIssuedSerial: -1
|
||||||
|
|
||||||
onActivePlayerChanged: _updateArtUrl()
|
onActivePlayerChanged: _updateArtUrl()
|
||||||
|
|
||||||
@@ -183,8 +182,8 @@ Singleton {
|
|||||||
const p = activePlayer;
|
const p = activePlayer;
|
||||||
if (!p)
|
if (!p)
|
||||||
return "";
|
return "";
|
||||||
// Scope artwork ownership to the MPRIS object and track.
|
// dbusName is constant per player; uniqueId is per-track and would churn the key.
|
||||||
const playerId = p.uniqueId || p.identity || "";
|
const playerId = p.dbusName || p.identity || "";
|
||||||
const tid = p.metadata && p.metadata["mpris:trackid"] ? p.metadata["mpris:trackid"].toString() : "";
|
const tid = p.metadata && p.metadata["mpris:trackid"] ? p.metadata["mpris:trackid"].toString() : "";
|
||||||
return playerId + " " + tid + " " + (p.trackTitle || "") + " " + (p.trackArtist || "");
|
return playerId + " " + tid + " " + (p.trackTitle || "") + " " + (p.trackArtist || "");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user