// ==UserScript==
// @name Netflix subtitle downloader
// @description Allows you to download subtitles from Netflix
// @namespace tithen-firion
// @include https://www.netflix.com/*
// @version 1.3
// @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==
var xhrHijacker = xhrHijacker || function(process) {
if(typeof process != "function") {
process = function(){ console.log(arguments); };
}
function postMyMessage(from_, detail, arg1, arg2, arg3) {
if(typeof arg1 == "string")
detail = {
xhr: detail,
origin: arg1,
id: arg2,
args: arg3
};
window.dispatchEvent(new CustomEvent("xhrHijacker_message_from_" + from_, {detail: detail}));
}
function processMessage(e) {
var d = e.detail;
process(d.xhr, d.id, d.origin, d.args);
postMyMessage("userscript", d);
}
window.addEventListener("xhrHijacker_message_from_injected", processMessage, false);
function injection() {
var xhrs = {};
var real = {
open: XMLHttpRequest.prototype.open,
send: XMLHttpRequest.prototype.send
}
function addRandomProperty(object, data, prefix) {
if(typeof prefix != "string")
prefix = "";
var x;
do {
x = prefix + Math.random();
} while(object.hasOwnProperty(x));
object[x] = data;
return x;
}
function searchForPropertyName(object, data) {
for(var e in object) {
if(object.hasOwnProperty(e) && object[e] == data)
return e;
}
}
function processMessage(e) {
var d = e.detail;
var args;
if(typeof d.args === "object") {
// args = Array.prototype.slice.call(d.args, 0); // doesn't work
args = [];
for(var i = d.args.length-1; i >= 0; --i)
args[i] = d.args[i];
} else
args = d.args;
if(d.origin == "open" || d.origin == "send" ) {
real[d.origin].apply(d.xhr, args);
} else if(d.origin == "load") {
delete xhrs[d.id];
}
}
window.addEventListener("xhrHijacker_message_from_userscript", processMessage, false);
XMLHttpRequest.prototype.open = function() {
var id = addRandomProperty(xhrs, this);
this.addEventListener("load", function() {
postMyMessage("injected", this, "load", id);
}, false);
this.addEventListener("readystatechange", function() {
postMyMessage("injected", this, "readystatechange", id);
}, false);
postMyMessage("injected", this, "open", id, arguments);
};
XMLHttpRequest.prototype.send = function() {
var id = searchForPropertyName(xhrs, this);
postMyMessage("injected", this, "send", id, arguments);
};
}
var grantUsed = false;
if(typeof unsafeWindow !== 'undefined' && window !== unsafeWindow) {
var x;
do {
x = Math.random();
} while(window.hasOwnProperty(x) || unsafeWindow.hasOwnProperty(x));
if(!unsafeWindow[x])
grantUsed = true;
delete window[x];
}
console.time("xhrHijacker - injecting code");
if(grantUsed) {
console.info("xhrHijacker - inject");
window.setTimeout(postMyMessage.toString() + "(" + injection.toString() + ")()", 0);
} else {
console.info("xhrHijacker - execute");
injection();
}
console.timeEnd("xhrHijacker - injecting code");
}
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)");
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) {
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;
}
saveAsSrt(subs, filename);
}
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);
var el = document.querySelector(".player-status-main-title");
var title = seriesTitle = el.innerText.replace(/[:*?"<>|\\\/]+/g, "_").replace(/ /g, ".");
var m = el.nextElementSibling.innerText.match(/^[^\d]*?(\d+)[^\d]*?(\d+)[^\d]*?$/);
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 parser = new DOMParser();
var xmlDoc = parser.parseFromString(xhr.response, "text/xml");
processXml(xmlDoc, title);
}
}
});
GM_registerMenuCommand("Download subs for this episode", downloadThis);
GM_registerMenuCommand("Download subs from this ep till last available", downloadAll);