mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-29 17:40:50 -07:00
feat(youtube): add YouTube browser window and Whisper subtitle source (#273)
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
launchTexthookerOnly,
|
||||
runAppCommandWithInherit,
|
||||
} from '../mpv.js';
|
||||
import { shouldForwardLogLevel } from '../types.js';
|
||||
import type { LauncherCommandContext } from './context.js';
|
||||
|
||||
type AppCommandDeps = {
|
||||
@@ -12,6 +13,10 @@ type AppCommandDeps = {
|
||||
appPath: string,
|
||||
logLevel: LauncherCommandContext['args']['logLevel'],
|
||||
) => void;
|
||||
launchYoutubeBrowserDetached: (
|
||||
appPath: string,
|
||||
logLevel: LauncherCommandContext['args']['logLevel'],
|
||||
) => void;
|
||||
launchAppBackgroundDetached: (
|
||||
appPath: string,
|
||||
logLevel: LauncherCommandContext['args']['logLevel'],
|
||||
@@ -22,6 +27,11 @@ const defaultAppCommandDeps: AppCommandDeps = {
|
||||
runAppCommandWithInherit,
|
||||
launchSyncUiDetached: (appPath, logLevel) =>
|
||||
launchAppCommandDetached(appPath, ['--sync-window'], logLevel, 'sync-ui'),
|
||||
launchYoutubeBrowserDetached: (appPath, logLevel) => {
|
||||
const appArgs = ['--youtube-browser'];
|
||||
if (shouldForwardLogLevel(logLevel)) appArgs.push('--log-level', logLevel);
|
||||
launchAppCommandDetached(appPath, appArgs, logLevel, 'youtube-browser');
|
||||
},
|
||||
launchAppBackgroundDetached,
|
||||
};
|
||||
|
||||
@@ -41,6 +51,10 @@ export function runAppPassthroughCommand(
|
||||
deps.launchSyncUiDetached(appPath, args.logLevel);
|
||||
return true;
|
||||
}
|
||||
if (args.youtubeBrowser) {
|
||||
deps.launchYoutubeBrowserDetached(appPath, args.logLevel);
|
||||
return true;
|
||||
}
|
||||
if (!args.appPassthrough) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -207,6 +207,7 @@ test('app command starts default macOS background app detached from launcher', (
|
||||
calls.push('attached');
|
||||
},
|
||||
launchSyncUiDetached: () => calls.push('sync-ui'),
|
||||
launchYoutubeBrowserDetached: () => calls.push('youtube'),
|
||||
launchAppBackgroundDetached: (appPath, logLevel) => {
|
||||
calls.push(`detached:${appPath}:${logLevel}`);
|
||||
},
|
||||
@@ -227,6 +228,7 @@ test('app command starts default Linux background app detached from launcher', (
|
||||
calls.push('attached');
|
||||
},
|
||||
launchSyncUiDetached: () => calls.push('sync-ui'),
|
||||
launchYoutubeBrowserDetached: () => calls.push('youtube'),
|
||||
launchAppBackgroundDetached: (appPath, logLevel) => {
|
||||
calls.push(`detached:${appPath}:${logLevel}`);
|
||||
},
|
||||
@@ -248,6 +250,7 @@ test('app command keeps explicit passthrough args attached', () => {
|
||||
forwarded.push(appArgs);
|
||||
},
|
||||
launchSyncUiDetached: () => detached.push('sync-ui'),
|
||||
launchYoutubeBrowserDetached: () => detached.push('youtube'),
|
||||
launchAppBackgroundDetached: () => {
|
||||
detached.push('detached');
|
||||
},
|
||||
@@ -266,6 +269,7 @@ test('sync UI command launches the app detached from the terminal', () => {
|
||||
const handled = runAppPassthroughCommand(context, {
|
||||
runAppCommandWithInherit: () => calls.push('piped'),
|
||||
launchSyncUiDetached: (appPath, logLevel) => calls.push(`sync-ui:${appPath}:${logLevel}`),
|
||||
launchYoutubeBrowserDetached: () => calls.push('youtube'),
|
||||
launchAppBackgroundDetached: () => calls.push('detached'),
|
||||
});
|
||||
|
||||
@@ -273,6 +277,23 @@ test('sync UI command launches the app detached from the terminal', () => {
|
||||
assert.deepEqual(calls, ['sync-ui:/tmp/subminer.app:warn']);
|
||||
});
|
||||
|
||||
test('youtube command launches the YouTube browser detached from the terminal', () => {
|
||||
const context = createContext();
|
||||
context.args.youtubeBrowser = true;
|
||||
const calls: string[] = [];
|
||||
|
||||
const handled = runAppPassthroughCommand(context, {
|
||||
runAppCommandWithInherit: () => calls.push('piped'),
|
||||
launchSyncUiDetached: () => calls.push('sync-ui'),
|
||||
launchYoutubeBrowserDetached: (appPath, logLevel) =>
|
||||
calls.push(`youtube:${appPath}:${logLevel}`),
|
||||
launchAppBackgroundDetached: () => calls.push('detached'),
|
||||
});
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(calls, ['youtube:/tmp/subminer.app:warn']);
|
||||
});
|
||||
|
||||
test('mpv pre-app command exits non-zero when socket is not ready', async () => {
|
||||
const context = createContext();
|
||||
context.args.mpvStatus = true;
|
||||
|
||||
@@ -16,20 +16,9 @@ function createContext(): LauncherCommandContext {
|
||||
recursive: false,
|
||||
profile: '',
|
||||
startOverlay: false,
|
||||
youtubeMode: 'download',
|
||||
whisperBin: '',
|
||||
whisperModel: '',
|
||||
whisperVadModel: '',
|
||||
whisperThreads: 0,
|
||||
youtubeSubgenOutDir: '',
|
||||
youtubeSubgenAudioFormat: '',
|
||||
youtubeSubgenKeepTemp: false,
|
||||
youtubeFixWithAi: false,
|
||||
youtubePrimarySubLangs: [],
|
||||
youtubeSecondarySubLangs: [],
|
||||
youtubeAudioLangs: [],
|
||||
youtubeWhisperSourceLanguage: '',
|
||||
aiConfig: {},
|
||||
useTexthooker: false,
|
||||
autoStartOverlay: false,
|
||||
texthookerOnly: false,
|
||||
@@ -63,6 +52,7 @@ function createContext(): LauncherCommandContext {
|
||||
logsExport: false,
|
||||
version: false,
|
||||
settings: false,
|
||||
youtubeBrowser: false,
|
||||
configPath: false,
|
||||
configShow: false,
|
||||
mpvIdle: false,
|
||||
@@ -162,7 +152,7 @@ test('youtube playback launches overlay with app-owned youtube flow args', async
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'startMpv',
|
||||
'startOverlay:--youtube-play https://www.youtube.com/watch?v=65Ovd7t8sNw --youtube-mode download',
|
||||
'startOverlay:--youtube-play https://www.youtube.com/watch?v=65Ovd7t8sNw',
|
||||
]);
|
||||
assert.equal(receivedStartMpvOptions[0]?.startPaused, true);
|
||||
assert.equal(receivedStartMpvOptions[0]?.disableYoutubeSubtitleAutoLoad, true);
|
||||
|
||||
@@ -240,7 +240,6 @@ export async function runPlaybackCommandWithDeps(
|
||||
|
||||
const isYoutubeUrl = selectedTarget.kind === 'url' && isYoutubeTarget(selectedTarget.target);
|
||||
const isAppOwnedYoutubeFlow = isYoutubeUrl;
|
||||
const youtubeMode = args.youtubeMode ?? 'download';
|
||||
const configDir = getLauncherConfigDir();
|
||||
|
||||
if (isYoutubeUrl) {
|
||||
@@ -325,7 +324,7 @@ export async function runPlaybackCommandWithDeps(
|
||||
);
|
||||
}
|
||||
const extraAppArgs = isAppOwnedYoutubeFlow
|
||||
? ['--youtube-play', selectedTarget.target, '--youtube-mode', youtubeMode]
|
||||
? ['--youtube-play', selectedTarget.target]
|
||||
: shouldLauncherAttachRunningApp
|
||||
? [
|
||||
pluginRuntimeConfig.autoStartVisibleOverlay
|
||||
|
||||
@@ -12,25 +12,6 @@ import { getDefaultSocketPath } from './types.js';
|
||||
|
||||
test('parseLauncherYoutubeSubgenConfig keeps only valid typed values', () => {
|
||||
const parsed = parseLauncherYoutubeSubgenConfig({
|
||||
ai: {
|
||||
enabled: true,
|
||||
apiKey: 'shared-key',
|
||||
baseUrl: 'https://openrouter.ai/api',
|
||||
model: 'openrouter/shared-model',
|
||||
systemPrompt: 'Legacy shared prompt.',
|
||||
requestTimeoutMs: 12000,
|
||||
},
|
||||
youtubeSubgen: {
|
||||
whisperBin: '/usr/bin/whisper',
|
||||
whisperModel: '/models/base.bin',
|
||||
whisperVadModel: '/models/vad.bin',
|
||||
whisperThreads: 6.8,
|
||||
fixWithAi: true,
|
||||
ai: {
|
||||
model: 'openrouter/subgen-model',
|
||||
systemPrompt: 'Fix subtitles only.',
|
||||
},
|
||||
},
|
||||
youtube: {
|
||||
primarySubLanguages: ['ja', 42, 'en'],
|
||||
},
|
||||
@@ -49,14 +30,6 @@ test('parseLauncherYoutubeSubgenConfig keeps only valid typed values', () => {
|
||||
assert.equal('mode' in parsed, false);
|
||||
assert.deepEqual(parsed.primarySubLanguages, ['ja', 'en']);
|
||||
assert.deepEqual(parsed.secondarySubLanguages, ['eng', 'deu']);
|
||||
assert.equal(parsed.whisperVadModel, '/models/vad.bin');
|
||||
assert.equal(parsed.whisperThreads, 6);
|
||||
assert.equal(parsed.fixWithAi, true);
|
||||
assert.equal(parsed.ai?.enabled, true);
|
||||
assert.equal(parsed.ai?.apiKey, 'shared-key');
|
||||
assert.equal(parsed.ai?.model, 'openrouter/subgen-model');
|
||||
assert.equal(parsed.ai?.systemPrompt, 'Fix subtitles only.');
|
||||
assert.equal(parsed.ai?.requestTimeoutMs, 12000);
|
||||
assert.equal(parsed.jimakuLanguagePreference, 'ja');
|
||||
assert.equal(parsed.jimakuMaxEntryResults, 8);
|
||||
});
|
||||
|
||||
@@ -19,30 +19,16 @@ function withTempDir<T>(fn: (dir: string) => T): T {
|
||||
}
|
||||
}
|
||||
|
||||
test('createDefaultArgs normalizes configured language codes and env thread override', () => {
|
||||
const originalThreads = process.env.SUBMINER_WHISPER_THREADS;
|
||||
process.env.SUBMINER_WHISPER_THREADS = '7';
|
||||
test('createDefaultArgs normalizes configured language codes', () => {
|
||||
const parsed = createDefaultArgs({
|
||||
primarySubLanguages: [' JA ', 'jpn', 'ja'],
|
||||
secondarySubLanguages: ['en', 'ENG', ''],
|
||||
});
|
||||
|
||||
try {
|
||||
const parsed = createDefaultArgs({
|
||||
primarySubLanguages: [' JA ', 'jpn', 'ja'],
|
||||
secondarySubLanguages: ['en', 'ENG', ''],
|
||||
whisperThreads: 2,
|
||||
});
|
||||
|
||||
assert.deepEqual(parsed.youtubePrimarySubLangs, ['ja', 'jpn']);
|
||||
assert.deepEqual(parsed.youtubeSecondarySubLangs, ['en', 'eng']);
|
||||
assert.deepEqual(parsed.youtubeAudioLangs, ['ja', 'jpn', 'en', 'eng']);
|
||||
assert.equal(parsed.whisperThreads, 7);
|
||||
assert.equal(parsed.youtubeWhisperSourceLanguage, 'ja');
|
||||
assert.equal(parsed.profile, '');
|
||||
} finally {
|
||||
if (originalThreads === undefined) {
|
||||
delete process.env.SUBMINER_WHISPER_THREADS;
|
||||
} else {
|
||||
process.env.SUBMINER_WHISPER_THREADS = originalThreads;
|
||||
}
|
||||
}
|
||||
assert.deepEqual(parsed.youtubePrimarySubLangs, ['ja', 'jpn']);
|
||||
assert.deepEqual(parsed.youtubeSecondarySubLangs, ['en', 'eng']);
|
||||
assert.deepEqual(parsed.youtubeAudioLangs, ['ja', 'jpn', 'en', 'eng']);
|
||||
assert.equal(parsed.profile, '');
|
||||
});
|
||||
|
||||
test('createDefaultArgs seeds mpv profile from launcher config', () => {
|
||||
@@ -120,6 +106,7 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => {
|
||||
logLevel: 'warn',
|
||||
},
|
||||
settingsInvocation: null,
|
||||
youtubeBrowserInvocation: null,
|
||||
mpvInvocation: null,
|
||||
appInvocation: null,
|
||||
dictionaryTriggered: false,
|
||||
@@ -174,6 +161,7 @@ test('applyInvocationsToArgs maps settings invocation to settings window', () =>
|
||||
settingsInvocation: {
|
||||
logLevel: undefined,
|
||||
},
|
||||
youtubeBrowserInvocation: null,
|
||||
mpvInvocation: null,
|
||||
appInvocation: null,
|
||||
dictionaryTriggered: false,
|
||||
@@ -221,6 +209,7 @@ test('applyInvocationsToArgs fails when config invocation has no action', () =>
|
||||
action: undefined,
|
||||
},
|
||||
settingsInvocation: null,
|
||||
youtubeBrowserInvocation: null,
|
||||
mpvInvocation: null,
|
||||
appInvocation: null,
|
||||
dictionaryTriggered: false,
|
||||
@@ -266,6 +255,7 @@ test('applyInvocationsToArgs maps texthooker browser-open request', () => {
|
||||
jellyfinInvocation: null,
|
||||
configInvocation: null,
|
||||
settingsInvocation: null,
|
||||
youtubeBrowserInvocation: null,
|
||||
mpvInvocation: null,
|
||||
appInvocation: null,
|
||||
dictionaryTriggered: false,
|
||||
|
||||
@@ -13,14 +13,8 @@ import {
|
||||
DEFAULT_JIMAKU_API_BASE_URL,
|
||||
DEFAULT_YOUTUBE_PRIMARY_SUB_LANGS,
|
||||
DEFAULT_YOUTUBE_SECONDARY_SUB_LANGS,
|
||||
DEFAULT_YOUTUBE_SUBGEN_OUT_DIR,
|
||||
} from '../types.js';
|
||||
import {
|
||||
inferWhisperLanguage,
|
||||
isUrlTarget,
|
||||
resolvePathMaybe,
|
||||
uniqueNormalizedLangCodes,
|
||||
} from '../util.js';
|
||||
import { isUrlTarget, resolvePathMaybe, uniqueNormalizedLangCodes } from '../util.js';
|
||||
import type { CliInvocations } from './cli-parser-builder.js';
|
||||
|
||||
function ensureTarget(target: string, parsed: Args): void {
|
||||
@@ -131,18 +125,6 @@ export function createDefaultArgs(
|
||||
recursive: false,
|
||||
profile: mpvConfig.profile ?? '',
|
||||
startOverlay: false,
|
||||
whisperBin: process.env.SUBMINER_WHISPER_BIN || launcherConfig.whisperBin || '',
|
||||
whisperModel: process.env.SUBMINER_WHISPER_MODEL || launcherConfig.whisperModel || '',
|
||||
whisperVadModel: process.env.SUBMINER_WHISPER_VAD_MODEL || launcherConfig.whisperVadModel || '',
|
||||
whisperThreads: (() => {
|
||||
const envValue = Number.parseInt(process.env.SUBMINER_WHISPER_THREADS || '', 10);
|
||||
if (Number.isInteger(envValue) && envValue > 0) return envValue;
|
||||
return launcherConfig.whisperThreads || 4;
|
||||
})(),
|
||||
youtubeSubgenOutDir: process.env.SUBMINER_YT_SUBGEN_OUT_DIR || DEFAULT_YOUTUBE_SUBGEN_OUT_DIR,
|
||||
youtubeSubgenAudioFormat: process.env.SUBMINER_YT_SUBGEN_AUDIO_FORMAT || 'm4a',
|
||||
youtubeSubgenKeepTemp: process.env.SUBMINER_YT_SUBGEN_KEEP_TEMP === '1',
|
||||
youtubeFixWithAi: launcherConfig.fixWithAi === true,
|
||||
jimakuApiKey: process.env.SUBMINER_JIMAKU_API_KEY || '',
|
||||
jimakuApiKeyCommand: process.env.SUBMINER_JIMAKU_API_KEY_COMMAND || '',
|
||||
jimakuApiBaseUrl: process.env.SUBMINER_JIMAKU_API_BASE_URL || DEFAULT_JIMAKU_API_BASE_URL,
|
||||
@@ -170,6 +152,7 @@ export function createDefaultArgs(
|
||||
version: false,
|
||||
update: false,
|
||||
settings: false,
|
||||
youtubeBrowser: false,
|
||||
configPath: false,
|
||||
configShow: false,
|
||||
mpvIdle: false,
|
||||
@@ -185,16 +168,6 @@ export function createDefaultArgs(
|
||||
youtubePrimarySubLangs: primarySubLangs,
|
||||
youtubeSecondarySubLangs: secondarySubLangs,
|
||||
youtubeAudioLangs,
|
||||
youtubeWhisperSourceLanguage: inferWhisperLanguage(primarySubLangs, 'ja'),
|
||||
aiConfig: {
|
||||
enabled: launcherConfig.ai?.enabled,
|
||||
apiKey: launcherConfig.ai?.apiKey,
|
||||
apiKeyCommand: launcherConfig.ai?.apiKeyCommand,
|
||||
baseUrl: launcherConfig.ai?.baseUrl,
|
||||
model: launcherConfig.ai?.model,
|
||||
systemPrompt: launcherConfig.ai?.systemPrompt,
|
||||
requestTimeoutMs: launcherConfig.ai?.requestTimeoutMs,
|
||||
},
|
||||
useTexthooker: true,
|
||||
autoStartOverlay: false,
|
||||
texthookerOnly: false,
|
||||
@@ -363,6 +336,13 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
|
||||
parsed.settings = true;
|
||||
}
|
||||
|
||||
if (invocations.youtubeBrowserInvocation) {
|
||||
if (invocations.youtubeBrowserInvocation.logLevel) {
|
||||
parsed.logLevel = parseLogLevel(invocations.youtubeBrowserInvocation.logLevel);
|
||||
}
|
||||
parsed.youtubeBrowser = true;
|
||||
}
|
||||
|
||||
if (invocations.mpvInvocation) {
|
||||
if (invocations.mpvInvocation.logLevel) {
|
||||
parsed.logLevel = parseLogLevel(invocations.mpvInvocation.logLevel);
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface CliInvocations {
|
||||
jellyfinInvocation: JellyfinInvocation | null;
|
||||
configInvocation: CommandActionInvocation | null;
|
||||
settingsInvocation: CommandActionInvocation | null;
|
||||
youtubeBrowserInvocation: CommandActionInvocation | null;
|
||||
mpvInvocation: CommandActionInvocation | null;
|
||||
appInvocation: { appArgs: string[] } | null;
|
||||
dictionaryTriggered: boolean;
|
||||
@@ -121,6 +122,8 @@ function getTopLevelCommand(argv: string[]): { name: string; index: number } | n
|
||||
'doctor',
|
||||
'config',
|
||||
'settings',
|
||||
'youtube',
|
||||
'yt',
|
||||
'mpv',
|
||||
'logs',
|
||||
'dictionary',
|
||||
@@ -175,6 +178,7 @@ export function parseCliPrograms(
|
||||
let jellyfinInvocation: JellyfinInvocation | null = null;
|
||||
let configInvocation: CommandActionInvocation | null = null;
|
||||
let settingsInvocation: CommandActionInvocation | null = null;
|
||||
let youtubeBrowserInvocation: CommandActionInvocation | null = null;
|
||||
let mpvInvocation: CommandActionInvocation | null = null;
|
||||
let appInvocation: { appArgs: string[] } | null = null;
|
||||
let dictionaryTriggered = false;
|
||||
@@ -512,6 +516,17 @@ export function parseCliPrograms(
|
||||
};
|
||||
});
|
||||
|
||||
commandProgram
|
||||
.command('youtube')
|
||||
.alias('yt')
|
||||
.description('Open the YouTube browser (videos play in mpv)')
|
||||
.option('--log-level <level>', 'Log level')
|
||||
.action((options: Record<string, unknown>) => {
|
||||
youtubeBrowserInvocation = {
|
||||
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
commandProgram
|
||||
.command('mpv')
|
||||
.description('MPV helpers')
|
||||
@@ -567,6 +582,7 @@ export function parseCliPrograms(
|
||||
jellyfinInvocation,
|
||||
configInvocation,
|
||||
settingsInvocation,
|
||||
youtubeBrowserInvocation,
|
||||
mpvInvocation,
|
||||
appInvocation,
|
||||
dictionaryTriggered,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LauncherYoutubeSubgenConfig } from '../types.js';
|
||||
import { mergeAiConfig } from '../../src/ai/config.js';
|
||||
|
||||
function asStringArray(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) return undefined;
|
||||
@@ -9,11 +8,6 @@ function asStringArray(value: unknown): string[] | undefined {
|
||||
export function parseLauncherYoutubeSubgenConfig(
|
||||
root: Record<string, unknown>,
|
||||
): LauncherYoutubeSubgenConfig {
|
||||
const youtubeSubgenRaw = root.youtubeSubgen;
|
||||
const youtubeSubgen =
|
||||
youtubeSubgenRaw && typeof youtubeSubgenRaw === 'object'
|
||||
? (youtubeSubgenRaw as Record<string, unknown>)
|
||||
: null;
|
||||
const youtubeRaw = root.youtube;
|
||||
const youtube =
|
||||
youtubeRaw && typeof youtubeRaw === 'object' ? (youtubeRaw as Record<string, unknown>) : null;
|
||||
@@ -25,58 +19,11 @@ export function parseLauncherYoutubeSubgenConfig(
|
||||
const jimakuRaw = root.jimaku;
|
||||
const jimaku =
|
||||
jimakuRaw && typeof jimakuRaw === 'object' ? (jimakuRaw as Record<string, unknown>) : null;
|
||||
const aiRaw = root.ai;
|
||||
const ai = aiRaw && typeof aiRaw === 'object' ? (aiRaw as Record<string, unknown>) : null;
|
||||
const youtubeAiRaw = youtubeSubgen?.ai;
|
||||
const youtubeAi =
|
||||
youtubeAiRaw && typeof youtubeAiRaw === 'object'
|
||||
? (youtubeAiRaw as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
const jimakuLanguagePreference = jimaku?.languagePreference;
|
||||
const jimakuMaxEntryResults = jimaku?.maxEntryResults;
|
||||
|
||||
return {
|
||||
whisperBin:
|
||||
typeof youtubeSubgen?.whisperBin === 'string' ? youtubeSubgen.whisperBin : undefined,
|
||||
whisperModel:
|
||||
typeof youtubeSubgen?.whisperModel === 'string' ? youtubeSubgen.whisperModel : undefined,
|
||||
whisperVadModel:
|
||||
typeof youtubeSubgen?.whisperVadModel === 'string'
|
||||
? youtubeSubgen.whisperVadModel
|
||||
: undefined,
|
||||
whisperThreads:
|
||||
typeof youtubeSubgen?.whisperThreads === 'number' &&
|
||||
Number.isFinite(youtubeSubgen.whisperThreads) &&
|
||||
youtubeSubgen.whisperThreads > 0
|
||||
? Math.floor(youtubeSubgen.whisperThreads)
|
||||
: undefined,
|
||||
fixWithAi: typeof youtubeSubgen?.fixWithAi === 'boolean' ? youtubeSubgen.fixWithAi : undefined,
|
||||
ai: mergeAiConfig(
|
||||
ai
|
||||
? {
|
||||
enabled: typeof ai.enabled === 'boolean' ? ai.enabled : undefined,
|
||||
apiKey: typeof ai.apiKey === 'string' ? ai.apiKey : undefined,
|
||||
apiKeyCommand: typeof ai.apiKeyCommand === 'string' ? ai.apiKeyCommand : undefined,
|
||||
baseUrl: typeof ai.baseUrl === 'string' ? ai.baseUrl : undefined,
|
||||
model: typeof ai.model === 'string' ? ai.model : undefined,
|
||||
systemPrompt: typeof ai.systemPrompt === 'string' ? ai.systemPrompt : undefined,
|
||||
requestTimeoutMs:
|
||||
typeof ai.requestTimeoutMs === 'number' &&
|
||||
Number.isFinite(ai.requestTimeoutMs) &&
|
||||
ai.requestTimeoutMs > 0
|
||||
? Math.floor(ai.requestTimeoutMs)
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
youtubeAi
|
||||
? {
|
||||
model: typeof youtubeAi.model === 'string' ? youtubeAi.model : undefined,
|
||||
systemPrompt:
|
||||
typeof youtubeAi.systemPrompt === 'string' ? youtubeAi.systemPrompt : undefined,
|
||||
}
|
||||
: undefined,
|
||||
),
|
||||
primarySubLanguages: asStringArray(youtube?.primarySubLanguages),
|
||||
secondarySubLanguages: asStringArray(secondarySub?.secondarySubLanguages),
|
||||
jimakuApiKey: typeof jimaku?.apiKey === 'string' ? jimaku.apiKey : undefined,
|
||||
|
||||
@@ -10,20 +10,9 @@ function createArgs(): Args {
|
||||
recursive: false,
|
||||
profile: '',
|
||||
startOverlay: false,
|
||||
youtubeMode: 'download',
|
||||
whisperBin: '',
|
||||
whisperModel: '',
|
||||
whisperVadModel: '',
|
||||
whisperThreads: 0,
|
||||
youtubeSubgenOutDir: '',
|
||||
youtubeSubgenAudioFormat: '',
|
||||
youtubeSubgenKeepTemp: false,
|
||||
youtubeFixWithAi: false,
|
||||
youtubePrimarySubLangs: [],
|
||||
youtubeSecondarySubLangs: [],
|
||||
youtubeAudioLangs: [],
|
||||
youtubeWhisperSourceLanguage: '',
|
||||
aiConfig: {},
|
||||
useTexthooker: false,
|
||||
autoStartOverlay: false,
|
||||
texthookerOnly: false,
|
||||
@@ -57,6 +46,7 @@ function createArgs(): Args {
|
||||
logsExport: false,
|
||||
version: false,
|
||||
settings: false,
|
||||
youtubeBrowser: false,
|
||||
configPath: false,
|
||||
configShow: false,
|
||||
mpvIdle: false,
|
||||
|
||||
@@ -330,6 +330,27 @@ test('launcher settings command forwards app settings window command', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('launcher youtube command forwards the YouTube browser flag and log level', () => {
|
||||
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\n${RUNTIME_PLUGIN_PREFLIGHT_SH}exit 0\n`);
|
||||
fs.chmodSync(appPath, 0o755);
|
||||
|
||||
const env = {
|
||||
...makeTestEnv(homeDir, xdgConfigHome),
|
||||
SUBMINER_APPIMAGE_PATH: appPath,
|
||||
SUBMINER_TEST_CAPTURE: capturePath,
|
||||
};
|
||||
const result = runLauncher(['yt', '--log-level', 'debug'], env);
|
||||
|
||||
assert.equal(result.status, 0);
|
||||
assert.equal(fs.readFileSync(capturePath, 'utf8'), '--youtube-browser\n--log-level\ndebug\n');
|
||||
});
|
||||
});
|
||||
|
||||
test('launcher settings command suppresses known Electron macOS menu diagnostics', () => {
|
||||
withTempDir((root) => {
|
||||
const homeDir = path.join(root, 'home');
|
||||
|
||||
+2
-20
@@ -581,19 +581,9 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
|
||||
recursive: false,
|
||||
profile: '',
|
||||
startOverlay: false,
|
||||
whisperBin: '',
|
||||
whisperModel: '',
|
||||
whisperVadModel: '',
|
||||
whisperThreads: 4,
|
||||
youtubeSubgenOutDir: '',
|
||||
youtubeSubgenAudioFormat: 'wav',
|
||||
youtubeSubgenKeepTemp: false,
|
||||
youtubeFixWithAi: false,
|
||||
youtubePrimarySubLangs: [],
|
||||
youtubeSecondarySubLangs: [],
|
||||
youtubeAudioLangs: [],
|
||||
youtubeWhisperSourceLanguage: 'ja',
|
||||
aiConfig: {},
|
||||
useTexthooker: false,
|
||||
autoStartOverlay: false,
|
||||
texthookerOnly: false,
|
||||
@@ -627,6 +617,7 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
|
||||
logsExport: false,
|
||||
version: false,
|
||||
settings: false,
|
||||
youtubeBrowser: false,
|
||||
configPath: false,
|
||||
configShow: false,
|
||||
mpvIdle: false,
|
||||
@@ -1084,30 +1075,21 @@ test('cleanupPlaybackSession stops launcher-managed overlay app and mpv-owned ch
|
||||
return true;
|
||||
},
|
||||
} as unknown as NonNullable<typeof state.mpvProc>;
|
||||
const helperProc = {
|
||||
killed: false,
|
||||
kill: () => {
|
||||
calls.push('helper-kill');
|
||||
return true;
|
||||
},
|
||||
} as unknown as NonNullable<typeof state.overlayProc>;
|
||||
|
||||
state.stopRequested = false;
|
||||
state.appPath = appPath;
|
||||
state.overlayManagedByLauncher = true;
|
||||
state.overlayProc = overlayProc;
|
||||
state.mpvProc = mpvProc;
|
||||
state.youtubeSubgenChildren.add(helperProc);
|
||||
|
||||
try {
|
||||
await cleanupPlaybackSession(makeArgs());
|
||||
|
||||
assert.deepEqual(calls, ['overlay-kill', 'mpv-kill', 'helper-kill']);
|
||||
assert.deepEqual(calls, ['overlay-kill', 'mpv-kill']);
|
||||
assert.match(fs.readFileSync(appInvocationsPath, 'utf8'), /--stop/);
|
||||
} finally {
|
||||
state.overlayProc = null;
|
||||
state.mpvProc = null;
|
||||
state.youtubeSubgenChildren.clear();
|
||||
state.overlayManagedByLauncher = false;
|
||||
state.appPath = '';
|
||||
state.stopRequested = false;
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
export const state = {
|
||||
overlayProc: null as ReturnType<typeof spawn> | null,
|
||||
mpvProc: null as ReturnType<typeof spawn> | null,
|
||||
youtubeSubgenChildren: new Set<ReturnType<typeof spawn>>(),
|
||||
appPath: '' as string,
|
||||
overlayManagedByLauncher: false,
|
||||
stopRequested: false,
|
||||
@@ -1186,17 +1185,6 @@ export function stopOverlay(args: Args): void {
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of state.youtubeSubgenChildren) {
|
||||
if (!child.killed) {
|
||||
try {
|
||||
child.kill('SIGTERM');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
state.youtubeSubgenChildren.clear();
|
||||
|
||||
void terminateTrackedDetachedMpv(args.logLevel);
|
||||
}
|
||||
|
||||
@@ -1211,17 +1199,6 @@ export async function cleanupPlaybackSession(args: Args): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of state.youtubeSubgenChildren) {
|
||||
if (!child.killed) {
|
||||
try {
|
||||
child.kill('SIGTERM');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
state.youtubeSubgenChildren.clear();
|
||||
|
||||
await terminateTrackedDetachedMpv(args.logLevel);
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,15 @@ test('parseArgs maps settings command to settings window', () => {
|
||||
assert.equal(parsed.configShow, false);
|
||||
});
|
||||
|
||||
test('parseArgs maps youtube command and yt alias to the YouTube browser', () => {
|
||||
assert.equal(parseArgs(['youtube'], 'subminer', {}).youtubeBrowser, true);
|
||||
|
||||
const alias = parseArgs(['yt', '--log-level', 'debug'], 'subminer', {});
|
||||
assert.equal(alias.youtubeBrowser, true);
|
||||
assert.equal(alias.logLevel, 'debug');
|
||||
assert.equal(alias.target, '');
|
||||
});
|
||||
|
||||
test('parseArgs maps config path action to config path output', () => {
|
||||
const parsed = parseArgs(['config', 'path'], 'subminer', {});
|
||||
|
||||
|
||||
+1
-42
@@ -1,4 +1,3 @@
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import type { MpvBackend, MpvLaunchMode } from '../src/types/config.js';
|
||||
import type { SubtitleGenerationConfig } from '../src/shared/subtitle-generation.js';
|
||||
@@ -27,12 +26,6 @@ export const YOUTUBE_AUDIO_EXTENSIONS = new Set([
|
||||
'.aac',
|
||||
'.flac',
|
||||
]);
|
||||
export const DEFAULT_YOUTUBE_SUBGEN_OUT_DIR = path.join(
|
||||
os.homedir(),
|
||||
'.cache',
|
||||
'subminer',
|
||||
'youtube-subs',
|
||||
);
|
||||
export function getDefaultLauncherLogFile(options?: {
|
||||
platform?: NodeJS.Platform;
|
||||
homeDir?: string;
|
||||
@@ -78,16 +71,6 @@ export function shouldForwardLogLevel(level: LogLevel): boolean {
|
||||
export type Backend = 'auto' | 'hyprland' | 'sway' | 'x11' | 'macos' | 'windows';
|
||||
export type JimakuLanguagePreference = 'ja' | 'en' | 'none';
|
||||
|
||||
export interface LauncherAiConfig {
|
||||
enabled?: boolean;
|
||||
apiKey?: string;
|
||||
apiKeyCommand?: string;
|
||||
baseUrl?: string;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
requestTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface Args {
|
||||
generateSubtitles?: {
|
||||
mediaPath?: string;
|
||||
@@ -102,20 +85,9 @@ export interface Args {
|
||||
recursive: boolean;
|
||||
profile: string;
|
||||
startOverlay: boolean;
|
||||
youtubeMode?: 'download' | 'generate';
|
||||
whisperBin: string;
|
||||
whisperModel: string;
|
||||
whisperVadModel: string;
|
||||
whisperThreads: number;
|
||||
youtubeSubgenOutDir: string;
|
||||
youtubeSubgenAudioFormat: string;
|
||||
youtubeSubgenKeepTemp: boolean;
|
||||
youtubeFixWithAi: boolean;
|
||||
youtubePrimarySubLangs: string[];
|
||||
youtubeSecondarySubLangs: string[];
|
||||
youtubeAudioLangs: string[];
|
||||
youtubeWhisperSourceLanguage: string;
|
||||
aiConfig: LauncherAiConfig;
|
||||
useTexthooker: boolean;
|
||||
autoStartOverlay: boolean;
|
||||
texthookerOnly: boolean;
|
||||
@@ -161,6 +133,7 @@ export interface Args {
|
||||
version: boolean;
|
||||
update?: boolean;
|
||||
settings: boolean;
|
||||
youtubeBrowser: boolean;
|
||||
configPath: boolean;
|
||||
configShow: boolean;
|
||||
mpvIdle: boolean;
|
||||
@@ -176,12 +149,6 @@ export interface Args {
|
||||
}
|
||||
|
||||
export interface LauncherYoutubeSubgenConfig {
|
||||
whisperBin?: string;
|
||||
whisperModel?: string;
|
||||
whisperVadModel?: string;
|
||||
whisperThreads?: number;
|
||||
fixWithAi?: boolean;
|
||||
ai?: LauncherAiConfig;
|
||||
primarySubLanguages?: string[];
|
||||
secondarySubLanguages?: string[];
|
||||
jimakuApiKey?: string;
|
||||
@@ -252,14 +219,6 @@ export interface SubtitleCandidate {
|
||||
source: 'manual' | 'whisper' | 'whisper-fixed' | 'whisper-translate' | 'whisper-translate-fixed';
|
||||
}
|
||||
|
||||
export interface YoutubeSubgenOutputs {
|
||||
basename: string;
|
||||
primaryPath?: string;
|
||||
secondaryPath?: string;
|
||||
primaryNative?: boolean;
|
||||
secondaryNative?: boolean;
|
||||
}
|
||||
|
||||
export interface MpvTrack {
|
||||
type?: string;
|
||||
id?: number;
|
||||
|
||||
@@ -236,14 +236,6 @@ export function parseBoolLike(value: string): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function inferWhisperLanguage(langCodes: string[], fallback: string): string {
|
||||
for (const lang of uniqueNormalizedLangCodes(langCodes)) {
|
||||
if (lang === 'jpn') return 'ja';
|
||||
if (lang.length >= 2) return lang.slice(0, 2);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export interface CommandInvocationOptions {
|
||||
normalizeWindowsShellArgs?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user