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
|
// MPV Launcher
|
||||||
// Optional mpv.exe override for Windows playback entry points.
|
// 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.
|
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"mpv": {
|
"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.
|
}, // Optional mpv.exe override for Windows playback entry points.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -461,10 +461,12 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// MPV Launcher
|
// MPV Launcher
|
||||||
// Optional mpv.exe override for Windows playback entry points.
|
// 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.
|
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"mpv": {
|
"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.
|
}, // Optional mpv.exe override for Windows playback entry points.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function createContext(): LauncherCommandContext {
|
|||||||
jellyfinServer: '',
|
jellyfinServer: '',
|
||||||
jellyfinUsername: '',
|
jellyfinUsername: '',
|
||||||
jellyfinPassword: '',
|
jellyfinPassword: '',
|
||||||
|
launchMode: 'normal',
|
||||||
},
|
},
|
||||||
scriptPath: '/tmp/subminer',
|
scriptPath: '/tmp/subminer',
|
||||||
scriptName: 'subminer',
|
scriptName: 'subminer',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { parseLauncherYoutubeSubgenConfig } from './config/youtube-subgen-config.js';
|
import { parseLauncherYoutubeSubgenConfig } from './config/youtube-subgen-config.js';
|
||||||
import { parseLauncherJellyfinConfig } from './config/jellyfin-config.js';
|
import { parseLauncherJellyfinConfig } from './config/jellyfin-config.js';
|
||||||
|
import { parseLauncherMpvConfig } from './config/mpv-config.js';
|
||||||
import { readExternalYomitanProfilePath } from './config.js';
|
import { readExternalYomitanProfilePath } from './config.js';
|
||||||
import {
|
import {
|
||||||
getPluginConfigCandidates,
|
getPluginConfigCandidates,
|
||||||
@@ -80,6 +81,27 @@ test('parseLauncherJellyfinConfig omits legacy token and user id fields', () =>
|
|||||||
assert.equal('userId' in parsed, false);
|
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', () => {
|
test('parsePluginRuntimeConfigContent reads socket path and startup gate options', () => {
|
||||||
const parsed = parsePluginRuntimeConfigContent(`
|
const parsed = parsePluginRuntimeConfigContent(`
|
||||||
# comment
|
# comment
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { fail } from './log.js';
|
|||||||
import type {
|
import type {
|
||||||
Args,
|
Args,
|
||||||
LauncherJellyfinConfig,
|
LauncherJellyfinConfig,
|
||||||
|
LauncherMpvConfig,
|
||||||
LauncherYoutubeSubgenConfig,
|
LauncherYoutubeSubgenConfig,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
PluginRuntimeConfig,
|
PluginRuntimeConfig,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
} from './config/args-normalizer.js';
|
} from './config/args-normalizer.js';
|
||||||
import { parseCliPrograms, resolveTopLevelCommand } from './config/cli-parser-builder.js';
|
import { parseCliPrograms, resolveTopLevelCommand } from './config/cli-parser-builder.js';
|
||||||
import { parseLauncherJellyfinConfig } from './config/jellyfin-config.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 { readPluginRuntimeConfig as readPluginRuntimeConfigValue } from './config/plugin-runtime-config.js';
|
||||||
import { readLauncherMainConfigObject } from './config/shared-config-reader.js';
|
import { readLauncherMainConfigObject } from './config/shared-config-reader.js';
|
||||||
import { parseLauncherYoutubeSubgenConfig } from './config/youtube-subgen-config.js';
|
import { parseLauncherYoutubeSubgenConfig } from './config/youtube-subgen-config.js';
|
||||||
@@ -44,6 +46,12 @@ export function loadLauncherJellyfinConfig(): LauncherJellyfinConfig {
|
|||||||
return parseLauncherJellyfinConfig(root);
|
return parseLauncherJellyfinConfig(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadLauncherMpvConfig(): LauncherMpvConfig {
|
||||||
|
const root = readLauncherMainConfigObject();
|
||||||
|
if (!root) return {};
|
||||||
|
return parseLauncherMpvConfig(root);
|
||||||
|
}
|
||||||
|
|
||||||
export function hasLauncherExternalYomitanProfileConfig(): boolean {
|
export function hasLauncherExternalYomitanProfileConfig(): boolean {
|
||||||
return readExternalYomitanProfilePath(readLauncherMainConfigObject()) !== null;
|
return readExternalYomitanProfilePath(readLauncherMainConfigObject()) !== null;
|
||||||
}
|
}
|
||||||
@@ -56,9 +64,10 @@ export function parseArgs(
|
|||||||
argv: string[],
|
argv: string[],
|
||||||
scriptName: string,
|
scriptName: string,
|
||||||
launcherConfig: LauncherYoutubeSubgenConfig,
|
launcherConfig: LauncherYoutubeSubgenConfig,
|
||||||
|
launcherMpvConfig: LauncherMpvConfig = {},
|
||||||
): Args {
|
): Args {
|
||||||
const topLevelCommand = resolveTopLevelCommand(argv);
|
const topLevelCommand = resolveTopLevelCommand(argv);
|
||||||
const parsed = createDefaultArgs(launcherConfig);
|
const parsed = createDefaultArgs(launcherConfig, launcherMpvConfig);
|
||||||
|
|
||||||
if (topLevelCommand && (topLevelCommand.name === 'app' || topLevelCommand.name === 'bin')) {
|
if (topLevelCommand && (topLevelCommand.name === 'app' || topLevelCommand.name === 'bin')) {
|
||||||
parsed.appPassthrough = true;
|
parsed.appPassthrough = true;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fail } from '../log.js';
|
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 {
|
import {
|
||||||
DEFAULT_JIMAKU_API_BASE_URL,
|
DEFAULT_JIMAKU_API_BASE_URL,
|
||||||
DEFAULT_YOUTUBE_PRIMARY_SUB_LANGS,
|
DEFAULT_YOUTUBE_PRIMARY_SUB_LANGS,
|
||||||
@@ -83,7 +89,10 @@ function parseDictionaryTarget(value: string): string {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig): Args {
|
export function createDefaultArgs(
|
||||||
|
launcherConfig: LauncherYoutubeSubgenConfig,
|
||||||
|
mpvConfig: LauncherMpvConfig = {},
|
||||||
|
): Args {
|
||||||
const configuredSecondaryLangs = uniqueNormalizedLangCodes(
|
const configuredSecondaryLangs = uniqueNormalizedLangCodes(
|
||||||
launcherConfig.secondarySubLanguages ?? [],
|
launcherConfig.secondarySubLanguages ?? [],
|
||||||
);
|
);
|
||||||
@@ -148,6 +157,7 @@ export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig):
|
|||||||
jellyfinServer: '',
|
jellyfinServer: '',
|
||||||
jellyfinUsername: '',
|
jellyfinUsername: '',
|
||||||
jellyfinPassword: '',
|
jellyfinPassword: '',
|
||||||
|
launchMode: mpvConfig.launchMode ?? 'normal',
|
||||||
youtubePrimarySubLangs: primarySubLangs,
|
youtubePrimarySubLangs: primarySubLangs,
|
||||||
youtubeSecondarySubLangs: secondarySubLangs,
|
youtubeSecondarySubLangs: secondarySubLangs,
|
||||||
youtubeAudioLangs,
|
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 path from 'node:path';
|
||||||
import {
|
import {
|
||||||
loadLauncherJellyfinConfig,
|
loadLauncherJellyfinConfig,
|
||||||
|
loadLauncherMpvConfig,
|
||||||
loadLauncherYoutubeSubgenConfig,
|
loadLauncherYoutubeSubgenConfig,
|
||||||
parseArgs,
|
parseArgs,
|
||||||
readPluginRuntimeConfig,
|
readPluginRuntimeConfig,
|
||||||
@@ -52,7 +53,8 @@ async function main(): Promise<void> {
|
|||||||
const scriptPath = process.argv[1] || 'subminer';
|
const scriptPath = process.argv[1] || 'subminer';
|
||||||
const scriptName = path.basename(scriptPath);
|
const scriptName = path.basename(scriptPath);
|
||||||
const launcherConfig = loadLauncherYoutubeSubgenConfig();
|
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 pluginRuntimeConfig = readPluginRuntimeConfig(args.logLevel);
|
||||||
const appPath = findAppBinary(scriptPath);
|
const appPath = findAppBinary(scriptPath);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import net from 'node:net';
|
|||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import type { Args } from './types';
|
import type { Args } from './types';
|
||||||
import {
|
import {
|
||||||
|
buildConfiguredMpvDefaultArgs,
|
||||||
buildMpvBackendArgs,
|
buildMpvBackendArgs,
|
||||||
buildMpvEnv,
|
buildMpvEnv,
|
||||||
cleanupPlaybackSession,
|
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', () => {
|
test('launchTexthookerOnly exits non-zero when app binary cannot be spawned', () => {
|
||||||
const error = withProcessExitIntercept(() => {
|
const error = withProcessExitIntercept(() => {
|
||||||
launchTexthookerOnly('/definitely-missing-subminer-binary', makeArgs());
|
launchTexthookerOnly('/definitely-missing-subminer-binary', makeArgs());
|
||||||
@@ -401,6 +429,7 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
|
|||||||
jellyfinServer: '',
|
jellyfinServer: '',
|
||||||
jellyfinUsername: '',
|
jellyfinUsername: '',
|
||||||
jellyfinPassword: '',
|
jellyfinPassword: '',
|
||||||
|
launchMode: 'normal',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import net from 'node:net';
|
import net from 'node:net';
|
||||||
import { spawn, spawnSync } from 'node:child_process';
|
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 type { LogLevel, Backend, Args, MpvTrack } from './types.js';
|
||||||
import { DEFAULT_MPV_SUBMINER_ARGS, DEFAULT_YOUTUBE_YTDL_FORMAT } from './types.js';
|
import { DEFAULT_MPV_SUBMINER_ARGS, DEFAULT_YOUTUBE_YTDL_FORMAT } from './types.js';
|
||||||
import { appendToAppLog, getAppLogPath, log, fail, getMpvLogPath } from './log.js';
|
import { appendToAppLog, getAppLogPath, log, fail, getMpvLogPath } from './log.js';
|
||||||
@@ -673,9 +674,7 @@ export async function startMpv(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mpvArgs: string[] = [];
|
const mpvArgs: string[] = [];
|
||||||
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
|
mpvArgs.push(...buildConfiguredMpvDefaultArgs(args));
|
||||||
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
|
|
||||||
mpvArgs.push(...buildMpvBackendArgs(args));
|
|
||||||
if (targetKind === 'url' && isYoutubeTarget(target)) {
|
if (targetKind === 'url' && isYoutubeTarget(target)) {
|
||||||
log('info', args.logLevel, 'Applying URL playback options');
|
log('info', args.logLevel, 'Applying URL playback options');
|
||||||
mpvArgs.push('--ytdl=yes');
|
mpvArgs.push('--ytdl=yes');
|
||||||
@@ -703,7 +702,6 @@ export async function startMpv(
|
|||||||
if (args.mpvArgs) {
|
if (args.mpvArgs) {
|
||||||
mpvArgs.push(...parseMpvArgString(args.mpvArgs));
|
mpvArgs.push(...parseMpvArgString(args.mpvArgs));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preloadedSubtitles?.primaryPath) {
|
if (preloadedSubtitles?.primaryPath) {
|
||||||
mpvArgs.push(`--sub-file=${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'];
|
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 {
|
function appendCapturedAppOutput(kind: 'STDOUT' | 'STDERR', chunk: string): void {
|
||||||
const normalized = chunk.replace(/\r\n/g, '\n');
|
const normalized = chunk.replace(/\r\n/g, '\n');
|
||||||
for (const line of normalized.split('\n')) {
|
for (const line of normalized.split('\n')) {
|
||||||
@@ -1209,10 +1219,7 @@ export function launchMpvIdleDetached(
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
const mpvArgs: string[] = [];
|
const mpvArgs: string[] = buildConfiguredMpvDefaultArgs(args);
|
||||||
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
|
|
||||||
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
|
|
||||||
mpvArgs.push(...buildMpvBackendArgs(args));
|
|
||||||
if (args.mpvArgs) {
|
if (args.mpvArgs) {
|
||||||
mpvArgs.push(...parseMpvArgString(args.mpvArgs));
|
mpvArgs.push(...parseMpvArgString(args.mpvArgs));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ test('parseArgs maps mpv idle action', () => {
|
|||||||
assert.equal(parsed.mpvStatus, false);
|
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', () => {
|
test('parseArgs maps dictionary command and log-level override', () => {
|
||||||
const parsed = parseArgs(['dictionary', '.', '--log-level', 'debug'], 'subminer', {});
|
const parsed = parseArgs(['dictionary', '.', '--log-level', 'debug'], 'subminer', {});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
|
import type { MpvLaunchMode } from '../src/types/config.js';
|
||||||
import { resolveDefaultLogFilePath } from '../src/shared/log-files.js';
|
import { resolveDefaultLogFilePath } from '../src/shared/log-files.js';
|
||||||
export { VIDEO_EXTENSIONS } from '../src/shared/video-extensions.js';
|
export { VIDEO_EXTENSIONS } from '../src/shared/video-extensions.js';
|
||||||
|
|
||||||
@@ -140,6 +141,7 @@ export interface Args {
|
|||||||
jellyfinServer: string;
|
jellyfinServer: string;
|
||||||
jellyfinUsername: string;
|
jellyfinUsername: string;
|
||||||
jellyfinPassword: string;
|
jellyfinPassword: string;
|
||||||
|
launchMode: MpvLaunchMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LauncherYoutubeSubgenConfig {
|
export interface LauncherYoutubeSubgenConfig {
|
||||||
@@ -167,6 +169,10 @@ export interface LauncherJellyfinConfig {
|
|||||||
iconCacheDir?: string;
|
iconCacheDir?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LauncherMpvConfig {
|
||||||
|
launchMode?: MpvLaunchMode;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginRuntimeConfig {
|
export interface PluginRuntimeConfig {
|
||||||
socketPath: string;
|
socketPath: string;
|
||||||
autoStart: boolean;
|
autoStart: boolean;
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
|||||||
},
|
},
|
||||||
mpv: {
|
mpv: {
|
||||||
executablePath: '',
|
executablePath: '',
|
||||||
|
launchMode: 'normal',
|
||||||
},
|
},
|
||||||
anilist: {
|
anilist: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ test('config option registry includes critical paths and has unique entries', ()
|
|||||||
'anilist.characterDictionary.enabled',
|
'anilist.characterDictionary.enabled',
|
||||||
'anilist.characterDictionary.collapsibleSections.description',
|
'anilist.characterDictionary.collapsibleSections.description',
|
||||||
'mpv.executablePath',
|
'mpv.executablePath',
|
||||||
|
'mpv.launchMode',
|
||||||
'yomitan.externalProfilePath',
|
'yomitan.externalProfilePath',
|
||||||
'immersionTracking.enabled',
|
'immersionTracking.enabled',
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ResolvedConfig } from '../../types/config';
|
import { ResolvedConfig } from '../../types/config';
|
||||||
|
import { MPV_LAUNCH_MODE_VALUES } from '../../shared/mpv-launch-mode';
|
||||||
import { ConfigOptionRegistryEntry, RuntimeOptionRegistryEntry } from './shared';
|
import { ConfigOptionRegistryEntry, RuntimeOptionRegistryEntry } from './shared';
|
||||||
|
|
||||||
export function buildIntegrationConfigOptionRegistry(
|
export function buildIntegrationConfigOptionRegistry(
|
||||||
@@ -245,6 +246,14 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
description:
|
description:
|
||||||
'Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.',
|
'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',
|
path: 'jellyfin.enabled',
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
|||||||
title: 'MPV Launcher',
|
title: 'MPV Launcher',
|
||||||
description: [
|
description: [
|
||||||
'Optional mpv.exe override for Windows playback entry points.',
|
'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.',
|
'Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.',
|
||||||
],
|
],
|
||||||
key: 'mpv',
|
key: 'mpv',
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ test('resolveConfig trims configured mpv executable path', () => {
|
|||||||
assert.deepEqual(warnings, []);
|
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', () => {
|
test('resolveConfig warns for invalid mpv executable path type', () => {
|
||||||
const { resolved, warnings } = resolveConfig({
|
const { resolved, warnings } = resolveConfig({
|
||||||
mpv: {
|
mpv: {
|
||||||
@@ -29,3 +40,20 @@ test('resolveConfig warns for invalid mpv executable path type', () => {
|
|||||||
message: 'Expected string.',
|
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 os from 'node:os';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
import { MPV_LAUNCH_MODE_VALUES, parseMpvLaunchMode } from '../../shared/mpv-launch-mode';
|
||||||
import { ResolveContext } from './context';
|
import { ResolveContext } from './context';
|
||||||
import { asBoolean, asNumber, asString, isObject } from './shared';
|
import { asBoolean, asNumber, asString, isObject } from './shared';
|
||||||
|
|
||||||
@@ -240,6 +241,18 @@ export function applyIntegrationConfig(context: ResolveContext): void {
|
|||||||
'Expected string.',
|
'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) {
|
} else if (src.mpv !== undefined) {
|
||||||
warn('mpv', src.mpv, resolved.mpv, 'Expected object.');
|
warn('mpv', src.mpv, resolved.mpv, 'Expected object.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { requestSingleInstanceLockEarly } from './main/early-single-instance';
|
import { requestSingleInstanceLockEarly } from './main/early-single-instance';
|
||||||
import { resolvePackagedFirstRunPluginAssets } from './main/runtime/first-run-setup-plugin';
|
import { resolvePackagedFirstRunPluginAssets } from './main/runtime/first-run-setup-plugin';
|
||||||
import { createWindowsMpvLaunchDeps, launchWindowsMpv } from './main/runtime/windows-mpv-launch';
|
import { createWindowsMpvLaunchDeps, launchWindowsMpv } from './main/runtime/windows-mpv-launch';
|
||||||
|
import { parseMpvLaunchMode } from './shared/mpv-launch-mode';
|
||||||
import { runStatsDaemonControlFromProcess } from './stats-daemon-entry';
|
import { runStatsDaemonControlFromProcess } from './stats-daemon-entry';
|
||||||
|
|
||||||
const DEFAULT_TEXTHOOKER_PORT = 5174;
|
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 {
|
function resolveBundledWindowsMpvPluginEntrypoint(): string | undefined {
|
||||||
const assets = resolvePackagedFirstRunPluginAssets({
|
const assets = resolvePackagedFirstRunPluginAssets({
|
||||||
dirname: __dirname,
|
dirname: __dirname,
|
||||||
@@ -64,6 +50,31 @@ function resolveBundledWindowsMpvPluginEntrypoint(): string | undefined {
|
|||||||
return path.join(assets.pluginDirSource, 'main.lua');
|
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);
|
process.argv = normalizeStartupArgv(process.argv, process.env);
|
||||||
applySanitizedEnv(sanitizeStartupEnv(process.env));
|
applySanitizedEnv(sanitizeStartupEnv(process.env));
|
||||||
const userDataPath = configureEarlyAppPaths(app);
|
const userDataPath = configureEarlyAppPaths(app);
|
||||||
@@ -92,6 +103,7 @@ if (shouldHandleLaunchMpvAtEntry(process.argv, process.env)) {
|
|||||||
const sanitizedEnv = sanitizeLaunchMpvEnv(process.env);
|
const sanitizedEnv = sanitizeLaunchMpvEnv(process.env);
|
||||||
applySanitizedEnv(sanitizedEnv);
|
applySanitizedEnv(sanitizedEnv);
|
||||||
void app.whenReady().then(async () => {
|
void app.whenReady().then(async () => {
|
||||||
|
const configuredMpvLaunch = readConfiguredWindowsMpvLaunch(userDataPath);
|
||||||
const result = await launchWindowsMpv(
|
const result = await launchWindowsMpv(
|
||||||
normalizeLaunchMpvTargets(process.argv),
|
normalizeLaunchMpvTargets(process.argv),
|
||||||
createWindowsMpvLaunchDeps({
|
createWindowsMpvLaunchDeps({
|
||||||
@@ -103,7 +115,8 @@ if (shouldHandleLaunchMpvAtEntry(process.argv, process.env)) {
|
|||||||
normalizeLaunchMpvExtraArgs(process.argv),
|
normalizeLaunchMpvExtraArgs(process.argv),
|
||||||
process.execPath,
|
process.execPath,
|
||||||
resolveBundledWindowsMpvPluginEntrypoint(),
|
resolveBundledWindowsMpvPluginEntrypoint(),
|
||||||
readConfiguredWindowsMpvExecutablePath(userDataPath),
|
configuredMpvLaunch.executablePath,
|
||||||
|
configuredMpvLaunch.launchMode,
|
||||||
);
|
);
|
||||||
app.exit(result.ok ? 0 : 1);
|
app.exit(result.ok ? 0 : 1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1052,6 +1052,7 @@ const youtubePlaybackRuntime = createYoutubePlaybackRuntime({
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
getResolvedConfig().mpv.executablePath,
|
getResolvedConfig().mpv.executablePath,
|
||||||
|
getResolvedConfig().mpv.launchMode,
|
||||||
),
|
),
|
||||||
waitForYoutubeMpvConnected: (timeoutMs) => waitForYoutubeMpvConnected(timeoutMs),
|
waitForYoutubeMpvConnected: (timeoutMs) => waitForYoutubeMpvConnected(timeoutMs),
|
||||||
prepareYoutubePlaybackInMpv: (request) => prepareYoutubePlaybackInMpv(request),
|
prepareYoutubePlaybackInMpv: (request) => prepareYoutubePlaybackInMpv(request),
|
||||||
@@ -2031,6 +2032,7 @@ const {
|
|||||||
},
|
},
|
||||||
launchMpvIdleForJellyfinPlaybackMainDeps: {
|
launchMpvIdleForJellyfinPlaybackMainDeps: {
|
||||||
getSocketPath: () => appState.mpvSocketPath,
|
getSocketPath: () => appState.mpvSocketPath,
|
||||||
|
getLaunchMode: () => getResolvedConfig().mpv.launchMode,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
execPath: process.execPath,
|
execPath: process.execPath,
|
||||||
defaultMpvLogPath: DEFAULT_MPV_LOG_PATH,
|
defaultMpvLogPath: DEFAULT_MPV_LOG_PATH,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ test('composeJellyfinRuntimeHandlers returns callable jellyfin runtime handlers'
|
|||||||
},
|
},
|
||||||
launchMpvIdleForJellyfinPlaybackMainDeps: {
|
launchMpvIdleForJellyfinPlaybackMainDeps: {
|
||||||
getSocketPath: () => '/tmp/test-mpv.sock',
|
getSocketPath: () => '/tmp/test-mpv.sock',
|
||||||
|
getLaunchMode: () => 'normal',
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
execPath: process.execPath,
|
execPath: process.execPath,
|
||||||
defaultMpvLogPath: '/tmp/test-mpv.log',
|
defaultMpvLogPath: '/tmp/test-mpv.log',
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ test('launch mpv for jellyfin main deps builder maps callbacks', () => {
|
|||||||
};
|
};
|
||||||
const deps = createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler({
|
const deps = createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler({
|
||||||
getSocketPath: () => '/tmp/mpv.sock',
|
getSocketPath: () => '/tmp/mpv.sock',
|
||||||
|
getLaunchMode: () => 'fullscreen',
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
execPath: '/tmp/subminer',
|
execPath: '/tmp/subminer',
|
||||||
defaultMpvLogPath: '/tmp/mpv.log',
|
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.getSocketPath(), '/tmp/mpv.sock');
|
||||||
|
assert.equal(deps.getLaunchMode(), 'fullscreen');
|
||||||
assert.equal(deps.platform, 'darwin');
|
assert.equal(deps.platform, 'darwin');
|
||||||
assert.equal(deps.execPath, '/tmp/subminer');
|
assert.equal(deps.execPath, '/tmp/subminer');
|
||||||
assert.equal(deps.defaultMpvLogPath, '/tmp/mpv.log');
|
assert.equal(deps.defaultMpvLogPath, '/tmp/mpv.log');
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export function createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler(
|
|||||||
) {
|
) {
|
||||||
return (): LaunchMpvForJellyfinDeps => ({
|
return (): LaunchMpvForJellyfinDeps => ({
|
||||||
getSocketPath: () => deps.getSocketPath(),
|
getSocketPath: () => deps.getSocketPath(),
|
||||||
|
getLaunchMode: () => deps.getLaunchMode(),
|
||||||
platform: deps.platform,
|
platform: deps.platform,
|
||||||
execPath: deps.execPath,
|
execPath: deps.execPath,
|
||||||
defaultMpvLogPath: deps.defaultMpvLogPath,
|
defaultMpvLogPath: deps.defaultMpvLogPath,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ test('createLaunchMpvIdleForJellyfinPlaybackHandler builds expected mpv args', (
|
|||||||
const logs: string[] = [];
|
const logs: string[] = [];
|
||||||
const launch = createLaunchMpvIdleForJellyfinPlaybackHandler({
|
const launch = createLaunchMpvIdleForJellyfinPlaybackHandler({
|
||||||
getSocketPath: () => '/tmp/subminer.sock',
|
getSocketPath: () => '/tmp/subminer.sock',
|
||||||
|
getLaunchMode: () => 'maximized',
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
execPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner',
|
execPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner',
|
||||||
defaultMpvLogPath: '/tmp/mp.log',
|
defaultMpvLogPath: '/tmp/mp.log',
|
||||||
@@ -49,6 +50,7 @@ test('createLaunchMpvIdleForJellyfinPlaybackHandler builds expected mpv args', (
|
|||||||
|
|
||||||
launch();
|
launch();
|
||||||
assert.equal(spawnedArgs.length, 1);
|
assert.equal(spawnedArgs.length, 1);
|
||||||
|
assert.ok(spawnedArgs[0]!.includes('--window-maximized=yes'));
|
||||||
assert.ok(spawnedArgs[0]!.includes('--idle=yes'));
|
assert.ok(spawnedArgs[0]!.includes('--idle=yes'));
|
||||||
assert.ok(spawnedArgs[0]!.some((arg) => arg.includes('--input-ipc-server=/tmp/subminer.sock')));
|
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')));
|
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 = {
|
type MpvClientLike = {
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
connect: () => void;
|
connect: () => void;
|
||||||
@@ -34,6 +37,7 @@ export function createWaitForMpvConnectedHandler(deps: WaitForMpvConnectedDeps)
|
|||||||
|
|
||||||
export type LaunchMpvForJellyfinDeps = {
|
export type LaunchMpvForJellyfinDeps = {
|
||||||
getSocketPath: () => string;
|
getSocketPath: () => string;
|
||||||
|
getLaunchMode: () => MpvLaunchMode;
|
||||||
platform: NodeJS.Platform;
|
platform: NodeJS.Platform;
|
||||||
execPath: string;
|
execPath: string;
|
||||||
defaultMpvLogPath: 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 scriptOpts = `--script-opts=subminer-binary_path=${deps.execPath},subminer-socket_path=${socketPath}`;
|
||||||
const mpvArgs = [
|
const mpvArgs = [
|
||||||
...deps.defaultMpvArgs,
|
...deps.defaultMpvArgs,
|
||||||
|
...buildMpvLaunchModeArgs(deps.getLaunchMode()),
|
||||||
'--idle=yes',
|
'--idle=yes',
|
||||||
scriptOpts,
|
scriptOpts,
|
||||||
`--log-file=${deps.defaultMpvLogPath}`,
|
`--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', () => {
|
test('buildWindowsMpvLaunchArgs keeps shortcut-only launches in idle mode', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
buildWindowsMpvLaunchArgs(
|
buildWindowsMpvLaunchArgs(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { spawn, spawnSync } from 'node:child_process';
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
import { buildMpvLaunchModeArgs } from '../../shared/mpv-launch-mode';
|
||||||
|
import type { MpvLaunchMode } from '../../types/config';
|
||||||
|
|
||||||
export interface WindowsMpvLaunchDeps {
|
export interface WindowsMpvLaunchDeps {
|
||||||
getEnv: (name: string) => string | undefined;
|
getEnv: (name: string) => string | undefined;
|
||||||
@@ -89,6 +91,7 @@ export function buildWindowsMpvLaunchArgs(
|
|||||||
extraArgs: string[] = [],
|
extraArgs: string[] = [],
|
||||||
binaryPath?: string,
|
binaryPath?: string,
|
||||||
pluginEntrypointPath?: string,
|
pluginEntrypointPath?: string,
|
||||||
|
launchMode: MpvLaunchMode = 'normal',
|
||||||
): string[] {
|
): string[] {
|
||||||
const launchIdle = targets.length === 0;
|
const launchIdle = targets.length === 0;
|
||||||
const inputIpcServer =
|
const inputIpcServer =
|
||||||
@@ -119,6 +122,7 @@ export function buildWindowsMpvLaunchArgs(
|
|||||||
'--secondary-sid=auto',
|
'--secondary-sid=auto',
|
||||||
'--secondary-sub-visibility=no',
|
'--secondary-sub-visibility=no',
|
||||||
...(scriptOpts ? [scriptOpts] : []),
|
...(scriptOpts ? [scriptOpts] : []),
|
||||||
|
...buildMpvLaunchModeArgs(launchMode),
|
||||||
...extraArgs,
|
...extraArgs,
|
||||||
...targets,
|
...targets,
|
||||||
];
|
];
|
||||||
@@ -131,6 +135,7 @@ export async function launchWindowsMpv(
|
|||||||
binaryPath?: string,
|
binaryPath?: string,
|
||||||
pluginEntrypointPath?: string,
|
pluginEntrypointPath?: string,
|
||||||
configuredMpvPath?: string,
|
configuredMpvPath?: string,
|
||||||
|
launchMode: MpvLaunchMode = 'normal',
|
||||||
): Promise<{ ok: boolean; mpvPath: string }> {
|
): Promise<{ ok: boolean; mpvPath: string }> {
|
||||||
const normalizedConfiguredPath = normalizeCandidate(configuredMpvPath);
|
const normalizedConfiguredPath = normalizeCandidate(configuredMpvPath);
|
||||||
const mpvPath = resolveWindowsMpvPath(deps, normalizedConfiguredPath);
|
const mpvPath = resolveWindowsMpvPath(deps, normalizedConfiguredPath);
|
||||||
@@ -147,7 +152,13 @@ export async function launchWindowsMpv(
|
|||||||
try {
|
try {
|
||||||
await deps.spawnDetached(
|
await deps.spawnDetached(
|
||||||
mpvPath,
|
mpvPath,
|
||||||
buildWindowsMpvLaunchArgs(targets, extraArgs, binaryPath, pluginEntrypointPath),
|
buildWindowsMpvLaunchArgs(
|
||||||
|
targets,
|
||||||
|
extraArgs,
|
||||||
|
binaryPath,
|
||||||
|
pluginEntrypointPath,
|
||||||
|
launchMode,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return { ok: true, mpvPath };
|
return { ok: true, mpvPath };
|
||||||
} catch (error) {
|
} 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;
|
openBrowser?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MpvLaunchMode = 'normal' | 'maximized' | 'fullscreen';
|
||||||
|
|
||||||
export interface MpvConfig {
|
export interface MpvConfig {
|
||||||
executablePath?: string;
|
executablePath?: string;
|
||||||
|
launchMode?: MpvLaunchMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubsyncMode = 'auto' | 'manual';
|
export type SubsyncMode = 'auto' | 'manual';
|
||||||
@@ -129,6 +132,7 @@ export interface ResolvedConfig {
|
|||||||
texthooker: Required<TexthookerConfig>;
|
texthooker: Required<TexthookerConfig>;
|
||||||
mpv: {
|
mpv: {
|
||||||
executablePath: string;
|
executablePath: string;
|
||||||
|
launchMode: MpvLaunchMode;
|
||||||
};
|
};
|
||||||
controller: {
|
controller: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user