Netflix - subtitle downloader

Allows you to download subtitles from Netflix

Stan na 08-11-2024. Zobacz najnowsza wersja.

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