fix: pin installed mpv plugin to current binary

This commit is contained in:
2026-03-29 15:16:48 -07:00
parent c939c5804f
commit a0b0b9f972
5 changed files with 186 additions and 4 deletions

View File

@@ -0,0 +1,37 @@
---
id: TASK-249
title: Fix AniList token persistence on setup login
status: Done
assignee: []
created_date: '2026-03-29 10:08'
updated_date: '2026-03-29 19:42'
labels:
- anilist
- bug
dependencies: []
documentation:
- src/main/runtime/anilist-setup.ts
- src/core/services/anilist/anilist-token-store.ts
- src/main/runtime/anilist-token-refresh.ts
- docs-site/anilist-integration.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
AniList setup can appear successful but the token is not persisted across restarts. Investigate the setup callback and token store path so the app either saves the token reliably or surfaces persistence failure instead of reopening setup on every launch.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 AniList setup login persists a usable token across app restarts when safeStorage works
- [ ] #2 If token persistence fails the setup flow reports the failure instead of pretending login succeeded
- [ ] #3 Regression coverage exists for the callback/save path and the refresh path that reopens setup when no token is available
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Pinned installed mpv plugin configs to the current SubMiner binary so standalone mpv launches reuse the same app identity that saved AniList tokens. Added startup self-heal for existing blank binary_path configs, install-time binary_path writes for fresh plugin installs, regression tests for both paths, and docs updates describing the new behavior.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -172,7 +172,7 @@ Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still re
### Windows Usage Notes ### Windows Usage Notes
- Launch `SubMiner.exe` once to let the first-run setup flow seed `%APPDATA%\\SubMiner\\config.jsonc`, offer mpv plugin installation, open bundled Yomitan settings, and optionally create `SubMiner mpv` Start Menu/Desktop shortcuts. - Launch `SubMiner.exe` once to let the first-run setup flow seed `%APPDATA%\\SubMiner\\config.jsonc`, offer mpv plugin installation, open bundled Yomitan settings, and optionally create `SubMiner mpv` Start Menu/Desktop shortcuts.
- If you use the mpv plugin, leave `binary_path` empty unless SubMiner is installed in a non-standard location. - First-run mpv plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is installed in a non-standard location.
- Windows plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket`; do not keep `/tmp/subminer-socket` on Windows. - Windows plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket`; do not keep `/tmp/subminer-socket` on Windows.
- Native window tracking is built in on Windows; no `xdotool`, `xwininfo`, or compositor-specific helper is required. - Native window tracking is built in on Windows; no `xdotool`, `xwininfo`, or compositor-specific helper is required.
@@ -201,6 +201,7 @@ mpv must be launched with `--input-ipc-server=/tmp/subminer-socket` for SubMiner
::: :::
On Windows, the packaged plugin config is rewritten to `socket_path=\\.\pipe\subminer-socket`. On Windows, the packaged plugin config is rewritten to `socket_path=\\.\pipe\subminer-socket`.
First-run setup also pins `binary_path` to the current app binary so mpv launches the same SubMiner build that installed the plugin.
```bash ```bash
# Option 1: install from release assets bundle # Option 1: install from release assets bundle

View File

