Skip to content

Instantly share code, notes, and snippets.

@chriskempson
Last active October 11, 2018 10:21
Show Gist options
  • Save chriskempson/701d85f49be1b5cfb42737a09ad6a116 to your computer and use it in GitHub Desktop.
Save chriskempson/701d85f49be1b5cfb42737a09ad6a116 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Netflix Study Assist
// @author Chris Kempson (chriskempson.com)
// @version 1.0.0 (2018-08-23)
// @grant none
// @include *netflix.com*
// ==/UserScript==
// TODO: Stop space from being clobbered on multiline subtitles for languages that use spaces
// Settings
var ignoreList = ['♪〜', '〜♪', '♪~', '~♪'];
var displayLogWindowOnLoad = false;
var showDebugMessagesInLogWindow = false;
// Style subtitles and bring them to foreground so that a popup dictionary can select them
document.body.insertAdjacentHTML('beforeend', '<style>.player-timedtext-text-container { z-index: 1; } .player-timedtext-text-container span { font-size: 4em; font-weight: normal !important; text-shadow: none !important; -webkit-text-stroke: 0.125em black; paint-order: stroke fill; }</style>');
// Inject log window
document.body.insertAdjacentHTML('beforeend', '<style>#netflix-study-assist-log { display: none; background:rgba(50,50,50,0.75); border-radius: 5px; position: fixed; top: 0; right: 0; margin: 10px 10px 0 0; color: #eee; padding: 0 10px 8px 10px; z-index: 1; width: 250px; font-size: 10px; } #netflix-study-assist-log-inner { margin-top: 8px; height: 350px; overflow: auto; } #netflix-study-assist-log h1 { font-size: 12px } #netflix-study-assist-log hr { border: none; border-bottom: 1px solid #777; } #netflix-study-assist-log p { margin: 0; line-height: 14px; } #netflix-study-assist-log .light { color: #aaa; } #netflix-study-assist-log .red { color: #ee2222; } #netflix-study-assist-log .blue { color: #1166cc; } #netflix-study-assist-log .green { color: #227711; }</style><div id="netflix-study-assist-log"><h1>Netflix Study Assist Log</h1><hr /><div id="netflix-study-assist-log-inner"></div></div>');
// Get page elements
var logWindow = document.getElementById('netflix-study-assist-log');
var logWindowInner = document.getElementById('netflix-study-assist-log-inner');
// Variables
var netflixStudyAssistEnabled = false;
var logWindowHidden = true;
var player = null;
var textBasedSubtitles = null;
var subtitleObserver = null
var mouseClickHandler = null;
var keydownHandler = null;
var playerControls = null;
var backButton = null;
console.log('Netflix Study Assist: Loaded');
log('<span class="light">Ready...</span>');
// Display log window on load
if (displayLogWindowOnLoad) toggleLogWindow();
// Handle keypresses for log window
function loggerKeydownHandler(event)
{
switch (event.key) {
case 'l':
toggleLogWindow();
break;
case 'e':
enableNetflixStudyAssist();
break;
case 'd':
disableNetflixStudyAssist();
break;
}
}
document.addEventListener('keydown', loggerKeydownHandler, true);
// Check for URL changes. Wanted to use onpopstate but it wasn't working ;_;
var oldUrl = '';
function checkUrlChange(currentUrl)
{
// Detect navigational page change
if(currentUrl != oldUrl) {
log('<span class="light">URL change detected: ' + currentUrl + '</span>', 'debug');
oldUrl = currentUrl;
// Start things rolling if we're on the right page...
if (currentUrl.includes('watch')) {
log('<span class="light">URL string includes \'watch\', init() called</span>', 'debug');
init();
}
}
oldUrl = window.location.href;
setTimeout(function() {
checkUrlChange(window.location.href);
}, 1000);
}
checkUrlChange(window.location.href);
// Check if player has loaded first
function init()
{
log('<span class="light">Waiting for Netflix video player...<span>', 'debug');
// Remove previous handlers and disconnect previous observers
if (subtitleObserver) subtitleObserver.disconnect(textBasedSubtitles);
if (mouseClickHandler) document.removeEventListener('click', mouseClickHandler);
if (keydownHandler) document.removeEventListener('keydown', keydownHandler);
(new MutationObserver(check)).observe(document, {childList: true, subtree: true });
function check(changes, observer) {
if (document.getElementsByTagName('video')[0]) {
// MutationObserver no longer needed
observer.disconnect();
log('<span class="light">Netflix video player loaded, ready.<span>', 'debug');
// Get page elements
player = document.getElementsByTagName('video')[0];
textBasedSubtitles = document.getElementsByClassName('player-timedtext')[0];
playerControls = document.getElementsByClassName('PlayerControls--bottom-controls')[0];
backButton = document.getElementsByClassName('button-bvuiExit')[0];
// Variables
var lastTextBasedSubtitlesInnerHTML = '';
var nextTextBasedSubtitlesInnerHTML = '';
var pausedBySubtitleChange = false;
var skipNextPause = false;
var waitForPlayStatus = false;
// Pause on subtitle changes
//
// Netflix updates subtitles even when their player is paused, I've no idea why
// they do this. The code below takes this into account and resets
// Netflix's changes.
var MutationObserverCallback = function(mutations) {
// First pause condition
//
// Pause if text based subtitles have been cleared
// The first condition should be all we need but for some reason this observer
// is triggered multiple times even when the text is ''. The second condition
// tries to detect this.
if (textBasedSubtitles.innerHTML == '' && lastTextBasedSubtitlesInnerHTML != '' && !ignoreList.includes(strip(lastTextBasedSubtitlesInnerHTML))) {
if (showDebugMessagesInLogWindow) { log('<span class="red">Pause Condition 1 Triggered:</span><br />' + strip(lastTextBasedSubtitlesInnerHTML), 'debug'); }
else { log(strip(lastTextBasedSubtitlesInnerHTML)); }
if (netflixStudyAssistEnabled == true && skipNextPause == false) {
player.pause();
pausedBySubtitleChange = true;
// Replace text with last text and prevent subtitles from being hidden
textBasedSubtitles.innerHTML = lastTextBasedSubtitlesInnerHTML;
textBasedSubtitles.style.display = 'inherit';
log('<span class="light">Subtitles updated for Pause Condition 1.</span>');
}
// Reset so next pause is not skipped
skipNextPause = false;
}
// Overwrite Netflix's subtitle updates when we are dealing pause condition 2
if (nextTextBasedSubtitlesInnerHTML && textBasedSubtitles.innerHTML != lastTextBasedSubtitlesInnerHTML) {
textBasedSubtitles.innerHTML = lastTextBasedSubtitlesInnerHTML;
log('<span class="light">Subtitles overwritten for pause condition 2.</span>', 'debug');
}
// Second pause condition
if (strip(textBasedSubtitles.innerHTML) != strip(lastTextBasedSubtitlesInnerHTML) && textBasedSubtitles.innerHTML != '' && lastTextBasedSubtitlesInnerHTML != '' && nextTextBasedSubtitlesInnerHTML == '' && waitForPlayStatus == false) {
log('<span class="green">Pause Condition 2 (Current Subtitle) Triggered:</span><br />' + strip(textBasedSubtitles.innerHTML), 'debug');
if (showDebugMessagesInLogWindow) { log('<span class="red">Pause Condition 2 (Last Subtitle) Triggered:</span><br />' + strip(lastTextBasedSubtitlesInnerHTML), 'debug'); }
else { log(strip(lastTextBasedSubtitlesInnerHTML)); }
if (netflixStudyAssistEnabled == true && skipNextPause == false) {
player.pause();
// TODO: Not needed?
// Prevent the second pause condition from being triggered whilst the player is paused
// waitForPlayStatus = true;
// Replace text with last text and store next text for display by togglePlayerStatus()
nextTextBasedSubtitlesInnerHTML = textBasedSubtitles.innerHTML;
textBasedSubtitles.innerHTML = lastTextBasedSubtitlesInnerHTML;
log('<span class="light">Subtitles updated for Pause Condition 2.</span>');
}
// Reset so next pause is not skipped
skipNextPause = false;
}
// Store current subtitles for later
lastTextBasedSubtitlesInnerHTML = textBasedSubtitles.innerHTML;
};
subtitleObserver = new MutationObserver(MutationObserverCallback);
subtitleObserver.observe(textBasedSubtitles, { childList: true, subtree: true });
// Player play/pause toggle
var togglePlayerStatus = function() {
log('<span class="light">togglePlayerStatus() called</span>', 'debug');
if (player.paused) {
player.play();
log('<span class="light">Resume playback</span>', 'debug');
// No longer need to halt the checking for pause triggers
waitForPlayStatus = false;
// This condition is met when the player has been paused automatically.
// On resuming play we no longer need the subtitles to remain on the screen.
if (textBasedSubtitles.innerHTML == lastTextBasedSubtitlesInnerHTML && pausedBySubtitleChange == true) {
log('<span class="blue">Resume Condition 1 Triggered</span>', 'debug');
// Blank subtitles
textBasedSubtitles.innerHTML = '';
// Skipe next pause condition to avoid pause being triggered by the line above
skipNextPause = true;
}
// Display next stubtitles on play. This is to meet the second pause condition whereby a subtitle
// changes to the next subtitle without clearing to blank.
if (nextTextBasedSubtitlesInnerHTML) {
log('<span class="blue">Resume Condition 2 Triggered</span>', 'debug');
// Replace subtitles with next set of subtiles after playback resumes, by this point
// subtiles will have been set to previous subtitles
textBasedSubtitles.innerHTML = nextTextBasedSubtitlesInnerHTML;
nextTextBasedSubtitlesInnerHTML = '';
// Skipe next pause condition to avoid pause being triggered by the line above
skipNextPause = true;
}
} else {
player.pause();
log('<span class="light">Pause playback</span>', 'debug');
}
pausedBySubtitleChange = false;
};
// Mouse click toggles play/pause
mouseClickHandler = function() {
togglePlayerStatus();
};
document.addEventListener('click', mouseClickHandler, true);
// Handle keypresses
keydownHandler = function(event) {
switch (event.key) {
// Start/stop player with spacebar even when controls are not showing
case ' ':
if (netflixStudyAssistEnabled == true) {
event.stopPropagation();
togglePlayerStatus();
}
break;
}
};
document.addEventListener('keydown', keydownHandler, true);
}
}
}
// Enable Netflix Study Assist
function enableNetflixStudyAssist()
{
if (netflixStudyAssistEnabled == false) {
// Hide navigation controls
playerControls.style.display = 'none';
backButton.style.display = 'none';
netflixStudyAssistEnabled = true;
log('<span class="green">Autopause Enabled & Controls Hidden.</span>', 'debug');
}
}
// Disable Netflix Study Assist
function disableNetflixStudyAssist()
{
if (netflixStudyAssistEnabled == true) {
// Show navigation controls
playerControls.style.display = 'inherit';
backButton.style.display = 'inherit';
netflixStudyAssistEnabled = false;
log('<span class="green">Autopause Disabled & Controls Unhidden.<span>', 'debug');
}
}
// Toggle Log Window
function toggleLogWindow()
{
if (logWindowHidden == true) {
logWindow.style.display = 'inherit';
logWindowHidden = false;
}
else {
logWindow.style.display = 'none';
logWindowHidden = true;
}
}
// Strip HTML and leave text
function strip(html)
{
var tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
}
// Add to log window
function log(message, type = false)
{
// Prevent debug messages from being displayed
if (showDebugMessagesInLogWindow == false && type == 'debug') return;
var now = new Date();
var date = pad(now.getHours(), 2) + ':' + pad(now.getMinutes(), 2) + ':' + pad(now.getSeconds(), 2);
if (showDebugMessagesInLogWindow) date += ':' + now.getMilliseconds();
logWindowInner.insertAdjacentHTML('afterbegin','<p><span class="light">' + date + ':</span> ' + message + '</p>');
}
// Pad integers with leading 0s
// https://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript#10073788
function pad(n, width, z)
{
z = z || '0';
n = n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment