FFmpeg with multiple language files

This commit is contained in:
Izuco 2021-07-02 22:39:38 +02:00
parent 6edf34a768
commit 5a95739e33
7 changed files with 307 additions and 280 deletions

View file

@ -1,2 +1 @@
content: ./videos/
trash: ./videos/_trash/

View file

@ -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
View file

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

View file

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

View file

@ -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'

View file

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

View file