refactor: remove legacy youtube launcher mode plumbing

This commit is contained in:
2026-03-23 19:18:04 -07:00
parent d9489c0033
commit e487998c40
20 changed files with 18 additions and 210 deletions

View File

@@ -10,7 +10,6 @@ test('launcher root help lists subcommands', () => {
assert.match(output, /Commands:/); assert.match(output, /Commands:/);
assert.match(output, /jellyfin\|jf/); assert.match(output, /jellyfin\|jf/);
assert.match(output, /yt\|youtube/);
assert.match(output, /doctor/); assert.match(output, /doctor/);
assert.match(output, /config/); assert.match(output, /config/);
assert.match(output, /mpv/); assert.match(output, /mpv/);

View File

@@ -111,7 +111,6 @@ export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig):
youtubeSubgenAudioFormat: process.env.SUBMINER_YT_SUBGEN_AUDIO_FORMAT || 'm4a', youtubeSubgenAudioFormat: process.env.SUBMINER_YT_SUBGEN_AUDIO_FORMAT || 'm4a',
youtubeSubgenKeepTemp: process.env.SUBMINER_YT_SUBGEN_KEEP_TEMP === '1', youtubeSubgenKeepTemp: process.env.SUBMINER_YT_SUBGEN_KEEP_TEMP === '1',
youtubeFixWithAi: launcherConfig.fixWithAi === true, youtubeFixWithAi: launcherConfig.fixWithAi === true,
youtubeMode: undefined,
jimakuApiKey: process.env.SUBMINER_JIMAKU_API_KEY || '', jimakuApiKey: process.env.SUBMINER_JIMAKU_API_KEY || '',
jimakuApiKeyCommand: process.env.SUBMINER_JIMAKU_API_KEY_COMMAND || '', jimakuApiKeyCommand: process.env.SUBMINER_JIMAKU_API_KEY_COMMAND || '',
jimakuApiBaseUrl: process.env.SUBMINER_JIMAKU_API_BASE_URL || DEFAULT_JIMAKU_API_BASE_URL, jimakuApiBaseUrl: process.env.SUBMINER_JIMAKU_API_BASE_URL || DEFAULT_JIMAKU_API_BASE_URL,
@@ -250,29 +249,6 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
parsed.jellyfinLogout = Boolean(modeFlags.logout); parsed.jellyfinLogout = Boolean(modeFlags.logout);
} }
if (invocations.ytInvocation) {
if (invocations.ytInvocation.mode) {
parsed.youtubeMode = invocations.ytInvocation.mode;
}
if (invocations.ytInvocation.logLevel)
parsed.logLevel = parseLogLevel(invocations.ytInvocation.logLevel);
if (invocations.ytInvocation.outDir)
parsed.youtubeSubgenOutDir = invocations.ytInvocation.outDir;
if (invocations.ytInvocation.keepTemp) parsed.youtubeSubgenKeepTemp = true;
if (invocations.ytInvocation.whisperBin)
parsed.whisperBin = invocations.ytInvocation.whisperBin;
if (invocations.ytInvocation.whisperModel)
parsed.whisperModel = invocations.ytInvocation.whisperModel;
if (invocations.ytInvocation.whisperVadModel)
parsed.whisperVadModel = invocations.ytInvocation.whisperVadModel;
if (invocations.ytInvocation.whisperThreads)
parsed.whisperThreads = invocations.ytInvocation.whisperThreads;
if (invocations.ytInvocation.ytSubgenAudioFormat) {
parsed.youtubeSubgenAudioFormat = invocations.ytInvocation.ytSubgenAudioFormat;
}
if (invocations.ytInvocation.target) ensureTarget(invocations.ytInvocation.target, parsed);
}
if (invocations.dictionaryLogLevel) { if (invocations.dictionaryLogLevel) {
parsed.logLevel = parseLogLevel(invocations.dictionaryLogLevel); parsed.logLevel = parseLogLevel(invocations.dictionaryLogLevel);
} }

View File

