mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-26 04:19:27 -07:00
feat: add manual AniList selection for character dictionaries
This commit is contained in:
@@ -190,7 +190,43 @@ test('dictionary command forwards --dictionary and target path to app binary', (
|
||||
});
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(forwarded, [['--dictionary', '--dictionary-target', '/tmp/anime']]);
|
||||
assert.deepEqual(forwarded, [['--start', '--dictionary', '--dictionary-target', '/tmp/anime']]);
|
||||
});
|
||||
|
||||
test('dictionary command forwards candidate and selection modes to app binary', () => {
|
||||
const candidatesContext = createContext();
|
||||
candidatesContext.args.dictionary = true;
|
||||
candidatesContext.args.dictionaryCandidates = true;
|
||||
candidatesContext.args.dictionaryTarget = '/tmp/anime.mkv';
|
||||
const selectContext = createContext();
|
||||
selectContext.args.dictionary = true;
|
||||
selectContext.args.dictionarySelect = true;
|
||||
selectContext.args.dictionaryAnilistId = 21355;
|
||||
selectContext.args.dictionaryTarget = '/tmp/anime.mkv';
|
||||
const forwarded: string[][] = [];
|
||||
|
||||
runDictionaryCommand(candidatesContext, {
|
||||
runAppCommandWithInherit: (_appPath, appArgs) => {
|
||||
forwarded.push(appArgs);
|
||||
},
|
||||
});
|
||||
runDictionaryCommand(selectContext, {
|
||||
runAppCommandWithInherit: (_appPath, appArgs) => {
|
||||
forwarded.push(appArgs);
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(forwarded, [
|
||||
['--start', '--dictionary-candidates', '--dictionary-target', '/tmp/anime.mkv'],
|
||||
[
|
||||
'--start',
|
||||
'--dictionary-select',
|
||||
'--dictionary-anilist-id',
|
||||
'21355',
|
||||
'--dictionary-target',
|
||||
'/tmp/anime.mkv',
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('dictionary command returns after app handoff starts', () => {
|
||||
|
||||
@@ -18,7 +18,20 @@ export function runDictionaryCommand(
|
||||
return false;
|
||||
}
|
||||
|
||||
const forwarded = ['--dictionary'];
|
||||
const forwarded = [
|
||||
'--start',
|
||||
args.dictionaryCandidates
|
||||
? '--dictionary-candidates'
|
||||
: args.dictionarySelect
|
||||
? '--dictionary-select'
|
||||
: '--dictionary',
|
||||
];
|
||||
if (args.dictionarySelect) {
|
||||
if (!args.dictionaryAnilistId) {
|
||||
throw new Error('Dictionary selection requires an AniList media ID.');
|
||||
}
|
||||
forwarded.push('--dictionary-anilist-id', String(args.dictionaryAnilistId));
|
||||
}
|
||||
if (typeof args.dictionaryTarget === 'string' && args.dictionaryTarget.trim()) {
|
||||
forwarded.push('--dictionary-target', args.dictionaryTarget);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ function createContext(): LauncherCommandContext {
|
||||
jellyfinPlay: false,
|
||||
jellyfinDiscovery: false,
|
||||
dictionary: false,
|
||||
dictionaryCandidates: false,
|
||||
dictionarySelect: false,
|
||||
stats: false,
|
||||
doctor: false,
|
||||
doctorRefreshKnownWords: false,
|
||||
|
||||
@@ -129,6 +129,9 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => {
|
||||
dictionaryTriggered: false,
|
||||
dictionaryTarget: null,
|
||||
dictionaryLogLevel: null,
|
||||
dictionaryCandidates: false,
|
||||
dictionarySelect: false,
|
||||
dictionaryAnilistId: null,
|
||||
statsTriggered: false,
|
||||
statsBackground: false,
|
||||
statsStop: false,
|
||||
|
||||
@@ -89,6 +89,14 @@ function parseDictionaryTarget(value: string): string {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function parseDictionaryAnilistId(value: string): number {
|
||||
const id = Number.parseInt(value, 10);
|
||||
if (!Number.isSafeInteger(id) || id <= 0 || String(id) !== value.trim()) {
|
||||
fail(`Invalid AniList media ID: ${value}`);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function createDefaultArgs(
|
||||
launcherConfig: LauncherYoutubeSubgenConfig,
|
||||
mpvConfig: LauncherMpvConfig = {},
|
||||
@@ -138,6 +146,8 @@ export function createDefaultArgs(
|
||||
jellyfinPlay: false,
|
||||
jellyfinDiscovery: false,
|
||||
dictionary: false,
|
||||
dictionaryCandidates: false,
|
||||
dictionarySelect: false,
|
||||
stats: false,
|
||||
statsBackground: false,
|
||||
statsStop: false,
|
||||
@@ -214,6 +224,11 @@ export function applyRootOptionsToArgs(
|
||||
|
||||
export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations): void {
|
||||
if (invocations.dictionaryTriggered) parsed.dictionary = true;
|
||||
if (invocations.dictionaryCandidates) parsed.dictionaryCandidates = true;
|
||||
if (invocations.dictionarySelect) parsed.dictionarySelect = true;
|
||||
if (invocations.dictionaryAnilistId) {
|
||||
parsed.dictionaryAnilistId = parseDictionaryAnilistId(invocations.dictionaryAnilistId);
|
||||
}
|
||||
if (invocations.statsTriggered) parsed.stats = true;
|
||||
if (invocations.statsBackground) parsed.statsBackground = true;
|
||||
if (invocations.statsStop) parsed.statsStop = true;
|
||||
@@ -222,6 +237,12 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
|
||||
if (invocations.statsCleanupLifetime) parsed.statsCleanupLifetime = true;
|
||||
if (invocations.dictionaryTarget) {
|
||||
parsed.dictionaryTarget = parseDictionaryTarget(invocations.dictionaryTarget);
|
||||
} else if (
|
||||
invocations.dictionaryTriggered &&
|
||||
!invocations.dictionaryCandidates &&
|
||||
!invocations.dictionarySelect
|
||||
) {
|
||||
fail('Dictionary target path is required.');
|
||||
}
|
||||
if (invocations.doctorTriggered) parsed.doctor = true;
|
||||
if (invocations.doctorRefreshKnownWords) parsed.doctorRefreshKnownWords = true;
|
||||
|
||||
@@ -27,6 +27,9 @@ export interface CliInvocations {
|
||||
dictionaryTriggered: boolean;
|
||||
dictionaryTarget: string | null;
|
||||
dictionaryLogLevel: string | null;
|
||||
dictionaryCandidates: boolean;
|
||||
dictionarySelect: boolean;
|
||||
dictionaryAnilistId: string | null;
|
||||
statsTriggered: boolean;
|
||||
statsBackground: boolean;
|
||||
statsStop: boolean;
|
||||
@@ -136,6 +139,9 @@ export function parseCliPrograms(
|
||||
let dictionaryTriggered = false;
|
||||
let dictionaryTarget: string | null = null;
|
||||
let dictionaryLogLevel: string | null = null;
|
||||
let dictionaryCandidates = false;
|
||||
let dictionarySelect = false;
|
||||
let dictionaryAnilistId: string | null = null;
|
||||
let statsTriggered = false;
|
||||
let statsBackground = false;
|
||||
let statsStop = false;
|
||||
@@ -207,13 +213,18 @@ export function parseCliPrograms(
|
||||
commandProgram
|
||||
.command('dictionary')
|
||||
.alias('dict')
|
||||
.description('Generate character dictionary ZIP from a file or directory target')
|
||||
.argument('<target>', 'Video file path or anime directory path')
|
||||
.description('Generate or correct character dictionary AniList matches')
|
||||
.argument('[target]', 'Video file path or anime directory path')
|
||||
.option('--candidates', 'List AniList candidates for a character dictionary target')
|
||||
.option('--select <id>', 'Pin an AniList media ID for the target series')
|
||||
.option('--log-level <level>', 'Log level')
|
||||
.action((target: string, options: Record<string, unknown>) => {
|
||||
.action((target: string | undefined, options: Record<string, unknown>) => {
|
||||
dictionaryTriggered = true;
|
||||
dictionaryTarget = target;
|
||||
dictionaryTarget = target ?? null;
|
||||
dictionaryLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
|
||||
dictionaryCandidates = options.candidates === true;
|
||||
dictionarySelect = typeof options.select === 'string';
|
||||
dictionaryAnilistId = typeof options.select === 'string' ? options.select : null;
|
||||
});
|
||||
|
||||
commandProgram
|
||||
@@ -338,6 +349,9 @@ export function parseCliPrograms(
|
||||
dictionaryTriggered,
|
||||
dictionaryTarget,
|
||||
dictionaryLogLevel,
|
||||
dictionaryCandidates,
|
||||
dictionarySelect,
|
||||
dictionaryAnilistId,
|
||||
statsTriggered,
|
||||
statsBackground,
|
||||
statsStop,
|
||||
|
||||
@@ -464,7 +464,40 @@ test('dictionary command forwards --dictionary and --dictionary-target to app co
|
||||
assert.equal(result.status, 0);
|
||||
assert.equal(
|
||||
fs.readFileSync(capturePath, 'utf8'),
|
||||
`--dictionary\n--dictionary-target\n${targetPath}\n`,
|
||||
`--start\n--dictionary\n--dictionary-target\n${targetPath}\n`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('dictionary command forwards manual AniList selection modes to app command path', () => {
|
||||
withTempDir((root) => {
|
||||
const homeDir = path.join(root, 'home');
|
||||
const xdgConfigHome = path.join(root, 'xdg');
|
||||
const appPath = path.join(root, 'fake-subminer.sh');
|
||||
const capturePath = path.join(root, 'captured-args.txt');
|
||||
fs.writeFileSync(
|
||||
appPath,
|
||||
'#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" >> "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n',
|
||||
);
|
||||
fs.chmodSync(appPath, 0o755);
|
||||
|
||||
const env = {
|
||||
...makeTestEnv(homeDir, xdgConfigHome),
|
||||
SUBMINER_APPIMAGE_PATH: appPath,
|
||||
SUBMINER_TEST_CAPTURE: capturePath,
|
||||
};
|
||||
const targetPath = path.join(root, 'anime.mkv');
|
||||
fs.writeFileSync(targetPath, '');
|
||||
|
||||
assert.equal(runLauncher(['dictionary', '--candidates', targetPath], env).status, 0);
|
||||
assert.equal(
|
||||
fs.readFileSync(capturePath, 'utf8'),
|
||||
`--start\n--dictionary-candidates\n--dictionary-target\n${targetPath}\n`,
|
||||
);
|
||||
assert.equal(runLauncher(['dictionary', '--select', '21355', targetPath], env).status, 0);
|
||||
assert.equal(
|
||||
fs.readFileSync(capturePath, 'utf8'),
|
||||
`--start\n--dictionary-select\n--dictionary-anilist-id\n21355\n--dictionary-target\n${targetPath}\n`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -415,6 +415,8 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
|
||||
jellyfinPlay: false,
|
||||
jellyfinDiscovery: false,
|
||||
dictionary: false,
|
||||
dictionaryCandidates: false,
|
||||
dictionarySelect: false,
|
||||
stats: false,
|
||||
doctor: false,
|
||||
doctorRefreshKnownWords: false,
|
||||
|
||||
@@ -99,6 +99,17 @@ test('parseArgs maps dictionary command and log-level override', () => {
|
||||
assert.equal(parsed.logLevel, 'debug');
|
||||
});
|
||||
|
||||
test('parseArgs maps dictionary candidate lookup and manual selection', () => {
|
||||
const candidateParsed = parseArgs(['dictionary', '--candidates', '.'], 'subminer', {});
|
||||
assert.equal(candidateParsed.dictionaryCandidates, true);
|
||||
assert.equal(candidateParsed.dictionaryTarget, process.cwd());
|
||||
|
||||
const selectParsed = parseArgs(['dictionary', '--select', '21355', '.'], 'subminer', {});
|
||||
assert.equal(selectParsed.dictionarySelect, true);
|
||||
assert.equal(selectParsed.dictionaryAnilistId, 21355);
|
||||
assert.equal(selectParsed.dictionaryTarget, process.cwd());
|
||||
});
|
||||
|
||||
test('parseArgs maps stats command and log-level override', () => {
|
||||
const parsed = parseArgs(['stats', '--log-level', 'debug'], 'subminer', {});
|
||||
|
||||
|
||||
@@ -121,6 +121,9 @@ export interface Args {
|
||||
jellyfinPlay: boolean;
|
||||
jellyfinDiscovery: boolean;
|
||||
dictionary: boolean;
|
||||
dictionaryCandidates: boolean;
|
||||
dictionarySelect: boolean;
|
||||
dictionaryAnilistId?: number;
|
||||
stats: boolean;
|
||||
statsBackground?: boolean;
|
||||
statsStop?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user