multi-downloader-nx/funi.js
Apple 09d774c229 Added subtitle language support
So @STRATOS99K that should be the second part of your request.

Can you test it on your end? If something is not quite working feel free to open an issue in my fork or comment here. I will fix it tomorrow since its already 1:45 AM here.
2021-05-12 01:44:59 +02:00

879 lines
29 KiB
JavaScript

#!/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 crypto = require('crypto');
const got = require('got');
const iso639 = require('iso-639');
// 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 (argv.all) {
fnSlug.push({title:eps[e].item.titleSlug,episode:eps[e].item.episodeSlug});
epSelEps.push(epStrId);
is_selected = true;
}
else 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)',
'zhMN': 'Chinese (Mandarin, PRC)'
};
// 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;
}
let subLangAvailable = m.some(a => a.ext == 'vtt' && a.languages && a.languages[0].code === argv.subLang);
if (!subLangAvailable) {
console.log(`[WARN] Unable to find subtitle language '${argv.subLang}'. Defaulting to English.`);
argv.subLang = 'en';
}
for(let i in m){
let fpp = m[i].filePath.split('.');
let fpe = fpp[fpp.length-1];
if(fpe == 'vtt' && (( !m[i].languages ) || (m[i].languages[0].code === argv.subLang))){ // dfxp (TTML), srt, vtt
return m[i].filePath;
}
}
return false;
}
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;
video: if (!argv.novids) {
// download video
let reqVideo = await getData({
url: videoUrl,
useProxy: (argv.ssp ? false : true),
debug: argv.debug,
});
if (!reqVideo.ok) { break video; }
let chunkList = m3u8(reqVideo.res.body);
let tsFile = path.join(cfg.dir.content, fnOutput);
dlFailed = !await downloadFile(tsFile, chunkList);
}
else{
console.log('[INFO] Skip video downloading...\n');
}
if (!argv.noaudio && 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);
let tsFileA = path.join(cfg.dir.content, fnOutput + `.${plAud.language}`);
dlFailedA = !await downloadFile(tsFileA, chunkListA);
}
// 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);
let tshTrgA = '';
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}`);
tshTrgA = path.join(cfg.dir.trash, 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;
}
}
let langCode;
for (let lang in iso639.iso_639_2) {
let langObj = iso639.iso_639_2[lang];
if (langObj.hasOwnProperty('639-1') && langObj['639-1'] === plAud['language']) {
langCode = langObj['639-2'];
}
}
if (!langCode)
langCode = argv.sub ? 'jpn' : 'eng';
// 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;
}
if ( argv.novids ){
console.log('[INFO] Video not downloaded. Skip muxing video.');
}
// 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:${langCode}`);
mkvmux.push('--no-video','--audio-tracks','0');
mkvmux.push('--no-subtitles','--no-attachments');
mkvmux.push(`${muxTrgA}.ts`);
}
else{
mkvmux.push('--language',`1:${langCode}`);
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,' '));
shlp.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 += '-map 1:a ';
}
ffmux += addSubs ? `-i "${muxTrg}${subsExt}" ` : '';
ffmux += '-map 0 -c:v copy -c:a copy ';
ffmux += addSubs ? `-map ${plAud.uri ? 2 : 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:${plAud.uri?1:0} language=${langCode} `;
ffmux += addSubs ? '-metadata:s:s:0 language=eng ' : '';
ffmux += `"${muxTrg}.${ffext}"`;
// mux to mkv
shlp.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 (plAud.uri)
fs.renameSync(muxTrgA+'.ts', tshTrgA + '.ts');
if(subsUrl && addSubs){
fs.renameSync(muxTrg +subsExt, tshTrg +subsExt);
}
}
else{
fs.unlinkSync(muxTrg+'.ts');
if (plAud.uri)
fs.unlinkSync(muxTrgA+'.ts');
if(subsUrl && addSubs){
fs.unlinkSync(muxTrg +subsExt);
}
}
console.log('\n[INFO] Done!\n');
}
async function downloadFile(filename, chunkList) {
let offset = 0;
fileCheck: if (fs.existsSync(filename + '.ts')) {
if (fs.existsSync(filename + '.ts.resume')) {
const resume = JSON.parse(fs.readFileSync(`${filename}.ts.resume`));
if (resume.total === chunkList.segments.length) {
offset = resume.downloaded;
break fileCheck;
}
}
let rwts = await shlp.question(`[Q] File «${filename + '.ts'}» already exists! Rewrite? (y/N)`);
rwts = rwts || 'N';
if (!['Y', 'y'].includes(rwts[0])) {
return false;
}
fs.unlinkSync(filename + '.ts');
}
let start = Date.now();
console.log(`[INFO] Started ${filename}.ts`);
for (let i = offset; i < chunkList.segments.length; i+=argv.partsize) {
let cur = [];
for (let a = 0; a < Math.min(argv.partsize, chunkList.segments.length - i); a++) {
cur.push(downloadPart(chunkList.segments[i + a], i + a, chunkList.segments.length)
.catch(e => e));
}
let p = await Promise.all(cur);
if (p.some(el => el instanceof Error)) {
console.log(`[ERROR] An error occured while downloading ${filename}.ts`);
return false;
}
fs.writeFileSync(`${filename}.ts.resume`, JSON.stringify({ total: chunkList.segments.length, downloaded: i + argv.partsize }, null, 4));
for (let a = 0; a < p.length; a++) {
fs.writeFileSync(filename + '.ts', p[a].content, { flag: 'a' });
}
logDownloadInfo(start, i + Math.min(argv.partsize, chunkList.segments.length - i),
chunkList.segments.length, i + Math.min(argv.partsize, chunkList.segments.length - i),
chunkList.segments.length);
}
if (fs.existsSync(`${filename}.ts.resume`))
fs.unlinkSync(`${filename}.ts.resume`);
console.log(`[INFO] Finished ${filename}.ts`);
return true;
}
async function downloadPart(chunk, index) {
let key = await generateCrypto(chunk, index);
let headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0'
};
if (chunk.byterange)
headers.Range = `bytes=${chunk.byterange.offset}-${chunk.byterange.offset+chunk.byterange.length-1}`;
let res = (await got({
url: chunk.uri,
headers,
responseType: 'buffer'
}).catch(error => console.log(`[ERROR] ${error.name}: ${error.code||error.message}`)));
if (!res.body) { return new Error('Invalid State'); }
try {
let dec = key.update(res.body);
dec = Buffer.concat([dec, key.final()]);
return { content: dec, index: index};
} catch (e) { return e; }
}
let keys = {};
async function generateCrypto(chunk, index) {
let key = keys[chunk.key.uri];
if (!key) {
let reqKey = await getData({
url: chunk.key.uri,
responseType: 'buffer'
});
if (!reqKey.ok) { console.log('[ERROR] Can\'t get key'); return; }
key = reqKey.res.body;
keys[chunk.key.uri] = key;
}
let iv = Buffer.alloc(16);
let ivs = chunk.key.iv ? chunk.key.iv : [0, 0, 0, index];
for (let i in ivs) {
iv.writeUInt32BE(ivs[i], i * 4);
}
key = crypto.createDecipheriv('aes-128-cbc', key, iv);
return key;
}
/* Snacked from hls-download */
function logDownloadInfo (dateStart, partsDL, partsTotal, partsDLRes, partsTotalRes) {
const dateElapsed = Date.now() - dateStart;
const percentFxd = (partsDL / partsTotal * 100).toFixed();
const percent = percentFxd < 100 ? percentFxd : (partsTotal == partsDL ? 100 : 99);
const revParts = parseInt(dateElapsed * (partsTotal / partsDL - 1));
const time = shlp.formatTime((revParts / 1000).toFixed());
console.log(`[INFO] ${partsDLRes} of ${partsTotalRes} parts downloaded [${percent}%] (${time})`);
}
// 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;
}