mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-01-11 20:10:20 +00:00
Starting with merge
This commit is contained in:
parent
79384bdc34
commit
4a83046116
22 changed files with 2892 additions and 4303 deletions
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Found a Bug? Than report it here :)
|
||||
title: "[BUG] <Short description here>"
|
||||
title: "[BUG] [Funimation/Crunchy] <Short description here>"
|
||||
labels: bug
|
||||
assignees: izu-co
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ assignees: izu-co
|
|||
** Main info: **
|
||||
|
||||
Script version:
|
||||
Service:
|
||||
Show ID:
|
||||
Episode ID:
|
||||
Command used:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 AniDL
|
||||
Copyright (c) 2019-2021 AniDL
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
1258
crunchy-beta.js
Normal file
1258
crunchy-beta.js
Normal file
File diff suppressed because it is too large
Load diff
31
crunchy/config/cli-defaults.yml
Normal file
31
crunchy/config/cli-defaults.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Downloading
|
||||
videoQuality: 720p
|
||||
nServer: 1
|
||||
kStream: 1
|
||||
tsparts: 10
|
||||
hsLang: none
|
||||
dlSubs: all
|
||||
|
||||
# Muxing
|
||||
dubLanguage: jpn
|
||||
defSubLang: none
|
||||
useBCPtags: false
|
||||
mp4mux: false
|
||||
muxSubs: false
|
||||
|
||||
# Filenaming
|
||||
filenameTemplate: "[{rel_group}] {title} - {ep_num} [{suffix}]"
|
||||
releaseGroup: CR
|
||||
epNumLength: 2
|
||||
fileSuffix: SIZEp
|
||||
|
||||
# Proxy
|
||||
proxy: ''
|
||||
proxy_auth: ''
|
||||
proxy_ups: false
|
||||
use_curl: false
|
||||
|
||||
# Utilities
|
||||
useFolder: false
|
||||
noCleanUp: false
|
||||
noTrashFolder: false
|
||||
77
crunchy/modules/build-test.js
Normal file
77
crunchy/modules/build-test.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// build requirements
|
||||
const pkg = require('../package.json');
|
||||
const fs = require('fs-extra');
|
||||
const { exec } = require('pkg');
|
||||
const { lookpath } = require('lookpath');
|
||||
|
||||
const buildsDir = './_builds';
|
||||
const curNodeVer = 'node16-';
|
||||
|
||||
// main
|
||||
(async function (){
|
||||
doBuild();
|
||||
})();
|
||||
|
||||
// do build
|
||||
async function doBuild(nodeVer){
|
||||
const buildStr = `${pkg.name}-${pkg.version}`;
|
||||
nodeVer = nodeVer ? nodeVer : '';
|
||||
const acceptableBuilds = ['win64','linux64','macos64'];
|
||||
const buildType = process.argv[2];
|
||||
if(!acceptableBuilds.includes(buildType)){
|
||||
console.error('[ERROR] unknown build type!');
|
||||
process.exit(1);
|
||||
}
|
||||
if(!fs.existsSync(buildsDir)){
|
||||
fs.mkdirSync(buildsDir);
|
||||
}
|
||||
const buildFull = `${buildStr}-${buildType}`;
|
||||
const buildDir = `${buildsDir}`;
|
||||
|
||||
const buildConfig = [
|
||||
pkg.main,
|
||||
'--target', nodeVer + getTarget(buildType),
|
||||
'--output', `${buildDir}/${pkg.short_name}`,
|
||||
];
|
||||
const buildConfigBeta = [
|
||||
`${pkg.short_name}-beta.js`,
|
||||
'--target', nodeVer + getTarget(buildType),
|
||||
'--output', `${buildDir}/${pkg.short_name}-beta`,
|
||||
];
|
||||
console.log(`[Build] Build configuration: ${buildFull}`);
|
||||
try {
|
||||
const targetCrClassic = await lookpath(`${buildDir}/${pkg.short_name}`);
|
||||
const targetCrBeta = await lookpath(`${buildDir}/${pkg.short_name}-beta`);
|
||||
if(!fs.existsSync(targetCrClassic)){
|
||||
fs.removeSync(targetCrClassic);
|
||||
}
|
||||
if(!fs.existsSync(targetCrBeta)){
|
||||
fs.removeSync(targetCrBeta);
|
||||
}
|
||||
await exec(buildConfig);
|
||||
await exec(buildConfigBeta);
|
||||
}
|
||||
catch(e){
|
||||
console.log(e);
|
||||
if(nodeVer == ''){
|
||||
await doBuild(curNodeVer);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('[LOG] Build ready:', buildFull);
|
||||
}
|
||||
|
||||
function getTarget(bt){
|
||||
switch(bt){
|
||||
case 'win64':
|
||||
return 'windows-x64';
|
||||
case 'linux64':
|
||||
return 'linux-x64';
|
||||
case 'macos64':
|
||||
return 'macos-x64';
|
||||
default:
|
||||
return 'windows-x64';
|
||||
}
|
||||
}
|
||||
80
crunchy/modules/build.js
Normal file
80
crunchy/modules/build.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// build requirements
|
||||
const pkg = require('../package.json');
|
||||
const fs = require('fs-extra');
|
||||
const { exec } = require('pkg');
|
||||
|
||||
const buildsDir = './_builds';
|
||||
const curNodeVer = 'node16-';
|
||||
|
||||
// main
|
||||
(async function (){
|
||||
doBuild();
|
||||
})();
|
||||
|
||||
// do build
|
||||
async function doBuild(nodeVer){
|
||||
const buildStr = `${pkg.name}-${pkg.version}`;
|
||||
nodeVer = nodeVer ? nodeVer : '';
|
||||
const acceptableBuilds = ['win64','linux64','macos64'];
|
||||
const buildType = process.argv[2];
|
||||
if(!acceptableBuilds.includes(buildType)){
|
||||
console.error('[ERROR] unknown build type!');
|
||||
process.exit(1);
|
||||
}
|
||||
if(!fs.existsSync(buildsDir)){
|
||||
fs.mkdirSync(buildsDir);
|
||||
}
|
||||
const buildFull = `${buildStr}-${buildType}`;
|
||||
const buildDir = `${buildsDir}/${buildFull}`;
|
||||
if(fs.existsSync(buildDir)){
|
||||
fs.removeSync(buildDir);
|
||||
}
|
||||
fs.mkdirSync(buildDir);
|
||||
const buildConfig = [
|
||||
pkg.main,
|
||||
'--target', nodeVer + getTarget(buildType),
|
||||
'--output', `${buildDir}/${pkg.short_name}`,
|
||||
];
|
||||
const buildConfigBeta = [
|
||||
`${pkg.short_name}-beta.js`,
|
||||
'--target', nodeVer + getTarget(buildType),
|
||||
'--output', `${buildDir}/${pkg.short_name}-beta`,
|
||||
];
|
||||
console.log(`[Build] Build configuration: ${buildFull}`);
|
||||
try {
|
||||
await exec(buildConfig);
|
||||
await exec(buildConfigBeta);
|
||||
}
|
||||
catch(e){
|
||||
console.log(e);
|
||||
if(nodeVer == ''){
|
||||
await doBuild(curNodeVer);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
if(buildType == 'win64'){
|
||||
fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
|
||||
}
|
||||
fs.copySync('./docs/', `${buildDir}/docs/`);
|
||||
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
|
||||
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
|
||||
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
|
||||
}
|
||||
require('child_process').execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`, {stdio:[0,1,2]});
|
||||
console.log('[LOG] Build ready:', `${buildsDir}/${buildFull}.7z`);
|
||||
}
|
||||
|
||||
function getTarget(bt){
|
||||
switch(bt){
|
||||
case 'win64':
|
||||
return 'windows-x64';
|
||||
case 'linux64':
|
||||
return 'linux-x64';
|
||||
case 'macos64':
|
||||
return 'macos-x64';
|
||||
default:
|
||||
return 'windows-x64';
|
||||
}
|
||||
}
|
||||
3
crunchy/modules/cmd-here.bat
Normal file
3
crunchy/modules/cmd-here.bat
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@echo off
|
||||
title CmdHere
|
||||
cmd /k PROMPT @$S$P$_$_$G$S
|
||||
122
crunchy/modules/module.eps-filter.js
Normal file
122
crunchy/modules/module.eps-filter.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
const epNumLen = { E: 4, S: 3, M: 7 };
|
||||
const maxRange = 1000;
|
||||
|
||||
// selectors
|
||||
const epRegex = new RegExp (/^(?:E?|S|M)(\d+)$/);
|
||||
const betaEpRegex = new RegExp (/^[0-9A-Z]{9}$/);
|
||||
const epLtReg = new RegExp (/(?:E|S|M)/);
|
||||
|
||||
class doFilter {
|
||||
constructor(){}
|
||||
ifMaxEp(type, num){
|
||||
const maxEp = Math.pow(10, epNumLen[type]) - 1;
|
||||
return num > maxEp ? true : false;
|
||||
}
|
||||
powNum(type){
|
||||
return Math.pow(10, epNumLen[type]);
|
||||
}
|
||||
checkFilter(inputEps){
|
||||
// check
|
||||
inputEps = typeof inputEps != 'undefined'
|
||||
? inputEps.toString().split(',') : [];
|
||||
// input range
|
||||
const inputEpsRange = [];
|
||||
|
||||
// filter wrong numbers
|
||||
inputEps = inputEps.map((e) => {
|
||||
// convert to uppercase
|
||||
e = e.toUpperCase();
|
||||
// if range
|
||||
if(e.match('-') && e.split('-').length == 2){
|
||||
const eRange = e.split('-');
|
||||
// check range
|
||||
if (!eRange[0].match(epRegex)) return '';
|
||||
// set ep latter and pad
|
||||
const epLetter = eRange[0].match(epLtReg) ? eRange[0].match(epLtReg)[0] : 'E';
|
||||
const padLen = epNumLen[epLetter];
|
||||
// parse range
|
||||
eRange[0] = eRange[0].replace(epLtReg, '');
|
||||
eRange[0] = parseInt(eRange[0]);
|
||||
eRange[0] = this.ifMaxEp(epLetter, eRange[0]) ? this.powNum(epLetter) - 1 : eRange[0];
|
||||
eRange[1] = eRange[1].match(/^\d+$/) ? parseInt(eRange[1]) : 0;
|
||||
eRange[1] = this.ifMaxEp(epLetter, eRange[1]) ? this.powNum(epLetter) - 1 : eRange[1];
|
||||
// check if correct range
|
||||
if (eRange[0] > eRange[1]){
|
||||
const parsedEl = [
|
||||
epLetter != 'E' ? epLetter : '',
|
||||
eRange[0].toString().padStart(padLen, '0'),
|
||||
].join('');
|
||||
return parsedEl;
|
||||
}
|
||||
if(eRange[1] - eRange[0] + 1 > maxRange){
|
||||
eRange[1] = eRange[0] + maxRange - 1;
|
||||
}
|
||||
const rangeLength = eRange[1] - eRange[0] + 1;
|
||||
const epsRangeArr = Array(rangeLength).fill(0);
|
||||
for(const i in epsRangeArr){
|
||||
const parsedRangeEl = [
|
||||
epLetter != 'E' ? epLetter : '',
|
||||
(parseInt(i) + eRange[0]).toString().padStart(padLen, '0'),
|
||||
].join('');
|
||||
inputEpsRange.push(parsedRangeEl);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
else if(e.match(epRegex)){
|
||||
const epLetter = e.match(epLtReg) ? e.match(epLtReg)[0] : 'E';
|
||||
const padLen = epNumLen[epLetter];
|
||||
e = parseInt(e.replace(epLtReg, ''));
|
||||
e = this.ifMaxEp(epLetter, e) ? this.powNum(epLetter) - 1 : e;
|
||||
return (epLetter != 'E' ? epLetter : '') + e.toString().padStart(padLen, '0');
|
||||
}
|
||||
else if(e.match(betaEpRegex)){
|
||||
return e;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
// end
|
||||
inputEps = [...new Set(inputEps.concat(inputEpsRange))];
|
||||
inputEps = inputEps.indexOf('') > -1 ? inputEps.slice(1) : inputEps;
|
||||
return inputEps;
|
||||
}
|
||||
checkMediaFilter(e){
|
||||
e = e.split(',');
|
||||
const epLetter = 'M';
|
||||
const inpMedia = [''];
|
||||
// map select
|
||||
e.map((e) => {
|
||||
if(e.match('-')){
|
||||
const eRange = e.split('-');
|
||||
if(eRange[0].match(/^m?\d+$/i)){
|
||||
eRange[0] = eRange[0].replace(/^m/i,'');
|
||||
eRange[0] = parseInt(eRange[0]);
|
||||
eRange[0] = this.ifMaxEp(epLetter, eRange[0]) ? this.powNum(epLetter) - 1 : eRange[0];
|
||||
inpMedia.push(eRange[0].toString());
|
||||
}
|
||||
}
|
||||
else if(e.match(/^m?\d+$/i)){
|
||||
const eMedia = parseInt(e.replace(/^m/i,''));
|
||||
const eMediaStr = this.ifMaxEp(epLetter, eMedia) ? this.powNum(epLetter) - 1 : eMedia;
|
||||
inpMedia.push(eMediaStr.toString());
|
||||
}
|
||||
});
|
||||
return [...new Set(inpMedia)].splice(1);
|
||||
}
|
||||
checkBetaFilter(e){
|
||||
e = ['', ...e.split(',')];
|
||||
e = e.map((e) => {
|
||||
if(e.match(betaEpRegex)){
|
||||
return e;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
e = [...new Set(e)].splice(1);
|
||||
e = e.length > 100 ? e.slice(0, 100) : e;
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
epNumLen,
|
||||
doFilter,
|
||||
};
|
||||
285
crunchy/modules/module.req.js
Normal file
285
crunchy/modules/module.req.js
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const shlp = require('sei-helper');
|
||||
const got = require('got');
|
||||
const cookieFile = require('./module.cookieFile');
|
||||
const yamlCfg = require('./module.cfg-loader');
|
||||
const curlReq = require('./module.curl-req');
|
||||
|
||||
// set usable cookies
|
||||
const usefulCookies = {
|
||||
auth: [
|
||||
'etp_rt',
|
||||
'c_visitor',
|
||||
],
|
||||
sess: [
|
||||
'session_id',
|
||||
],
|
||||
};
|
||||
|
||||
// req
|
||||
const Req = class {
|
||||
constructor(domain, argv, is_beta){
|
||||
// settings and cookies
|
||||
this.is_beta = Boolean(is_beta);
|
||||
this.loadSessTxt = this.is_beta ? false : true;
|
||||
// main cfg
|
||||
this.domain = domain;
|
||||
this.argv = argv;
|
||||
// session cfg
|
||||
this.sessCfg = yamlCfg.sessCfgFile,
|
||||
this.session = this.is_beta ? {} : yamlCfg.loadCRSession();
|
||||
this.cfgDir = yamlCfg.cfgFolder;
|
||||
this.curl = false;
|
||||
}
|
||||
async getData (durl, params) {
|
||||
params = params || {};
|
||||
// options
|
||||
let options = {
|
||||
method: params.method ? params.method : 'GET',
|
||||
headers: {
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:90.0) Gecko/20100101 Firefox/90.0',
|
||||
},
|
||||
};
|
||||
// additional params
|
||||
if(params.headers){
|
||||
options.headers = {...options.headers, ...params.headers};
|
||||
}
|
||||
if(options.method == 'POST'){
|
||||
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
if(params.body){
|
||||
options.body = params.body;
|
||||
}
|
||||
if(params.binary == true){
|
||||
options.responseType = 'buffer';
|
||||
}
|
||||
if(typeof params.followRedirect == 'boolean'){
|
||||
options.followRedirect = params.followRedirect;
|
||||
}
|
||||
// check if cookies.txt exists
|
||||
const sessTxtFile = path.join(this.cfgDir, 'cookies.txt');
|
||||
if(!this.is_beta && this.loadSessTxt && fs.existsSync(sessTxtFile)){
|
||||
const cookiesTxtName = path.basename(sessTxtFile);
|
||||
try{
|
||||
// console.log(`[INFO] Loading custom ${cookiesTxtName} file...`);
|
||||
const netcookie = fs.readFileSync(sessTxtFile, 'utf8');
|
||||
fs.unlinkSync(sessTxtFile);
|
||||
this.setNewCookie('', true, netcookie);
|
||||
}
|
||||
catch(e){
|
||||
console.log(`[ERROR] Cannot load ${cookiesTxtName} file!`);
|
||||
}
|
||||
}
|
||||
this.loadSessTxt = false;
|
||||
// proxy
|
||||
if(params.useProxy && this.argv.proxy && this.argv.curl){
|
||||
try{
|
||||
options.curlProxy = buildProxy(this.argv.proxy);
|
||||
options.curlProxyAuth = this.argv['proxy-auth'];
|
||||
}
|
||||
catch(e){
|
||||
console.log(`[WARN] Not valid proxy URL${e.input?' ('+e.input+')':''}!`);
|
||||
console.log('[WARN] Skipping...\n');
|
||||
this.argv.proxy = false;
|
||||
}
|
||||
}
|
||||
// if auth
|
||||
let cookie = [];
|
||||
const loc = new URL(durl);
|
||||
if(!this.is_beta && Object.values(this.domain).includes(loc.origin)){
|
||||
for(let uCookie of usefulCookies.auth){
|
||||
const checkedCookie = this.checkCookieVal(this.session[uCookie]);
|
||||
if(checkedCookie){
|
||||
cookie.push(uCookie);
|
||||
}
|
||||
}
|
||||
for(let uCookie of usefulCookies.sess){
|
||||
if(this.checkSessId(this.session[uCookie]) && !this.argv.nosess){
|
||||
cookie.push(uCookie);
|
||||
}
|
||||
}
|
||||
if(!params.skipCookies){
|
||||
cookie.push('c_locale');
|
||||
options.headers.Cookie = shlp.cookie.make({
|
||||
...{ c_locale : { value: 'enUS' } },
|
||||
...this.session,
|
||||
}, cookie);
|
||||
}
|
||||
}
|
||||
// avoid cloudflare protection
|
||||
if(loc.origin == this.domain.www){
|
||||
options.minVersion = 'TLSv1.3';
|
||||
options.maxVersion = 'TLSv1.3';
|
||||
options.http2 = true;
|
||||
}
|
||||
// debug
|
||||
options.hooks = {
|
||||
beforeRequest: [
|
||||
(options) => {
|
||||
if(this.argv.debug){
|
||||
console.log('[DEBUG] GOT OPTIONS:');
|
||||
console.log(options);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
if(this.argv.debug){
|
||||
options.curlDebug = true;
|
||||
}
|
||||
// try do request
|
||||
try {
|
||||
let res;
|
||||
if(this.curl && this.argv.curl && Object.values(this.domain).includes(loc.origin)){
|
||||
res = await curlReq(this.curl, durl.toString(), options, this.cfgDir);
|
||||
}
|
||||
else{
|
||||
res = await got(durl.toString(), options);
|
||||
}
|
||||
if(!this.is_beta && !params.skipCookies && res && res.headers && res.headers['set-cookie']){
|
||||
this.setNewCookie(res.headers['set-cookie'], false);
|
||||
for(let uCookie of usefulCookies.sess){
|
||||
if(this.session[uCookie] && this.argv.nosess){
|
||||
this.argv.nosess = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
res,
|
||||
};
|
||||
}
|
||||
catch(error){
|
||||
if(error.response && error.response.statusCode && error.response.statusMessage){
|
||||
console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
|
||||
}
|
||||
else{
|
||||
console.log(`[ERROR] ${error.name}: ${error.code || error.message}`);
|
||||
}
|
||||
if(error.response && !error.res){
|
||||
error.res = error.response;
|
||||
const docTitle = error.res.body.match(/<title>(.*)<\/title>/);
|
||||
if(error.res.body && docTitle){
|
||||
console.log('[ERROR]', docTitle[1]);
|
||||
}
|
||||
}
|
||||
if(error.res && error.res.body && error.response.statusCode
|
||||
&& error.response.statusCode != 404 && error.response.statusCode != 403){
|
||||
console.log('[ERROR] Body:', error.res.body);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
setNewCookie(setCookie, isAuth, fileData){
|
||||
let cookieUpdated = [], lastExp = 0;
|
||||
setCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie);
|
||||
for(let cookieName of Object.keys(setCookie)){
|
||||
if(setCookie[cookieName] && setCookie[cookieName].value && setCookie[cookieName].value == 'deleted'){
|
||||
delete setCookie[cookieName];
|
||||
}
|
||||
}
|
||||
for(let uCookie of usefulCookies.auth){
|
||||
const cookieForceExp = 60*60*24*7;
|
||||
const cookieExpCur = this.session[uCookie] ? this.session[uCookie] : { expires: 0 };
|
||||
const cookieExp = new Date(cookieExpCur.expires).getTime() - cookieForceExp;
|
||||
if(cookieExp > lastExp){
|
||||
lastExp = cookieExp;
|
||||
}
|
||||
}
|
||||
for(let uCookie of usefulCookies.auth){
|
||||
if(!setCookie[uCookie]){
|
||||
continue;
|
||||
}
|
||||
if(isAuth || setCookie[uCookie] && Date.now() > lastExp){
|
||||
this.session[uCookie] = setCookie[uCookie];
|
||||
cookieUpdated.push(uCookie);
|
||||
}
|
||||
}
|
||||
for(let uCookie of usefulCookies.sess){
|
||||
if(!setCookie[uCookie]){
|
||||
continue;
|
||||
}
|
||||
if(
|
||||
isAuth
|
||||
|| this.argv.nosess && setCookie[uCookie]
|
||||
|| setCookie[uCookie] && !this.checkSessId(this.session[uCookie])
|
||||
){
|
||||
const sessionExp = 60*60;
|
||||
this.session[uCookie] = setCookie[uCookie];
|
||||
this.session[uCookie].expires = new Date(Date.now() + sessionExp*1000);
|
||||
this.session[uCookie]['Max-Age'] = sessionExp.toString();
|
||||
cookieUpdated.push(uCookie);
|
||||
}
|
||||
}
|
||||
if(cookieUpdated.length > 0){
|
||||
if(this.argv.debug){
|
||||
console.log('[SAVING FILE]',`${this.sessCfg}.yml`);
|
||||
}
|
||||
yamlCfg.saveCRSession(this.session);
|
||||
console.log(`[INFO] Cookies were updated! (${cookieUpdated.join(', ')})\n`);
|
||||
}
|
||||
}
|
||||
checkCookieVal(chcookie){
|
||||
return chcookie
|
||||
&& chcookie.toString() == '[object Object]'
|
||||
&& typeof chcookie.value == 'string'
|
||||
? true : false;
|
||||
}
|
||||
checkSessId(session_id){
|
||||
if(session_id && typeof session_id.expires == 'string'){
|
||||
session_id.expires = new Date(session_id.expires);
|
||||
}
|
||||
return session_id
|
||||
&& session_id.toString() == '[object Object]'
|
||||
&& typeof session_id.expires == 'object'
|
||||
&& Date.now() < new Date(session_id.expires).getTime()
|
||||
&& typeof session_id.value == 'string'
|
||||
? true : false;
|
||||
}
|
||||
uuidv4(){
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildProxy,
|
||||
usefulCookies,
|
||||
Req,
|
||||
};
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# Funimation Downloader NX
|
||||
# Anime Downloader NX by AniDL
|
||||
|
||||
Funimation Downloader NX is capable of downloading videos from the *Funimation* streaming service.
|
||||
This downloader can download anime from diffrent sites. Currently supported are *Funimation* and *Crunchyroll*.
|
||||
|
||||
## Legal Warning
|
||||
|
||||
This application is not endorsed by or affiliated with *Funimation*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||
This application is not endorsed by or affiliated with *Funimation* or *Crunchyroll*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
|
|
|||
1
index.ts
Normal file
1
index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
import { appArgv } from "./modules/module.app-args";
|
||||
73
modules/module.api-urls.ts
Normal file
73
modules/module.api-urls.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// api domains
|
||||
const domain = {
|
||||
www: 'https://www.crunchyroll.com',
|
||||
api: 'https://api.crunchyroll.com',
|
||||
www_beta: 'https://beta.crunchyroll.com',
|
||||
api_beta: 'https://beta-api.crunchyroll.com',
|
||||
};
|
||||
|
||||
export type APIType = {
|
||||
newani: string,
|
||||
search1: string,
|
||||
search2: string,
|
||||
rss_cid: string,
|
||||
rss_gid: string
|
||||
media_page: string
|
||||
series_page: string
|
||||
auth: string
|
||||
// mobile api
|
||||
search3: string
|
||||
session: string
|
||||
collections: string
|
||||
// beta api
|
||||
beta_auth: string
|
||||
beta_authBasic: string
|
||||
beta_authBasicMob: string
|
||||
beta_profile: string
|
||||
beta_cmsToken: string
|
||||
beta_search: string
|
||||
beta_browse: string
|
||||
beta_cms: string,
|
||||
beta_authHeader: HeadersInit,
|
||||
beta_authHeaderMob: HeadersInit
|
||||
}
|
||||
|
||||
// api urls
|
||||
const api: APIType = {
|
||||
// web
|
||||
newani: `${domain.www}/rss/anime`,
|
||||
search1: `${domain.www}/ajax/?req=RpcApiSearch_GetSearchCandidates`,
|
||||
search2: `${domain.www}/search_page`,
|
||||
rss_cid: `${domain.www}/syndication/feed?type=episodes&id=`, // &lang=enUS
|
||||
rss_gid: `${domain.www}/syndication/feed?type=episodes&group_id=`, // &lang=enUS
|
||||
media_page: `${domain.www}/media-`,
|
||||
series_page: `${domain.www}/series-`,
|
||||
auth: `${domain.www}/login`,
|
||||
// mobile api
|
||||
search3: `${domain.api}/autocomplete.0.json`,
|
||||
session: `${domain.api}/start_session.0.json`,
|
||||
collections: `${domain.api}/list_collections.0.json`,
|
||||
// beta api
|
||||
beta_auth: `${domain.api_beta}/auth/v1/token`,
|
||||
beta_authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6',
|
||||
beta_authBasicMob: 'Basic YTZ5eGxvYW04c2VqaThsZDhldnc6aFQ3d2FjWHhNaURJcDhSNE9kekJybWVoQUtLTEVKUEE=',
|
||||
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
|
||||
beta_cmsToken: `${domain.api_beta}/index/v2`,
|
||||
beta_search: `${domain.api_beta}/content/v1/search`,
|
||||
beta_browse: `${domain.api_beta}/content/v1/browse`,
|
||||
beta_cms: `${domain.api_beta}/cms/v2`,
|
||||
beta_authHeader: {},
|
||||
beta_authHeaderMob: {}
|
||||
};
|
||||
|
||||
// set header
|
||||
api.beta_authHeader = {
|
||||
Authorization: api.beta_authBasic,
|
||||
};
|
||||
api.beta_authHeaderMob = {
|
||||
Authorization: api.beta_authBasicMob,
|
||||
};
|
||||
|
||||
export {
|
||||
domain, api
|
||||
};
|
||||
|
|
@ -1,4 +1,18 @@
|
|||
import yargs from 'yargs';
|
||||
import * as langsData from "./module.langsData";
|
||||
|
||||
yargs(process.argv.slice(2));
|
||||
|
||||
const groups = {
|
||||
'auth': 'Authentication:',
|
||||
'fonts': 'Fonts:',
|
||||
'search': 'Search:',
|
||||
'dl': 'Downloading:',
|
||||
'mux': 'Muxing:',
|
||||
'fileName': 'Filename Template:',
|
||||
'debug': 'Debug:',
|
||||
'util': 'Utilities:'
|
||||
}
|
||||
|
||||
const availableFilenameVars = [
|
||||
'title',
|
||||
|
|
@ -6,7 +20,8 @@ const availableFilenameVars = [
|
|||
'showTitle',
|
||||
'season',
|
||||
'width',
|
||||
'height'
|
||||
'height',
|
||||
'service'
|
||||
];
|
||||
|
||||
export type possibleDubs = (
|
||||
|
|
@ -18,238 +33,235 @@ export type possibleSubs = (
|
|||
const subLang: possibleSubs = ['enUS', 'esLA', 'ptBR'];
|
||||
const dubLang: possibleDubs = ['enUS', 'esLA', 'ptBR', 'zhMN', 'jaJP'];
|
||||
|
||||
|
||||
const appArgv = (cfg: {
|
||||
[key: string]: unknown
|
||||
[key: string]: unknown
|
||||
}) => {
|
||||
// init
|
||||
const parseDefault = <T = unknown>(key: string, _default: T) : T=> {
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
|
||||
return cfg[key] as T;
|
||||
} else
|
||||
return _default;
|
||||
};
|
||||
const argv = yargs.parserConfiguration({
|
||||
'duplicate-arguments-array': true,
|
||||
'camel-case-expansion': false
|
||||
})
|
||||
// main
|
||||
.wrap(Math.min(120)) // yargs.terminalWidth()
|
||||
.help(false).version(false)
|
||||
.usage('Usage: $0 [options]')
|
||||
// auth
|
||||
.option('auth', {
|
||||
group: 'Authentication:',
|
||||
describe: 'Enter authentication mode',
|
||||
type: 'boolean',
|
||||
})
|
||||
// search
|
||||
.option('search', {
|
||||
alias: 'f',
|
||||
group: 'Search:',
|
||||
describe: 'Search show ids',
|
||||
type: 'string',
|
||||
})
|
||||
// select show and eps
|
||||
.option('s', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Sets the show id',
|
||||
type: 'number',
|
||||
})
|
||||
.option('e', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Select episode ids (comma-separated, hyphen-sequence)',
|
||||
type: 'string',
|
||||
})
|
||||
.option('all', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Used to download all episodes from the show',
|
||||
type: 'boolean',
|
||||
default: parseDefault<boolean>('all', false)
|
||||
})
|
||||
.option('partsize', {
|
||||
group: 'Downloading:',
|
||||
describe: 'The amount of parts that should be downloaded in paralell',
|
||||
type: 'number',
|
||||
default: parseDefault<number>('partsize', 10)
|
||||
})
|
||||
// quality
|
||||
.option('q', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Select video layer (0 is max)',
|
||||
choices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
default: parseDefault<number>('videoLayer', 7),
|
||||
type: 'number',
|
||||
})
|
||||
// alt listing
|
||||
.option('alt', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Alternative episode listing (if available)',
|
||||
default: parseDefault<boolean>('altList', false),
|
||||
type: 'boolean',
|
||||
})
|
||||
// switch to subs
|
||||
.option('dub', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Download non-Japanese Dub (English Dub mode by default)',
|
||||
choices: dubLang,
|
||||
default: parseDefault<possibleDubs>('dub', ['enUS']),
|
||||
type: 'array',
|
||||
})
|
||||
.option('subLang', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Set the subtitle language (English is default and fallback)',
|
||||
default: parseDefault<possibleSubs>('subLang', ['enUS']),
|
||||
choices: subLang,
|
||||
type: 'array'
|
||||
})
|
||||
.option('fontSize', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Used to set the fontsize of the subtitles',
|
||||
default: parseDefault<number>('fontSize', 55),
|
||||
type: 'number'
|
||||
})
|
||||
.option('allSubs', {
|
||||
group: 'Downloading:',
|
||||
describe: 'If set to true, all available subs will get downloaded',
|
||||
default: false,
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('allDubs', {
|
||||
group: 'Downloading:',
|
||||
describe: 'If set to true, all available dubs will get downloaded',
|
||||
default: false,
|
||||
type: 'boolean'
|
||||
})
|
||||
// simulcast
|
||||
.option('simul', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Force downloading simulcast ver. instead of uncut ver. (if uncut ver. available)',
|
||||
default: parseDefault<boolean>('forceSimul', false),
|
||||
type: 'boolean',
|
||||
})
|
||||
// server number
|
||||
.option('x', {
|
||||
alias: 'server',
|
||||
group: 'Downloading:',
|
||||
describe: 'Select server',
|
||||
choices: [1, 2, 3, 4],
|
||||
default: parseDefault<number>('nServer', 1),
|
||||
type: 'number',
|
||||
})
|
||||
// skip
|
||||
.option('noaudio', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Skip downloading audio',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('novids', {
|
||||
group: 'Downloading:',
|
||||
alias: 'skipdl',
|
||||
describe: 'Skip downloading video',
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('nosubs', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Skip downloading subtitles for English Dub (if available)',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
})
|
||||
// proxy
|
||||
.option('proxy', {
|
||||
group: 'Proxy:',
|
||||
describe: 'Set http(s)/socks proxy WHATWG url',
|
||||
default: parseDefault<boolean>('proxy', false),
|
||||
hidden: true,
|
||||
})
|
||||
.option('proxy-auth', {
|
||||
group: 'Proxy:',
|
||||
describe: 'Colon-separated username and password for proxy',
|
||||
default: parseDefault<string|boolean>('proxy_auth', false),
|
||||
hidden: true,
|
||||
})
|
||||
.option('ssp', {
|
||||
group: 'Proxy:',
|
||||
describe: 'Don\'t use proxy for stream and subtitles downloading',
|
||||
default: parseDefault<boolean>('proxy_ssp', false),
|
||||
hidden: true,
|
||||
type: 'boolean',
|
||||
})
|
||||
// muxing
|
||||
.option('skipmux', {
|
||||
group: 'Muxing:',
|
||||
describe: 'Skip muxing video and subtitles',
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('mp4', {
|
||||
group: 'Muxing:',
|
||||
describe: 'Mux into mp4',
|
||||
default: parseDefault<boolean>('mp4mux', false),
|
||||
type: 'boolean'
|
||||
})
|
||||
// filenaming
|
||||
.option('fileName', {
|
||||
group: 'Filename Template:',
|
||||
describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou may use ${availableFilenameVars
|
||||
.map(a => `'${a}'`).join(', ')} as variables.`,
|
||||
type: 'string',
|
||||
default: parseDefault<string>('fileName', '[Funimation] ${showTitle} - ${episode} [${height}p]')
|
||||
})
|
||||
.option('numbers', {
|
||||
group: 'Filename Template:',
|
||||
describe: `Set how long a number in the title should be at least.\n${[[3, 5, '005'], [2, 1, '01'], [1, 20, '20']]
|
||||
.map(val => `Set in config: ${val[0]}; Episode number: ${val[1]}; Output: ${val[2]}`).join('\n')}`,
|
||||
type: 'number',
|
||||
default: parseDefault<number>('numbers', 2)
|
||||
})
|
||||
// util
|
||||
.option('nocleanup', {
|
||||
group: 'Utilities:',
|
||||
describe: 'Dont\'t delete the input files after muxing',
|
||||
default: parseDefault<boolean>('noCleanUp', false),
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('timeout', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Set the timeout of all download reqests. Set in millisecods',
|
||||
type: 'number',
|
||||
default: parseDefault('timeout', 60 * 1000)
|
||||
})
|
||||
.option('debug', {
|
||||
group: 'Utilities:',
|
||||
describe: 'Used to enter debug mode. Please use this flag when opening an issue to get more information'
|
||||
+ '\n!Be careful! - Your token might be exposed so make sure to delete it!',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
})
|
||||
// help
|
||||
.option('help', {
|
||||
alias: 'h',
|
||||
group: 'Help:',
|
||||
describe: 'Show this help',
|
||||
type: 'boolean'
|
||||
})
|
||||
// usage
|
||||
.example([
|
||||
['$0 --search "My Hero"', 'search "My Hero" in title'],
|
||||
['$0 -s 124389 -e 1,2,3', 'download episodes 1-3 from show with id 124389'],
|
||||
['$0 -s 124389 -e 1-3,2-7,s1-2', 'download episodes 1-7 and "S"-episodes 1-2 from show with id 124389'],
|
||||
])
|
||||
|
||||
// --
|
||||
.parseSync();
|
||||
// Resolve unwanted arrays
|
||||
if (argv.allDubs)
|
||||
argv.dub = dubLang;
|
||||
if (argv.allSubs)
|
||||
argv.subLang = subLang;
|
||||
for (const key in argv) {
|
||||
if (argv[key] instanceof Array && !(key === 'subLang' || key === 'dub')) {
|
||||
argv[key] = (argv[key] as Array<unknown>).pop();
|
||||
}
|
||||
}
|
||||
return argv;
|
||||
};
|
||||
const argv = yargs.parserConfiguration({
|
||||
"duplicate-arguments-array": false,
|
||||
"camel-case-expansion": false
|
||||
})
|
||||
.wrap(yargs.terminalWidth())
|
||||
.usage('Usage: $0 [options]')
|
||||
.help(false).version(false)
|
||||
.option('auth', {
|
||||
group: groups.auth,
|
||||
describe: 'Enter authentication mode',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('dlFonts', {
|
||||
group: groups.fonts,
|
||||
describe: 'Download all required fonts for mkv muxing',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('search', {
|
||||
group: groups.search,
|
||||
alias: 'f',
|
||||
describe: 'Search for an anime',
|
||||
type: 'string'
|
||||
})
|
||||
.option('search-type', {
|
||||
group: groups.search,
|
||||
describe: 'Search type used for crunchyroll',
|
||||
choices: [ '', 'top_results', 'series', 'movie_listing', 'episode' ],
|
||||
default: '',
|
||||
type: 'string',
|
||||
})
|
||||
.option('page', {
|
||||
group: groups.search,
|
||||
alias: 'p',
|
||||
describe: 'Page number for search results',
|
||||
type: 'number',
|
||||
})
|
||||
.option('search-locale', {
|
||||
group: groups.search,
|
||||
describe: 'Search locale used for crunchyroll',
|
||||
choices: langsData.searchLocales,
|
||||
default: '',
|
||||
type: 'string',
|
||||
})
|
||||
.option('new', {
|
||||
group: groups.dl,
|
||||
describe: 'Get last updated series list from crunchyroll',
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('movie-listing', {
|
||||
group: groups.dl,
|
||||
alias: 'flm',
|
||||
describe: 'Get video list by Movie Listing ID',
|
||||
type: 'string',
|
||||
})
|
||||
.option('series', {
|
||||
group: groups.dl,
|
||||
alias: 'srz',
|
||||
describe: 'Get season list by Series ID',
|
||||
type: 'string',
|
||||
})
|
||||
.option('s', {
|
||||
group: groups.dl,
|
||||
describe: 'Set the season ID',
|
||||
type: 'string'
|
||||
})
|
||||
.option('e', {
|
||||
group: groups.dl,
|
||||
describe: 'Sets the Episode Number/IDs (comma-separated, hyphen-sequence)',
|
||||
type: 'string',
|
||||
})
|
||||
.option('q', {
|
||||
group: groups.dl,
|
||||
describe: 'Set the quality layer. Use 0 to get the best quality.',
|
||||
default: parseDefault<number>('videoLayer', 7),
|
||||
type: 'number'
|
||||
})
|
||||
.option('server', {
|
||||
group: groups.dl,
|
||||
alias: 'x',
|
||||
describe: 'Select server',
|
||||
choices: [1, 2, 3, 4],
|
||||
default: parseDefault<number>('nServer', 1),
|
||||
type: 'number',
|
||||
})
|
||||
.option('kstream', {
|
||||
group: groups.dl,
|
||||
alias: 'k',
|
||||
describe: 'Select specific stream for crunchyroll',
|
||||
choices: [1, 2, 3, 4, 5, 6, 7],
|
||||
default: parseDefault<number>('kStream', 1),
|
||||
type: 'number',
|
||||
})
|
||||
.option('partsize', {
|
||||
group: groups.dl,
|
||||
describe: 'Set the amount of parts that should be downloaded in paralell',
|
||||
type: 'number',
|
||||
default: parseDefault<number>('partsize', 10)
|
||||
})
|
||||
.option('hsland', {
|
||||
group: groups.dl,
|
||||
describe: 'Download video with specific hardsubs',
|
||||
choices: langsData.subtitleLanguagesFilter.slice(1),
|
||||
default: parseDefault<string>('hsLang', 'none'),
|
||||
type: 'string',
|
||||
})
|
||||
.option('subLang', {
|
||||
group: groups.dl,
|
||||
describe: 'Set the subtitles to download (Funi only)',
|
||||
choices: subLang,
|
||||
default: parseDefault<string[]>('subLang', []),
|
||||
type: 'array'
|
||||
})
|
||||
.option('novids', {
|
||||
group: groups.dl,
|
||||
describe: 'Skip downloading videos',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('noaudio', {
|
||||
group: groups.dl,
|
||||
describe: 'Skip downloading audio',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('nosubs', {
|
||||
group: groups.dl,
|
||||
describe: 'Skip downloading subtitles',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('dub', {
|
||||
group: groups.dl,
|
||||
describe: 'Set languages to download (funi only)',
|
||||
choices: dubLang,
|
||||
default: parseDefault<possibleDubs>('dub', ['enUS']),
|
||||
type: 'array'
|
||||
})
|
||||
.option('all', {
|
||||
group: groups.dl,
|
||||
describe: 'Used to download all episodes from the show (Funi only)',
|
||||
type: 'boolean',
|
||||
default: parseDefault<boolean>('all', false)
|
||||
})
|
||||
.option('fontSize', {
|
||||
group: groups.dl,
|
||||
describe: 'Used to set the fontsize of the subtitles',
|
||||
default: parseDefault<number>('fontSize', 55),
|
||||
type: 'number'
|
||||
})
|
||||
.option('allSubs', {
|
||||
group: groups.dl,
|
||||
describe: 'If set to true, all available subs will get downloaded (Funi only)',
|
||||
default: false,
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('allDubs', {
|
||||
group: groups.dl,
|
||||
describe: 'If set to true, all available dubs will get downloaded (Funi only)',
|
||||
default: false,
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('timeout', {
|
||||
group: groups.dl,
|
||||
describe: 'Set the timeout of all download reqests. Set in millisecods',
|
||||
type: 'number',
|
||||
default: parseDefault('timeout', 60 * 1000)
|
||||
})
|
||||
.option('simul', {
|
||||
group: groups.dl,
|
||||
describe: 'Force downloading simulcast ver. instead of uncut ver. (if uncut ver. available) (Funi only)',
|
||||
default: parseDefault<boolean>('forceSimul', false),
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('mp4', {
|
||||
group: groups.mux,
|
||||
describe: 'Mux video into mp4',
|
||||
default: parseDefault<boolean>('mp4mux', false),
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('skipmux', {
|
||||
group: groups.mux,
|
||||
describe: 'Skip muxing video and subtitles',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('fileName', {
|
||||
group: groups.fileName,
|
||||
describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou may use ${availableFilenameVars
|
||||
.map(a => `'${a}'`).join(', ')} as variables.`,
|
||||
type: 'string',
|
||||
default: parseDefault<string>('fileName', '[${service}] ${showTitle} - ${episode} [${height}p]')
|
||||
})
|
||||
.option('numbers', {
|
||||
group: groups.fileName,
|
||||
describe: `Set how long a number in the title should be at least.\n${[[3, 5, '005'], [2, 1, '01'], [1, 20, '20']]
|
||||
.map(val => `Set in config: ${val[0]}; Episode number: ${val[1]}; Output: ${val[2]}`).join('\n')}`,
|
||||
type: 'number',
|
||||
default: parseDefault<number>('numbers', 2)
|
||||
})
|
||||
.option('nosess', {
|
||||
group: groups.debug,
|
||||
type: 'boolean',
|
||||
default: 'Reset session cookie for testing purposes'
|
||||
})
|
||||
.option('debug', {
|
||||
group: groups.debug,
|
||||
describe: 'Debug mode (tokens may be revield in the console output)',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('nocleanup', {
|
||||
group: groups.util,
|
||||
describe: 'Don\'t delete subtitles and videos after muxing',
|
||||
default: parseDefault<boolean>('noCleanUp', false),
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('help', {
|
||||
alias: 'h',
|
||||
group: 'Help:',
|
||||
describe: 'Show this help',
|
||||
type: 'boolean'
|
||||
})
|
||||
.parseSync();
|
||||
}
|
||||
|
||||
const showHelp = yargs.showHelp;
|
||||
|
||||
|
|
@ -257,6 +269,6 @@ export {
|
|||
appArgv,
|
||||
showHelp,
|
||||
availableFilenameVars,
|
||||
dubLang,
|
||||
subLang
|
||||
};
|
||||
subLang,
|
||||
dubLang
|
||||
}
|
||||
|
|
@ -7,10 +7,14 @@ import { lookpath } from 'lookpath';
|
|||
const workingDir = (process as NodeJS.Process & {
|
||||
pkg?: unknown
|
||||
}).pkg ? path.dirname(process.execPath) : path.join(__dirname, '/..');
|
||||
const binCfgFile = path.join(workingDir, 'config', 'bin-path');
|
||||
const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
|
||||
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
||||
const tokenFile = path.join(workingDir, 'config', 'token');
|
||||
const binCfgFile = path.join(workingDir, 'config', 'bin-path');
|
||||
const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
|
||||
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
||||
const sessCfgFile = path.join(workingDir, 'config', 'session');
|
||||
const tokenFile = {
|
||||
funi: path.join(workingDir, 'config', 'funi_token'),
|
||||
cr: path.join(workingDir, 'config', 'cr_token')
|
||||
};
|
||||
|
||||
const loadYamlCfgFile = <T extends Record<string, any>>(file: string, isSess?: boolean): T => {
|
||||
if(fs.existsSync(`${file}.user.yml`) && !isSess){
|
||||
|
|
@ -49,13 +53,13 @@ const loadCfg = () : ConfigObject => {
|
|||
const defaultCfg: ConfigObject = {
|
||||
bin: {},
|
||||
dir: loadYamlCfgFile<{
|
||||
content: string,
|
||||
trash: string,
|
||||
fonts: string
|
||||
}>(dirCfgFile),
|
||||
content: string,
|
||||
trash: string,
|
||||
fonts: string
|
||||
}>(dirCfgFile),
|
||||
cli: loadYamlCfgFile<{
|
||||
[key: string]: any
|
||||
}>(cliCfgFile),
|
||||
[key: string]: any
|
||||
}>(cliCfgFile),
|
||||
};
|
||||
const defaultDirs = {
|
||||
fonts: '${wdir}/fonts/',
|
||||
|
|
@ -65,7 +69,7 @@ const loadCfg = () : ConfigObject => {
|
|||
if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) {
|
||||
defaultCfg.dir = defaultDirs;
|
||||
}
|
||||
|
||||
|
||||
const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[];
|
||||
for (const key of keys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') {
|
||||
|
|
@ -75,7 +79,7 @@ const loadCfg = () : ConfigObject => {
|
|||
defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, ''));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(!fs.existsSync(defaultCfg.dir.content)){
|
||||
try{
|
||||
fs.ensureDirSync(defaultCfg.dir.content);
|
||||
|
|
@ -118,14 +122,57 @@ const loadBinCfg = async () => {
|
|||
return binCfg;
|
||||
};
|
||||
|
||||
const loadCRSession = () => {
|
||||
let session = loadYamlCfgFile(sessCfgFile, true);
|
||||
if(typeof session !== 'object' || session === null || Array.isArray(session)){
|
||||
session = {};
|
||||
}
|
||||
for(const cv of Object.keys(session)){
|
||||
if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){
|
||||
session[cv] = {};
|
||||
}
|
||||
}
|
||||
return session;
|
||||
};
|
||||
|
||||
const saveCRSession = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(sessCfgFile);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${sessCfgFile}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Can\'t save session file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadCRToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.cr, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveCRToken = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(tokenFile.cr);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadFuniToken = () => {
|
||||
const loadedToken = loadYamlCfgFile<{
|
||||
token?: string
|
||||
}>(tokenFile, true);
|
||||
token?: string
|
||||
}>(tokenFile.funi, true);
|
||||
let token: false|string = false;
|
||||
if (loadedToken && loadedToken.token)
|
||||
token = loadedToken.token;
|
||||
// info if token not set
|
||||
token = loadedToken.token;
|
||||
// info if token not set
|
||||
if(!token){
|
||||
console.log('[INFO] Token not set!\n');
|
||||
}
|
||||
|
|
@ -135,14 +182,14 @@ const loadFuniToken = () => {
|
|||
const saveFuniToken = (data: {
|
||||
token?: string
|
||||
}) => {
|
||||
const cfgFolder = path.dirname(tokenFile);
|
||||
const cfgFolder = path.dirname(tokenFile.funi);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile}.yml`, yaml.stringify(data));
|
||||
fs.writeFileSync(`${tokenFile.funi}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.log('[ERROR] Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
export { loadBinCfg, loadCfg, loadFuniToken, saveFuniToken };
|
||||
export { loadBinCfg, loadCfg, loadFuniToken, saveFuniToken, saveCRSession, saveCRToken, loadCRToken, loadCRSession };
|
||||
26
modules/module.cookieFile.ts
Normal file
26
modules/module.cookieFile.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const parse = (data: string) => {
|
||||
const res: Record<string, {
|
||||
value: string,
|
||||
expires: Date,
|
||||
path: string,
|
||||
domain: string,
|
||||
secure: boolean
|
||||
}> = {};
|
||||
const split = data.replace(/\r/g,'').split('\n');
|
||||
for (const line of split) {
|
||||
const c = line.split('\t');
|
||||
if(c.length < 7){
|
||||
continue;
|
||||
}
|
||||
res[c[5]] = {
|
||||
value: c[6],
|
||||
expires: new Date(parseInt(c[4])*1000),
|
||||
path: c[2],
|
||||
domain: c[0].replace(/^\./,''),
|
||||
secure: c[3] == 'TRUE' ? true : false
|
||||
};
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = parse;
|
||||
161
modules/module.curl-req.ts
Normal file
161
modules/module.curl-req.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// build-in
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export type CurlOptions = {
|
||||
headers?: Record<string, string>,
|
||||
curlProxy?: boolean,
|
||||
curlProxyAuth?: string,
|
||||
minVersion?: string,
|
||||
http2?: string,
|
||||
body?: unknown,
|
||||
curlDebug?: boolean
|
||||
} | undefined;
|
||||
|
||||
export type Res = {
|
||||
httpVersion: string,
|
||||
statusCode: string,
|
||||
statusMessage: string,
|
||||
rawHeaders: string,
|
||||
headers: Record<string, string[]|string>,
|
||||
rawBody: Buffer,
|
||||
body: string,
|
||||
}
|
||||
|
||||
// req
|
||||
const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache: string) => {
|
||||
|
||||
let curlOpt = [
|
||||
`"${curlBin}"`,
|
||||
`"${url}"`,
|
||||
];
|
||||
|
||||
options = options || {};
|
||||
|
||||
if(options.headers && Object.keys(options.headers).length > 0){
|
||||
for(let h of Object.keys(options.headers)){
|
||||
let hC = options.headers[h];
|
||||
curlOpt.push('-H', `"${h}: ${hC}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if(options.curlProxy){
|
||||
curlOpt.push('--proxy-insecure', '-x', `"${options.curlProxy}"`);
|
||||
if(options.curlProxyAuth && typeof options.curlProxyAuth == 'string' && options.curlProxyAuth.match(':')){
|
||||
curlOpt.push('-U', `"${options.curlProxyAuth}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const reqId = uuidv4();
|
||||
const headFile = path.join(cache, `/res-headers-${reqId}`);
|
||||
const bodyFile = path.join(cache, `/res-body-${reqId}`);
|
||||
const errFile = path.join(cache, `/res-err-${reqId}`);
|
||||
|
||||
curlOpt.push('-D', `"${headFile}"`);
|
||||
curlOpt.push('-o', `"${bodyFile}"`);
|
||||
curlOpt.push('--stderr', `"${errFile}"`);
|
||||
curlOpt.push('-L', '-s', '-S');
|
||||
|
||||
if(options.minVersion == 'TLSv1.3'){
|
||||
curlOpt.push('--tlsv1.3');
|
||||
}
|
||||
if(options.http2){
|
||||
curlOpt.push('--http2');
|
||||
}
|
||||
|
||||
if(options.body){
|
||||
curlOpt.push('--data-urlencode', `"${options.body}"`);
|
||||
}
|
||||
|
||||
const curlComm = curlOpt.join(' ');
|
||||
|
||||
try{
|
||||
if(options.curlDebug){
|
||||
console.log(curlComm, '\n');
|
||||
}
|
||||
child_process.execSync(curlComm, { stdio: 'inherit', windowsHide: true });
|
||||
}
|
||||
catch(next){
|
||||
const errData = { name: 'RequestError', message: 'EACCES' };
|
||||
try{
|
||||
fs.unlinkSync(headFile);
|
||||
}
|
||||
catch(e){
|
||||
// ignore it...
|
||||
}
|
||||
try{
|
||||
errData.message =
|
||||
fs.readFileSync(errFile, 'utf8')
|
||||
.replace(/^curl: /, '');
|
||||
fs.unlinkSync(errFile);
|
||||
}
|
||||
catch(e){
|
||||
// ignore it...
|
||||
}
|
||||
throw errData;
|
||||
}
|
||||
|
||||
const rawHeaders = fs.readFileSync(headFile, 'utf8');
|
||||
const rawBody = fs.readFileSync(bodyFile);
|
||||
fs.unlinkSync(headFile);
|
||||
fs.unlinkSync(bodyFile);
|
||||
fs.unlinkSync(errFile);
|
||||
|
||||
let res: Res = {
|
||||
httpVersion: '',
|
||||
statusCode: '',
|
||||
statusMessage: '',
|
||||
rawHeaders: rawHeaders,
|
||||
headers: {},
|
||||
rawBody: rawBody,
|
||||
body: rawBody.toString(),
|
||||
};
|
||||
|
||||
let headersCont = rawHeaders.replace(/\r/g, '').split('\n');
|
||||
|
||||
for(let h of headersCont){
|
||||
if( h == '' ){ continue; }
|
||||
if(!h.match(':')){
|
||||
let statusRes = h.split(' ');
|
||||
res.httpVersion = statusRes[0].split('/')[1];
|
||||
res.statusCode = statusRes[1];
|
||||
res.statusMessage = statusRes.slice(2).join(' ');
|
||||
}
|
||||
else{
|
||||
let resHeader = h.split(': ');
|
||||
let resHeadName = resHeader[0].toLowerCase();
|
||||
let resHeadCont = resHeader.slice(1).join(': ');
|
||||
if(resHeadName == 'set-cookie'){
|
||||
if(!Object.prototype.hasOwnProperty.call(res.headers, resHeadName)){
|
||||
res.headers[resHeadName] = [];
|
||||
}
|
||||
(res.headers[resHeadName] as string[]).push(resHeadCont);
|
||||
}
|
||||
else{
|
||||
res.headers[resHeadName] = resHeadCont;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!res.statusCode.match(/^(2|3)\d\d$/)){
|
||||
let httpStatusMessage = res.statusMessage ? ` (${res.statusMessage})` : '';
|
||||
throw {
|
||||
name: 'HTTPError',
|
||||
message: `Response code ${res.statusCode}${httpStatusMessage}`,
|
||||
response: res
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
|
||||
};
|
||||
|
||||
function uuidv4() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = curlReq;
|
||||
93
modules/module.fontsData.ts
Normal file
93
modules/module.fontsData.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// fonts src
|
||||
const root = 'https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/';
|
||||
|
||||
// file list
|
||||
const fonts = {
|
||||
'Adobe Arabic': 'AdobeArabic-Bold.otf',
|
||||
'Andale Mono': 'andalemo.ttf',
|
||||
'Arial': 'arial.ttf',
|
||||
'Arial Bold': 'arialbd.ttf',
|
||||
'Arial Bold Italic': 'arialbi.ttf',
|
||||
'Arial Italic': 'ariali.ttf',
|
||||
'Arial Unicode MS': 'arialuni.ttf',
|
||||
'Arial Black': 'ariblk.ttf',
|
||||
'Comic Sans MS': 'comic.ttf',
|
||||
'Comic Sans MS Bold': 'comicbd.ttf',
|
||||
'Courier New': 'cour.ttf',
|
||||
'Courier New Bold': 'courbd.ttf',
|
||||
'Courier New Bold Italic': 'courbi.ttf',
|
||||
'Courier New Italic': 'couri.ttf',
|
||||
'DejaVu LGC Sans Mono Bold': 'DejaVuLGCSansMono-Bold.ttf',
|
||||
'DejaVu LGC Sans Mono Bold Oblique': 'DejaVuLGCSansMono-BoldOblique.ttf',
|
||||
'DejaVu LGC Sans Mono Oblique': 'DejaVuLGCSansMono-Oblique.ttf',
|
||||
'DejaVu LGC Sans Mono': 'DejaVuLGCSansMono.ttf',
|
||||
'DejaVu Sans Bold': 'DejaVuSans-Bold.ttf',
|
||||
'DejaVu Sans Bold Oblique': 'DejaVuSans-BoldOblique.ttf',
|
||||
'DejaVu Sans ExtraLight': 'DejaVuSans-ExtraLight.ttf',
|
||||
'DejaVu Sans Oblique': 'DejaVuSans-Oblique.ttf',
|
||||
'DejaVu Sans': 'DejaVuSans.ttf',
|
||||
'DejaVu Sans Condensed Bold': 'DejaVuSansCondensed-Bold.ttf',
|
||||
'DejaVu Sans Condensed Bold Oblique': 'DejaVuSansCondensed-BoldOblique.ttf',
|
||||
'DejaVu Sans Condensed Oblique': 'DejaVuSansCondensed-Oblique.ttf',
|
||||
'DejaVu Sans Condensed': 'DejaVuSansCondensed.ttf',
|
||||
'DejaVu Sans Mono Bold': 'DejaVuSansMono-Bold.ttf',
|
||||
'DejaVu Sans Mono Bold Oblique': 'DejaVuSansMono-BoldOblique.ttf',
|
||||
'DejaVu Sans Mono Oblique': 'DejaVuSansMono-Oblique.ttf',
|
||||
'DejaVu Sans Mono': 'DejaVuSansMono.ttf',
|
||||
'Georgia': 'georgia.ttf',
|
||||
'Georgia Bold': 'georgiab.ttf',
|
||||
'Georgia Italic': 'georgiai.ttf',
|
||||
'Georgia Bold Italic': 'georgiaz.ttf',
|
||||
'Impact': 'impact.ttf',
|
||||
'Rubik Black': 'Rubik-Black.ttf',
|
||||
'Rubik Black Italic': 'Rubik-BlackItalic.ttf',
|
||||
'Rubik Bold': 'Rubik-Bold.ttf',
|
||||
'Rubik Bold Italic': 'Rubik-BoldItalic.ttf',
|
||||
'Rubik Italic': 'Rubik-Italic.ttf',
|
||||
'Rubik Light': 'Rubik-Light.ttf',
|
||||
'Rubik Light Italic': 'Rubik-LightItalic.ttf',
|
||||
'Rubik Medium': 'Rubik-Medium.ttf',
|
||||
'Rubik Medium Italic': 'Rubik-MediumItalic.ttf',
|
||||
'Rubik': 'Rubik-Regular.ttf',
|
||||
'Tahoma': 'tahoma.ttf',
|
||||
'Times New Roman': 'times.ttf',
|
||||
'Times New Roman Bold': 'timesbd.ttf',
|
||||
'Times New Roman Bold Italic': 'timesbi.ttf',
|
||||
'Times New Roman Italic': 'timesi.ttf',
|
||||
'Trebuchet MS': 'trebuc.ttf',
|
||||
'Trebuchet MS Bold': 'trebucbd.ttf',
|
||||
'Trebuchet MS Bold Italic': 'trebucbi.ttf',
|
||||
'Trebuchet MS Italic': 'trebucit.ttf',
|
||||
'Verdana': 'verdana.ttf',
|
||||
'Verdana Bold': 'verdanab.ttf',
|
||||
'Verdana Italic': 'verdanai.ttf',
|
||||
'Verdana Bold Italic': 'verdanaz.ttf',
|
||||
'Webdings': 'webdings.ttf',
|
||||
};
|
||||
|
||||
// collect styles from ass string
|
||||
function assFonts(ass: string){
|
||||
let strings = ass.replace(/\r/g,'').split('\n');
|
||||
let styles = [];
|
||||
for(let s of strings){
|
||||
if(s.match(/^Style: /)){
|
||||
let addStyle = s.split(',');
|
||||
styles.push(addStyle[1]);
|
||||
}
|
||||
}
|
||||
return [...new Set(styles)];
|
||||
}
|
||||
|
||||
// font mime type
|
||||
function fontMime(fontFile: string){
|
||||
if(fontFile.match(/\.otf$/)){
|
||||
return 'application/vnd.ms-opentype';
|
||||
}
|
||||
if(fontFile.match(/\.ttf$/)){
|
||||
return 'application/x-truetype-font';
|
||||
}
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
// output
|
||||
export { root, fonts, assFonts, fontMime };
|
||||
176
modules/module.langsData.ts
Normal file
176
modules/module.langsData.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// available langs
|
||||
|
||||
export type LanguageItem = {
|
||||
cr_locale: string,
|
||||
locale: string,
|
||||
code: string,
|
||||
name: string,
|
||||
language?: string
|
||||
}
|
||||
|
||||
const languages: LanguageItem[] = [
|
||||
{ cr_locale: 'en-US', locale: 'en', code: 'eng', name: 'English' },
|
||||
{ cr_locale: 'es-LA', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-419', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-ES', locale: 'es', code: 'spa', name: 'Spanish' },
|
||||
{ cr_locale: 'pt-BR', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
|
||||
{ cr_locale: 'fr-FR', locale: 'fr', code: 'fra', name: 'French' },
|
||||
{ cr_locale: 'de-DE', locale: 'de', code: 'deu', name: 'German' },
|
||||
{ cr_locale: 'ar-ME', locale: 'ar', code: 'ara', name: 'Arabic' },
|
||||
{ cr_locale: 'ar-SA', locale: 'ar', code: 'ara', name: 'Arabic' },
|
||||
{ cr_locale: 'it-IT', locale: 'it', code: 'ita', name: 'Italian' },
|
||||
{ cr_locale: 'ru-RU', locale: 'ru', code: 'rus', name: 'Russian' },
|
||||
{ cr_locale: 'tr-TR', locale: 'tr', code: 'tur', name: 'Turkish' },
|
||||
{ cr_locale: 'ja-JP', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
||||
];
|
||||
|
||||
// add en language names
|
||||
(() =>{
|
||||
for(let languageIndex in languages){
|
||||
if(!languages[languageIndex].language){
|
||||
languages[languageIndex].language = languages[languageIndex].name;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// construct dub language codes
|
||||
const dubLanguageCodes = (() => {
|
||||
const dubLanguageCodesArray = [];
|
||||
for(const language of languages){
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
})();
|
||||
|
||||
// construct subtitle languages filter
|
||||
const subtitleLanguagesFilter = (() => {
|
||||
const subtitleLanguagesExtraParameters = ['all', 'none'];
|
||||
return [...subtitleLanguagesExtraParameters, ...new Set(languages.map(l => { return l.locale; }).slice(0, -1))];
|
||||
})();
|
||||
|
||||
const searchLocales = (() => {
|
||||
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1))];
|
||||
})();
|
||||
|
||||
// convert
|
||||
const fixLanguageTag = (tag: string) => {
|
||||
tag = typeof tag == 'string' ? tag : 'und';
|
||||
const tagLangLC = tag.match(/^(\w{2})-?(\w{2})$/);
|
||||
if(tagLangLC){
|
||||
const tagLang = `${tagLangLC[1]}-${tagLangLC[2].toUpperCase()}`;
|
||||
if(findLang(tagLang).cr_locale != 'und'){
|
||||
return findLang(tagLang).cr_locale;
|
||||
}
|
||||
else{
|
||||
return tagLang;
|
||||
}
|
||||
}
|
||||
else{
|
||||
return tag;
|
||||
}
|
||||
};
|
||||
|
||||
// find lang by cr_locale
|
||||
const findLang = (cr_locale: string) => {
|
||||
const lang = languages.find(l => { return l.cr_locale == cr_locale; });
|
||||
return lang ? lang : { cr_locale: 'und', locale: 'un', code: 'und', name: '', language: '' };
|
||||
};
|
||||
|
||||
const fixAndFindCrLC = (cr_locale: string) => {
|
||||
return findLang(fixLanguageTag(cr_locale));
|
||||
};
|
||||
|
||||
// rss subs lang parser
|
||||
const parseRssSubtitlesString = (subs: string) => {
|
||||
const splitMap = subs.replace(/\s/g, '').split(',').map((s) => {
|
||||
return fixAndFindCrLC(s).locale;
|
||||
});
|
||||
const sort = sortTags(splitMap);
|
||||
return sort.join(', ');
|
||||
};
|
||||
|
||||
|
||||
// parse subtitles Array
|
||||
const parseSubtitlesArray = (tags: string[]) => {
|
||||
const sort = sortSubtitles(tags.map((t) => {
|
||||
return { locale: fixAndFindCrLC(t).locale };
|
||||
}));
|
||||
return sort.map((t) => { return t.locale; }).join(', ');
|
||||
};
|
||||
|
||||
// sort subtitles
|
||||
const sortSubtitles = (data: Partial<LanguageItem>[], sortkey: keyof LanguageItem = 'locale') => {
|
||||
const idx: Record<string, number> = {};
|
||||
sortkey = sortkey || 'locale';
|
||||
const tags = [...new Set(Object.values(languages).map(e => e.locale))];
|
||||
for(const l of tags){
|
||||
idx[l] = Object.keys(idx).length + 1;
|
||||
}
|
||||
data.sort((a, b) => {
|
||||
const ia = idx[a[sortkey] as string] ? idx[a[sortkey] as string] : 50;
|
||||
const ib = idx[b[sortkey] as string] ? idx[b[sortkey] as string] : 50;
|
||||
return ia - ib;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
const sortTags = (data: string[]) => {
|
||||
const retData = data.map(e => { return { locale: e }; });
|
||||
const sort = sortSubtitles(retData);
|
||||
return sort.map(e => e.locale);
|
||||
};
|
||||
|
||||
const subsFile = (fnOutput:string, subsIndex: string, langItem: LanguageItem) => {
|
||||
subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0');
|
||||
return `${fnOutput}.${subsIndex} ${langItem.code} ${langItem.language}.ass`;
|
||||
};
|
||||
|
||||
// construct dub langs const
|
||||
const dubLanguages = (() => {
|
||||
const dubDb: Record<string, string> = {};
|
||||
for(const lang of languages){
|
||||
if(!Object.keys(dubDb).includes(lang.name)){
|
||||
dubDb[lang.name] = lang.code;
|
||||
}
|
||||
}
|
||||
return dubDb;
|
||||
})();
|
||||
|
||||
// dub regex
|
||||
const dubRegExpStr =
|
||||
`\\((${Object.keys(dubLanguages).join('|')})(?: (Dub|VO))?\\)$`;
|
||||
const dubRegExp = new RegExp(dubRegExpStr);
|
||||
|
||||
// code to lang name
|
||||
const langCode2name = (code: string) => {
|
||||
const codeIdx = dubLanguageCodes.indexOf(code);
|
||||
return Object.keys(dubLanguages)[codeIdx];
|
||||
};
|
||||
|
||||
// locale to lang name
|
||||
const locale2language = (locale: string) => {
|
||||
const filteredLocale = languages.filter(l => {
|
||||
return l.locale == locale;
|
||||
});
|
||||
return filteredLocale[0];
|
||||
};
|
||||
|
||||
// output
|
||||
export {
|
||||
languages,
|
||||
dubLanguageCodes,
|
||||
dubLanguages,
|
||||
langCode2name,
|
||||
locale2language,
|
||||
dubRegExp,
|
||||
subtitleLanguagesFilter,
|
||||
searchLocales,
|
||||
fixLanguageTag,
|
||||
findLang,
|
||||
fixAndFindCrLC,
|
||||
parseRssSubtitlesString,
|
||||
parseSubtitlesArray,
|
||||
sortSubtitles,
|
||||
sortTags,
|
||||
subsFile,
|
||||
};
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
import * as iso639 from 'iso-639';
|
||||
import { fonts, fontMime } from "./module.fontsData";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { LanguageItem } from './module.langsData';
|
||||
|
||||
export type MergerInput = {
|
||||
path: string,
|
||||
|
|
@ -8,6 +12,15 @@ export type MergerInput = {
|
|||
export type SubtitleInput = {
|
||||
language: string,
|
||||
file: string,
|
||||
fonts?: ParsedFont[]
|
||||
}
|
||||
|
||||
export type Font = keyof typeof fonts;
|
||||
|
||||
export type ParsedFont = {
|
||||
name: string,
|
||||
path: string,
|
||||
mime: string,
|
||||
}
|
||||
|
||||
export type MergerOptions = {
|
||||
|
|
@ -16,7 +29,7 @@ export type MergerOptions = {
|
|||
onlyAudio: MergerInput[],
|
||||
subtitels: SubtitleInput[],
|
||||
output: string,
|
||||
simul?: boolean
|
||||
simul?: boolean,
|
||||
}
|
||||
|
||||
class Merger {
|
||||
|
|
@ -166,6 +179,13 @@ class Merger {
|
|||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push('--language', `0:${Merger.getLanguageCode(subObj.language)}`);
|
||||
args.push(`"${subObj.file}"`);
|
||||
if (subObj.fonts && subObj.fonts.length > 0) {
|
||||
for (const f of subObj.fonts) {
|
||||
args.push('--attachment-name', f.name);
|
||||
args.push('--attachment-mime-type', f.mime);
|
||||
args.push('--attach-file', f.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
args.push(
|
||||
|
|
@ -174,6 +194,7 @@ class Merger {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
return args.join(' ');
|
||||
};
|
||||
|
||||
|
|
@ -200,6 +221,40 @@ class Merger {
|
|||
|
||||
}
|
||||
|
||||
public static makeFontsList (fontsDir: string, subs: {
|
||||
language: LanguageItem,
|
||||
fonts: Font[]
|
||||
}[]) : ParsedFont[] {
|
||||
let fontsNameList: Font[] = [], fontsList = [], subsList = [], isNstr = true;
|
||||
for(const s of subs){
|
||||
fontsNameList.push(...s.fonts);
|
||||
subsList.push(s.language.locale);
|
||||
}
|
||||
fontsNameList = [...new Set(fontsNameList)];
|
||||
if(subsList.length > 0){
|
||||
console.log('\n[INFO] Subtitles: %s (Total: %s)', subsList.join(', '), subsList.length);
|
||||
isNstr = false;
|
||||
}
|
||||
if(fontsNameList.length > 0){
|
||||
console.log((isNstr ? '\n' : '') + '[INFO] Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length);
|
||||
}
|
||||
for(const f of fontsNameList){
|
||||
const fontFile = fonts[f];
|
||||
if(fontFile){
|
||||
const fontPath = path.join(fontsDir, fontFile);
|
||||
const mime = fontMime(fontFile);
|
||||
if(fs.existsSync(fontPath) && fs.statSync(fontPath).size != 0){
|
||||
fontsList.push({
|
||||
name: fontFile,
|
||||
path: fontPath,
|
||||
mime: mime,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return fontsList;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default Merger;
|
||||
4185
package-lock.json
generated
4185
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -24,6 +24,7 @@
|
|||
"license": "MIT",
|
||||
"main": "funi.js",
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"got": "^11.7.0",
|
||||
|
|
@ -59,6 +60,5 @@
|
|||
"eslint-fix": "eslint *.js modules --fix",
|
||||
"pretest": "npm run tsc",
|
||||
"test": "cd lib && node modules/build win64 && node modules/build linux64 && node modules/build macos64"
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@
|
|||
/* Advanced Options */
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"exclude": [
|
||||
"./videos",
|
||||
|
|
|
|||
Loading…
Reference in a new issue