Enhance AniList character dictionary sync and subtitle features (#15)

This commit is contained in:
2026-03-07 18:30:59 -08:00
committed by GitHub
parent 2f07c3407a
commit e18985fb14
696 changed files with 14297 additions and 173564 deletions

View File

@@ -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,36 @@ 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']]);
});
test('dictionary command throws if app handoff unexpectedly returns', () => {
const context = createContext();
context.args.dictionary = true;
assert.throws(
() =>
runDictionaryCommand(context, {
runAppCommandWithInherit: () => undefined as never,
}),
/unexpectedly returned/,
);
});

View 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);
throw new Error('Dictionary command app handoff unexpectedly returned.');
}

View File

@@ -1,5 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { spawn } from 'node:child_process';
import { fail, log } from '../log.js';
import { commandExists, isYoutubeTarget, realpathMaybe, resolvePathMaybe } from '../util.js';
import { collectVideos, showFzfMenu, showRofiMenu } from '../picker.js';
@@ -14,6 +15,15 @@ import {
import { generateYoutubeSubtitles } from '../youtube.js';
import type { Args } from '../types.js';
import type { LauncherCommandContext } from './context.js';
import { ensureLauncherSetupReady } from '../setup-gate.js';
import {
getDefaultConfigDir,
getSetupStatePath,
readSetupState,
} from '../../src/shared/setup-state.js';
const SETUP_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
const SETUP_POLL_INTERVAL_MS = 500;
function checkDependencies(args: Args): void {
const missing: string[] = [];
@@ -85,12 +95,47 @@ function registerCleanup(context: LauncherCommandContext): void {
});
}
async function ensurePlaybackSetupReady(context: LauncherCommandContext): Promise<void> {
const { args, appPath } = context;
if (!appPath) return;
const configDir = getDefaultConfigDir({
xdgConfigHome: process.env.XDG_CONFIG_HOME,
homeDir: os.homedir(),
});
const statePath = getSetupStatePath(configDir);
const ready = await ensureLauncherSetupReady({
readSetupState: () => readSetupState(statePath),
launchSetupApp: () => {
const setupArgs = ['--background', '--setup'];
if (args.logLevel) {
setupArgs.push('--log-level', args.logLevel);
}
const child = spawn(appPath, setupArgs, {
detached: true,
stdio: 'ignore',
});
child.unref();
},
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
now: () => Date.now(),
timeoutMs: SETUP_WAIT_TIMEOUT_MS,
pollIntervalMs: SETUP_POLL_INTERVAL_MS,
});
if (!ready) {
fail('SubMiner setup is incomplete. Complete setup in the app, then retry playback.');
}
}
export async function runPlaybackCommand(context: LauncherCommandContext): Promise<void> {
const { args, appPath, scriptPath, mpvSocketPath, pluginRuntimeConfig, processAdapter } = context;
if (!appPath) {
fail('SubMiner AppImage not found. Install to ~/.local/bin/ or set SUBMINER_APPIMAGE_PATH.');
}
await ensurePlaybackSetupReady(context);
if (!args.target) {
checkPickerDependencies(args);
}