mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
298 lines
9.8 KiB
TypeScript
298 lines
9.8 KiB
TypeScript
import { Command } from 'commander';
|
|
|
|
export interface JellyfinInvocation {
|
|
action?: string;
|
|
discovery?: boolean;
|
|
play?: boolean;
|
|
login?: boolean;
|
|
logout?: boolean;
|
|
setup?: boolean;
|
|
server?: string;
|
|
username?: string;
|
|
password?: string;
|
|
passwordStore?: string;
|
|
logLevel?: string;
|
|
}
|
|
|
|
export interface YtInvocation {
|
|
target?: string;
|
|
mode?: string;
|
|
outDir?: string;
|
|
keepTemp?: boolean;
|
|
whisperBin?: string;
|
|
whisperModel?: string;
|
|
ytSubgenAudioFormat?: string;
|
|
logLevel?: string;
|
|
}
|
|
|
|
export interface CommandActionInvocation {
|
|
action: string;
|
|
logLevel?: string;
|
|
}
|
|
|
|
export interface CliInvocations {
|
|
jellyfinInvocation: JellyfinInvocation | null;
|
|
ytInvocation: YtInvocation | null;
|
|
configInvocation: CommandActionInvocation | null;
|
|
mpvInvocation: CommandActionInvocation | null;
|
|
appInvocation: { appArgs: string[] } | null;
|
|
doctorTriggered: boolean;
|
|
doctorLogLevel: string | null;
|
|
texthookerTriggered: boolean;
|
|
texthookerLogLevel: string | null;
|
|
}
|
|
|
|
function applyRootOptions(program: Command): void {
|
|
program
|
|
.option('-b, --backend <backend>', 'Display backend')
|
|
.option('-d, --directory <dir>', 'Directory to browse')
|
|
.option('-r, --recursive', 'Search directories recursively')
|
|
.option('-p, --profile <profile>', 'MPV profile')
|
|
.option('--start', 'Explicitly start overlay')
|
|
.option('--log-level <level>', 'Log level')
|
|
.option('-R, --rofi', 'Use rofi picker')
|
|
.option('-S, --start-overlay', 'Auto-start overlay')
|
|
.option('-T, --no-texthooker', 'Disable texthooker-ui server');
|
|
}
|
|
|
|
function buildSubcommandHelpText(program: Command): string {
|
|
const subcommands = program.commands
|
|
.filter((command) => command.name() !== 'help')
|
|
.map((command) => {
|
|
const aliases = command.aliases();
|
|
const term = aliases.length > 0 ? `${command.name()}|${aliases[0]}` : command.name();
|
|
return { term, description: command.description() };
|
|
});
|
|
|
|
if (subcommands.length === 0) return '';
|
|
const longestTerm = Math.max(...subcommands.map((entry) => entry.term.length));
|
|
const lines = subcommands.map((entry) =>
|
|
` ${entry.term.padEnd(longestTerm)} ${entry.description || ''}`.trimEnd(),
|
|
);
|
|
return `\nCommands:\n${lines.join('\n')}\n`;
|
|
}
|
|
|
|
function getTopLevelCommand(argv: string[]): { name: string; index: number } | null {
|
|
const commandNames = new Set([
|
|
'jellyfin',
|
|
'jf',
|
|
'yt',
|
|
'youtube',
|
|
'doctor',
|
|
'config',
|
|
'mpv',
|
|
'texthooker',
|
|
'app',
|
|
'bin',
|
|
'help',
|
|
]);
|
|
const optionsWithValue = new Set([
|
|
'-b',
|
|
'--backend',
|
|
'-d',
|
|
'--directory',
|
|
'-p',
|
|
'--profile',
|
|
'--log-level',
|
|
]);
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const token = argv[i] || '';
|
|
if (token === '--') return null;
|
|
if (token.startsWith('-')) {
|
|
if (optionsWithValue.has(token)) i += 1;
|
|
continue;
|
|
}
|
|
return commandNames.has(token) ? { name: token, index: i } : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function hasTopLevelCommand(argv: string[]): boolean {
|
|
return getTopLevelCommand(argv) !== null;
|
|
}
|
|
|
|
export function resolveTopLevelCommand(argv: string[]): { name: string; index: number } | null {
|
|
return getTopLevelCommand(argv);
|
|
}
|
|
|
|
export function parseCliPrograms(
|
|
argv: string[],
|
|
scriptName: string,
|
|
): {
|
|
options: Record<string, unknown>;
|
|
rootTarget: unknown;
|
|
invocations: CliInvocations;
|
|
} {
|
|
let jellyfinInvocation: JellyfinInvocation | null = null;
|
|
let ytInvocation: YtInvocation | null = null;
|
|
let configInvocation: CommandActionInvocation | null = null;
|
|
let mpvInvocation: CommandActionInvocation | null = null;
|
|
let appInvocation: { appArgs: string[] } | null = null;
|
|
let doctorLogLevel: string | null = null;
|
|
let texthookerLogLevel: string | null = null;
|
|
let doctorTriggered = false;
|
|
let texthookerTriggered = false;
|
|
|
|
const commandProgram = new Command();
|
|
commandProgram
|
|
.name(scriptName)
|
|
.description('Launch MPV with SubMiner sentence mining overlay')
|
|
.showHelpAfterError(true)
|
|
.enablePositionalOptions()
|
|
.allowExcessArguments(false)
|
|
.allowUnknownOption(false)
|
|
.exitOverride();
|
|
applyRootOptions(commandProgram);
|
|
|
|
const rootProgram = new Command();
|
|
rootProgram
|
|
.name(scriptName)
|
|
.description('Launch MPV with SubMiner sentence mining overlay')
|
|
.usage('[options] [command] [target]')
|
|
.showHelpAfterError(true)
|
|
.allowExcessArguments(false)
|
|
.allowUnknownOption(false)
|
|
.exitOverride()
|
|
.argument('[target]', 'file, directory, or URL');
|
|
applyRootOptions(rootProgram);
|
|
|
|
commandProgram
|
|
.command('jellyfin')
|
|
.alias('jf')
|
|
.description('Jellyfin workflows')
|
|
.argument('[action]', 'setup|discovery|play|login|logout')
|
|
.option('-d, --discovery', 'Cast discovery mode')
|
|
.option('-p, --play', 'Interactive play picker')
|
|
.option('-l, --login', 'Login flow')
|
|
.option('--logout', 'Clear token/session')
|
|
.option('--setup', 'Open setup window')
|
|
.option('-s, --server <url>', 'Jellyfin server URL')
|
|
.option('-u, --username <name>', 'Jellyfin username')
|
|
.option('-w, --password <pass>', 'Jellyfin password')
|
|
.option('--password-store <backend>', 'Pass through Electron safeStorage backend')
|
|
.option('--log-level <level>', 'Log level')
|
|
.action((action: string | undefined, options: Record<string, unknown>) => {
|
|
jellyfinInvocation = {
|
|
action,
|
|
discovery: options.discovery === true,
|
|
play: options.play === true,
|
|
login: options.login === true,
|
|
logout: options.logout === true,
|
|
setup: options.setup === true,
|
|
server: typeof options.server === 'string' ? options.server : undefined,
|
|
username: typeof options.username === 'string' ? options.username : undefined,
|
|
password: typeof options.password === 'string' ? options.password : undefined,
|
|
passwordStore: typeof options.passwordStore === 'string' ? options.passwordStore : undefined,
|
|
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
|
};
|
|
});
|
|
|
|
commandProgram
|
|
.command('yt')
|
|
.alias('youtube')
|
|
.description('YouTube workflows')
|
|
.argument('[target]', 'YouTube URL or ytsearch: query')
|
|
.option('-m, --mode <mode>', 'Subtitle generation mode')
|
|
.option('-o, --out-dir <dir>', 'Subtitle output dir')
|
|
.option('--keep-temp', 'Keep temp files')
|
|
.option('--whisper-bin <path>', 'whisper.cpp CLI path')
|
|
.option('--whisper-model <path>', 'whisper model path')
|
|
.option('--yt-subgen-audio-format <format>', 'Audio extraction format')
|
|
.option('--log-level <level>', 'Log level')
|
|
.action((target: string | undefined, options: Record<string, unknown>) => {
|
|
ytInvocation = {
|
|
target,
|
|
mode: typeof options.mode === 'string' ? options.mode : undefined,
|
|
outDir: typeof options.outDir === 'string' ? options.outDir : undefined,
|
|
keepTemp: options.keepTemp === true,
|
|
whisperBin: typeof options.whisperBin === 'string' ? options.whisperBin : undefined,
|
|
whisperModel: typeof options.whisperModel === 'string' ? options.whisperModel : undefined,
|
|
ytSubgenAudioFormat:
|
|
typeof options.ytSubgenAudioFormat === 'string' ? options.ytSubgenAudioFormat : undefined,
|
|
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
|
};
|
|
});
|
|
|
|
commandProgram
|
|
.command('doctor')
|
|
.description('Run dependency and environment checks')
|
|
.option('--log-level <level>', 'Log level')
|
|
.action((options: Record<string, unknown>) => {
|
|
doctorTriggered = true;
|
|
doctorLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
|
|
});
|
|
|
|
commandProgram
|
|
.command('config')
|
|
.description('Config helpers')
|
|
.argument('[action]', 'path|show', 'path')
|
|
.option('--log-level <level>', 'Log level')
|
|
.action((action: string, options: Record<string, unknown>) => {
|
|
configInvocation = {
|
|
action,
|
|
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
|
};
|
|
});
|
|
|
|
commandProgram
|
|
.command('mpv')
|
|
.description('MPV helpers')
|
|
.argument('[action]', 'status|socket|idle', 'status')
|
|
.option('--log-level <level>', 'Log level')
|
|
.action((action: string, options: Record<string, unknown>) => {
|
|
mpvInvocation = {
|
|
action,
|
|
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
|
};
|
|
});
|
|
|
|
commandProgram
|
|
.command('texthooker')
|
|
.description('Launch texthooker-only mode')
|
|
.option('--log-level <level>', 'Log level')
|
|
.action((options: Record<string, unknown>) => {
|
|
texthookerTriggered = true;
|
|
texthookerLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
|
|
});
|
|
|
|
commandProgram
|
|
.command('app')
|
|
.alias('bin')
|
|
.description('Pass arguments directly to SubMiner binary')
|
|
.allowUnknownOption(true)
|
|
.allowExcessArguments(true)
|
|
.argument('[appArgs...]', 'Arguments forwarded to SubMiner app binary')
|
|
.action((appArgs: string[] | undefined) => {
|
|
appInvocation = { appArgs: Array.isArray(appArgs) ? appArgs : [] };
|
|
});
|
|
|
|
rootProgram.addHelpText('after', buildSubcommandHelpText(commandProgram));
|
|
|
|
const selectedProgram = hasTopLevelCommand(argv) ? commandProgram : rootProgram;
|
|
try {
|
|
selectedProgram.parse(['node', scriptName, ...argv]);
|
|
} catch (error) {
|
|
const commanderError = error as { code?: string; message?: string };
|
|
if (commanderError?.code === 'commander.helpDisplayed') {
|
|
process.exit(0);
|
|
}
|
|
throw new Error(commanderError?.message || String(error));
|
|
}
|
|
|
|
return {
|
|
options: selectedProgram.opts<Record<string, unknown>>(),
|
|
rootTarget: rootProgram.processedArgs[0],
|
|
invocations: {
|
|
jellyfinInvocation,
|
|
ytInvocation,
|
|
configInvocation,
|
|
mpvInvocation,
|
|
appInvocation,
|
|
doctorTriggered,
|
|
doctorLogLevel,
|
|
texthookerTriggered,
|
|
texthookerLogLevel,
|
|
},
|
|
};
|
|
}
|