text: #!/usr/bin/env node
  
  // modules build-in
  const fs = require('fs');
  const path = require('path');
  
  // package json
  const packageJson = require('./package.json');
  
  // program name
  console.log(`\n=== Funimation Downloader NX ${packageJson.version} ===\n`);
  const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
  
  // modules extra
  const yaml = require('yaml');
  const shlp = require('sei-helper');
  const { lookpath } = require('lookpath');
  const m3u8 = require('m3u8-parsed');
  const streamdl = require('hls-download');
  const crypto = require("crypto");
  const got = require('got');
  
  // extra
  const appYargs = require('./modules/module.app-args');
  const getYamlCfg = require('./modules/module.cfg-loader');
  const getData = require('./modules/module.getdata.js');
  const vttConvert = require('./modules/module.vttconvert');
  
  // new-cfg
  const cfgFolder  = path.join(__dirname, '/config');
  const binCfgFile = path.join(cfgFolder, 'bin-path');
  const dirCfgFile = path.join(cfgFolder, 'dir-path');
  const cliCfgFile = path.join(cfgFolder, 'cli-defaults');
  const tokenFile  = path.join(cfgFolder, 'token');
  
  // params
  let cfg = {
      bin: getYamlCfg(binCfgFile),
      dir: getYamlCfg(dirCfgFile),
      cli: getYamlCfg(cliCfgFile),
  };
  
  // token
  let token = getYamlCfg(tokenFile);
  token = token.token ? token.token : false;
  
  // info if token not set
  if(!token){
      console.log('[INFO] Token not set!\n');
  }
  
  // cli
  const argv = appYargs.appArgv(cfg.cli);
  
  // check page
  if(!isNaN(parseInt(argv.p, 10)) && parseInt(argv.p, 10) > 0){
      argv.p = parseInt(argv.p, 10);
  }
  else{
      argv.p = 1;
  }
  
  // fn variables
  let fnTitle = '',
      fnEpNum = '',
      fnSuffix = '',
      fnOutput = '',
      tsDlPath = false,
      stDlPath = false,
      batchDL = false;
  
  // select mode
  if(argv.auth){
      auth();
  }
  else if(argv.search){
      searchShow();
  }
  else if(argv.s && !isNaN(parseInt(argv.s, 10)) && parseInt(argv.s, 10) > 0){
      getShow();
  }
  else{
      appYargs.showHelp();
      process.exit();
  }
  
  // auth
  async function auth(){
      let authOpts = {};
      authOpts.user = await shlp.question('[Q] LOGIN/EMAIL');
      authOpts.pass = await shlp.question('[Q] PASSWORD   ');
      let authData =  await getData({
          baseUrl: api_host,
          url: '/auth/login/',
          useProxy: true,
          auth: authOpts,
          debug: argv.debug,
      });
      if(authData.ok){
          authData = JSON.parse(authData.res.body);
          if(authData.token){
              console.log('[INFO] Authentication success, your token: %s%s\n', authData.token.slice(0,8),'*'.repeat(32));
              fs.writeFileSync(tokenFile + '.yml', yaml.stringify({'token': authData.token}));
          }
          else if(authData.error){
              console.log('[ERROR]%s\n', authData.error);
              process.exit(1);
          }
      }
  }
  
  // search show
  async function searchShow(){
      let qs = {unique: true, limit: 100, q: argv.search, offset: (argv.p-1)*1000 };
      let searchData = await getData({
          baseUrl: api_host,
          url: '/source/funimation/search/auto/',
          querystring: qs,
          token: token,
          useToken: true,
          useProxy: true,
          debug: argv.debug,
      });
      if(!searchData.ok){return;}
      searchData = JSON.parse(searchData.res.body);
      if(searchData.detail){
          console.log(`[ERROR] ${searchData.detail}`);
          return;
      }
      if(searchData.items && searchData.items.hits){
          let shows = searchData.items.hits;
          console.log('[INFO] Search Results:');
          for(let ssn in shows){
              console.log(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:''));
          }
      }
      console.log('[INFO] Total shows found: %s\n',searchData.count);
  }
  
  // get show
  async function getShow(){
      // show main data
      let showData = await getData({
          baseUrl: api_host,
          url: `/source/catalog/title/${parseInt(argv.s, 10)}`,
          token: token,
          useToken: true,
          useProxy: true,
          debug: argv.debug,
      });
      // check errors
      if(!showData.ok){return;}
      showData = JSON.parse(showData.res.body);
      if(showData.status){
          console.log('[ERROR] Error #%d: %s\n', showData.status, showData.data.errors[0].detail);
          process.exit(1);
      }
      else if(!showData.items || showData.items.length<1){
          console.log('[ERROR] Show not found\n');
          process.exit(0);
      }
      showData = showData.items[0];
      console.log('[#%s] %s (%s)',showData.id,showData.title,showData.releaseYear);
      // show episodes
      let qs = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: parseInt(argv.s,10) };
      if(argv.alt){ qs.language = 'English'; }
      let episodesData = await getData({
          baseUrl: api_host,
          url: '/funimation/episodes/',
          querystring: qs,
          token: token,
          useToken: true,
          useProxy: true,
          debug: argv.debug,
      });
      if(!episodesData.ok){return;}
      let eps = JSON.parse(episodesData.res.body).items, fnSlug = [], is_selected = false;
      argv.e = typeof argv.e == 'number' || typeof argv.e == 'string' ? argv.e.toString() : '';
      argv.e = argv.e.match(',') ? argv.e.split(',') : [argv.e];
      let epSelList = argv.e, epSelRanges = [], epSelEps = [];
      epSelList = epSelList.map((e)=>{
          if(e.match('-')){
              e = e.split('-');
              if( e[0].match(/^(?:[A-Z]+|)\d+$/i) && e[1].match(/^\d+$/) ){
                  e[0] = e[0].replace(/^(?:([A-Z]+)|)(0+)/i,'$1');
                  let letter = e[0].match(/^([A-Z]+)\d+$/i) ? e[0].match(/^([A-Z]+)\d+$/i)[1].toUpperCase() : '';
                  e[0] = e[0].replace(/^[A-Z]+(\d+)$/i,'$1');
                  e[0] = parseInt(e[0]);
                  e[1] = parseInt(e[1]);
                  if(e[0] < e[1]){
                      for(let i=e[0];i<e[1]+1;i++){
                          epSelRanges.push(letter+i);
                      }
                      return '';
                  }
                  else{
                      return (letter+e[0]);
                  }
              }
              else{
                  return '';
              }
          }
          else if(e.match(/^(?:[A-Z]+|)\d+$/i)){
              return e.replace(/^(?:([A-Z]+)|)(0+)/i,'$1').toUpperCase();
          }
          else{
              return '';
          }
      });
      epSelList = [...new Set(epSelList.concat(epSelRanges))];
      // parse episodes list
      for(let e in eps){
          let showStrId = eps[e].ids.externalShowId;
          let epStrId = eps[e].ids.externalEpisodeId.replace(new RegExp('^'+showStrId),'');
          // select
          if(epSelList.includes(epStrId.replace(/^(?:([A-Z]+)|)(0+)/,'$1'))){
              fnSlug.push({title:eps[e].item.titleSlug,episode:eps[e].item.episodeSlug});
              epSelEps.push(epStrId);
              is_selected = true;
          }
          else{
              is_selected = false;
          }
          // console vars
          let tx_snum = eps[e].item.seasonNum==1?'':` S${eps[e].item.seasonNum}`;
          let tx_type = eps[e].mediaCategory != 'episode' ? eps[e].mediaCategory : '';
          let tx_enum = eps[e].item.episodeNum !== '' ?
              `#${(eps[e].item.episodeNum < 10 ? '0' : '')+eps[e].item.episodeNum}` : '#'+eps[e].item.episodeId;
          let qua_str = eps[e].quality.height ? eps[e].quality.quality + eps[e].quality.height : 'UNK';
          let aud_str = eps[e].audio.length > 0 ? `, ${eps[e].audio.join(', ')}` : '';
          let rtm_str = eps[e].item.runtime !== '' ? eps[e].item.runtime : '??:??';
          // console string
          let episodeIdStr = epStrId;
          let conOut  = `[${episodeIdStr}] `;
          conOut += `${eps[e].item.titleName+tx_snum} - ${tx_type+tx_enum} ${eps[e].item.episodeName} `;
          conOut += `(${rtm_str}) [${qua_str+aud_str}]`;
          conOut += is_selected ? ' (selected)' : '';
          conOut += eps.length-1 == e ? '\n' : '';
          console.log(conOut);
      }
      if(fnSlug.length>1){
          batchDL = true;
      }
      if(fnSlug.length<1){
          console.log('[INFO] Episodes not selected!\n');
          process.exit();
      }
      else{
          console.log('[INFO] Selected Episodes: %s\n',epSelEps.join(', '));
          for(let fnEp=0;fnEp<fnSlug.length;fnEp++){
              await getEpisode(fnSlug[fnEp]);
          }
      }
  }
  
  async function getEpisode(fnSlug){
      let episodeData = await getData({
          baseUrl: api_host,
          url: `/source/catalog/episode/${fnSlug.title}/${fnSlug.episode}/`,
          token: token,
          useToken: true,
          useProxy: true,
          debug: argv.debug,
      });
      if(!episodeData.ok){return;}
      let ep = JSON.parse(episodeData.res.body).items[0], streamId = 0;
      // build fn
      fnTitle = argv.t ? argv.t : ep.parent.title;
      ep.number = isNaN(ep.number) ? ep.number : ( parseInt(ep.number, 10) < 10 ? '0' + ep.number : ep.number );
      if(ep.mediaCategory != 'Episode'){
          ep.number = ep.number !== '' ? ep.mediaCategory+ep.number : ep.mediaCategory+'#'+ep.id;
      }
      fnEpNum = argv.ep && !batchDL ? ( parseInt(argv.ep, 10) < 10 ? '0' + argv.ep : argv.ep ) : ep.number;
      
      // is uncut
      let uncut = {
          Japanese: false,
          English: false
      };
      
      // end
      console.log(
          '[INFO] %s - S%sE%s - %s',
          ep.parent.title,
          (ep.parent.seasonNumber ? ep.parent.seasonNumber : '?'),
          (ep.number ? ep.number : '?'),
          ep.title
      );
      
      console.log('[INFO] Available streams (Non-Encrypted):');
      
      // map medias
      let media = ep.media.map(function(m){
          if(m.mediaType == 'experience'){
              if(m.version.match(/uncut/i)){
                  uncut[m.language] = true;
              }
              return {
                  id: m.id,
                  language: m.language,
                  version: m.version,
                  type: m.experienceType,
                  subtitles: getSubsUrl(m.mediaChildren),
              };
          }
          else{
              return { id: 0, type: '' };
          }
      });
      
      const dubType = {
          'enUS': 'English',
          'esLA': 'Spanish (Latin Am)',
          'ptBR': 'Portuguese (Brazil)',
      };
      
      // select
      media = media.reverse();
      for(let m of media){
          let selected = false;
          if(m.id > 0 && m.type == 'Non-Encrypted'){
              let dub_type = m.language;
              let selUncut = !argv.simul && uncut[dub_type] && m.version.match(/uncut/i) 
                  ? true 
                  : (!uncut[dub_type] || argv.simul && m.version.match(/simulcast/i) ? true : false);
              if(dub_type == 'Japanese' && argv.sub && selUncut){
                  streamId = m.id;
                  stDlPath = m.subtitles;
                  selected = true;
              }
              else if(dub_type == dubType[argv.dub] && !argv.sub && selUncut){
                  streamId = m.id;
                  stDlPath = m.subtitles;
                  selected = true;
              }
              console.log(`[#${m.id}] ${dub_type} [${m.version}]`,(selected?'(selected)':''));
          }
      }
      
      if(streamId<1){
          console.log('[ERROR] Track not selected\n');
          return;
      }
      else{
          let streamData = await getData({
              baseUrl: api_host,
              url: `/source/catalog/video/${streamId}/signed`,
              token: token,
              dinstid: 'uuid',
              useToken: true,
              useProxy: true,
              debug: argv.debug,
          });
          if(!streamData.ok){return;}
          streamData = JSON.parse(streamData.res.body);
          tsDlPath = false;
          if(streamData.errors){
              console.log('[ERROR] Error #%s: %s\n',streamData.errors[0].code,streamData.errors[0].detail);
              return;
          }
          else{
              for(let u in streamData.items){
                  if(streamData.items[u].videoType == 'm3u8'){
                      tsDlPath = streamData.items[u].src;
                      break;
                  }
              }
          }
          if(!tsDlPath){
              console.log('[ERROR] Unknown error\n');
              return;
          }
          else{
              await downloadStreams();
          }
      }
  }
  
  function getSubsUrl(m){
      if(argv.nosubs && !argv.sub){
          return false;
      }
      for(let i in m){
          let fpp = m[i].filePath.split('.');
          let fpe = fpp[fpp.length-1];
          if(fpe == 'vtt'){ // dfxp (TTML), srt, vtt
              return m[i].filePath;
          }
      }
      return false;
  }
  
  function exec(pname, fpath, pargs, spc){
      pargs = pargs ? ' ' + pargs : '';
      spc   = Boolean(spc) ? '\n' : '';
      console.log(`\n> "${pname}"${pargs}${spc}`);
      try {
  	require('child_process').execSync((fpath + pargs), { stdio: 'inherit' });
      } catch (ex) {
          console.log(`\n> "${pname}" failed with status ${ex.status}`);
  	throw ex;
      }
  }
  
  async function downloadStreams(){
      
      // req playlist
      let plQualityReq = await getData({
          url: tsDlPath,
          useProxy: (argv.ssp ? false : true),
          debug: argv.debug,
      });
      if(!plQualityReq.ok){return;}
      
      let plQualityLinkList = m3u8(plQualityReq.res.body);
      
      let mainServersList = [
          'vmfst-api.prd.funimationsvc.com',
          'd132fumi6di1wa.cloudfront.net',
          'funiprod.akamaized.net',
      ];
      
      let plServerList = [],
          plStreams    = {},
          plLayersStr  = [],
          plLayersRes  = {},
          plMaxLayer   = 1;
          plNewIds     = 1;
          plAud        = { uri: '' };
      
      // new uris
      let vplReg = /streaming_video_(\d+)_(\d+)_(\d+)_index\.m3u8/;
      if(plQualityLinkList.playlists[0].uri.match(vplReg)){
          if(plQualityLinkList.mediaGroups.AUDIO['audio-aacl-128']){
              let audioData = plQualityLinkList.mediaGroups.AUDIO['audio-aacl-128'];
              audioEl = Object.keys(audioData);
              audioData = audioData[audioEl[0]];
              plAud = { ...audioData, ...{ langStr: audioEl[0] } };
          }
          plQualityLinkList.playlists.sort((a, b) => {
              let av = parseInt(a.uri.match(vplReg)[3]);
              let bv = parseInt(b.uri.match(vplReg)[3]);
              if(av  > bv){
                  return 1;
              }
              if (av < bv) {
                  return -1;
              }
              return 0;
          })
      }
      
      for(let s of plQualityLinkList.playlists){
          if(s.uri.match(/_Layer(\d+)\.m3u8/) || s.uri.match(vplReg)){
              // set layer and max layer
              let plLayerId = 0;
              if(s.uri.match(/_Layer(\d+)\.m3u8/)){
                  plLayerId = parseInt(s.uri.match(/_Layer(\d+)\.m3u8/)[1]);
              }
              else{
                  plLayerId = plNewIds, plNewIds++;
              }
              plMaxLayer    = plMaxLayer < plLayerId ? plLayerId : plMaxLayer;
              // set urls and servers
              let plUrlDl  = s.uri;
              let plServer = new URL(plUrlDl).host;
              if(!plServerList.includes(plServer)){
                  plServerList.push(plServer);
              }
              if(!Object.keys(plStreams).includes(plServer)){
                  plStreams[plServer] = {};
              }
              if(plStreams[plServer][plLayerId] && plStreams[plServer][plLayerId] != plUrlDl){
                  console.log(`[WARN] Non duplicate url for ${plServer} detected, please report to developer!`);
              }
              else{
                  plStreams[plServer][plLayerId] = plUrlDl;
              }
              // set plLayersStr
              let plResolution = `${s.attributes.RESOLUTION.height}p`;
              plLayersRes[plLayerId] = plResolution;
              let plBandwidth  = Math.round(s.attributes.BANDWIDTH/1024);
              if(plLayerId<10){
                  plLayerId = plLayerId.toString().padStart(2,' ');
              }
              let qualityStrAdd   = `${plLayerId}: ${plResolution} (${plBandwidth}KiB/s)`;
              let qualityStrRegx  = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g,'\\$1'),'m');
              let qualityStrMatch = !plLayersStr.join('\r\n').match(qualityStrRegx);
              if(qualityStrMatch){
                  plLayersStr.push(qualityStrAdd);
              }
          }
          else {
              console.log(s.uri);
          }
      }
      
      for(let s of mainServersList){
          if(plServerList.includes(s)){
              plServerList.splice(plServerList.indexOf(s), 1);
              plServerList.unshift(s);
              break;
          }
      }
      
      if(typeof argv.q == 'object' && argv.q.length > 1){
          argv.q = argv.q[argv.q.length-1];
      }
      
      argv.q = argv.q < 1 || argv.q > plMaxLayer ? plMaxLayer : argv.q;
      
      let plSelectedServer = plServerList[argv.x-1];
      let plSelectedList   = plStreams[plSelectedServer];
      let videoUrl = argv.x < plServerList.length+1 && plSelectedList[argv.q] ? plSelectedList[argv.q] : '';
      
      plLayersStr.sort();
      console.log(`[INFO] Servers available:\n\t${plServerList.join('\n\t')}`);
      console.log(`[INFO] Available qualities:\n\t${plLayersStr.join('\n\t')}`);
      
      if(videoUrl != ''){
          console.log(`[INFO] Selected layer: ${argv.q} (${plLayersRes[argv.q]}) @ ${plSelectedServer}`);
          console.log('[INFO] Stream URL:',videoUrl);
          fnSuffix = argv.suffix.replace('SIZEp',plLayersRes[argv.q]);
          fnOutput = shlp.cleanupFilename(`[${argv.a}] ${fnTitle} - ${fnEpNum} [${fnSuffix}]`);
          console.log(`[INFO] Output filename: ${fnOutput}.ts`);
      }
      else if(argv.x > plServerList.length){
          console.log('[ERROR] Server not selected!\n');
          return;
      }
      else{
          console.log('[ERROR] Layer not selected!\n');
          return;
      }
      
      let dlFailed = false;
      let dlFailedA = false;
      
      
      if (!argv.novids) {
          // download video
          let reqVideo = await getData({
              url: videoUrl,
              useProxy: (argv.ssp ? false : true),
              debug: argv.debug,
          });
          if (!reqVideo.ok) { return; }
          
          let chunkList = m3u8(reqVideo.res.body);
          chunkList.baseUrl = videoUrl.split('/').slice(0, -1).join('/') + '/';
          
          let tsFile = path.join(cfg.dir.content, fnOutput);
          
          if (chunkList.segments[0].uri.match(/streaming_video_(\d+)_(\d+)_(\d+)\.ts/)) {
              if (fs.existsSync(tsFile + '.ts')) {
                  let rwts = await shlp.question(`[Q] File «${tsFile + '.ts'}» already exists! Rewrite? (y/N)`);
                  rwts = rwts || 'N';
                  if (!['Y', 'y'].includes(rwts[0])) {
                      return;
                  }
                  fs.unlinkSync(tsFile + '.ts')
              }
  
              let chunk = chunkList.segments[0]
              
              let reqKey = await getData({
                  url: chunk.key.uri,
                  responseType: 'buffer'
              })
              if (!reqKey.ok) { return; }
              let key = reqKey.res.body;
              let iv = Buffer.alloc(16);
              let ivs = chunk.key.iv ? chunk.key.iv : [0, 0, 0, 1];
              for (let i in ivs) {
                  iv.writeUInt32BE(ivs[i], i * 4);
              }
              key = crypto.createDecipheriv('aes-128-cbc', key, iv);
              
              let progress, intervall;
          
              function logInfo() {
                  if (progress && progress.percent && progress.transferred)
                      console.log(`[INFO] Downloaded ${progress.percent.toFixed(2) * 100}% (${(progress.transferred/1024).toFixed(0)}kb/${progress.total?(progress.total/1024).toFixed(0) + 'kb':'unknown'})`);
              }
  
              let res = (await got({
                  url: chunk.uri,
                  headers: {
                      'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0',
                  },
                  responseType: 'buffer'
              }).on("downloadProgress", (pro) => {
                  progress = pro
                  if (intervall === undefined)
                      intervall = setInterval(logInfo, 2500)
              })
              .catch(error => console.log(`[ERROR] ${error.name}: ${error.code||error.message}`)))
              clearInterval(intervall)
  
              if (!res.body) { return; }
              let dec = key.update(res.body);
              dec = Buffer.concat([dec, key.final()]);
              fs.writeFileSync(tsFile + '.ts', dec)
  
              console.log(`[INFO] Finished ${tsFile}`)
          } else {
              let proxyHLS = false;
              if (argv.proxy && !argv.ssp) {
                  try {
                      proxyHLS = {};
                      proxyHLS.url = buildProxyUrl(argv.proxy, argv['proxy-auth']);
                  }
                  catch(e){
                      console.log(`\n[WARN] Not valid proxy URL${e.input?' ('+e.input+')':''}!`);
                      console.log('[WARN] Skiping...');
                      proxyHLS = false;
                  }
              }
      
              let streamdlParams = {
                  fn: tsFile + '.ts',
                  m3u8json: chunkList,
                  baseurl: chunkList.baseUrl,
                  pcount: 10,
                  proxy: (proxyHLS ? proxyHLS : false)
              };
              
              let dldata = await new streamdl(streamdlParams).download();
              if(!dldata.ok){
                  fs.writeFileSync(`${tsFile}.ts.resume`, JSON.stringify(dldata.parts));
                  console.log(`[ERROR] DL Stats: ${JSON.stringify(dldata.parts)}\n`);
                  dlFailed = true;
              }
              else if(fs.existsSync(`${tsFile}.ts.resume`) && dldata.ok){
                  fs.unlinkSync(`${tsFile}.ts.resume`);
              }
          }
      }
      else{
          console.log('[INFO] Skip video downloading...\n');
      }
      
      if (!argv.novids && plAud.uri) {
          // download audio
          let reqAudio = await getData({
              url: plAud.uri,
              useProxy: (argv.ssp ? false : true),
              debug: argv.debug,
          });
          if (!reqAudio.ok) { return; }
          
          let chunkListA = m3u8(reqAudio.res.body);
          chunkListA.baseUrl = plAud.uri.split('/').slice(0, -1).join('/') + '/';
  
          let tsFileA = path.join(cfg.dir.content, fnOutput + `.${plAud.language}`);
  
          if (fs.existsSync(tsFileA + '.ts')) {
              let rwts = await shlp.question(`[Q] File «${tsFileA + '.ts'}» already exists! Rewrite? (y/N)`);
              rwts = rwts || 'N';
              if (!['Y', 'y'].includes(rwts[0])) {
                  return;
              }
              fs.unlinkSync(tsFileA + '.ts')
          }
  
          let chunk = chunkListA.segments[0]
          
          let reqKey = await getData({
              url: chunk.key.uri,
              responseType: 'buffer'
          })
  
          if (!reqKey.ok) { return; }
          let key = reqKey.res.body;
          let iv = Buffer.alloc(16);
          let ivs = chunk.key.iv ? chunk.key.iv : [0, 0, 0, 1];
          for (let i in ivs) {
              iv.writeUInt32BE(ivs[i], i * 4);
          }
          key = crypto.createDecipheriv('aes-128-cbc', key, iv);
          
          let progress, intervall;
          
          function logInfo() {
              if (progress && progress.percent && progress.transferred)
                  console.log(`[INFO] Downloaded ${progress.percent.toFixed(2) * 100}% (${(progress.transferred/1024).toFixed(0)}kb/${progress.total?(progress.total/1024).toFixed(0) + 'kb':'unknown'})`);
          }
  
          let res = (await got({
              url: chunk.uri,
              headers: {
                  'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0',
              },
              responseType: 'buffer'
          }).on("downloadProgress", (pro) => {
              progress = pro
              if (intervall === undefined)
                  intervall = setInterval(logInfo, 2500)
          })
          .catch(error => console.log(`[ERROR] ${error.name}: ${error.code||error.message}`)))
          clearInterval(intervall)
          if (!res.body) { return; }
          let dec = key.update(res.body);
          dec = Buffer.concat([dec, key.final()]);
          fs.writeFileSync(tsFileA + '.ts', dec)
  
          console.log(`[INFO] Finished ${tsFileA}`)
      }
      
       // add subs
      let subsUrl = stDlPath;
      let subsExt = !argv.mp4 || argv.mp4 && !argv.mks && argv.ass ? '.ass' : '.srt';
      let addSubs = argv.mks && subsUrl ? true : false;
      
      // download subtitles
      if(subsUrl){
          console.log('[INFO] Downloading subtitles...');
          console.log(subsUrl);
          let subsSrc = await getData({
              url: subsUrl,
              useProxy: true,
              debug: argv.debug,
          });
          if(subsSrc.ok){
              let assData = vttConvert(subsSrc.res.body, (subsExt == '.srt' ? true : false));
              let assFile = path.join(cfg.dir.content, fnOutput) + subsExt;
              fs.writeFileSync(assFile, assData);
              console.log('[INFO] Subtitles downloaded!');
          }
          else{
              console.log('[ERROR] Failed to download subtitles!');
              addSubs = false;
          }
      }
      
      if(dlFailed || dlFailedA){
          console.log('\n[INFO] TS file not fully downloaded, skip muxing video...\n');
          return;
      }
      
      if(argv.skipmux){
          return;
      }
      
      let muxTrg = path.join(cfg.dir.content, fnOutput);
      let muxTrgA = '';
      let tshTrg = path.join(cfg.dir.trash, fnOutput);
      
      if(!fs.existsSync(`${muxTrg}.ts`) || !fs.statSync(`${muxTrg}.ts`).isFile()){
          console.log('\n[INFO] TS file not found, skip muxing video...\n');
          return;
      }
      
      if(plAud.uri){
          muxTrgA = path.join(cfg.dir.content, fnOutput + `.${plAud.language}`);
          if(!fs.existsSync(`${muxTrgA}.ts`) || !fs.statSync(`${muxTrgA}.ts`).isFile()){
              console.log('\n[INFO] TS file not found, skip muxing video...\n');
              return;
          }
      }
      
      // usage
      let usableMKVmerge = true;
      let usableFFmpeg = true;
      
      // check exec path
      let mkvmergebinfile = await lookpath(path.join(cfg.bin.mkvmerge));
      let ffmpegbinfile   = await lookpath(path.join(cfg.bin.ffmpeg));
      
      // check exec
      if( !argv.mp4 && !mkvmergebinfile ){
          console.log('[WARN] MKVMerge not found, skip using this...');
          usableMKVmerge = false;
      }
      if( !usableMKVmerge && !ffmpegbinfile || argv.mp4 && !ffmpegbinfile ){
          console.log('[WARN] FFmpeg not found, skip using this...');
          usableFFmpeg = false;
      }
      
      // ftag
      argv.ftag = argv.ftag ? argv.ftag : argv.a;
      argv.ftag = shlp.cleanupFilename(argv.ftag);
      
      // select muxer
      if(!argv.mp4 && usableMKVmerge){
          // mux to mkv
          let mkvmux  = [];
          mkvmux.push('-o',`${muxTrg}.mkv`);
          mkvmux.push('--no-date','--disable-track-statistics-tags','--engage','no_variable_data');
          mkvmux.push('--track-name',`0:[${argv.ftag}]`);
          if(plAud.uri){
              mkvmux.push('--video-tracks','0','--no-audio');
              mkvmux.push('--no-subtitles','--no-attachments');
              mkvmux.push(`${muxTrg}.ts`);
              mkvmux.push('--language',`0:${argv.sub?'jpn':''}`);
              mkvmux.push('--no-video','--audio-tracks','0');
              mkvmux.push('--no-subtitles','--no-attachments');
              mkvmux.push(`${muxTrgA}.ts`);
          }
          else{
              mkvmux.push('--language',`1:${argv.sub?'jpn':''}`);
              mkvmux.push('--video-tracks','0','--audio-tracks','1');
              mkvmux.push('--no-subtitles','--no-attachments');
              mkvmux.push(`${muxTrg}.ts`);
          }
          if(addSubs){
              mkvmux.push('--language','0:eng');
              mkvmux.push(`${muxTrg}${subsExt}`);
          }
          fs.writeFileSync(`${muxTrg}.json`,JSON.stringify(mkvmux,null,'  '));
          exec('mkvmerge',`"${mkvmergebinfile}"`,`@"${muxTrg}.json"`);
          fs.unlinkSync(`${muxTrg}.json`);
      }
      else if(usableFFmpeg){
          let ffext = !argv.mp4 ? 'mkv' : 'mp4';
          let ffmux = `-i "${muxTrg}.ts" `;
          if(plAud.uri){
              ffmux += `-i "${muxTrgA}.ts" `;
          }
          ffmux += addSubs ? `-i "${muxTrg}${subsExt}" ` : '';
          ffmux += '-map 0 -c:v copy -c:a copy ';
          ffmux += addSubs ? '-map 1 ' : '';
          ffmux += addSubs && !argv.mp4 ? '-c:s ass ' : '';
          ffmux += addSubs &&  argv.mp4 ? '-c:s mov_text ' : '';
          ffmux += '-metadata encoding_tool="no_variable_data" ';
          ffmux += `-metadata:s:v:0 title="[${argv.a}]" -metadata:s:a:0 language=${argv.sub?'jpn':''} `;
          ffmux += addSubs ? '-metadata:s:s:0 language=eng ' : '';
          ffmux += `"${muxTrg}.${ffext}"`;
          // mux to mkv
          exec('ffmpeg',`"${ffmpegbinfile}"`,ffmux);
      }
      else{
          console.log('\n[INFO] Done!\n');
          return;
      }
      if(argv.notrashfolder && argv.nocleanup){
          // don't move or delete temp files
      }
      else if(argv.nocleanup){
          fs.renameSync(muxTrg+'.ts', tshTrg + '.ts');
          if(subsUrl && addSubs){
              fs.renameSync(muxTrg +subsExt, tshTrg +subsExt);
          }
      }
      else{
          fs.unlinkSync(muxTrg+'.ts');
          if(subsUrl && addSubs){
              fs.unlinkSync(muxTrg +subsExt);
          }
      }
      console.log('\n[INFO] Done!\n');
  }
  
  // make proxy URL
  function buildProxy(proxyBaseUrl, proxyAuth){
      if(!proxyBaseUrl.match(/^(https?|socks4|socks5):/)){
          proxyBaseUrl = 'http://' + proxyBaseUrl;
      }
      
      let proxyCfg = new URL(proxyBaseUrl);
      let proxyStr = `${proxyCfg.protocol}//`;
      
      if(typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == ''){
          throw new Error('[ERROR] Hostname and port required for proxy!');
      }
      
      if(proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')){
          proxyCfg.username = proxyAuth.split(':')[0];
          proxyCfg.password = proxyAuth.split(':')[1];
          proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`;
      }
      
      proxyStr += proxyCfg.hostname;
      
      if(!proxyCfg.port && proxyCfg.protocol == 'http:'){
          proxyStr += ':80';
      }
      else if(!proxyCfg.port && proxyCfg.protocol == 'https:'){
          proxyStr += ':443';
      }
      
      return proxyStr;
  }
  