Skip to content

Instantly share code, notes, and snippets.

@eramdam
Last active May 3, 2020 23:37
Show Gist options
  • Save eramdam/bafd97f73865ab9584a6aeebb39cb83d to your computer and use it in GitHub Desktop.
Save eramdam/bafd97f73865ab9584a6aeebb39cb83d to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Screenshot Plex
// @namespace Violentmonkey Scripts
// @match *://192.168.86.250:32400/*/*
// @match *://app.plex.tv/*
// @grant none
// @version 1.0
// @author @Eramdam
// @description 5/3/2020, 1:03:26 PM
// @require https://unpkg.com/[email protected]/dist/FileSaver.min.js
// @require https://html2canvas.hertzen.com/dist/html2canvas.min.js
// ==/UserScript==
// Taken from https://github.com/mgcrea/js-canvas-object-fit/
const EXIF_ORIENTATIONS = [
{ op: 'none', radians: 0 },
{ op: 'none', radians: 0 },
{ op: 'flip-x', radians: 0 },
{ op: 'none', radians: Math.PI },
{ op: 'flip-y', radians: 0 },
{ op: 'flip-x', radians: Math.PI / 2 },
{ op: 'none', radians: Math.PI / 2 },
{ op: 'flip-x', radians: -Math.PI / 2 },
{ op: 'none', radians: -Math.PI / 2 },
];
const drawImage = (
ctx,
image,
x,
y,
width,
height,
{
objectFit = 'cover',
orientation = 0,
offsetX = 1 / 2,
offsetY = 1 / 2,
} = {}
) => {
// Orientation value
const rotation = EXIF_ORIENTATIONS[orientation].radians;
const isHalfRotated = rotation !== 0 && rotation % Math.PI === 0;
const isQuarterRotated =
rotation !== 0 && !isHalfRotated && rotation % (Math.PI / 2) === 0;
const isRotatedClockwise = rotation / (Math.PI / 2) < 0; // @NOTE handle 2*PI rotation?
// Size values
const imageWidth = !isQuarterRotated ? image.width : image.height;
const imageHeight = !isQuarterRotated ? image.height : image.width;
// Resize values
const resizeRatio = Math[objectFit === 'cover' ? 'max' : 'min'](
width / imageWidth,
height / imageHeight
);
const resizeWidth = !isQuarterRotated
? imageWidth * resizeRatio
: imageHeight * resizeRatio;
const resizeHeight = !isQuarterRotated
? imageHeight * resizeRatio
: imageWidth * resizeRatio;
// Cropping values
const sWidth = !isQuarterRotated
? imageWidth / (resizeWidth / width)
: imageHeight / (resizeWidth / height);
const sHeight = !isQuarterRotated
? imageHeight / (resizeHeight / height)
: imageWidth / (resizeHeight / width);
const sX = (image.width - sWidth) * offsetX;
const sY = (image.height - sHeight) * offsetY;
// Positionning values
let tX = 0;
let tY = 0;
if (isHalfRotated) {
tX = -width - x;
tY = -height - y;
} else if (isQuarterRotated) {
tX = !isRotatedClockwise ? x : -height - x;
tY = isRotatedClockwise ? y : -width - y;
}
const tWidth = !isQuarterRotated ? width : height;
const tHeight = !isQuarterRotated ? height : width;
// Draw image
if (rotation) {
ctx.rotate(rotation);
}
if (objectFit !== 'none') {
ctx.drawImage(image, sX, sY, sWidth, sHeight, tX, tY, tWidth, tHeight);
} else {
ctx.drawImage(image, tX, tY, tWidth, tHeight);
}
if (rotation) {
ctx.rotate(-rotation);
}
};
const svgPhotoIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560" id="plex-icon-sidebar-photos-560" aria-hidden="true" class="PlexIcon-plexIcon-8Tamaj"><path d="M440 120h100c11.038 0 20 8.962 20 20v360c0 11.038-8.962 20-20 20H20c-11.038 0-20-8.962-20-20V140c0-11.038 8.962-20 20-20h100V60c0-11.038 8.962-20 20-20h280c11.038 0 20 8.962 20 20v60zm-160 40c-88.306 0-160 71.694-160 160s71.694 160 160 160 160-71.694 160-160-71.694-160-160-160zm0 80c44.153 0 80 35.847 80 80s-35.847 80-80 80-80-35.847-80-80 35.847-80 80-80z"></path></svg>`;
const log = (...args) => console.log('[SCREENSHOT-PLEX]', ...args);
async function takeScreenshot() {
log('gonna create imageBitmap');
const data = await createImageBitmap(document.querySelector('video'));
const c = document.createElement('canvas');
c.width = data.width;
c.height = data.height;
const ctx = c.getContext('2d');
log('Got video data', data);
ctx.drawImage(data, 0, 0, data.width, data.height);
const hasSubs = document.querySelectorAll('.libjass-subs').length > 0;
// const hasSubs = false;
if (html2canvas && hasSubs) {
try {
log('grabbing subs');
const subNode = document.querySelector('.libjass-subs');
const subCanvas = await html2canvas(subNode, {
backgroundColor: null,
foreignObjectRendering: true,
ignoreElements: (element) => {
const shouldIgnore =
!element.closest('.libjass-wrapper') &&
!element.contains(subNode);
log('maybe ignore?', {
element,
shouldIgnore,
});
return shouldIgnore;
},
});
log('Got sub data', subCanvas);
drawImage(ctx, subCanvas, 0, 0, c.width, c.height, {
objectFit: 'contain',
});
} catch (e) {
console.error(e);
}
}
log('Drawn video image data');
c.toBlob((blob) => {
log('Gonna donlowd');
saveAs(blob, `screencapture-${document.title}-${Date.now()}.png`);
window.stop();
});
}
(() => {
const observer = new MutationObserver(() => {
const playerUIRoot = document.querySelector(
'[class^="AudioVideoPlayerView-container"]'
);
if (!playerUIRoot) {
return;
}
if (
!playerUIRoot.querySelector(
'[class^="PlayerControls-buttonGroupCenter-"]'
) ||
playerUIRoot.querySelector('.screenshotButton')
) {
return;
}
const parent = playerUIRoot.querySelector(
'[class^="PlayerControls-buttonGroupCenter-"]'
);
const newButton = document.createElement('button');
newButton.className =
'PlayerIconButton-playerButton-1DmNp4 IconButton-button-9An-7I Link-link-2n0yJn Link-default-2XA2bN';
newButton.classList.add('screenshotButton');
newButton.setAttribute('aria-label', 'Take a screenshot');
newButton.setAttribute('title', 'Take a screenshot');
newButton.setAttribute('role', 'button');
newButton.setAttribute('type', 'button');
newButton.innerHTML = svgPhotoIcon;
newButton.onclick = () => {
log('Gonna take screenshot');
takeScreenshot();
};
parent.insertAdjacentElement('beforeend', newButton);
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
document.body.addEventListener(
'keydown',
(event) => {
if (event.key === 'F12') {
event.preventDefault();
event.stopPropagation();
takeScreenshot();
}
},
true
);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment