Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/ii-base/services/MprisController.qml
Fimeg 8f42fc953d publish: the public projection begins here
This is a projection, not a development branch. The tree above was constructed
from the internal source named below under a manifest that decides which paths
may leave, then scanned as a whole tree rather than as a series of patches, and
only then published.

Public history starts here because the history before it was not admissible,
and neither was the tree. What used to stand in this repository included a
rescue copy of another machine, a directory of phone handoffs, deployment
wired to one house, and a submodule pointing at a forge no stranger can reach.
None of that was ever the product. It stays in the private forge, which is
allowed to hold the whole working organism, and this is what was deliberately
sent out instead.

Three mechanisms produced this tree, in decreasing order of trust. A top-level
path the manifest does not name never arrives at all, which is the one that
catches directories nobody has thought of yet. Named internal files inside
admitted roots are dropped. A short, reviewed table replaces deployment
defaults that a public build must not carry -- an endpoint aimed at one LAN, a
VPN profile belonging to one phone, packaging built from one checkout path.

Everything after this commit is an ordinary publication with the same three
trailers, so a force push stops being routine and starts meaning that
something deliberate happened. The trailers bind the projection to its source
without pretending the public SHA is the private one: same lineage, different
tree, and the record says so.

Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047
Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27
Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
2026-09-04 15:55:48 -04:00

