Skip to content

Instantly share code, notes, and snippets.

@RobinDaugherty
Last active August 14, 2024 08:40
Show Gist options
  • Save RobinDaugherty/97c6b10f607952e175c0154dab4e39ff to your computer and use it in GitHub Desktop.
Save RobinDaugherty/97c6b10f607952e175c0154dab4e39ff to your computer and use it in GitHub Desktop.
Tamper script for NPO Start

NPO Start tamper script

Make it possible for someone to use the video player and subtitles to learn the language.

For TamperMonkey/Greasemonkey.

  • Make the player is the full width of the content area, moving the episode list is moved to the bottom of the page.
  • Add speed controls for intervals between 0.5 and 1.0.
  • Always enable Nederlands subtitles
  • Hide the shitty controls provided by default, show the standard OS-provided controls.
  • Make the subtitles interactive, so you can select it to look up words.
  • Make spacebar pause/unpase the video.

Enhancements I'd Like to MAke

  • Remember subtitle preference and keep it, rather than always enabling it. (Or the default NPO behavior of always disabling it.)
  • A way to open the default controls, such as to select a different subtitle language.
  • Remember playback speed preference and keep it.
// ==UserScript==
// @name NPO Series Player
// @namespace http://tampermonkey.net/
// @version 2024-07-16
// @description While watching a series, make the player full-width.
// @author You
// @match https://npo.nl/start/serie/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_addStyle
// ==/UserScript==
// TODO: when a new view starts on the existing page (automatically moving to the next episode),
// set up the video player again
(function() {
'use strict';
let hasStartedUp = false;
let showSidebar = false;
let desiredPlaybackRate = 1.0;
function uiIsPresent() {
return !!document.querySelector('[class^="PlayerSidebarContainer_PlayerSidebarContainer"],[class*=" PlayerSidebarContainer_PlayerSidebarContainer"]')
}
function startUI() {
// The element has a class name like "PlayerSidebarContainer_PlayerSidebarContainer__RANDOMSHIT"
const sidebar = document.querySelector('[class^="PlayerSidebarContainer_PlayerSidebarContainer"],[class*=" PlayerSidebarContainer_PlayerSidebarContainer"]');
if (!sidebar) { return; }
GM_addStyle(`
/* Don't show the shitty controls that NPO layers on top */
.bmpui-ui-controlbar, .bmpui-ui-container, .bmpui-ui-titlebar {
display: none !important;
}
.bmpui-ui-uicontainer .bmpui-ui-subtitle-overlay, .bmpui-ui-uicontainer .bmpui-ui-subtitle-overlay.bmpui-controlbar-visible {
/* Don't move the subtitles up when the video is paused */
bottom: 40px !important;
/* Don't disable interaction with the subtitle text. */
pointer-events: all !important;
}
.playback-controls {
display: flex;
justify-content: center;
gap: 3em;
}
/* Style the controls we're adding to the page. */
button.tm-set-playback-rate {
padding: 0.7em;
background-color: rgba(255,255,255,0.1);
color: white;
font-weight: bold;
border: 1px solid #ccc;
border-radius: 8px;
}
button.tm-set-playback-rate.selected {
color: rgb(255, 109, 0);
}
`)
// The player's container has no unique class at all
const playerContainer = sidebar.nextElementSibling;
const contentContainer = sidebar.parentElement.parentElement;
if (playerContainer) {
// Make the video full-width
playerContainer.className = "col-span-12";
}
if (contentContainer) {
// Move the sidebar content (list of episodes) to under the episode description.
contentContainer.appendChild(sidebar);
}
const playerInfo = playerContainer.querySelector('[data-testid="player-info"]');
if (playerInfo) {
const playbackControls = document.createElement('div');
playbackControls.className = 'playback-controls';
const playbackControlButtons = [
makeSkipButton(-2),
makeSkipButton(-5),
makeRateChangeButton(0.675),
makeRateChangeButton(0.75),
makeRateChangeButton(0.825),
makeRateChangeButton(0.9),
makeRateChangeButton(1.0),
];
playbackControls.append(...playbackControlButtons);
playerInfo.insertAdjacentElement('beforebegin', playbackControls);
}
// When space is pressed, pause/unpause the video instead of paging down.
// This is super necessary since the the subtitles can be interacted with, clicking the video will not play/pause.
document.addEventListener('keydown', function(e) {
if (e.code == "Space") {
e.preventDefault();
const video = document.querySelector('video');
if (video?.paused) {
video.play();
} else {
video.pause();
}
}
});
}
function makeRateChangeButton(rate) {
const element = document.createElement('button');
element.onclick = function() {
setVideoPlaybackRate(rate);
}
element.className = 'tm-set-playback-rate';
element.setAttribute('data-playback-rate', rate);
element.appendChild(document.createTextNode(`${rate} X`));
return element;
}
function setVideoPlaybackRate(rate) {
document.querySelector(`.tm-set-playback-rate.selected`)?.classList?.remove('selected');
document.querySelector(`.tm-set-playback-rate[data-playback-rate="${rate}"]`)?.classList?.add('selected');
document.querySelector('video').playbackRate = rate;
}
function makeSkipButton(distance) {
const element = document.createElement('button');
element.onclick = function() {
skipVideo(distance);
}
element.className = 'tm-skip-video';
element.setAttribute('data-skip-distance', distance);
element.appendChild(document.createTextNode(`Skip ${distance}`))
return element;
}
function skipVideo(distance) {
const video = document.querySelector('video');
video.currentTime = video.currentTime + distance
}
let startupTimer = window.setInterval(startup, 100);
function startup() {
if (!uiIsPresent()) {
return;
}
window.clearInterval(startupTimer);
startUI();
setUpVideoControls();
hasStartedUp = true;
}
function videoIsPresent() {
return !!document.querySelector('video');
}
let videoHasStarted = false;
let videoSetupTimer = window.setInterval(videoStartup, 100);
function videoStartup() {
if (!videoIsPresent()) {
return;
}
window.clearInterval(videoSetupTimer);
// Once the video appears, wait for it to start playing. That is done by the overlay control provided by NPO.
const video = document.querySelector('video');
video.addEventListener('play', setUpVideoControls);
video.addEventListener('pause', fixupSubtitles);
}
function setUpVideoControls() {
// Once the overlay control is used, hide the ugly, useless thing.
document.querySelector('.bmpui-ui-playbacktoggle-overlay').style.display = 'none';
// Show the standard video controls.
document.querySelector('video').setAttribute('controls', 'true');
// Turn on subtitles by clicking the control. NPO doesn't remember whether they were turned on.
document.querySelector('button[aria-label="Nederlands"]').click()
// To ensure the correct button is highlighted.
// TODO: remember the preference and keep it.
setVideoPlaybackRate(1.0);
const video = document.querySelector('video');
video.removeEventListener('play', setUpVideoControls);
}
function fixupSubtitles() {
const parent = document.querySelector('.bmpui-ui-subtitle-label');
parent.style.whiteSpace = 'preserve-breaks';
// Replace <br> tags with a span containing a linefeed character.
// This allows screen readers and translation tools to see the lines as part of the same sentence/clause.
const breaks = parent.querySelectorAll('br');
breaks.forEach((br) => {
const newlineSpan = document.createElement('span');
newlineSpan.textContent = "\x0a";
br.replaceWith(newlineSpan);
});
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment