Greasy Fork is available in English.

Netflix subtitle downloader

Allows you to download subtitles from Netflix

2017/01/23のページです。最新版はこちら。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==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);