From a0b0b9f972757c8aa803cbe69d1ae57d2241f81c Mon Sep 17 00:00:00 2001 From: sudacode Date: Sun, 29 Mar 2026 15:16:48 -0700 Subject: [PATCH] fix: pin installed mpv plugin to current binary --- ...niList-token-persistence-on-setup-login.md | 37 ++++++++++ docs-site/installation.md | 3 +- src/main.ts | 8 ++ .../runtime/first-run-setup-plugin.test.ts | 73 ++++++++++++++++++- src/main/runtime/first-run-setup-plugin.ts | 69 ++++++++++++++++++ 5 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 backlog/tasks/task-249 - Fix-AniList-token-persistence-on-setup-login.md diff --git a/backlog/tasks/task-249 - Fix-AniList-token-persistence-on-setup-login.md b/backlog/tasks/task-249 - Fix-AniList-token-persistence-on-setup-login.md new file mode 100644 index 0000000..abf7501 --- /dev/null +++ b/backlog/tasks/task-249 - Fix-AniList-token-persistence-on-setup-login.md @@ -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 + + +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. + + +## Acceptance Criteria + +- [ ] #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 + + +## Final Summary + + +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. + diff --git a/docs-site/installation.md b/docs-site/installation.md index a2aff07..bac4bcb 100644 --- a/docs-site/installation.md +++ b/docs-site/installation.md @@ -172,7 +172,7 @@ Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still re ### 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. -- 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. - 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`. +First-run setup also pins `binary_path` to the current app binary so mpv launches the same SubMiner build that installed the plugin. ```bash # Option 1: install from release assets bundle diff --git a/src/main.ts b/src/main.ts index 683c083..9a65f43 100644 --- a/src/main.ts +++ b/src/main.ts @@ -356,6 +356,7 @@ import { import { detectInstalledFirstRunPlugin, installFirstRunPluginToDefaultLocation, + syncInstalledFirstRunPluginBinaryPath, } from './main/runtime/first-run-setup-plugin'; import { applyWindowsMpvShortcuts, @@ -1036,6 +1037,12 @@ const resolveWindowsMpvShortcutRuntimePaths = () => appDataDir: app.getPath('appData'), desktopDir: app.getPath('desktop'), }); +syncInstalledFirstRunPluginBinaryPath({ + platform: process.platform, + homeDir: os.homedir(), + xdgConfigHome: process.env.XDG_CONFIG_HOME, + binaryPath: process.execPath, +}); const firstRunSetupService = createFirstRunSetupService({ platform: process.platform, configDir: CONFIG_DIR, @@ -1065,6 +1072,7 @@ const firstRunSetupService = createFirstRunSetupService({ dirname: __dirname, appPath: app.getAppPath(), resourcesPath: process.resourcesPath, + binaryPath: process.execPath, }), detectWindowsMpvShortcuts: () => { if (process.platform !== 'win32') { diff --git a/src/main/runtime/first-run-setup-plugin.test.ts b/src/main/runtime/first-run-setup-plugin.test.ts index 100c385..1eebb02 100644 --- a/src/main/runtime/first-run-setup-plugin.test.ts +++ b/src/main/runtime/first-run-setup-plugin.test.ts @@ -7,6 +7,7 @@ import { detectInstalledFirstRunPlugin, installFirstRunPluginToDefaultLocation, resolvePackagedFirstRunPluginAssets, + syncInstalledFirstRunPluginBinaryPath, } from './first-run-setup-plugin'; 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'), appPath: path.join(root, 'app'), resourcesPath, + binaryPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner', }); assert.equal(result.ok, true); assert.equal(result.pluginInstallStatus, 'installed'); assert.equal(detectInstalledFirstRunPlugin(installPaths), true); 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 scriptOptsEntries = fs.readdirSync(installPaths.scriptOptsDir); @@ -113,13 +118,17 @@ test('installFirstRunPluginToDefaultLocation installs plugin to Windows mpv defa dirname: path.join(root, 'dist', 'main', 'runtime'), appPath: path.join(root, 'app'), resourcesPath, + binaryPath: 'C:\\Program Files\\SubMiner\\SubMiner.exe', }); assert.equal(result.ok, true); assert.equal(result.pluginInstallStatus, 'installed'); assert.equal(detectInstalledFirstRunPlugin(installPaths), true); 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'), appPath: path.join(root, 'app'), resourcesPath, + binaryPath: 'C:\\Program Files\\SubMiner\\SubMiner.exe', }); assert.equal(result.ok, true); assert.equal( 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', ); }); }); diff --git a/src/main/runtime/first-run-setup-plugin.ts b/src/main/runtime/first-run-setup-plugin.ts index c00184b..8a24ca2 100644 --- a/src/main/runtime/first-run-setup-plugin.ts +++ b/src/main/runtime/first-run-setup-plugin.ts @@ -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: { dirname: string; appPath: string; @@ -79,6 +116,7 @@ export function installFirstRunPluginToDefaultLocation(options: { dirname: string; appPath: string; resourcesPath: string; + binaryPath: string; }): PluginInstallResult { const installPaths = resolveDefaultMpvInstallPaths( options.platform, @@ -116,6 +154,7 @@ export function installFirstRunPluginToDefaultLocation(options: { backupExistingPath(installPaths.pluginConfigPath); fs.cpSync(assets.pluginDirSource, installPaths.pluginDir, { recursive: true }); fs.copyFileSync(assets.pluginConfigSource, installPaths.pluginConfigPath); + rewriteInstalledPluginBinaryPath(installPaths.pluginConfigPath, options.binaryPath); if (options.platform === 'win32') { rewriteInstalledWindowsPluginConfig(installPaths.pluginConfigPath); } @@ -127,3 +166,33 @@ export function installFirstRunPluginToDefaultLocation(options: { 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, + }; +}