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
+58 -8
View File
@@ -32,31 +32,81 @@ export async function waitForSetupCompletion(deps: {
return 'timeout';
}
export async function waitForLegacyMpvPluginPromptResolution(deps: {
readSetupState: () => SetupState | null;
sleep: (ms: number) => Promise<void>;
now: () => number;
timeoutMs: number;
pollIntervalMs: number;
initialState?: SetupState | null;
}): Promise<'acknowledged' | 'cancelled' | 'timeout'> {
const deadline = deps.now() + deps.timeoutMs;
const initialCompleted = isSetupCompleted(deps.initialState);
const initialCompletedAt = deps.initialState?.completedAt ?? null;
while (deps.now() <= deadline) {
const state = deps.readSetupState();
if (
isSetupCompleted(state) &&
(!initialCompleted || state?.completedAt !== initialCompletedAt)
) {
return 'acknowledged';
}
if (!initialCompleted && state?.status === 'cancelled') {
return 'cancelled';
}
await deps.sleep(deps.pollIntervalMs);
}
return 'timeout';
}
export async function ensureLauncherSetupReady(deps: {
readSetupState: () => SetupState | null;
isExternalYomitanConfigured?: () => boolean;
isPluginInstalled?: () => boolean;
hasLegacyMpvPlugin?: () => boolean;
launchSetupApp: () => void;
sleep: (ms: number) => Promise<void>;
now: () => number;
timeoutMs: number;
pollIntervalMs: number;
}): Promise<boolean> {
const initialState = deps.readSetupState();
let setupLaunched = false;
const launchSetupApp = () => {
if (setupLaunched) return;
setupLaunched = true;
deps.launchSetupApp();
};
if (deps.hasLegacyMpvPlugin?.()) {
launchSetupApp();
const result = await waitForLegacyMpvPluginPromptResolution({
readSetupState: deps.readSetupState,
sleep: deps.sleep,
now: deps.now,
timeoutMs: deps.timeoutMs,
pollIntervalMs: deps.pollIntervalMs,
initialState,
});
if (result === 'cancelled' || result === 'timeout') {
return false;
}
}
if (deps.isExternalYomitanConfigured?.()) {
return true;
}
if (deps.isPluginInstalled?.()) {
return true;
}
const initialState = deps.readSetupState();
if (isSetupCompleted(initialState)) {
const stateAfterLegacyPrompt = deps.readSetupState();
if (isSetupCompleted(stateAfterLegacyPrompt)) {
return true;
}
deps.launchSetupApp();
launchSetupApp();
const result = await waitForSetupCompletion({
...deps,
ignoreInitialCancelledState: initialState?.status === 'cancelled',
ignoreInitialCancelledState: stateAfterLegacyPrompt?.status === 'cancelled',
});
return result === 'completed';
}