282 lines
9.8 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
// From https://git.outfoxxed.me/outfoxxed/nixnew
// It does not have a license, but the author is okay with redistribution.
import QtQml.Models
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
import qs.modules.common
/**
* A service that provides easy access to the active Mpris player.
*/
Singleton {
id: root;
property list<MprisPlayer> players: Mpris.players.values.filter(player => isRealPlayer(player));
property MprisPlayer trackedPlayer: null;
property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null;
signal trackChanged(reverse: bool);
property bool __reverse: false;
property var activeTrack;
readonly property bool hasActivePlasmaIntegration: Mpris.players.values.some(
p => p.dbusName?.startsWith('org.mpris.MediaPlayer2.plasma-browser-integration')
)
function isRealPlayer(player) {
if (!Config.options.media.filterDuplicatePlayers) {
return true;
}
return (
// Remove native browser buses only if plasma-browser-integration is actually active on D-Bus
!(hasActivePlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && !(hasActivePlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
// playerctld just copies other buses and we don't need duplicates
!player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') &&
// Non-instance mpd bus
!(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd')));
}
// Last non-empty trackArtUrl seen per player (keyed by dbusName). Some
// MPRIS players (Firefox via firefox-mpris, Spotify) emit transient empty
// trackArtUrl between/during tracks. Consumers like PlayerControl that get
// recreated on panel open/close need a stable fallback so the cover
// doesn't vanish whenever they happen to mount during an empty window.
// dbusName is the right key here: MprisPlayer.uniqueId in Quickshell is a
// per-track identifier (it increments when the track changes), not a
// per-player one, so keying on it cross-contaminates between players that
// happen to share a uniqueId value.
property var stableArtUrlByPlayer: ({})
function _captureArtUrl(player) {
if (player?.trackArtUrl?.length > 0 && !!player?.dbusName) {
const map = Object.assign({}, root.stableArtUrlByPlayer);
map[player.dbusName] = player.trackArtUrl;
root.stableArtUrlByPlayer = map;
}
}
// Best (largest) downloaded cover file seen per player for the current
// track, keyed by dbusName. Persists across PlayerControl lifecycles so
// reopening the panel doesn't downgrade to a thumbnail when Firefox
// happens to be sitting on its low-res variant. Entry shape:
// { trackKey: string, artFilePath: string, artBytes: number }
property var bestArtByPlayer: ({})
function rememberBestArt(player, trackKey, artFilePath, artBytes) {
const id = player?.dbusName;
if (!id || artBytes <= 0 || !artFilePath) return;
const existing = root.bestArtByPlayer[id];
// Same track and existing is already >= new size: nothing to do.
if (existing && existing.trackKey === trackKey && existing.artBytes >= artBytes) return;
const map = Object.assign({}, root.bestArtByPlayer);
map[id] = { trackKey: trackKey, artFilePath: artFilePath, artBytes: artBytes };
root.bestArtByPlayer = map;
}
function getBestArt(player, trackKey) {
const id = player?.dbusName;
if (!id) return null;
const entry = root.bestArtByPlayer[id];
if (!entry || entry.trackKey !== trackKey) return null;
return entry;
}
function _trackKeyOf(player) {
// title|artist (album omitted on purpose). Some players (Firefox via
// firefox-mpris) emit metadata progressively: first trackArtUrl +
// title + artist with album="", then a moment later update album.
// If album were part of the key, the high-res variant emitted with
// the partial metadata and the low-res variant emitted with the
// completed metadata would look like different tracks to the
// "never-downgrade" guard.
return `${player?.trackTitle ?? ""}|${player?.trackArtist ?? ""}`;
}
// Per-player worker. Holds a Process that downloads each new trackArtUrl
// the player emits, regardless of whether the media controls panel is
// open. This is what makes the cover stay sharp across panel
// close/reopen and across auto-advance while the panel is closed —
// PlayerControl is no longer the only thing watching for art emissions.
component PlayerWorker: QtObject {
id: worker
required property MprisPlayer player
function _fetchArt() {
const url = worker.player?.trackArtUrl;
if (!url || url.length === 0) return;
artDownloader.trackKey = root._trackKeyOf(worker.player);
artDownloader.targetFile = url;
artDownloader.artFilePath = `${Directories.coverArt}/${Qt.md5(url)}`;
artDownloader.running = true;
}
property Process artDownloader: Process {
property string trackKey
property string targetFile
property string artFilePath
property int sizeBytes: 0
stdout: SplitParser {
onRead: data => {
const n = parseInt(data.trim());
if (!isNaN(n)) artDownloader.sizeBytes = n;
}
}
command: ["bash", "-c", `[ -f ${artFilePath} ] || curl -4 -sSL '${targetFile}' -o '${artFilePath}'; stat -c %s '${artFilePath}' 2>/dev/null`]
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0 || sizeBytes <= 0 || artFilePath.length === 0) return;
root.rememberBestArt(worker.player, trackKey, artFilePath, sizeBytes);
}
}
property Connections _conn: Connections {
target: worker.player
function onPlaybackStateChanged() {
if (root.trackedPlayer !== worker.player) root.trackedPlayer = worker.player;
}
function onTrackArtUrlChanged() {
root._captureArtUrl(worker.player);
worker._fetchArt();
}
}
Component.onCompleted: {
if (root.trackedPlayer == null || worker.player.isPlaying) {
root.trackedPlayer = worker.player;
}
root._captureArtUrl(worker.player);
worker._fetchArt();
}
Component.onDestruction: {
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
for (const p of Mpris.players.values) {
if (p.playbackState.isPlaying) {
root.trackedPlayer = p;
break;
}
}
if (root.trackedPlayer == null && Mpris.players.values.length != 0) {
root.trackedPlayer = Mpris.players.values[0];
}
}
}
}
Instantiator {
model: Mpris.players
delegate: PlayerWorker {
required property MprisPlayer modelData
player: modelData
}
}
Connections {
target: activePlayer
function onPostTrackChanged() {
root.updateTrack();
}
function onTrackArtUrlChanged() {
// console.log("arturl:", activePlayer.trackArtUrl)
// root.updateTrack();
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
// cantata likes to send cover updates *BEFORE* updating the track info.
// as such, art url changes shouldn't be able to break the reverse animation
const r = root.__reverse;
root.updateTrack();
root.__reverse = r;
}
}
}
onActivePlayerChanged: this.updateTrack();
function updateTrack() {
//console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
this.activeTrack = {
uniqueId: this.activePlayer?.uniqueId ?? 0,
artUrl: this.activePlayer?.trackArtUrl ?? "",
title: this.activePlayer?.trackTitle || Translation.tr("Unknown Title"),
artist: this.activePlayer?.trackArtist || Translation.tr("Unknown Artist"),
album: this.activePlayer?.trackAlbum || Translation.tr("Unknown Album"),
};
this.trackChanged(__reverse);
this.__reverse = false;
}
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying;
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false;
function togglePlaying() {
if (this.canTogglePlaying) this.activePlayer.togglePlaying();
}
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false;
function previous() {
if (this.canGoPrevious) {
this.__reverse = true;
this.activePlayer.previous();
}
}
property bool canGoNext: this.activePlayer?.canGoNext ?? false;
function next() {
if (this.canGoNext) {
this.__reverse = false;
this.activePlayer.next();
}
}
property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl;
property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl;
property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None;
function setLoopState(loopState: var) {
if (this.loopSupported) {
this.activePlayer.loopState = loopState;
}
}
property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl;
property bool hasShuffle: this.activePlayer?.shuffle ?? false;
function setShuffle(shuffle: bool) {
if (this.shuffleSupported) {
this.activePlayer.shuffle = shuffle;
}
}
function setActivePlayer(player: MprisPlayer) {
const targetPlayer = player ?? Mpris.players[0];
console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`)
if (targetPlayer && this.activePlayer) {
this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer);
} else {
// always animate forward if going to null
this.__reverse = false;
}
this.trackedPlayer = targetPlayer;
}
IpcHandler {
target: "mpris"
function pauseAll(): void {
for (const player of Mpris.players.values) {
if (player.canPause) player.pause();
}
}
function playPause(): void { root.togglePlaying(); }
function previous(): void { root.previous(); }
function next(): void { root.next(); }
}
}