mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-08 02:10:30 -07:00
[codex] Replace mpv fullscreen toggle with launch mode config (#48)
Co-authored-by: bee <autumn@skerritt.blog>
This commit is contained in:
4
changes/2026.04.06-mpv-launch-mode.md
Normal file
4
changes/2026.04.06-mpv-launch-mode.md
Normal file
@@ -0,0 +1,4 @@
|
||||
type: changed
|
||||
area: launcher
|
||||
|
||||
- Replaced the launcher-only fullscreen toggle with `mpv.launchMode` so SubMiner-managed mpv playback can start in normal, maximized, or fullscreen mode.
|
||||
@@ -461,10 +461,12 @@
|
||||
// ==========================================
|
||||
// MPV Launcher
|
||||
// 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.
|
||||
// ==========================================
|
||||
"mpv": {
|
||||
"executablePath": "" // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||
"launchMode": "normal" // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
||||
}, // Optional mpv.exe override for Windows playback entry points.
|
||||
|
||||
// ==========================================
|
||||
|
||||
@@ -461,10 +461,12 @@
|
||||
// ==========================================
|
||||
// MPV Launcher
|
||||
// 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.
|
||||
// ==========================================
|
||||
"mpv": {
|
||||
"executablePath": "" // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||
"launchMode": "normal" // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
||||
}, // Optional mpv.exe override for Windows playback entry points.
|
||||
|
||||
// ==========================================
|
||||
|
||||
@@ -58,6 +58,7 @@ function createContext(): LauncherCommandContext {
|
||||
jellyfinServer: '',
|
||||
jellyfinUsername: '',
|
||||
jellyfinPassword: '',
|
||||
launchMode: 'normal',
|
||||
},
|
||||
scriptPath: '/tmp/subminer',
|
||||
scriptName: 'subminer',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
12
launcher/config/mpv-config.ts
Normal file
12
launcher/config/mpv-config.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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', {});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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