mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-09 16:19:25 -07:00
[codex] Replace mpv fullscreen toggle with launch mode config (#48)
Co-authored-by: bee <autumn@skerritt.blog>
This commit is contained in:
@@ -93,6 +93,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
},
|
||||
mpv: {
|
||||
executablePath: '',
|
||||
launchMode: 'normal',
|
||||
},
|
||||
anilist: {
|
||||
enabled: false,
|
||||
|
||||
@@ -29,6 +29,7 @@ test('config option registry includes critical paths and has unique entries', ()
|
||||
'anilist.characterDictionary.enabled',
|
||||
'anilist.characterDictionary.collapsibleSections.description',
|
||||
'mpv.executablePath',
|
||||
'mpv.launchMode',
|
||||
'yomitan.externalProfilePath',
|
||||
'immersionTracking.enabled',
|
||||
]) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
import { MPV_LAUNCH_MODE_VALUES } from '../../shared/mpv-launch-mode';
|
||||
import { ConfigOptionRegistryEntry, RuntimeOptionRegistryEntry } from './shared';
|
||||
|
||||
export function buildIntegrationConfigOptionRegistry(
|
||||
@@ -245,6 +246,14 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
description:
|
||||
'Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.',
|
||||
},
|
||||
{
|
||||
path: 'mpv.launchMode',
|
||||
kind: 'enum',
|
||||
enumValues: MPV_LAUNCH_MODE_VALUES,
|
||||
defaultValue: defaultConfig.mpv.launchMode,
|
||||
description:
|
||||
'Default window state for SubMiner-managed mpv launches.',
|
||||
},
|
||||
{
|
||||
path: 'jellyfin.enabled',
|
||||
kind: 'boolean',
|
||||
|
||||
@@ -159,6 +159,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
title: 'MPV Launcher',
|
||||
description: [
|
||||
'Optional mpv.exe override for Windows playback entry points.',
|
||||
'Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.',
|
||||
'Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.',
|
||||
],
|
||||
key: 'mpv',
|
||||
|
||||
@@ -13,6 +13,17 @@ test('resolveConfig trims configured mpv executable path', () => {
|
||||
assert.deepEqual(warnings, []);
|
||||
});
|
||||
|
||||
test('resolveConfig parses configured mpv launch mode', () => {
|
||||
const { resolved, warnings } = resolveConfig({
|
||||
mpv: {
|
||||
launchMode: 'maximized',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(resolved.mpv.launchMode, 'maximized');
|
||||
assert.deepEqual(warnings, []);
|
||||
});
|
||||
|
||||
test('resolveConfig warns for invalid mpv executable path type', () => {
|
||||
const { resolved, warnings } = resolveConfig({
|
||||
mpv: {
|
||||
@@ -29,3 +40,20 @@ test('resolveConfig warns for invalid mpv executable path type', () => {
|
||||
message: 'Expected string.',
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveConfig warns for invalid mpv launch mode', () => {
|
||||
const { resolved, warnings } = resolveConfig({
|
||||
mpv: {
|
||||
launchMode: 'cinema' as never,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(resolved.mpv.launchMode, 'normal');
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.deepEqual(warnings[0], {
|
||||
path: 'mpv.launchMode',
|
||||
value: 'cinema',
|
||||
fallback: 'normal',
|
||||
message: "Expected one of: 'normal', 'maximized', 'fullscreen'.",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { MPV_LAUNCH_MODE_VALUES, parseMpvLaunchMode } from '../../shared/mpv-launch-mode';
|
||||
import { ResolveContext } from './context';
|
||||
import { asBoolean, asNumber, asString, isObject } from './shared';
|
||||
|
||||
@@ -240,6 +241,18 @@ export function applyIntegrationConfig(context: ResolveContext): void {
|
||||
'Expected string.',
|
||||
);
|
||||
}
|
||||
|
||||
const launchMode = parseMpvLaunchMode(src.mpv.launchMode);
|
||||
if (launchMode !== undefined) {
|
||||
resolved.mpv.launchMode = launchMode;
|
||||
} else if (src.mpv.launchMode !== undefined) {
|
||||
warn(
|
||||
'mpv.launchMode',
|
||||
src.mpv.launchMode,
|
||||
resolved.mpv.launchMode,
|
||||
`Expected one of: ${MPV_LAUNCH_MODE_VALUES.map((value) => `'${value}'`).join(', ')}.`,
|
||||
);
|
||||
}
|
||||
} else if (src.mpv !== undefined) {
|
||||
warn('mpv', src.mpv, resolved.mpv, 'Expected object.');
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { requestSingleInstanceLockEarly } from './main/early-single-instance';
|
||||
import { resolvePackagedFirstRunPluginAssets } from './main/runtime/first-run-setup-plugin';
|
||||
import { createWindowsMpvLaunchDeps, launchWindowsMpv } from './main/runtime/windows-mpv-launch';
|
||||
import { parseMpvLaunchMode } from './shared/mpv-launch-mode';
|
||||
import { runStatsDaemonControlFromProcess } from './stats-daemon-entry';
|
||||
|
||||
const DEFAULT_TEXTHOOKER_PORT = 5174;
|
||||
@@ -36,21 +37,6 @@ function applySanitizedEnv(sanitizedEnv: NodeJS.ProcessEnv): void {
|
||||
}
|
||||
}
|
||||
|
||||
function readConfiguredWindowsMpvExecutablePath(configDir: string): string {
|
||||
const loadResult = loadRawConfigStrict({
|
||||
configDir,
|
||||
configFileJsonc: path.join(configDir, 'config.jsonc'),
|
||||
configFileJson: path.join(configDir, 'config.json'),
|
||||
});
|
||||
if (!loadResult.ok) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return typeof loadResult.config.mpv?.executablePath === 'string'
|
||||
? loadResult.config.mpv.executablePath.trim()
|
||||
: '';
|
||||
}
|
||||
|
||||
function resolveBundledWindowsMpvPluginEntrypoint(): string | undefined {
|
||||
const assets = resolvePackagedFirstRunPluginAssets({
|
||||
dirname: __dirname,
|
||||
@@ -64,6 +50,31 @@ function resolveBundledWindowsMpvPluginEntrypoint(): string | undefined {
|
||||
return path.join(assets.pluginDirSource, 'main.lua');
|
||||
}
|
||||
|
||||
function readConfiguredWindowsMpvLaunch(configDir: string): {
|
||||
executablePath: string;
|
||||
launchMode: 'normal' | 'maximized' | 'fullscreen';
|
||||
} {
|
||||
const loadResult = loadRawConfigStrict({
|
||||
configDir,
|
||||
configFileJsonc: path.join(configDir, 'config.jsonc'),
|
||||
configFileJson: path.join(configDir, 'config.json'),
|
||||
});
|
||||
if (!loadResult.ok) {
|
||||
return {
|
||||
executablePath: '',
|
||||
launchMode: 'normal',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
executablePath:
|
||||
typeof loadResult.config.mpv?.executablePath === 'string'
|
||||
? loadResult.config.mpv.executablePath.trim()
|
||||
: '',
|
||||
launchMode: parseMpvLaunchMode(loadResult.config.mpv?.launchMode) ?? 'normal',
|
||||
};
|
||||
}
|
||||
|
||||
process.argv = normalizeStartupArgv(process.argv, process.env);
|
||||
applySanitizedEnv(sanitizeStartupEnv(process.env));
|
||||
const userDataPath = configureEarlyAppPaths(app);
|
||||
@@ -92,6 +103,7 @@ if (shouldHandleLaunchMpvAtEntry(process.argv, process.env)) {
|
||||
const sanitizedEnv = sanitizeLaunchMpvEnv(process.env);
|
||||
applySanitizedEnv(sanitizedEnv);
|
||||
void app.whenReady().then(async () => {
|
||||
const configuredMpvLaunch = readConfiguredWindowsMpvLaunch(userDataPath);
|
||||
const result = await launchWindowsMpv(
|
||||
normalizeLaunchMpvTargets(process.argv),
|
||||
createWindowsMpvLaunchDeps({
|
||||
@@ -103,7 +115,8 @@ if (shouldHandleLaunchMpvAtEntry(process.argv, process.env)) {
|
||||
normalizeLaunchMpvExtraArgs(process.argv),
|
||||
process.execPath,
|
||||
resolveBundledWindowsMpvPluginEntrypoint(),
|
||||
readConfiguredWindowsMpvExecutablePath(userDataPath),
|
||||
configuredMpvLaunch.executablePath,
|
||||
configuredMpvLaunch.launchMode,
|
||||
);
|
||||
app.exit(result.ok ? 0 : 1);
|
||||
});
|
||||
|
||||
@@ -1052,6 +1052,7 @@ const youtubePlaybackRuntime = createYoutubePlaybackRuntime({
|
||||
undefined,
|
||||
undefined,
|
||||
getResolvedConfig().mpv.executablePath,
|
||||
getResolvedConfig().mpv.launchMode,
|
||||
),
|
||||
waitForYoutubeMpvConnected: (timeoutMs) => waitForYoutubeMpvConnected(timeoutMs),
|
||||
prepareYoutubePlaybackInMpv: (request) => prepareYoutubePlaybackInMpv(request),
|
||||
@@ -2031,6 +2032,7 @@ const {
|
||||
},
|
||||
launchMpvIdleForJellyfinPlaybackMainDeps: {
|
||||
getSocketPath: () => appState.mpvSocketPath,
|
||||
getLaunchMode: () => getResolvedConfig().mpv.launchMode,
|
||||
platform: process.platform,
|
||||
execPath: process.execPath,
|
||||
defaultMpvLogPath: DEFAULT_MPV_LOG_PATH,
|
||||
|
||||
@@ -26,6 +26,7 @@ test('composeJellyfinRuntimeHandlers returns callable jellyfin runtime handlers'
|
||||
},
|
||||
launchMpvIdleForJellyfinPlaybackMainDeps: {
|
||||
getSocketPath: () => '/tmp/test-mpv.sock',
|
||||
getLaunchMode: () => 'normal',
|
||||
platform: 'linux',
|
||||
execPath: process.execPath,
|
||||
defaultMpvLogPath: '/tmp/test-mpv.log',
|
||||
|
||||
@@ -33,6 +33,7 @@ test('launch mpv for jellyfin main deps builder maps callbacks', () => {
|
||||
};
|
||||
const deps = createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler({
|
||||
getSocketPath: () => '/tmp/mpv.sock',
|
||||
getLaunchMode: () => 'fullscreen',
|
||||
platform: 'darwin',
|
||||
execPath: '/tmp/subminer',
|
||||
defaultMpvLogPath: '/tmp/mpv.log',
|
||||
@@ -47,6 +48,7 @@ test('launch mpv for jellyfin main deps builder maps callbacks', () => {
|
||||
})();
|
||||
|
||||
assert.equal(deps.getSocketPath(), '/tmp/mpv.sock');
|
||||
assert.equal(deps.getLaunchMode(), 'fullscreen');
|
||||
assert.equal(deps.platform, 'darwin');
|
||||
assert.equal(deps.execPath, '/tmp/subminer');
|
||||
assert.equal(deps.defaultMpvLogPath, '/tmp/mpv.log');
|
||||
|
||||
@@ -17,6 +17,7 @@ export function createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler(
|
||||
) {
|
||||
return (): LaunchMpvForJellyfinDeps => ({
|
||||
getSocketPath: () => deps.getSocketPath(),
|
||||
getLaunchMode: () => deps.getLaunchMode(),
|
||||
platform: deps.platform,
|
||||
execPath: deps.execPath,
|
||||
defaultMpvLogPath: deps.defaultMpvLogPath,
|
||||
|
||||
@@ -31,6 +31,7 @@ test('createLaunchMpvIdleForJellyfinPlaybackHandler builds expected mpv args', (
|
||||
const logs: string[] = [];
|
||||
const launch = createLaunchMpvIdleForJellyfinPlaybackHandler({
|
||||
getSocketPath: () => '/tmp/subminer.sock',
|
||||
getLaunchMode: () => 'maximized',
|
||||
platform: 'darwin',
|
||||
execPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner',
|
||||
defaultMpvLogPath: '/tmp/mp.log',
|
||||
@@ -49,6 +50,7 @@ test('createLaunchMpvIdleForJellyfinPlaybackHandler builds expected mpv args', (
|
||||
|
||||
launch();
|
||||
assert.equal(spawnedArgs.length, 1);
|
||||
assert.ok(spawnedArgs[0]!.includes('--window-maximized=yes'));
|
||||
assert.ok(spawnedArgs[0]!.includes('--idle=yes'));
|
||||
assert.ok(spawnedArgs[0]!.some((arg) => arg.includes('--input-ipc-server=/tmp/subminer.sock')));
|
||||
assert.ok(logs.some((entry) => entry.includes('Launched mpv for Jellyfin playback')));
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { buildMpvLaunchModeArgs } from '../../shared/mpv-launch-mode';
|
||||
import type { MpvLaunchMode } from '../../types/config';
|
||||
|
||||
type MpvClientLike = {
|
||||
connected: boolean;
|
||||
connect: () => void;
|
||||
@@ -34,6 +37,7 @@ export function createWaitForMpvConnectedHandler(deps: WaitForMpvConnectedDeps)
|
||||
|
||||
export type LaunchMpvForJellyfinDeps = {
|
||||
getSocketPath: () => string;
|
||||
getLaunchMode: () => MpvLaunchMode;
|
||||
platform: NodeJS.Platform;
|
||||
execPath: string;
|
||||
defaultMpvLogPath: string;
|
||||
@@ -58,6 +62,7 @@ export function createLaunchMpvIdleForJellyfinPlaybackHandler(deps: LaunchMpvFor
|
||||
const scriptOpts = `--script-opts=subminer-binary_path=${deps.execPath},subminer-socket_path=${socketPath}`;
|
||||
const mpvArgs = [
|
||||
...deps.defaultMpvArgs,
|
||||
...buildMpvLaunchModeArgs(deps.getLaunchMode()),
|
||||
'--idle=yes',
|
||||
scriptOpts,
|
||||
`--log-file=${deps.defaultMpvLogPath}`,
|
||||
|
||||
@@ -80,6 +80,35 @@ test('buildWindowsMpvLaunchArgs uses explicit SubMiner defaults and targets', ()
|
||||
);
|
||||
});
|
||||
|
||||
test('buildWindowsMpvLaunchArgs inserts maximized launch mode before explicit extra args when configured', () => {
|
||||
assert.deepEqual(
|
||||
buildWindowsMpvLaunchArgs(
|
||||
['C:\\video.mkv'],
|
||||
['--window-maximized=no'],
|
||||
'C:\\SubMiner\\SubMiner.exe',
|
||||
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
||||
'maximized',
|
||||
),
|
||||
[
|
||||
'--player-operation-mode=pseudo-gui',
|
||||
'--force-window=immediate',
|
||||
'--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
||||
'--input-ipc-server=\\\\.\\pipe\\subminer-socket',
|
||||
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
||||
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
||||
'--sub-auto=fuzzy',
|
||||
'--sub-file-paths=subs;subtitles',
|
||||
'--sid=auto',
|
||||
'--secondary-sid=auto',
|
||||
'--secondary-sub-visibility=no',
|
||||
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
||||
'--window-maximized=yes',
|
||||
'--window-maximized=no',
|
||||
'C:\\video.mkv',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('buildWindowsMpvLaunchArgs keeps shortcut-only launches in idle mode', () => {
|
||||
assert.deepEqual(
|
||||
buildWindowsMpvLaunchArgs(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { buildMpvLaunchModeArgs } from '../../shared/mpv-launch-mode';
|
||||
import type { MpvLaunchMode } from '../../types/config';
|
||||
|
||||
export interface WindowsMpvLaunchDeps {
|
||||
getEnv: (name: string) => string | undefined;
|
||||
@@ -89,6 +91,7 @@ export function buildWindowsMpvLaunchArgs(
|
||||
extraArgs: string[] = [],
|
||||
binaryPath?: string,
|
||||
pluginEntrypointPath?: string,
|
||||
launchMode: MpvLaunchMode = 'normal',
|
||||
): string[] {
|
||||
const launchIdle = targets.length === 0;
|
||||
const inputIpcServer =
|
||||
@@ -119,6 +122,7 @@ export function buildWindowsMpvLaunchArgs(
|
||||
'--secondary-sid=auto',
|
||||
'--secondary-sub-visibility=no',
|
||||
...(scriptOpts ? [scriptOpts] : []),
|
||||
...buildMpvLaunchModeArgs(launchMode),
|
||||
...extraArgs,
|
||||
...targets,
|
||||
];
|
||||
@@ -131,6 +135,7 @@ export async function launchWindowsMpv(
|
||||
binaryPath?: string,
|
||||
pluginEntrypointPath?: string,
|
||||
configuredMpvPath?: string,
|
||||
launchMode: MpvLaunchMode = 'normal',
|
||||
): Promise<{ ok: boolean; mpvPath: string }> {
|
||||
const normalizedConfiguredPath = normalizeCandidate(configuredMpvPath);
|
||||
const mpvPath = resolveWindowsMpvPath(deps, normalizedConfiguredPath);
|
||||
@@ -147,7 +152,13 @@ export async function launchWindowsMpv(
|
||||
try {
|
||||
await deps.spawnDetached(
|
||||
mpvPath,
|
||||
buildWindowsMpvLaunchArgs(targets, extraArgs, binaryPath, pluginEntrypointPath),
|
||||
buildWindowsMpvLaunchArgs(
|
||||
targets,
|
||||
extraArgs,
|
||||
binaryPath,
|
||||
pluginEntrypointPath,
|
||||
launchMode,
|
||||
),
|
||||
);
|
||||
return { ok: true, mpvPath };
|
||||
} catch (error) {
|
||||
|
||||
15
src/shared/mpv-launch-mode.test.ts
Normal file
15
src/shared/mpv-launch-mode.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { buildMpvLaunchModeArgs, parseMpvLaunchMode } from './mpv-launch-mode';
|
||||
|
||||
test('parseMpvLaunchMode normalizes valid string values', () => {
|
||||
assert.equal(parseMpvLaunchMode(' fullscreen '), 'fullscreen');
|
||||
assert.equal(parseMpvLaunchMode('MAXIMIZED'), 'maximized');
|
||||
assert.equal(parseMpvLaunchMode('normal'), 'normal');
|
||||
});
|
||||
|
||||
test('buildMpvLaunchModeArgs returns the expected mpv flags', () => {
|
||||
assert.deepEqual(buildMpvLaunchModeArgs('normal'), []);
|
||||
assert.deepEqual(buildMpvLaunchModeArgs('maximized'), ['--window-maximized=yes']);
|
||||
assert.deepEqual(buildMpvLaunchModeArgs('fullscreen'), ['--fullscreen']);
|
||||
});
|
||||
24
src/shared/mpv-launch-mode.ts
Normal file
24
src/shared/mpv-launch-mode.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { MpvLaunchMode } from '../types/config';
|
||||
|
||||
export const MPV_LAUNCH_MODE_VALUES = ['normal', 'maximized', 'fullscreen'] as const satisfies
|
||||
readonly MpvLaunchMode[];
|
||||
|
||||
export function parseMpvLaunchMode(value: unknown): MpvLaunchMode | undefined {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return MPV_LAUNCH_MODE_VALUES.find((candidate) => candidate === normalized);
|
||||
}
|
||||
|
||||
export function buildMpvLaunchModeArgs(launchMode: MpvLaunchMode): string[] {
|
||||
switch (launchMode) {
|
||||
case 'fullscreen':
|
||||
return ['--fullscreen'];
|
||||
case 'maximized':
|
||||
return ['--window-maximized=yes'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -50,8 +50,11 @@ export interface TexthookerConfig {
|
||||
openBrowser?: boolean;
|
||||
}
|
||||
|
||||
export type MpvLaunchMode = 'normal' | 'maximized' | 'fullscreen';
|
||||
|
||||
export interface MpvConfig {
|
||||
executablePath?: string;
|
||||
launchMode?: MpvLaunchMode;
|
||||
}
|
||||
|
||||
export type SubsyncMode = 'auto' | 'manual';
|
||||
@@ -129,6 +132,7 @@ export interface ResolvedConfig {
|
||||
texthooker: Required<TexthookerConfig>;
|
||||
mpv: {
|
||||
executablePath: string;
|
||||
launchMode: MpvLaunchMode;
|
||||
};
|
||||
controller: {
|
||||
enabled: boolean;
|
||||
|
||||
Reference in New Issue
Block a user