[codex] Replace mpv fullscreen toggle with launch mode config (#48)

Co-authored-by: bee <autumn@skerritt.blog>
This commit is contained in:
Autumn (Bee)
2026-04-07 16:38:15 +09:00
committed by GitHub
parent 7a64488ed5
commit bc7dde3b02
31 changed files with 305 additions and 31 deletions

View File

@@ -58,6 +58,7 @@ function createContext(): LauncherCommandContext {
jellyfinServer: '',
jellyfinUsername: '',
jellyfinPassword: '',
launchMode: 'normal',
},
scriptPath: '/tmp/subminer',
scriptName: 'subminer',

View File

@@ -2,6 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { parseLauncherYoutubeSubgenConfig } from './config/youtube-subgen-config.js';
import { parseLauncherJellyfinConfig } from './config/jellyfin-config.js';
import { parseLauncherMpvConfig } from './config/mpv-config.js';
import { readExternalYomitanProfilePath } from './config.js';
import {
getPluginConfigCandidates,
@@ -80,6 +81,27 @@ test('parseLauncherJellyfinConfig omits legacy token and user id fields', () =>
assert.equal('userId' in parsed, false);
});
test('parseLauncherMpvConfig reads launch mode preference', () => {
const parsed = parseLauncherMpvConfig({
mpv: {
launchMode: ' maximized ',
executablePath: 'ignored-here',
},
});
assert.equal(parsed.launchMode, 'maximized');
});
test('parseLauncherMpvConfig ignores invalid launch mode values', () => {
const parsed = parseLauncherMpvConfig({
mpv: {
launchMode: 'wide',
},
});
assert.equal(parsed.launchMode, undefined);
});
test('parsePluginRuntimeConfigContent reads socket path and startup gate options', () => {
const parsed = parsePluginRuntimeConfigContent(`
# comment

View File

@@ -2,6 +2,7 @@ import { fail } from './log.js';
import type {
Args,
LauncherJellyfinConfig,
LauncherMpvConfig,
LauncherYoutubeSubgenConfig,
LogLevel,
PluginRuntimeConfig,
@@ -13,6 +14,7 @@ import {
} from './config/args-normalizer.js';
import { parseCliPrograms, resolveTopLevelCommand } from './config/cli-parser-builder.js';
import { parseLauncherJellyfinConfig } from './config/jellyfin-config.js';
import { parseLauncherMpvConfig } from './config/mpv-config.js';
import { readPluginRuntimeConfig as readPluginRuntimeConfigValue } from './config/plugin-runtime-config.js';
import { readLauncherMainConfigObject } from './config/shared-config-reader.js';
import { parseLauncherYoutubeSubgenConfig } from './config/youtube-subgen-config.js';
@@ -44,6 +46,12 @@ export function loadLauncherJellyfinConfig(): LauncherJellyfinConfig {
return parseLauncherJellyfinConfig(root);
}
export function loadLauncherMpvConfig(): LauncherMpvConfig {
const root = readLauncherMainConfigObject();
if (!root) return {};
return parseLauncherMpvConfig(root);
}
export function hasLauncherExternalYomitanProfileConfig(): boolean {
return readExternalYomitanProfilePath(readLauncherMainConfigObject()) !== null;
}
@@ -56,9 +64,10 @@ export function parseArgs(
argv: string[],
scriptName: string,
launcherConfig: LauncherYoutubeSubgenConfig,
launcherMpvConfig: LauncherMpvConfig = {},
): Args {
const topLevelCommand = resolveTopLevelCommand(argv);
const parsed = createDefaultArgs(launcherConfig);
const parsed = createDefaultArgs(launcherConfig, launcherMpvConfig);
if (topLevelCommand && (topLevelCommand.name === 'app' || topLevelCommand.name === 'bin')) {
parsed.appPassthrough = true;

View File

@@ -1,7 +1,13 @@
import fs from 'node:fs';
import path from 'node:path';
import { fail } from '../log.js';
import type { Args, Backend, LauncherYoutubeSubgenConfig, LogLevel } from '../types.js';
import type {
Args,
Backend,
LauncherMpvConfig,
LauncherYoutubeSubgenConfig,
LogLevel,
} from '../types.js';
import {
DEFAULT_JIMAKU_API_BASE_URL,
DEFAULT_YOUTUBE_PRIMARY_SUB_LANGS,
@@ -83,7 +89,10 @@ function parseDictionaryTarget(value: string): string {
return resolved;
}
export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig): Args {
export function createDefaultArgs(
launcherConfig: LauncherYoutubeSubgenConfig,
mpvConfig: LauncherMpvConfig = {},
): Args {
const configuredSecondaryLangs = uniqueNormalizedLangCodes(
launcherConfig.secondarySubLanguages ?? [],
);
@@ -148,6 +157,7 @@ export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig):
jellyfinServer: '',
jellyfinUsername: '',
jellyfinPassword: '',
launchMode: mpvConfig.launchMode ?? 'normal',
youtubePrimarySubLangs: primarySubLangs,
youtubeSecondarySubLangs: secondarySubLangs,
youtubeAudioLangs,

View File

@@ -0,0 +1,12 @@
import { parseMpvLaunchMode } from '../../src/shared/mpv-launch-mode.js';
import type { LauncherMpvConfig } from '../types.js';
export function parseLauncherMpvConfig(root: Record<string, unknown>): LauncherMpvConfig {
const mpvRaw = root.mpv;
if (!mpvRaw || typeof mpvRaw !== 'object') return {};
const mpv = mpvRaw as Record<string, unknown>;
return {
launchMode: parseMpvLaunchMode(mpv.launchMode),
};
}

View File

@@ -1,6 +1,7 @@
import path from 'node:path';
import {
loadLauncherJellyfinConfig,
loadLauncherMpvConfig,
loadLauncherYoutubeSubgenConfig,
parseArgs,
readPluginRuntimeConfig,
@@ -52,7 +53,8 @@ async function main(): Promise<void> {
const scriptPath = process.argv[1] || 'subminer';
const scriptName = path.basename(scriptPath);
const launcherConfig = loadLauncherYoutubeSubgenConfig();
const args = parseArgs(process.argv.slice(2), scriptName, launcherConfig);
const launcherMpvConfig = loadLauncherMpvConfig();
const args = parseArgs(process.argv.slice(2), scriptName, launcherConfig, launcherMpvConfig);
const pluginRuntimeConfig = readPluginRuntimeConfig(args.logLevel);
const appPath = findAppBinary(scriptPath);

View File

@@ -7,6 +7,7 @@ import net from 'node:net';
import { EventEmitter } from 'node:events';
import type { Args } from './types';
import {
buildConfiguredMpvDefaultArgs,
buildMpvBackendArgs,
buildMpvEnv,
cleanupPlaybackSession,
@@ -234,6 +235,33 @@ test('buildMpvBackendArgs keeps supported Hyprland and Sway auto backends unchan
});
});
test('buildConfiguredMpvDefaultArgs appends maximized launch mode to configured defaults', () => {
withPlatform('linux', () => {
assert.deepEqual(
buildConfiguredMpvDefaultArgs(makeArgs({ launchMode: 'maximized' }), {
DISPLAY: ':1',
WAYLAND_DISPLAY: 'wayland-0',
XDG_SESSION_TYPE: 'wayland',
XDG_CURRENT_DESKTOP: 'KDE',
XDG_SESSION_DESKTOP: 'plasma',
}),
[
'--sub-auto=fuzzy',
'--sub-file-paths=.;subs;subtitles',
'--sid=auto',
'--secondary-sid=auto',
'--secondary-sub-visibility=no',
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
'--vo=gpu',
'--gpu-api=opengl',
'--gpu-context=x11egl,x11',
'--window-maximized=yes',
],
);
});
});
test('launchTexthookerOnly exits non-zero when app binary cannot be spawned', () => {
const error = withProcessExitIntercept(() => {
launchTexthookerOnly('/definitely-missing-subminer-binary', makeArgs());
@@ -401,6 +429,7 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
jellyfinServer: '',
jellyfinUsername: '',
jellyfinPassword: '',
launchMode: 'normal',
...overrides,
};
}

View File

@@ -3,6 +3,7 @@ import path from 'node:path';
import os from 'node:os';
import net from 'node:net';
import { spawn, spawnSync } from 'node:child_process';
import { buildMpvLaunchModeArgs } from '../src/shared/mpv-launch-mode.js';
import type { LogLevel, Backend, Args, MpvTrack } from './types.js';
import { DEFAULT_MPV_SUBMINER_ARGS, DEFAULT_YOUTUBE_YTDL_FORMAT } from './types.js';
import { appendToAppLog, getAppLogPath, log, fail, getMpvLogPath } from './log.js';
@@ -673,9 +674,7 @@ export async function startMpv(
}
const mpvArgs: string[] = [];
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
mpvArgs.push(...buildMpvBackendArgs(args));
mpvArgs.push(...buildConfiguredMpvDefaultArgs(args));
if (targetKind === 'url' && isYoutubeTarget(target)) {
log('info', args.logLevel, 'Applying URL playback options');
mpvArgs.push('--ytdl=yes');
@@ -703,7 +702,6 @@ export async function startMpv(
if (args.mpvArgs) {
mpvArgs.push(...parseMpvArgString(args.mpvArgs));
}
if (preloadedSubtitles?.primaryPath) {
mpvArgs.push(`--sub-file=${preloadedSubtitles.primaryPath}`);
}
@@ -979,6 +977,18 @@ export function buildMpvBackendArgs(
return ['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'];
}
export function buildConfiguredMpvDefaultArgs(
args: Pick<Args, 'profile' | 'backend' | 'launchMode'>,
baseEnv: NodeJS.ProcessEnv = process.env,
): string[] {
const mpvArgs: string[] = [];
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
mpvArgs.push(...buildMpvBackendArgs(args, baseEnv));
mpvArgs.push(...buildMpvLaunchModeArgs(args.launchMode));
return mpvArgs;
}
function appendCapturedAppOutput(kind: 'STDOUT' | 'STDERR', chunk: string): void {
const normalized = chunk.replace(/\r\n/g, '\n');
for (const line of normalized.split('\n')) {
@@ -1209,10 +1219,7 @@ export function launchMpvIdleDetached(
// ignore
}
const mpvArgs: string[] = [];
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
mpvArgs.push(...buildMpvBackendArgs(args));
const mpvArgs: string[] = buildConfiguredMpvDefaultArgs(args);
if (args.mpvArgs) {
mpvArgs.push(...parseMpvArgString(args.mpvArgs));
}

View File

@@ -85,6 +85,12 @@ test('parseArgs maps mpv idle action', () => {
assert.equal(parsed.mpvStatus, false);
});
test('parseArgs applies configured mpv launch mode default', () => {
const parsed = parseArgs([], 'subminer', {}, { launchMode: 'maximized' });
assert.equal(parsed.launchMode, 'maximized');
});
test('parseArgs maps dictionary command and log-level override', () => {
const parsed = parseArgs(['dictionary', '.', '--log-level', 'debug'], 'subminer', {});

View File

@@ -1,5 +1,6 @@
import path from 'node:path';
import os from 'node:os';
import type { MpvLaunchMode } from '../src/types/config.js';
import { resolveDefaultLogFilePath } from '../src/shared/log-files.js';
export { VIDEO_EXTENSIONS } from '../src/shared/video-extensions.js';
@@ -140,6 +141,7 @@ export interface Args {
jellyfinServer: string;
jellyfinUsername: string;
jellyfinPassword: string;
launchMode: MpvLaunchMode;
}
export interface LauncherYoutubeSubgenConfig {
@@ -167,6 +169,10 @@ export interface LauncherJellyfinConfig {
iconCacheDir?: string;
}
export interface LauncherMpvConfig {
launchMode?: MpvLaunchMode;
}
export interface PluginRuntimeConfig {
socketPath: string;
autoStart: boolean;