refactor: unify cli and runtime wiring for startup and youtube flow

This commit is contained in:
2026-03-22 18:38:54 -07:00
parent 3fb33af116
commit 7d8d2ae7a7
48 changed files with 1009 additions and 370 deletions

View File

@@ -60,6 +60,9 @@ test('build cli command context deps maps handlers and values', () => {
runJellyfinCommand: async () => {
calls.push('run-jellyfin');
},
runYoutubePlaybackFlow: async () => {
calls.push('run-youtube');
},
openYomitanSettings: () => calls.push('yomitan'),
cycleSecondarySubMode: () => calls.push('cycle-secondary'),
openRuntimeOptionsPalette: () => calls.push('runtime-options'),

View File

@@ -36,6 +36,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
runJellyfinCommand: (args: CliArgs) => Promise<void>;
runYoutubePlaybackFlow: CliCommandContextFactoryDeps['runYoutubePlaybackFlow'];
openYomitanSettings: () => void;
cycleSecondarySubMode: () => void;
openRuntimeOptionsPalette: () => void;
@@ -83,6 +84,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
generateCharacterDictionary: deps.generateCharacterDictionary,
runStatsCommand: deps.runStatsCommand,
runJellyfinCommand: deps.runJellyfinCommand,
runYoutubePlaybackFlow: deps.runYoutubePlaybackFlow,
openYomitanSettings: deps.openYomitanSettings,
cycleSecondarySubMode: deps.cycleSecondarySubMode,
openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette,

View File

@@ -9,6 +9,7 @@ test('cli command context factory composes main deps and context handlers', () =
mpvClient: null,
texthookerPort: 5174,
overlayRuntimeInitialized: false,
youtubePlaybackFlowPending: false,
};
const createContext = createCliCommandContextFactory({
@@ -63,6 +64,7 @@ test('cli command context factory composes main deps and context handlers', () =
}),
runStatsCommand: async () => {},
runJellyfinCommand: async () => {},
runYoutubePlaybackFlow: async () => {},
openYomitanSettings: () => {},
cycleSecondarySubMode: () => {},
openRuntimeOptionsPalette: () => {},

View File

@@ -9,6 +9,7 @@ test('cli command context main deps builder maps state and callbacks', async ()
mpvClient: null,
texthookerPort: 5174,
overlayRuntimeInitialized: false,
youtubePlaybackFlowPending: false,
};
const build = createBuildCliCommandContextMainDepsHandler({
@@ -84,6 +85,9 @@ test('cli command context main deps builder maps state and callbacks', async ()
runJellyfinCommand: async () => {
calls.push('run-jellyfin');
},
runYoutubePlaybackFlow: async () => {
calls.push('run-youtube');
},
openYomitanSettings: () => calls.push('open-yomitan'),
cycleSecondarySubMode: () => calls.push('cycle-secondary'),

View File

@@ -1,4 +1,5 @@
import type { CliArgs } from '../../cli/args';
import type { YoutubeFlowMode } from '../../types';
import type { CliCommandContextFactoryDeps } from './cli-command-context';
type CliCommandContextMainState = {
@@ -41,6 +42,11 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
runJellyfinCommand: (args: CliArgs) => Promise<void>;
runYoutubePlaybackFlow: (request: {
url: string;
mode: YoutubeFlowMode;
source: 'initial' | 'second-instance';
}) => Promise<void>;
openYomitanSettings: () => void;
cycleSecondarySubMode: () => void;
@@ -95,6 +101,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
deps.generateCharacterDictionary(targetPath),
runStatsCommand: (args: CliArgs, source) => deps.runStatsCommand(args, source),
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
runYoutubePlaybackFlow: (request) => deps.runYoutubePlaybackFlow(request),
openYomitanSettings: () => deps.openYomitanSettings(),
cycleSecondarySubMode: () => deps.cycleSecondarySubMode(),
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),

View File

@@ -50,6 +50,7 @@ function createDeps() {
}),
runStatsCommand: async () => {},
runJellyfinCommand: async () => {},
runYoutubePlaybackFlow: async () => {},
openYomitanSettings: () => {},
cycleSecondarySubMode: () => {},
openRuntimeOptionsPalette: () => {},

View File

@@ -1,4 +1,5 @@
import type { CliArgs } from '../../cli/args';
import type { YoutubeFlowMode } from '../../types';
import type {
CliCommandRuntimeServiceContext,
CliCommandRuntimeServiceContextHandlers,
@@ -41,6 +42,11 @@ export type CliCommandContextFactoryDeps = {
generateCharacterDictionary: CliCommandRuntimeServiceContext['generateCharacterDictionary'];
runStatsCommand: CliCommandRuntimeServiceContext['runStatsCommand'];
runJellyfinCommand: (args: CliArgs) => Promise<void>;
runYoutubePlaybackFlow: (request: {
url: string;
mode: YoutubeFlowMode;
source: 'initial' | 'second-instance';
}) => Promise<void>;
openYomitanSettings: () => void;
cycleSecondarySubMode: () => void;
openRuntimeOptionsPalette: () => void;
@@ -95,6 +101,7 @@ export function createCliCommandContext(
generateCharacterDictionary: deps.generateCharacterDictionary,
runStatsCommand: deps.runStatsCommand,
runJellyfinCommand: deps.runJellyfinCommand,
runYoutubePlaybackFlow: deps.runYoutubePlaybackFlow,
openYomitanSettings: deps.openYomitanSettings,
cycleSecondarySubMode: deps.cycleSecondarySubMode,
openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette,

View File

@@ -67,6 +67,7 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
getAnilistQueueStatus: () => ({}) as never,
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
onYoutubePickerResolve: async () => ({ ok: true, message: 'ok' }),
},
ankiJimakuDeps: {
patchAnkiConnectEnabled: () => {},

View File

@@ -72,6 +72,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -280,6 +281,7 @@ test('composeMpvRuntimeHandlers skips MeCab warmup when all POS-dependent annota
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -411,6 +413,7 @@ test('composeMpvRuntimeHandlers runs tokenization warmup once across sequential
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -550,6 +553,7 @@ test('composeMpvRuntimeHandlers does not block first tokenization on dictionary
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -683,6 +687,7 @@ test('composeMpvRuntimeHandlers shows annotation loading OSD after tokenization-
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -830,6 +835,7 @@ test('composeMpvRuntimeHandlers reuses completed background tokenization warmups
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},

View File

@@ -25,6 +25,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: false,
youtubePlaybackFlowPending: false,
};
const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({

View File

@@ -34,6 +34,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
currentSubtitleData?: SubtitleData | null;
playbackPaused: boolean | null;
previousSecondarySubVisibility: boolean | null;
youtubePlaybackFlowPending: boolean;
};
getQuitOnDisconnectArmed: () => boolean;
scheduleQuitCheck: (callback: () => void) => void;

View File

@@ -33,13 +33,17 @@ export function resolveWindowsMpvPath(deps: WindowsMpvLaunchDeps): string {
return '';
}
export function buildWindowsMpvLaunchArgs(targets: string[]): string[] {
return ['--player-operation-mode=pseudo-gui', '--profile=subminer', ...targets];
export function buildWindowsMpvLaunchArgs(
targets: string[],
extraArgs: string[] = [],
): string[] {
return ['--player-operation-mode=pseudo-gui', '--profile=subminer', ...extraArgs, ...targets];
}
export function launchWindowsMpv(
targets: string[],
deps: WindowsMpvLaunchDeps,
extraArgs: string[] = [],
): { ok: boolean; mpvPath: string } {
const mpvPath = resolveWindowsMpvPath(deps);
if (!mpvPath) {
@@ -51,7 +55,7 @@ export function launchWindowsMpv(
}
try {
deps.spawnDetached(mpvPath, buildWindowsMpvLaunchArgs(targets));
deps.spawnDetached(mpvPath, buildWindowsMpvLaunchArgs(targets, extraArgs));
return { ok: true, mpvPath };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);