mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-09 16:19:25 -07:00
Add Windows mpv executable path override
This commit is contained in:
5
changes/272-windows-mpv-executable-path.md
Normal file
5
changes/272-windows-mpv-executable-path.md
Normal 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.
|
||||||
@@ -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
|
"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.
|
}, // 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
|
// Jellyfin
|
||||||
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
**macOS** — macOS 10.13 or later. Accessibility permission required for window tracking.
|
**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
|
### Optional Tools
|
||||||
|
|
||||||
@@ -172,6 +172,7 @@ Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still re
|
|||||||
### Windows Usage Notes
|
### 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.
|
- 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`.
|
- `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.
|
- 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.
|
- Windows plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket`; do not keep `/tmp/subminer-socket` on Windows.
|
||||||
|
|||||||
@@ -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
|
"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.
|
}, // 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
|
// Jellyfin
|
||||||
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ You can use it three ways:
|
|||||||
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
|
& "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
|
### Launcher Subcommands
|
||||||
|
|
||||||
@@ -164,6 +164,7 @@ Setup flow:
|
|||||||
- Yomitan shortcut: open bundled Yomitan settings directly from the setup window
|
- 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
|
- 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 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
|
- refresh: re-check plugin + dictionary state without restarting
|
||||||
- `Finish setup` stays disabled until the config, plugin, and dictionary gates are satisfied
|
- `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
|
- finish action writes setup completion state and suppresses future auto-open prompts
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
"docs:preview": "bun run --cwd docs-site docs:preview",
|
"docs:preview": "bun run --cwd docs-site docs:preview",
|
||||||
"docs:test": "bun run --cwd docs-site test",
|
"docs:test": "bun run --cwd docs-site test",
|
||||||
"test:docs:kb": "bun test scripts/docs-knowledge-base.test.ts",
|
"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: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/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: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: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: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",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
|
|||||||
@@ -35,8 +35,17 @@ const {
|
|||||||
startupWarmups,
|
startupWarmups,
|
||||||
auto_start_overlay,
|
auto_start_overlay,
|
||||||
} = CORE_DEFAULT_CONFIG;
|
} = CORE_DEFAULT_CONFIG;
|
||||||
const { ankiConnect, jimaku, anilist, yomitan, jellyfin, discordPresence, ai, youtubeSubgen } =
|
const {
|
||||||
INTEGRATIONS_DEFAULT_CONFIG;
|
ankiConnect,
|
||||||
|
jimaku,
|
||||||
|
anilist,
|
||||||
|
mpv,
|
||||||
|
yomitan,
|
||||||
|
jellyfin,
|
||||||
|
discordPresence,
|
||||||
|
ai,
|
||||||
|
youtubeSubgen,
|
||||||
|
} = INTEGRATIONS_DEFAULT_CONFIG;
|
||||||
const { subtitleStyle, subtitleSidebar } = SUBTITLE_DEFAULT_CONFIG;
|
const { subtitleStyle, subtitleSidebar } = SUBTITLE_DEFAULT_CONFIG;
|
||||||
const { immersionTracking } = IMMERSION_DEFAULT_CONFIG;
|
const { immersionTracking } = IMMERSION_DEFAULT_CONFIG;
|
||||||
const { stats } = STATS_DEFAULT_CONFIG;
|
const { stats } = STATS_DEFAULT_CONFIG;
|
||||||
@@ -60,6 +69,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
|||||||
auto_start_overlay,
|
auto_start_overlay,
|
||||||
jimaku,
|
jimaku,
|
||||||
anilist,
|
anilist,
|
||||||
|
mpv,
|
||||||
yomitan,
|
yomitan,
|
||||||
jellyfin,
|
jellyfin,
|
||||||
discordPresence,
|
discordPresence,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
|||||||
| 'ankiConnect'
|
| 'ankiConnect'
|
||||||
| 'jimaku'
|
| 'jimaku'
|
||||||
| 'anilist'
|
| 'anilist'
|
||||||
|
| 'mpv'
|
||||||
| 'yomitan'
|
| 'yomitan'
|
||||||
| 'jellyfin'
|
| 'jellyfin'
|
||||||
| 'discordPresence'
|
| 'discordPresence'
|
||||||
@@ -90,6 +91,9 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
|||||||
languagePreference: 'ja',
|
languagePreference: 'ja',
|
||||||
maxEntryResults: 10,
|
maxEntryResults: 10,
|
||||||
},
|
},
|
||||||
|
mpv: {
|
||||||
|
executablePath: '',
|
||||||
|
},
|
||||||
anilist: {
|
anilist: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ test('config option registry includes critical paths and has unique entries', ()
|
|||||||
'ankiConnect.enabled',
|
'ankiConnect.enabled',
|
||||||
'anilist.characterDictionary.enabled',
|
'anilist.characterDictionary.enabled',
|
||||||
'anilist.characterDictionary.collapsibleSections.description',
|
'anilist.characterDictionary.collapsibleSections.description',
|
||||||
|
'mpv.executablePath',
|
||||||
'yomitan.externalProfilePath',
|
'yomitan.externalProfilePath',
|
||||||
'immersionTracking.enabled',
|
'immersionTracking.enabled',
|
||||||
]) {
|
]) {
|
||||||
@@ -48,6 +49,7 @@ test('config template sections include expected domains and unique keys', () =>
|
|||||||
'subtitleStyle',
|
'subtitleStyle',
|
||||||
'ankiConnect',
|
'ankiConnect',
|
||||||
'yomitan',
|
'yomitan',
|
||||||
|
'mpv',
|
||||||
'immersionTracking',
|
'immersionTracking',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,13 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
description:
|
description:
|
||||||
'Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay',
|
'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',
|
path: 'jellyfin.enabled',
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
|
|||||||
@@ -153,6 +153,14 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
|||||||
],
|
],
|
||||||
key: 'yomitan',
|
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',
|
title: 'Jellyfin',
|
||||||
description: [
|
description: [
|
||||||
|
|||||||
31
src/config/resolve/integrations.test.ts
Normal file
31
src/config/resolve/integrations.test.ts
Normal 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.',
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -228,6 +228,22 @@ export function applyIntegrationConfig(context: ResolveContext): void {
|
|||||||
warn('yomitan', src.yomitan, resolved.yomitan, 'Expected object.');
|
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)) {
|
if (isObject(src.jellyfin)) {
|
||||||
const enabled = asBoolean(src.jellyfin.enabled);
|
const enabled = asBoolean(src.jellyfin.enabled);
|
||||||
if (enabled !== undefined) {
|
if (enabled !== undefined) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { app, dialog } from 'electron';
|
import { app, dialog } from 'electron';
|
||||||
import { printHelp } from './cli/help';
|
import { printHelp } from './cli/help';
|
||||||
|
import { loadRawConfigStrict } from './config/load';
|
||||||
import {
|
import {
|
||||||
configureEarlyAppPaths,
|
configureEarlyAppPaths,
|
||||||
normalizeLaunchMpvExtraArgs,
|
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 {
|
function resolveBundledWindowsMpvPluginEntrypoint(): string | undefined {
|
||||||
const assets = resolvePackagedFirstRunPluginAssets({
|
const assets = resolvePackagedFirstRunPluginAssets({
|
||||||
dirname: __dirname,
|
dirname: __dirname,
|
||||||
@@ -50,7 +66,7 @@ function resolveBundledWindowsMpvPluginEntrypoint(): string | undefined {
|
|||||||
|
|
||||||
process.argv = normalizeStartupArgv(process.argv, process.env);
|
process.argv = normalizeStartupArgv(process.argv, process.env);
|
||||||
applySanitizedEnv(sanitizeStartupEnv(process.env));
|
applySanitizedEnv(sanitizeStartupEnv(process.env));
|
||||||
configureEarlyAppPaths(app);
|
const userDataPath = configureEarlyAppPaths(app);
|
||||||
|
|
||||||
if (shouldDetachBackgroundLaunch(process.argv, process.env)) {
|
if (shouldDetachBackgroundLaunch(process.argv, process.env)) {
|
||||||
const child = spawn(process.execPath, process.argv.slice(1), {
|
const child = spawn(process.execPath, process.argv.slice(1), {
|
||||||
@@ -87,6 +103,7 @@ if (shouldHandleLaunchMpvAtEntry(process.argv, process.env)) {
|
|||||||
normalizeLaunchMpvExtraArgs(process.argv),
|
normalizeLaunchMpvExtraArgs(process.argv),
|
||||||
process.execPath,
|
process.execPath,
|
||||||
resolveBundledWindowsMpvPluginEntrypoint(),
|
resolveBundledWindowsMpvPluginEntrypoint(),
|
||||||
|
readConfiguredWindowsMpvExecutablePath(userDataPath),
|
||||||
);
|
);
|
||||||
app.exit(result.ok ? 0 : 1);
|
app.exit(result.ok ? 0 : 1);
|
||||||
});
|
});
|
||||||
|
|||||||
16
src/main.ts
16
src/main.ts
@@ -1037,6 +1037,9 @@ const youtubePlaybackRuntime = createYoutubePlaybackRuntime({
|
|||||||
showError: (title, content) => dialog.showErrorBox(title, content),
|
showError: (title, content) => dialog.showErrorBox(title, content),
|
||||||
}),
|
}),
|
||||||
[...args, `--log-file=${DEFAULT_MPV_LOG_PATH}`],
|
[...args, `--log-file=${DEFAULT_MPV_LOG_PATH}`],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
getResolvedConfig().mpv.executablePath,
|
||||||
),
|
),
|
||||||
waitForYoutubeMpvConnected: (timeoutMs) => waitForYoutubeMpvConnected(timeoutMs),
|
waitForYoutubeMpvConnected: (timeoutMs) => waitForYoutubeMpvConnected(timeoutMs),
|
||||||
prepareYoutubePlaybackInMpv: (request) => prepareYoutubePlaybackInMpv(request),
|
prepareYoutubePlaybackInMpv: (request) => prepareYoutubePlaybackInMpv(request),
|
||||||
@@ -2220,6 +2223,7 @@ const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
|
|||||||
externalYomitanConfigured: snapshot.externalYomitanConfigured,
|
externalYomitanConfigured: snapshot.externalYomitanConfigured,
|
||||||
pluginStatus: snapshot.pluginStatus,
|
pluginStatus: snapshot.pluginStatus,
|
||||||
pluginInstallPathSummary: snapshot.pluginInstallPathSummary,
|
pluginInstallPathSummary: snapshot.pluginInstallPathSummary,
|
||||||
|
mpvExecutablePath: getResolvedConfig().mpv.executablePath,
|
||||||
windowsMpvShortcuts: snapshot.windowsMpvShortcuts,
|
windowsMpvShortcuts: snapshot.windowsMpvShortcuts,
|
||||||
message: firstRunSetupMessage,
|
message: firstRunSetupMessage,
|
||||||
};
|
};
|
||||||
@@ -2232,6 +2236,18 @@ const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
|
|||||||
firstRunSetupMessage = snapshot.message;
|
firstRunSetupMessage = snapshot.message;
|
||||||
return;
|
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') {
|
if (submission.action === 'configure-windows-mpv-shortcuts') {
|
||||||
const snapshot = await firstRunSetupService.configureWindowsMpvShortcuts({
|
const snapshot = await firstRunSetupService.configureWindowsMpvShortcuts({
|
||||||
startMenuEnabled: submission.startMenuEnabled === true,
|
startMenuEnabled: submission.startMenuEnabled === true,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ test('buildFirstRunSetupHtml renders macchiato setup actions and disabled finish
|
|||||||
externalYomitanConfigured: false,
|
externalYomitanConfigured: false,
|
||||||
pluginStatus: 'required',
|
pluginStatus: 'required',
|
||||||
pluginInstallPathSummary: null,
|
pluginInstallPathSummary: null,
|
||||||
|
mpvExecutablePath: '',
|
||||||
windowsMpvShortcuts: {
|
windowsMpvShortcuts: {
|
||||||
supported: false,
|
supported: false,
|
||||||
startMenuEnabled: true,
|
startMenuEnabled: true,
|
||||||
@@ -43,6 +44,7 @@ test('buildFirstRunSetupHtml switches plugin action to reinstall when already in
|
|||||||
externalYomitanConfigured: false,
|
externalYomitanConfigured: false,
|
||||||
pluginStatus: 'installed',
|
pluginStatus: 'installed',
|
||||||
pluginInstallPathSummary: '/tmp/mpv',
|
pluginInstallPathSummary: '/tmp/mpv',
|
||||||
|
mpvExecutablePath: 'C:\\Program Files\\mpv\\mpv.exe',
|
||||||
windowsMpvShortcuts: {
|
windowsMpvShortcuts: {
|
||||||
supported: true,
|
supported: true,
|
||||||
startMenuEnabled: 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, /Reinstall mpv plugin/);
|
||||||
|
assert.match(html, /mpv executable path/);
|
||||||
|
assert.match(html, /Leave blank to auto-discover mpv\.exe from PATH\./);
|
||||||
assert.match(
|
assert.match(
|
||||||
html,
|
html,
|
||||||
/Finish stays unlocked once the mpv plugin is installed and Yomitan reports at least one installed dictionary\./,
|
/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,
|
externalYomitanConfigured: false,
|
||||||
pluginStatus: 'required',
|
pluginStatus: 'required',
|
||||||
pluginInstallPathSummary: null,
|
pluginInstallPathSummary: null,
|
||||||
|
mpvExecutablePath: '',
|
||||||
windowsMpvShortcuts: {
|
windowsMpvShortcuts: {
|
||||||
supported: false,
|
supported: false,
|
||||||
startMenuEnabled: true,
|
startMenuEnabled: true,
|
||||||
@@ -91,6 +96,7 @@ test('buildFirstRunSetupHtml explains external yomitan mode and keeps finish ena
|
|||||||
externalYomitanConfigured: true,
|
externalYomitanConfigured: true,
|
||||||
pluginStatus: 'installed',
|
pluginStatus: 'installed',
|
||||||
pluginInstallPathSummary: null,
|
pluginInstallPathSummary: null,
|
||||||
|
mpvExecutablePath: '',
|
||||||
windowsMpvShortcuts: {
|
windowsMpvShortcuts: {
|
||||||
supported: false,
|
supported: false,
|
||||||
startMenuEnabled: true,
|
startMenuEnabled: true,
|
||||||
@@ -107,6 +113,15 @@ test('buildFirstRunSetupHtml explains external yomitan mode and keeps finish ena
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('parseFirstRunSetupSubmissionUrl parses supported custom actions', () => {
|
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'), {
|
assert.deepEqual(parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=refresh'), {
|
||||||
action: 'refresh',
|
action: 'refresh',
|
||||||
});
|
});
|
||||||
@@ -192,6 +207,7 @@ test('closing incomplete first-run setup quits app outside background mode', asy
|
|||||||
externalYomitanConfigured: false,
|
externalYomitanConfigured: false,
|
||||||
pluginStatus: 'required',
|
pluginStatus: 'required',
|
||||||
pluginInstallPathSummary: null,
|
pluginInstallPathSummary: null,
|
||||||
|
mpvExecutablePath: '',
|
||||||
windowsMpvShortcuts: {
|
windowsMpvShortcuts: {
|
||||||
supported: false,
|
supported: false,
|
||||||
startMenuEnabled: true,
|
startMenuEnabled: true,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type FirstRunSetupWindowLike = FocusableWindowLike & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type FirstRunSetupAction =
|
export type FirstRunSetupAction =
|
||||||
|
| 'configure-mpv-executable-path'
|
||||||
| 'install-plugin'
|
| 'install-plugin'
|
||||||
| 'configure-windows-mpv-shortcuts'
|
| 'configure-windows-mpv-shortcuts'
|
||||||
| 'open-yomitan-settings'
|
| 'open-yomitan-settings'
|
||||||
@@ -25,6 +26,7 @@ export type FirstRunSetupAction =
|
|||||||
|
|
||||||
export interface FirstRunSetupSubmission {
|
export interface FirstRunSetupSubmission {
|
||||||
action: FirstRunSetupAction;
|
action: FirstRunSetupAction;
|
||||||
|
mpvExecutablePath?: string;
|
||||||
startMenuEnabled?: boolean;
|
startMenuEnabled?: boolean;
|
||||||
desktopEnabled?: boolean;
|
desktopEnabled?: boolean;
|
||||||
}
|
}
|
||||||
@@ -36,6 +38,7 @@ export interface FirstRunSetupHtmlModel {
|
|||||||
externalYomitanConfigured: boolean;
|
externalYomitanConfigured: boolean;
|
||||||
pluginStatus: 'installed' | 'required' | 'failed';
|
pluginStatus: 'installed' | 'required' | 'failed';
|
||||||
pluginInstallPathSummary: string | null;
|
pluginInstallPathSummary: string | null;
|
||||||
|
mpvExecutablePath: string;
|
||||||
windowsMpvShortcuts: {
|
windowsMpvShortcuts: {
|
||||||
supported: boolean;
|
supported: boolean;
|
||||||
startMenuEnabled: boolean;
|
startMenuEnabled: boolean;
|
||||||
@@ -90,6 +93,34 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
|
|||||||
: model.windowsMpvShortcuts.status === 'skipped'
|
: model.windowsMpvShortcuts.status === 'skipped'
|
||||||
? 'muted'
|
? 'muted'
|
||||||
: 'warn';
|
: '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
|
const windowsShortcutCard = model.windowsMpvShortcuts.supported
|
||||||
? `
|
? `
|
||||||
<div class="card block">
|
<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.warn { background: rgba(238, 212, 159, 0.18); color: var(--yellow); }
|
||||||
.badge.muted { background: rgba(184, 192, 224, 0.12); color: var(--muted); }
|
.badge.muted { background: rgba(184, 192, 224, 0.12); color: var(--muted); }
|
||||||
.badge.danger { background: rgba(237, 135, 150, 0.16); color: var(--red); }
|
.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 {
|
.actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -282,6 +331,7 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
|
|||||||
</div>
|
</div>
|
||||||
${renderStatusBadge(yomitanBadgeLabel, yomitanBadgeTone)}
|
${renderStatusBadge(yomitanBadgeLabel, yomitanBadgeTone)}
|
||||||
</div>
|
</div>
|
||||||
|
${mpvExecutablePathCard}
|
||||||
${windowsShortcutCard}
|
${windowsShortcutCard}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button onclick="window.location.href='subminer://first-run-setup?action=install-plugin'">${pluginActionLabel}</button>
|
<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 parsed = new URL(rawUrl);
|
||||||
const action = parsed.searchParams.get('action');
|
const action = parsed.searchParams.get('action');
|
||||||
if (
|
if (
|
||||||
|
action !== 'configure-mpv-executable-path' &&
|
||||||
action !== 'install-plugin' &&
|
action !== 'install-plugin' &&
|
||||||
action !== 'configure-windows-mpv-shortcuts' &&
|
action !== 'configure-windows-mpv-shortcuts' &&
|
||||||
action !== 'open-yomitan-settings' &&
|
action !== 'open-yomitan-settings' &&
|
||||||
@@ -311,6 +362,12 @@ export function parseFirstRunSetupSubmissionUrl(rawUrl: string): FirstRunSetupSu
|
|||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (action === 'configure-mpv-executable-path') {
|
||||||
|
return {
|
||||||
|
action,
|
||||||
|
mpvExecutablePath: parsed.searchParams.get('mpvExecutablePath') ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
if (action === 'configure-windows-mpv-shortcuts') {
|
if (action === 'configure-windows-mpv-shortcuts') {
|
||||||
return {
|
return {
|
||||||
action,
|
action,
|
||||||
|
|||||||
@@ -29,6 +29,19 @@ test('resolveWindowsMpvPath prefers SUBMINER_MPV_PATH', () => {
|
|||||||
assert.equal(resolved, 'C:\\mpv\\mpv.exe');
|
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', () => {
|
test('resolveWindowsMpvPath falls back to where.exe output', () => {
|
||||||
const resolved = resolveWindowsMpvPath(
|
const resolved = resolveWindowsMpvPath(
|
||||||
createDeps({
|
createDeps({
|
||||||
@@ -132,7 +145,7 @@ test('launchWindowsMpv reports missing mpv path', () => {
|
|||||||
|
|
||||||
assert.equal(result.ok, false);
|
assert.equal(result.ok, false);
|
||||||
assert.equal(result.mpvPath, '');
|
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', () => {
|
test('launchWindowsMpv spawns detached mpv with targets', () => {
|
||||||
|
|||||||
@@ -13,7 +13,15 @@ function normalizeCandidate(candidate: string | undefined): string {
|
|||||||
return typeof candidate === 'string' ? candidate.trim() : '';
|
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'));
|
const envPath = normalizeCandidate(deps.getEnv('SUBMINER_MPV_PATH'));
|
||||||
if (envPath && deps.fileExists(envPath)) {
|
if (envPath && deps.fileExists(envPath)) {
|
||||||
return envPath;
|
return envPath;
|
||||||
@@ -97,12 +105,13 @@ export function launchWindowsMpv(
|
|||||||
extraArgs: string[] = [],
|
extraArgs: string[] = [],
|
||||||
binaryPath?: string,
|
binaryPath?: string,
|
||||||
pluginEntrypointPath?: string,
|
pluginEntrypointPath?: string,
|
||||||
|
configuredMpvPath?: string,
|
||||||
): { ok: boolean; mpvPath: string } {
|
): { ok: boolean; mpvPath: string } {
|
||||||
const mpvPath = resolveWindowsMpvPath(deps);
|
const mpvPath = resolveWindowsMpvPath(deps, configuredMpvPath);
|
||||||
if (!mpvPath) {
|
if (!mpvPath) {
|
||||||
deps.showError(
|
deps.showError(
|
||||||
'SubMiner mpv launcher',
|
'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: '' };
|
return { ok: false, mpvPath: '' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ export interface TexthookerConfig {
|
|||||||
openBrowser?: boolean;
|
openBrowser?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MpvConfig {
|
||||||
|
executablePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type SubsyncMode = 'auto' | 'manual';
|
export type SubsyncMode = 'auto' | 'manual';
|
||||||
|
|
||||||
export interface SubsyncConfig {
|
export interface SubsyncConfig {
|
||||||
@@ -90,6 +94,7 @@ export interface Config {
|
|||||||
websocket?: WebSocketConfig;
|
websocket?: WebSocketConfig;
|
||||||
annotationWebsocket?: AnnotationWebSocketConfig;
|
annotationWebsocket?: AnnotationWebSocketConfig;
|
||||||
texthooker?: TexthookerConfig;
|
texthooker?: TexthookerConfig;
|
||||||
|
mpv?: MpvConfig;
|
||||||
controller?: ControllerConfig;
|
controller?: ControllerConfig;
|
||||||
ankiConnect?: AnkiConnectConfig;
|
ankiConnect?: AnkiConnectConfig;
|
||||||
shortcuts?: ShortcutsConfig;
|
shortcuts?: ShortcutsConfig;
|
||||||
@@ -122,6 +127,9 @@ export interface ResolvedConfig {
|
|||||||
websocket: Required<WebSocketConfig>;
|
websocket: Required<WebSocketConfig>;
|
||||||
annotationWebsocket: Required<AnnotationWebSocketConfig>;
|
annotationWebsocket: Required<AnnotationWebSocketConfig>;
|
||||||
texthooker: Required<TexthookerConfig>;
|
texthooker: Required<TexthookerConfig>;
|
||||||
|
mpv: {
|
||||||
|
executablePath: string;
|
||||||
|
};
|
||||||
controller: {
|
controller: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
preferredGamepadId: string;
|
preferredGamepadId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user