mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-30 06:12:06 -07:00
fix: pin installed mpv plugin to current binary
This commit is contained in:
@@ -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 -->
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user