Netflix subtitle downloader

Allows you to download subtitles from Netflix

נכון ליום 23-01-2017. ראה הגרסה האחרונה.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Netflix subtitle downloader
// @description Allows you to download subtitles from Netflix
// @namespace   tithen-firion
// @include     https://www.netflix.com/*
// @version     1.4
// @require     https://greatest.deepsurf.us/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 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);