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

media: improve album art handling

This commit is contained in:
bbedward
2026-07-08 12:01:33 -04:00
parent e94af2a7aa
commit a62ae336e0
4 changed files with 159 additions and 62 deletions
+34 -21
View File
@@ -294,7 +294,19 @@ Item {
id: bgContainer id: bgContainer
anchors.fill: parent anchors.fill: parent
readonly property string curArt: TrackArtService.resolvedArtUrl // Fall back to the live mpris url so the background is never blank.
readonly property string curArt: {
const resolved = TrackArtService.resolvedArtUrl;
if (resolved !== "")
return resolved;
const p = root.activePlayer;
if (!p)
return "";
if (p.trackArtUrl)
return p.trackArtUrl;
const m = p.metadata;
return m && m["mpris:artUrl"] ? m["mpris:artUrl"].toString() : "";
}
// Two layers crossfade: new art loads into the hidden one and fades in once decoded. // Two layers crossfade: new art loads into the hidden one and fades in once decoded.
property bool _showA: true property bool _showA: true
visible: layerA.ready || layerB.ready visible: layerA.ready || layerB.ready
@@ -305,18 +317,29 @@ Item {
function syncArt() { function syncArt() {
if (curArt === "") if (curArt === "")
return; return;
const frontArt = _showA ? layerA.art : layerB.art; const front = _showA ? layerA : layerB;
const backArt = _showA ? layerB.art : layerA.art; const back = _showA ? layerB : layerA;
if (frontArt == curArt) if (front.art == curArt)
return; return;
if (backArt == curArt) { if (back.art == curArt) {
// Already decoded in the hidden layer; flip once ready (else promote() does).
if (back.ready)
_showA = !_showA; _showA = !_showA;
return; return;
} }
if (_showA) back.art = curArt;
layerB.art = curArt; }
else
layerA.art = curArt; // Flip to the hidden layer only when it holds the current art, ignoring stale
// Ready re-emits (e.g. popout re-expose) that would otherwise ping-pong _showA.
function promote(layer) {
const back = _showA ? layerB : layerA;
if (layer !== back)
return;
if (layer.art != curArt)
return;
_showA = (layer === layerA);
root.maybeFinishSwitch();
} }
component BgBlurLayer: ClippingRectangle { component BgBlurLayer: ClippingRectangle {
@@ -370,23 +393,13 @@ Item {
BgBlurLayer { BgBlurLayer {
id: layerA id: layerA
front: bgContainer._showA front: bgContainer._showA
onLoaded: { onLoaded: bgContainer.promote(layerA)
if (!bgContainer._showA) {
bgContainer._showA = true;
root.maybeFinishSwitch();
}
}
} }
BgBlurLayer { BgBlurLayer {
id: layerB id: layerB
front: !bgContainer._showA front: !bgContainer._showA
onLoaded: { onLoaded: bgContainer.promote(layerB)
if (bgContainer._showA) {
bgContainer._showA = false;
root.maybeFinishSwitch();
}
}
} }
Rectangle { Rectangle {
+6 -6
View File
@@ -26,13 +26,9 @@ Singleton {
readonly property color accentTrack: Theme.withAlpha(accent, 0.28) readonly property color accentTrack: Theme.withAlpha(accent, 0.28)
readonly property color accentSubtle: Theme.withAlpha(accent, 0.55) readonly property color accentSubtle: Theme.withAlpha(accent, 0.55)
// Plain-named alias: underscore-prefixed props with onChanged handlers crash config load.
readonly property string artUrl: TrackArtService.resolvedArtUrl readonly property string artUrl: TrackArtService.resolvedArtUrl
onArtUrlChanged: {
if (artUrl === "")
_accent = null;
}
// Hold the last accent across the brief artUrl blank between tracks; never reset to primary.
property var _accent: null property var _accent: null
ColorQuantizer { ColorQuantizer {
@@ -40,7 +36,11 @@ Singleton {
source: root.artUrl source: root.artUrl
depth: 4 depth: 4
rescaleSize: 64 rescaleSize: 64
onColorsChanged: root._accent = root._pickAccent(colors) onColorsChanged: {
const a = root._pickAccent(colors);
if (a !== null)
root._accent = a;
}
} }
function _pickAccent(colors) { function _pickAccent(colors) {
+38 -23
View File
@@ -13,6 +13,8 @@ Singleton {
property string resolvedArtUrl: "" property string resolvedArtUrl: ""
property alias _bgArtSource: root.resolvedArtUrl property alias _bgArtSource: root.resolvedArtUrl
property bool loading: false property bool loading: false
// sha1s of placeholder art to reject (Chrome's own logo, shown before real cover).
readonly property var _artHashDenylist: ["764a730860c5b8a7bbee690ee5a443672ae37dc8"]
function djb2Hash(str) { function djb2Hash(str) {
if (!str) return ""; if (!str) return "";
@@ -54,6 +56,11 @@ Singleton {
return ""; return "";
} }
function _commit(u) {
resolvedArtUrl = u;
_committedArtKey = u !== "" ? _pendingArtKey : "";
}
function loadArtwork(url) { function loadArtwork(url) {
if (!url || url === "") { if (!url || url === "") {
// Keep stale art; only blank once the empty url debounce settles. // Keep stale art; only blank once the empty url debounce settles.
@@ -81,7 +88,7 @@ Singleton {
return; return;
if (exitCode === 0) { if (exitCode === 0) {
resolvedArtUrl = localFileUrl; _commit(localFileUrl);
loading = false; loading = false;
} 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; }";
@@ -98,18 +105,14 @@ Singleton {
return; return;
if (maxExitCode === 0) { if (maxExitCode === 0) {
resolvedArtUrl = localFileUrl; _commit(localFileUrl);
loading = false; loading = false;
} else { } else {
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => { Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => {
if (_lastArtUrl !== targetUrl) if (_lastArtUrl !== targetUrl)
return; return;
if (mqExitCode === 0) { _commit(mqExitCode === 0 ? localFileUrl : targetUrl);
resolvedArtUrl = localFileUrl;
} else {
resolvedArtUrl = targetUrl; // Ultimate fallback
}
loading = false; loading = false;
}, 50, 15000); }, 50, 15000);
} }
@@ -121,11 +124,7 @@ Singleton {
if (_lastArtUrl !== targetUrl) if (_lastArtUrl !== targetUrl)
return; return;
if (dlExitCode === 0) { _commit(dlExitCode === 0 ? localFileUrl : targetUrl);
resolvedArtUrl = localFileUrl;
} else {
resolvedArtUrl = targetUrl; // Fallback to raw URL
}
loading = false; loading = false;
}, 50, 15000); }, 50, 15000);
} }
@@ -137,11 +136,22 @@ Singleton {
loading = true; loading = true;
const localUrl = url; const localUrl = url;
const filePath = url.startsWith("file://") ? url.substring(7) : url; const filePath = url.startsWith("file://") ? url.substring(7) : url;
// Cover file often lands after the metadata update, so poll briefly. // Cover lands after metadata, so poll; hash only to reject placeholder art.
Proc.runCommand(null, ["sh", "-c", "for i in $(seq 20); do [ -f \"$1\" ] && exit 0; sleep 0.15; done; exit 1", "sh", filePath], (output, exitCode) => { const script = "f=\"$1\"; for i in $(seq 20); do [ -f \"$f\" ] && break; sleep 0.15; done; [ -f \"$f\" ] || exit 1; sha1sum \"$f\" | cut -c1-40";
Proc.runCommand(null, ["sh", "-c", script, "sh", filePath], (output, exitCode) => {
if (_lastArtUrl !== localUrl) if (_lastArtUrl !== localUrl)
return; return;
resolvedArtUrl = exitCode === 0 ? localUrl : ""; if (exitCode !== 0) {
// Keep current art rather than blanking (avoids an accent/art flash).
loading = false;
return;
}
// Placeholder (Chrome logo): skip without committing so the real cover still resolves.
if (_artHashDenylist.indexOf((output || "").trim()) !== -1) {
loading = false;
return;
}
_commit(localUrl);
loading = false; loading = false;
}, 50, 5000); }, 50, 5000);
} }
@@ -151,13 +161,14 @@ Singleton {
interval: 800 interval: 800
onTriggered: { onTriggered: {
if (root._lastArtUrl === "") if (root._lastArtUrl === "")
root.resolvedArtUrl = ""; root._commit("");
} }
} }
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
property string _resolvedTrackKey: "" property string _committedArtKey: ""
property string _pendingArtKey: ""
onActivePlayerChanged: _updateArtUrl() onActivePlayerChanged: _updateArtUrl()
@@ -173,17 +184,21 @@ Singleton {
const p = activePlayer; const p = activePlayer;
if (!p) if (!p)
return ""; return "";
// Prefer the stable track id; title/artist/album fill in progressively (Chrome).
const tid = p.metadata && p.metadata["mpris:trackid"] ? p.metadata["mpris:trackid"].toString() : "";
if (tid !== "")
return tid;
return (p.trackTitle || "") + "" + (p.trackArtist || "") + "" + (p.trackAlbum || ""); return (p.trackTitle || "") + "" + (p.trackArtist || "") + "" + (p.trackAlbum || "");
} }
function _updateArtUrl() { function _updateArtUrl() {
const url = getArtworkUrl(activePlayer);
const key = _trackKey(); const key = _trackKey();
// Ignore metadata jitter once resolved, but only when the url still matches // Skip once real art is committed for this track (dedup Chrome's multi-size
// (trackArtUrl can update before the title, and skipping then wedges old art). // re-publish). The lock is set in _commit(), never optimistically, so a rejected
if (key !== "" && key === _resolvedTrackKey && url === _lastArtUrl && resolvedArtUrl !== "") // placeholder or a short-circuited duplicate url can't wedge the real cover out.
if (key !== "" && key === _committedArtKey)
return; return;
_resolvedTrackKey = key; _pendingArtKey = key;
loadArtwork(url); loadArtwork(getArtworkUrl(activePlayer));
} }
} }
+79 -10
View File
@@ -9,7 +9,22 @@ Item {
property MprisPlayer activePlayer property MprisPlayer activePlayer
property string artUrl: TrackArtService.resolvedArtUrl property string artUrl: TrackArtService.resolvedArtUrl
property string lastValidArtUrl: "" property string lastValidArtUrl: ""
property alias albumArtStatus: albumArt.imageStatus // Live mpris url — always valid for the current track; fallback so art is never blank.
readonly property string rawArtUrl: {
const p = activePlayer;
if (!p)
return "";
if (p.trackArtUrl)
return p.trackArtUrl;
const m = p.metadata;
return m && m["mpris:artUrl"] ? m["mpris:artUrl"].toString() : "";
}
readonly property string curArt: artUrl || lastValidArtUrl || rawArtUrl
property string _prevArt: ""
property bool _fadePending: false
property string _srcOverride: "" // forces the live url when the resolved one fails
readonly property string _mainSrc: _srcOverride !== "" ? _srcOverride : curArt
readonly property int albumArtStatus: mainArt.imageStatus
property real albumSize: Math.min(width, height) * 0.88 property real albumSize: Math.min(width, height) * 0.88
property bool showAnimation: true property bool showAnimation: true
property real animationScale: 1.0 property real animationScale: 1.0
@@ -48,10 +63,49 @@ Item {
lastValidArtUrl = ""; lastValidArtUrl = "";
} }
onArtUrlChanged: { onCurArtChanged: {
if (artUrl && albumArtStatus !== Image.Error) { _srcOverride = "";
lastValidArtUrl = artUrl; // Keep the outgoing art covering mainArt until the new art decodes, then fade —
// hides mainArt's placeholder base so no primary circle flashes mid-load.
if (_prevArt !== "" && _prevArt !== curArt) {
fadeArt.imageSource = _prevArt;
fadeArt.opacity = 1;
_fadePending = true;
fadeSafety.restart();
Qt.callLater(_maybeStartFade); // catch cached (synchronous) loads
} }
_prevArt = curArt;
}
function _maybeStartFade() {
if (!_fadePending)
return;
if (mainArt.imageStatus !== Image.Ready && mainArt.imageStatus !== Image.Error)
return;
_fadePending = false;
fadeSafety.stop();
fadeOut.restart();
}
Timer {
id: fadeSafety
interval: 1200
onTriggered: {
if (root._fadePending) {
root._fadePending = false;
fadeOut.restart();
}
}
}
NumberAnimation {
id: fadeOut
target: fadeArt
property: "opacity"
from: 1
to: 0
duration: 300
easing.type: Easing.InOutQuad
} }
function updateBands() { function updateBands() {
@@ -166,21 +220,36 @@ Item {
} }
DankCircularImage { DankCircularImage {
id: albumArt id: mainArt
width: albumSize width: albumSize
height: albumSize height: albumSize
anchors.centerIn: parent anchors.centerIn: parent
z: 1 z: 1
imageSource: root._mainSrc
imageSource: artUrl || lastValidArtUrl || ""
fallbackIcon: "album" fallbackIcon: "album"
border.color: MediaAccentService.accent border.color: MediaAccentService.accent
border.width: 2 border.width: 2
onImageSourceChanged: { onImageStatusChanged: {
if (imageSource && imageStatus !== Image.Error) { if (imageStatus === Image.Ready && imageSource !== "")
lastValidArtUrl = imageSource; root.lastValidArtUrl = imageSource;
else if (imageStatus === Image.Error && root._srcOverride === "" && root.rawArtUrl !== "" && root.rawArtUrl !== imageSource)
root._srcOverride = root.rawArtUrl; // resolved url dead → use live mpris url
root._maybeStartFade();
} }
} }
// Outgoing art, shown on top only while fading out over the new mainArt.
DankCircularImage {
id: fadeArt
width: albumSize
height: albumSize
anchors.centerIn: parent
z: 2
fallbackIcon: ""
border.color: MediaAccentService.accent
border.width: 2
opacity: 0
visible: opacity > 0
} }
} }