feat: inject bundled mpv plugin for managed launches, remove legacy glob (#62)

* 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

* harden bootstrap version load and clean plugin on uninstall

- Use pcall for version.lua in bootstrap.lua so missing version module does not crash plugin startup
- Remove plugin/subminer from app-data dirs in uninstall-linux and uninstall-macos targets
- Add Lua compat test asserting bootstrap uses defensive pcall for version load
- Add release-workflow test asserting uninstall targets clean bundled plugin dirs
- Delete completed planning document
This commit is contained in:
2026-05-12 23:11:19 -07:00
committed by GitHub
parent e5c1135501
commit 7c9b65db8b
43 changed files with 2116 additions and 481 deletions
+43 -3
View File
@@ -2,6 +2,7 @@ import fs from 'node:fs';
import { spawn, spawnSync } from 'node:child_process';
import { buildMpvLaunchModeArgs } from '../../shared/mpv-launch-mode';
import type { MpvLaunchMode } from '../../types/config';
import type { InstalledMpvPluginDetection } from './first-run-setup-plugin';
export interface WindowsMpvLaunchDeps {
getEnv: (name: string) => string | undefined;
@@ -13,6 +14,15 @@ export interface WindowsMpvLaunchDeps {
export type ConfiguredWindowsMpvPathStatus = 'blank' | 'configured' | 'invalid';
export interface WindowsMpvRuntimePluginPolicy {
detectInstalledMpvPlugin?: (mpvPath: string) => InstalledMpvPluginDetection;
notifyInstalledPluginDetected?: (detection: InstalledMpvPluginDetection) => void;
resolveInstalledPluginBeforeLaunch?: (
detection: InstalledMpvPluginDetection,
mpvPath: string,
) => Promise<'removed' | 'continue' | 'cancel'> | 'removed' | 'continue' | 'cancel';
}
function normalizeCandidate(candidate: string | undefined): string {
return typeof candidate === 'string' ? candidate.trim() : '';
}
@@ -100,10 +110,12 @@ export function buildWindowsMpvLaunchArgs(
typeof pluginEntrypointPath === 'string' && pluginEntrypointPath.trim().length > 0
? `--script=${pluginEntrypointPath.trim()}`
: null;
const scriptOptPairs = scriptEntrypoint
const hasBinaryPath = typeof binaryPath === 'string' && binaryPath.trim().length > 0;
const shouldPassSubminerScriptOpts = scriptEntrypoint || hasBinaryPath;
const scriptOptPairs = shouldPassSubminerScriptOpts
? [`subminer-socket_path=${inputIpcServer.replace(/,/g, '\\,')}`]
: [];
if (scriptEntrypoint && typeof binaryPath === 'string' && binaryPath.trim().length > 0) {
if (hasBinaryPath) {
scriptOptPairs.unshift(`subminer-binary_path=${binaryPath.trim().replace(/,/g, '\\,')}`);
}
const scriptOpts = scriptOptPairs.length > 0 ? `--script-opts=${scriptOptPairs.join(',')}` : null;
@@ -136,6 +148,7 @@ export async function launchWindowsMpv(
pluginEntrypointPath?: string,
configuredMpvPath?: string,
launchMode: MpvLaunchMode = 'normal',
runtimePluginPolicy?: WindowsMpvRuntimePluginPolicy,
): Promise<{ ok: boolean; mpvPath: string }> {
const normalizedConfiguredPath = normalizeCandidate(configuredMpvPath);
const mpvPath = resolveWindowsMpvPath(deps, normalizedConfiguredPath);
@@ -150,9 +163,36 @@ export async function launchWindowsMpv(
}
try {
let installedPlugin = runtimePluginPolicy?.detectInstalledMpvPlugin?.(mpvPath);
let installedPluginPrompted = false;
if (installedPlugin?.installed) {
const resolution = await runtimePluginPolicy?.resolveInstalledPluginBeforeLaunch?.(
installedPlugin,
mpvPath,
);
installedPluginPrompted = resolution != null;
if (resolution === 'cancel') {
return { ok: false, mpvPath };
}
if (resolution === 'removed') {
installedPlugin = runtimePluginPolicy?.detectInstalledMpvPlugin?.(mpvPath);
}
}
const runtimePluginEntrypointPath = installedPlugin?.installed
? undefined
: pluginEntrypointPath;
if (installedPlugin?.installed && !installedPluginPrompted) {
runtimePluginPolicy?.notifyInstalledPluginDetected?.(installedPlugin);
}
await deps.spawnDetached(
mpvPath,
buildWindowsMpvLaunchArgs(targets, extraArgs, binaryPath, pluginEntrypointPath, launchMode),
buildWindowsMpvLaunchArgs(
targets,
extraArgs,
binaryPath,
runtimePluginEntrypointPath,
launchMode,
),
);
return { ok: true, mpvPath };
} catch (error) {