chore: add project management metadata and remaining repository files

This commit is contained in:
2026-02-22 21:43:43 -08:00
parent 64020a9069
commit 4ebabbe639
37 changed files with 7531 additions and 0 deletions

View File

@@ -0,0 +1,257 @@
import fs from 'node:fs';
import { fail } from '../log.js';
import type {
Args,
Backend,
LauncherYoutubeSubgenConfig,
LogLevel,
YoutubeSubgenMode,
} from '../types.js';
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 type { CliInvocations } from './cli-parser-builder.js';
function ensureTarget(target: string, parsed: Args): void {
if (isUrlTarget(target)) {
parsed.target = target;
parsed.targetKind = 'url';
return;
}
const resolved = resolvePathMaybe(target);
let stat: fs.Stats | null = null;
try {
stat = fs.statSync(resolved);
} catch {
stat = null;
}
if (stat?.isFile()) {
parsed.target = resolved;
parsed.targetKind = 'file';
return;
}
if (stat?.isDirectory()) {
parsed.directory = resolved;
return;
}
fail(`Not a file, directory, or supported URL: ${target}`);
}
function parseLogLevel(value: string): LogLevel {
if (value === 'debug' || value === 'info' || value === 'warn' || value === 'error') {
return value;
}
fail(`Invalid log level: ${value} (must be debug, info, warn, or error)`);
}
function parseYoutubeMode(value: string): YoutubeSubgenMode {
const normalized = value.toLowerCase();
if (normalized === 'automatic' || normalized === 'preprocess' || normalized === 'off') {
return normalized as YoutubeSubgenMode;
}
fail(`Invalid yt-subgen mode: ${value} (must be automatic, preprocess, or off)`);
}
function parseBackend(value: string): Backend {
if (value === 'auto' || value === 'hyprland' || value === 'x11' || value === 'macos') {
return value as Backend;
}
fail(`Invalid backend: ${value} (must be auto, hyprland, x11, or macos)`);
}
export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig): Args {
const envMode = (process.env.SUBMINER_YT_SUBGEN_MODE || '').toLowerCase();
const defaultMode: YoutubeSubgenMode =
envMode === 'preprocess' || envMode === 'off' || envMode === 'automatic'
? (envMode as YoutubeSubgenMode)
: launcherConfig.mode
? launcherConfig.mode
: 'automatic';
const configuredSecondaryLangs = uniqueNormalizedLangCodes(
launcherConfig.secondarySubLanguages ?? [],
);
const configuredPrimaryLangs = uniqueNormalizedLangCodes(
launcherConfig.primarySubLanguages ?? [],
);
const primarySubLangs =
configuredPrimaryLangs.length > 0
? configuredPrimaryLangs
: [...DEFAULT_YOUTUBE_PRIMARY_SUB_LANGS];
const secondarySubLangs =
configuredSecondaryLangs.length > 0
? configuredSecondaryLangs
: [...DEFAULT_YOUTUBE_SECONDARY_SUB_LANGS];
const youtubeAudioLangs = uniqueNormalizedLangCodes([...primarySubLangs, ...secondarySubLangs]);
const parsed: Args = {
backend: 'auto',
directory: '.',
recursive: false,
profile: 'subminer',
startOverlay: false,
youtubeSubgenMode: defaultMode,
whisperBin: process.env.SUBMINER_WHISPER_BIN || launcherConfig.whisperBin || '',
whisperModel: process.env.SUBMINER_WHISPER_MODEL || launcherConfig.whisperModel || '',
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',
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,
jimakuLanguagePreference: launcherConfig.jimakuLanguagePreference || 'ja',
jimakuMaxEntryResults: launcherConfig.jimakuMaxEntryResults || 10,
jellyfin: false,
jellyfinLogin: false,
jellyfinLogout: false,
jellyfinPlay: false,
jellyfinDiscovery: false,
doctor: false,
configPath: false,
configShow: false,
mpvIdle: false,
mpvSocket: false,
mpvStatus: false,
appPassthrough: false,
appArgs: [],
jellyfinServer: '',
jellyfinUsername: '',
jellyfinPassword: '',
youtubePrimarySubLangs: primarySubLangs,
youtubeSecondarySubLangs: secondarySubLangs,
youtubeAudioLangs,
youtubeWhisperSourceLanguage: inferWhisperLanguage(primarySubLangs, 'ja'),
useTexthooker: true,
autoStartOverlay: false,
texthookerOnly: false,
useRofi: false,
logLevel: 'info',
target: '',
targetKind: '',
};
if (launcherConfig.jimakuApiKey) parsed.jimakuApiKey = launcherConfig.jimakuApiKey;
if (launcherConfig.jimakuApiKeyCommand)
parsed.jimakuApiKeyCommand = launcherConfig.jimakuApiKeyCommand;
if (launcherConfig.jimakuApiBaseUrl) parsed.jimakuApiBaseUrl = launcherConfig.jimakuApiBaseUrl;
if (launcherConfig.jimakuLanguagePreference)
parsed.jimakuLanguagePreference = launcherConfig.jimakuLanguagePreference;
if (launcherConfig.jimakuMaxEntryResults !== undefined)
parsed.jimakuMaxEntryResults = launcherConfig.jimakuMaxEntryResults;
return parsed;
}
export function applyRootOptionsToArgs(
parsed: Args,
options: Record<string, unknown>,
rootTarget: unknown,
): void {
if (typeof options.backend === 'string') parsed.backend = parseBackend(options.backend);
if (typeof options.directory === 'string') parsed.directory = options.directory;
if (options.recursive === true) parsed.recursive = true;
if (typeof options.profile === 'string') parsed.profile = options.profile;
if (options.start === true) parsed.startOverlay = true;
if (typeof options.logLevel === 'string') parsed.logLevel = parseLogLevel(options.logLevel);
if (options.rofi === true) parsed.useRofi = true;
if (options.startOverlay === true) parsed.autoStartOverlay = true;
if (options.texthooker === false) parsed.useTexthooker = false;
if (typeof rootTarget === 'string' && rootTarget) ensureTarget(rootTarget, parsed);
}
export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations): void {
if (invocations.doctorTriggered) parsed.doctor = true;
if (invocations.texthookerTriggered) parsed.texthookerOnly = true;
if (invocations.jellyfinInvocation) {
if (invocations.jellyfinInvocation.logLevel) {
parsed.logLevel = parseLogLevel(invocations.jellyfinInvocation.logLevel);
}
const action = (invocations.jellyfinInvocation.action || '').toLowerCase();
if (action && !['setup', 'discovery', 'play', 'login', 'logout'].includes(action)) {
fail(`Unknown jellyfin action: ${invocations.jellyfinInvocation.action}`);
}
parsed.jellyfinServer = invocations.jellyfinInvocation.server || '';
parsed.jellyfinUsername = invocations.jellyfinInvocation.username || '';
parsed.jellyfinPassword = invocations.jellyfinInvocation.password || '';
const modeFlags = {
setup: invocations.jellyfinInvocation.setup || action === 'setup',
discovery: invocations.jellyfinInvocation.discovery || action === 'discovery',
play: invocations.jellyfinInvocation.play || action === 'play',
login: invocations.jellyfinInvocation.login || action === 'login',
logout: invocations.jellyfinInvocation.logout || action === 'logout',
};
if (
!modeFlags.setup &&
!modeFlags.discovery &&
!modeFlags.play &&
!modeFlags.login &&
!modeFlags.logout
) {
modeFlags.setup = true;
}
parsed.jellyfin = Boolean(modeFlags.setup);
parsed.jellyfinDiscovery = Boolean(modeFlags.discovery);
parsed.jellyfinPlay = Boolean(modeFlags.play);
parsed.jellyfinLogin = Boolean(modeFlags.login);
parsed.jellyfinLogout = Boolean(modeFlags.logout);
}
if (invocations.ytInvocation) {
if (invocations.ytInvocation.logLevel)
parsed.logLevel = parseLogLevel(invocations.ytInvocation.logLevel);
if (invocations.ytInvocation.mode)
parsed.youtubeSubgenMode = parseYoutubeMode(invocations.ytInvocation.mode);
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.ytSubgenAudioFormat) {
parsed.youtubeSubgenAudioFormat = invocations.ytInvocation.ytSubgenAudioFormat;
}
if (invocations.ytInvocation.target) ensureTarget(invocations.ytInvocation.target, parsed);
}
if (invocations.doctorLogLevel) parsed.logLevel = parseLogLevel(invocations.doctorLogLevel);
if (invocations.texthookerLogLevel)
parsed.logLevel = parseLogLevel(invocations.texthookerLogLevel);
if (invocations.configInvocation) {
if (invocations.configInvocation.logLevel) {
parsed.logLevel = parseLogLevel(invocations.configInvocation.logLevel);
}
const action = (invocations.configInvocation.action || 'path').toLowerCase();
if (action === 'path') parsed.configPath = true;
else if (action === 'show') parsed.configShow = true;
else fail(`Unknown config action: ${invocations.configInvocation.action}`);
}
if (invocations.mpvInvocation) {
if (invocations.mpvInvocation.logLevel) {
parsed.logLevel = parseLogLevel(invocations.mpvInvocation.logLevel);
}
const action = (invocations.mpvInvocation.action || 'status').toLowerCase();
if (action === 'status') parsed.mpvStatus = true;
else if (action === 'socket') parsed.mpvSocket = true;
else if (action === 'idle' || action === 'start') parsed.mpvIdle = true;
else fail(`Unknown mpv action: ${invocations.mpvInvocation.action}`);
}
if (invocations.appInvocation) {
parsed.appPassthrough = true;
parsed.appArgs = invocations.appInvocation.appArgs;
}
}

