mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-03-11 17:45:30 +00:00
FFmpeg with multiple language files
This commit is contained in:
parent
6edf34a768
commit
5a95739e33
7 changed files with 307 additions and 280 deletions
|
|
@ -1,2 +1 @@
|
|||
content: ./videos/
|
||||
trash: ./videos/_trash/
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ After installing NodeJS with NPM go to directory with `package.json` file and ty
|
|||
|
||||
### Utility
|
||||
|
||||
* `--nocleanup` move unnecessary files to trash folder after completion instead of deleting
|
||||
* `--nocleanup` don't delete the input files after the muxing
|
||||
* `-h`, `--help` show all options
|
||||
|
||||
## Filename Template
|
||||
|
|
|
|||
157
funi.js
157
funi.js
|
|
@ -84,8 +84,8 @@ let title = '',
|
|||
fnEpNum = 0,
|
||||
fnOutput = '',
|
||||
season = 0,
|
||||
tsDlPath = false,
|
||||
stDlPath = undefined;
|
||||
tsDlPath = [],
|
||||
stDlPath = [];
|
||||
|
||||
// select mode
|
||||
if(argv.auth){
|
||||
|
|
@ -284,7 +284,7 @@ async function getEpisode(fnSlug){
|
|||
debug: argv.debug,
|
||||
});
|
||||
if(!episodeData.ok){return;}
|
||||
let ep = JSON.parse(episodeData.res.body).items[0], streamId = 0;
|
||||
let ep = JSON.parse(episodeData.res.body).items[0], streamIds = [];
|
||||
// build fn
|
||||
showTitle = ep.parent.title;
|
||||
title = ep.title;
|
||||
|
|
@ -334,7 +334,8 @@ async function getEpisode(fnSlug){
|
|||
'enUS': 'English',
|
||||
'esLA': 'Spanish (Latin Am)',
|
||||
'ptBR': 'Portuguese (Brazil)',
|
||||
'zhMN': 'Chinese (Mandarin, PRC)'
|
||||
'zhMN': 'Chinese (Mandarin, PRC)',
|
||||
'jpJP': 'Japanese'
|
||||
};
|
||||
|
||||
// select
|
||||
|
|
@ -343,33 +344,47 @@ async function getEpisode(fnSlug){
|
|||
let selected = false;
|
||||
if(m.id > 0 && m.type == 'Non-Encrypted'){
|
||||
let dub_type = m.language;
|
||||
let localSubs = []
|
||||
let selUncut = !argv.simul && uncut[dub_type] && m.version.match(/uncut/i)
|
||||
? true
|
||||
: (!uncut[dub_type] || argv.simul && m.version.match(/simulcast/i) ? true : false);
|
||||
if(dub_type == 'Japanese' && argv.sub && selUncut){
|
||||
streamId = m.id;
|
||||
stDlPath = m.subtitles;
|
||||
for (let curDub of argv.dub) {
|
||||
if(dub_type == dubType[curDub] && selUncut){
|
||||
streamIds.push({
|
||||
id: m.id,
|
||||
lang: merger.getLanguageCode(dubType[curDub], curDub.slice(0, -2))
|
||||
});
|
||||
stDlPath.push(...m.subtitles);
|
||||
localSubs = m.subtitles
|
||||
selected = true;
|
||||
}
|
||||
else if(dub_type == dubType[argv.dub] && !argv.sub && selUncut){
|
||||
streamId = m.id;
|
||||
stDlPath = m.subtitles;
|
||||
selected = true;
|
||||
}
|
||||
console.log(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
|
||||
stDlPath && selected ? ` (using ${stDlPath.map(a => `'${a.langName}'`).join(', ')} for subtitles)` : ''
|
||||
localSubs && localSubs.length > 0 && selected ? ` (using ${localSubs.map(a => `'${a.langName}'`).join(', ')} for subtitles)` : ''
|
||||
}`);
|
||||
}
|
||||
}
|
||||
|
||||
if(streamId<1){
|
||||
let already = []
|
||||
stDlPath = stDlPath.filter(a => {
|
||||
if (already.includes(a.language)) {
|
||||
return false;
|
||||
} else {
|
||||
already.push(a.language)
|
||||
return true;
|
||||
}
|
||||
})
|
||||
|
||||
if(streamIds.length <1){
|
||||
console.log('[ERROR] Track not selected\n');
|
||||
return;
|
||||
}
|
||||
else{
|
||||
tsDlPath = [];
|
||||
for (let streamId of streamIds) {
|
||||
let streamData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: `/source/catalog/video/${streamId}/signed`,
|
||||
url: `/source/catalog/video/${streamId.id}/signed`,
|
||||
token: token,
|
||||
dinstid: 'uuid',
|
||||
useToken: true,
|
||||
|
|
@ -378,7 +393,6 @@ async function getEpisode(fnSlug){
|
|||
});
|
||||
if(!streamData.ok){return;}
|
||||
streamData = JSON.parse(streamData.res.body);
|
||||
tsDlPath = false;
|
||||
if(streamData.errors){
|
||||
console.log('[ERROR] Error #%s: %s\n',streamData.errors[0].code,streamData.errors[0].detail);
|
||||
return;
|
||||
|
|
@ -386,12 +400,16 @@ async function getEpisode(fnSlug){
|
|||
else{
|
||||
for(let u in streamData.items){
|
||||
if(streamData.items[u].videoType == 'm3u8'){
|
||||
tsDlPath = streamData.items[u].src;
|
||||
tsDlPath.push({
|
||||
path: streamData.items[u].src,
|
||||
lang: streamId.lang
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!tsDlPath){
|
||||
}
|
||||
if(tsDlPath.length < 1){
|
||||
console.log('[ERROR] Unknown error\n');
|
||||
return;
|
||||
}
|
||||
|
|
@ -443,8 +461,14 @@ function getSubsUrl(m){
|
|||
async function downloadStreams(){
|
||||
|
||||
// req playlist
|
||||
|
||||
let purvideo = []
|
||||
let puraudio = []
|
||||
let audioAndVideo = []
|
||||
let outName;
|
||||
for (let streamPath of tsDlPath) {
|
||||
let plQualityReq = await getData({
|
||||
url: tsDlPath,
|
||||
url: streamPath.path,
|
||||
useProxy: (argv.ssp ? false : true),
|
||||
debug: argv.debug,
|
||||
});
|
||||
|
|
@ -560,6 +584,7 @@ async function downloadStreams(){
|
|||
console.log('[INFO] Stream URL:',videoUrl);
|
||||
|
||||
fnOutput = parseFileName(argv.fileName, title, fnEpNum, showTitle, season, plLayersRes[argv.q].width, plLayersRes[argv.q].height);
|
||||
outName = fnOutput;
|
||||
console.log(`[INFO] Output filename: ${fnOutput}.ts`);
|
||||
}
|
||||
else if(argv.x > plServerList.length){
|
||||
|
|
@ -574,8 +599,14 @@ async function downloadStreams(){
|
|||
let dlFailed = false;
|
||||
let dlFailedA = false;
|
||||
|
||||
|
||||
video: if (!argv.novids) {
|
||||
if (plAud.uri && (purvideo.length > 1 || audioAndVideo.length > 1)) {
|
||||
console.log("break 1")
|
||||
break video;
|
||||
} else if (!plAud.uri && (audioAndVideo.some(a => a.lang === streamPath.lang) || puraudio.some(a => a.lang === streamPath.lang))) {
|
||||
console.log("break 2")
|
||||
break video;
|
||||
}
|
||||
// download video
|
||||
let reqVideo = await getData({
|
||||
url: videoUrl,
|
||||
|
|
@ -586,15 +617,29 @@ async function downloadStreams(){
|
|||
|
||||
let chunkList = m3u8(reqVideo.res.body);
|
||||
|
||||
let tsFile = path.join(cfg.dir.content, fnOutput);
|
||||
let tsFile = path.join(cfg.dir.content, `${fnOutput}.video${(plAud.uri ? '' : '.' + streamPath.lang )}`);
|
||||
dlFailed = !await downloadFile(tsFile, chunkList);
|
||||
if (!dlFailed) {
|
||||
if (plAud.uri) {
|
||||
purvideo.push({
|
||||
path: `${tsFile}.ts`,
|
||||
lang: plAud.language
|
||||
})
|
||||
} else {
|
||||
audioAndVideo.push({
|
||||
path: `${tsFile}.ts`,
|
||||
lang: streamPath.lang
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
console.log('[INFO] Skip video downloading...\n');
|
||||
}
|
||||
|
||||
if (!argv.noaudio && plAud.uri) {
|
||||
// download audio
|
||||
if (audioAndVideo.some(a => a.lang === plAud.language) || puraudio.some(a => a.lang === plAud.language))
|
||||
return;
|
||||
let reqAudio = await getData({
|
||||
url: plAud.uri,
|
||||
useProxy: (argv.ssp ? false : true),
|
||||
|
|
@ -604,17 +649,25 @@ async function downloadStreams(){
|
|||
|
||||
let chunkListA = m3u8(reqAudio.res.body);
|
||||
|
||||
let tsFileA = path.join(cfg.dir.content, fnOutput + `.${plAud.language}`);
|
||||
let tsFileA = path.join(cfg.dir.content, fnOutput + `.audio.${plAud.language}`);
|
||||
|
||||
dlFailedA = !await downloadFile(tsFileA, chunkListA);
|
||||
if (!dlFailedA)
|
||||
puraudio.push({
|
||||
path: `${tsFileA}.ts`,
|
||||
lang: plAud.language
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// add subs
|
||||
let subsExt = !argv.mp4 || argv.mp4 && !argv.mks && argv.ass ? '.ass' : '.srt';
|
||||
let addSubs = argv.mks && tsDlPath ? true : false;
|
||||
|
||||
// download subtitles
|
||||
if(stDlPath){
|
||||
if(stDlPath.length > 0){
|
||||
console.log('[INFO] Downloading subtitles...');
|
||||
for (let subObject of stDlPath) {
|
||||
let subsSrc = await getData({
|
||||
|
|
@ -625,7 +678,6 @@ async function downloadStreams(){
|
|||
if(subsSrc.ok){
|
||||
let assData = vttConvert(subsSrc.res.body, (subsExt == '.srt' ? true : false), subObject.langName, argv.fontSize);
|
||||
subObject.file = path.join(cfg.dir.content, fnOutput) + subObject.ext + subsExt;
|
||||
subObject.trashFile = path.join(cfg.dir.trash, fnOutput) + subObject.ext + subsExt;
|
||||
fs.writeFileSync(subObject.file, assData);
|
||||
}
|
||||
else{
|
||||
|
|
@ -638,37 +690,19 @@ async function downloadStreams(){
|
|||
console.log('[INFO] Subtitles downloaded!');
|
||||
}
|
||||
|
||||
/* TODO Remove */
|
||||
if(false && (dlFailed || dlFailedA)){
|
||||
console.log('\n[INFO] TS file not fully downloaded, skip muxing video...\n');
|
||||
if((puraudio.length < 1 && audioAndVideo.length < 1) || (purvideo.length < 1 && audioAndVideo.length < 1)){
|
||||
console.log('\n[INFO] Unable to locate a video AND audio file\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if(argv.skipmux){
|
||||
console.log("[INFO] Skipping muxing...")
|
||||
return;
|
||||
}
|
||||
|
||||
let muxTrg = path.join(cfg.dir.content, fnOutput);
|
||||
let muxTrgA = '';
|
||||
let tshTrg = path.join(cfg.dir.trash, fnOutput);
|
||||
let tshTrgA = '';
|
||||
|
||||
if(!fs.existsSync(`${muxTrg}.ts`) || !fs.statSync(`${muxTrg}.ts`).isFile()){
|
||||
console.log('\n[INFO] TS file not found, skip muxing video...\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if(plAud.uri){
|
||||
muxTrgA = path.join(cfg.dir.content, fnOutput + `.${plAud.language}`);
|
||||
tshTrgA = path.join(cfg.dir.trash, fnOutput + `.${plAud.language}`);
|
||||
if(!fs.existsSync(`${muxTrgA}.ts`) || !fs.statSync(`${muxTrgA}.ts`).isFile()){
|
||||
console.log('\n[INFO] TS file not found, skip muxing video...\n');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// usage
|
||||
let usableMKVmerge = true;
|
||||
/* TODO MkvMerge */
|
||||
let usableMKVmerge = false;
|
||||
let usableFFmpeg = true;
|
||||
|
||||
// check exec path
|
||||
|
|
@ -690,44 +724,23 @@ async function downloadStreams(){
|
|||
|
||||
if(!argv.mp4 && usableMKVmerge){
|
||||
let ffext = !argv.mp4 ? 'mkv' : 'mp4';
|
||||
let command = merger.buildCommandMkvMerge(`${muxTrg}.ts`, plAud, stDlPath, `${muxTrg}.${ffext}`);
|
||||
let command = merger.buildCommandMkvMerge(audioAndVideo, purvideo, puraudio, stDlPath, `${path.join(cfg.dir.content, outName)}.${ffext}`);
|
||||
console.log(command);
|
||||
shlp.exec('mkvmerge', `"${mkvmergebinfile}"`, command);
|
||||
}
|
||||
else if(usableFFmpeg){
|
||||
let ffext = !argv.mp4 ? 'mkv' : 'mp4';
|
||||
let command = merger.buildCommandFFmpeg(`${muxTrg}.ts`, plAud, stDlPath, `${muxTrg}.${ffext}`);
|
||||
let command = merger.buildCommandFFmpeg(audioAndVideo, purvideo, puraudio, stDlPath, `${path.join(cfg.dir.content, outName)}.${ffext}`);
|
||||
shlp.exec('ffmpeg',`"${ffmpegbinfile}"`,command);
|
||||
}
|
||||
else{
|
||||
console.log('\n[INFO] Done!\n');
|
||||
return;
|
||||
}
|
||||
/* TODO Remove */
|
||||
if (argv.nocleanup)
|
||||
return;
|
||||
if(argv.notrashfolder && argv.nocleanup){
|
||||
// don't move or delete temp files
|
||||
}
|
||||
else if(argv.nocleanup){
|
||||
fs.renameSync(muxTrg+'.ts', tshTrg + '.ts');
|
||||
if (plAud.uri)
|
||||
fs.renameSync(muxTrgA+'.ts', tshTrgA + '.ts');
|
||||
if(addSubs){
|
||||
for (let subObj of stDlPath) {
|
||||
fs.renameSync(subObj.file ? subObj.file : muxTrg+subsExt, subObj.trashFile ? subObj.trashFile : tshTrg + subsExt);
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
fs.unlinkSync(muxTrg+'.ts');
|
||||
if (plAud.uri)
|
||||
fs.unlinkSync(muxTrgA+'.ts');
|
||||
if(addSubs){
|
||||
for (let subObj of stDlPath)
|
||||
fs.unlinkSync(subObj.file ? subObj.file : muxTrg + subsExt);
|
||||
}
|
||||
}
|
||||
|
||||
audioAndVideo.concat(puraudio).concat(purvideo).forEach(a => fs.unlinkSync(a.path))
|
||||
console.log('\n[INFO] Done!\n');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ const nodeVer = 'node14-';
|
|||
fs.mkdirSync(`${buildDir}/bin`);
|
||||
fs.mkdirSync(`${buildDir}/config`);
|
||||
fs.mkdirSync(`${buildDir}/videos`);
|
||||
fs.mkdirSync(`${buildDir}/videos/_trash`);
|
||||
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`);
|
||||
|
|
|
|||
|
|
@ -2,30 +2,59 @@ const iso639 = require('iso-639');
|
|||
const argv = require('../funi').argv;
|
||||
|
||||
/**
|
||||
* @param {string} videoFile
|
||||
* @param {object} audioFile
|
||||
* @param {Array<object>} videoAndAudio
|
||||
* @param {Array<object>} onlyVid
|
||||
* @param {Array<object>} onlyAuido
|
||||
* @param {Array<object>} subtitles
|
||||
* @param {string} output
|
||||
* @returns {string}
|
||||
*/
|
||||
const buildCommandFFmpeg = (videoFile, audioSettings, subtitles, output) => {
|
||||
const buildCommandFFmpeg = (videoAndAudio, onlyVid, onlyAuido, subtitles, output) => {
|
||||
let args = [];
|
||||
args.push(`-i "${videoFile}"`);
|
||||
let metaData = [];
|
||||
|
||||
let index = 0;
|
||||
let hasVideo = false;
|
||||
for (let vid of videoAndAudio) {
|
||||
args.push(`-i "${vid.path}"`)
|
||||
if (!hasVideo) {
|
||||
metaData.push(`-map ${index}`)
|
||||
metaData.push(`-metadata:s:a:${index} language=${getLanguageCode(vid.lang, vid.lang)}`)
|
||||
metaData.push(`-metadata:s:v:${index} title="[Funimation]"`)
|
||||
hasVideo = true
|
||||
} else {
|
||||
metaData.push(`-map ${index}:a`)
|
||||
metaData.push(`-metadata:s:a:${index} language=${getLanguageCode(vid.lang, vid.lang)}`)
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
for (let vid of onlyVid) {
|
||||
if (!hasVideo) {
|
||||
args.push(`-i "${vid.path}"`)
|
||||
metaData.push(`-map ${index}`)
|
||||
metaData.push(`-metadata:s:a:${index} language=${getLanguageCode(vid.lang, vid.lang)}`)
|
||||
metaData.push(`-metadata:s:v:${index} title="[Funimation]"`)
|
||||
hasVideo = true
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
for (let aud of onlyAuido) {
|
||||
args.push(`-i "${aud.path}"`)
|
||||
metaData.push(`-map ${index}`)
|
||||
metaData.push(`-metadata:s:a:${index} language=${getLanguageCode(aud.lang, aud.lang)}`)
|
||||
index++;
|
||||
}
|
||||
|
||||
if (audioSettings.uri)
|
||||
args.push(`-i "${audioSettings.uri}"`);
|
||||
for (let index in subtitles) {
|
||||
let sub = subtitles[index];
|
||||
args.push(`-i "${sub.file}"`);
|
||||
}
|
||||
|
||||
args.push('-map 0');
|
||||
if (audioSettings.uri)
|
||||
args.push( '-map 1');
|
||||
|
||||
args.push(...subtitles.map((_, index) => `-map ${index + (audioSettings.uri ? 2 : 1)}`));
|
||||
args.push(...subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
|
||||
args.push(...metaData)
|
||||
args.push(
|
||||
'-metadata:s:v:0 title="[Funimation]"',
|
||||
`-metadata:s:a:0 language=${getLanguageCode(audioSettings.language, argv.sub ? 'jpn' : 'eng')}`,
|
||||
'-c:v copy',
|
||||
'-c:a copy',
|
||||
'-c:s mov_text',
|
||||
|
|
@ -58,14 +87,14 @@ const buildCommandMkvMerge = (videoFile, audioSettings, subtitles, output) => {
|
|||
'--no-audio'
|
||||
);
|
||||
args.push(`"${videoFile}"`);
|
||||
args.push(`--language 0:${getLanguageCode(audioSettings.language, argv.sub ? 'jpn' : 'eng')}`);
|
||||
args.push(`--language 0:${getLanguageCode(audioSettings.language, argv.todo ? 'jpn' : 'eng')}`);
|
||||
args.push(
|
||||
'--no-video',
|
||||
'--audio-tracks 0'
|
||||
);
|
||||
args.push(`"${audioSettings.uri}"`);
|
||||
} else{
|
||||
args.push(`--language 1:${argv.sub ? 'jpn' : 'eng'}`);
|
||||
args.push(`--language 1:${argv.todo ? 'jpn' : 'eng'}`);
|
||||
args.push(
|
||||
'--video-tracks 0',
|
||||
'--audio-tracks 1'
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const appArgv = (cfg) => {
|
|||
// init
|
||||
const argv = yargs.parserConfiguration({
|
||||
'duplicate-arguments-array': true,
|
||||
'camel-case-expansion': false
|
||||
"camel-case-expansion": false
|
||||
})
|
||||
// main
|
||||
.wrap(Math.min(120)) // yargs.terminalWidth()
|
||||
|
|
@ -74,16 +74,10 @@ const appArgv = (cfg) => {
|
|||
.option('dub', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Download non-Japanese Dub (English Dub mode by default)',
|
||||
choices: [ 'enUS', 'esLA', 'ptBR', 'zhMN' ],
|
||||
choices: [ 'enUS', 'esLA', 'ptBR', 'zhMN', 'jpJP' ],
|
||||
default: cfg.dub || 'enUS',
|
||||
type: 'array',
|
||||
})
|
||||
.option('sub', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Japanese Dub with subtitles mode (English Dub mode by default)',
|
||||
default: cfg.subsMode || false,
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('subLang', {
|
||||
group: 'Downloading:',
|
||||
describe: 'Set the subtitle language (English is default and fallback)',
|
||||
|
|
@ -187,17 +181,10 @@ const appArgv = (cfg) => {
|
|||
// util
|
||||
.option('nocleanup', {
|
||||
group: 'Utilities:',
|
||||
describe: 'Move temporary files to trash folder instead of deleting',
|
||||
describe: 'Dont\'t delete the input files after muxing',
|
||||
default: cfg.noCleanUp || false,
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('notrashfolder', {
|
||||
implies: ['nocleanup'],
|
||||
group: 'Utilities:',
|
||||
describe: 'Don\'t move temporary files to trash folder (Used with --nocleanup)',
|
||||
default: cfg.noTrashFolder || false,
|
||||
type: 'boolean'
|
||||
})
|
||||
// help
|
||||
.option('help', {
|
||||
alias: 'h',
|
||||
|
|
@ -217,8 +204,8 @@ const appArgv = (cfg) => {
|
|||
|
||||
// Resolve unwanted arrays
|
||||
for (let key in argv) {
|
||||
if (argv[key] instanceof Array && !(key === 'subLang' || key === 'dub')) {
|
||||
argv[key] = argv[key].pop();
|
||||
if (argv[key] instanceof Array && !(key === "subLang" || key === "dub")) {
|
||||
argv[key] = argv[key].pop()
|
||||
}
|
||||
}
|
||||
return argv;
|
||||
|
|
|
|||
Loading…
Reference in a new issue