- // ==UserScript==
- // @name Netflix subtitle downloader
- // @description Allows you to download subtitles from Netflix
- // @namespace tithen-firion
- // @include https://www.netflix.com/*
- // @version 1.1
- // @require https://greasyfork.org/scripts/26651-xhrhijacker/code/xhrHijacker.js?version=170146
- // @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)");
- 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);