Add Windows mpv executable path override

This commit is contained in:
2026-04-03 11:47:26 -07:00
parent c664f9f605
commit d2201833f0
20 changed files with 250 additions and 11 deletions

View File

@@ -0,0 +1,5 @@
type: fixed
area: launcher
- Added a blank-by-default `mpv.executablePath` override for Windows playback so users can point SubMiner at `mpv.exe` when it is not on `PATH`.
- Kept the Windows shortcut and `--launch-mpv` flow simple by preserving PATH auto-discovery as the default and exposing the override in first-run setup.

View File

@@ -458,6 +458,15 @@
"externalProfilePath": "" // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
}, // Optional external Yomitan profile integration.
// ==========================================
// MPV Launcher
// Optional mpv.exe override for Windows playback entry points.
// 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.
}, // Optional mpv.exe override for Windows playback entry points.
// ==========================================
// Jellyfin
// Optional Jellyfin integration for auth, browsing, and playback launch.

View File

@@ -23,7 +23,7 @@
**macOS** — macOS 10.13 or later. Accessibility permission required for window tracking.
**Windows** — Windows 10 or later. Install `mpv` and keep it available on `PATH`; SubMiner's packaged build handles window tracking directly.
**Windows** — Windows 10 or later. Install `mpv`; keep it on `PATH` for auto-discovery or set `mpv.executablePath` in config if `mpv.exe` lives elsewhere. SubMiner's packaged build handles window tracking directly.
### Optional Tools
@@ -172,6 +172,7 @@ Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still re
### Windows Usage Notes
- Launch `SubMiner.exe` once to let the first-run setup flow seed `%APPDATA%\\SubMiner\\config.jsonc`, require mpv plugin installation, and open bundled Yomitan settings. The optional `SubMiner mpv` Start Menu/Desktop shortcut can also be created during setup, and on Windows it is the recommended way to launch mpv playback with SubMiner defaults.
- If `mpv.exe` is not on `PATH`, set `mpv.executablePath` in `config.jsonc` or use the first-run setup field to point at the executable. Leave it blank to keep PATH auto-discovery.
- `SubMiner.exe --launch-mpv` and the optional `SubMiner mpv` shortcut pass SubMiner's default mpv socket/subtitle args directly and do not require an `mpv.conf` profile named `subminer`.
- First-run mpv plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is installed in a non-standard location.
- Windows plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket`; do not keep `/tmp/subminer-socket` on Windows.

View File

@@ -458,6 +458,15 @@
"externalProfilePath": "" // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
}, // Optional external Yomitan profile integration.
// ==========================================
// MPV Launcher
// Optional mpv.exe override for Windows playback entry points.
// 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.
}, // Optional mpv.exe override for Windows playback entry points.
// ==========================================
// Jellyfin
// Optional Jellyfin integration for auth, browsing, and playback launch.

View File

@@ -133,7 +133,7 @@ You can use it three ways:
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
```
This flow requires `mpv.exe` to be on `PATH`. If it is installed elsewhere, set `SUBMINER_MPV_PATH` to the full `mpv.exe` path before launching. On Windows, `--launch-mpv` does not require an `mpv.conf` profile named `subminer`.
This flow requires `mpv.exe` to be discoverable. Leave `mpv.executablePath` blank to auto-discover from `PATH`, or set it to the full `mpv.exe` path if mpv is installed elsewhere. `SUBMINER_MPV_PATH` is still honored as a fallback. On Windows, `--launch-mpv` does not require an `mpv.conf` profile named `subminer`.
### Launcher Subcommands
@@ -164,6 +164,7 @@ Setup flow:
- Yomitan shortcut: open bundled Yomitan settings directly from the setup window
- dictionary check: ensure at least one bundled Yomitan dictionary is available, unless an external Yomitan profile is configured
- Windows: optionally create or remove `SubMiner mpv` Start Menu/Desktop shortcuts (`SubMiner.exe --launch-mpv`)
- Windows: optionally set `mpv.executablePath` if `mpv.exe` is not on `PATH`
- refresh: re-check plugin + dictionary state without restarting
- `Finish setup` stays disabled until the config, plugin, and dictionary gates are satisfied
- finish action writes setup completion state and suppresses future auto-open prompts

View File

@@ -38,8 +38,8 @@
"docs:preview": "bun run --cwd docs-site docs:preview",
"docs:test": "bun run --cwd docs-site test",
"test:docs:kb": "bun test scripts/docs-knowledge-base.test.ts",
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts src/verify-config-example.test.ts",
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js dist/verify-config-example.test.js",
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/integrations.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts src/verify-config-example.test.ts",
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/integrations.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js dist/verify-config-example.test.js",
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-binary-windows.lua",
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",