View File

@@ -0,0 +1,294 @@
import { Command } from 'commander';
export interface JellyfinInvocation {
action?: string;
discovery?: boolean;
play?: boolean;
login?: boolean;
logout?: boolean;
setup?: boolean;
server?: string;
username?: string;
password?: string;
logLevel?: string;
}
export interface YtInvocation {
target?: string;
mode?: string;
outDir?: string;
keepTemp?: boolean;
whisperBin?: string;
whisperModel?: string;
ytSubgenAudioFormat?: string;
logLevel?: string;
}
export interface CommandActionInvocation {
action: string;
logLevel?: string;
}
export interface CliInvocations {
jellyfinInvocation: JellyfinInvocation | null;
ytInvocation: YtInvocation | null;
configInvocation: CommandActionInvocation | null;
mpvInvocation: CommandActionInvocation | null;
appInvocation: { appArgs: string[] } | null;
doctorTriggered: boolean;
doctorLogLevel: string | null;
texthookerTriggered: boolean;
texthookerLogLevel: string | null;
}
function applyRootOptions(program: Command): void {
program
.option('-b, --backend <backend>', 'Display backend')
.option('-d, --directory <dir>', 'Directory to browse')
.option('-r, --recursive', 'Search directories recursively')
.option('-p, --profile <profile>', 'MPV profile')
.option('--start', 'Explicitly start overlay')
.option('--log-level <level>', 'Log level')
.option('-R, --rofi', 'Use rofi picker')
.option('-S, --start-overlay', 'Auto-start overlay')
.option('-T, --no-texthooker', 'Disable texthooker-ui server');
}
function buildSubcommandHelpText(program: Command): string {
const subcommands = program.commands
.filter((command) => command.name() !== 'help')
.map((command) => {
const aliases = command.aliases();
const term = aliases.length > 0 ? `${command.name()}|${aliases[0]}` : command.name();
return { term, description: command.description() };
});
if (subcommands.length === 0) return '';
const longestTerm = Math.max(...subcommands.map((entry) => entry.term.length));
const lines = subcommands.map((entry) =>
` ${entry.term.padEnd(longestTerm)} ${entry.description || ''}`.trimEnd(),
);
return `\nCommands:\n${lines.join('\n')}\n`;
}
function getTopLevelCommand(argv: string[]): { name: string; index: number } | null {
const commandNames = new Set([
'jellyfin',
'jf',
'yt',
'youtube',
'doctor',
'config',
'mpv',
'texthooker',
'app',
'bin',
'help',
]);
const optionsWithValue = new Set([
'-b',
'--backend',
'-d',
'--directory',
'-p',
'--profile',
'--log-level',
]);
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i] || '';
if (token === '--') return null;
if (token.startsWith('-')) {
if (optionsWithValue.has(token)) i += 1;
continue;
}
return commandNames.has(token) ? { name: token, index: i } : null;
}
return null;
}
function hasTopLevelCommand(argv: string[]): boolean {
return getTopLevelCommand(argv) !== null;
}
export function resolveTopLevelCommand(argv: string[]): { name: string; index: number } | null {
return getTopLevelCommand(argv);
}
export function parseCliPrograms(
argv: string[],
scriptName: string,
): {
options: Record<string, unknown>;
rootTarget: unknown;
invocations: CliInvocations;
} {
let jellyfinInvocation: JellyfinInvocation | null = null;
let ytInvocation: YtInvocation | null = null;
let configInvocation: CommandActionInvocation | null = null;
let mpvInvocation: CommandActionInvocation | null = null;
let appInvocation: { appArgs: string[] } | null = null;
let doctorLogLevel: string | null = null;
let texthookerLogLevel: string | null = null;
let doctorTriggered = false;
let texthookerTriggered = false;
const commandProgram = new Command();
commandProgram
.name(scriptName)
.description('Launch MPV with SubMiner sentence mining overlay')
.showHelpAfterError(true)
.enablePositionalOptions()
.allowExcessArguments(false)
.allowUnknownOption(false)
.exitOverride();
applyRootOptions(commandProgram);
const rootProgram = new Command();
rootProgram
.name(scriptName)
.description('Launch MPV with SubMiner sentence mining overlay')
.usage('[options] [command] [target]')
.showHelpAfterError(true)
.allowExcessArguments(false)
.allowUnknownOption(false)
.exitOverride()
.argument('[target]', 'file, directory, or URL');
applyRootOptions(rootProgram);
commandProgram
.command('jellyfin')
.alias('jf')
.description('Jellyfin workflows')
.argument('[action]', 'setup|discovery|play|login|logout')
.option('-d, --discovery', 'Cast discovery mode')
.option('-p, --play', 'Interactive play picker')
.option('-l, --login', 'Login flow')
.option('--logout', 'Clear token/session')
.option('--setup', 'Open setup window')
.option('-s, --server <url>', 'Jellyfin server URL')
.option('-u, --username <name>', 'Jellyfin username')
.option('-w, --password <pass>', 'Jellyfin password')
.option('--log-level <level>', 'Log level')
.action((action: string | undefined, options: Record<string, unknown>) => {
jellyfinInvocation = {
action,
discovery: options.discovery === true,
play: options.play === true,
login: options.login === true,
logout: options.logout === true,
setup: options.setup === true,
server: typeof options.server === 'string' ? options.server : undefined,
username: typeof options.username === 'string' ? options.username : undefined,
password: typeof options.password === 'string' ? options.password : undefined,
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
};
});
commandProgram
.command('yt')
.alias('youtube')
.description('YouTube workflows')
.argument('[target]', 'YouTube URL or ytsearch: query')
.option('-m, --mode <mode>', 'Subtitle generation 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('--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 : 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,
ytSubgenAudioFormat:
typeof options.ytSubgenAudioFormat === 'string' ? options.ytSubgenAudioFormat : undefined,
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
};
});
commandProgram
.command('doctor')
.description('Run dependency and environment checks')
.option('--log-level <level>', 'Log level')
.action((options: Record<string, unknown>) => {
doctorTriggered = true;
doctorLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
});
commandProgram
.command('config')
.description('Config helpers')
.argument('[action]', 'path|show', 'path')
.option('--log-level <level>', 'Log level')
.action((action: string, options: Record<string, unknown>) => {
configInvocation = {
action,
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
};
});
commandProgram
.command('mpv')
.description('MPV helpers')
.argument('[action]', 'status|socket|idle', 'status')
.option('--log-level <level>', 'Log level')
.action((action: string, options: Record<string, unknown>) => {
mpvInvocation = {
action,
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
};
});
commandProgram
.command('texthooker')
.description('Launch texthooker-only mode')
.option('--log-level <level>', 'Log level')
.action((options: Record<string, unknown>) => {
texthookerTriggered = true;
texthookerLogLevel = typeof options.logLevel === 'string' ? options.logLevel : null;
});
commandProgram
.command('app')
.alias('bin')
.description('Pass arguments directly to SubMiner binary')
.allowUnknownOption(true)
.allowExcessArguments(true)
.argument('[appArgs...]', 'Arguments forwarded to SubMiner app binary')
.action((appArgs: string[] | undefined) => {
appInvocation = { appArgs: Array.isArray(appArgs) ? appArgs : [] };
});
rootProgram.addHelpText('after', buildSubcommandHelpText(commandProgram));
const selectedProgram = hasTopLevelCommand(argv) ? commandProgram : rootProgram;
try {
selectedProgram.parse(['node', scriptName, ...argv]);
} catch (error) {
const commanderError = error as { code?: string; message?: string };
if (commanderError?.code === 'commander.helpDisplayed') {
process.exit(0);
}
throw new Error(commanderError?.message || String(error));
}
return {
options: selectedProgram.opts<Record<string, unknown>>(),
rootTarget: rootProgram.processedArgs[0],
invocations: {
jellyfinInvocation,
ytInvocation,
configInvocation,
mpvInvocation,
appInvocation,
doctorTriggered,
doctorLogLevel,
texthookerTriggered,
texthookerLogLevel,
},
};
}

