Greasy Fork is available in English.

Netflix - subtitle downloader

Allows you to download subtitles from Netflix

Versión del día 02/02/2020. Echa un vistazo a la versión más reciente.

  1. // ==UserScript==
  2. // @name Netflix - subtitle downloader
  3. // @description Allows you to download subtitles from Netflix
  4. // @license MIT
  5. // @version 3.0.11
  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. const MAIN_TITLE = '.player-status-main-title, .ellipsize-text>h4, .video-title>h4';
  14. const TRACK_MENU = '#player-menu-track-settings, .audio-subtitle-controller';
  15. const NEXT_EPISODE = '.player-next-episode:not(.player-hidden), .button-nfplayerNextEpisode';
  16.  
  17. const WEBVTT = 'webvtt-lssdh-ios8';
  18.  
  19. const DOWNLOAD_MENU = `<lh class="list-header">Netflix subtitle downloader</lh>
  20. <li class="list-header">Netflix subtitle downloader</li>
  21. <li class="track download">Download subs for this episode</li>
  22. <li class="track download-all">Download subs from this ep till last available</li>`;
  23.  
  24. const SCRIPT_CSS = `.player-timed-text-tracks, .track-list-subtitles{ border-right:1px solid #000 }
  25. .player-timed-text-tracks+.player-timed-text-tracks, .track-list-subtitles+.track-list-subtitles{ border-right:0 }
  26. .subtitle-downloader-menu { list-style:none }
  27. #player-menu-track-settings .subtitle-downloader-menu li.list-header,
  28. .audio-subtitle-controller .subtitle-downloader-menu lh.list-header{ display:none }`;
  29.  
  30. const SUB_TYPES = {
  31. 'subtitles': '',
  32. 'closedcaptions': '[cc]'
  33. };
  34.  
  35. let zip;
  36. let subCache = {};
  37. let batch = false;
  38.  
  39. const randomProperty = obj => {
  40. const keys = Object.keys(obj);
  41. return obj[keys[keys.length * Math.random() << 0]];
  42. };
  43.  
  44. // get show name or full name with episode number
  45. const __getTitle = full => {
  46. if(typeof full === 'undefined')
  47. full = true;
  48. const titleElement = document.querySelector(MAIN_TITLE);
  49. if(titleElement === null)
  50. return null;
  51. const title = [titleElement.textContent.replace(/[:*?"<>|\\\/]+/g, '_').replace(/ /g, '.')];
  52. if(full) {
  53. const episodeElement = titleElement.nextElementSibling;
  54. if(episodeElement) {
  55. const m = episodeElement.textContent.match(/^[^\d]*(\d+)[^\d]+(\d+)[^\d]*$/);
  56. if(m && m.length == 3) {
  57. title.push(`S${m[1].padStart(2, '0')}E${m[2].padStart(2, '0')}`);
  58. }
  59. else {
  60. title.push(episodeElement.textContent.trim().replace(/[:*?"<>|\\\/]+/g, '_').replace(/ /g, '.'));
  61. }
  62. }
  63. title.push('WEBRip.Netflix');
  64. }
  65. return title.join('.');
  66. };
  67. // helper function, periodically checking for the title and resolving promise if found
  68. const _getTitle = (full, resolve) => {
  69. const title = __getTitle(full);
  70. if(title === null)
  71. window.setTimeout(_getTitle, 200, full, resolve);
  72. else
  73. resolve(title);
  74. };
  75. // promise of a title
  76. const getTitle = full => new Promise(resolve => {
  77. _getTitle(full, resolve);
  78. });
  79.  
  80. const processSubInfo = async result => {
  81. const tracks = result.timedtexttracks;
  82. const titleP = getTitle();
  83. const subs = {};
  84. for(const track of tracks) {
  85. if(track.isNoneTrack)
  86. continue;
  87. if(typeof track.ttDownloadables[WEBVTT] === 'undefined')
  88. continue;
  89.  
  90. let type = SUB_TYPES[track.rawTrackType];
  91. if(typeof type === 'undefined')
  92. type = `[${track.rawTrackType}]`;
  93. const lang = track.language + type + (track.isForcedNarrative ? '-forced' : '');
  94. subs[lang] = randomProperty(track.ttDownloadables[WEBVTT].downloadUrls);
  95. }
  96. subCache[result.movieId] = {titleP, subs};
  97.  
  98. if(batch) {
  99. downloadAll();
  100. }
  101. };
  102.  
  103. const getMovieID = () => window.location.pathname.split('/').pop();
  104.  
  105.  
  106. const _save = async (_zip, title) => {
  107. const content = await _zip.generateAsync({type:'blob'});
  108. saveAs(content, title + '.zip');
  109. };
  110.  
  111. const _download = async _zip => {
  112. const showTitle = getTitle(false);
  113. const {titleP, subs} = subCache[getMovieID()];
  114. const downloaded = [];
  115. for(const [lang, url] of Object.entries(subs)) {
  116. const result = await fetch(url, {mode: "cors"});
  117. const data = await result.text();
  118. downloaded.push({lang, data});
  119. }
  120. const title = await titleP;
  121.  
  122. downloaded.forEach(x => {
  123. const {lang, data} = x;
  124. _zip.file(`${title}.${lang}.vtt`, data);
  125. });
  126.  
  127. return await showTitle;
  128. };
  129.  
  130. const downloadThis = async () => {
  131. const _zip = new JSZip();
  132. const showTitle = await _download(_zip);
  133. _save(_zip, showTitle);
  134. };
  135.  
  136. const downloadAll = async () => {
  137. zip = zip || new JSZip();
  138. batch = true;
  139. const showTitle = await _download(zip);
  140. const nextEp = document.querySelector(NEXT_EPISODE);
  141. if(nextEp)
  142. nextEp.click();
  143. else {
  144. await _save(zip, showTitle);
  145. zip = undefined;
  146. batch = false;
  147. }
  148. };
  149.  
  150. const processMessage = e => {
  151. processSubInfo(e.detail);
  152. }
  153.  
  154. const injection = () => {
  155. const WEBVTT = 'webvtt-lssdh-ios8';
  156. const MANIFEST_URL = "/manifest";
  157.  
  158. // hijack JSON.parse and JSON.stringify functions
  159. ((parse, stringify) => {
  160. JSON.parse = function (text) {
  161. const data = parse(text);
  162. if (data && data.result && data.result.timedtexttracks && data.result.movieId) {
  163. window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: data.result}));
  164. }
  165. return data;
  166. };
  167. JSON.stringify = function (data) {
  168. if (data && data.url === MANIFEST_URL) {
  169. for (let v of Object.values(data)) {
  170. try {
  171. if (v.profiles)
  172. v.profiles.unshift(WEBVTT);
  173. if (v.showAllSubDubTracks != null)
  174. v.showAllSubDubTracks = true;
  175. }
  176. catch (e) {
  177. if (e instanceof TypeError)
  178. continue;
  179. else
  180. throw e;
  181. }
  182. }
  183. }
  184. return stringify(data);
  185. };
  186. })(JSON.parse, JSON.stringify);
  187. }
  188.  
  189. window.addEventListener('netflix_sub_downloader_data', processMessage, false);
  190.  
  191. // inject script
  192. const sc = document.createElement('script');
  193. sc.innerHTML = '(' + injection.toString() + ')()';
  194. document.head.appendChild(sc);
  195. document.head.removeChild(sc);
  196.  
  197. // add CSS style
  198. const s = document.createElement('style');
  199. s.innerHTML = SCRIPT_CSS;
  200. document.head.appendChild(s);
  201.  
  202. // add menu when it's not there
  203. const observer = new MutationObserver(function(mutations) {
  204. mutations.forEach(function(mutation) {
  205. mutation.addedNodes.forEach(function(node) {
  206. if(node.nodeName.toUpperCase() == 'DIV') {
  207. let trackMenu = (node.parentNode || node).querySelector(TRACK_MENU);
  208. if(trackMenu !== null && trackMenu.querySelector('.subtitle-downloader-menu') === null) {
  209. let ol = document.createElement('ol');
  210. ol.setAttribute('class', 'subtitle-downloader-menu player-timed-text-tracks track-list track-list-subtitles');
  211. ol.innerHTML = DOWNLOAD_MENU;
  212. trackMenu.appendChild(ol);
  213. ol.querySelector('.download').addEventListener('click', downloadThis);
  214. ol.querySelector('.download-all').addEventListener('click', downloadAll);
  215. }
  216. }
  217. });
  218. });
  219. });
  220. observer.observe(document.body, { childList: true, subtree: true });