mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 17:01:25 -07:00
fix(jellyfin): find mpv when SubMiner is launched from Finder
Apps opened from Finder, the Dock, or Spotlight inherit launchd's minimal PATH, so spawning bare `mpv` failed for Homebrew installs and Jellyfin casting never got an IPC socket. - On macOS and Linux, resolve mpv from mpv.executablePath, SUBMINER_MPV_PATH, PATH, then (macOS) Homebrew, /usr/local/bin, and /Applications/mpv.app - An invalid mpv.executablePath errors instead of silently falling back - Timing preview uses the same lookup - Docs: mpv lookup order, a Jellyfin casting troubleshooting entry, and the macOS 27 rename of Accessibility to Device Control and Data Access Closes #297
This commit is contained in:
14 files changed
+186
-31
No files matched your search
@@ -5,9 +5,11 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createPosixMpvPathDeps,
|
||||
createWindowsMpvPathDeps,
|
||||
getConfiguredWindowsMpvPathStatus,
|
||||
resolveMpvExecutablePath,
|
||||
resolvePosixMpvPath,
|
||||
resolveWindowsMpvPath,
|
||||
spawnMpvProcess,
|
||||
} from './mpv-process';
|
||||
@@ -95,6 +97,79 @@ test('resolveWindowsMpvPath returns blank when nothing resolves', () => {
|
||||
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 = [
|
||||
{
|
||||
name: 'forces the X11 backend for an unsupported Wayland session',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import {
|
||||
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 {
|
||||
const executablePath =
|
||||
process.platform === 'win32'
|
||||
? resolveWindowsMpvPath(createWindowsMpvPathDeps(), configuredMpvPath)
|
||||
: 'mpv';
|
||||
const executablePath = findMpvExecutablePath(configuredMpvPath);
|
||||
if (!executablePath) {
|
||||
const binary = process.platform === 'win32' ? 'mpv.exe' : 'mpv';
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user