@@ -14,19 +14,6 @@ export interface JellyfinInvocation {
logLevel?: string; logLevel?: string;
} }
export interface YtInvocation {
target?: string;
mode?: 'download' | 'generate';
outDir?: string;
keepTemp?: boolean;
whisperBin?: string;
whisperModel?: string;
whisperVadModel?: string;
whisperThreads?: number;
ytSubgenAudioFormat?: string;
logLevel?: string;
}
export interface CommandActionInvocation { export interface CommandActionInvocation {
action: string; action: string;
logLevel?: string; logLevel?: string;
@@ -34,7 +21,6 @@ export interface CommandActionInvocation {
export interface CliInvocations { export interface CliInvocations {
jellyfinInvocation: JellyfinInvocation | null; jellyfinInvocation: JellyfinInvocation | null;
ytInvocation: YtInvocation | null;
configInvocation: CommandActionInvocation | null; configInvocation: CommandActionInvocation | null;
mpvInvocation: CommandActionInvocation | null; mpvInvocation: CommandActionInvocation | null;
appInvocation: { appArgs: string[] } | null; appInvocation: { appArgs: string[] } | null;
@@ -90,8 +76,6 @@ function getTopLevelCommand(argv: string[]): { name: string; index: number } | n
const commandNames = new Set([ const commandNames = new Set([
'jellyfin', 'jellyfin',
'jf', 'jf',
'yt',
'youtube',
'doctor', 'doctor',
'config', 'config',
'mpv', 'mpv',
@@ -143,7 +127,6 @@ export function parseCliPrograms(
invocations: CliInvocations; invocations: CliInvocations;
} { } {
let jellyfinInvocation: JellyfinInvocation | null = null; let jellyfinInvocation: JellyfinInvocation | null = null;
let ytInvocation: YtInvocation | null = null;
let configInvocation: CommandActionInvocation | null = null; let configInvocation: CommandActionInvocation | null = null;
let mpvInvocation: CommandActionInvocation | null = null; let mpvInvocation: CommandActionInvocation | null = null;
let appInvocation: { appArgs: string[] } | null = null; let appInvocation: { appArgs: string[] } | null = null;
@@ -218,43 +201,6 @@ export function parseCliPrograms(
}; };
}); });
commandProgram
.command('yt')
.alias('youtube')
.description('YouTube workflows')
.argument('[target]', 'YouTube URL or ytsearch: query')
.option('--mode <mode>', 'YouTube subtitle acquisition mode')
.option('-o, --out-dir <dir>', 'Subtitle output dir')
.option('--keep-temp', 'Keep temp files')
.option('--whisper-bin <path>', 'whisper.cpp CLI path')
.option('--whisper-model <path>', 'whisper model path')
.option('--whisper-vad-model <path>', 'whisper.cpp VAD model path')
.option('--whisper-threads <n>', 'whisper.cpp thread count')
.option('--yt-subgen-audio-format <format>', 'Audio extraction format')
.option('--log-level <level>', 'Log level')
.action((target: string | undefined, options: Record<string, unknown>) => {
ytInvocation = {
target,
mode:
typeof options.mode === 'string' && (options.mode === 'download' || options.mode === 'generate')
? options.mode
: undefined,
outDir: typeof options.outDir === 'string' ? options.outDir : undefined,
keepTemp: options.keepTemp === true,
whisperBin: typeof options.whisperBin === 'string' ? options.whisperBin : undefined,
whisperModel: typeof options.whisperModel === 'string' ? options.whisperModel : undefined,
whisperVadModel:
typeof options.whisperVadModel === 'string' ? options.whisperVadModel : undefined,
whisperThreads:
typeof options.whisperThreads === 'number' && Number.isFinite(options.whisperThreads)
? Math.floor(options.whisperThreads)
: undefined,
ytSubgenAudioFormat:
typeof options.ytSubgenAudioFormat === 'string' ? options.ytSubgenAudioFormat : undefined,
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
};
});
commandProgram commandProgram
.command('dictionary') .command('dictionary')
.alias('dict') .alias('dict')
@@ -388,7 +334,6 @@ export function parseCliPrograms(
rootTarget: rootProgram.processedArgs[0], rootTarget: rootProgram.processedArgs[0],
invocations: { invocations: {
jellyfinInvocation, jellyfinInvocation,
ytInvocation,
configInvocation, configInvocation,
mpvInvocation, mpvInvocation,
appInvocation, appInvocation,

View File

@@ -362,7 +362,7 @@ ${bunBinary} -e "const net=require('node:net'); const fs=require('node:fs'); con
}); });
}); });
test('launcher disables plugin startup pause gate for app-owned youtube flow', { timeout: 15000 }, () => { test('launcher routes youtube urls through regular playback startup', { timeout: 15000 }, () => {
withTempDir((root) => { withTempDir((root) => {
const homeDir = path.join(root, 'home'); const homeDir = path.join(root, 'home');
const xdgConfigHome = path.join(root, 'xdg'); const xdgConfigHome = path.join(root, 'xdg');
@@ -430,13 +430,16 @@ ${bunBinary} -e "const net=require('node:net'); const fs=require('node:fs'); con
SUBMINER_TEST_MPV_ARGS: mpvArgsPath, SUBMINER_TEST_MPV_ARGS: mpvArgsPath,
SUBMINER_TEST_CAPTURE: path.join(root, 'captured-args.txt'), SUBMINER_TEST_CAPTURE: path.join(root, 'captured-args.txt'),
}; };
const result = runLauncher(['yt', 'https://www.youtube.com/watch?v=abc123'], env); const result = runLauncher(['https://www.youtube.com/watch?v=abc123'], env);
assert.equal(result.status, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`); assert.equal(result.status, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
assert.match( const forwardedArgs = fs
fs.readFileSync(mpvArgsPath, 'utf8'), .readFileSync(mpvArgsPath, 'utf8')
/--script-opts=.*subminer-auto_start_pause_until_ready=no/, .trim()
); .split('\n')
.map((item) => item.trim())
.filter(Boolean);
assert.equal(forwardedArgs.includes('https://www.youtube.com/watch?v=abc123'), true);
}); });
}); });

View File

@@ -85,13 +85,6 @@ test('parseArgs maps mpv idle action', () => {
assert.equal(parsed.mpvStatus, false); assert.equal(parsed.mpvStatus, false);
}); });
test('parseArgs captures youtube mode forwarding', () => {
const parsed = parseArgs(['youtube', 'https://example.com', '--mode', 'generate'], 'subminer', {});
assert.equal(parsed.target, 'https://example.com');
assert.equal(parsed.youtubeMode, 'generate');
});
test('parseArgs maps dictionary command and log-level override', () => { test('parseArgs maps dictionary command and log-level override', () => {
const parsed = parseArgs(['dictionary', '.', '--log-level', 'debug'], 'subminer', {}); const parsed = parseArgs(['dictionary', '.', '--log-level', 'debug'], 'subminer', {});

View File

@@ -13,8 +13,6 @@ ${B}Session${R}
--background Start in tray/background mode --background Start in tray/background mode
--start Connect to mpv and launch overlay --start Connect to mpv and launch overlay
--launch-mpv ${D}[targets...]${R} Launch mpv with the SubMiner mpv profile and exit --launch-mpv ${D}[targets...]${R} Launch mpv with the SubMiner mpv profile and exit
--youtube-play ${D}URL${R} Start app-owned YouTube subtitle auto-load flow for a URL
--youtube-mode ${D}download|generate${R} Subtitle acquisition mode for YouTube flow
--stop Stop the running instance --stop Stop the running instance
--stats Open the stats dashboard in your browser --stats Open the stats dashboard in your browser
--texthooker Start texthooker server only ${D}(no overlay)${R} --texthooker Start texthooker server only ${D}(no overlay)${R}

View File

@@ -54,7 +54,7 @@ test('cli command runtime handler prepares overlay prerequisites before overlay
}, },
}); });
handler({ youtubePlay: 'https://www.youtube.com/watch?v=test' } as never); handler({ settings: true } as never);
assert.deepEqual(calls, ['prereqs', 'context', 'cli:initial:ctx']); assert.deepEqual(calls, ['prereqs', 'context', 'cli:initial:ctx']);
}); });

View File

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

View File

@@ -115,7 +115,7 @@ test('initial args handler forwards args to cli handler', () => {
test('initial args handler bootstraps overlay before initial overlay-runtime commands', () => { test('initial args handler bootstraps overlay before initial overlay-runtime commands', () => {
const calls: string[] = []; const calls: string[] = [];
const args = { youtubePlay: 'https://youtube.com/watch?v=abc' } as never; const args = { settings: true } as never;
const handleInitialArgs = createHandleInitialArgsHandler({ const handleInitialArgs = createHandleInitialArgsHandler({
getInitialArgs: () => args, getInitialArgs: () => args,
isBackgroundMode: () => false, isBackgroundMode: () => false,

View File

@@ -1,7 +1,6 @@
import type { SubtitleData } from '../../types'; import type { SubtitleData } from '../../types';
export function createHandleMpvSubtitleChangeHandler(deps: { export function createHandleMpvSubtitleChangeHandler(deps: {
shouldSuppressSubtitleEvents?: () => boolean;
setCurrentSubText: (text: string) => void; setCurrentSubText: (text: string) => void;
getImmediateSubtitlePayload?: (text: string) => SubtitleData | null; getImmediateSubtitlePayload?: (text: string) => SubtitleData | null;
emitImmediateSubtitle?: (payload: SubtitleData) => void; emitImmediateSubtitle?: (payload: SubtitleData) => void;
@@ -11,10 +10,6 @@ export function createHandleMpvSubtitleChangeHandler(deps: {
}) { }) {
return ({ text }: { text: string }): void => { return ({ text }: { text: string }): void => {
deps.setCurrentSubText(text); deps.setCurrentSubText(text);
if (deps.shouldSuppressSubtitleEvents?.()) {
deps.refreshDiscordPresence();
return;
}
const immediatePayload = deps.getImmediateSubtitlePayload?.(text) ?? null; const immediatePayload = deps.getImmediateSubtitlePayload?.(text) ?? null;
if (immediatePayload) { if (immediatePayload) {
(deps.emitImmediateSubtitle ?? deps.broadcastSubtitle)(immediatePayload); (deps.emitImmediateSubtitle ?? deps.broadcastSubtitle)(immediatePayload);
@@ -30,27 +25,19 @@ export function createHandleMpvSubtitleChangeHandler(deps: {
} }
export function createHandleMpvSubtitleAssChangeHandler(deps: { export function createHandleMpvSubtitleAssChangeHandler(deps: {
shouldSuppressSubtitleEvents?: () => boolean;
setCurrentSubAssText: (text: string) => void; setCurrentSubAssText: (text: string) => void;
broadcastSubtitleAss: (text: string) => void; broadcastSubtitleAss: (text: string) => void;
}) { }) {
return ({ text }: { text: string }): void => { return ({ text }: { text: string }): void => {
deps.setCurrentSubAssText(text); deps.setCurrentSubAssText(text);
if (deps.shouldSuppressSubtitleEvents?.()) {
return;
}
deps.broadcastSubtitleAss(text); deps.broadcastSubtitleAss(text);
}; };
} }
export function createHandleMpvSecondarySubtitleChangeHandler(deps: { export function createHandleMpvSecondarySubtitleChangeHandler(deps: {
shouldSuppressSubtitleEvents?: () => boolean;
broadcastSecondarySubtitle: (text: string) => void; broadcastSecondarySubtitle: (text: string) => void;
}) { }) {
return ({ text }: { text: string }): void => { return ({ text }: { text: string }): void => {
if (deps.shouldSuppressSubtitleEvents?.()) {
return;
}
deps.broadcastSecondarySubtitle(text); deps.broadcastSecondarySubtitle(text);
}; };
} }

View File

@@ -26,8 +26,6 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
calls.push('post-watch'); calls.push('post-watch');
}, },
logSubtitleTimingError: () => calls.push('subtitle-error'), logSubtitleTimingError: () => calls.push('subtitle-error'),
shouldSuppressSubtitleEvents: () => false,
setCurrentSubText: (text) => calls.push(`set-sub:${text}`), setCurrentSubText: (text) => calls.push(`set-sub:${text}`),
broadcastSubtitle: (payload) => calls.push(`broadcast-sub:${payload.text}`), broadcastSubtitle: (payload) => calls.push(`broadcast-sub:${payload.text}`),
onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`), onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`),
@@ -94,67 +92,3 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
assert.ok(calls.includes('sync-immersion')); assert.ok(calls.includes('sync-immersion'));
assert.ok(calls.includes('flush-playback')); assert.ok(calls.includes('flush-playback'));
}); });
test('main mpv event binder suppresses subtitle broadcasts while youtube flow is pending', () => {
const handlers = new Map<string, (payload: unknown) => void>();
const calls: string[] = [];
const bind = createBindMpvMainEventHandlersHandler({
reportJellyfinRemoteStopped: () => {},
syncOverlayMpvSubtitleSuppression: () => {},
resetSubtitleSidebarEmbeddedLayout: () => {},
hasInitialJellyfinPlayArg: () => false,
isOverlayRuntimeInitialized: () => false,
isQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
isMpvConnected: () => false,
quitApp: () => {},
recordImmersionSubtitleLine: () => {},
hasSubtitleTimingTracker: () => false,
recordSubtitleTiming: () => {},
maybeRunAnilistPostWatchUpdate: async () => {},
logSubtitleTimingError: () => {},
shouldSuppressSubtitleEvents: () => true,
setCurrentSubText: (text) => calls.push(`set-sub:${text}`),
broadcastSubtitle: (payload) => calls.push(`broadcast-sub:${payload.text}`),
onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`),
refreshDiscordPresence: () => calls.push('presence-refresh'),
setCurrentSubAssText: (text) => calls.push(`set-ass:${text}`),
broadcastSubtitleAss: (text) => calls.push(`broadcast-ass:${text}`),
broadcastSecondarySubtitle: (text) => calls.push(`broadcast-secondary:${text}`),
updateCurrentMediaPath: () => {},
restoreMpvSubVisibility: () => {},
getCurrentAnilistMediaKey: () => null,
resetAnilistMediaTracking: () => {},
maybeProbeAnilistDuration: () => {},
ensureAnilistMediaGuess: () => {},
syncImmersionMediaState: () => {},
updateCurrentMediaTitle: () => {},
resetAnilistMediaGuessState: () => {},
notifyImmersionTitleUpdate: () => {},
recordPlaybackPosition: () => {},
recordMediaDuration: () => {},
reportJellyfinRemoteProgress: () => {},
recordPauseState: () => {},
updateSubtitleRenderMetrics: () => {},
setPreviousSecondarySubVisibility: () => {},
});
bind({
on: (event, handler) => {
handlers.set(event, handler as (payload: unknown) => void);
},
});
handlers.get('subtitle-change')?.({ text: 'line' });
handlers.get('subtitle-ass-change')?.({ text: 'ass' });
handlers.get('secondary-subtitle-change')?.({ text: 'sec' });
assert.ok(calls.includes('set-sub:line'));
assert.ok(calls.includes('set-ass:ass'));
assert.ok(calls.includes('presence-refresh'));
assert.ok(!calls.includes('broadcast-sub:line'));
assert.ok(!calls.includes('subtitle-change:line'));
assert.ok(!calls.includes('broadcast-ass:ass'));
assert.ok(!calls.includes('broadcast-secondary:sec'));
});

View File

@@ -35,7 +35,6 @@ export function createBindMpvMainEventHandlersHandler(deps: {
recordSubtitleTiming: (text: string, start: number, end: number) => void; recordSubtitleTiming: (text: string, start: number, end: number) => void;
maybeRunAnilistPostWatchUpdate: () => Promise<void>; maybeRunAnilistPostWatchUpdate: () => Promise<void>;
logSubtitleTimingError: (message: string, error: unknown) => void; logSubtitleTimingError: (message: string, error: unknown) => void;
shouldSuppressSubtitleEvents?: () => boolean;
setCurrentSubText: (text: string) => void; setCurrentSubText: (text: string) => void;
getImmediateSubtitlePayload?: (text: string) => SubtitleData | null; getImmediateSubtitlePayload?: (text: string) => SubtitleData | null;
@@ -100,7 +99,6 @@ export function createBindMpvMainEventHandlersHandler(deps: {
logError: (message, error) => deps.logSubtitleTimingError(message, error), logError: (message, error) => deps.logSubtitleTimingError(message, error),
}); });
const handleMpvSubtitleChange = createHandleMpvSubtitleChangeHandler({ const handleMpvSubtitleChange = createHandleMpvSubtitleChangeHandler({
shouldSuppressSubtitleEvents: () => deps.shouldSuppressSubtitleEvents?.() ?? false,
setCurrentSubText: (text) => deps.setCurrentSubText(text), setCurrentSubText: (text) => deps.setCurrentSubText(text),
getImmediateSubtitlePayload: (text) => deps.getImmediateSubtitlePayload?.(text) ?? null, getImmediateSubtitlePayload: (text) => deps.getImmediateSubtitlePayload?.(text) ?? null,
emitImmediateSubtitle: (payload) => deps.emitImmediateSubtitle?.(payload), emitImmediateSubtitle: (payload) => deps.emitImmediateSubtitle?.(payload),
@@ -109,12 +107,10 @@ export function createBindMpvMainEventHandlersHandler(deps: {
refreshDiscordPresence: () => deps.refreshDiscordPresence(), refreshDiscordPresence: () => deps.refreshDiscordPresence(),
}); });
const handleMpvSubtitleAssChange = createHandleMpvSubtitleAssChangeHandler({ const handleMpvSubtitleAssChange = createHandleMpvSubtitleAssChangeHandler({
shouldSuppressSubtitleEvents: () => deps.shouldSuppressSubtitleEvents?.() ?? false,
setCurrentSubAssText: (text) => deps.setCurrentSubAssText(text), setCurrentSubAssText: (text) => deps.setCurrentSubAssText(text),
broadcastSubtitleAss: (text) => deps.broadcastSubtitleAss(text), broadcastSubtitleAss: (text) => deps.broadcastSubtitleAss(text),
}); });
const handleMpvSecondarySubtitleChange = createHandleMpvSecondarySubtitleChangeHandler({ const handleMpvSecondarySubtitleChange = createHandleMpvSecondarySubtitleChangeHandler({
shouldSuppressSubtitleEvents: () => deps.shouldSuppressSubtitleEvents?.() ?? false,
broadcastSecondarySubtitle: (text) => deps.broadcastSecondarySubtitle(text), broadcastSecondarySubtitle: (text) => deps.broadcastSecondarySubtitle(text),
}); });
const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({ const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({

View File

@@ -25,7 +25,6 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
currentSubAssText: '', currentSubAssText: '',
playbackPaused: null, playbackPaused: null,
previousSecondarySubVisibility: false, previousSecondarySubVisibility: false,
youtubePlaybackFlowPending: false,
}; };
const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({ const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({
@@ -75,7 +74,6 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
deps.recordSubtitleTiming('y', 0, 1); deps.recordSubtitleTiming('y', 0, 1);
await deps.maybeRunAnilistPostWatchUpdate(); await deps.maybeRunAnilistPostWatchUpdate();
deps.logSubtitleTimingError('err', new Error('boom')); deps.logSubtitleTimingError('err', new Error('boom'));
assert.equal(deps.shouldSuppressSubtitleEvents?.(), false);
deps.setCurrentSubText('sub'); deps.setCurrentSubText('sub');
deps.broadcastSubtitle({ text: 'sub', tokens: null }); deps.broadcastSubtitle({ text: 'sub', tokens: null });
deps.onSubtitleChange('sub'); deps.onSubtitleChange('sub');
@@ -119,7 +117,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
assert.ok(calls.includes('reset-sidebar-layout')); assert.ok(calls.includes('reset-sidebar-layout'));
}); });
test('mpv main event main deps suppress subtitle events while youtube flow is pending', () => { test('mpv main event main deps wire subtitle callbacks without suppression gate', () => {
const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({ const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({
appState: { appState: {
initialArgs: null, initialArgs: null,
@@ -131,7 +129,6 @@ test('mpv main event main deps suppress subtitle events while youtube flow is pe
currentSubAssText: '', currentSubAssText: '',
playbackPaused: null, playbackPaused: null,
previousSecondarySubVisibility: false, previousSecondarySubVisibility: false,
youtubePlaybackFlowPending: true,
}, },
getQuitOnDisconnectArmed: () => false, getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {}, scheduleQuitCheck: () => {},
@@ -158,5 +155,6 @@ test('mpv main event main deps suppress subtitle events while youtube flow is pe
refreshDiscordPresence: () => {}, refreshDiscordPresence: () => {},
})(); })();
assert.equal(deps.shouldSuppressSubtitleEvents?.(), true); deps.setCurrentSubText('sub');
assert.equal(typeof deps.setCurrentSubText, 'function');
}); });

View File

@@ -34,7 +34,6 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
currentSubtitleData?: SubtitleData | null; currentSubtitleData?: SubtitleData | null;
playbackPaused: boolean | null; playbackPaused: boolean | null;
previousSecondarySubVisibility: boolean | null; previousSecondarySubVisibility: boolean | null;
youtubePlaybackFlowPending: boolean;
}; };
getQuitOnDisconnectArmed: () => boolean; getQuitOnDisconnectArmed: () => boolean;
scheduleQuitCheck: (callback: () => void) => void; scheduleQuitCheck: (callback: () => void) => void;
@@ -120,7 +119,6 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
maybeRunAnilistPostWatchUpdate: () => deps.maybeRunAnilistPostWatchUpdate(), maybeRunAnilistPostWatchUpdate: () => deps.maybeRunAnilistPostWatchUpdate(),
logSubtitleTimingError: (message: string, error: unknown) => logSubtitleTimingError: (message: string, error: unknown) =>
deps.logSubtitleTimingError(message, error), deps.logSubtitleTimingError(message, error),
shouldSuppressSubtitleEvents: () => deps.appState.youtubePlaybackFlowPending,
setCurrentSubText: (text: string) => { setCurrentSubText: (text: string) => {
deps.appState.currentSubText = text; deps.appState.currentSubText = text;
}, },

View File

@@ -6,7 +6,6 @@ import type { YoutubePickerOpenPayload } from '../../types';
const payload: YoutubePickerOpenPayload = { const payload: YoutubePickerOpenPayload = {
sessionId: 'yt-1', sessionId: 'yt-1',
url: 'https://example.com/watch?v=abc', url: 'https://example.com/watch?v=abc',
mode: 'download',
tracks: [], tracks: [],
defaultPrimaryTrackId: null, defaultPrimaryTrackId: null,
defaultSecondaryTrackId: null, defaultSecondaryTrackId: null,

View File

@@ -95,14 +95,14 @@ test('transitionAnilistUpdateInFlightState updates inFlight only', () => {
assert.notEqual(transitioned, current); assert.notEqual(transitioned, current);
}); });
test('applyStartupState does not mark youtube playback flow pending from startup args alone', () => { test('applyStartupState preserves cleared startup-only runtime flags', () => {
const appState = createAppState({ const appState = createAppState({
mpvSocketPath: '/tmp/mpv.sock', mpvSocketPath: '/tmp/mpv.sock',
texthookerPort: 4000, texthookerPort: 4000,
}); });
applyStartupState(appState, { applyStartupState(appState, {
initialArgs: parseArgs(['--youtube-play', 'https://www.youtube.com/watch?v=video123']), initialArgs: parseArgs(['--settings']),
mpvSocketPath: '/tmp/mpv.sock', mpvSocketPath: '/tmp/mpv.sock',
texthookerPort: 4000, texthookerPort: 4000,
backendOverride: null, backendOverride: null,
@@ -111,5 +111,5 @@ test('applyStartupState does not mark youtube playback flow pending from startup
backgroundMode: false, backgroundMode: false,
}); });
assert.equal(appState.youtubePlaybackFlowPending, false); assert.equal(appState.initialArgs?.settings, true);
}); });

View File

@@ -188,7 +188,6 @@ export interface AppState {
overlayDebugVisualizationEnabled: boolean; overlayDebugVisualizationEnabled: boolean;
statsOverlayVisible: boolean; statsOverlayVisible: boolean;
subsyncInProgress: boolean; subsyncInProgress: boolean;
youtubePlaybackFlowPending: boolean;
initialArgs: CliArgs | null; initialArgs: CliArgs | null;
mpvSocketPath: string; mpvSocketPath: string;
texthookerPort: number; texthookerPort: number;
@@ -273,7 +272,6 @@ export function createAppState(values: AppStateInitialValues): AppState {
fieldGroupingResolver: null, fieldGroupingResolver: null,
fieldGroupingResolverSequence: 0, fieldGroupingResolverSequence: 0,
subsyncInProgress: false, subsyncInProgress: false,
youtubePlaybackFlowPending: false,
initialArgs: null, initialArgs: null,
mpvSocketPath: values.mpvSocketPath, mpvSocketPath: values.mpvSocketPath,
texthookerPort: values.texthookerPort, texthookerPort: values.texthookerPort,
@@ -293,7 +291,6 @@ export function createAppState(values: AppStateInitialValues): AppState {
export function applyStartupState(appState: AppState, startupState: StartupState): void { export function applyStartupState(appState: AppState, startupState: StartupState): void {
appState.initialArgs = startupState.initialArgs; appState.initialArgs = startupState.initialArgs;
appState.youtubePlaybackFlowPending = false;
appState.mpvSocketPath = startupState.mpvSocketPath; appState.mpvSocketPath = startupState.mpvSocketPath;
appState.texthookerPort = startupState.texthookerPort; appState.texthookerPort = startupState.texthookerPort;
appState.backendOverride = startupState.backendOverride; appState.backendOverride = startupState.backendOverride;

View File

@@ -151,7 +151,6 @@ test('youtube track picker close restores focus and mouse-ignore state', () => {
modal.openYoutubePickerModal({ modal.openYoutubePickerModal({
sessionId: 'yt-1', sessionId: 'yt-1',
url: 'https://example.com', url: 'https://example.com',
mode: 'download',
tracks: [], tracks: [],
defaultPrimaryTrackId: null, defaultPrimaryTrackId: null,
defaultSecondaryTrackId: null, defaultSecondaryTrackId: null,
@@ -241,7 +240,6 @@ test('youtube track picker re-acknowledges repeated open requests', () => {
modal.openYoutubePickerModal({ modal.openYoutubePickerModal({
sessionId: 'yt-1', sessionId: 'yt-1',
url: 'https://example.com/one', url: 'https://example.com/one',
mode: 'download',
tracks: [], tracks: [],
defaultPrimaryTrackId: null, defaultPrimaryTrackId: null,
defaultSecondaryTrackId: null, defaultSecondaryTrackId: null,
@@ -250,7 +248,6 @@ test('youtube track picker re-acknowledges repeated open requests', () => {
modal.openYoutubePickerModal({ modal.openYoutubePickerModal({
sessionId: 'yt-2', sessionId: 'yt-2',
url: 'https://example.com/two', url: 'https://example.com/two',
mode: 'generate',
tracks: [], tracks: [],
defaultPrimaryTrackId: null, defaultPrimaryTrackId: null,
defaultSecondaryTrackId: null, defaultSecondaryTrackId: null,
@@ -327,7 +324,6 @@ test('youtube track picker surfaces rejected resolve calls as modal status', asy
modal.openYoutubePickerModal({ modal.openYoutubePickerModal({
sessionId: 'yt-1', sessionId: 'yt-1',
url: 'https://example.com', url: 'https://example.com',
mode: 'download',
tracks: [ tracks: [
{ {
id: 'auto:ja-orig', id: 'auto:ja-orig',
@@ -432,7 +428,6 @@ test('youtube track picker ignores duplicate resolve submissions while request i
modal.openYoutubePickerModal({ modal.openYoutubePickerModal({
sessionId: 'yt-1', sessionId: 'yt-1',
url: 'https://example.com', url: 'https://example.com',
mode: 'download',
tracks: [ tracks: [
{ {
id: 'auto:ja-orig', id: 'auto:ja-orig',
@@ -534,7 +529,6 @@ test('youtube track picker only consumes handled keys', async () => {
modal.openYoutubePickerModal({ modal.openYoutubePickerModal({
sessionId: 'yt-1', sessionId: 'yt-1',
url: 'https://example.com', url: 'https://example.com',
mode: 'download',
tracks: [], tracks: [],
defaultPrimaryTrackId: null, defaultPrimaryTrackId: null,
defaultSecondaryTrackId: null, defaultSecondaryTrackId: null,
@@ -640,7 +634,6 @@ test('youtube track picker ignores immediate Enter after open before allowing ke
modal.openYoutubePickerModal({ modal.openYoutubePickerModal({
sessionId: 'yt-1', sessionId: 'yt-1',
url: 'https://example.com', url: 'https://example.com',
mode: 'download',
tracks: [ tracks: [
{ {
id: 'auto:ja-orig', id: 'auto:ja-orig',

View File

@@ -96,7 +96,7 @@ export function createYoutubeTrackPickerModal(
function applyPayload(payload: YoutubePickerOpenPayload): void { function applyPayload(payload: YoutubePickerOpenPayload): void {
ctx.state.youtubePickerPayload = payload; ctx.state.youtubePickerPayload = payload;
ctx.dom.youtubePickerTitle.textContent = `${payload.mode === 'generate' ? 'Generate' : 'Download'} subtitles for ${payload.url}`; ctx.dom.youtubePickerTitle.textContent = `Select YouTube subtitles for ${payload.url}`;
ctx.dom.youtubePickerPrimarySelect.innerHTML = ''; ctx.dom.youtubePickerPrimarySelect.innerHTML = '';
ctx.dom.youtubePickerSecondarySelect.innerHTML = ''; ctx.dom.youtubePickerSecondarySelect.innerHTML = '';

View File

@@ -561,7 +561,6 @@ export interface ControllerRuntimeSnapshot {
} }
export type JimakuLanguagePreference = 'ja' | 'en' | 'none'; export type JimakuLanguagePreference = 'ja' | 'en' | 'none';
export type YoutubeFlowMode = 'download' | 'generate';
export type { YoutubeTrackKind }; export type { YoutubeTrackKind };
export interface YoutubeTrackOption { export interface YoutubeTrackOption {
@@ -578,7 +577,6 @@ export interface YoutubeTrackOption {
export interface YoutubePickerOpenPayload { export interface YoutubePickerOpenPayload {
sessionId: string; sessionId: string;
url: string; url: string;
mode: YoutubeFlowMode;
tracks: YoutubeTrackOption[]; tracks: YoutubeTrackOption[];
defaultPrimaryTrackId: string | null; defaultPrimaryTrackId: string | null;
defaultSecondaryTrackId: string | null; defaultSecondaryTrackId: string | null;