mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-13 08:12:54 -07:00
feat: inject bundled mpv plugin for managed launches, remove legacy glob
- SubMiner-managed launcher and Windows shortcut launches inject the bundled plugin when no global plugin is detected - First-run setup detects and removes legacy global plugin files via OS trash before managed playback starts - Makefile `install-plugin` target and Windows config-rewrite script removed; Linux/macOS install now copies plugin to app data dir - AniList stats search and post-watch tracking now go through the shared rate limiter - Stats cover-art lookup reuses cached AniList data before issuing a new request - Closing mpv in a launcher-managed session now terminates the background Electron app
This commit is contained in:
+216
-2
@@ -4,6 +4,10 @@ import os from 'node:os';
|
||||
import net from 'node:net';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { buildMpvLaunchModeArgs } from '../src/shared/mpv-launch-mode.js';
|
||||
import {
|
||||
detectInstalledMpvPlugin,
|
||||
type InstalledMpvPluginDetection,
|
||||
} from '../src/main/runtime/first-run-setup-plugin.js';
|
||||
import type { LogLevel, Backend, Args, MpvTrack } from './types.js';
|
||||
import { DEFAULT_MPV_SUBMINER_ARGS, DEFAULT_YOUTUBE_YTDL_FORMAT } from './types.js';
|
||||
import { appendToAppLog, getAppLogPath, log, fail, getMpvLogPath } from './log.js';
|
||||
@@ -42,6 +46,13 @@ const DETACHED_IDLE_MPV_PID_FILE = path.join(os.tmpdir(), 'subminer-idle-mpv.pid
|
||||
const OVERLAY_START_SOCKET_READY_TIMEOUT_MS = 900;
|
||||
const OVERLAY_START_COMMAND_SETTLE_TIMEOUT_MS = 700;
|
||||
|
||||
export interface LauncherRuntimePluginPlan {
|
||||
scriptPath: string | null;
|
||||
installedPlugin: InstalledMpvPluginDetection;
|
||||
warningMessage: string | null;
|
||||
errorMessage: string | null;
|
||||
}
|
||||
|
||||
export function parseMpvArgString(input: string): string[] {
|
||||
const chars = input;
|
||||
const args: string[] = [];
|
||||
@@ -226,6 +237,182 @@ export function makeTempDir(prefix: string): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
function normalizeRuntimePluginEntrypoint(
|
||||
candidate: string,
|
||||
deps: {
|
||||
pathModule: typeof path;
|
||||
existsSync: (candidate: string) => boolean;
|
||||
},
|
||||
): string | null {
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.endsWith('.lua')) {
|
||||
return deps.existsSync(trimmed) ? trimmed : null;
|
||||
}
|
||||
const entrypoint = deps.pathModule.join(trimmed, 'main.lua');
|
||||
return deps.existsSync(entrypoint) ? entrypoint : null;
|
||||
}
|
||||
|
||||
function pushMacAppRuntimePluginCandidate(
|
||||
candidates: string[],
|
||||
appPath: string,
|
||||
pathModule: typeof path,
|
||||
): void {
|
||||
const appIndex = appPath.indexOf('.app');
|
||||
if (appIndex < 0) return;
|
||||
candidates.push(
|
||||
pathModule.join(
|
||||
appPath.slice(0, appIndex + '.app'.length),
|
||||
'Contents',
|
||||
'Resources',
|
||||
'plugin',
|
||||
'subminer',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveLauncherRuntimePluginPath(options: {
|
||||
appPath: string;
|
||||
scriptPath?: string;
|
||||
platform?: NodeJS.Platform;
|
||||
homeDir?: string;
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
dirname?: string;
|
||||
pathModule?: typeof path;
|
||||
existsSync?: (candidate: string) => boolean;
|
||||
}): string | null {
|
||||
const pathModule = options.pathModule ?? path;
|
||||
const existsSync = options.existsSync ?? fs.existsSync;
|
||||
const env = options.env ?? process.env;
|
||||
const dirname = options.dirname ?? __dirname;
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const platform = options.platform ?? process.platform;
|
||||
const homeDir = options.homeDir ?? os.homedir();
|
||||
const candidates: string[] = [];
|
||||
|
||||
if (env.SUBMINER_MPV_PLUGIN_PATH) {
|
||||
candidates.push(env.SUBMINER_MPV_PLUGIN_PATH);
|
||||
}
|
||||
|
||||
pushMacAppRuntimePluginCandidate(candidates, options.appPath, pathModule);
|
||||
|
||||
const appDir = pathModule.dirname(options.appPath);
|
||||
candidates.push(
|
||||
pathModule.join(appDir, 'resources', 'plugin', 'subminer'),
|
||||
pathModule.join(appDir, 'plugin', 'subminer'),
|
||||
);
|
||||
|
||||
if (options.scriptPath) {
|
||||
const scriptDir = pathModule.dirname(realpathMaybe(options.scriptPath));
|
||||
candidates.push(
|
||||
pathModule.join(scriptDir, '..', 'share', 'SubMiner', 'plugin', 'subminer'),
|
||||
pathModule.join(scriptDir, '..', 'lib', 'SubMiner', 'plugin', 'subminer'),
|
||||
pathModule.join(scriptDir, 'plugin', 'subminer'),
|
||||
);
|
||||
}
|
||||
|
||||
if (platform === 'darwin') {
|
||||
candidates.push(
|
||||
pathModule.join(homeDir, 'Library', 'Application Support', 'SubMiner', 'plugin', 'subminer'),
|
||||
);
|
||||
} else if (platform !== 'win32') {
|
||||
candidates.push(
|
||||
pathModule.join(
|
||||
env.XDG_DATA_HOME?.trim() || pathModule.join(homeDir, '.local', 'share'),
|
||||
'SubMiner',
|
||||
'plugin',
|
||||
'subminer',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
candidates.push(
|
||||
pathModule.join(cwd, 'plugin', 'subminer'),
|
||||
pathModule.join(dirname, '..', 'plugin', 'subminer'),
|
||||
pathModule.join(dirname, '..', '..', 'plugin', 'subminer'),
|
||||
);
|
||||
|
||||
const seen = new Set<string>();
|
||||
for (const candidate of candidates) {
|
||||
const resolved = pathModule.resolve(candidate);
|
||||
if (seen.has(resolved)) continue;
|
||||
seen.add(resolved);
|
||||
const entrypoint = normalizeRuntimePluginEntrypoint(resolved, { pathModule, existsSync });
|
||||
if (entrypoint) {
|
||||
return entrypoint;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveLauncherRuntimePluginPlan(options: {
|
||||
runtimePluginPath?: string | null;
|
||||
platform?: NodeJS.Platform;
|
||||
homeDir?: string;
|
||||
xdgConfigHome?: string;
|
||||
appDataDir?: string;
|
||||
mpvExecutablePath?: string;
|
||||
existsSync?: (candidate: string) => boolean;
|
||||
readFileSync?: (candidate: string, encoding: BufferEncoding) => string;
|
||||
}): LauncherRuntimePluginPlan {
|
||||
const installedPlugin = detectInstalledMpvPlugin({
|
||||
platform: options.platform ?? process.platform,
|
||||
homeDir: options.homeDir ?? os.homedir(),
|
||||
xdgConfigHome: options.xdgConfigHome ?? process.env.XDG_CONFIG_HOME,
|
||||
appDataDir: options.appDataDir ?? process.env.APPDATA,
|
||||
mpvExecutablePath: options.mpvExecutablePath,
|
||||
existsSync: options.existsSync,
|
||||
readFileSync: options.readFileSync,
|
||||
});
|
||||
|
||||
if (installedPlugin.installed) {
|
||||
const versionText = installedPlugin.version
|
||||
? `Detected plugin version: ${installedPlugin.version}.`
|
||||
: 'Detected plugin version: unknown or legacy.';
|
||||
return {
|
||||
scriptPath: null,
|
||||
installedPlugin,
|
||||
warningMessage: `SubMiner detected an installed mpv plugin at: ${installedPlugin.path}. This mpv session will use the installed plugin. Remove it to use SubMiner's bundled runtime plugin automatically. ${versionText}`,
|
||||
errorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (options.runtimePluginPath) {
|
||||
return {
|
||||
scriptPath: options.runtimePluginPath,
|
||||
installedPlugin,
|
||||
warningMessage: null,
|
||||
errorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scriptPath: null,
|
||||
installedPlugin,
|
||||
warningMessage: null,
|
||||
errorMessage:
|
||||
'Packaged mpv plugin assets were not found. Install the SubMiner assets bundle or set SUBMINER_MPV_PLUGIN_PATH to plugin/subminer/main.lua.',
|
||||
};
|
||||
}
|
||||
|
||||
function appendRuntimePluginLaunchArgs(
|
||||
mpvArgs: string[],
|
||||
plan: LauncherRuntimePluginPlan,
|
||||
logLevel: LogLevel,
|
||||
): void {
|
||||
if (plan.warningMessage) {
|
||||
log('warn', logLevel, plan.warningMessage);
|
||||
}
|
||||
if (plan.errorMessage) {
|
||||
fail(plan.errorMessage);
|
||||
}
|
||||
if (plan.scriptPath) {
|
||||
mpvArgs.push(`--script=${plan.scriptPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function detectBackend(
|
||||
backend: Backend,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
@@ -658,7 +845,11 @@ export async function startMpv(
|
||||
socketPath: string,
|
||||
appPath: string,
|
||||
preloadedSubtitles?: { primaryPath?: string; secondaryPath?: string },
|
||||
options?: { startPaused?: boolean; disableYoutubeSubtitleAutoLoad?: boolean },
|
||||
options?: {
|
||||
startPaused?: boolean;
|
||||
disableYoutubeSubtitleAutoLoad?: boolean;
|
||||
runtimePluginPath?: string | null;
|
||||
},
|
||||
): Promise<void> {
|
||||
if (targetKind === 'file' && (!fs.existsSync(target) || !fs.statSync(target).isFile())) {
|
||||
fail(`Video file not found: ${target}`);
|
||||
@@ -672,6 +863,14 @@ export async function startMpv(
|
||||
|
||||
const mpvArgs: string[] = [];
|
||||
mpvArgs.push(...buildConfiguredMpvDefaultArgs(args));
|
||||
appendRuntimePluginLaunchArgs(
|
||||
mpvArgs,
|
||||
resolveLauncherRuntimePluginPlan({
|
||||
runtimePluginPath:
|
||||
options?.runtimePluginPath ?? resolveLauncherRuntimePluginPath({ appPath }),
|
||||
}),
|
||||
args.logLevel,
|
||||
);
|
||||
if (targetKind === 'url' && isYoutubeTarget(target)) {
|
||||
log('info', args.logLevel, 'Applying URL playback options');
|
||||
mpvArgs.push('--ytdl=yes');
|
||||
@@ -811,7 +1010,7 @@ export async function startOverlay(
|
||||
env: buildAppEnv(),
|
||||
});
|
||||
attachAppProcessLogging(state.overlayProc);
|
||||
state.overlayManagedByLauncher = true;
|
||||
markOverlayManagedByLauncher(appPath);
|
||||
|
||||
const [socketReady] = await Promise.all([
|
||||
waitForUnixSocketReady(socketPath, OVERLAY_START_SOCKET_READY_TIMEOUT_MS),
|
||||
@@ -831,6 +1030,13 @@ export async function startOverlay(
|
||||
}
|
||||
}
|
||||
|
||||
export function markOverlayManagedByLauncher(appPath?: string): void {
|
||||
if (appPath) {
|
||||
state.appPath = appPath;
|
||||
}
|
||||
state.overlayManagedByLauncher = true;
|
||||
}
|
||||
|
||||
export function openUrlInDefaultBrowser(url: string, logLevel: LogLevel): void {
|
||||
const target =
|
||||
process.platform === 'darwin'
|
||||
@@ -1236,6 +1442,7 @@ export function launchMpvIdleDetached(
|
||||
socketPath: string,
|
||||
appPath: string,
|
||||
args: Args,
|
||||
runtimePluginPath?: string | null,
|
||||
): Promise<void> {
|
||||
return (async () => {
|
||||
await terminateTrackedDetachedMpv(args.logLevel);
|
||||
@@ -1246,6 +1453,13 @@ export function launchMpvIdleDetached(
|
||||
}
|
||||
|
||||
const mpvArgs: string[] = buildConfiguredMpvDefaultArgs(args);
|
||||
appendRuntimePluginLaunchArgs(
|
||||
mpvArgs,
|
||||
resolveLauncherRuntimePluginPlan({
|
||||
runtimePluginPath: runtimePluginPath ?? resolveLauncherRuntimePluginPath({ appPath }),
|
||||
}),
|
||||
args.logLevel,
|
||||
);
|
||||
if (args.mpvArgs) {
|
||||
mpvArgs.push(...parseMpvArgString(args.mpvArgs));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user