View File

@@ -35,8 +35,17 @@ const {
startupWarmups,
auto_start_overlay,
} = CORE_DEFAULT_CONFIG;
const { ankiConnect, jimaku, anilist, yomitan, jellyfin, discordPresence, ai, youtubeSubgen } =
INTEGRATIONS_DEFAULT_CONFIG;
const {
ankiConnect,
jimaku,
anilist,
mpv,
yomitan,
jellyfin,
discordPresence,
ai,
youtubeSubgen,
} = INTEGRATIONS_DEFAULT_CONFIG;
const { subtitleStyle, subtitleSidebar } = SUBTITLE_DEFAULT_CONFIG;
const { immersionTracking } = IMMERSION_DEFAULT_CONFIG;
const { stats } = STATS_DEFAULT_CONFIG;
@@ -60,6 +69,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
auto_start_overlay,
jimaku,
anilist,
mpv,
yomitan,
jellyfin,
discordPresence,

View File

@@ -5,6 +5,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
| 'ankiConnect'
| 'jimaku'
| 'anilist'
| 'mpv'
| 'yomitan'
| 'jellyfin'
| 'discordPresence'
@@ -90,6 +91,9 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
languagePreference: 'ja',
maxEntryResults: 10,
},
mpv: {
executablePath: '',
},
anilist: {
enabled: false,
accessToken: '',

View File

@@ -28,6 +28,7 @@ test('config option registry includes critical paths and has unique entries', ()
'ankiConnect.enabled',
'anilist.characterDictionary.enabled',
'anilist.characterDictionary.collapsibleSections.description',
'mpv.executablePath',
'yomitan.externalProfilePath',
'immersionTracking.enabled',
]) {
@@ -48,6 +49,7 @@ test('config template sections include expected domains and unique keys', () =>
'subtitleStyle',
'ankiConnect',
'yomitan',
'mpv',
'immersionTracking',
];

View File

@@ -238,6 +238,13 @@ export function buildIntegrationConfigOptionRegistry(
description:
'Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay',
},
{
path: 'mpv.executablePath',
kind: 'string',
defaultValue: defaultConfig.mpv.executablePath,
description:
'Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.',
},
{
path: 'jellyfin.enabled',
kind: 'boolean',

View File

@@ -153,6 +153,14 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
],
key: 'yomitan',
},
{
title: 'MPV Launcher',
description: [
'Optional mpv.exe override for Windows playback entry points.',
'Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.',
],
key: 'mpv',
},
{
title: 'Jellyfin',
description: [

View File

@@ -0,0 +1,31 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolveConfig } from '../resolve';
test('resolveConfig trims configured mpv executable path', () => {
const { resolved, warnings } = resolveConfig({
mpv: {
executablePath: ' C:\\Program Files\\mpv\\mpv.exe ',
},
});
assert.equal(resolved.mpv.executablePath, 'C:\\Program Files\\mpv\\mpv.exe');
assert.deepEqual(warnings, []);
});
test('resolveConfig warns for invalid mpv executable path type', () => {
const { resolved, warnings } = resolveConfig({
mpv: {
executablePath: 42 as never,
},
});
assert.equal(resolved.mpv.executablePath, '');
assert.equal(warnings.length, 1);
assert.deepEqual(warnings[0], {
path: 'mpv.executablePath',
value: 42,
fallback: '',
message: 'Expected string.',
});
});

View File

@@ -228,6 +228,22 @@ export function applyIntegrationConfig(context: ResolveContext): void {
warn('yomitan', src.yomitan, resolved.yomitan, 'Expected object.');
}
if (isObject(src.mpv)) {
const executablePath = asString(src.mpv.executablePath);
if (executablePath !== undefined) {
resolved.mpv.executablePath = executablePath.trim();
} else if (src.mpv.executablePath !== undefined) {
warn(
'mpv.executablePath',
src.mpv.executablePath,
resolved.mpv.executablePath,
'Expected string.',
);
}
} else if (src.mpv !== undefined) {
warn('mpv', src.mpv, resolved.mpv, 'Expected object.');
}
if (isObject(src.jellyfin)) {
const enabled = asBoolean(src.jellyfin.enabled);
if (enabled !== undefined) {

View File

@@ -2,6 +2,7 @@ import path from 'node:path';
import { spawn } from 'node:child_process';
import { app, dialog } from 'electron';
import { printHelp } from './cli/help';
import { loadRawConfigStrict } from './config/load';
import {
configureEarlyAppPaths,
normalizeLaunchMpvExtraArgs,
@@ -35,6 +36,21 @@ 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,
@@ -50,7 +66,7 @@ function resolveBundledWindowsMpvPluginEntrypoint(): string | undefined {
process.argv = normalizeStartupArgv(process.argv, process.env);
applySanitizedEnv(sanitizeStartupEnv(process.env));
configureEarlyAppPaths(app);
const userDataPath = configureEarlyAppPaths(app);
if (shouldDetachBackgroundLaunch(process.argv, process.env)) {
const child = spawn(process.execPath, process.argv.slice(1), {
@@ -87,6 +103,7 @@ if (shouldHandleLaunchMpvAtEntry(process.argv, process.env)) {
normalizeLaunchMpvExtraArgs(process.argv),
process.execPath,
resolveBundledWindowsMpvPluginEntrypoint(),
readConfiguredWindowsMpvExecutablePath(userDataPath),
);
app.exit(result.ok ? 0 : 1);
});

View File

@@ -1037,6 +1037,9 @@ const youtubePlaybackRuntime = createYoutubePlaybackRuntime({
showError: (title, content) => dialog.showErrorBox(title, content),
}),
[...args, `--log-file=${DEFAULT_MPV_LOG_PATH}`],
undefined,
undefined,
getResolvedConfig().mpv.executablePath,
),
waitForYoutubeMpvConnected: (timeoutMs) => waitForYoutubeMpvConnected(timeoutMs),
prepareYoutubePlaybackInMpv: (request) => prepareYoutubePlaybackInMpv(request),
@@ -2220,6 +2223,7 @@ const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
externalYomitanConfigured: snapshot.externalYomitanConfigured,
pluginStatus: snapshot.pluginStatus,
pluginInstallPathSummary: snapshot.pluginInstallPathSummary,
mpvExecutablePath: getResolvedConfig().mpv.executablePath,
windowsMpvShortcuts: snapshot.windowsMpvShortcuts,
message: firstRunSetupMessage,
};
@@ -2232,6 +2236,18 @@ const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
firstRunSetupMessage = snapshot.message;
return;
}
if (submission.action === 'configure-mpv-executable-path') {
const mpvExecutablePath = submission.mpvExecutablePath?.trim() ?? '';
configService.patchRawConfig({
mpv: {
executablePath: mpvExecutablePath,
},
});
firstRunSetupMessage = mpvExecutablePath
? `Saved mpv executable path: ${mpvExecutablePath}`
: 'Cleared mpv executable path. SubMiner will auto-discover mpv.exe from PATH.';
return;
}
if (submission.action === 'configure-windows-mpv-shortcuts') {
const snapshot = await firstRunSetupService.configureWindowsMpvShortcuts({
startMenuEnabled: submission.startMenuEnabled === true,

View File

@@ -16,6 +16,7 @@ test('buildFirstRunSetupHtml renders macchiato setup actions and disabled finish
externalYomitanConfigured: false,
pluginStatus: 'required',
pluginInstallPathSummary: null,
mpvExecutablePath: '',
windowsMpvShortcuts: {
supported: false,
startMenuEnabled: true,
@@ -43,6 +44,7 @@ test('buildFirstRunSetupHtml switches plugin action to reinstall when already in
externalYomitanConfigured: false,
pluginStatus: 'installed',
pluginInstallPathSummary: '/tmp/mpv',
mpvExecutablePath: 'C:\\Program Files\\mpv\\mpv.exe',
windowsMpvShortcuts: {
supported: true,
startMenuEnabled: true,
@@ -55,6 +57,8 @@ test('buildFirstRunSetupHtml switches plugin action to reinstall when already in
});
assert.match(html, /Reinstall mpv plugin/);
assert.match(html, /mpv executable path/);
assert.match(html, /Leave blank to auto-discover mpv\.exe from PATH\./);
assert.match(
html,
/Finish stays unlocked once the mpv plugin is installed and Yomitan reports at least one installed dictionary\./,
@@ -69,6 +73,7 @@ test('buildFirstRunSetupHtml explains the config blocker when setup is missing c
externalYomitanConfigured: false,
pluginStatus: 'required',
pluginInstallPathSummary: null,
mpvExecutablePath: '',
windowsMpvShortcuts: {
supported: false,
startMenuEnabled: true,
@@ -91,6 +96,7 @@ test('buildFirstRunSetupHtml explains external yomitan mode and keeps finish ena
externalYomitanConfigured: true,
pluginStatus: 'installed',
pluginInstallPathSummary: null,
mpvExecutablePath: '',
windowsMpvShortcuts: {
supported: false,
startMenuEnabled: true,
@@ -107,6 +113,15 @@ test('buildFirstRunSetupHtml explains external yomitan mode and keeps finish ena
});
test('parseFirstRunSetupSubmissionUrl parses supported custom actions', () => {
assert.deepEqual(
parseFirstRunSetupSubmissionUrl(
'subminer://first-run-setup?action=configure-mpv-executable-path&mpvExecutablePath=C%3A%5CApps%5Cmpv%5Cmpv.exe',
),
{
action: 'configure-mpv-executable-path',
mpvExecutablePath: 'C:\\Apps\\mpv\\mpv.exe',
},
);
assert.deepEqual(parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=refresh'), {
action: 'refresh',
});
@@ -192,6 +207,7 @@ test('closing incomplete first-run setup quits app outside background mode', asy
externalYomitanConfigured: false,
pluginStatus: 'required',
pluginInstallPathSummary: null,
mpvExecutablePath: '',
windowsMpvShortcuts: {
supported: false,
startMenuEnabled: true,

View File

@@ -17,6 +17,7 @@ type FirstRunSetupWindowLike = FocusableWindowLike & {
};
export type FirstRunSetupAction =
| 'configure-mpv-executable-path'
| 'install-plugin'
| 'configure-windows-mpv-shortcuts'
| 'open-yomitan-settings'
@@ -25,6 +26,7 @@ export type FirstRunSetupAction =
export interface FirstRunSetupSubmission {
action: FirstRunSetupAction;
mpvExecutablePath?: string;
startMenuEnabled?: boolean;
desktopEnabled?: boolean;
}
@@ -36,6 +38,7 @@ export interface FirstRunSetupHtmlModel {
externalYomitanConfigured: boolean;
pluginStatus: 'installed' | 'required' | 'failed';
pluginInstallPathSummary: string | null;
mpvExecutablePath: string;
windowsMpvShortcuts: {
supported: boolean;
startMenuEnabled: boolean;
@@ -90,6 +93,34 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
: model.windowsMpvShortcuts.status === 'skipped'
? 'muted'
: 'warn';
const mpvExecutablePathLabel =
model.mpvExecutablePath.trim().length > 0 ? 'Configured' : 'Blank';
const mpvExecutablePathTone = model.mpvExecutablePath.trim().length > 0 ? 'ready' : 'muted';
const mpvExecutablePathCard = model.windowsMpvShortcuts.supported
? `
<div class="card block">
<div class="card-head">
<div>
<strong>mpv executable path</strong>
<div class="meta">Leave blank to auto-discover mpv.exe from PATH.</div>
<div class="meta">Current: ${escapeHtml(model.mpvExecutablePath.trim().length > 0 ? model.mpvExecutablePath : 'blank (PATH discovery)')}</div>
</div>
${renderStatusBadge(mpvExecutablePathLabel, mpvExecutablePathTone)}
</div>
<form
class="path-form"
onsubmit="event.preventDefault(); const params = new URLSearchParams({ action: 'configure-mpv-executable-path', mpvExecutablePath: document.getElementById('mpv-executable-path').value }); window.location.href = 'subminer://first-run-setup?' + params.toString();"
>
<input
id="mpv-executable-path"
type="text"
value="${escapeHtml(model.mpvExecutablePath)}"
placeholder="C:\\Program Files\\mpv\\mpv.exe"
/>
<button type="submit">Save mpv executable path</button>
</form>
</div>`
: '';
const windowsShortcutCard = model.windowsMpvShortcuts.supported
? `
<div class="card block">
@@ -218,6 +249,24 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
.badge.warn { background: rgba(238, 212, 159, 0.18); color: var(--yellow); }
.badge.muted { background: rgba(184, 192, 224, 0.12); color: var(--muted); }
.badge.danger { background: rgba(237, 135, 150, 0.16); color: var(--red); }
.path-form {
display: grid;
gap: 8px;
margin-top: 12px;
}
.path-form input[type='text'] {
width: 100%;
box-sizing: border-box;
border: 1px solid rgba(202, 211, 245, 0.12);
border-radius: 10px;
padding: 9px 10px;
color: var(--text);
background: rgba(30, 32, 48, 0.72);
font: inherit;
}
.path-form input[type='text']::placeholder {
color: rgba(184, 192, 224, 0.65);
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -282,6 +331,7 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
</div>
${renderStatusBadge(yomitanBadgeLabel, yomitanBadgeTone)}
</div>
${mpvExecutablePathCard}
${windowsShortcutCard}
<div class="actions">
<button onclick="window.location.href='subminer://first-run-setup?action=install-plugin'">${pluginActionLabel}</button>
@@ -303,6 +353,7 @@ export function parseFirstRunSetupSubmissionUrl(rawUrl: string): FirstRunSetupSu
const parsed = new URL(rawUrl);
const action = parsed.searchParams.get('action');
if (
action !== 'configure-mpv-executable-path' &&
action !== 'install-plugin' &&
action !== 'configure-windows-mpv-shortcuts' &&
action !== 'open-yomitan-settings' &&
@@ -311,6 +362,12 @@ export function parseFirstRunSetupSubmissionUrl(rawUrl: string): FirstRunSetupSu
) {
return null;
}
if (action === 'configure-mpv-executable-path') {
return {
action,
mpvExecutablePath: parsed.searchParams.get('mpvExecutablePath') ?? '',
};
}
if (action === 'configure-windows-mpv-shortcuts') {
return {
action,

View File

@@ -29,6 +29,19 @@ test('resolveWindowsMpvPath prefers SUBMINER_MPV_PATH', () => {
assert.equal(resolved, 'C:\\mpv\\mpv.exe');
});
test('resolveWindowsMpvPath prefers configured executable path before PATH', () => {
const resolved = resolveWindowsMpvPath(
createDeps({
getEnv: () => undefined,
runWhere: () => ({ status: 0, stdout: 'C:\\tools\\mpv.exe\r\n' }),
fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe',
}),
' C:\\mpv\\mpv.exe ',
);
assert.equal(resolved, 'C:\\mpv\\mpv.exe');
});
test('resolveWindowsMpvPath falls back to where.exe output', () => {
const resolved = resolveWindowsMpvPath(
createDeps({
@@ -132,7 +145,7 @@ test('launchWindowsMpv reports missing mpv path', () => {
assert.equal(result.ok, false);
assert.equal(result.mpvPath, '');
assert.match(errors[0] ?? '', /Could not find mpv\.exe/i);
assert.match(errors[0] ?? '', /mpv\.executablePath/i);
});
test('launchWindowsMpv spawns detached mpv with targets', () => {

View File

@@ -13,7 +13,15 @@ function normalizeCandidate(candidate: string | undefined): string {
return typeof candidate === 'string' ? candidate.trim() : '';
}
export function resolveWindowsMpvPath(deps: WindowsMpvLaunchDeps): string {
export function resolveWindowsMpvPath(
deps: WindowsMpvLaunchDeps,
configuredMpvPath = '',
): string {
const configPath = normalizeCandidate(configuredMpvPath);
if (configPath && deps.fileExists(configPath)) {
return configPath;
}
const envPath = normalizeCandidate(deps.getEnv('SUBMINER_MPV_PATH'));
if (envPath && deps.fileExists(envPath)) {
return envPath;
@@ -97,12 +105,13 @@ export function launchWindowsMpv(
extraArgs: string[] = [],
binaryPath?: string,
pluginEntrypointPath?: string,
configuredMpvPath?: string,
): { ok: boolean; mpvPath: string } {
const mpvPath = resolveWindowsMpvPath(deps);
const mpvPath = resolveWindowsMpvPath(deps, configuredMpvPath);
if (!mpvPath) {
deps.showError(
'SubMiner mpv launcher',
'Could not find mpv.exe. Install mpv and add it to PATH, or set SUBMINER_MPV_PATH.',
'Could not find mpv.exe. Set mpv.executablePath, set SUBMINER_MPV_PATH, or add mpv.exe to PATH.',
);
return { ok: false, mpvPath: '' };
}

View File

@@ -50,6 +50,10 @@ export interface TexthookerConfig {
openBrowser?: boolean;
}
export interface MpvConfig {
executablePath?: string;
}
export type SubsyncMode = 'auto' | 'manual';
export interface SubsyncConfig {
@@ -90,6 +94,7 @@ export interface Config {
websocket?: WebSocketConfig;
annotationWebsocket?: AnnotationWebSocketConfig;
texthooker?: TexthookerConfig;
mpv?: MpvConfig;
controller?: ControllerConfig;
ankiConnect?: AnkiConnectConfig;
shortcuts?: ShortcutsConfig;
@@ -122,6 +127,9 @@ export interface ResolvedConfig {
websocket: Required<WebSocketConfig>;
annotationWebsocket: Required<AnnotationWebSocketConfig>;
texthooker: Required<TexthookerConfig>;
mpv: {
executablePath: string;
};
controller: {
enabled: boolean;
preferredGamepadId: string;