multi-downloader-nx_mirror/modules/module.app-args.ts
stratumadev 5f192a31c0 feat(crunchy): add subtitleTimestampFix function (#1121)
Fix Crunchyroll subtitles with invalid durations.
If start > video length  the line is deleted.
If only end > video length  end is trimmed to video duration.
2025-11-28 00:40:11 +01:00

322 lines
8.5 KiB
TypeScript

import { Command } from 'commander';
import { args, AvailableMuxer, groups } from './module.args';
import { LanguageItem } from './module.langsData';
import { DownloadInfo } from '../@types/messageHandler';
import { HLSCallback } from './hls-download';
import leven from 'leven';
import { console } from './log';
import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from '../@types/enums';
import pj from '../package.json';
export let argvC: {
[x: string]: unknown;
ccTag: string;
defaultAudio: LanguageItem;
defaultSub: LanguageItem;
ffmpegOptions: string[];
mkvmergeOptions: string[];
force: 'Y' | 'y' | 'N' | 'n' | 'C' | 'c';
skipUpdate: boolean;
videoTitle: string;
override: string[];
fsRetryTime: number;
forceMuxer: AvailableMuxer | undefined;
username: string | undefined;
password: string | undefined;
token: string | undefined;
silentAuth: boolean;
skipSubMux: boolean;
downloadArchive: boolean;
addArchive: boolean;
but: boolean;
auth: boolean | undefined;
dlFonts: boolean | undefined;
search: string | undefined;
searchType: string;
page: number | undefined;
locale: string;
new: boolean | undefined;
movieListing: string | undefined;
showRaw: string | undefined;
seasonRaw: string | undefined;
series: string | undefined;
s: string | undefined;
srz: string | undefined;
e: string | undefined;
extid: string | undefined;
q: number;
x: number;
cstream: keyof typeof CrunchyVideoPlayStreams;
vstream: keyof typeof CrunchyVideoPlayStreams;
astream: keyof typeof CrunchyAudioPlayStreams;
tsd: boolean | undefined;
partsize: number;
hslang: string;
dlsubs: string[];
skipMuxOnSubFail: boolean;
novids: boolean | undefined;
noaudio: boolean | undefined;
nosubs: boolean | undefined;
dubLang: string[];
all: boolean;
fontSize: number;
combineLines: boolean;
allDubs: boolean;
timeout: number;
waittime: number;
simul: boolean;
mp4: boolean;
skipmux: boolean | undefined;
fileName: string;
numbers: number;
nosess: string;
debug: boolean | undefined;
raw: boolean;
rawoutput: string;
nocleanup: boolean;
help: boolean | undefined;
service: 'crunchy' | 'hidive' | 'adn';
update: boolean;
fontName: string | undefined;
_: (string | number)[];
$0: string;
dlVideoOnce: boolean;
chapters: boolean;
removeBumpers: boolean;
originalFontSize: boolean;
keepAllVideos: boolean;
syncTiming: boolean;
callbackMaker?: (data: DownloadInfo) => HLSCallback;
// Subtitle Fix Options
noASSConv: boolean;
noSubFix: boolean;
srtAssFix: boolean;
layoutResFix: boolean;
scaledBorderAndShadowFix: boolean;
scaledBorderAndShadow: 'yes' | 'no';
originalScriptFix: boolean;
subtitleTimestampFix: boolean;
// Proxy
proxy: string;
proxyAll: boolean;
};
export type ArgvType = typeof argvC;
// This functions manages slight mismatches like -srz and returns it as --srz
const processArgv = () => {
const argv = [];
const arrayFlags = args.filter((a) => a.type === 'array').map((a) => `--${a.name}`);
for (let i = 0; i < process.argv.length; i++) {
const arg = process.argv[i];
if (/^-[a-zA-Z]{2,}$/.test(arg)) {
const found = args.find((a) => a.name === arg.substring(1) || a.alias === arg.substring(1));
if (found) {
argv.push(`--${found.name}`);
continue;
}
}
if (arrayFlags.includes(arg)) {
const col = [];
let n = i + 1;
while (n < process.argv.length && !process.argv[n].startsWith('-')) {
col.push(process.argv[n]);
n++;
}
argv.push(arg);
argv.push(col.join(' '));
i = n - 1;
continue;
}
argv.push(arg);
}
return argv;
};
const appArgv = (
cfg: {
[key: string]: unknown;
},
isGUI = false
) => {
if (argvC) return argvC;
const argv = getCommander(cfg, isGUI).parse(processArgv());
const parsed = argv.opts() as ArgvType;
// Be sure that both vars (name and alias) are defined
for (const item of args) {
const name = item.name;
const alias = item.alias;
if (!alias) continue;
if (parsed[name] !== undefined) {
parsed[alias] = parsed[name];
}
if (parsed[alias] !== undefined) {
parsed[name] = parsed[alias];
}
}
if (!isGUI && (process.argv.length <= 2 || parsed.help)) {
argv.outputHelp();
process.exit(0);
}
argvC = parsed;
return parsed;
};
const overrideArguments = (cfg: { [key: string]: unknown }, override: Partial<typeof argvC>, isGUI = false) => {
const argv = getCommander(cfg, isGUI);
const baseArgv = [...processArgv()];
for (const [key, val] of Object.entries(override)) {
if (val === undefined) continue;
if (typeof val === 'boolean') {
if (val) baseArgv.push(`--${key}`);
} else {
baseArgv.push(`--${key}`, String(val));
}
}
const data = argv.parse(baseArgv);
const parsed = data.opts() as ArgvType;
// Be sure that both vars (name and alias) are defined
for (const item of args) {
const name = item.name;
const alias = item.alias;
if (!alias) continue;
if (parsed[name] !== undefined) {
parsed[alias] = parsed[name];
}
if (parsed[alias] !== undefined) {
parsed[name] = parsed[alias];
}
}
if (!isGUI && (process.argv.length <= 2 || parsed.help)) {
argv.outputHelp();
process.exit(0);
}
argvC = parsed;
};
export { appArgv, overrideArguments };
const getCommander = (cfg: Record<string, unknown>, isGUI: boolean) => {
const program = new Command();
program
.name(process.platform === 'win32' ? 'aniDL.exe' : 'aniDL')
.description(pj.description)
.version(pj.version, '-v, --version', 'Show version')
.allowUnknownOption(false)
.allowExcessArguments(true);
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 data = args.map((a) => {
return {
...a,
demandOption: !isGUI && a.demandOption,
group: groups[a.group],
default: typeof a.default === 'object' && !Array.isArray(a.default) ? parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default
};
});
for (const item of data) {
const option = program.createOption(
(item.alias
? `${item.alias.length === 1 ? `-${item.alias}` : `--${item.alias}`}, ${item.name.length === 1 ? `-${item.name}` : `--${item.name}`}`
: item.name.length === 1
? `-${item.name}`
: `--${item.name}`) + (item.type === 'boolean' ? '' : ` <value>`),
item.describe ?? ''
);
if (item.default !== undefined) option.default(item.default);
option.argParser((value) => {
if (item.type === 'boolean') {
if (value === undefined) return true;
if (value === 'true') return true;
if (value === 'false') return false;
return Boolean(value);
}
if (item.type === 'array') {
if (typeof value === 'string' && value.includes(',')) {
return value.split(',').map((v) => v.trim());
}
if (typeof value === 'string' && value.includes(' ')) {
return value.split(' ').map((v) => v.trim());
}
return Array.isArray(value) ? value : [value];
}
if (item.type === 'number') {
const num = Number(value);
return Number.isFinite(num) ? num : 0;
}
if (item.choices && !(isGUI && item.name === 'service')) {
if (!item.choices.includes(value)) {
console.error(`Invalid value '${value}' for --${item.name}. Allowed: ${item.choices.join(', ')}`);
process.exit(1);
}
}
if (item.transformer) return item.transformer(value);
return value;
});
program.addOption(option);
}
// Custom logic for suggesting corrections for misspelled options
program.hook('preAction', (_, command) => {
const used = command.parent?.args || [];
const validOptions = [...args.map((a) => a.name), ...args.map((a) => a.alias).filter((a): a is string => a !== undefined)];
const unknownOptions = used.filter((arg) => arg.startsWith('-'));
const suggestions: Record<string, boolean> = {};
unknownOptions.forEach((opt) => {
const cleaned = opt.replace(/^-+/, '');
const closest = validOptions.find((vo) => {
const dist = leven(vo, cleaned);
return dist <= 2 && dist > 0;
});
if (closest && !suggestions[closest]) {
console.info(`Unknown option ${opt}, did you mean --${closest}?`);
suggestions[closest] = true;
} else if (!suggestions[cleaned]) {
console.info(`Unknown option ${opt}`);
suggestions[cleaned] = true;
}
});
});
return program;
};