Netflix subtitle downloader

Allows you to download subtitles from Netflix

目前為 2017-10-09 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Netflix subtitle downloader
// @description Allows you to download subtitles from Netflix
// @namespace   tithen-firion
// @include     https://www.netflix.com/*
// @version     1.8
// @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), .button-nfplayerNextEpisode");
    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) {
  try {
    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;
    }
  }
  catch(e) {
    console.error(e);
    alert('Failed to convert to SRT format');
    return;
  }
  saveAsSrt(subs, filename);
}
function processResponse(responseText) {
  try{
  var el = document.querySelector(".player-status-main-title, .ellipsize-text>h4");
  if(el === null) {
    window.setTimeout(processResponse, 200, responseText);
    return;
  }
  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";
  }
  var selectedSubs = document.querySelector(".player-timed-text-tracks > .player-track-selected");
  if(selectedSubs !== null)
    title += "." + selectedSubs.getAttribute("data-id").split(";")[2];
  try {
    var parser = new DOMParser();
    var xmlDoc = parser.parseFromString(responseText, "text/xml");
  }
  catch(e) {
    console.error(e);
    alert('Failed to parse XML subtitle file');
    return;
  }
  processXml(xmlDoc, title);
  }catch(e){console.log(e)}
}

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);
      processResponse(xhr.response);
    }
  }
});

GM_registerMenuCommand("Download subs for this episode", downloadThis);
GM_registerMenuCommand("Download subs from this ep till last available", downloadAll);