quickshell: fall back to the page favicon when no artwork

This commit is contained in:
rope 2026-07-30 15:56:03 +01:00
parent 02ebdc36ce
commit 88ab7944f4

View file

@ -182,6 +182,50 @@ in
${pkgs.curl}/bin/curl -sf --max-time 10 "https://api.open-meteo.com/v1/forecast?latitude=$lat&longitude=$lon&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset&timezone=auto&forecast_days=7"
'';
# Last-resort artwork: when a page publishes no MediaSession artwork
# (Jellyfin's web player, a bare <video> tag), fall back to its
# favicon. Filenames are often content-hashed, so /favicon.ico can't
# be guessed — the page's <link> tags have to be read. Prefers
# apple-touch-icon (180px) over favicon.ico (16px). Prints nothing on
# any failure, which the QML treats as "no icon".
faviconScript = pkgs.writeShellScript "favicon-fetch" ''
url="$1"
[ -z "$url" ] && exit 0
case "$url" in http://*|https://*) ;; *) exit 0 ;; esac
page=''${url%%#*} # drop SPA fragment (#/video)
proto=''${page%%://*}
rest=''${page#*://}
host=''${rest%%/*}
origin="$proto://$host"
dir=''${page%/*}/
html=$(${pkgs.curl}/bin/curl -sfL --max-time 5 "$page" 2>/dev/null)
[ -z "$html" ] && html=$(${pkgs.curl}/bin/curl -sfL --max-time 5 "$origin/" 2>/dev/null)
[ -z "$html" ] && exit 0
# Minified HTML is one long line, so pull each <link> out with
# grep -o rather than trying to split on newlines.
pick() {
printf '%s' "$html" \
| grep -oiE '<link[^>]*>' \
| grep -iE "rel=[\"']?$1" \
| grep -oiE "href=[\"'][^\"']+[\"']" \
| head -1 | sed -E "s/^href=[\"']//; s/[\"']\$//"
}
href=$(pick "apple-touch-icon")
[ -z "$href" ] && href=$(pick "(shortcut +)?icon[\"' ]")
[ -z "$href" ] && exit 0
case "$href" in
http://*|https://*) printf '%s' "$href" ;;
//*) printf '%s:%s' "$proto" "$href" ;;
/*) printf '%s%s' "$origin" "$href" ;;
*) printf '%s%s' "$dir" "$href" ;;
esac
'';
# cava in raw mode: one newline-terminated frame per tick, bars as
# semicolon-separated 0-100 ints — trivially parsed by SplitParser.
# Drives the media card's spectrum ring. Only spawned while the
@ -499,6 +543,7 @@ in
// Recoloured to the theme accent at draw time (ColorOverlay),
// so the stock blue/grey snowflake artwork is fine here.
readonly property string nixLogo: "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"
readonly property string faviconFetch: "${faviconScript}"
readonly property string cava: "${pkgs.cava}/bin/cava"
readonly property string cavaConfig: "${cavaConfig}"
}
@ -4022,6 +4067,33 @@ in
id: mediaCard
required property var modelData
property var pwNode: calPopup.streamFor(modelData)
// xesam:url is the only field that says where
// the audio actually comes from; identity just
// names the browser.
readonly property string pageUrl: {
let md = modelData.metadata || ({});
return md["xesam:url"] || "";
}
property string faviconUrl: ""
function refreshFavicon() {
faviconUrl = "";
// Rewriting a live command truncates the
// read, so skip while one is in flight.
if (pageUrl !== "" && !faviconProc.running)
faviconProc.running = true;
}
onPageUrlChanged: refreshFavicon()
Component.onCompleted: refreshFavicon()
Process {
id: faviconProc
command: [Commands.faviconFetch, mediaCard.pageUrl]
stdout: StdioCollector {
onStreamFinished: mediaCard.faviconUrl = text.trim()
}
}
width: parent.width
height: mediaCol.height + 16
radius: Theme.radiusCard
@ -4125,13 +4197,30 @@ in
}
Rectangle {
anchors.centerIn: parent
width: 34; height: 34
radius: 17
width: 40; height: 40
radius: 20
color: Theme.base0C
opacity: 0.25
}
// The site's own favicon, if it
// has one. Kept small and centred
// rather than filling the disc
// most are 16-32px and look awful
// upscaled to 86.
Image {
id: faviconImg
anchors.centerIn: parent
width: 28; height: 28
fillMode: Image.PreserveAspectFit
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
source: mediaCard.faviconUrl
}
SIcon {
anchors.centerIn: parent
visible: faviconImg.status !== Image.Ready
text: sourceBadge.glyph !== "" ? sourceBadge.glyph : "music"
color: Theme.base05
font.pixelSize: 18