diff --git a/changes/mpv-profile-config.md b/changes/mpv-profile-config.md new file mode 100644 index 00000000..ddc5be68 --- /dev/null +++ b/changes/mpv-profile-config.md @@ -0,0 +1,4 @@ +type: added +area: launcher + +- Added `mpv.profile` config and settings support for passing an mpv profile to SubMiner-managed mpv launches. diff --git a/config.example.jsonc b/config.example.jsonc index 393e2f29..47c9cd3e 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -611,11 +611,13 @@ // Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin. // autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display. // Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback. + // Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none. // 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. "launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen + "profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile. "socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin. "backend": "auto", // Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform. Values: auto | hyprland | sway | x11 | macos | windows "autoStartSubMiner": true, // Start SubMiner in the background when SubMiner-managed mpv loads a file. Values: true | false diff --git a/docs-site/configuration.md b/docs-site/configuration.md index aba9ea72..41728451 100644 --- a/docs-site/configuration.md +++ b/docs-site/configuration.md @@ -178,7 +178,7 @@ The configuration file includes several main sections: - [**Discord Rich Presence**](#discord-rich-presence) - Optional Discord activity card updates - [**Immersion Tracking**](#immersion-tracking) - Track subtitle sessions and mining activity in SQLite - [**Stats Dashboard**](#stats-dashboard) - Local dashboard and overlay for immersion progress -- [**MPV Launcher**](#mpv-launcher) - mpv executable path and window launch mode +- [**MPV Launcher**](#mpv-launcher) - mpv executable path, profile, and window launch mode - [**YouTube Playback Settings**](#youtube-playback-settings) - Defaults for YouTube subtitle loading - [**Updates**](#updates) - Automatic update checks, notifications, and prerelease testing @@ -1455,12 +1455,13 @@ Usage notes: ### MPV Launcher -Configure the mpv executable and window state for SubMiner-managed mpv launches (launcher playback, Windows `--launch-mpv`, and Jellyfin idle mpv startup): +Configure the mpv executable, profile, and window state for SubMiner-managed mpv launches (launcher playback, Windows `--launch-mpv`, and Jellyfin idle mpv startup): ```json { "mpv": { "executablePath": "", + "profile": "", "launchMode": "normal" } } @@ -1469,8 +1470,11 @@ Configure the mpv executable and window state for SubMiner-managed mpv launches | Option | Values | Description | | ---------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `executablePath` | string | Absolute path to `mpv.exe` for Windows launch flows. Leave empty to auto-discover from `SUBMINER_MPV_PATH` or `PATH` (default `""`) | +| `profile` | string | mpv profile name passed as `--profile=`. Leave empty to pass no profile (default `""`) | | `launchMode` | `"normal"` \| `"maximized"` \| `"fullscreen"` | Window state when SubMiner spawns mpv (default `"normal"`) | +If `mpv.profile` is configured and the launcher also receives `--profile`, SubMiner passes both as a comma-separated mpv profile list. + Launch mode behavior: - **`normal`** — mpv opens at its default window size with no extra flags. diff --git a/docs-site/public/config.example.jsonc b/docs-site/public/config.example.jsonc index 393e2f29..47c9cd3e 100644 --- a/docs-site/public/config.example.jsonc +++ b/docs-site/public/config.example.jsonc @@ -611,11 +611,13 @@ // Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin. // autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display. // Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback. + // Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none. // 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. "launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen + "profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile. "socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin. "backend": "auto", // Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform. Values: auto | hyprland | sway | x11 | macos | windows "autoStartSubMiner": true, // Start SubMiner in the background when SubMiner-managed mpv loads a file. Values: true | false diff --git a/launcher/config-domain-parsers.test.ts b/launcher/config-domain-parsers.test.ts index 7480a8f3..1e5ce357 100644 --- a/launcher/config-domain-parsers.test.ts +++ b/launcher/config-domain-parsers.test.ts @@ -229,6 +229,29 @@ test('getDefaultSocketPath returns Windows named pipe default', () => { assert.equal(getDefaultSocketPath('win32'), '\\\\.\\pipe\\subminer-socket'); }); +test('parseLauncherMpvConfig reads configured mpv profile', () => { + assert.deepEqual( + parseLauncherMpvConfig({ + mpv: { + profile: ' anime ', + }, + }), + { + launchMode: undefined, + socketPath: undefined, + backend: undefined, + autoStartSubMiner: undefined, + pauseUntilOverlayReady: undefined, + subminerBinaryPath: undefined, + profile: 'anime', + aniskipEnabled: undefined, + aniskipButtonKey: undefined, + }, + ); + + assert.equal(parseLauncherMpvConfig({ mpv: { profile: ' ' } }).profile, undefined); +}); + test('readExternalYomitanProfilePath detects configured external profile paths', () => { assert.equal( readExternalYomitanProfilePath({ diff --git a/launcher/config/args-normalizer.test.ts b/launcher/config/args-normalizer.test.ts index 2cf4f26f..116fa183 100644 --- a/launcher/config/args-normalizer.test.ts +++ b/launcher/config/args-normalizer.test.ts @@ -45,6 +45,20 @@ test('createDefaultArgs normalizes configured language codes and env thread over } }); +test('createDefaultArgs seeds mpv profile from launcher config', () => { + const parsed = createDefaultArgs({}, { profile: 'anime' }); + + assert.equal(parsed.profile, 'anime'); +}); + +test('applyRootOptionsToArgs appends CLI mpv profile to configured profile', () => { + const parsed = createDefaultArgs({}, { profile: 'anime' }); + + applyRootOptionsToArgs(parsed, { profile: 'hdr' }, undefined); + + assert.equal(parsed.profile, 'anime,hdr'); +}); + test('applyRootOptionsToArgs maps file, directory, and url targets', () => { withTempDir((dir) => { const filePath = path.join(dir, 'movie.mkv'); diff --git a/launcher/config/args-normalizer.ts b/launcher/config/args-normalizer.ts index e7f87b26..9c29eaa7 100644 --- a/launcher/config/args-normalizer.ts +++ b/launcher/config/args-normalizer.ts @@ -68,6 +68,12 @@ function parseBackend(value: string): Backend { fail(`Invalid backend: ${value} (must be auto, hyprland, sway, x11, macos, or windows)`); } +function appendMpvProfile(current: string, next: string): string { + const trimmed = next.trim(); + if (!trimmed) return current; + return current ? `${current},${trimmed}` : trimmed; +} + function parseDictionaryTarget(value: string): string { const trimmed = value.trim(); if (!trimmed) { @@ -121,7 +127,7 @@ export function createDefaultArgs( backend: mpvConfig.backend ?? 'auto', directory: '.', recursive: false, - profile: '', + profile: mpvConfig.profile ?? '', startOverlay: false, whisperBin: process.env.SUBMINER_WHISPER_BIN || launcherConfig.whisperBin || '', whisperModel: process.env.SUBMINER_WHISPER_MODEL || launcherConfig.whisperModel || '', @@ -215,7 +221,8 @@ export function applyRootOptionsToArgs( if (typeof options.backend === 'string') parsed.backend = parseBackend(options.backend); if (typeof options.directory === 'string') parsed.directory = options.directory; if (options.recursive === true) parsed.recursive = true; - if (typeof options.profile === 'string') parsed.profile = options.profile; + if (typeof options.profile === 'string') + parsed.profile = appendMpvProfile(parsed.profile, options.profile); if (options.start === true) parsed.startOverlay = true; if (typeof options.logLevel === 'string') parsed.logLevel = parseLogLevel(options.logLevel); if (typeof options.passwordStore === 'string') parsed.passwordStore = options.passwordStore; diff --git a/launcher/config/mpv-config.ts b/launcher/config/mpv-config.ts index 3a8b5cb0..507d3733 100644 --- a/launcher/config/mpv-config.ts +++ b/launcher/config/mpv-config.ts @@ -31,6 +31,7 @@ export function parseLauncherMpvConfig(root: Record): LauncherM return { launchMode: parseMpvLaunchMode(mpv.launchMode), + profile: parseNonEmptyString(mpv.profile), socketPath: parseNonEmptyString(mpv.socketPath), backend: parseBackend(mpv.backend), autoStartSubMiner: diff --git a/launcher/mpv.test.ts b/launcher/mpv.test.ts index ea62a4e5..5f4b1232 100644 --- a/launcher/mpv.test.ts +++ b/launcher/mpv.test.ts @@ -268,6 +268,18 @@ test('buildConfiguredMpvDefaultArgs appends maximized launch mode to configured }); }); +test('buildConfiguredMpvDefaultArgs passes configured mpv profile before SubMiner defaults', () => { + withPlatform('linux', () => { + assert.deepEqual( + buildConfiguredMpvDefaultArgs(makeArgs({ profile: 'anime,hdr' }), { + DISPLAY: ':1', + XDG_SESSION_TYPE: 'x11', + }).slice(0, 2), + ['--profile=anime,hdr', '--sub-auto=fuzzy'], + ); + }); +}); + test('buildConfiguredMpvDefaultArgs disables macOS menu shortcuts so SubMiner bindings reach mpv', () => { withPlatform('darwin', () => { assert.equal( diff --git a/launcher/parse-args.test.ts b/launcher/parse-args.test.ts index 3c74229c..fc85ca89 100644 --- a/launcher/parse-args.test.ts +++ b/launcher/parse-args.test.ts @@ -30,6 +30,12 @@ test('parseArgs captures mpv args string', () => { assert.equal(parsed.mpvArgs, '--pause=yes --title="movie night"'); }); +test('parseArgs appends CLI mpv profile to configured mpv profile', () => { + const parsed = parseArgs(['--profile', 'hdr'], 'subminer', {}, { profile: 'anime' }); + + assert.equal(parsed.profile, 'anime,hdr'); +}); + test('parseArgs maps root settings window option', () => { const parsed = parseArgs(['--settings'], 'subminer', {}); diff --git a/launcher/types.ts b/launcher/types.ts index b156027b..28741aaf 100644 --- a/launcher/types.ts +++ b/launcher/types.ts @@ -175,6 +175,7 @@ export interface LauncherJellyfinConfig { export interface LauncherMpvConfig { launchMode?: MpvLaunchMode; + profile?: string; socketPath?: string; backend?: MpvBackend; autoStartSubMiner?: boolean; diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 9e975e30..4dcb29fc 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -150,6 +150,7 @@ test('loads defaults when config is missing', () => { assert.equal(config.updates.channel, 'stable'); assert.equal(config.mpv.socketPath, '/tmp/subminer-socket'); assert.equal(config.mpv.backend, 'auto'); + assert.equal(config.mpv.profile, ''); assert.equal(config.mpv.autoStartSubMiner, true); assert.equal(config.mpv.pauseUntilOverlayReady, true); assert.equal(config.mpv.subminerBinaryPath, ''); @@ -357,6 +358,7 @@ test('parses managed mpv plugin runtime settings from config', () => { "mpv": { "socketPath": "/tmp/custom-subminer.sock", "backend": "x11", + "profile": " anime ", "autoStartSubMiner": false, "pauseUntilOverlayReady": false, "subminerBinaryPath": "/opt/SubMiner/SubMiner.AppImage", @@ -371,6 +373,7 @@ test('parses managed mpv plugin runtime settings from config', () => { const config = validService.getConfig(); assert.equal(config.mpv.socketPath, '/tmp/custom-subminer.sock'); assert.equal(config.mpv.backend, 'x11'); + assert.equal(config.mpv.profile, 'anime'); assert.equal(config.mpv.autoStartSubMiner, false); assert.equal(config.mpv.pauseUntilOverlayReady, false); assert.equal(config.mpv.subminerBinaryPath, '/opt/SubMiner/SubMiner.AppImage'); @@ -384,6 +387,7 @@ test('parses managed mpv plugin runtime settings from config', () => { "mpv": { "socketPath": "", "backend": "weston", + "profile": 12, "autoStartSubMiner": "yes", "pauseUntilOverlayReady": "no", "subminerBinaryPath": 42, @@ -399,6 +403,7 @@ test('parses managed mpv plugin runtime settings from config', () => { const warnings = invalidService.getWarnings(); assert.equal(invalidConfig.mpv.socketPath, DEFAULT_CONFIG.mpv.socketPath); assert.equal(invalidConfig.mpv.backend, DEFAULT_CONFIG.mpv.backend); + assert.equal(invalidConfig.mpv.profile, DEFAULT_CONFIG.mpv.profile); assert.equal(invalidConfig.mpv.autoStartSubMiner, DEFAULT_CONFIG.mpv.autoStartSubMiner); assert.equal(invalidConfig.mpv.pauseUntilOverlayReady, DEFAULT_CONFIG.mpv.pauseUntilOverlayReady); assert.equal(invalidConfig.mpv.subminerBinaryPath, DEFAULT_CONFIG.mpv.subminerBinaryPath); @@ -406,6 +411,7 @@ test('parses managed mpv plugin runtime settings from config', () => { assert.equal(invalidConfig.mpv.aniskipButtonKey, DEFAULT_CONFIG.mpv.aniskipButtonKey); assert.ok(warnings.some((warning) => warning.path === 'mpv.socketPath')); assert.ok(warnings.some((warning) => warning.path === 'mpv.backend')); + assert.ok(warnings.some((warning) => warning.path === 'mpv.profile')); assert.ok(warnings.some((warning) => warning.path === 'mpv.autoStartSubMiner')); assert.ok(warnings.some((warning) => warning.path === 'mpv.pauseUntilOverlayReady')); assert.ok(warnings.some((warning) => warning.path === 'mpv.subminerBinaryPath')); diff --git a/src/config/definitions/defaults-integrations.ts b/src/config/definitions/defaults-integrations.ts index 81e1e134..4d269271 100644 --- a/src/config/definitions/defaults-integrations.ts +++ b/src/config/definitions/defaults-integrations.ts @@ -94,6 +94,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick< mpv: { executablePath: '', launchMode: 'normal', + profile: '', socketPath: getDefaultMpvSocketPath(), backend: 'auto', autoStartSubMiner: true, diff --git a/src/config/definitions/domain-registry.test.ts b/src/config/definitions/domain-registry.test.ts index fbf0e947..792cac8e 100644 --- a/src/config/definitions/domain-registry.test.ts +++ b/src/config/definitions/domain-registry.test.ts @@ -105,6 +105,7 @@ test('config option registry includes critical paths and has unique entries', () 'anilist.characterDictionary.collapsibleSections.description', 'mpv.executablePath', 'mpv.launchMode', + 'mpv.profile', 'mpv.socketPath', 'mpv.backend', 'mpv.autoStartSubMiner', diff --git a/src/config/definitions/options-integrations.ts b/src/config/definitions/options-integrations.ts index e875eb88..d396d98a 100644 --- a/src/config/definitions/options-integrations.ts +++ b/src/config/definitions/options-integrations.ts @@ -449,6 +449,13 @@ export function buildIntegrationConfigOptionRegistry( defaultValue: defaultConfig.mpv.launchMode, description: 'Default window state for SubMiner-managed mpv launches.', }, + { + path: 'mpv.profile', + kind: 'string', + defaultValue: defaultConfig.mpv.profile, + description: + 'Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.', + }, { path: 'mpv.socketPath', kind: 'string', diff --git a/src/config/definitions/template-sections.ts b/src/config/definitions/template-sections.ts index 97bdb938..32fad4a9 100644 --- a/src/config/definitions/template-sections.ts +++ b/src/config/definitions/template-sections.ts @@ -175,6 +175,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [ 'Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin.', 'autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.', 'Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.', + 'Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none.', 'Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.', ], key: 'mpv', diff --git a/src/config/resolve/integrations.ts b/src/config/resolve/integrations.ts index 41d22899..8793c9ef 100644 --- a/src/config/resolve/integrations.ts +++ b/src/config/resolve/integrations.ts @@ -254,6 +254,13 @@ export function applyIntegrationConfig(context: ResolveContext): void { ); } + const profile = asString(src.mpv.profile); + if (profile !== undefined) { + resolved.mpv.profile = profile.trim(); + } else if (src.mpv.profile !== undefined) { + warn('mpv.profile', src.mpv.profile, resolved.mpv.profile, 'Expected string.'); + } + const socketPath = asString(src.mpv.socketPath); if (socketPath !== undefined && socketPath.trim().length > 0) { resolved.mpv.socketPath = socketPath.trim(); diff --git a/src/config/settings/registry.test.ts b/src/config/settings/registry.test.ts index 28510171..bb84cc5d 100644 --- a/src/config/settings/registry.test.ts +++ b/src/config/settings/registry.test.ts @@ -24,6 +24,8 @@ test('settings registry splits viewing into appearance and behavior categories', assert.equal(field('youtube.primarySubLanguages').section, 'YouTube Playback Settings'); assert.equal(field('mpv.launchMode').category, 'behavior'); assert.equal(field('mpv.launchMode').section, 'mpv Playback'); + assert.equal(field('mpv.profile').category, 'behavior'); + assert.equal(field('mpv.profile').section, 'mpv Playback'); assert.ok( fields.findIndex((candidate) => candidate.configPath === 'subtitleStyle.primaryDefaultMode') < fields.findIndex((candidate) => candidate.configPath === 'secondarySub.defaultMode'), @@ -298,6 +300,7 @@ test('settings registry keeps unsafe config siblings restart-required', () => { 'ankiConnect.url', 'ankiConnect.proxy.enabled', 'mpv.socketPath', + 'mpv.profile', 'websocket.port', ]) { assert.equal(field(path).restartBehavior, 'restart', path); diff --git a/src/config/settings/registry.ts b/src/config/settings/registry.ts index 5ae21dcd..c8309ae2 100644 --- a/src/config/settings/registry.ts +++ b/src/config/settings/registry.ts @@ -184,6 +184,7 @@ const PATH_ORDER = new Map( 'mpv.backend', 'mpv.subminerBinaryPath', 'mpv.aniskipEnabled', + 'mpv.profile', 'mpv.launchMode', 'mpv.executablePath', 'mpv.aniskipButtonKey', @@ -225,6 +226,7 @@ const LABEL_OVERRIDES: Record = { 'mpv.executablePath': 'mpv Executable Path', 'mpv.subminerBinaryPath': 'SubMiner Binary Path', 'mpv.socketPath': 'mpv IPC Socket Path', + 'mpv.profile': 'mpv Profile', 'mpv.autoStartSubMiner': 'Auto-start SubMiner', 'mpv.pauseUntilOverlayReady': 'Pause Until Overlay Ready', 'mpv.aniskipEnabled': 'Enable AniSkip', diff --git a/src/types/config.ts b/src/types/config.ts index 8921cbe8..a73abc89 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -58,6 +58,7 @@ export type MpvBackend = 'auto' | 'hyprland' | 'sway' | 'x11' | 'macos' | 'windo export interface MpvConfig { executablePath?: string; launchMode?: MpvLaunchMode; + profile?: string; socketPath?: string; backend?: MpvBackend; autoStartSubMiner?: boolean; @@ -156,6 +157,7 @@ export interface ResolvedConfig { mpv: { executablePath: string; launchMode: MpvLaunchMode; + profile: string; socketPath: string; backend: MpvBackend; autoStartSubMiner: boolean;