Netflix - subtitle downloader

Allows you to download subtitles from Netflix

  1. // ==UserScript==
  2. // @name Netflix - subtitle downloader
  3. // @description Allows you to download subtitles from Netflix
  4. // @license MIT
  5. // @version 4.2.8
  6. // @namespace tithen-firion.github.io
  7. // @include https://www.netflix.com/*
  8. // @grant unsafeWindow
  9. // @require https://cdn.jsdelivr.net/npm/jszip@3.7.1/dist/jszip.min.js
  10. // @require https://cdn.jsdelivr.net/npm/file-saver-es@2.0.5/dist/FileSaver.min.js
  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.innerHTML = 'Click to stop';
  34. this.progressElement.style.cursor = 'pointer';
  35. this.progressElement.style.fontSize = '16px';
  36. this.progressElement.style.textAlign = 'center';
  37. this.progressElement.style.width = '100%';
  38. this.progressElement.style.height = '20px';
  39. this.progressElement.style.background = 'transparent';
  40. this.stop = new Promise(resolve => {
  41. this.progressElement.addEventListener('click', () => {resolve(STOP_THE_DOWNLOAD)});
  42. });
  43.  
  44. container.appendChild(this.progressElement);
  45. }
  46.  
  47. increment() {
  48. this.current += 1;
  49. if(this.current <= this.max) {
  50. let p = this.current / this.max * 100;
  51. this.progressElement.style.background = `linear-gradient(to right, green ${p}%, transparent ${p}%)`;
  52. }
  53. }
  54.  
  55. destroy() {
  56. this.progressElement.remove();
  57. }
  58. }
  59.  
  60. const STOP_THE_DOWNLOAD = 'NETFLIX_SUBTITLE_DOWNLOADER_STOP_THE_DOWNLOAD';
  61.  
  62. const WEBVTT = 'webvtt-lssdh-ios8';
  63. const DFXP = 'dfxp-ls-sdh';
  64. const SIMPLE = 'simplesdh';
  65. const IMSC1_1 = 'imsc1.1';
  66. const ALL_FORMATS = [IMSC1_1, DFXP, WEBVTT, SIMPLE];
  67. const ALL_FORMATS_prefer_vtt = [WEBVTT, IMSC1_1, DFXP, SIMPLE];
  68.  
  69. const FORMAT_NAMES = {};
  70. FORMAT_NAMES[WEBVTT] = 'WebVTT';
  71. FORMAT_NAMES[DFXP] = 'IMSC1.1/DFXP/XML';
  72.  
  73. const EXTENSIONS = {};
  74. EXTENSIONS[WEBVTT] = 'vtt';
  75. EXTENSIONS[DFXP] = 'dfxp';
  76. EXTENSIONS[SIMPLE] = 'xml';
  77. EXTENSIONS[IMSC1_1] = 'xml';
  78.  
  79. const DOWNLOAD_MENU = `
  80. <ol>
  81. <li class="header">Netflix subtitle downloader</li>
  82. <li class="download">Download subs for this <span class="series">episode</span><span class="not-series">movie</span></li>
  83. <li class="download-to-end series">Download subs from this ep till last available</li>
  84. <li class="download-season series">Download subs for this season</li>
  85. <li class="download-all series">Download subs for all seasons</li>
  86. <li class="ep-title-in-filename">Add episode title to filename: <span></span></li>
  87. <li class="force-all-lang">Force Netflix to show all languages: <span></span></li>
  88. <li class="pref-locale">Preferred locale: <span></span></li>
  89. <li class="lang-setting">Languages to download: <span></span></li>
  90. <li class="sub-format">Subtitle format: prefer <span></span></li>
  91. <li class="batch-delay">Batch delay: <span></span></li>
  92. </ol>
  93. `;
  94.  
  95. const SCRIPT_CSS = `
  96. #subtitle-downloader-menu {
  97. position: absolute;
  98. display: none;
  99. width: 300px;
  100. top: 0;
  101. left: calc( 50% - 150px );
  102. }
  103. #subtitle-downloader-menu ol {
  104. list-style: none;
  105. position: relative;
  106. width: 300px;
  107. background: #333;
  108. color: #fff;
  109. padding: 0;
  110. margin: auto;
  111. font-size: 12px;
  112. z-index: 99999998;
  113. }
  114. body:hover #subtitle-downloader-menu { display: block; }
  115. #subtitle-downloader-menu li { padding: 10px; }
  116. #subtitle-downloader-menu li.header { font-weight: bold; }
  117. #subtitle-downloader-menu li:not(.header):hover { background: #666; }
  118. #subtitle-downloader-menu li:not(.header) {
  119. display: none;
  120. cursor: pointer;
  121. }
  122. #subtitle-downloader-menu:hover li { display: block; }
  123.  
  124. #subtitle-downloader-menu:not(.series) .series{ display: none; }
  125. #subtitle-downloader-menu.series .not-series{ display: none; }
  126. `;
  127.  
  128. const SUB_TYPES = {
  129. 'subtitles': '',
  130. 'closedcaptions': '[cc]'
  131. };
  132.  
  133. let idOverrides = {};
  134. let subCache = {};
  135. let titleCache = {};
  136.  
  137. let batch = null;
  138. try {
  139. batch = JSON.parse(sessionStorage.getItem('NSD_batch'));
  140. }
  141. catch(ignore) {}
  142.  
  143. let batchAll = null;
  144. let batchSeason = null;
  145. let batchToEnd = null;
  146.  
  147. let epTitleInFilename = localStorage.getItem('NSD_ep-title-in-filename') === 'true';
  148. let forceSubs = localStorage.getItem('NSD_force-all-lang') !== 'false';
  149. let prefLocale = localStorage.getItem('NSD_pref-locale') || '';
  150. let langs = localStorage.getItem('NSD_lang-setting') || '';
  151. let subFormat = localStorage.getItem('NSD_sub-format') || WEBVTT;
  152. let batchDelay = parseFloat(localStorage.getItem('NSD_batch-delay') || '0');
  153.  
  154. const setEpTitleInFilename = () => {
  155. document.querySelector('#subtitle-downloader-menu .ep-title-in-filename > span').innerHTML = (epTitleInFilename ? 'on' : 'off');
  156. };
  157. const setForceText = () => {
  158. document.querySelector('#subtitle-downloader-menu .force-all-lang > span').innerHTML = (forceSubs ? 'on' : 'off');
  159. };
  160. const setLocaleText = () => {
  161. document.querySelector('#subtitle-downloader-menu .pref-locale > span').innerHTML = (prefLocale === '' ? 'disabled' : prefLocale);
  162. };
  163. const setLangsText = () => {
  164. document.querySelector('#subtitle-downloader-menu .lang-setting > span').innerHTML = (langs === '' ? 'all' : langs);
  165. };
  166. const setFormatText = () => {
  167. document.querySelector('#subtitle-downloader-menu .sub-format > span').innerHTML = FORMAT_NAMES[subFormat];
  168. };
  169. const setBatchDelayText = () => {
  170. document.querySelector('#subtitle-downloader-menu .batch-delay > span').innerHTML = batchDelay;
  171. };
  172.  
  173. const setBatch = b => {
  174. if(b === null)
  175. sessionStorage.removeItem('NSD_batch');
  176. else
  177. sessionStorage.setItem('NSD_batch', JSON.stringify(b));
  178. };
  179.  
  180. const toggleEpTitleInFilename = () => {
  181. epTitleInFilename = !epTitleInFilename;
  182. if(epTitleInFilename)
  183. localStorage.setItem('NSD_ep-title-in-filename', epTitleInFilename);
  184. else
  185. localStorage.removeItem('NSD_ep-title-in-filename');
  186. setEpTitleInFilename();
  187. };
  188. const toggleForceLang = () => {
  189. forceSubs = !forceSubs;
  190. if(forceSubs)
  191. localStorage.removeItem('NSD_force-all-lang');
  192. else
  193. localStorage.setItem('NSD_force-all-lang', forceSubs);
  194. document.location.reload();
  195. };
  196. const setPreferredLocale = () => {
  197. const result = prompt('Netflix limited "force all subtitles" usage. Now you have to set a preferred locale to show subtitles for that language.\nPossible values (you can enter only one at a time!):\nar, cs, da, de, el, en, es, es-ES, fi, fr, he, hi, hr, hu, id, it, ja, ko, ms, nb, nl, pl, pt, pt-BR, ro, ru, sv, ta, te, th, tr, uk, vi, zh', prefLocale);
  198. if(result !== null) {
  199. prefLocale = result;
  200. if(prefLocale === '')
  201. localStorage.removeItem('NSD_pref-locale');
  202. else
  203. localStorage.setItem('NSD_pref-locale', prefLocale);
  204. document.location.reload();
  205. }
  206. };
  207. const setLangToDownload = () => {
  208. const result = prompt('Languages to download, comma separated. Leave empty to download all subtitles.\nExample: en,de,fr', langs);
  209. if(result !== null) {
  210. langs = result;
  211. if(langs === '')
  212. localStorage.removeItem('NSD_lang-setting');
  213. else
  214. localStorage.setItem('NSD_lang-setting', langs);
  215. setLangsText();
  216. }
  217. };
  218. const setSubFormat = () => {
  219. if(subFormat === WEBVTT) {
  220. localStorage.setItem('NSD_sub-format', DFXP);
  221. subFormat = DFXP;
  222. }
  223. else {
  224. localStorage.removeItem('NSD_sub-format');
  225. subFormat = WEBVTT;
  226. }
  227. setFormatText();
  228. };
  229. const setBatchDelay = () => {
  230. let result = prompt('Delay (in seconds) between switching pages when downloading subs in batch:', batchDelay);
  231. if(result !== null) {
  232. result = parseFloat(result.replace(',', '.'));
  233. if(result < 0 || !Number.isFinite(result))
  234. result = 0;
  235. batchDelay = result;
  236. if(batchDelay == 0)
  237. localStorage.removeItem('NSD_batch-delay');
  238. else
  239. localStorage.setItem('NSD_batch-delay', batchDelay);
  240. setBatchDelayText();
  241. }
  242. };
  243.  
  244. const asyncSleep = (seconds, value) => new Promise(resolve => {
  245. window.setTimeout(resolve, seconds * 1000, value);
  246. });
  247.  
  248. const popRandomElement = arr => {
  249. return arr.splice(arr.length * Math.random() << 0, 1)[0];
  250. };
  251.  
  252. const processSubInfo = async result => {
  253. const tracks = result.timedtexttracks;
  254. const subs = {};
  255. let reportError = true;
  256. for(const track of tracks) {
  257. if(track.isNoneTrack)
  258. continue;
  259.  
  260. let type = SUB_TYPES[track.rawTrackType];
  261. if(typeof type === 'undefined')
  262. type = `[${track.rawTrackType}]`;
  263. const variant = (typeof track.trackVariant === 'undefined' ? '' : `-${track.trackVariant}`);
  264. const lang = track.language + type + variant + (track.isForcedNarrative ? '-forced' : '');
  265.  
  266. const formats = {};
  267. for(let format of ALL_FORMATS) {
  268. const downloadables = track.ttDownloadables[format];
  269. if(typeof downloadables !== 'undefined') {
  270. let urls;
  271. if(typeof downloadables.downloadUrls !== 'undefined')
  272. urls = Object.values(downloadables.downloadUrls);
  273. else if(typeof downloadables.urls !== 'undefined')
  274. urls = downloadables.urls.map(({url}) => url);
  275. else {
  276. console.log('processSubInfo:', lang, Object.keys(downloadables));
  277. if(reportError) {
  278. reportError = false;
  279. alert("Can't find subtitle URL, check the console for more information!");
  280. }
  281. continue;
  282. }
  283. formats[format] = [urls, EXTENSIONS[format]];
  284. }
  285. }
  286.  
  287. if(Object.keys(formats).length > 0) {
  288. for(let i = 0; ; ++i) {
  289. const langKey = lang + (i == 0 ? "" : `-${i}`);
  290. if(typeof subs[langKey] === "undefined") {
  291. subs[langKey] = formats;
  292. break;
  293. }
  294. }
  295. }
  296. }
  297. subCache[result.movieId] = subs;
  298. };
  299.  
  300. const checkSubsCache = async menu => {
  301. while(getSubsFromCache(true) === null) {
  302. await asyncSleep(0.1);
  303. }
  304.  
  305. // show menu if on watch page
  306. menu.style.display = (document.location.pathname.split('/')[1] === 'watch' ? '' : 'none');
  307.  
  308. if(batch !== null && batch.length > 0) {
  309. downloadBatch(true);
  310. }
  311. };
  312.  
  313. const processMetadata = data => {
  314. // add menu when it's not there
  315. let menu = document.querySelector('#subtitle-downloader-menu');
  316. if(menu === null) {
  317. menu = document.createElement('div');
  318. menu.id = 'subtitle-downloader-menu';
  319. menu.innerHTML = DOWNLOAD_MENU;
  320. document.body.appendChild(menu);
  321. menu.querySelector('.download').addEventListener('click', downloadThis);
  322. menu.querySelector('.download-to-end').addEventListener('click', downloadToEnd);
  323. menu.querySelector('.download-season').addEventListener('click', downloadSeason);
  324. menu.querySelector('.download-all').addEventListener('click', downloadAll);
  325. menu.querySelector('.ep-title-in-filename').addEventListener('click', toggleEpTitleInFilename);
  326. menu.querySelector('.force-all-lang').addEventListener('click', toggleForceLang);
  327. menu.querySelector('.pref-locale').addEventListener('click', setPreferredLocale);
  328. menu.querySelector('.lang-setting').addEventListener('click', setLangToDownload);
  329. menu.querySelector('.sub-format').addEventListener('click', setSubFormat);
  330. menu.querySelector('.batch-delay').addEventListener('click', setBatchDelay);
  331. setEpTitleInFilename();
  332. setForceText();
  333. setLocaleText();
  334. setLangsText();
  335. setFormatText();
  336. }
  337. // hide menu, at this point sub info is still missing
  338. menu.style.display = 'none';
  339. menu.classList.remove('series');
  340.  
  341. const result = data.video;
  342. const {type, title} = result;
  343. if(type === 'show') {
  344. batchAll = [];
  345. batchSeason = [];
  346. batchToEnd = [];
  347. const allEpisodes = [];
  348. let currentSeason = 0;
  349. menu.classList.add('series');
  350. for(const season of result.seasons) {
  351. for(const episode of season.episodes) {
  352. if(episode.id === result.currentEpisode)
  353. currentSeason = season.seq;
  354. allEpisodes.push([season.seq, episode.seq, episode.id]);
  355. titleCache[episode.id] = {
  356. type, title,
  357. season: season.seq,
  358. episode: episode.seq,
  359. subtitle: episode.title,
  360. hiddenNumber: episode.hiddenEpisodeNumbers
  361. };
  362. }
  363. }
  364.  
  365. allEpisodes.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  366. let toEnd = false;
  367. for(const [season, episode, id] of allEpisodes) {
  368. batchAll.push(id);
  369. if(season === currentSeason)
  370. batchSeason.push(id);
  371. if(id === result.currentEpisode)
  372. toEnd = true;
  373. if(toEnd)
  374. batchToEnd.push(id);
  375. }
  376. }
  377. else if(type === 'movie' || type === 'supplemental') {
  378. titleCache[result.id] = {type, title};
  379. }
  380. else {
  381. console.debug('[Netflix Subtitle Downloader] unknown video type:', type, result)
  382. return;
  383. }
  384. checkSubsCache(menu);
  385. };
  386.  
  387. const getVideoId = () => window.location.pathname.split('/').pop();
  388.  
  389. const getXFromCache = (cache, name, silent) => {
  390. const id = getVideoId();
  391. if(cache.hasOwnProperty(id))
  392. return cache[id];
  393.  
  394. let newID = undefined;
  395. try {
  396. newID = unsafeWindow.netflix.falcorCache.videos[id].current.value[1];
  397. }
  398. catch(ignore) {}
  399. if(typeof newID !== 'undefined' && cache.hasOwnProperty(newID))
  400. return cache[newID];
  401.  
  402. newID = idOverrides[id];
  403. if(typeof newID !== 'undefined' && cache.hasOwnProperty(newID))
  404. return cache[newID];
  405.  
  406. if(silent === true)
  407. return null;
  408.  
  409. alert("Couldn't find the " + name + ". Wait until the player is loaded. If that doesn't help refresh the page.");
  410. throw '';
  411. };
  412.  
  413. const getSubsFromCache = silent => getXFromCache(subCache, 'subs', silent);
  414.  
  415. const pad = (number, letter) => `${letter}${number.toString().padStart(2, '0')}`;
  416.  
  417. const safeTitle = title => title.trim().replace(/[:*?"<>|\\\/]+/g, '_').replace(/ /g, '.');
  418.  
  419. const getTitleFromCache = () => {
  420. const title = getXFromCache(titleCache, 'title');
  421. const titleParts = [title.title];
  422. if(title.type === 'show') {
  423. const season = pad(title.season, 'S');
  424. if(title.hiddenNumber) {
  425. titleParts.push(season);
  426. titleParts.push(title.subtitle);
  427. }
  428. else {
  429. titleParts.push(season + pad(title.episode, 'E'));
  430. if(epTitleInFilename)
  431. titleParts.push(title.subtitle);
  432. }
  433. }
  434. return [safeTitle(titleParts.join('.')), safeTitle(title.title)];
  435. };
  436.  
  437. const pickFormat = formats => {
  438. const preferred = (subFormat === DFXP ? ALL_FORMATS : ALL_FORMATS_prefer_vtt);
  439.  
  440. for(let format of preferred) {
  441. if(typeof formats[format] !== 'undefined')
  442. return formats[format];
  443. }
  444. };
  445.  
  446.  
  447. const _save = async (_zip, title) => {
  448. const content = await _zip.generateAsync({type:'blob'});
  449. saveAs(content, title + '.zip');
  450. };
  451.  
  452. const _download = async _zip => {
  453. const subs = getSubsFromCache();
  454. const [title, seriesTitle] = getTitleFromCache();
  455. const downloaded = [];
  456.  
  457. let filteredLangs;
  458. if(langs === '')
  459. filteredLangs = Object.keys(subs);
  460. else {
  461. const regularExpression = new RegExp(
  462. '^(' + langs
  463. .replace(/\[/g, '\\[')
  464. .replace(/\]/g, '\\]')
  465. .replace(/\-/g, '\\-')
  466. .replace(/\s/g, '')
  467. .replace(/,/g, '|')
  468. + ')'
  469. );
  470. filteredLangs = [];
  471. for(const lang of Object.keys(subs)) {
  472. if(lang.match(regularExpression))
  473. filteredLangs.push(lang);
  474. }
  475. }
  476.  
  477. const progress = new ProgressBar(filteredLangs.length);
  478. let stop = false;
  479. for(const lang of filteredLangs) {
  480. const [urls, extension] = pickFormat(subs[lang]);
  481. while(urls.length > 0) {
  482. let url = popRandomElement(urls);
  483. const resultPromise = fetch(url, {mode: "cors"});
  484. let result;
  485. try {
  486. // Promise.any isn't supported in all browsers, use Promise.race instead
  487. result = await Promise.race([resultPromise, progress.stop, asyncSleep(30, STOP_THE_DOWNLOAD)]);
  488. }
  489. catch(e) {
  490. // the only promise that can be rejected is the one from fetch
  491. // if that happens we want to stop the download anyway
  492. result = STOP_THE_DOWNLOAD;
  493. }
  494. if(result === STOP_THE_DOWNLOAD) {
  495. stop = true;
  496. break;
  497. }
  498. progress.increment();
  499. const data = await result.text();
  500. if(data.length > 0) {
  501. downloaded.push({lang, data, extension});
  502. break;
  503. }
  504. }
  505. if(stop)
  506. break;
  507. }
  508.  
  509. downloaded.forEach(x => {
  510. const {lang, data, extension} = x;
  511. _zip.file(`${title}.WEBRip.Netflix.${lang}.${extension}`, data);
  512. });
  513.  
  514. if(await Promise.race([progress.stop, {}]) === STOP_THE_DOWNLOAD)
  515. stop = true;
  516. progress.destroy();
  517.  
  518. return [seriesTitle, stop];
  519. };
  520.  
  521. const downloadThis = async () => {
  522. const _zip = new JSZip();
  523. const [title, stop] = await _download(_zip);
  524. _save(_zip, title);
  525. };
  526.  
  527. const cleanBatch = async () => {
  528. setBatch(null);
  529. return;
  530. const cache = await caches.open('NSD');
  531. cache.delete('/subs.zip');
  532. await caches.delete('NSD');
  533. }
  534.  
  535. const readAsBinaryString = blob => new Promise(resolve => {
  536. const reader = new FileReader();
  537. reader.onload = function(event) {
  538. resolve(event.target.result);
  539. };
  540. reader.readAsBinaryString(blob);
  541. });
  542.  
  543. const downloadBatch = async auto => {
  544. const cache = await caches.open('NSD');
  545. let zip, title, stop;
  546. if(auto === true) {
  547. try {
  548. const response = await cache.match('/subs.zip');
  549. const blob = await response.blob();
  550. zip = await JSZip.loadAsync(await readAsBinaryString(blob));
  551. }
  552. catch(error) {
  553. console.error(error);
  554. alert('An error occured when loading the zip file with subs from the cache. More info in the browser console.');
  555. await cleanBatch();
  556. return;
  557. }
  558. }
  559. else
  560. zip = new JSZip();
  561.  
  562. try {
  563. [title, stop] = await _download(zip);
  564. }
  565. catch(error) {
  566. title = 'unknown';
  567. stop = true;
  568. }
  569.  
  570. const id = parseInt(getVideoId());
  571. batch = batch.filter(x => x !== id);
  572.  
  573. if(stop || batch.length == 0) {
  574. await _save(zip, title);
  575. await cleanBatch();
  576. }
  577. else {
  578. setBatch(batch);
  579. cache.put('/subs.zip', new Response(await zip.generateAsync({type:'blob'})));
  580. await asyncSleep(batchDelay);
  581. window.location = window.location.origin + '/watch/' + batch[0];
  582. }
  583. };
  584.  
  585. const downloadAll = () => {
  586. batch = batchAll;
  587. downloadBatch();
  588. };
  589.  
  590. const downloadSeason = () => {
  591. batch = batchSeason;
  592. downloadBatch();
  593. };
  594.  
  595. const downloadToEnd = () => {
  596. batch = batchToEnd;
  597. downloadBatch();
  598. };
  599.  
  600. const processMessage = e => {
  601. const {type, data} = e.detail;
  602. if(type === 'subs')
  603. processSubInfo(data);
  604. else if(type === 'id_override')
  605. idOverrides[data[0]] = data[1];
  606. else if(type === 'metadata')
  607. processMetadata(data);
  608. }
  609.  
  610. const injection = (ALL_FORMATS) => {
  611. const MANIFEST_PATTERN = new RegExp('manifest|licensedManifest');
  612. const forceSubs = localStorage.getItem('NSD_force-all-lang') !== 'false';
  613. const prefLocale = localStorage.getItem('NSD_pref-locale') || '';
  614.  
  615. // hide the menu when we go back to the browse list
  616. window.addEventListener('popstate', () => {
  617. const display = (document.location.pathname.split('/')[1] === 'watch' ? '' : 'none');
  618. const menu = document.querySelector('#subtitle-downloader-menu');
  619. menu.style.display = display;
  620. });
  621.  
  622. // hijack JSON.parse and JSON.stringify functions
  623. ((parse, stringify, open, realFetch) => {
  624. JSON.parse = function (text) {
  625. const data = parse(text);
  626.  
  627. if (data && data.result && data.result.timedtexttracks && data.result.movieId) {
  628. window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: {type: 'subs', data: data.result}}));
  629. }
  630. return data;
  631. };
  632.  
  633. JSON.stringify = function (data) {
  634. /*{
  635. let text = stringify(data);
  636. if (text.includes('dfxp-ls-sdh'))
  637. console.log(text, data);
  638. }*/
  639.  
  640. if (data && typeof data.url === 'string' && data.url.search(MANIFEST_PATTERN) > -1) {
  641. for (let v of Object.values(data)) {
  642. try {
  643. if (v.profiles) {
  644. for(const profile_name of ALL_FORMATS) {
  645. if(!v.profiles.includes(profile_name)) {
  646. v.profiles.unshift(profile_name);
  647. }
  648. }
  649. }
  650. if (v.showAllSubDubTracks != null && forceSubs)
  651. v.showAllSubDubTracks = true;
  652. if (prefLocale !== '')
  653. v.preferredTextLocale = prefLocale;
  654. }
  655. catch (e) {
  656. if (e instanceof TypeError)
  657. continue;
  658. else
  659. throw e;
  660. }
  661. }
  662. }
  663. if(data && typeof data.movieId === 'number') {
  664. try {
  665. let videoId = data.params.sessionParams.uiplaycontext.video_id;
  666. if(typeof videoId === 'number' && videoId !== data.movieId)
  667. window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: {type: 'id_override', data: [videoId, data.movieId]}}));
  668. }
  669. catch(ignore) {}
  670. }
  671. return stringify(data);
  672. };
  673.  
  674. XMLHttpRequest.prototype.open = function() {
  675. if(arguments[1] && arguments[1].includes('/metadata?'))
  676. this.addEventListener('load', async () => {
  677. let data = this.response;
  678. if(data instanceof Blob)
  679. data = JSON.parse(await data.text());
  680. else if(typeof data === "string")
  681. data = JSON.parse(data);
  682. window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: {type: 'metadata', data: data}}));
  683. }, false);
  684. open.apply(this, arguments);
  685. };
  686.  
  687. window.fetch = async (...args) => {
  688. const response = realFetch(...args);
  689. if(args[0] && args[0].includes('/metadata?')) {
  690. const copied = (await response).clone();
  691. const data = await copied.json();
  692. window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: {type: 'metadata', data: data}}));
  693. }
  694. return response;
  695. };
  696. })(JSON.parse, JSON.stringify, XMLHttpRequest.prototype.open, window.fetch);
  697. }
  698.  
  699. window.addEventListener('netflix_sub_downloader_data', processMessage, false);
  700.  
  701. // inject script
  702. const sc = document.createElement('script');
  703. sc.innerHTML = '(' + injection.toString() + ')(' + JSON.stringify(ALL_FORMATS) + ')';
  704. document.head.appendChild(sc);
  705. document.head.removeChild(sc);
  706.  
  707. // add CSS style
  708. const s = document.createElement('style');
  709. s.innerHTML = SCRIPT_CSS;
  710. document.head.appendChild(s);
  711.  
  712. const observer = new MutationObserver(function(mutations) {
  713. mutations.forEach(function(mutation) {
  714. mutation.addedNodes.forEach(function(node) {
  715. // add scrollbar - Netflix doesn't expect you to have this manu languages to choose from...
  716. try {
  717. (node.parentNode || node).querySelector('.watch-video--selector-audio-subtitle').parentNode.style.overflowY = 'scroll';
  718. }
  719. catch(ignore) {}
  720. });
  721. });
  722. });
  723. observer.observe(document.body, { childList: true, subtree: true });