mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 05:01:26 -07:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be65241cc2
|
||
|
|
7c1c674313
|
No files matched your search
@@ -199,7 +199,7 @@ The AppImage is all you need. First-run setup can install the optional `subminer
|
|||||||
|
|
||||||
Download the latest DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
|
Download the latest DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
|
||||||
|
|
||||||
Then enable SubMiner under **System Settings > Privacy & Security > Accessibility**, or the overlay cannot follow the mpv window. If macOS blocks the first launch, right-click the app and choose **Open**.
|
Then enable SubMiner under **System Settings > Privacy & Security > Accessibility** (named **Device Control and Data Access** on macOS 27), or the overlay cannot follow the mpv window. If macOS blocks the first launch, right-click the app and choose **Open**.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"brace-expansion": "5.0.11",
|
"brace-expansion": "5.0.11",
|
||||||
"electron-builder-squirrel-windows": "26.16.1",
|
"electron-builder-squirrel-windows": "26.16.1",
|
||||||
"fast-uri": "3.1.7",
|
"fast-uri": "3.1.7",
|
||||||
|
"follow-redirects": "1.16.1",
|
||||||
"form-data": "4.0.6",
|
"form-data": "4.0.6",
|
||||||
"ip-address": "10.2.0",
|
"ip-address": "10.2.0",
|
||||||
"js-yaml": "4.3.2",
|
"js-yaml": "4.3.2",
|
||||||
@@ -350,7 +351,7 @@
|
|||||||
|
|
||||||
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
|
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
|
||||||
|
|
||||||
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
|
"follow-redirects": ["follow-redirects@1.16.1", "", {}, "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg=="],
|
||||||
|
|
||||||
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
|
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: docs
|
||||||
|
area: docs
|
||||||
|
|
||||||
|
- Documented how casting finds mpv on every platform, added a troubleshooting entry for casts that never start mpv, and noted that macOS 27 renames the Accessibility privacy list to "Device Control and Data Access".
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: jellyfin
|
||||||
|
|
||||||
|
- Fixed casting from Jellyfin on macOS when SubMiner is opened from Finder, the Dock, or Spotlight. SubMiner now honors `mpv.executablePath` and `SUBMINER_MPV_PATH` on macOS and Linux, and on macOS also finds mpv in `/opt/homebrew/bin`, `/usr/local/bin`, and `/Applications/mpv.app` instead of failing with `connect ENOENT /tmp/subminer-socket`.
|
||||||
|
- The subtitle timing preview uses the same mpv lookup.
|
||||||
@@ -734,10 +734,10 @@
|
|||||||
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
|
// 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.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.
|
// 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.
|
// Leave mpv.executablePath blank to auto-discover mpv from SUBMINER_MPV_PATH, PATH, or (macOS) Homebrew and /Applications/mpv.app.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"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 the mpv binary SubMiner starts itself (Windows launches, Jellyfin casting, timing preview). Leave empty to auto-discover from SUBMINER_MPV_PATH, PATH, and on macOS the Homebrew and /Applications/mpv.app locations.
|
||||||
"launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
"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.
|
"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.
|
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
|
||||||
|
|||||||
+12
-12
@@ -579,18 +579,18 @@ A local web dashboard at `http://127.0.0.1:<serverPort>`, also available as an o
|
|||||||
|
|
||||||
Settings for mpv instances that SubMiner starts, and for the bundled mpv plugin. See [mpv plugin](/mpv-plugin).
|
Settings for mpv instances that SubMiner starts, and for the bundled mpv plugin. See [mpv plugin](/mpv-plugin).
|
||||||
|
|
||||||
| Key | Default | What it does |
|
| Key | Default | What it does |
|
||||||
| ---------------------------- | ----------------- | --------------------------------------------------------------------------- |
|
| ---------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------- |
|
||||||
| `mpv.executablePath` | `""` | Path to `mpv.exe` on Windows. Empty checks `SUBMINER_MPV_PATH`, then `PATH` |
|
| `mpv.executablePath` | `""` | Full path to mpv. Empty auto-discovers it ([lookup order](/jellyfin-integration#casting-from-jellyfin)) |
|
||||||
| `mpv.launchMode` | `"normal"` | Window state: `normal`, `maximized`, or `fullscreen` |
|
| `mpv.launchMode` | `"normal"` | Window state: `normal`, `maximized`, or `fullscreen` |
|
||||||
| `mpv.profile` | `""` | mpv profile to pass. Combined with a launcher `--profile` if both are set |
|
| `mpv.profile` | `""` | mpv profile to pass. Combined with a launcher `--profile` if both are set |
|
||||||
| `mpv.socketPath` | platform-specific | mpv IPC socket. See the warning under [Config file](#configuration-file) |
|
| `mpv.socketPath` | platform-specific | mpv IPC socket. See the warning under [Config file](#configuration-file) |
|
||||||
| `mpv.backend` | `"auto"` | Window tracking: `auto`, `hyprland`, `sway`, `x11`, `macos`, `windows` |
|
| `mpv.backend` | `"auto"` | Window tracking: `auto`, `hyprland`, `sway`, `x11`, `macos`, `windows` |
|
||||||
| `mpv.autoStartSubMiner` | `true` | Start SubMiner in the background when mpv loads a file |
|
| `mpv.autoStartSubMiner` | `true` | Start SubMiner in the background when mpv loads a file |
|
||||||
| `mpv.pauseUntilOverlayReady` | `true` | Keep mpv paused until subtitles are ready, up to 30 seconds |
|
| `mpv.pauseUntilOverlayReady` | `true` | Keep mpv paused until subtitles are ready, up to 30 seconds |
|
||||||
| `mpv.subminerBinaryPath` | `""` | SubMiner app path for the plugin. Empty uses the detected path |
|
| `mpv.subminerBinaryPath` | `""` | SubMiner app path for the plugin. Empty uses the detected path |
|
||||||
| `mpv.aniskipEnabled` | `true` | Detect intros with AniSkip and show a skip prompt |
|
| `mpv.aniskipEnabled` | `true` | Detect intros with AniSkip and show a skip prompt |
|
||||||
| `mpv.aniskipButtonKey` | `"TAB"` | mpv key that skips the intro while the prompt is shown |
|
| `mpv.aniskipButtonKey` | `"TAB"` | mpv key that skips the intro while the prompt is shown |
|
||||||
|
|
||||||
### YouTube playback settings
|
### YouTube playback settings
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ First-run setup can install the `subminer` command for you.
|
|||||||
xattr -d com.apple.quarantine /Applications/SubMiner.app
|
xattr -d com.apple.quarantine /Applications/SubMiner.app
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Open **System Settings > Privacy & Security > Accessibility** and enable SubMiner (add it if it is missing). The overlay cannot follow the mpv window without this.
|
3. Open **System Settings > Privacy & Security > Accessibility** and enable SubMiner (add it if it is missing). On macOS 27 this list is named **Device Control and Data Access**. The overlay cannot follow the mpv window without this.
|
||||||
|
|
||||||
First-run setup can install the `subminer` command for you.
|
First-run setup can install the `subminer` command for you.
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ SubMiner selects a Japanese subtitle track automatically and resets mpv's subtit
|
|||||||
|
|
||||||
If an episode has no Japanese subtitles, you can [generate them with Whisper](/subtitle-generation#generating-from-the-overlay) while it plays.
|
If an episode has no Japanese subtitles, you can [generate them with Whisper](/subtitle-generation#generating-from-the-overlay) while it plays.
|
||||||
|
|
||||||
On Windows, casting finds mpv through `mpv.executablePath`, then `SUBMINER_MPV_PATH`, then `PATH`. An invalid `mpv.executablePath` stops mpv from starting.
|
Casting finds mpv through `mpv.executablePath`, then `SUBMINER_MPV_PATH`, then `PATH`. On macOS it then checks `/opt/homebrew/bin/mpv`, `/usr/local/bin/mpv`, and `/Applications/mpv.app`, because an app opened from Finder, the Dock, or Spotlight does not get your shell's `PATH`. If mpv lives somewhere else, set `mpv.executablePath` to its full path (`command -v mpv` prints it). An invalid `mpv.executablePath` stops mpv from starting.
|
||||||
|
|
||||||
## Playing from the terminal
|
## Playing from the terminal
|
||||||
|
|
||||||
|
|||||||
@@ -734,10 +734,10 @@
|
|||||||
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
|
// 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.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.
|
// 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.
|
// Leave mpv.executablePath blank to auto-discover mpv from SUBMINER_MPV_PATH, PATH, or (macOS) Homebrew and /Applications/mpv.app.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"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 the mpv binary SubMiner starts itself (Windows launches, Jellyfin casting, timing preview). Leave empty to auto-discover from SUBMINER_MPV_PATH, PATH, and on macOS the Homebrew and /Applications/mpv.app locations.
|
||||||
"launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
"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.
|
"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.
|
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
|
||||||
|
|||||||
@@ -39,11 +39,18 @@ SubMiner reads subtitles from mpv over an IPC socket (a named pipe on Windows).
|
|||||||
|
|
||||||
SubMiner reconnects on its own if mpv restarts.
|
SubMiner reconnects on its own if mpv restarts.
|
||||||
|
|
||||||
|
## Casting from Jellyfin does nothing
|
||||||
|
|
||||||
|
When `connect ENOENT /tmp/subminer-socket` follows a cast in the app log, SubMiner could not start mpv, so the socket never appeared. Look for `Failed handling Jellyfin remote Play event` or `Failed to launch mpv for Jellyfin remote playback` just before it.
|
||||||
|
|
||||||
|
- Check that `mpv --version` works in a terminal.
|
||||||
|
- If it does, SubMiner is not finding mpv where it is installed. Set `mpv.executablePath` to the output of `command -v mpv`. See [Casting from Jellyfin](/jellyfin-integration#casting-from-jellyfin) for where SubMiner looks.
|
||||||
|
|
||||||
## Overlay does not appear
|
## Overlay does not appear
|
||||||
|
|
||||||
- Confirm SubMiner is running (`SubMiner.AppImage --start`, or check for the process).
|
- Confirm SubMiner is running (`SubMiner.AppImage --start`, or check for the process).
|
||||||
- Linux: Hyprland and Sway work natively. Any other compositor needs mpv and SubMiner under X11 or Xwayland, with `xdotool`, `xprop`, and `xwininfo` installed. See [KDE Plasma and other Wayland compositors](#kde-plasma-and-other-wayland-compositors).
|
- Linux: Hyprland and Sway work natively. Any other compositor needs mpv and SubMiner under X11 or Xwayland, with `xdotool`, `xprop`, and `xwininfo` installed. See [KDE Plasma and other Wayland compositors](#kde-plasma-and-other-wayland-compositors).
|
||||||
- macOS: grant Accessibility permission in System Settings > Privacy & Security > Accessibility.
|
- macOS: grant Accessibility permission. See [macOS](#macos).
|
||||||
|
|
||||||
## Overlay is on the wrong monitor or position
|
## Overlay is on the wrong monitor or position
|
||||||
|
|
||||||
@@ -255,5 +262,5 @@ mpv --gpu-context=x11vk,x11egl,x11 video.mkv
|
|||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
|
|
||||||
- Accessibility permission is required for window tracking: System Settings > Privacy & Security > Accessibility.
|
- Accessibility permission is required for window tracking: System Settings > Privacy & Security > Accessibility. On macOS 27 this list is named **Device Control and Data Access**.
|
||||||
- If Gatekeeper blocks the app, right-click it and choose Open, or run `xattr -d com.apple.quarantine /path/to/SubMiner.app`.
|
- If Gatekeeper blocks the app, right-click it and choose Open, or run `xattr -d com.apple.quarantine /path/to/SubMiner.app`.
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
"brace-expansion": "5.0.11",
|
"brace-expansion": "5.0.11",
|
||||||
"electron-builder-squirrel-windows": "26.16.1",
|
"electron-builder-squirrel-windows": "26.16.1",
|
||||||
"fast-uri": "3.1.7",
|
"fast-uri": "3.1.7",
|
||||||
|
"follow-redirects": "1.16.1",
|
||||||
"form-data": "4.0.6",
|
"form-data": "4.0.6",
|
||||||
"ip-address": "10.2.0",
|
"ip-address": "10.2.0",
|
||||||
"js-yaml": "4.3.2",
|
"js-yaml": "4.3.2",
|
||||||
|
|||||||
@@ -554,7 +554,7 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
kind: 'string',
|
kind: 'string',
|
||||||
defaultValue: defaultConfig.mpv.executablePath,
|
defaultValue: defaultConfig.mpv.executablePath,
|
||||||
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 the mpv binary SubMiner starts itself (Windows launches, Jellyfin casting, timing preview). Leave empty to auto-discover from SUBMINER_MPV_PATH, PATH, and on macOS the Homebrew and /Applications/mpv.app locations.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mpv.launchMode',
|
path: 'mpv.launchMode',
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
|||||||
'autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.',
|
'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.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.',
|
'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.',
|
'Leave mpv.executablePath blank to auto-discover mpv from SUBMINER_MPV_PATH, PATH, or (macOS) Homebrew and /Applications/mpv.app.',
|
||||||
],
|
],
|
||||||
key: 'mpv',
|
key: 'mpv',
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-3
@@ -392,7 +392,11 @@ import {
|
|||||||
getConfiguredWindowsMpvPathStatus,
|
getConfiguredWindowsMpvPathStatus,
|
||||||
launchWindowsMpv,
|
launchWindowsMpv,
|
||||||
} from './main/runtime/windows-mpv-launch';
|
} from './main/runtime/windows-mpv-launch';
|
||||||
import { resolveMpvExecutablePath, spawnMpvProcess } from './main/runtime/mpv-process';
|
import {
|
||||||
|
findMpvExecutablePath,
|
||||||
|
resolveMpvExecutablePath,
|
||||||
|
spawnMpvProcess,
|
||||||
|
} from './main/runtime/mpv-process';
|
||||||
import { createWaitForMpvConnectedHandler } from './main/runtime/jellyfin-remote-connection';
|
import { createWaitForMpvConnectedHandler } from './main/runtime/jellyfin-remote-connection';
|
||||||
import {
|
import {
|
||||||
DEFAULT_JELLYFIN_CLIENT_NAME,
|
DEFAULT_JELLYFIN_CLIENT_NAME,
|
||||||
@@ -2995,8 +2999,7 @@ const mediaTimingReviewRuntime = createMediaTimingReviewRuntime({
|
|||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
getCurrentMediaPath: () =>
|
getCurrentMediaPath: () =>
|
||||||
appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
|
appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
|
||||||
getMpvExecutablePath: () =>
|
getMpvExecutablePath: () => findMpvExecutablePath(configService.getConfig().mpv.executablePath),
|
||||||
configService.getConfig().mpv.executablePath || process.env.SUBMINER_MPV_PATH?.trim() || '',
|
|
||||||
createPreviewSession: () => new MediaTimingPreviewSession(),
|
createPreviewSession: () => new MediaTimingPreviewSession(),
|
||||||
generateWaveform: (options) => generateSpeechWaveform(options),
|
generateWaveform: (options) => generateSpeechWaveform(options),
|
||||||
generateFrame: (options) => mediaTimingFrameExtractor.generate(options),
|
generateFrame: (options) => mediaTimingFrameExtractor.generate(options),
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import {
|
import {
|
||||||
|
createPosixMpvPathDeps,
|
||||||
createWindowsMpvPathDeps,
|
createWindowsMpvPathDeps,
|
||||||
getConfiguredWindowsMpvPathStatus,
|
getConfiguredWindowsMpvPathStatus,
|
||||||
resolveMpvExecutablePath,
|
resolveMpvExecutablePath,
|
||||||
|
resolvePosixMpvPath,
|
||||||
resolveWindowsMpvPath,
|
resolveWindowsMpvPath,
|
||||||
spawnMpvProcess,
|
spawnMpvProcess,
|
||||||
} from './mpv-process';
|
} from './mpv-process';
|
||||||
@@ -95,6 +97,79 @@ test('resolveWindowsMpvPath returns blank when nothing resolves', () => {
|
|||||||
assert.equal(resolveWindowsMpvPath(missing.deps), '');
|
assert.equal(resolveWindowsMpvPath(missing.deps), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// launchd's default PATH, which apps opened from Finder, the Dock, or Spotlight inherit.
|
||||||
|
const LAUNCHD_PATH = '/usr/bin:/bin:/usr/sbin:/sbin';
|
||||||
|
|
||||||
|
function makePosixDeps(options: {
|
||||||
|
platform: NodeJS.Platform;
|
||||||
|
executables?: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
}) {
|
||||||
|
const executables = new Set(options.executables ?? []);
|
||||||
|
return createPosixMpvPathDeps({
|
||||||
|
platform: options.platform,
|
||||||
|
getEnv: (name) => options.env?.[name],
|
||||||
|
isExecutable: (candidate) => executables.has(candidate),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('resolvePosixMpvPath prefers a configured executable and does not fall back when it is invalid', () => {
|
||||||
|
const deps = makePosixDeps({
|
||||||
|
platform: 'darwin',
|
||||||
|
executables: ['/cfg/mpv', '/opt/homebrew/bin/mpv'],
|
||||||
|
env: { PATH: LAUNCHD_PATH },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(resolvePosixMpvPath(deps, ' /cfg/mpv '), '/cfg/mpv');
|
||||||
|
assert.equal(resolvePosixMpvPath(deps, '/missing/mpv'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolvePosixMpvPath uses SUBMINER_MPV_PATH before searching PATH', () => {
|
||||||
|
const deps = makePosixDeps({
|
||||||
|
platform: 'linux',
|
||||||
|
executables: ['/env/mpv', '/usr/bin/mpv'],
|
||||||
|
env: { SUBMINER_MPV_PATH: ' /env/mpv ', PATH: '/usr/bin' },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(resolvePosixMpvPath(deps), '/env/mpv');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolvePosixMpvPath takes the first PATH entry that has mpv', () => {
|
||||||
|
const deps = makePosixDeps({
|
||||||
|
platform: 'darwin',
|
||||||
|
executables: ['/second/mpv', '/third/mpv', '/opt/homebrew/bin/mpv'],
|
||||||
|
env: { SUBMINER_MPV_PATH: '/stale/mpv', PATH: '/first::/second:/third' },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(resolvePosixMpvPath(deps), '/second/mpv');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolvePosixMpvPath finds Homebrew mpv on macOS when the app only has the launchd PATH', () => {
|
||||||
|
const homebrew = makePosixDeps({
|
||||||
|
platform: 'darwin',
|
||||||
|
executables: ['/opt/homebrew/bin/mpv'],
|
||||||
|
env: { PATH: LAUNCHD_PATH },
|
||||||
|
});
|
||||||
|
const appBundle = makePosixDeps({
|
||||||
|
platform: 'darwin',
|
||||||
|
executables: ['/Applications/mpv.app/Contents/MacOS/mpv'],
|
||||||
|
env: { PATH: LAUNCHD_PATH },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(resolvePosixMpvPath(homebrew), '/opt/homebrew/bin/mpv');
|
||||||
|
assert.equal(resolvePosixMpvPath(appBundle), '/Applications/mpv.app/Contents/MacOS/mpv');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolvePosixMpvPath only probes macOS install locations on macOS', () => {
|
||||||
|
const deps = makePosixDeps({
|
||||||
|
platform: 'linux',
|
||||||
|
executables: ['/opt/homebrew/bin/mpv'],
|
||||||
|
env: { PATH: LAUNCHD_PATH },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(resolvePosixMpvPath(deps), '');
|
||||||
|
});
|
||||||
|
|
||||||
const X11_SPAWN_CASES = [
|
const X11_SPAWN_CASES = [
|
||||||
{
|
{
|
||||||
name: 'forces the X11 backend for an unsupported Wayland session',
|
name: 'forces the X11 backend for an unsupported Wayland session',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
import { spawn, spawnSync } from 'node:child_process';
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
import {
|
import {
|
||||||
MPV_X11_BACKEND_ARGS,
|
MPV_X11_BACKEND_ARGS,
|
||||||
@@ -98,14 +99,74 @@ export function spawnMpvProcess(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PosixMpvPathDeps {
|
||||||
|
platform: NodeJS.Platform;
|
||||||
|
getEnv: (name: string) => string | undefined;
|
||||||
|
isExecutable: (candidate: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps opened from Finder, the Dock, or Spotlight inherit launchd's minimal PATH
|
||||||
|
// (/usr/bin:/bin:/usr/sbin:/sbin), so Homebrew and app-bundle installs are probed directly.
|
||||||
|
const MACOS_MPV_FALLBACK_PATHS = [
|
||||||
|
'/opt/homebrew/bin/mpv',
|
||||||
|
'/usr/local/bin/mpv',
|
||||||
|
'/Applications/mpv.app/Contents/MacOS/mpv',
|
||||||
|
];
|
||||||
|
|
||||||
|
function isExecutableFile(candidate: string): boolean {
|
||||||
|
try {
|
||||||
|
fs.accessSync(candidate, fs.constants.X_OK);
|
||||||
|
return fs.statSync(candidate).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPosixMpvPathDeps(
|
||||||
|
overrides: Partial<PosixMpvPathDeps> = {},
|
||||||
|
): PosixMpvPathDeps {
|
||||||
|
return {
|
||||||
|
platform: overrides.platform ?? process.platform,
|
||||||
|
getEnv: overrides.getEnv ?? ((name) => process.env[name]),
|
||||||
|
isExecutable: overrides.isExecutable ?? isExecutableFile,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolution order: mpv.executablePath, SUBMINER_MPV_PATH, PATH, then macOS install
|
||||||
|
// locations. A configured path that is not executable resolves to '' instead of falling back.
|
||||||
|
export function resolvePosixMpvPath(deps: PosixMpvPathDeps, configuredMpvPath = ''): string {
|
||||||
|
const configPath = configuredMpvPath.trim();
|
||||||
|
if (configPath) {
|
||||||
|
return deps.isExecutable(configPath) ? configPath : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const envPath = deps.getEnv('SUBMINER_MPV_PATH')?.trim();
|
||||||
|
if (envPath && deps.isExecutable(envPath)) {
|
||||||
|
return envPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathCandidates = (deps.getEnv('PATH') ?? '')
|
||||||
|
.split(path.posix.delimiter)
|
||||||
|
.filter((dir) => dir.length > 0)
|
||||||
|
.map((dir) => path.posix.join(dir, 'mpv'));
|
||||||
|
const candidates =
|
||||||
|
deps.platform === 'darwin' ? [...pathCandidates, ...MACOS_MPV_FALLBACK_PATHS] : pathCandidates;
|
||||||
|
return candidates.find((candidate) => deps.isExecutable(candidate)) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the mpv binary SubMiner should spawn itself, or '' when none is found.
|
||||||
|
export function findMpvExecutablePath(configuredMpvPath = ''): string {
|
||||||
|
return process.platform === 'win32'
|
||||||
|
? resolveWindowsMpvPath(createWindowsMpvPathDeps(), configuredMpvPath)
|
||||||
|
: resolvePosixMpvPath(createPosixMpvPathDeps(), configuredMpvPath);
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveMpvExecutablePath(configuredMpvPath = ''): string {
|
export function resolveMpvExecutablePath(configuredMpvPath = ''): string {
|
||||||
const executablePath =
|
const executablePath = findMpvExecutablePath(configuredMpvPath);
|
||||||
process.platform === 'win32'
|
|
||||||
? resolveWindowsMpvPath(createWindowsMpvPathDeps(), configuredMpvPath)
|
|
||||||
: 'mpv';
|
|
||||||
if (!executablePath) {
|
if (!executablePath) {
|
||||||
|
const binary = process.platform === 'win32' ? 'mpv.exe' : 'mpv';
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Could not find mpv.exe. Check mpv.executablePath, SUBMINER_MPV_PATH, or PATH.',
|
`Could not find ${binary}. Check mpv.executablePath, SUBMINER_MPV_PATH, or PATH.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return executablePath;
|
return executablePath;
|
||||||
|
|||||||
Reference in new issue
Block a user