basti1012.bplaced.net

Du siehst gerade eine vereinfachte Darstellung unserer Inhalte. Normale Ansicht mit richtiger Formatierung.
    Ermöglicht das Herunterladen von Untertiteln von Netflix

    Code

                                        
                                    // ==UserScript==
    // @name        Netflix - subtitle downloader
    // @description Allows you to download subtitles from Netflix
    // @license     MIT
    // @version     4.1.0
    // @namespace   tithen-firion.github.io
    // @include     https://www.netflix.com/*
    // @grant       unsafeWindow
    // @require     https://cdn.jsdelivr.net/npm/jszip@3.7.1/dist/jszip.min.js
    // @require     https://cdn.jsdelivr.net/npm/file-saver-es@2.0.5/dist/FileSaver.min.js
    // ==/UserScript==
     
    class ProgressBar {
      constructor(max) {
        this.current = 0;
        this.max = max;
     
        let container = document.querySelector('#userscript_progress_bars');
        if(container === null) {
          container = document.createElement('div');
          container.id = 'userscript_progress_bars'
          document.body.appendChild(container)
          container.style
          container.style.position = 'fixed';
          container.style.top = 0;
          container.style.left = 0;
          container.style.width = '100%';
          container.style.background = 'red';
          container.style.zIndex = '99999999';
        }
     
        this.progressElement = document.createElement('div');
        this.progressElement.innerHTML = 'Click to stop';
        this.progressElement.style.cursor = 'pointer';
        this.progressElement.style.fontSize = '16px';
        this.progressElement.style.textAlign = 'center';
        this.progressElement.style.width = '100%';
        this.progressElement.style.height = '20px';
        this.progressElement.style.background = 'transparent';
        this.stop = new Promise(resolve => {
          this.progressElement.addEventListener('click', () => {resolve(STOP_THE_DOWNLOAD)});
        });
     
        container.appendChild(this.progressElement);
      }
     
      increment() {
        this.current += 1;
        if(this.current <= this.max) {
          let p = this.current / this.max * 100;
          this.progressElement.style.background = `linear-gradient(to right, green ${p}%, transparent ${p}%)`;
        }
      }
     
      destroy() {
        this.progressElement.remove();
      }
    }
     
    const STOP_THE_DOWNLOAD = 'NETFLIX_SUBTITLE_DOWNLOADER_STOP_THE_DOWNLOAD';
     
    const WEBVTT = 'webvtt-lssdh-ios8';
    const DFXP = 'dfxp-ls-sdh';
    const SIMPLE = 'simplesdh';
    const ALL_FORMATS = [WEBVTT, DFXP, SIMPLE];
     
    const FORMAT_NAMES = {};
    FORMAT_NAMES[WEBVTT] = 'WebVTT';
    FORMAT_NAMES[DFXP] = 'DFXP/XML';
     
    const EXTENSIONS = {};
    EXTENSIONS[WEBVTT] = 'vtt';
    EXTENSIONS[DFXP] = 'dfxp';
    EXTENSIONS[SIMPLE] = 'xml';
     
    const DOWNLOAD_MENU = `
    <ol>
    <li class="header">Netflix subtitle downloader</li>
    <li class="download">Download subs for this <span class="series">episode</span><span class="not-series">movie</span></li>
    <li class="download-to-end series">Download subs from this ep till last available</li>
    <li class="download-season series">Download subs for this season</li>
    <li class="download-all series">Download subs for all seasons</li>
    <li class="ep-title-in-filename">Add episode title to filename: <span></span></li>
    <li class="force-all-lang">Force Netflix to show all languages: <span></span></li>
    <li class="lang-setting">Languages to download: <span></span></li>
    <li class="sub-format">Subtitle format: prefer <span></span></li>
    </ol>
    `;
     
    const SCRIPT_CSS = `
    #subtitle-downloader-menu {
      position: absolute;
      display: none;
      width: 300px;
      top: 0;
      left: calc( 50% - 150px );
    }
    #subtitle-downloader-menu ol {
      list-style: none;
      position: relative;
      width: 300px;
      background: #333;
      color: #fff;
      padding: 0;
      margin: auto;
      font-size: 12px;
      z-index: 99999998;
    }
    body:hover #subtitle-downloader-menu { display: block; }
    #subtitle-downloader-menu li { padding: 10px; }
    #subtitle-downloader-menu li.header { font-weight: bold; }
    #subtitle-downloader-menu li:not(.header):hover { background: #666; }
    #subtitle-downloader-menu li:not(.header) {
      display: none;
      cursor: pointer;
    }
    #subtitle-downloader-menu:hover li { display: block; }
     
    #subtitle-downloader-menu:not(.series) .series{ display: none; }
    #subtitle-downloader-menu.series .not-series{ display: none; }
    `;
     
    const SUB_TYPES = {
      'subtitles': '',
      'closedcaptions': '[cc]'
    };
     
    let idOverrides = {};
    let subCache = {};
    let titleCache = {};
     
    let batch = null;
    try {
      batch = JSON.parse(sessionStorage.getItem('NSD_batch'));
    }
    catch(ignore) {}
     
    let batchAll = null;
    let batchSeason = null;
    let batchToEnd = null;
     
    let epTitleInFilename = localStorage.getItem('NSD_ep-title-in-filename') === 'true';
    let forceSubs = localStorage.getItem('NSD_force-all-lang') !== 'false';
    let langs = localStorage.getItem('NSD_lang-setting') || '';
    let subFormat = localStorage.getItem('NSD_sub-format') || WEBVTT;
     
    const setEpTitleInFilename = () => {
      document.querySelector('#subtitle-downloader-menu .ep-title-in-filename > span').innerHTML = (epTitleInFilename ? 'on' : 'off');
    };
    const setForceText = () => {
      document.querySelector('#subtitle-downloader-menu .force-all-lang > span').innerHTML = (forceSubs ? 'on' : 'off');
    };
    const setLangsText = () => {
      document.querySelector('#subtitle-downloader-menu .lang-setting > span').innerHTML = (langs === '' ? 'all' : langs);
    };
    const setFormatText = () => {
      document.querySelector('#subtitle-downloader-menu .sub-format > span').innerHTML = FORMAT_NAMES[subFormat];
    };
     
    const setBatch = b => {
      if(b === null)
        sessionStorage.removeItem('NSD_batch');
      else
        sessionStorage.setItem('NSD_batch', JSON.stringify(b));
    };
     
    const toggleEpTitleInFilename = () => {
      epTitleInFilename = !epTitleInFilename;
      if(epTitleInFilename)
        localStorage.setItem('NSD_ep-title-in-filename', epTitleInFilename);
      else
        localStorage.removeItem('NSD_ep-title-in-filename');
      setEpTitleInFilename();
    };
    const toggleForceLang = () => {
      forceSubs = !forceSubs;
      if(forceSubs)
        localStorage.removeItem('NSD_force-all-lang');
      else
        localStorage.setItem('NSD_force-all-lang', forceSubs);
      document.location.reload();
    };
    const setLangToDownload = () => {
      const result = prompt('Languages to download, comma separated. Leave empty to download all subtitles.\nExample: en,de,fr', langs);
      if(result !== null) {
        langs = result;
        if(langs === '')
          localStorage.removeItem('NSD_lang-setting');
        else
          localStorage.setItem('NSD_lang-setting', langs);
        setLangsText();
      }
    };
    const setSubFormat = () => {
      if(subFormat === WEBVTT) {
        localStorage.setItem('NSD_sub-format', DFXP);
        subFormat = DFXP;
      }
      else {
        localStorage.removeItem('NSD_sub-format');
        subFormat = WEBVTT;
      }
      setFormatText();
    };
     
    const asyncSleep = (seconds, value) => new Promise(resolve => {
      window.setTimeout(resolve, seconds * 1000, value);
    });
     
    const popRandomElement = arr => {
      return arr.splice(arr.length * Math.random() << 0, 1)[0];
    };
     
    const processSubInfo = async result => {
      const tracks = result.timedtexttracks;
      const subs = {};
      for(const track of tracks) {
        if(track.isNoneTrack)
          continue;
     
        let type = SUB_TYPES[track.rawTrackType];
        if(typeof type === 'undefined')
          type = `[${track.rawTrackType}]`;
        const lang = track.language + type + (track.isForcedNarrative ? '-forced' : '');
     
        const formats = {};
        for(let format of ALL_FORMATS) {
          if(typeof track.ttDownloadables[format] !== 'undefined')
            formats[format] = [Object.values(track.ttDownloadables[format].downloadUrls), EXTENSIONS[format]];
        }
     
        if(Object.keys(formats).length > 0)
          subs[lang] = formats;
      }
      subCache[result.movieId] = subs;
    };
     
    const checkSubsCache = async menu => {
      while(getSubsFromCache(true) === null) {
        await asyncSleep(0.1);
      }
     
      // show menu if on watch page
      menu.style.display = (document.location.pathname.split('/')[1] === 'watch' ? '' : 'none');
     
      if(batch !== null && batch.length > 0) {
        downloadBatch(true);
      }
    };
     
    const processMetadata = data => {
      // add menu when it's not there
      let menu = document.querySelector('#subtitle-downloader-menu');
      if(menu === null) {
        menu = document.createElement('div');
        menu.id = 'subtitle-downloader-menu';
        menu.innerHTML = DOWNLOAD_MENU;
        document.body.appendChild(menu);
        menu.querySelector('.download').addEventListener('click', downloadThis);
        menu.querySelector('.download-to-end').addEventListener('click', downloadToEnd);
        menu.querySelector('.download-season').addEventListener('click', downloadSeason);
        menu.querySelector('.download-all').addEventListener('click', downloadAll);
        menu.querySelector('.ep-title-in-filename').addEventListener('click', toggleEpTitleInFilename);
        menu.querySelector('.force-all-lang').addEventListener('click', toggleForceLang);
        menu.querySelector('.lang-setting').addEventListener('click', setLangToDownload);
        menu.querySelector('.sub-format').addEventListener('click', setSubFormat);
        setEpTitleInFilename();
        setForceText();
        setLangsText();
        setFormatText();
      }
      // hide menu, at this point sub info is still missing
      menu.style.display = 'none';
      menu.classList.remove('series');
     
      const result = data.video;
      const {type, title} = result;
      if(type === 'show') {
        batchAll = [];
        batchSeason = [];
        batchToEnd = [];
        const allEpisodes = [];
        let currentSeason = 0;
        menu.classList.add('series');
        for(const season of result.seasons) {
          for(const episode of season.episodes) {
            if(episode.id === result.currentEpisode)
              currentSeason = season.seq;
            allEpisodes.push([season.seq, episode.seq, episode.id]);
            titleCache[episode.id] = {
              type, title,
              season: season.seq,
              episode: episode.seq,
              subtitle: episode.title,
              hiddenNumber: episode.hiddenEpisodeNumbers
            };
          }
        }
     
        allEpisodes.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
        let toEnd = false;
        for(const [season, episode, id] of allEpisodes) {
          batchAll.push(id);
          if(season === currentSeason)
            batchSeason.push(id);
          if(id === result.currentEpisode)
            toEnd = true;
          if(toEnd)
            batchToEnd.push(id);
        }
      }
      else if(type === 'movie' || type === 'supplemental') {
        titleCache[result.id] = {type, title};
      }
      else {
      	console.debug('[Netflix Subtitle Downloader] unknown video type:', type, result)
        return;
      }
      checkSubsCache(menu);
    };
     
    const getVideoId = () => window.location.pathname.split('/').pop();
     
    const getXFromCache = (cache, name, silent) => {
      const id = getVideoId();
      if(cache.hasOwnProperty(id))
        return cache[id];
     
      let newID = undefined;
      try {
        newID = unsafeWindow.netflix.falcorCache.videos[id].current.value[1];
      }
      catch(ignore) {}
      if(typeof newID !== 'undefined' && cache.hasOwnProperty(newID))
        return cache[newID];
     
      newID = idOverrides[id];
      if(typeof newID !== 'undefined' && cache.hasOwnProperty(newID))
        return cache[newID];
     
      if(silent === true)
        return null;
     
      alert("Couldn't find the " + name + ". Wait until the player is loaded. If that doesn't help refresh the page.");
      throw '';
    };
     
    const getSubsFromCache = silent => getXFromCache(subCache, 'subs', silent);
     
    const pad = (number, letter) => `${letter}${number.toString().padStart(2, '0')}`;
     
    const safeTitle = title => title.trim().replace(/[:*?"<>|\\\/]+/g, '_').replace(/ /g, '.');
     
    const getTitleFromCache = () => {
      const title = getXFromCache(titleCache, 'title');
      const titleParts = [title.title];
      if(title.type === 'show') {
        const season = pad(title.season, 'S');
        if(title.hiddenNumber) {
          titleParts.push(season);
          titleParts.push(title.subtitle);
        }
        else {
          titleParts.push(season + pad(title.episode, 'E'));
          if(epTitleInFilename)
            titleParts.push(title.subtitle);
        }
      }
      return [safeTitle(titleParts.join('.')), safeTitle(title.title)];
    };
     
    const pickFormat = formats => {
      const preferred = ALL_FORMATS.slice();
      if(subFormat === DFXP)
        preferred.push(preferred.shift());
     
      for(let format of preferred) {
        if(typeof formats[format] !== 'undefined')
          return formats[format];
      }
    };
     
     
    const _save = async (_zip, title) => {
      const content = await _zip.generateAsync({type:'blob'});
      saveAs(content, title + '.zip');
    };
     
    const _download = async _zip => {
      const subs = getSubsFromCache();
      const [title, seriesTitle] = getTitleFromCache();
      const downloaded = [];
     
      let filteredLangs;
      if(langs === '')
        filteredLangs = Object.keys(subs);
      else {
        const regularExpression = new RegExp(
          '^(' + langs
            .replace(/\[/g, '\\[')
            .replace(/\]/g, '\\]')
            .replace(/\-/g, '\\-')
            .replace(/\s/g, '')
            .replace(/,/g, '|')
          + ')'
        );
        filteredLangs = [];
        for(const lang of Object.keys(subs)) {
          if(lang.match(regularExpression))
            filteredLangs.push(lang);
        }
      }
     
      const progress = new ProgressBar(filteredLangs.length);
      let stop = false;
      for(const lang of filteredLangs) {
        const [urls, extension] = pickFormat(subs[lang]);
        while(urls.length > 0) {
          let url = popRandomElement(urls);
          const resultPromise = fetch(url, {mode: "cors"});
          let result;
          try {
            // Promise.any isn't supported in all browsers, use Promise.race instead
            result = await Promise.race([resultPromise, progress.stop, asyncSleep(30, STOP_THE_DOWNLOAD)]);
          }
          catch(e) {
            // the only promise that can be rejected is the one from fetch
            // if that happens we want to stop the download anyway
            result = STOP_THE_DOWNLOAD;
          }
          if(result === STOP_THE_DOWNLOAD) {
            stop = true;
            break;
          }
          progress.increment();
          const data = await result.text();
          if(data.length > 0) {
            downloaded.push({lang, data, extension});
            break;
          }
        }
        if(stop)
          break;
      }
     
      downloaded.forEach(x => {
        const {lang, data, extension} = x;
        _zip.file(`${title}.WEBRip.Netflix.${lang}.${extension}`, data);
      });
     
      if(await Promise.race([progress.stop, {}]) === STOP_THE_DOWNLOAD)
        stop = true;
      progress.destroy();
     
      return [seriesTitle, stop];
    };
     
    const downloadThis = async () => {
      const _zip = new JSZip();
      const [title, stop] = await _download(_zip);
      _save(_zip, title);
    };
     
    const cleanBatch = async () => {
      setBatch(null);
      return;
      const cache = await caches.open('NSD');
      cache.delete('/subs.zip');
      await caches.delete('NSD');
    }
     
    const readAsBinaryString = blob => new Promise(resolve => {
      const reader = new FileReader();
      reader.onload = function(event) {
        resolve(event.target.result);
      };
      reader.readAsBinaryString(blob);
    });
     
    const downloadBatch = async auto => {
      const cache = await caches.open('NSD');
      let zip, title, stop;
      if(auto === true) {
        try {
          const response = await cache.match('/subs.zip');
          const blob = await response.blob();
          zip = await JSZip.loadAsync(await readAsBinaryString(blob));
        }
        catch(error) {
          console.error(error);
          alert('An error occured when loading the zip file with subs from the cache. More info in the browser console.');
          await cleanBatch();
          return;
        }
      }
      else
        zip = new JSZip();
     
      try {
        [title, stop] = await _download(zip);
      }
      catch(error) {
        title = 'unknown';
    		stop = true;
      }
     
      const id = parseInt(getVideoId());
      batch = batch.filter(x => x !== id);
     
      if(stop || batch.length == 0) {
        await _save(zip, title);
        await cleanBatch();
      }
      else {
        setBatch(batch);
        cache.put('/subs.zip', new Response(await zip.generateAsync({type:'blob'})));
        window.location = window.location.origin + '/watch/' + batch[0];
      }
    };
     
    const downloadAll = () => {
      batch = batchAll;
      downloadBatch();
    };
     
    const downloadSeason = () => {
      batch = batchSeason;
      downloadBatch();
    };
     
    const downloadToEnd = () => {
      batch = batchToEnd;
      downloadBatch();
    };
     
    const processMessage = e => {
      const {type, data} = e.detail;
      if(type === 'subs')
        processSubInfo(data);
      else if(type === 'id_override')
        idOverrides[data[0]] = data[1];
      else if(type === 'metadata')
        processMetadata(data);
    }
     
    const injection = () => {
      const WEBVTT = 'webvtt-lssdh-ios8';
      const MANIFEST_PATTERN = new RegExp('manifest|licensedManifest');
      const forceSubs = localStorage.getItem('NSD_force-all-lang') !== 'false';
     
      // hide the menu when we go back to the browse list
      window.addEventListener('popstate', () => {
        const display = (document.location.pathname.split('/')[1] === 'watch' ? '' : 'none');
        const menu = document.querySelector('#subtitle-downloader-menu');
        menu.style.display = display;
      });
     
      // hijack JSON.parse and JSON.stringify functions
      ((parse, stringify, open) => {
        JSON.parse = function (text) {
          const data = parse(text);
     
          if (data && data.result && data.result.timedtexttracks && data.result.movieId) {
            window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: {type: 'subs', data: data.result}}));
          }
          return data;
        };
     
        JSON.stringify = function (data) {
          /*{
            let text = stringify(data);
            if (text.includes('dfxp-ls-sdh'))
              console.log(text, data);
          }*/
     
          if (data && typeof data.url === 'string' && data.url.search(MANIFEST_PATTERN) > -1) {
            for (let v of Object.values(data)) {
              try {
                if (v.profiles)
                  v.profiles.unshift(WEBVTT);
                if (v.showAllSubDubTracks != null && forceSubs)
                  v.showAllSubDubTracks = true;
              }
              catch (e) {
                if (e instanceof TypeError)
                  continue;
                else
                  throw e;
              }
            }
          }
          if(data && typeof data.movieId === 'number') {
            try {
              let videoId = data.params.sessionParams.uiplaycontext.video_id;
              if(typeof videoId === 'number' && videoId !== data.movieId)
                window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: {type: 'id_override', data: [videoId, data.movieId]}}));
            }
            catch(ignore) {}
          }
          return stringify(data);
        };
     
        XMLHttpRequest.prototype.open = function() {
          if(arguments[1] && arguments[1].includes('/metadata?'))
            this.addEventListener('load', () => {
              window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: {type: 'metadata', data: this.response}}));
            }, false);
          open.apply(this, arguments);
        };
      })(JSON.parse, JSON.stringify, XMLHttpRequest.prototype.open);
    }
     
    window.addEventListener('netflix_sub_downloader_data', processMessage, false);
     
    // inject script
    const sc = document.createElement('script');
    sc.innerHTML = '(' + injection.toString() + ')()';
    document.head.appendChild(sc);
    document.head.removeChild(sc);
     
    // add CSS style
    const s = document.createElement('style');
    s.innerHTML = SCRIPT_CSS;
    document.head.appendChild(s);
     
    const observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        mutation.addedNodes.forEach(function(node) {
          // add scrollbar - Netflix doesn't expect you to have this manu languages to choose from...
          try {
            (node.parentNode || node).querySelector('.watch-video--selector-audio-subtitle').parentNode.style.overflowY = 'scroll';
          }
          catch(ignore) {}
        });
      });
    });
    observer.observe(document.body, { childList: true, subtree: true });