mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-01-11 20:10:20 +00:00
4.7.0
This commit is contained in:
parent
d2383962ca
commit
6454f93d9d
9 changed files with 532 additions and 178 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -13,5 +13,7 @@ package-lock.json
|
|||
*.ts
|
||||
*.mkv
|
||||
*.mp4
|
||||
*.ass
|
||||
*.srt
|
||||
*.resume
|
||||
*.user.yml
|
||||
|
|
|
|||
251
funi.js
251
funi.js
|
|
@ -3,7 +3,6 @@
|
|||
// modules build-in
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
// package json
|
||||
const packageJson = require('./package.json');
|
||||
|
|
@ -12,40 +11,26 @@ const packageJson = require('./package.json');
|
|||
console.log(`\n=== Funimation Downloader NX ${packageJson.version} ===\n`);
|
||||
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
|
||||
|
||||
// request
|
||||
const got = require('got');
|
||||
|
||||
// modules extra
|
||||
const yaml = require('yaml');
|
||||
const shlp = require('sei-helper');
|
||||
const yargs = require('yargs');
|
||||
const FormData = require('form-data');
|
||||
const { lookpath } = require('lookpath');
|
||||
|
||||
// m3u8 and ttml
|
||||
const m3u8 = require('m3u8-parsed');
|
||||
const streamdl = require('hls-download');
|
||||
const { ttml2srt } = require('ttml2srt');
|
||||
|
||||
// get cfg file
|
||||
function getYamlCfg(file){
|
||||
let data = {};
|
||||
if(fs.existsSync(file)){
|
||||
try{
|
||||
data = yaml.parse(fs.readFileSync(file, 'utf8'));
|
||||
return data;
|
||||
}
|
||||
catch(e){}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
// extra
|
||||
const modulesFolder = __dirname + '/modules';
|
||||
const getYamlCfg = require(modulesFolder+'/module.cfg-loader');
|
||||
const getData = require(modulesFolder + '/module.getdata.js');
|
||||
const vttConvert = require(modulesFolder + '/module.vttconvert');
|
||||
|
||||
// new-cfg
|
||||
const cfgFolder = __dirname + '/config';
|
||||
const binCfgFile = path.join(cfgFolder,'bin-path.yml');
|
||||
const dirCfgFile = path.join(cfgFolder,'dir-path.yml');
|
||||
const cliCfgFile = path.join(cfgFolder,'cli-defaults.yml');
|
||||
const tokenFile = path.join(cfgFolder,'token.yml');
|
||||
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 = {
|
||||
|
|
@ -103,10 +88,10 @@ let argv = yargs
|
|||
.boolean('nosubs')
|
||||
|
||||
// proxy
|
||||
.describe('proxy','http(s)/socks proxy WHATWG url (ex. https://myproxyhost:1080/)')
|
||||
.describe('proxy-auth','Colon-separated username and password for proxy')
|
||||
.describe('ssp','Ignore proxy settings for stream downloading')
|
||||
.boolean('ssp')
|
||||
// .describe('proxy','http(s)/socks proxy WHATWG url (ex. https://myproxyhost:1080/)')
|
||||
// .describe('proxy-auth','Colon-separated username and password for proxy')
|
||||
// .describe('ssp','Ignore proxy settings for stream downloading')
|
||||
// .boolean('ssp')
|
||||
|
||||
.describe('mp4','Mux into mp4')
|
||||
.boolean('mp4')
|
||||
|
|
@ -153,16 +138,6 @@ let fnTitle = '',
|
|||
stDlPath = false,
|
||||
batchDL = false;
|
||||
|
||||
// go to work folder
|
||||
try {
|
||||
fs.accessSync(cfg.dir.content, fs.R_OK | fs.W_OK);
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
console.log('[ERROR] %s',e.messsage);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// select mode
|
||||
if(argv.auth){
|
||||
auth();
|
||||
|
|
@ -170,7 +145,7 @@ if(argv.auth){
|
|||
else if(argv.search){
|
||||
searchShow();
|
||||
}
|
||||
else if(argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0){
|
||||
else if(argv.s && !isNaN(parseInt(argv.s, 10)) && parseInt(argv.s, 10) > 0){
|
||||
getShow();
|
||||
}
|
||||
else{
|
||||
|
|
@ -188,15 +163,16 @@ async function auth(){
|
|||
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,yaml.stringify({'token':authData.token}));
|
||||
fs.writeFileSync(tokenFile, yaml.stringify({'token': authData.token}));
|
||||
}
|
||||
else if(authData.error){
|
||||
console.log('[ERROR]',authData.error,'\n');
|
||||
console.log('[ERROR]%s\n', authData.error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -209,8 +185,10 @@ async function searchShow(){
|
|||
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);
|
||||
|
|
@ -233,9 +211,11 @@ async function getShow(){
|
|||
// show main data
|
||||
let showData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: `/source/catalog/title/${parseInt(argv.s,10)}`,
|
||||
url: `/source/catalog/title/${parseInt(argv.s, 10)}`,
|
||||
token: token,
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
debug: argv.debug,
|
||||
});
|
||||
// check errors
|
||||
if(!showData.ok){return;}
|
||||
|
|
@ -257,8 +237,10 @@ async function getShow(){
|
|||
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;
|
||||
|
|
@ -345,8 +327,10 @@ 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;
|
||||
|
|
@ -357,23 +341,27 @@ async function getEpisode(fnSlug){
|
|||
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.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.mediaType == 'experience'){
|
||||
if(m.version.match(/uncut/i)){
|
||||
uncut[m.language] = true;
|
||||
}
|
||||
|
|
@ -389,6 +377,7 @@ async function getEpisode(fnSlug){
|
|||
return { id: 0, type: '' };
|
||||
}
|
||||
});
|
||||
|
||||
// select
|
||||
media = media.reverse();
|
||||
for(let m of media){
|
||||
|
|
@ -411,6 +400,7 @@ async function getEpisode(fnSlug){
|
|||
console.log(`[#${m.id}] ${dub_type} [${m.version}]`,(selected?'(selected)':''));
|
||||
}
|
||||
}
|
||||
|
||||
if(streamId<1){
|
||||
console.log('[ERROR] Track not selected\n');
|
||||
return;
|
||||
|
|
@ -419,9 +409,11 @@ async function getEpisode(fnSlug){
|
|||
let streamData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: `/source/catalog/video/${streamId}/signed`,
|
||||
token: token,
|
||||
dinstid: 'uuid',
|
||||
useToken: true,
|
||||
useProxy: true,
|
||||
dinstid: 'uuid',
|
||||
debug: argv.debug,
|
||||
});
|
||||
if(!streamData.ok){return;}
|
||||
streamData = JSON.parse(streamData.res.body);
|
||||
|
|
@ -455,7 +447,7 @@ function getSubsUrl(m){
|
|||
for(let i in m){
|
||||
let fpp = m[i].filePath.split('.');
|
||||
let fpe = fpp[fpp.length-1];
|
||||
if(fpe == 'dfxp'){ // dfxp, srt, vtt
|
||||
if(fpe == 'vtt'){ // dfxp (TTML), srt, vtt
|
||||
return m[i].filePath;
|
||||
}
|
||||
}
|
||||
|
|
@ -468,14 +460,16 @@ async function downloadStreams(){
|
|||
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'
|
||||
'funiprod.akamaized.net',
|
||||
];
|
||||
|
||||
let plServerList = [],
|
||||
|
|
@ -486,7 +480,7 @@ async function downloadStreams(){
|
|||
|
||||
for(let s of plQualityLinkList.playlists){
|
||||
// set layer and max layer
|
||||
let plLayerId = parseInt(s.uri.match(/_Layer(\d+)\.m3u8$/)[1]);
|
||||
let plLayerId = parseInt(s.uri.match(/_Layer(\d+)\.m3u8/)[1]);
|
||||
plMaxLayer = plMaxLayer < plLayerId ? plLayerId : plMaxLayer;
|
||||
// set urls and servers
|
||||
let plUrlDl = s.uri;
|
||||
|
|
@ -563,6 +557,7 @@ async function downloadStreams(){
|
|||
let reqVideo = await getData({
|
||||
url: videoUrl,
|
||||
useProxy: (argv.ssp ? false : true),
|
||||
debug: argv.debug,
|
||||
});
|
||||
if (!reqVideo.ok) { return; }
|
||||
|
||||
|
|
@ -606,23 +601,29 @@ async function downloadStreams(){
|
|||
console.log('[INFO] Skip video downloading...\n');
|
||||
}
|
||||
|
||||
// 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(stDlPath){
|
||||
if(subsUrl){
|
||||
console.log('[INFO] Downloading subtitles...');
|
||||
console.log(stDlPath);
|
||||
console.log(subsUrl);
|
||||
let subsSrc = await getData({
|
||||
url: stDlPath,
|
||||
url: subsUrl,
|
||||
useProxy: true,
|
||||
debug: argv.debug,
|
||||
});
|
||||
if(subsSrc.ok){
|
||||
let srtData = ttml2srt(subsSrc.res.body);
|
||||
let srtFile = path.join(cfg.dir.content, fnOutput) + '.srt';
|
||||
fs.writeFileSync(srtFile, srtData);
|
||||
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!');
|
||||
argv.mks = false;
|
||||
addSubs = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -639,13 +640,9 @@ async function downloadStreams(){
|
|||
return;
|
||||
}
|
||||
|
||||
// add subs
|
||||
let addSubs = argv.mks && stDlPath ? true : false;
|
||||
|
||||
// usage
|
||||
let usableMKVmerge = true;
|
||||
let usableFFmpeg = true;
|
||||
console.log(await lookpath(path.join(cfg.bin.ffmpeg + '.exe')));
|
||||
|
||||
// check exec path
|
||||
let mkvmergebinfile = await lookpath(path.join(cfg.bin.mkvmerge));
|
||||
|
|
@ -678,7 +675,7 @@ async function downloadStreams(){
|
|||
mkvmux.push(`${muxTrg}.ts`);
|
||||
if(addSubs){
|
||||
mkvmux.push('--language','0:eng');
|
||||
mkvmux.push(`${muxTrg}.srt`);
|
||||
mkvmux.push(`${muxTrg}${subsExt}`);
|
||||
}
|
||||
fs.writeFileSync(`${muxTrg}.json`,JSON.stringify(mkvmux,null,' '));
|
||||
shlp.exec('mkvmerge',`"${mkvmergebinfile}"`,`@"${muxTrg}.json"`);
|
||||
|
|
@ -687,10 +684,10 @@ async function downloadStreams(){
|
|||
else if(usableFFmpeg){
|
||||
let ffext = !argv.mp4 ? 'mkv' : 'mp4';
|
||||
let ffmux = `-i "${muxTrg}.ts" `;
|
||||
ffmux += addSubs ? `-i "${muxTrg}.srt" ` : '';
|
||||
ffmux += addSubs ? `-i "${muxTrg}${subsExt}" ` : '';
|
||||
ffmux += '-map 0 -c:v copy -c:a copy ';
|
||||
ffmux += addSubs ? '-map 1 ' : '';
|
||||
ffmux += addSubs && !argv.mp4 ? '-c:s srt ' : '';
|
||||
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':'eng'} `;
|
||||
|
|
@ -708,116 +705,46 @@ async function downloadStreams(){
|
|||
}
|
||||
else if(argv.nocleanup){
|
||||
fs.renameSync(muxTrg+'.ts', tshTrg + '.ts');
|
||||
if(stDlPath && argv.mks){
|
||||
fs.renameSync(muxTrg+'.srt', tshTrg + '.srt');
|
||||
if(subsUrl && addSubs){
|
||||
fs.renameSync(muxTrg +subsExt, tshTrg +subsExt);
|
||||
}
|
||||
}
|
||||
else{
|
||||
fs.unlinkSync(muxTrg+'.ts');
|
||||
if(stDlPath && argv.mks){
|
||||
fs.unlinkSync(muxTrg+'.srt');
|
||||
if(subsUrl && addSubs){
|
||||
fs.unlinkSync(muxTrg +subsExt);
|
||||
}
|
||||
}
|
||||
console.log('\n[INFO] Done!\n');
|
||||
}
|
||||
|
||||
// get data from url
|
||||
async function getData(options){
|
||||
let gOptions = {
|
||||
url: options.url,
|
||||
headers: {
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0',
|
||||
}
|
||||
};
|
||||
if(options.baseUrl){
|
||||
gOptions.prefixUrl = options.baseUrl;
|
||||
gOptions.url = gOptions.url.replace(/^\//,'');
|
||||
// make proxy URL
|
||||
function buildProxy(proxyBaseUrl, proxyAuth){
|
||||
if(!proxyBaseUrl.match(/^(https?|socks4|socks5):/)){
|
||||
proxyBaseUrl = 'http://' + proxyBaseUrl;
|
||||
}
|
||||
if(options.querystring){
|
||||
gOptions.url += `?${new URLSearchParams(options.querystring).toString()}`;
|
||||
}
|
||||
if(options.auth){
|
||||
gOptions.method = 'POST';
|
||||
gOptions.body = new FormData();
|
||||
gOptions.body.append('username', options.auth.user);
|
||||
gOptions.body.append('password', options.auth.pass);
|
||||
}
|
||||
if(options.useToken && token){
|
||||
gOptions.headers.Authorization = `Token ${token}`;
|
||||
}
|
||||
if(options.dinstid){
|
||||
gOptions.headers.devicetype = 'Android Phone';
|
||||
}
|
||||
// debug
|
||||
gOptions.hooks = {
|
||||
beforeRequest: [
|
||||
(options) => {
|
||||
if(argv.debug){
|
||||
console.log('[DEBUG] GOT OPTIONS:');
|
||||
console.log(options);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
if(options.useProxy && argv.proxy){
|
||||
try{
|
||||
const ProxyAgent = require('proxy-agent');
|
||||
let proxyUrl = buildProxyUrl(argv.proxy,argv['proxy-auth']);
|
||||
gOptions.agent = new ProxyAgent(proxyUrl);
|
||||
gOptions.timeout = 10000;
|
||||
}
|
||||
catch(e){
|
||||
console.log(`\n[WARN] Not valid proxy URL${e.input?' ('+e.input+')':''}!`);
|
||||
console.log('[WARN] Skiping...');
|
||||
argv.proxy = false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
if(argv.debug){
|
||||
console.log('[Debug] REQ:', gOptions);
|
||||
}
|
||||
let res = await got(gOptions);
|
||||
if(res.body && res.body.match(/^</)){
|
||||
throw { name: 'HTMLError', res };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
res,
|
||||
};
|
||||
}
|
||||
catch(error){
|
||||
if(argv.debug){
|
||||
console.log(error);
|
||||
}
|
||||
if(error.response && error.response.statusCode && error.response.statusMessage){
|
||||
console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
|
||||
}
|
||||
else if(error.name && error.name == 'HTMLError' && error.res && error.res.body){
|
||||
console.log(`[ERROR] ${error.name}:`);
|
||||
console.log(error.res.body);
|
||||
}
|
||||
else{
|
||||
console.log(`[ERROR] ${error.name}: ${error.code||error.message}`);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
function buildProxyUrl(proxyBaseUrl,proxyAuth){
|
||||
|
||||
let proxyCfg = new URL(proxyBaseUrl);
|
||||
if(!proxyCfg.hostname || !proxyCfg.port){
|
||||
throw new Error();
|
||||
let proxyStr = `${proxyCfg.protocol}//`;
|
||||
|
||||
if(typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == ''){
|
||||
throw new Error('[ERROR] Hostname and port required for proxy!');
|
||||
}
|
||||
if(proxyAuth && proxyAuth.match(':')){
|
||||
proxyCfg.auth = proxyAuth;
|
||||
|
||||
if(proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')){
|
||||
proxyCfg.username = proxyAuth.split(':')[0];
|
||||
proxyCfg.password = proxyAuth.split(':')[1];
|
||||
proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`;
|
||||
}
|
||||
return url.format({
|
||||
protocol: proxyCfg.protocol,
|
||||
slashes: true,
|
||||
auth: proxyCfg.auth,
|
||||
hostname: proxyCfg.hostname,
|
||||
port: proxyCfg.port,
|
||||
});
|
||||
|
||||
proxyStr += proxyCfg.hostname;
|
||||
|
||||
if(!proxyCfg.port && proxyCfg.protocol == 'http:'){
|
||||
proxyStr += ':80';
|
||||
}
|
||||
else if(!proxyCfg.port && proxyCfg.protocol == 'https:'){
|
||||
proxyStr += ':443';
|
||||
}
|
||||
|
||||
return proxyStr;
|
||||
}
|
||||
|
|
|
|||
BIN
modules/NotoSans-Regular.ttf
Normal file
BIN
modules/NotoSans-Regular.ttf
Normal file
Binary file not shown.
|
|
@ -7,7 +7,7 @@ const modulesCleanup = require('removeNPMAbsolutePaths');
|
|||
const { compile } = require('nexe');
|
||||
|
||||
const buildsDir = './_builds';
|
||||
const nodeVer = '-12.15.0';
|
||||
const nodeVer = '';
|
||||
|
||||
// main
|
||||
(async function(){
|
||||
|
|
@ -33,6 +33,7 @@ const nodeVer = '-12.15.0';
|
|||
fs.mkdirSync(`${buildDir}/videos`);
|
||||
fs.mkdirSync(`${buildDir}/videos/_trash`);
|
||||
const buildConfig = {
|
||||
loglevel: 'verbose',
|
||||
input: './crunchy.js',
|
||||
output: `${buildDir}/${pkg.short_name}`,
|
||||
target: getTarget(buildType) + nodeVer,
|
||||
|
|
@ -41,23 +42,19 @@ const nodeVer = '-12.15.0';
|
|||
],
|
||||
};
|
||||
console.log(`[Build] Build configuration: ${buildFull}`);
|
||||
await compile(buildConfig);
|
||||
if(fs.existsSync('./bin/ffmpeg')){
|
||||
// fs.copySync('./bin/ffmpeg', `${buildDir}/bin/ffmpeg`);
|
||||
try {
|
||||
await compile(buildConfig);
|
||||
}
|
||||
if(fs.existsSync('./bin/ffmpeg.exe')){
|
||||
// fs.copySync('./bin/ffmpeg.exe', `${buildDir}/bin/ffmpeg.exe`);
|
||||
}
|
||||
if(fs.existsSync('./bin/mkvmerge')){
|
||||
// fs.copySync('./bin/mkvmerge', `${buildDir}/bin/mkvmerge`);
|
||||
}
|
||||
if(fs.existsSync('./bin/mkvmerge.exe')){
|
||||
// fs.copySync('./bin/mkvmerge.exe', `${buildDir}/bin/mkvmerge.exe`);
|
||||
catch(e){
|
||||
console.log(e);
|
||||
process.exit();
|
||||
}
|
||||
fs.copySync('./bin/', `${buildDir}/bin/`);
|
||||
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
|
||||
fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
|
||||
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
|
||||
fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
|
||||
fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`);
|
||||
fs.copySync('./docs/', `${buildDir}/docs/`);
|
||||
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
|
||||
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
|
||||
|
|
|
|||
26
modules/module.cfg-loader.js
Normal file
26
modules/module.cfg-loader.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const yaml = require('yaml');
|
||||
const fs = require('fs');
|
||||
const existsFile = fs.existsSync;
|
||||
|
||||
const loadYamlFile = (file) => {
|
||||
return yaml.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
|
||||
const loadYamlCfg = (file) => {
|
||||
if(existsFile(`${file}.user.yml`)){
|
||||
file += '.user';
|
||||
}
|
||||
file += '.yml';
|
||||
if(fs.existsSync(file)){
|
||||
|
||||
try{
|
||||
return loadYamlFile(file, 'utf8');
|
||||
}
|
||||
catch(e){
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
module.exports = loadYamlCfg;
|
||||
149
modules/module.colors.json
Normal file
149
modules/module.colors.json
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
{
|
||||
"aliceblue": "#f0f8ff",
|
||||
"antiquewhite": "#faebd7",
|
||||
"aqua": "#00ffff",
|
||||
"aquamarine": "#7fffd4",
|
||||
"azure": "#f0ffff",
|
||||
"beige": "#f5f5dc",
|
||||
"bisque": "#ffe4c4",
|
||||
"black": "#000000",
|
||||
"blanchedalmond": "#ffebcd",
|
||||
"blue": "#0000ff",
|
||||
"blueviolet": "#8a2be2",
|
||||
"brown": "#a52a2a",
|
||||
"burlywood": "#deb887",
|
||||
"cadetblue": "#5f9ea0",
|
||||
"chartreuse": "#7fff00",
|
||||
"chocolate": "#d2691e",
|
||||
"coral": "#ff7f50",
|
||||
"cornflowerblue": "#6495ed",
|
||||
"cornsilk": "#fff8dc",
|
||||
"crimson": "#dc143c",
|
||||
"cyan": "#00ffff",
|
||||
"darkblue": "#00008b",
|
||||
"darkcyan": "#008b8b",
|
||||
"darkgoldenrod": "#b8860b",
|
||||
"darkgray": "#a9a9a9",
|
||||
"darkgreen": "#006400",
|
||||
"darkgrey": "#a9a9a9",
|
||||
"darkkhaki": "#bdb76b",
|
||||
"darkmagenta": "#8b008b",
|
||||
"darkolivegreen": "#556b2f",
|
||||
"darkorange": "#ff8c00",
|
||||
"darkorchid": "#9932cc",
|
||||
"darkred": "#8b0000",
|
||||
"darksalmon": "#e9967a",
|
||||
"darkseagreen": "#8fbc8f",
|
||||
"darkslateblue": "#483d8b",
|
||||
"darkslategray": "#2f4f4f",
|
||||
"darkslategrey": "#2f4f4f",
|
||||
"darkturquoise": "#00ced1",
|
||||
"darkviolet": "#9400d3",
|
||||
"deeppink": "#ff1493",
|
||||
"deepskyblue": "#00bfff",
|
||||
"dimgray": "#696969",
|
||||
"dimgrey": "#696969",
|
||||
"dodgerblue": "#1e90ff",
|
||||
"firebrick": "#b22222",
|
||||
"floralwhite": "#fffaf0",
|
||||
"forestgreen": "#228b22",
|
||||
"fuchsia": "#ff00ff",
|
||||
"gainsboro": "#dcdcdc",
|
||||
"ghostwhite": "#f8f8ff",
|
||||
"goldenrod": "#daa520",
|
||||
"gold": "#ffd700",
|
||||
"gray": "#808080",
|
||||
"green": "#008000",
|
||||
"greenyellow": "#adff2f",
|
||||
"grey": "#808080",
|
||||
"honeydew": "#f0fff0",
|
||||
"hotpink": "#ff69b4",
|
||||
"indianred": "#cd5c5c",
|
||||
"indigo": "#4b0082",
|
||||
"ivory": "#fffff0",
|
||||
"khaki": "#f0e68c",
|
||||
"lavenderblush": "#fff0f5",
|
||||
"lavender": "#e6e6fa",
|
||||
"lawngreen": "#7cfc00",
|
||||
"lemonchiffon": "#fffacd",
|
||||
"lightblue": "#add8e6",
|
||||
"lightcoral": "#f08080",
|
||||
"lightcyan": "#e0ffff",
|
||||
"lightgoldenrodyellow": "#fafad2",
|
||||
"lightgray": "#d3d3d3",
|
||||
"lightgreen": "#90ee90",
|
||||
"lightgrey": "#d3d3d3",
|
||||
"lightpink": "#ffb6c1",
|
||||
"lightsalmon": "#ffa07a",
|
||||
"lightseagreen": "#20b2aa",
|
||||
"lightskyblue": "#87cefa",
|
||||
"lightslategray": "#778899",
|
||||
"lightslategrey": "#778899",
|
||||
"lightsteelblue": "#b0c4de",
|
||||
"lightyellow": "#ffffe0",
|
||||
"lime": "#00ff00",
|
||||
"limegreen": "#32cd32",
|
||||
"linen": "#faf0e6",
|
||||
"magenta": "#ff00ff",
|
||||
"maroon": "#800000",
|
||||
"mediumaquamarine": "#66cdaa",
|
||||
"mediumblue": "#0000cd",
|
||||
"mediumorchid": "#ba55d3",
|
||||
"mediumpurple": "#9370db",
|
||||
"mediumseagreen": "#3cb371",
|
||||
"mediumslateblue": "#7b68ee",
|
||||
"mediumspringgreen": "#00fa9a",
|
||||
"mediumturquoise": "#48d1cc",
|
||||
"mediumvioletred": "#c71585",
|
||||
"midnightblue": "#191970",
|
||||
"mintcream": "#f5fffa",
|
||||
"mistyrose": "#ffe4e1",
|
||||
"moccasin": "#ffe4b5",
|
||||
"navajowhite": "#ffdead",
|
||||
"navy": "#000080",
|
||||
"oldlace": "#fdf5e6",
|
||||
"olive": "#808000",
|
||||
"olivedrab": "#6b8e23",
|
||||
"orange": "#ffa500",
|
||||
"orangered": "#ff4500",
|
||||
"orchid": "#da70d6",
|
||||
"palegoldenrod": "#eee8aa",
|
||||
"palegreen": "#98fb98",
|
||||
"paleturquoise": "#afeeee",
|
||||
"palevioletred": "#db7093",
|
||||
"papayawhip": "#ffefd5",
|
||||
"peachpuff": "#ffdab9",
|
||||
"peru": "#cd853f",
|
||||
"pink": "#ffc0cb",
|
||||
"plum": "#dda0dd",
|
||||
"powderblue": "#b0e0e6",
|
||||
"purple": "#800080",
|
||||
"red": "#ff0000",
|
||||
"rosybrown": "#bc8f8f",
|
||||
"royalblue": "#4169e1",
|
||||
"saddlebrown": "#8b4513",
|
||||
"salmon": "#fa8072",
|
||||
"sandybrown": "#f4a460",
|
||||
"seagreen": "#2e8b57",
|
||||
"seashell": "#fff5ee",
|
||||
"sienna": "#a0522d",
|
||||
"silver": "#c0c0c0",
|
||||
"skyblue": "#87ceeb",
|
||||
"slateblue": "#6a5acd",
|
||||
"slategray": "#708090",
|
||||
"slategrey": "#708090",
|
||||
"snow": "#fffafa",
|
||||
"springgreen": "#00ff7f",
|
||||
"steelblue": "#4682b4",
|
||||
"tan": "#d2b48c",
|
||||
"teal": "#008080",
|
||||
"thistle": "#d8bfd8",
|
||||
"tomato": "#ff6347",
|
||||
"turquoise": "#40e0d0",
|
||||
"violet": "#ee82ee",
|
||||
"wheat": "#f5deb3",
|
||||
"white": "#ffffff",
|
||||
"whitesmoke": "#f5f5f5",
|
||||
"yellow": "#ffff00",
|
||||
"yellowgreen": "#9acd32"
|
||||
}
|
||||
73
modules/module.getdata.js
Normal file
73
modules/module.getdata.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
const FormData = require('form-data');
|
||||
const got = require('got');
|
||||
|
||||
// do req
|
||||
const getData = async (options) => {
|
||||
let gOptions = {
|
||||
url: options.url,
|
||||
headers: {
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0',
|
||||
}
|
||||
};
|
||||
if(options.baseUrl){
|
||||
gOptions.prefixUrl = options.baseUrl;
|
||||
gOptions.url = gOptions.url.replace(/^\//,'');
|
||||
}
|
||||
if(options.querystring){
|
||||
gOptions.url += `?${new URLSearchParams(options.querystring).toString()}`;
|
||||
}
|
||||
if(options.auth){
|
||||
gOptions.method = 'POST';
|
||||
gOptions.body = new FormData();
|
||||
gOptions.body.append('username', options.auth.user);
|
||||
gOptions.body.append('password', options.auth.pass);
|
||||
}
|
||||
if(options.useToken && options.token){
|
||||
gOptions.headers.Authorization = `Token ${options.token}`;
|
||||
}
|
||||
if(options.dinstid){
|
||||
gOptions.headers.devicetype = 'Android Phone';
|
||||
}
|
||||
// debug
|
||||
gOptions.hooks = {
|
||||
beforeRequest: [
|
||||
(gotOpts) => {
|
||||
if(options.debug){
|
||||
console.log('[DEBUG] GOT OPTIONS:');
|
||||
console.log(gotOpts);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
try {
|
||||
let res = await got(gOptions);
|
||||
if(res.body && res.body.match(/^</)){
|
||||
throw { name: 'HTMLError', res };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
res,
|
||||
};
|
||||
}
|
||||
catch(error){
|
||||
if(options.debug){
|
||||
console.log(error);
|
||||
}
|
||||
if(error.response && error.response.statusCode && error.response.statusMessage){
|
||||
console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
|
||||
}
|
||||
else if(error.name && error.name == 'HTMLError' && error.res && error.res.body){
|
||||
console.log(`[ERROR] ${error.name}:`);
|
||||
console.log(error.res.body);
|
||||
}
|
||||
else{
|
||||
console.log(`[ERROR] ${error.name}: ${error.code||error.message}`);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = getData;
|
||||
182
modules/module.vttconvert.js
Normal file
182
modules/module.vttconvert.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// vtt loader
|
||||
function loadVtt(vttStr) {
|
||||
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
|
||||
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
|
||||
let data = [], lineBuf = [], record = null;
|
||||
// check lines
|
||||
for (let l of lines) {
|
||||
let m = l.match(rx);
|
||||
if (m) {
|
||||
if (lineBuf.length > 0) {
|
||||
lineBuf.pop();
|
||||
}
|
||||
if (record !== null) {
|
||||
record.text = lineBuf.join('\n');
|
||||
data.push(record);
|
||||
}
|
||||
record = {
|
||||
time_start: m[1],
|
||||
time_end: m[2],
|
||||
ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p, c) => (p[c[0]] = c[1]) && p, {}),
|
||||
};
|
||||
lineBuf = [];
|
||||
continue;
|
||||
}
|
||||
lineBuf.push(l);
|
||||
}
|
||||
if (record !== null) {
|
||||
if (lineBuf[lineBuf.length - 1] === '') {
|
||||
lineBuf.pop();
|
||||
}
|
||||
record.text = lineBuf.join('\n');
|
||||
data.push(record);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// ass specific
|
||||
function convertToAss(vttStr){
|
||||
let ass = [
|
||||
'\ufeff[Script Info]',
|
||||
'Title: English',
|
||||
'ScriptType: v4.00+',
|
||||
'PlayResX: 1280',
|
||||
'PlayResY: 720',
|
||||
'WrapStyle: 0',
|
||||
'ScaledBorderAndShadow: yes',
|
||||
'',
|
||||
'[V4+ Styles]',
|
||||
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, '
|
||||
+ 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, '
|
||||
+ 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
|
||||
'Style: Main,Noto Sans,55,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1',
|
||||
'Style: MainTop,Noto Sans,55,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1',
|
||||
'',
|
||||
'[Events]',
|
||||
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
|
||||
];
|
||||
|
||||
let vttData = loadVtt(vttStr);
|
||||
for (let l of vttData) {
|
||||
l = convertToAssLine(l, 'Main');
|
||||
ass = ass.concat(l);
|
||||
}
|
||||
|
||||
return ass.join('\r\n') + '\r\n';
|
||||
}
|
||||
|
||||
function convertToAssLine(l, style) {
|
||||
let start = convertTime(l.time_start);
|
||||
let end = convertTime(l.time_end);
|
||||
let text = convertToAssText(l.text);
|
||||
|
||||
// debugger
|
||||
if (l.ext_param.align != 'middle') {
|
||||
console.log('[WARN] Detected specific align param, please contact developer');
|
||||
cosnole.log(l);
|
||||
}
|
||||
if (l.ext_param.vertical) {
|
||||
console.log('[WARN] Detected specific vertical param, please contact developer');
|
||||
cosnole.log(l);
|
||||
}
|
||||
if (l.ext_param.line && l.ext_param.line != '7%') {
|
||||
console.log('[WARN] Detected specific line param, please contact developer');
|
||||
cosnole.log(l);
|
||||
}
|
||||
if (l.ext_param.position) {
|
||||
console.log('[WARN] Detected specific position param, please contact developer');
|
||||
cosnole.log(l);
|
||||
}
|
||||
if (l.text.match(/<font/)){
|
||||
console.log('[WARN] Detected specific color param, please contact developer');
|
||||
cosnole.log(l);
|
||||
}
|
||||
|
||||
if (l.ext_param.line === '7%') {
|
||||
style = 'MainTop';
|
||||
}
|
||||
|
||||
return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`;
|
||||
}
|
||||
|
||||
function convertToAssText(text) {
|
||||
text = text
|
||||
.replace(/\r/g, '')
|
||||
.replace(/\n/g, '\\N')
|
||||
.replace(/\\N +/g, '\\N')
|
||||
.replace(/ +\\N/g, '\\N')
|
||||
.replace(/(\\N)+/g, '\\N')
|
||||
.replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}')
|
||||
.replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}')
|
||||
.replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}')
|
||||
// .replace(/<c[^>]*>[^<]*<\/c>/g, '')
|
||||
// .replace(/<ruby[^>]*>[^<]*<\/ruby>/g, '')
|
||||
.replace(/<[^>]>/g, '')
|
||||
.replace(/\\N$/, '')
|
||||
.replace(/ +$/, '');
|
||||
return text;
|
||||
}
|
||||
|
||||
// srt specific
|
||||
function convertToSrt(vttStr){
|
||||
let srt = [], srtLineIdx = 0;
|
||||
|
||||
let vttData = loadVtt(vttStr);
|
||||
for (let l of vttData) {
|
||||
srtLineIdx++;
|
||||
l = convertToSrtLine(l, srtLineIdx);
|
||||
srt = srt.concat(l);
|
||||
}
|
||||
|
||||
return srt.join('\r\n') + '\r\n';
|
||||
}
|
||||
|
||||
function convertToSrtLine(l, idx) {
|
||||
let bom = idx == 1 ? '\ufeff' : '';
|
||||
let start = convertTime(l.time_start, true);
|
||||
let end = convertTime(l.time_end, true);
|
||||
let text = l.text;
|
||||
return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`;
|
||||
}
|
||||
|
||||
// time parser
|
||||
function convertTime(time, srtFormat) {
|
||||
let mTime = time.match(/([\d:]*)\.?(\d*)/);
|
||||
if (!mTime){
|
||||
return srtFormat ? '00:00:00,000' : '0:00:00.00';
|
||||
}
|
||||
return toSubsTime(mTime[0], srtFormat);
|
||||
}
|
||||
|
||||
function toSubsTime(str, srtFormat) {
|
||||
|
||||
let n = [], x, sx;
|
||||
x = str.split(/[:.]/).map(x => Number(x));
|
||||
|
||||
let msLen = srtFormat ? 3 : 2;
|
||||
let hLen = srtFormat ? 2 : 1;
|
||||
|
||||
x[3] = '0.' + ('' + x[3]).padStart(3, '0');
|
||||
sx = x[0]*60*60 + x[1]*60 + x[2] + Number(x[3])
|
||||
sx = sx.toFixed(msLen).split('.');
|
||||
|
||||
|
||||
n.unshift(padTimeNum('.', sx[1], msLen));
|
||||
sx = Number(sx[0]);
|
||||
|
||||
n.unshift(padTimeNum(':', sx%60, 2));
|
||||
n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2));
|
||||
n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen));
|
||||
|
||||
return n.join('');
|
||||
}
|
||||
|
||||
function padTimeNum(sep, input, pad){
|
||||
return sep + ('' + input).padStart(pad, '0');
|
||||
}
|
||||
|
||||
// export module
|
||||
module.exports = (vttStr, toSrt) => {
|
||||
const convert = toSrt ? convertToSrt : convertToAss;
|
||||
return convert(vttStr);
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "funimation-downloader-nx",
|
||||
"short_name": "funi",
|
||||
"version": "4.6.1",
|
||||
"version": "4.7.0-beta.1",
|
||||
"description": "Download videos from Funimation via cli.",
|
||||
"keywords": [
|
||||
"download",
|
||||
|
|
@ -29,11 +29,9 @@
|
|||
"hls-download": "^2.5.3",
|
||||
"lookpath": "^1.1.0",
|
||||
"m3u8-parsed": "^1.3.0",
|
||||
"proxy-agent": "^3.1.1",
|
||||
"sei-helper": "^3.3.0",
|
||||
"ttml2srt": "^1.2.0",
|
||||
"yaml": "^1.10.0",
|
||||
"yargs": "^16.0.3"
|
||||
"yargs": "^16.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fs-extra": "^9.0.1",
|
||||
|
|
|
|||
Loading…
Reference in a new issue