.dotfiles/.config/chromium/Default/Extensions/cimiefiiaegbelhefglklhhakcgmhkai/2.1_0/extension-mpris.js
2026-01-18 21:08:16 -07:00

410 lines
13 KiB
JavaScript

/*
Copyright (C) 2017-2019 Kai Uwe Broulik <kde@privat.broulik.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
let playerIds = [];
let supportedImageMimeTypes = [];
function currentPlayer() {
let playerId = playerIds[playerIds.length - 1];
if (!playerId) {
// Returning empty object instead of null so you can call player.id returning undefined instead of throwing
return {};
}
let segments = playerId.split("-");
return {
id: playerId,
tabId: parseInt(segments[0]),
frameId: parseInt(segments[1])
};
}
function playerIdFromSender(sender) {
return sender.tab.id + "-" + (sender.frameId || 0);
}
function playersOnTab(tabId) {
return playerIds.filter((playerId) => {
return playerId.startsWith(tabId + "-");
});
}
function sendPlayerTabMessage(player, action, payload) {
if (!player) {
return;
}
let message = {
subsystem: "mpris",
action: action
};
if (payload) {
message.payload = payload;
}
chrome.tabs.sendMessage(player.tabId, message, {
frameId: player.frameId
}, (resp) => {
const error = chrome.runtime.lastError;
// When player tab crashed, we get this error message.
// There's unfortunately no proper signal for this so we can really only know when we try to send a command
if (error && error.message === "Could not establish connection. Receiving end does not exist.") {
console.warn("Failed to send player command to tab", player.tabId, ", signalling player gone");
playerTabGone(player.tabId);
}
});
}
function playerTabGone(tabId) {
let players = playerIds;
players.forEach((playerId) => {
if (playerId.startsWith(tabId + "-")) {
playerGone(playerId);
}
});
}
function playerGone(playerId) {
let oldPlayer = currentPlayer();
var removedPlayerIdx = playerIds.indexOf(playerId);
if (removedPlayerIdx > -1) {
playerIds.splice(removedPlayerIdx, 1); // remove that player from the array
}
// If there is no more player on this tab, remove badge
const gonePlayerTabId = Number(playerId.split("-")[0]);
if (playersOnTab(gonePlayerTabId).length === 0) {
// Check whether that tab still exists before trying to clear the badge
chrome.tabs.get(gonePlayerTabId, (tab) => {
if (chrome.runtime.lastError /*silence error*/ || !tab) {
return;
}
chrome.action.setBadgeText({
text: "",
tabId: gonePlayerTabId // important to pass it as number!
});
try {
// Important to clear the color, too, so it reverts back to global badge setting
chrome.action.setBadgeBackgroundColor({
color: null,
tabId: gonePlayerTabId
});
} catch (e) {
// Silence warning about missing 'text' and 'color' in Chrome
}
});
}
let newPlayer = currentPlayer();
if (oldPlayer.id === newPlayer.id) {
return;
}
// all players gone :(
if (!newPlayer.id) {
sendPortMessage("mpris", "gone");
return;
}
// ask the now current player to identify to us
// we can't just pretend "playing" as the other player might be paused
sendPlayerTabMessage(newPlayer, "identify");
}
function hostSupportsFetchedArtwork() {
return supportedImageMimeTypes.length > 0;
}
function fetchPlayerArtwork(metadata, poster) {
let artworkUrl = "";
let artworkMimeType = "";
const player = currentPlayer();
if (!player.id) {
return artworkUrl;
}
if (metadata) {
const artwork = metadata.artwork || [];
// Basically MPrisPlugin::processMetadata.
let biggest = null;
for (let item of artwork) {
if (!item.src) {
continue;
}
if (item.type && !supportedImageMimeTypes.includes(item.type)) {
console.log("Not supported mime", item.type, "of", item.src);
continue;
}
if (item.sizes === "any") {
artworkUrl = item.src;
artworkMimeType = item.type;
break;
}
// "sizes" is a space-separated list of sizes, for some reason.
let sizes = (item.sizes || "").toLowerCase().split(" ");
for (let size of sizes) {
const sizeParts = size.split("x");
let actualSize = {width: NaN, height: NaN};
if (sizeParts.length == 2) {
actualSize.width = parseInt(sizeParts[0], 10);
actualSize.height = parseInt(sizeParts[1], 10);
}
if (biggest === null || (actualSize.width >= biggest.width && actualSize.height >= biggest.height)) {
artworkUrl = item.src;
artworkMimeType = item.type;
biggest = {width: actualSize.width, height: actualSize.height};
}
}
}
}
if (!artworkUrl) {
artworkUrl = poster || "";
}
let payload = {src: artworkUrl};
if (!artworkUrl) {
// Tell the browser that there's nothing more to wait for.
sendPortMessage("mpris", "artwork", payload);
return artworkUrl;
}
fetch(artworkUrl).then((response) => {
// Other player is current by now.
if (currentPlayer().id !== player.id) {
return;
}
if (!response.ok) {
sendPortMessage("mpris", "artwork", payload);
return;
}
response.blob().then((blob) => {
let reader = new FileReader();
reader.onloadend = function() {
if (currentPlayer().id === player.id) {
payload.dataUrl = reader.result;
payload.mimeType = artworkMimeType;
sendPortMessage("mpris", "artwork", payload);
}
}
reader.readAsDataURL(blob);
}, (err) => {
console.warn("Failed to read response of", artworkUrl, "as blob", err);
if (currentPlayer().id === player.id) {
sendPortMessage("mpris", "artwork", payload);
}
});
}, (err) => {
console.warn("Failed to get artwork from", artworkUrl, err);
if (currentPlayer().id === player.id) {
sendPortMessage("mpris", "artwork", payload);
}
});
return artworkUrl;
}
// when tab is closed, tell the player is gone
// below we also have a "gone" signal listener from the content script
// which is invoked in the pagehide handler of the page
chrome.tabs.onRemoved.addListener((tabId) => {
// Since we only get the tab id, search for all players from this tab and signal a "gone"
playerTabGone(tabId);
});
// There's no signal for when a tab process crashes (only in browser dev builds).
// We watch for the tab becoming inaudible and check if it's still around.
// With this heuristic we can at least mitigate MPRIS remaining stuck in a playing state.
chrome.tabs.onUpdated.addListener((tabId, changes) => {
if (!changes.hasOwnProperty("audible") || changes.audible === true) {
return;
}
// Now check if the tab is actually gone
chrome.scripting.executeScript({
target: {
tabId: tabId
},
func: () => {
return true;
}
}, (result) => {
const error = chrome.runtime.lastError;
if (error) {
// Chrome error in script_executor.cc "kRendererDestroyed"
if (error.message === "The tab was closed."
// chrome.scripting API with Manifest v3 gives this non-descript error.
|| error.message === "Cannot access contents of the page. Extension manifest must request permission to access the respective host.") {
console.warn("Player tab", tabId, "became inaudible and was considered crashed, signalling player gone");
playerTabGone(tabId);
}
}
});
});
// callbacks from host (Plasma) to our extension
addCallback("mpris", "supportedImageMimeTypes", (message) => {
supportedImageMimeTypes = message.mimeTypes;
});
addCallback("mpris", "raise", function (message) {
let player = currentPlayer();
if (player.tabId) {
raiseTab(player.tabId);
}
});
addCallback("mpris", ["play", "pause", "playPause", "stop", "next", "previous"], function (message, action) {
sendPlayerTabMessage(currentPlayer(), action);
});
addCallback("mpris", "setFullscreen", (message) => {
sendPlayerTabMessage(currentPlayer(), "setFullscreen", {
fullscreen: message.fullscreen
});
});
addCallback("mpris", "setVolume", function (message) {
sendPlayerTabMessage(currentPlayer(), "setVolume", {
volume: message.volume
});
});
addCallback("mpris", "setLoop", function (message) {
sendPlayerTabMessage(currentPlayer(), "setLoop", {
loop: message.loop
});
});
addCallback("mpris", "setPosition", function (message) {
sendPlayerTabMessage(currentPlayer(), "setPosition", {
position: message.position
});
})
addCallback("mpris", "setPlaybackRate", function (message) {
sendPlayerTabMessage(currentPlayer(), "setPlaybackRate", {
playbackRate: message.playbackRate
});
});
// callbacks from a browser tab to our extension
addRuntimeCallback("mpris", "playing", function (message, sender) {
// Before Firefox 67 it ran extensions in incognito mode by default.
// However, after the update the extension keeps running in incognito mode.
// So we keep disabling media controls for them to prevent accidental private
// information leak on lock screen or now playing auto status in a messenger
if (IS_FIREFOX && sender.tab.incognito) {
return;
}
let playerId = playerIdFromSender(sender);
let idx = playerIds.indexOf(playerId);
if (idx > -1) {
// Move it to the end of the list so it becomes current
playerIds.push(playerIds.splice(idx, 1)[0]);
} else {
playerIds.push(playerId);
}
var payload = message || {};
payload.tabTitle = sender.tab.title;
payload.url = sender.tab.url;
if (hostSupportsFetchedArtwork()) {
payload.pendingArtwork = fetchPlayerArtwork(payload.metadata, payload.poster);
}
sendPortMessage("mpris", "playing", payload);
// Add toolbar icon to make it obvious you now have controls to disable the player
chrome.action.setBadgeText({
text: "♪",
tabId: sender.tab.id
});
chrome.action.setBadgeBackgroundColor({
color: "#1d99f3", // Breeze "highlight" color
tabId: sender.tab.id
});
});
addRuntimeCallback("mpris", "gone", function (message, sender) {
playerGone(playerIdFromSender(sender));
});
addRuntimeCallback("mpris", "stopped", function (message, sender) {
// When player stopped, check if there's another one we could control now instead
let playerId = playerIdFromSender(sender);
if (currentPlayer().id === playerId) {
if (playerIds.length > 1) {
playerGone(playerId);
}
}
});
addRuntimeCallback("mpris", ["paused", "waiting", "canplay"], function (message, sender, action) {
if (currentPlayer().id === playerIdFromSender(sender)) {
sendPortMessage("mpris", action);
}
});
addRuntimeCallback("mpris", ["duration", "timeupdate", "seeking", "seeked", "ratechange", "volumechange", "titlechange", "fullscreenchange"], function (message, sender, action) {
if (currentPlayer().id === playerIdFromSender(sender)) {
sendPortMessage("mpris", action, message);
}
});
addRuntimeCallback("mpris", "metadata", function (message, sender) {
if (currentPlayer().id === playerIdFromSender(sender)) {
let payload = {
metadata: message
};
if (hostSupportsFetchedArtwork()) {
payload.pendingArtwork = fetchPlayerArtwork(payload, "");
}
sendPortMessage("mpris", "metadata", payload);
}
});
addRuntimeCallback("mpris", "callbacks", function (message, sender) {
if (currentPlayer().id === playerIdFromSender(sender)) {
sendPortMessage("mpris", "callbacks", {callbacks: message});
}
});
addRuntimeCallback("mpris", "hasTabPlayer", (message) => {
return Promise.resolve(playersOnTab(message.tabId));
});