Netflix subtitle downloader

Allows you to download subtitles from Netflix

As of 15/05/2017. See the latest version.

// ==UserScript==
// @name        Netflix subtitle downloader
// @description Allows you to download subtitles from Netflix
// @namespace   tithen-firion
// @include     https://www.netflix.com/*
// @version     1.5
// @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)");
    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 nextEl = el.nextElementSibling;
      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 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);