Files
SubMiner/src/cli/args.ts
sudacode 20f53c0b70 Switch known-word cache to incremental sync and doctor refresh
- Load persisted known-word cache on startup; reconcile adds/deletes/edits on timed sync
- Add `knownWords.addMinedWordsImmediately` (default `true`) for immediate mined-word updates
- Route full rebuild to explicit `subminer doctor --refresh-known-words` and expand tests/docs
2026-03-19 19:29:58 -07:00

486 lines
17 KiB
TypeScript

export interface CliArgs {
background: boolean;
start: boolean;
launchMpv: boolean;
launchMpvTargets: string[];
stop: boolean;
toggle: boolean;
toggleVisibleOverlay: boolean;
settings: boolean;
setup: boolean;
show: boolean;
hide: boolean;
showVisibleOverlay: boolean;
hideVisibleOverlay: boolean;
copySubtitle: boolean;
copySubtitleMultiple: boolean;
mineSentence: boolean;
mineSentenceMultiple: boolean;
updateLastCardFromClipboard: boolean;
refreshKnownWords: boolean;
toggleSecondarySub: boolean;
triggerFieldGrouping: boolean;
triggerSubsync: boolean;
markAudioCard: boolean;
openRuntimeOptions: boolean;
anilistStatus: boolean;
anilistLogout: boolean;
anilistSetup: boolean;
anilistRetryQueue: boolean;
dictionary: boolean;
dictionaryTarget?: string;
stats: boolean;
statsBackground?: boolean;
statsStop?: boolean;
statsCleanup?: boolean;
statsCleanupVocab?: boolean;
statsCleanupLifetime?: boolean;
statsResponsePath?: string;
jellyfin: boolean;
jellyfinLogin: boolean;
jellyfinLogout: boolean;
jellyfinLibraries: boolean;
jellyfinItems: boolean;
jellyfinSubtitles: boolean;
jellyfinSubtitleUrlsOnly: boolean;
jellyfinPlay: boolean;
jellyfinRemoteAnnounce: boolean;
jellyfinPreviewAuth: boolean;
texthooker: boolean;
help: boolean;
autoStartOverlay: boolean;
generateConfig: boolean;
configPath?: string;
backupOverwrite: boolean;
socketPath?: string;
backend?: string;
texthookerPort?: number;
jellyfinServer?: string;
jellyfinUsername?: string;
jellyfinPassword?: string;
jellyfinLibraryId?: string;
jellyfinItemId?: string;
jellyfinSearch?: string;
jellyfinLimit?: number;
jellyfinRecursive?: boolean;
jellyfinIncludeItemTypes?: string;
jellyfinAudioStreamIndex?: number;
jellyfinSubtitleStreamIndex?: number;
jellyfinResponsePath?: string;
debug: boolean;
logLevel?: 'debug' | 'info' | 'warn' | 'error';
}
export type CliCommandSource = 'initial' | 'second-instance';
export function parseArgs(argv: string[]): CliArgs {
const args: CliArgs = {
background: false,
start: false,
launchMpv: false,
launchMpvTargets: [],
stop: false,
toggle: false,
toggleVisibleOverlay: false,
settings: false,
setup: false,
show: false,
hide: false,
showVisibleOverlay: false,
hideVisibleOverlay: false,
copySubtitle: false,
copySubtitleMultiple: false,
mineSentence: false,
mineSentenceMultiple: false,
updateLastCardFromClipboard: false,
refreshKnownWords: false,
toggleSecondarySub: false,
triggerFieldGrouping: false,
triggerSubsync: false,
markAudioCard: false,
openRuntimeOptions: false,
anilistStatus: false,
anilistLogout: false,
anilistSetup: false,
anilistRetryQueue: false,
dictionary: false,
stats: false,
statsBackground: false,
statsStop: false,
statsCleanup: false,
statsCleanupVocab: false,
statsCleanupLifetime: false,
jellyfin: false,
jellyfinLogin: false,
jellyfinLogout: false,
jellyfinLibraries: false,
jellyfinItems: false,
jellyfinSubtitles: false,
jellyfinSubtitleUrlsOnly: false,
jellyfinPlay: false,
jellyfinRemoteAnnounce: false,
jellyfinPreviewAuth: false,
texthooker: false,
help: false,
autoStartOverlay: false,
generateConfig: false,
backupOverwrite: false,
debug: false,
};
const readValue = (value?: string): string | undefined => {
if (!value) return undefined;
if (value.startsWith('--')) return undefined;
return value;
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg || !arg.startsWith('--')) continue;
if (arg === '--background') args.background = true;
else if (arg === '--start') args.start = true;
else if (arg === '--launch-mpv') {
args.launchMpv = true;
args.launchMpvTargets = argv.slice(i + 1).filter((value) => value && !value.startsWith('--'));
break;
} else if (arg === '--stop') args.stop = true;
else if (arg === '--toggle') args.toggle = true;
else if (arg === '--toggle-visible-overlay') args.toggleVisibleOverlay = true;
else if (arg === '--settings' || arg === '--yomitan') args.settings = true;
else if (arg === '--setup') args.setup = true;
else if (arg === '--show') args.show = true;
else if (arg === '--hide') args.hide = true;
else if (arg === '--show-visible-overlay') args.showVisibleOverlay = true;
else if (arg === '--hide-visible-overlay') args.hideVisibleOverlay = true;
else if (arg === '--copy-subtitle') args.copySubtitle = true;
else if (arg === '--copy-subtitle-multiple') args.copySubtitleMultiple = true;
else if (arg === '--mine-sentence') args.mineSentence = true;
else if (arg === '--mine-sentence-multiple') args.mineSentenceMultiple = true;
else if (arg === '--update-last-card-from-clipboard') args.updateLastCardFromClipboard = true;
else if (arg === '--refresh-known-words') args.refreshKnownWords = true;
else if (arg === '--toggle-secondary-sub') args.toggleSecondarySub = true;
else if (arg === '--trigger-field-grouping') args.triggerFieldGrouping = true;
else if (arg === '--trigger-subsync') args.triggerSubsync = true;
else if (arg === '--mark-audio-card') args.markAudioCard = true;
else if (arg === '--open-runtime-options') args.openRuntimeOptions = true;
else if (arg === '--anilist-status') args.anilistStatus = true;
else if (arg === '--anilist-logout') args.anilistLogout = true;
else if (arg === '--anilist-setup') args.anilistSetup = true;
else if (arg === '--anilist-retry-queue') args.anilistRetryQueue = true;
else if (arg === '--dictionary') args.dictionary = true;
else if (arg.startsWith('--dictionary-target=')) {
const value = arg.split('=', 2)[1];
if (value) args.dictionaryTarget = value;
} else if (arg === '--dictionary-target') {
const value = readValue(argv[i + 1]);
if (value) args.dictionaryTarget = value;
} else if (arg === '--stats') args.stats = true;
else if (arg === '--stats-background') {
args.stats = true;
args.statsBackground = true;
} else if (arg === '--stats-stop') {
args.stats = true;
args.statsStop = true;
} else if (arg === '--stats-cleanup') args.statsCleanup = true;
else if (arg === '--stats-cleanup-vocab') args.statsCleanupVocab = true;
else if (arg === '--stats-cleanup-lifetime') args.statsCleanupLifetime = true;
else if (arg.startsWith('--stats-response-path=')) {
const value = arg.split('=', 2)[1];
if (value) args.statsResponsePath = value;
} else if (arg === '--stats-response-path') {
const value = readValue(argv[i + 1]);
if (value) args.statsResponsePath = value;
} else if (arg === '--jellyfin') args.jellyfin = true;
else if (arg === '--jellyfin-login') args.jellyfinLogin = true;
else if (arg === '--jellyfin-logout') args.jellyfinLogout = true;
else if (arg === '--jellyfin-libraries') args.jellyfinLibraries = true;
else if (arg === '--jellyfin-items') args.jellyfinItems = true;
else if (arg === '--jellyfin-subtitles') args.jellyfinSubtitles = true;
else if (arg === '--jellyfin-subtitle-urls') {
args.jellyfinSubtitles = true;
args.jellyfinSubtitleUrlsOnly = true;
} else if (arg === '--jellyfin-play') args.jellyfinPlay = true;
else if (arg === '--jellyfin-remote-announce') args.jellyfinRemoteAnnounce = true;
else if (arg === '--jellyfin-preview-auth') args.jellyfinPreviewAuth = true;
else if (arg === '--texthooker') args.texthooker = true;
else if (arg === '--auto-start-overlay') args.autoStartOverlay = true;
else if (arg === '--generate-config') args.generateConfig = true;
else if (arg === '--backup-overwrite') args.backupOverwrite = true;
else if (arg === '--help') args.help = true;
else if (arg === '--debug') args.debug = true;
else if (arg.startsWith('--log-level=')) {
const value = arg.split('=', 2)[1]?.toLowerCase();
if (value === 'debug' || value === 'info' || value === 'warn' || value === 'error') {
args.logLevel = value;
}
} else if (arg === '--log-level') {
const value = readValue(argv[i + 1])?.toLowerCase();
if (value === 'debug' || value === 'info' || value === 'warn' || value === 'error') {
args.logLevel = value;
}
} else if (arg.startsWith('--config-path=')) {
const value = arg.split('=', 2)[1];
if (value) args.configPath = value;
} else if (arg === '--config-path') {
const value = readValue(argv[i + 1]);
if (value) args.configPath = value;
} else if (arg.startsWith('--socket=')) {
const value = arg.split('=', 2)[1];
if (value) args.socketPath = value;
} else if (arg === '--socket') {
const value = readValue(argv[i + 1]);
if (value) args.socketPath = value;
} else if (arg.startsWith('--backend=')) {
const value = arg.split('=', 2)[1];
if (value) args.backend = value;
} else if (arg === '--backend') {
const value = readValue(argv[i + 1]);
if (value) args.backend = value;
} else if (arg.startsWith('--port=')) {
const value = Number(arg.split('=', 2)[1]);
if (!Number.isNaN(value)) args.texthookerPort = value;
} else if (arg === '--port') {
const value = Number(readValue(argv[i + 1]));
if (!Number.isNaN(value)) args.texthookerPort = value;
} else if (arg.startsWith('--jellyfin-server=')) {
const value = arg.split('=', 2)[1];
if (value) args.jellyfinServer = value;
} else if (arg === '--jellyfin-server') {
const value = readValue(argv[i + 1]);
if (value) args.jellyfinServer = value;
} else if (arg.startsWith('--jellyfin-username=')) {
const value = arg.split('=', 2)[1];
if (value) args.jellyfinUsername = value;
} else if (arg === '--jellyfin-username') {
const value = readValue(argv[i + 1]);
if (value) args.jellyfinUsername = value;
} else if (arg.startsWith('--jellyfin-password=')) {
const value = arg.split('=', 2)[1];
if (value) args.jellyfinPassword = value;
} else if (arg === '--jellyfin-password') {
const value = readValue(argv[i + 1]);
if (value) args.jellyfinPassword = value;
} else if (arg.startsWith('--jellyfin-library-id=')) {
const value = arg.split('=', 2)[1];
if (value) args.jellyfinLibraryId = value;
} else if (arg === '--jellyfin-library-id') {
const value = readValue(argv[i + 1]);
if (value) args.jellyfinLibraryId = value;
} else if (arg.startsWith('--jellyfin-item-id=')) {
const value = arg.split('=', 2)[1];
if (value) args.jellyfinItemId = value;
} else if (arg === '--jellyfin-item-id') {
const value = readValue(argv[i + 1]);
if (value) args.jellyfinItemId = value;
} else if (arg.startsWith('--jellyfin-search=')) {
const value = arg.split('=', 2)[1];
if (value) args.jellyfinSearch = value;
} else if (arg === '--jellyfin-search') {
const value = readValue(argv[i + 1]);
if (value) args.jellyfinSearch = value;
} else if (arg.startsWith('--jellyfin-limit=')) {
const value = Number(arg.split('=', 2)[1]);
if (Number.isFinite(value) && value > 0) args.jellyfinLimit = Math.floor(value);
} else if (arg === '--jellyfin-limit') {
const value = Number(readValue(argv[i + 1]));
if (Number.isFinite(value) && value > 0) args.jellyfinLimit = Math.floor(value);
} else if (arg.startsWith('--jellyfin-recursive=')) {
const value = arg.split('=', 2)[1]?.trim().toLowerCase();
if (value === 'true' || value === '1' || value === 'yes') args.jellyfinRecursive = true;
if (value === 'false' || value === '0' || value === 'no') args.jellyfinRecursive = false;
} else if (arg === '--jellyfin-recursive') {
const value = readValue(argv[i + 1])
?.trim()
.toLowerCase();
if (value === 'false' || value === '0' || value === 'no') {
args.jellyfinRecursive = false;
} else if (value === 'true' || value === '1' || value === 'yes') {
args.jellyfinRecursive = true;
}
} else if (arg === '--jellyfin-non-recursive') {
args.jellyfinRecursive = false;
} else if (arg.startsWith('--jellyfin-include-item-types=')) {
const value = arg.split('=', 2)[1];
if (value) args.jellyfinIncludeItemTypes = value;
} else if (arg === '--jellyfin-include-item-types') {
const value = readValue(argv[i + 1]);
if (value) args.jellyfinIncludeItemTypes = value;
} else if (arg.startsWith('--jellyfin-audio-stream-index=')) {
const value = Number(arg.split('=', 2)[1]);
if (Number.isInteger(value) && value >= 0) args.jellyfinAudioStreamIndex = value;
} else if (arg === '--jellyfin-audio-stream-index') {
const value = Number(readValue(argv[i + 1]));
if (Number.isInteger(value) && value >= 0) args.jellyfinAudioStreamIndex = value;
} else if (arg.startsWith('--jellyfin-subtitle-stream-index=')) {
const value = Number(arg.split('=', 2)[1]);
if (Number.isInteger(value) && value >= 0) args.jellyfinSubtitleStreamIndex = value;
} else if (arg === '--jellyfin-subtitle-stream-index') {
const value = Number(readValue(argv[i + 1]));
if (Number.isInteger(value) && value >= 0) args.jellyfinSubtitleStreamIndex = value;
} else if (arg.startsWith('--jellyfin-response-path=')) {
const value = arg.split('=', 2)[1];
if (value) args.jellyfinResponsePath = value;
} else if (arg === '--jellyfin-response-path') {
const value = readValue(argv[i + 1]);
if (value) args.jellyfinResponsePath = value;
}
}
return args;
}
export function hasExplicitCommand(args: CliArgs): boolean {
return (
args.background ||
args.start ||
args.launchMpv ||
args.stop ||
args.toggle ||
args.toggleVisibleOverlay ||
args.settings ||
args.setup ||
args.show ||
args.hide ||
args.showVisibleOverlay ||
args.hideVisibleOverlay ||
args.copySubtitle ||
args.copySubtitleMultiple ||
args.mineSentence ||
args.mineSentenceMultiple ||
args.updateLastCardFromClipboard ||
args.refreshKnownWords ||
args.toggleSecondarySub ||
args.triggerFieldGrouping ||
args.triggerSubsync ||
args.markAudioCard ||
args.openRuntimeOptions ||
args.anilistStatus ||
args.anilistLogout ||
args.anilistSetup ||
args.anilistRetryQueue ||
args.dictionary ||
args.stats ||
args.jellyfin ||
args.jellyfinLogin ||
args.jellyfinLogout ||
args.jellyfinLibraries ||
args.jellyfinItems ||
args.jellyfinSubtitles ||
args.jellyfinPlay ||
args.jellyfinRemoteAnnounce ||
args.jellyfinPreviewAuth ||
args.texthooker ||
args.generateConfig ||
args.help
);
}
export function isHeadlessInitialCommand(args: CliArgs): boolean {
return args.refreshKnownWords;
}
export function shouldStartApp(args: CliArgs): boolean {
if (args.stop && !args.start) return false;
if (
args.background ||
args.start ||
args.launchMpv ||
args.toggle ||
args.toggleVisibleOverlay ||
args.settings ||
args.setup ||
args.copySubtitle ||
args.copySubtitleMultiple ||
args.mineSentence ||
args.mineSentenceMultiple ||
args.updateLastCardFromClipboard ||
args.refreshKnownWords ||
args.toggleSecondarySub ||
args.triggerFieldGrouping ||
args.triggerSubsync ||
args.markAudioCard ||
args.openRuntimeOptions ||
args.dictionary ||
args.stats ||
args.jellyfin ||
args.jellyfinPlay ||
args.texthooker
) {
if (args.launchMpv) {
return false;
}
return true;
}
return false;
}
export function shouldRunSettingsOnlyStartup(args: CliArgs): boolean {
return (
args.settings &&
!args.background &&
!args.start &&
!args.stop &&
!args.toggle &&
!args.toggleVisibleOverlay &&
!args.show &&
!args.hide &&
!args.setup &&
!args.showVisibleOverlay &&
!args.hideVisibleOverlay &&
!args.copySubtitle &&
!args.copySubtitleMultiple &&
!args.mineSentence &&
!args.mineSentenceMultiple &&
!args.updateLastCardFromClipboard &&
!args.refreshKnownWords &&
!args.toggleSecondarySub &&
!args.triggerFieldGrouping &&
!args.triggerSubsync &&
!args.markAudioCard &&
!args.openRuntimeOptions &&
!args.anilistStatus &&
!args.anilistLogout &&
!args.anilistSetup &&
!args.anilistRetryQueue &&
!args.dictionary &&
!args.stats &&
!args.jellyfin &&
!args.jellyfinLogin &&
!args.jellyfinLogout &&
!args.jellyfinLibraries &&
!args.jellyfinItems &&
!args.jellyfinSubtitles &&
!args.jellyfinPlay &&
!args.jellyfinRemoteAnnounce &&
!args.jellyfinPreviewAuth &&
!args.texthooker &&
!args.help &&
!args.autoStartOverlay &&
!args.generateConfig &&
!args.backupOverwrite &&
!args.debug
);
}
export function commandNeedsOverlayRuntime(args: CliArgs): boolean {
return (
args.toggle ||
args.toggleVisibleOverlay ||
args.show ||
args.hide ||
args.showVisibleOverlay ||
args.hideVisibleOverlay ||
args.copySubtitle ||
args.copySubtitleMultiple ||
args.mineSentence ||
args.mineSentenceMultiple ||
args.updateLastCardFromClipboard ||
args.toggleSecondarySub ||
args.triggerFieldGrouping ||
args.triggerSubsync ||
args.markAudioCard ||
args.openRuntimeOptions
);
}