Netflix - subtitle downloader

Allows you to download subtitles from Netflix

目前為 2020-02-22 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Netflix - subtitle downloader
  3. // @description Allows you to download subtitles from Netflix
  4. // @license MIT
  5. // @version 3.2.1
  6. // @namespace tithen-firion.github.io
  7. // @include https://www.netflix.com/*
  8. // @grant unsafeWindow
  9. // @require https://cdn.jsdelivr.net/gh/Stuk/jszip@579beb1d45c8d586d8be4411d5b2e48dea018c06/dist/jszip.min.js?version=3.1.5
  10. // @require https://cdn.jsdelivr.net/gh/eligrey/FileSaver.js@283f438c31776b622670be002caf1986c40ce90c/dist/FileSaver.min.js?version=2018-12-29
  11. // ==/UserScript==
  12.  
  13. class ProgressBar {
  14. constructor(max) {
  15. this.current = 0;
  16. this.max = max;
  17.  
  18. let container = document.querySelector('#userscript_progress_bars');
  19. if(container === null) {
  20. container = document.createElement('div');
  21. container.id = 'userscript_progress_bars'
  22. document.body.appendChild(container)
  23. container.style
  24. container.style.position = 'fixed';
  25. container.style.top = 0;
  26. container.style.left = 0;
  27. container.style.width = '100%';
  28. container.style.background = 'red';
  29. container.style.zIndex = '99999999';
  30. }
  31.  
  32. this.progressElement = document.createElement('div');
  33. this.progressElement.style.width = 0;
  34. this.progressElement.style.height = '10px';
  35. this.progressElement.style.background = 'green';
  36.  
  37. container.appendChild(this.progressElement);
  38. }
  39. increment() {
  40. this.current += 1;
  41. if(this.current <= this.max)
  42. this.progressElement.style.width = this.current / this.max * 100 + '%';
  43. }
  44.  
  45. destroy() {
  46. this.progressElement.remove();
  47. }
  48. }
  49.  
  50. const MAIN_TITLE = '.player-status-main-title, .ellipsize-text>h4, .video-title>h4';
  51. const TRACK_MENU = '#player-menu-track-settings, .audio-subtitle-controller';
  52. const NEXT_EPISODE = '.player-next-episode:not(.player-hidden), .button-nfplayerNextEpisode';
  53.  
  54. const WEBVTT = 'webvtt-lssdh-ios8';
  55.  
  56. const DOWNLOAD_MENU = `<lh class="list-header">Netflix subtitle downloader</lh>
  57. <li class="list-header">Netflix subtitle downloader</li>
  58. <li class="track download">Download subs for this episode</li>
  59. <li class="track download-all">Download subs from this ep till last available</li>
  60. <li class="track force-all-lang">Force Netflix to show all languages: <span></span></li>
  61. <li class="track lang-setting">Languages to download: <span></span></li>`;
  62.  
  63. const SCRIPT_CSS = `.player-timed-text-tracks, .track-list-subtitles{ border-right:1px solid #000 }
  64. .player-timed-text-tracks+.player-timed-text-tracks, .track-list-subtitles+.track-list-subtitles{ border-right:0 }
  65. .subtitle-downloader-menu { list-style:none }
  66. #player-menu-track-settings .subtitle-downloader-menu li.list-header,
  67. .audio-subtitle-controller .subtitle-downloader-menu lh.list-header{ display:none }`;
  68.  
  69. const SUB_TYPES = {
  70. 'subtitles': '',
  71. 'closedcaptions': '[cc]'
  72. };
  73.  
  74. let zip;
  75. let subCache = {};
  76. let batch = false;
  77.  
  78. let forceSubs = localStorage.getItem('NSD_force-all-lang') !== 'false';
  79. let langs = localStorage.getItem('NSD_lang-setting') || '';
  80.  
  81. const setForceText = () => {
  82. document.querySelector('.subtitle-downloader-menu > .force-all-lang > span').innerHTML = (forceSubs ? 'on' : 'off');
  83. };
  84. const setLangsText = () => {
  85. document.querySelector('.subtitle-downloader-menu > .lang-setting > span').innerHTML = (langs === '' ? 'all' : langs);
  86. };
  87.  
  88. const toggleForceLang = () => {
  89. forceSubs = !forceSubs;
  90. if(forceSubs)
  91. localStorage.removeItem('NSD_force-all-lang');
  92. else
  93. localStorage.setItem('NSD_force-all-lang', forceSubs);
  94. document.location.reload();
  95. };
  96. const setLangToDownload = () => {
  97. const result = prompt('Languages to download, comma separated. Leave empty to download all subtitles.\nExample: en,de,fr', langs);
  98. if(result !== null) {
  99. langs = result;
  100. if(langs === '')
  101. localStorage.removeItem('NSD_lang-setting');
  102. else
  103. localStorage.setItem('NSD_lang-setting', langs);
  104. setLangsText();
  105. }
  106. };
  107.  
  108. const popRandomElement = arr => {
  109. return arr.splice(arr.length * Math.random() << 0, 1)[0];
  110. };
  111.  
  112. // get show name or full name with episode number
  113. const __getTitle = full => {
  114. if(typeof full === 'undefined')
  115. full = true;
  116. const titleElement = document.querySelector(MAIN_TITLE);
  117. if(titleElement === null)
  118. return null;
  119. const title = [titleElement.textContent.replace(/[:*?"<>|\\\/]+/g, '_').replace(/ /g, '.')];
  120. if(full) {
  121. const episodeElement = titleElement.nextElementSibling;
  122. if(episodeElement) {
  123. const m = episodeElement.textContent.match(/^[^\d]*(\d+)[^\d]+(\d+)[^\d]*$/);
  124. if(m && m.length == 3) {
  125. title.push(`S${m[1].padStart(2, '0')}E${m[2].padStart(2, '0')}`);
  126. }
  127. else {
  128. title.push(episodeElement.textContent.trim().replace(/[:*?"<>|\\\/]+/g, '_').replace(/ /g, '.'));
  129. }
  130. }
  131. title.push('WEBRip.Netflix');
  132. }
  133. return title.join('.');
  134. };
  135. // helper function, periodically checking for the title and resolving promise if found
  136. const _getTitle = (full, resolve) => {
  137. const title = __getTitle(full);
  138. if(title === null)
  139. window.setTimeout(_getTitle, 200, full, resolve);
  140. else
  141. resolve(title);
  142. };
  143. // promise of a title
  144. const getTitle = full => new Promise(resolve => {
  145. _getTitle(full, resolve);
  146. });
  147.  
  148. const processSubInfo = async result => {
  149. const tracks = result.timedtexttracks;
  150. const titleP = getTitle();
  151. const subs = {};
  152. for(const track of tracks) {
  153. if(track.isNoneTrack)
  154. continue;
  155. if(typeof track.ttDownloadables[WEBVTT] === 'undefined')
  156. continue;
  157.  
  158. let type = SUB_TYPES[track.rawTrackType];
  159. if(typeof type === 'undefined')
  160. type = `[${track.rawTrackType}]`;
  161. const lang = track.language + type + (track.isForcedNarrative ? '-forced' : '');
  162. subs[lang] = Object.values(track.ttDownloadables[WEBVTT].downloadUrls);
  163. }
  164. subCache[result.movieId] = {titleP, subs};
  165.  
  166. if(batch) {
  167. downloadAll();
  168. }
  169. };
  170.  
  171. const getMovieID = () => window.location.pathname.split('/').pop();
  172.  
  173.  
  174. const _save = async (_zip, title) => {
  175. const content = await _zip.generateAsync({type:'blob'});
  176. saveAs(content, title + '.zip');
  177. };
  178.  
  179. const _download = async _zip => {
  180. const showTitle = getTitle(false);
  181. const {titleP, subs} = subCache[getMovieID()];
  182. const downloaded = [];
  183.  
  184. let filteredLangs;
  185. if(langs === '')
  186. filteredLangs = Object.keys(subs);
  187. else {
  188. const regularExpression = new RegExp('^(' + langs.replace(/\-/g, '\\-').replace(/\s/g, '').replace(/,/g, '|') + ')');
  189. filteredLangs = [];
  190. for(const lang of Object.keys(subs)) {
  191. if(lang.match(regularExpression))
  192. filteredLangs.push(lang);
  193. }
  194. }
  195.  
  196. const progress = new ProgressBar(filteredLangs.length);
  197. for(const lang of filteredLangs) {
  198. const urls = subs[lang]
  199. while(urls.length > 0) {
  200. let url = popRandomElement(urls);
  201. const result = await fetch(url, {mode: "cors"});
  202. progress.increment();
  203. const data = await result.text();
  204. if(data.length > 0) {
  205. downloaded.push({lang, data});
  206. break;
  207. }
  208. }
  209. }
  210. progress.destroy();
  211. const title = await titleP;
  212.  
  213. downloaded.forEach(x => {
  214. const {lang, data} = x;
  215. _zip.file(`${title}.${lang}.vtt`, data);
  216. });
  217.  
  218. return await showTitle;
  219. };
  220.  
  221. const downloadThis = async () => {
  222. const _zip = new JSZip();
  223. const showTitle = await _download(_zip);
  224. _save(_zip, showTitle);
  225. };
  226.  
  227. const downloadAll = async () => {
  228. zip = zip || new JSZip();
  229. batch = true;
  230. const showTitle = await _download(zip);
  231. const nextEp = document.querySelector(NEXT_EPISODE);
  232. if(nextEp)
  233. nextEp.click();
  234. else {
  235. await _save(zip, showTitle);
  236. zip = undefined;
  237. batch = false;
  238. }
  239. };
  240.  
  241. const processMessage = e => {
  242. processSubInfo(e.detail);
  243. }
  244.  
  245. const injection = () => {
  246. const WEBVTT = 'webvtt-lssdh-ios8';
  247. const MANIFEST_URL = "/manifest";
  248. const forceSubs = localStorage.getItem('NSD_force-all-lang') !== 'false';
  249.  
  250. // hijack JSON.parse and JSON.stringify functions
  251. ((parse, stringify) => {
  252. JSON.parse = function (text) {
  253. const data = parse(text);
  254. if (data && data.result && data.result.timedtexttracks && data.result.movieId) {
  255. window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: data.result}));
  256. }
  257. return data;
  258. };
  259. JSON.stringify = function (data) {
  260. if (data && data.url === MANIFEST_URL) {
  261. for (let v of Object.values(data)) {
  262. try {
  263. if (v.profiles)
  264. v.profiles.unshift(WEBVTT);
  265. if (v.showAllSubDubTracks != null && forceSubs)
  266. v.showAllSubDubTracks = true;
  267. }
  268. catch (e) {
  269. if (e instanceof TypeError)
  270. continue;
  271. else
  272. throw e;
  273. }
  274. }
  275. }
  276. return stringify(data);
  277. };
  278. })(JSON.parse, JSON.stringify);
  279. }
  280.  
  281. window.addEventListener('netflix_sub_downloader_data', processMessage, false);
  282.  
  283. // inject script
  284. const sc = document.createElement('script');
  285. sc.innerHTML = '(' + injection.toString() + ')()';
  286. document.head.appendChild(sc);
  287. document.head.removeChild(sc);
  288.  
  289. // add CSS style
  290. const s = document.createElement('style');
  291. s.innerHTML = SCRIPT_CSS;
  292. document.head.appendChild(s);
  293.  
  294. // add menu when it's not there
  295. const observer = new MutationObserver(function(mutations) {
  296. mutations.forEach(function(mutation) {
  297. mutation.addedNodes.forEach(function(node) {
  298. if(node.nodeName.toUpperCase() == 'DIV') {
  299. let trackMenu = (node.parentNode || node).querySelector(TRACK_MENU);
  300. if(trackMenu !== null && trackMenu.querySelector('.subtitle-downloader-menu') === null) {
  301. let ol = document.createElement('ol');
  302. ol.setAttribute('class', 'subtitle-downloader-menu player-timed-text-tracks track-list track-list-subtitles');
  303. ol.innerHTML = DOWNLOAD_MENU;
  304. trackMenu.appendChild(ol);
  305. ol.querySelector('.download').addEventListener('click', downloadThis);
  306. ol.querySelector('.download-all').addEventListener('click', downloadAll);
  307. ol.querySelector('.force-all-lang').addEventListener('click', toggleForceLang);
  308. ol.querySelector('.lang-setting').addEventListener('click', setLangToDownload);
  309. setForceText();
  310. setLangsText();
  311. }
  312. }
  313. });
  314. });
  315. });
  316. observer.observe(document.body, { childList: true, subtree: true });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址