Last active
October 11, 2018 10:21
-
-
Save chriskempson/701d85f49be1b5cfb42737a09ad6a116 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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