View File

@@ -0,0 +1,16 @@
import type { LauncherJellyfinConfig } from '../types.js';
export function parseLauncherJellyfinConfig(root: Record<string, unknown>): LauncherJellyfinConfig {
const jellyfinRaw = root.jellyfin;
if (!jellyfinRaw || typeof jellyfinRaw !== 'object') return {};
const jellyfin = jellyfinRaw as Record<string, unknown>;
return {
enabled: typeof jellyfin.enabled === 'boolean' ? jellyfin.enabled : undefined,
serverUrl: typeof jellyfin.serverUrl === 'string' ? jellyfin.serverUrl : undefined,
username: typeof jellyfin.username === 'string' ? jellyfin.username : undefined,
defaultLibraryId:
typeof jellyfin.defaultLibraryId === 'string' ? jellyfin.defaultLibraryId : undefined,
pullPictures: typeof jellyfin.pullPictures === 'boolean' ? jellyfin.pullPictures : undefined,
iconCacheDir: typeof jellyfin.iconCacheDir === 'string' ? jellyfin.iconCacheDir : undefined,
};
}

View File

@@ -0,0 +1,57 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { log } from '../log.js';
import type { LogLevel, PluginRuntimeConfig } from '../types.js';
import { DEFAULT_SOCKET_PATH } from '../types.js';
export function getPluginConfigCandidates(): string[] {
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
return Array.from(
new Set([
path.join(xdgConfigHome, 'mpv', 'script-opts', 'subminer.conf'),
path.join(os.homedir(), '.config', 'mpv', 'script-opts', 'subminer.conf'),
]),
);
}
export function parsePluginRuntimeConfigContent(content: string): PluginRuntimeConfig {
const runtimeConfig: PluginRuntimeConfig = { socketPath: DEFAULT_SOCKET_PATH };
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim();
if (trimmed.length === 0 || trimmed.startsWith('#')) continue;
const socketMatch = trimmed.match(/^socket_path\s*=\s*(.+)$/i);
if (!socketMatch) continue;
const value = (socketMatch[1] || '').split('#', 1)[0]?.trim() || '';
if (value) runtimeConfig.socketPath = value;
}
return runtimeConfig;
}
export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig {
const candidates = getPluginConfigCandidates();
const defaults: PluginRuntimeConfig = { socketPath: DEFAULT_SOCKET_PATH };
for (const configPath of candidates) {
if (!fs.existsSync(configPath)) continue;
try {
const parsed = parsePluginRuntimeConfigContent(fs.readFileSync(configPath, 'utf8'));
log(
'debug',
logLevel,
`Using mpv plugin settings from ${configPath}: socket_path=${parsed.socketPath}`,
);
return parsed;
} catch {
log('warn', logLevel, `Failed to read ${configPath}; using launcher defaults`);
return defaults;
}
}
log(
'debug',
logLevel,
`No mpv subminer.conf found; using launcher defaults (socket_path=${defaults.socketPath})`,
);
return defaults;
}

