JustPaste.it

// ==UserScript==
// @name Netflix subtitle downloader
// @description Allows you to download subtitles from Netflix
// @namespace tithen-firion
// @include https://www.netflix.com/*
// @version 1.7
// @require https://greasyfork.org/scripts/26651-xhrhijacker/code/xhrHijacker.js?version=171120
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.js
// @grant GM_registerMenuCommand
// ==/UserScript==

function pad(n, w) {
n = n + '';
w = w || 2;
return n.length >= w ? n : new Array(w - n.length + 1).join(0) + n;
}

function downloadThis() {
if(typeof subFile === "undefined") {
//window.setTimeout(downloadThis, 100);
} else {
var blob = new Blob([subFile.content], {type: "text/plain;charset=utf-8"});
saveAs(blob, subFile.name, true);
}
}

function downloadAll() {
batch = true;
if(typeof subFile === "undefined") {
//window.setTimeout(downloadThis, 100);
} else {
zip = zip || new JSZip();
zip.file(subFile.name, subFile.content);
//var el = document.querySelector(".player-next-episode:not(.player-hidden)");
var el = document.querySelector(".PlayerControls--button-control-row > div:nth-child(5) > button:not(.PlayerControls--control-element-hidden)");
if(el)
el.click();
else
zip.generateAsync({type:"blob"})
.then(function(content) {
saveAs(content, seriesTitle + ".zip");
zip = undefined;
batch = false;
});
}
}

function formatTime(time) {
var tmp = time;
var ms = pad(time%1000, 3);
time = Math.floor(time/1000);
var s = pad(time%60);
time = Math.floor(time/60);
var m = pad(time%60);
var h = pad(Math.floor(time/60));
return h + ":" + m + ":" + s + "," + ms;
}
function saveAsSrt(subs, filename) {
txt = "";
subs.forEach(function(sub, i) {
txt += (i+1) + "\n" + formatTime(sub.s) + " --> " + formatTime(sub.e) + "\n" + sub.t + "\n\n";
});
subFile = {
name: filename + ".srt",
content: txt
};
if(batch)
downloadAll();
}

function toText(node, styles) {
var txt = "";
var children = node.childNodes;
for(let i = 0; i < children.length; ++i) {
if(children[i].nodeType === 3)
txt += children[i].textContent;
else if(children[i].nodeType === 1) {
if(children[i].nodeName.toUpperCase() === "BR")
txt += "\n";
else
txt += toText(children[i], styles);
}
}
if(node.hasAttribute("style")) {
var s = node.getAttribute("style");
if(s in styles)
txt = styles[s].s + txt + styles[s].e;
}
return txt;
}

function styleParserHelper(style, styleElem, attribute, expectedValue, tag, colour) {
var closeTag = false;
if(styleElem.hasAttribute(attribute)) {
let value = styleElem.getAttribute(attribute).trim();
let equal = value === expectedValue;
if(colour) {
if(!equal) {
style.s = "<" + tag + ' color="' + value + '">' + style.s;
closeTag = true;
}
} else if(equal) {
style.s = "<" + tag + ">" + style.s;
closeTag = true;
}
if(closeTag)
style.e += "</" + tag + ">";
}
}
function processXml(xml, filename) {
try {
var styles = {}, prevStart = -1, subs = [{s: 0, e: 500, t: "Subtitles downloaded with 'Netflix subtitle downloader' UserScript by Tithen-Firion."}];
var styleElems = xml.querySelectorAll("styling style");
for(let i = 0; i < styleElems.length; ++i) {
let id = styleElems[i].getAttribute("xml:id");
styles[id] = {s: "", e: ""};
styleParserHelper(styles[id], styleElems[i], "tts:fontWeight", "bold", "b");
styleParserHelper(styles[id], styleElems[i], "tts:fontStyle", "italic", "i");
styleParserHelper(styles[id], styleElems[i], "tts:textDecoration", "underline", "u");
styleParserHelper(styles[id], styleElems[i], "tts:color", "white", "font", true);
if(styles[id].s === "")
delete styles[id];
}
var subElems = xml.querySelectorAll("div p");
for(let i = 0; i < subElems.length; ++i) {
let el = subElems[i];
let start = Math.round(parseInt(el.getAttribute("begin"))/10000);
let end = Math.round(parseInt(el.getAttribute("end"))/10000);
let txt = toText(el, styles);
if(start === prevStart)
subs[subs.length-1].t += "\n" + txt;
else
subs.push({s: start, e: end, t: txt});
prevStart = start;
}
}
catch(e) {
console.error(e);
alert('Failed to convert to SRT format');
return;
}
saveAsSrt(subs, filename);
}
function processResponse(responseText) {
var el = document.querySelector(".PlayerControls--control-element.text-control.video-title.PlayerControls--control-element-hidden > .ellipsize-text > h4");
if(el === null) {
window.setTimeout(processResponse, 200, responseText);
return;
}
var title = seriesTitle = el.innerText.replace(/[:*?"<>|\\\/]+/g, "_").replace(/ /g, ".") + ".";
var nextEl = document.querySelector(".PlayerControls--control-element.text-control.video-title.PlayerControls--control-element-hidden > .ellipsize-text > span");
if(nextEl) {
var m = nextEl.innerText.match(/^[^\d]*?(\d+)[^\d]*?(\d+)[^\d]*?$/);
if(m && m.length === 3)
title += "S" + pad(m[1]) + "E" + pad(m[2]) + ".WEBRip.Netflix";
}
//title += document.querySelector(".player-timed-text-tracks > .player-track-selected").getAttribute("data-id").split(";")[2];
var xmlDoc;
try {
var parser = new DOMParser();
xmlDoc = parser.parseFromString(responseText, "text/xml");
}
catch(e) {
console.error(e);
alert('Failed to parse XML subtitle file');
return;
}
processXml(xmlDoc, title);
}

var IDs = [], batch = false, seriesTitle, zip, subFile;
xhrHijacker(function(xhr, id, origin, args) {
if(origin === "open") {
if(args[1].indexOf("/?o=") > -1)
IDs.push(id);
} else if(origin === "load") {
var index = IDs.indexOf(id);
if(index > -1) {
IDs.splice(index, 1);
processResponse(xhr.response);
}
}
});

GM_registerMenuCommand("Download subs for this episode", downloadThis);
GM_registerMenuCommand("Download subs from this ep till last available", downloadAll);