mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-06 19:57:26 -08:00
feat: add AniList character dictionary sync
This commit is contained in:
@@ -4,6 +4,7 @@ import { parseArgs } from '../config.js';
|
||||
import type { ProcessAdapter } from '../process-adapter.js';
|
||||
import type { LauncherCommandContext } from './context.js';
|
||||
import { runConfigCommand } from './config-command.js';
|
||||
import { runDictionaryCommand } from './dictionary-command.js';
|
||||
import { runDoctorCommand } from './doctor-command.js';
|
||||
import { runMpvPreAppCommand } from './mpv-command.js';
|
||||
|
||||
@@ -94,3 +95,23 @@ test('mpv pre-app command exits non-zero when socket is not ready', async () =>
|
||||
(error: unknown) => error instanceof ExitSignal && error.code === 1,
|
||||
);
|
||||
});
|
||||
|
||||
test('dictionary command forwards --dictionary and target path to app binary', () => {
|
||||
const context = createContext();
|
||||
context.args.dictionary = true;
|
||||
context.args.dictionaryTarget = '/tmp/anime';
|
||||
const forwarded: string[][] = [];
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
runDictionaryCommand(context, {
|
||||
runAppCommandWithInherit: (_appPath, appArgs) => {
|
||||
forwarded.push(appArgs);
|
||||
throw new ExitSignal(0);
|
||||
},
|
||||
}),
|
||||
(error: unknown) => error instanceof ExitSignal && error.code === 0,
|
||||
);
|
||||
|
||||
assert.deepEqual(forwarded, [['--dictionary', '--dictionary-target', '/tmp/anime']]);
|
||||
});
|
||||
|
||||
31
launcher/commands/dictionary-command.ts
Normal file
31
launcher/commands/dictionary-command.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { runAppCommandWithInherit } from '../mpv.js';
|
||||
import type { LauncherCommandContext } from './context.js';
|
||||
|
||||
interface DictionaryCommandDeps {
|
||||
runAppCommandWithInherit: (appPath: string, appArgs: string[]) => never;
|
||||
}
|
||||
|
||||
const defaultDeps: DictionaryCommandDeps = {
|
||||
runAppCommandWithInherit,
|
||||
};
|
||||
|
||||
export function runDictionaryCommand(
|
||||
context: LauncherCommandContext,
|
||||
deps: DictionaryCommandDeps = defaultDeps,
|
||||
): boolean {
|
||||
const { args, appPath } = context;
|
||||
if (!args.dictionary || !appPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const forwarded = ['--dictionary'];
|
||||
if (typeof args.dictionaryTarget === 'string' && args.dictionaryTarget.trim()) {
|
||||
forwarded.push('--dictionary-target', args.dictionaryTarget);
|
||||
}
|
||||
if (args.logLevel !== 'info') {
|
||||
forwarded.push('--log-level', args.logLevel);
|
||||
}
|
||||
|
||||
deps.runAppCommandWithInherit(appPath, forwarded);
|
||||
return true;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ test('launcher root help lists subcommands', () => {
|
||||
assert.match(output, /doctor/);
|
||||
assert.match(output, /config/);
|
||||
assert.match(output, /mpv/);
|
||||
assert.match(output, /dictionary\|dict/);
|
||||
assert.match(output, /texthooker/);
|
||||
assert.match(output, /app\|bin/);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fail } from '../log.js';
|
||||
import type {
|
||||
Args,
|
||||
@@ -68,6 +69,27 @@ function parseBackend(value: string): Backend {
|
||||
fail(`Invalid backend: ${value} (must be auto, hyprland, x11, or macos)`);
|
||||
}
|
||||
|
||||
function parseDictionaryTarget(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
fail('Dictionary target path is required.');
|
||||
}
|
||||
if (isUrlTarget(trimmed)) {
|
||||
fail('Dictionary target must be a local file or directory path, not a URL.');
|
||||
}
|
||||
const resolved = path.resolve(resolvePathMaybe(trimmed));
|
||||
let stat: fs.Stats | null = null;
|
||||
try {
|
||||
stat = fs.statSync(resolved);
|
||||
} catch {
|
||||
stat = null;
|
||||
}
|
||||
if (!stat || (!stat.isFile() && !stat.isDirectory())) {
|
||||
fail(`Dictionary target path must be an existing file or directory: ${trimmed}`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig): Args {
|
||||
const envMode = (process.env.SUBMINER_YT_SUBGEN_MODE || '').toLowerCase();
|
||||
const defaultMode: YoutubeSubgenMode =
|
||||
@@ -114,6 +136,7 @@ export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig):
|
||||
jellyfinLogout: false,
|
||||
jellyfinPlay: false,
|
||||
jellyfinDiscovery: false,
|
||||
dictionary: false,
|
||||
doctor: false,
|
||||
configPath: false,
|
||||
configShow: false,
|
||||
@@ -170,6 +193,10 @@ export function applyRootOptionsToArgs(
|
||||
}
|
||||
|
||||
export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations): void {
|
||||
if (invocations.dictionaryTriggered) parsed.dictionary = true;
|
||||
if (invocations.dictionaryTarget) {
|
||||
parsed.dictionaryTarget = parseDictionaryTarget(invocations.dictionaryTarget);
|
||||
}
|
||||
if (invocations.doctorTriggered) parsed.doctor = true;
|
||||
if (invocations.texthookerTriggered) parsed.texthookerOnly = true;
|
||||
|
||||
@@ -230,6 +257,10 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
|
||||
if (invocations.ytInvocation.target) ensureTarget(invocations.ytInvocation.target, parsed);
|
||||
}
|
||||
|
||||
if (invocations.dictionaryLogLevel) {
|
||||
parsed.logLevel = parseLogLevel(invocations.dictionaryLogLevel);
|
||||
}
|
||||
|
||||
if (invocations.doctorLogLevel) parsed.logLevel = parseLogLevel(invocations.doctorLogLevel);
|
||||
if (invocations.texthookerLogLevel)
|
||||
parsed.logLevel = parseLogLevel(invocations.texthookerLogLevel);
|
||||
|
||||
@@ -36,6 +36,9 @@ export interface CliInvocations {
|
||||
configInvocation: CommandActionInvocation | null;
|
||||
mpvInvocation: CommandActionInvocation | null;
|
||||
appInvocation: { appArgs: string[] } | null;
|
||||
dictionaryTriggered: boolean;
|
||||
dictionaryTarget: string | null;
|
||||
dictionaryLogLevel: string | null;
|
||||
doctorTriggered: boolean;
|
||||
doctorLogLevel: string | null;
|
||||
texthookerTriggered: boolean;
|
||||
@@ -81,6 +84,8 @@ function getTopLevelCommand(argv: string[]): { name: string; index: number } | n
|
||||
'doctor',
|
||||
'config',
|
||||
'mpv',
|
||||
'dictionary',
|
||||
'dict',
|
||||
'texthooker',
|
||||
'app',
|
||||
'bin',
|
||||
@@ -128,6 +133,9 @@ export function parseCliPrograms(
|
||||
let configInvocation: CommandActionInvocation | null = null;
|
||||
let mpvInvocation: CommandActionInvocation | null = null;
|
||||
let appInvocation: { appArgs: string[] } | null = null;
|
||||
let dictionaryTriggered = false;
|
||||
let dictionaryTarget: string | null = null;
|
||||
let dictionaryLogLevel: string | null = null;
|
||||
let doctorLogLevel: string | null = null;
|
||||
let texthookerLogLevel: string | null = null;
|
||||
let doctorTriggered = false;
|
||||
@@ -214,6 +222,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')
|
||||
.option('--log-level <level>', 'Log level')
|
||||
.action((target: string, options: Record<string, unknown>) => {
|
||||
dictionaryTriggered = true;
|
||||
dictionaryTarget = target;
|
||||
dictionaryLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
|
||||
});
|
||||
|
||||
commandProgram
|
||||
.command('doctor')
|
||||
.description('Run dependency and environment checks')
|
||||
@@ -289,6 +309,9 @@ export function parseCliPrograms(
|
||||
configInvocation,
|
||||
mpvInvocation,
|
||||
appInvocation,
|
||||
dictionaryTriggered,
|
||||
dictionaryTarget,
|
||||
dictionaryLogLevel,
|
||||
doctorTriggered,
|
||||
doctorLogLevel,
|
||||
texthookerTriggered,
|
||||
|
||||
@@ -162,6 +162,35 @@ test('doctor reports checks and exits non-zero without hard dependencies', () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('dictionary command forwards --dictionary and --dictionary-target 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-folder');
|
||||
fs.mkdirSync(targetPath, { recursive: true });
|
||||
const result = runLauncher(['dictionary', targetPath], env);
|
||||
|
||||
assert.equal(result.status, 0);
|
||||
assert.equal(
|
||||
fs.readFileSync(capturePath, 'utf8'),
|
||||
`--dictionary\n--dictionary-target\n${targetPath}\n`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('jellyfin discovery routes to app --background and remote announce with log-level forwarding', () => {
|
||||
withTempDir((root) => {
|
||||
const homeDir = path.join(root, 'home');
|
||||
|
||||
@@ -13,6 +13,7 @@ import { runDoctorCommand } from './commands/doctor-command.js';
|
||||
import { runConfigCommand } from './commands/config-command.js';
|
||||
import { runMpvPostAppCommand, runMpvPreAppCommand } from './commands/mpv-command.js';
|
||||
import { runAppPassthroughCommand, runTexthookerCommand } from './commands/app-command.js';
|
||||
import { runDictionaryCommand } from './commands/dictionary-command.js';
|
||||
import { runJellyfinCommand } from './commands/jellyfin-command.js';
|
||||
import { runPlaybackCommand } from './commands/playback-command.js';
|
||||
|
||||
@@ -90,6 +91,10 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (runDictionaryCommand(appContext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await runJellyfinCommand(appContext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
|
||||
jellyfinLogout: false,
|
||||
jellyfinPlay: false,
|
||||
jellyfinDiscovery: false,
|
||||
dictionary: false,
|
||||
doctor: false,
|
||||
configPath: false,
|
||||
configShow: false,
|
||||
|
||||
@@ -50,3 +50,11 @@ test('parseArgs maps mpv idle action', () => {
|
||||
assert.equal(parsed.mpvIdle, true);
|
||||
assert.equal(parsed.mpvStatus, false);
|
||||
});
|
||||
|
||||
test('parseArgs maps dictionary command and log-level override', () => {
|
||||
const parsed = parseArgs(['dictionary', '.', '--log-level', 'debug'], 'subminer', {});
|
||||
|
||||
assert.equal(parsed.dictionary, true);
|
||||
assert.equal(parsed.dictionaryTarget, process.cwd());
|
||||
assert.equal(parsed.logLevel, 'debug');
|
||||
});
|
||||
|
||||
@@ -92,6 +92,8 @@ export interface Args {
|
||||
jellyfinLogout: boolean;
|
||||
jellyfinPlay: boolean;
|
||||
jellyfinDiscovery: boolean;
|
||||
dictionary: boolean;
|
||||
dictionaryTarget?: string;
|
||||
doctor: boolean;
|
||||
configPath: boolean;
|
||||
configShow: boolean;
|
||||
|
||||
Reference in New Issue
Block a user