View File

@@ -0,0 +1,25 @@
import fs from 'node:fs';
import os from 'node:os';
import { parse as parseJsonc } from 'jsonc-parser';
import { resolveConfigFilePath } from '../../src/config/path-resolution.js';
export function resolveLauncherMainConfigPath(): string {
return resolveConfigFilePath({
xdgConfigHome: process.env.XDG_CONFIG_HOME,
homeDir: os.homedir(),
existsSync: fs.existsSync,
});
}
export function readLauncherMainConfigObject(): Record<string, unknown> | null {
const configPath = resolveLauncherMainConfigPath();
if (!fs.existsSync(configPath)) return null;
try {
const data = fs.readFileSync(configPath, 'utf8');
const parsed = configPath.endsWith('.jsonc') ? parseJsonc(data) : JSON.parse(data);
if (!parsed || typeof parsed !== 'object') return null;
return parsed as Record<string, unknown>;
} catch {
return null;
}
}

View File

@@ -0,0 +1,54 @@
import type { LauncherYoutubeSubgenConfig } from '../types.js';
function asStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined;
return value.filter((entry): entry is string => typeof entry === 'string');
}
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 secondarySubRaw = root.secondarySub;
const secondarySub =
secondarySubRaw && typeof secondarySubRaw === 'object'
? (secondarySubRaw as Record<string, unknown>)
: null;
const jimakuRaw = root.jimaku;
const jimaku =
jimakuRaw && typeof jimakuRaw === 'object' ? (jimakuRaw as Record<string, unknown>) : null;
const mode = youtubeSubgen?.mode;
const jimakuLanguagePreference = jimaku?.languagePreference;
const jimakuMaxEntryResults = jimaku?.maxEntryResults;
return {
mode: mode === 'automatic' || mode === 'preprocess' || mode === 'off' ? mode : undefined,
whisperBin:
typeof youtubeSubgen?.whisperBin === 'string' ? youtubeSubgen.whisperBin : undefined,
whisperModel:
typeof youtubeSubgen?.whisperModel === 'string' ? youtubeSubgen.whisperModel : undefined,
primarySubLanguages: asStringArray(youtubeSubgen?.primarySubLanguages),
secondarySubLanguages: asStringArray(secondarySub?.secondarySubLanguages),
jimakuApiKey: typeof jimaku?.apiKey === 'string' ? jimaku.apiKey : undefined,
jimakuApiKeyCommand:
typeof jimaku?.apiKeyCommand === 'string' ? jimaku.apiKeyCommand : undefined,
jimakuApiBaseUrl: typeof jimaku?.apiBaseUrl === 'string' ? jimaku.apiBaseUrl : undefined,
jimakuLanguagePreference:
jimakuLanguagePreference === 'ja' ||
jimakuLanguagePreference === 'en' ||
jimakuLanguagePreference === 'none'
? jimakuLanguagePreference
: undefined,
jimakuMaxEntryResults:
typeof jimakuMaxEntryResults === 'number' &&
Number.isFinite(jimakuMaxEntryResults) &&
jimakuMaxEntryResults > 0
? Math.floor(jimakuMaxEntryResults)
: undefined,
};
}