@@ -356,6 +356,7 @@ import {
import { import {
detectInstalledFirstRunPlugin, detectInstalledFirstRunPlugin,
installFirstRunPluginToDefaultLocation, installFirstRunPluginToDefaultLocation,
syncInstalledFirstRunPluginBinaryPath,
} from './main/runtime/first-run-setup-plugin'; } from './main/runtime/first-run-setup-plugin';
import { import {
applyWindowsMpvShortcuts, applyWindowsMpvShortcuts,
@@ -1036,6 +1037,12 @@ const resolveWindowsMpvShortcutRuntimePaths = () =>
appDataDir: app.getPath('appData'), appDataDir: app.getPath('appData'),
desktopDir: app.getPath('desktop'), desktopDir: app.getPath('desktop'),
}); });
syncInstalledFirstRunPluginBinaryPath({
platform: process.platform,
homeDir: os.homedir(),
xdgConfigHome: process.env.XDG_CONFIG_HOME,
binaryPath: process.execPath,
});
const firstRunSetupService = createFirstRunSetupService({ const firstRunSetupService = createFirstRunSetupService({
platform: process.platform, platform: process.platform,
configDir: CONFIG_DIR, configDir: CONFIG_DIR,
@@ -1065,6 +1072,7 @@ const firstRunSetupService = createFirstRunSetupService({
dirname: __dirname, dirname: __dirname,
appPath: app.getAppPath(), appPath: app.getAppPath(),
resourcesPath: process.resourcesPath, resourcesPath: process.resourcesPath,
binaryPath: process.execPath,
}), }),
detectWindowsMpvShortcuts: () => { detectWindowsMpvShortcuts: () => {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {

View File

@@ -7,6 +7,7 @@ import {
detectInstalledFirstRunPlugin, detectInstalledFirstRunPlugin,
installFirstRunPluginToDefaultLocation, installFirstRunPluginToDefaultLocation,
resolvePackagedFirstRunPluginAssets, resolvePackagedFirstRunPluginAssets,
syncInstalledFirstRunPluginBinaryPath,
} from './first-run-setup-plugin'; } from './first-run-setup-plugin';
import { resolveDefaultMpvInstallPaths } from '../../shared/setup-state'; import { resolveDefaultMpvInstallPaths } from '../../shared/setup-state';
@@ -68,13 +69,17 @@ test('installFirstRunPluginToDefaultLocation installs plugin and backs up existi
dirname: path.join(root, 'dist', 'main', 'runtime'), dirname: path.join(root, 'dist', 'main', 'runtime'),
appPath: path.join(root, 'app'), appPath: path.join(root, 'app'),
resourcesPath, resourcesPath,
binaryPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner',
}); });
assert.equal(result.ok, true); assert.equal(result.ok, true);
assert.equal(result.pluginInstallStatus, 'installed'); assert.equal(result.pluginInstallStatus, 'installed');
assert.equal(detectInstalledFirstRunPlugin(installPaths), true); assert.equal(detectInstalledFirstRunPlugin(installPaths), true);
assert.equal(fs.readFileSync(installPaths.pluginEntrypointPath, 'utf8'), '-- packaged plugin'); assert.equal(fs.readFileSync(installPaths.pluginEntrypointPath, 'utf8'), '-- packaged plugin');
assert.equal(fs.readFileSync(installPaths.pluginConfigPath, 'utf8'), 'configured=true\n'); assert.equal(
fs.readFileSync(installPaths.pluginConfigPath, 'utf8'),
'configured=true\nbinary_path=/Applications/SubMiner.app/Contents/MacOS/SubMiner\n',
);
const scriptsDirEntries = fs.readdirSync(installPaths.scriptsDir); const scriptsDirEntries = fs.readdirSync(installPaths.scriptsDir);
const scriptOptsEntries = fs.readdirSync(installPaths.scriptOptsDir); const scriptOptsEntries = fs.readdirSync(installPaths.scriptOptsDir);
@@ -113,13 +118,17 @@ test('installFirstRunPluginToDefaultLocation installs plugin to Windows mpv defa
dirname: path.join(root, 'dist', 'main', 'runtime'), dirname: path.join(root, 'dist', 'main', 'runtime'),
appPath: path.join(root, 'app'), appPath: path.join(root, 'app'),
resourcesPath, resourcesPath,
binaryPath: 'C:\\Program Files\\SubMiner\\SubMiner.exe',
}); });
assert.equal(result.ok, true); assert.equal(result.ok, true);
assert.equal(result.pluginInstallStatus, 'installed'); assert.equal(result.pluginInstallStatus, 'installed');
assert.equal(detectInstalledFirstRunPlugin(installPaths), true); assert.equal(detectInstalledFirstRunPlugin(installPaths), true);
assert.equal(fs.readFileSync(installPaths.pluginEntrypointPath, 'utf8'), '-- packaged plugin'); assert.equal(fs.readFileSync(installPaths.pluginEntrypointPath, 'utf8'), '-- packaged plugin');
assert.equal(fs.readFileSync(installPaths.pluginConfigPath, 'utf8'), 'configured=true\n'); assert.equal(
fs.readFileSync(installPaths.pluginConfigPath, 'utf8'),
'configured=true\nbinary_path=C:\\Program Files\\SubMiner\\SubMiner.exe\n',
);
}); });
}); });
@@ -146,12 +155,70 @@ test('installFirstRunPluginToDefaultLocation rewrites Windows plugin socket_path
dirname: path.join(root, 'dist', 'main', 'runtime'), dirname: path.join(root, 'dist', 'main', 'runtime'),
appPath: path.join(root, 'app'), appPath: path.join(root, 'app'),
resourcesPath, resourcesPath,
binaryPath: 'C:\\Program Files\\SubMiner\\SubMiner.exe',
}); });
assert.equal(result.ok, true); assert.equal(result.ok, true);
assert.equal( assert.equal(
fs.readFileSync(installPaths.pluginConfigPath, 'utf8'), fs.readFileSync(installPaths.pluginConfigPath, 'utf8'),
'binary_path=\nsocket_path=\\\\.\\pipe\\subminer-socket\n', 'binary_path=C:\\Program Files\\SubMiner\\SubMiner.exe\nsocket_path=\\\\.\\pipe\\subminer-socket\n',
);
});
});
test('syncInstalledFirstRunPluginBinaryPath fills blank binary_path for existing installs', () => {
withTempDir((root) => {
const homeDir = path.join(root, 'home');
const xdgConfigHome = path.join(root, 'xdg');
const installPaths = resolveDefaultMpvInstallPaths('linux', homeDir, xdgConfigHome);
fs.mkdirSync(path.dirname(installPaths.pluginConfigPath), { recursive: true });
fs.writeFileSync(installPaths.pluginConfigPath, 'binary_path=\nsocket_path=/tmp/subminer-socket\n');
const result = syncInstalledFirstRunPluginBinaryPath({
platform: 'linux',
homeDir,
xdgConfigHome,
binaryPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner',
});
assert.deepEqual(result, {
updated: true,
configPath: installPaths.pluginConfigPath,
});
assert.equal(
fs.readFileSync(installPaths.pluginConfigPath, 'utf8'),
'binary_path=/Applications/SubMiner.app/Contents/MacOS/SubMiner\nsocket_path=/tmp/subminer-socket\n',
);
});
});
test('syncInstalledFirstRunPluginBinaryPath preserves explicit binary_path overrides', () => {
withTempDir((root) => {
const homeDir = path.join(root, 'home');
const xdgConfigHome = path.join(root, 'xdg');
const installPaths = resolveDefaultMpvInstallPaths('linux', homeDir, xdgConfigHome);
fs.mkdirSync(path.dirname(installPaths.pluginConfigPath), { recursive: true });
fs.writeFileSync(
installPaths.pluginConfigPath,
'binary_path=/tmp/SubMiner/scripts/subminer-dev.sh\nsocket_path=/tmp/subminer-socket\n',
);
const result = syncInstalledFirstRunPluginBinaryPath({
platform: 'linux',
homeDir,
xdgConfigHome,
binaryPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner',
});
assert.deepEqual(result, {
updated: false,
configPath: installPaths.pluginConfigPath,
});
assert.equal(
fs.readFileSync(installPaths.pluginConfigPath, 'utf8'),
'binary_path=/tmp/SubMiner/scripts/subminer-dev.sh\nsocket_path=/tmp/subminer-socket\n',
); );
}); });
}); });

