This commit is contained in:
AniDL 2020-10-16 20:37:36 +03:00
parent d2383962ca
commit 6454f93d9d
9 changed files with 532 additions and 178 deletions

2
.gitignore vendored
View file

@ -13,5 +13,7 @@ package-lock.json
*.ts
*.mkv
*.mp4
*.ass
*.srt
*.resume
*.user.yml

251
funi.js
View file

@ -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;
}

Binary file not shown.

View file

@ -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`)){

View 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
View 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
View 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;

View 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);
};

View file

@ -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",