// ==UserScript==
// @name Plex now playing badge
// @namespace V@no
// @description Display a badge on favicon with a number of users streaming from the server
// @include http://localhost:32400/web/*
// @include http://127.0.0.1:32400/web/*
// @include https://app.plex.tv/desktop
// @include https://app.plex.tv/desktop/*
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACVklEQVRYhc2Xy08TURTGZ6NAn7RMp1xo587ChVUTY9mCCfFPMFZicGlMNNGli7YoIsYBNDyCDyKmQVGpQMJC0aVuTKCkoFCbEhQoCNrUkBDWn4uWR8s0tHVu603O+vzuPed857scpRTFDO6/Bxi4VIWAhyjGpJvglENkC1B3UsSEWxkg4CHwX6liC0Apxc2ztowAU02VcNXZ2QIcPUIxmfYKU16CYLMVM3cEfG6yQpIYN2Grqzrl1tO3rfh6V8CcbMG3Dh5yI2ELIEkUn24QBLwE082J5KE2HuEHFYh0mRHpNMN5LPeGzGkMz9XaEbxViS+tAuaSyee7zfj+sByLT4x457awBaCUYvgawew9C8L3d5Mv9RkRfWbAqk+PC2eq2QI4HSJmZR6RTjMWek1YTCb/OaDD+qAW4UeGnBoyLyWUGwnme0z48bgc0f5E8l8vtYj5NYiPlKHrssAWQJIogjKPpT4jVnx6rA9qERvS4M9oKTbGShAfLUXNiey0Ie9d0FBvw/LT5O1faREfLsPGWAk23x7G1vtD+NhhYAtAKcW4h8facx1+v9bgA8flFf8EUHNcRNSnR2yo2AD+XYBsjyoA414eay90KS9QMICGehui/YZ9PbB9OI5TDFUAJIki2FaRMgUFBZAvEiz0mlJ04KASqAbgdIgItfM7SricVMKCAYxcJwi1798FmQBULYGr1o6ZFqviNmQOsG1IMvkB5iXYsWQZHBFTgHRTmu4JJ1oEtmOoZMv3uuLzp23sAA76mLy5mnDDzKQ4269ZUZbR3ijKOlYD4C/uwVlNS+Cv+wAAAABJRU5ErkJggg==
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @author V@no
// @version 2.2
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_log
// @grant GM_registerMenuCommand
// ==/UserScript==
var prefsDefault = {
position: 2, //0 = top-left, 1 = top-right, 2 = bottom-right, 3 = bottom-left
offsetX: 0, //move badge away from x egde (use negative numbers for auto scale based on text size)
offsetY: 0, //move badge away from y edge (use negative numbers for auto scale based on text size)
textSize: 0, //text size, 0 = auto, 1 = 5px, 2 = 10px, so on
textMargin: -1, //margin around text, use negative number for auto scale based on text size
textColor: "#000000", // text color
backgroundColor: "#FFFFFF", //background color
borderColor: "#B90000", //border color
borderWidth: -1, //border width, -1 = auto based on text size
borderRadius: 0, //border corners radius
sizeIcon: 16, //image size in pixels, 0 = original
};
var prefsConfig = {
position: ["Top-Left", "Top-Right", "Bottom-Right", "Bottom-Left"],
positionReal: ["0", "1", "2", "3"],
sizeIcon: ["Original", "16x16", "32x32"],
sizeIconReal: ["0", "16", "32"],
textSize: ["Auto", "1", "2", "3"],
textSizeReal: ["0", "1", "2", "3"],
borderWidth: ["Auto", "0", "1", "2", "3", "4", "5", "6", "7", "8"],
borderWidthReal: ["-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"],
textMargin: ["Auto x4", "Auto x3", "Auto x2", "Auto", " 0", " 1", " 2", " 3", " 4"],
textMarginReal: ["-4", "-3", "-2", "-1", " 0", " 1", " 2", " 3", " 4"],
get: function(id, val, real)
{
let pos = this[id].indexOf(val);
if (pos == -1)
{
if (this[id+"Real"])
pos = this[id+"Real"].indexOf(String(val));
if (pos == -1)
{
pos = prefs[id];
}
}
if (real && this[id+"Real"])
{
pos = this[id+"Real"][pos];
pos = fixType.do(id, pos);
}
else
pos = this[id][pos];
return pos;
}
};
function clone(orig)
{
let obj = {};
for (let n in orig)
obj[n] = orig[n];
return obj;
}
function prefsUpdate (temp)
{
for(let n in prefs)
{
let pref = prefs[n];
if (typeof(pref) == "object" || typeof(pref) == "function")
continue;
pref = temp && GM_config.isOpen ? GM_config.fields[n].toValue() : GM_config.get(n);
if (n in prefsConfig)
pref = prefsConfig.get(n, pref, true);
pref = fixType.do(prefs[n], pref);
if (n.indexOf("Color") != -1)
pref = pref.replace(/[^#a-zA-Z0-9\-]+/g, "");
prefs[n] = pref;
}
}
var prefs = clone(prefsDefault),
prefsClone = {},
fixType = {
string: String,
number: parseInt,
boolean: Boolean,
do: function(o, n)
{
if (typeof(o) in this)
n = this[typeof(o)](n);
return n;
}
},
link = null,
head = document.getElementsByTagName('head')[0],
prev = null,
img = new Image(),
canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
size = 16,
multi,
nav,
configBlur = document.createElement("div"),
title = "Plex Badge Configuration",
style = document.createElement("style"),
css = function(){/*
#configBlur
{
z-index: 9998;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: grey;
opacity: 0.6;
display: none;
}
body[config] #configBlur
{
display: block;
}
body[config] #plex > div
{
filter: blur(5px);
}
*/};
style.innerHTML = css.toString().slice(14,-3).split("*//*").join("*/");
head.appendChild(style);
configBlur.id = "configBlur";
document.body.appendChild(configBlur);
configBlur.addEventListener("click", function(){GM_config.close();}, false);
/*
if (typeof GM_getMetadata === 'function') // Scriptish
title = GM_getMetadata('name');
else if (typeof GM_info !== 'undefined') // Greasemonkey, Tampermonkey &c.
title = GM_info.script.name;
title += (title ? ' ' : '') + 'Configuration';
*/
GM_config.init({
id: "plexNowPlayingBadge",
title: title,
fields: // Fields object
{
position: {
type: "select",
label: "Position",
labelPos: "left",
options: prefsConfig.position,
default: prefsConfig.position[prefsDefault.position]
},
offsetX: {
type: "select",
label: "Offset X",
labelPos: "left",
options: Array.apply(null, {length: 17}).map(function(v,i){return String(i);}), //0-16
default: prefs.offsetX
},
offsetY: {
type: "select",
label: "Offset Y",
labelPos: "left",
options: Array.apply(null, {length: 17}).map(function(v,i){return String(i);}), //0-16
default: prefs.offsetY
},
textSize: {
type: "select",
label: "Text size",
labelPos: "left",
options: prefsConfig.textSize,
default: prefsConfig.textSize[prefsDefault.textSize]
},
textMargin: {
type: "select",
label: "Text margin",
labelPos: "left",
options: prefsConfig.textMargin,
default: prefsConfig.get("textMargin", prefsDefault.textMargin)
},
textColor: {
type: "text",
label: "Text color",
labelPos: "left",
default: prefs.textColor
},
backgroundColor: {
type: "text",
label: "Background color",
labelPos: "left",
default: prefs.backgroundColor
},
borderColor: {
type: "text",
label: "Border color",
labelPos: "left",
default: prefs.borderColor
},
borderWidth: {
type: "select",
labelPos: "left",
label: "Border width",
options: prefsConfig.borderWidth,
default: prefsConfig.get("borderWidth", prefsDefault.borderWidth)
},
borderRadius: {
type: "select",
label: "Border radius",
labelPos: "left",
options: ["0", "1", "2", "3", "4", "5"],
default: prefs.borderRadius
},
sizeIcon: {
type: "select",
label: "Icon size",
labelPos: "left",
options: prefsConfig.sizeIcon,
default: prefsConfig.get("sizeIcon", prefsDefault.sizeIcon)
}
},
events: {
open: function()
{
prefsClone = clone(prefs);
GM_config.frame.style.width = "24em";
GM_config.frame.style.height = GM_config.frame.contentDocument.defaultView.document.body.firstChild.clientHeight + 20 + "px";
GM_config.frame.style.border = "0";
GM_config.frame.style.boxShadow = "0px 0px 100px 10px #000";
GM_config.center();
document.body.setAttribute("config","");
let input = function(e)
{
prefsUpdate(true);
loop(true);
};
for(let f in GM_config.fields)
{
if (!(f in prefs))
continue;
let field = GM_config.fields[f];
field.node.addEventListener("input", input, true);
}
loop(null);
},
close: function()
{
document.body.removeAttribute("config");
prefs = clone(prefsClone);
prefsUpdate();
loop(false);
},
save: function()
{
prefsClone = clone(prefs);
prefsUpdate();
loop(true);
},
reset: function()
{
prefs = clone(prefsDefault);
prefsUpdate(true);
loop(true);
}
},
css: [
".config_header{margin-bottom:1em !important;}"+
".config_var{display:table-row;}"+
".config_var > label{display:table-cell;white-space:nowrap;text-align:end;width:0;}"+
".config_var > *{margin:3px 3px 3px 0.5em;width:90%}"
]
});
GM_registerMenuCommand(title, function () { GM_config.open(); });
function drawText(text)
{
if (!img.loaded)
return;
if (prefs.sizeIcon)
img.width = img.height = prefs.sizeIcon;
else
{
img.width = img._width;
img.height = img._height;
}
let size = img.height;
canvas.width = canvas.height = size;
ctx.save();
ctx.drawImage(img, 0, 0, size, size);
if (text)
{
let multi = prefs.textSize ? prefs.textSize : size / 16,
textArray = text.toString().toUpperCase().split(''),
textHeight = 0,
textWidth = textArray.reduce(function (prev, cur)
{
let px = getPixelMap(cur);
if (px.length * multi > textHeight)
textHeight = px.length * multi;
return prev + px[0].length * multi + 1;
}, -1),
borderWidth = prefs.borderWidth == -1 ? multi : prefs.borderWidth,
textMargin = prefs.textMargin < 0 ? -prefs.textMargin * multi : prefs.textMargin,
width = textWidth + textMargin * 2 + borderWidth,
height = textHeight + textMargin * 2 + borderWidth,
xy = -borderWidth / 2,
offsetX = prefs.offsetX < 0 ? -prefs.offsetX * multi : prefs.offsetX,
offsetY = prefs.offsetY < 0 ? -prefs.offsetY * multi : prefs.offsetY,
x, y;
switch (prefs.position)
{
case 0:
x = borderWidth + offsetX;
y = borderWidth + offsetY;
break;
case 1:
x = size - width - offsetX;
y = borderWidth + offsetY;
break;
default:
case 2:
x = size - width - offsetX;
y = size - height - offsetY;
break;
case 3:
x = borderWidth + offsetX;
y = size - height - offsetX;
break;
}
ctx.translate(x, y);
// Draw Box
ctx.fillStyle = prefs.backgroundColor;//backgborderRadius
ctx.strokeStyle = prefs.borderColor;//border
ctx.lineWidth = borderWidth;
ctx.borderRadiusRect(xy, xy, width, height, prefs.borderRadius * multi).fill();
if (borderWidth)
ctx.borderRadiusRect(xy, xy, width, height, prefs.borderRadius * multi).stroke();
// Draw Text
ctx.fillStyle = prefs.textColor;
ctx.translate(textMargin, textMargin);
for(let i = 0; i < textArray.length; i++)
{
let px = getPixelMap(textArray[i]),
_y = 0;
for (let y = 0; y < px.length; y++)
{
let _x = 0;
for (let x = 0; x < px[y].length; x++)
{
if (px[y] && px[y][x])
{
for(let mx = 0; mx < multi; mx++)
{
for(let my = 0; my < multi; my++)
{
ctx.fillRect(_x + mx, _y + my, 1, 1);
}
}
}
_x += multi;
}
_y += multi;
}
ctx.translate(px[0].length * multi + 1, 0);
}
}
let data = canvas.toDataURL("image/x-icon");
ctx.restore();
ctx.clearRect(0, 0, size, size);
return data;
}//drawText()
//borrowed from https://chrome.google.com/webstore/detail/favicon-badges/fjnaohmeicdkcipkhddeaibfhmbobbfm/related?hl=en-US
/**
* Gets a character's pixel map
*/
function getPixelMap(sym)
{
let px = PIXELMAPS[sym];
if (!px)
px = PIXELMAPS['0'];
return px;
}
var PIXELMAPS = {
'0': [
[1,1,1],
[1,0,1],
[1,0,1],
[1,0,1],
[1,1,1]
],
'1': [
[0,1,0],
[1,1,0],
[0,1,0],
[0,1,0],
[1,1,1]
],
'2': [
[1,1,1],
[0,0,1],
[1,1,1],
[1,0,0],
[1,1,1]
],
'3': [
[1,1,1],
[0,0,1],
[0,1,1],
[0,0,1],
[1,1,1]
],
'4': [
[1,0,1],
[1,0,1],
[1,0,1],
[1,1,1],
[0,0,1]
],
'5': [
[1,1,1],
[1,0,0],
[1,1,1],
[0,0,1],
[1,1,1]
],
'6': [
[1,1,1],
[1,0,0],
[1,1,1],
[1,0,1],
[1,1,1]
],
'7': [
[1,1,1],
[0,0,1],
[0,0,1],
[0,1,0],
[0,1,0]
],
'8': [
[1,1,1],
[1,0,1],
[1,1,1],
[1,0,1],
[1,1,1]
],
'9': [
[1,1,1],
[1,0,1],
[1,1,1],
[0,0,1],
[1,1,1]
],
};
//https://stackoverflow.com/a/7838871/2930038
CanvasRenderingContext2D.prototype.borderRadiusRect = function (x, y, w, h, r)
{
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
this.beginPath();
this.moveTo(x+r, y);
this.arcTo(x+w, y, x+w, y+h, r);
this.arcTo(x+w, y+h, x, y+h, r);
this.arcTo(x, y+h, x, y, r);
this.arcTo(x, y, x+w, y, r);
this.closePath();
return this;
};
function loop(conf)
{
let isConf = typeof(conf) != "undefined";
if (!isConf)
clearTimeout(loop.timer);
if (!link || link.parentNode !== head)
{
let links = document.getElementsByTagName("link");
for(let i = 0; i < links.length; i++)
{
if (links[i].getAttribute("rel") == "shortcut icon")
{
link = links[i];
break;
}
}
if (link && !img.loaded)
{
img.setAttribute('crossOrigin','anonymous');
img.src = link.href;
img.onload = function()
{
img._width = img.width;
img._height = img.height;
img.loaded = true;
};
}
}
let data,
_badge = document.getElementsByClassName("activity-badge badge badge-transparent"),
badge = _badge.length ? _badge[0].innerText : "",
text = parseInt(badge);
if(conf === true)
{
text = prev;
prev = null;
}
else if ((isConf || GM_config.isOpen) && conf !== false)
text = Math.floor(Math.random() * 99) + 1;
if (isNaN(text))
text = 0;
if (prev != text && (data = drawText(text)))
{
link.href = data;
prev = text;
}
if (!nav)
{
nav = document.getElementById("nav-dropdown");
if (nav)
{
let li = document.createElement("li"),
lis = nav.getElementsByTagName("li"),
a = document.createElement("a");
for(let i = lis.length - 1; i >= 0; i--)
{
let c = lis[i];
if (c.className == "divider")
{
a.innerText = "Plex Badge Config";
a.href = "#";
a.addEventListener("click", function(e)
{
e.preventDefault();
GM_config.open();
}, false);
li.appendChild(a);
c.parentNode.insertBefore(c.cloneNode(false), c);
c.parentNode.insertBefore(li, c);
break;
}
}
}
}
if (!isConf)
loop.timer = setTimeout(loop, 3000);
}
prefsUpdate();
loop();