View File

@@ -28,6 +28,43 @@ function rewriteInstalledWindowsPluginConfig(configPath: string): void {
} }
} }
function sanitizePluginConfigValue(value: string): string {
return value.replace(/[\r\n]/g, '').trim();
}
function upsertPluginConfigLine(content: string, key: string, value: string): string {
const normalizedValue = sanitizePluginConfigValue(value);
const line = `${key}=${normalizedValue}`;
const pattern = new RegExp(`^${key}=.*$`, 'm');
if (pattern.test(content)) {
return content.replace(pattern, line);
}
const suffix = content.endsWith('\n') || content.length === 0 ? '' : '\n';
return `${content}${suffix}${line}\n`;
}
function rewriteInstalledPluginBinaryPath(configPath: string, binaryPath: string): boolean {
const content = fs.readFileSync(configPath, 'utf8');
const updated = upsertPluginConfigLine(content, 'binary_path', binaryPath);
if (updated === content) {
return false;
}
fs.writeFileSync(configPath, updated, 'utf8');
return true;
}
function readInstalledPluginBinaryPath(configPath: string): string | null {
const content = fs.readFileSync(configPath, 'utf8');
const match = content.match(/^binary_path=(.*)$/m);
if (!match) {
return null;
}
const rawValue = match[1] ?? '';
const value = sanitizePluginConfigValue(rawValue);
return value.length > 0 ? value : null;
}
export function resolvePackagedFirstRunPluginAssets(deps: { export function resolvePackagedFirstRunPluginAssets(deps: {
dirname: string; dirname: string;
appPath: string; appPath: string;
@@ -79,6 +116,7 @@ export function installFirstRunPluginToDefaultLocation(options: {
dirname: string; dirname: string;
appPath: string; appPath: string;
resourcesPath: string; resourcesPath: string;
binaryPath: string;
}): PluginInstallResult { }): PluginInstallResult {
const installPaths = resolveDefaultMpvInstallPaths( const installPaths = resolveDefaultMpvInstallPaths(
options.platform, options.platform,
@@ -116,6 +154,7 @@ export function installFirstRunPluginToDefaultLocation(options: {
backupExistingPath(installPaths.pluginConfigPath); backupExistingPath(installPaths.pluginConfigPath);
fs.cpSync(assets.pluginDirSource, installPaths.pluginDir, { recursive: true }); fs.cpSync(assets.pluginDirSource, installPaths.pluginDir, { recursive: true });
fs.copyFileSync(assets.pluginConfigSource, installPaths.pluginConfigPath); fs.copyFileSync(assets.pluginConfigSource, installPaths.pluginConfigPath);
rewriteInstalledPluginBinaryPath(installPaths.pluginConfigPath, options.binaryPath);
if (options.platform === 'win32') { if (options.platform === 'win32') {
rewriteInstalledWindowsPluginConfig(installPaths.pluginConfigPath); rewriteInstalledWindowsPluginConfig(installPaths.pluginConfigPath);
} }
@@ -127,3 +166,33 @@ export function installFirstRunPluginToDefaultLocation(options: {
message: `Installed mpv plugin to ${installPaths.mpvConfigDir}.`, message: `Installed mpv plugin to ${installPaths.mpvConfigDir}.`,
}; };
} }
export function syncInstalledFirstRunPluginBinaryPath(options: {
platform: NodeJS.Platform;
homeDir: string;
xdgConfigHome?: string;
binaryPath: string;
}): { updated: boolean; configPath: string | null } {
const installPaths = resolveDefaultMpvInstallPaths(
options.platform,
options.homeDir,
options.xdgConfigHome,
);
if (!installPaths.supported || !fs.existsSync(installPaths.pluginConfigPath)) {
return { updated: false, configPath: null };
}
const configuredBinaryPath = readInstalledPluginBinaryPath(installPaths.pluginConfigPath);
if (configuredBinaryPath) {
return { updated: false, configPath: installPaths.pluginConfigPath };
}
const updated = rewriteInstalledPluginBinaryPath(installPaths.pluginConfigPath, options.binaryPath);
if (options.platform === 'win32') {
rewriteInstalledWindowsPluginConfig(installPaths.pluginConfigPath);
}
return {
updated,
configPath: installPaths.pluginConfigPath,
};
}