mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-13 08:12:54 -07:00
75348aa72a
- 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
368 lines
13 KiB
TypeScript
368 lines
13 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
buildWindowsMpvLaunchArgs,
|
|
launchWindowsMpv,
|
|
resolveWindowsMpvPath,
|
|
type WindowsMpvLaunchDeps,
|
|
} from './windows-mpv-launch';
|
|
|
|
function createDeps(overrides: Partial<WindowsMpvLaunchDeps> = {}): WindowsMpvLaunchDeps {
|
|
return {
|
|
getEnv: () => undefined,
|
|
runWhere: () => ({ status: 1, stdout: '' }),
|
|
fileExists: () => false,
|
|
spawnDetached: async () => undefined,
|
|
showError: () => undefined,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('resolveWindowsMpvPath prefers SUBMINER_MPV_PATH', () => {
|
|
const resolved = resolveWindowsMpvPath(
|
|
createDeps({
|
|
getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined),
|
|
fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe',
|
|
}),
|
|
);
|
|
|
|
assert.equal(resolved, 'C:\\mpv\\mpv.exe');
|
|
});
|
|
|
|
test('resolveWindowsMpvPath prefers configured executable path before PATH', () => {
|
|
const resolved = resolveWindowsMpvPath(
|
|
createDeps({
|
|
getEnv: () => undefined,
|
|
runWhere: () => ({ status: 0, stdout: 'C:\\tools\\mpv.exe\r\n' }),
|
|
fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe',
|
|
}),
|
|
' C:\\mpv\\mpv.exe ',
|
|
);
|
|
|
|
assert.equal(resolved, 'C:\\mpv\\mpv.exe');
|
|
});
|
|
|
|
test('resolveWindowsMpvPath falls back to where.exe output', () => {
|
|
const resolved = resolveWindowsMpvPath(
|
|
createDeps({
|
|
runWhere: () => ({ status: 0, stdout: 'C:\\tools\\mpv.exe\r\nC:\\other\\mpv.exe\r\n' }),
|
|
fileExists: (candidate) => candidate === 'C:\\tools\\mpv.exe',
|
|
}),
|
|
);
|
|
|
|
assert.equal(resolved, 'C:\\tools\\mpv.exe');
|
|
});
|
|
|
|
test('buildWindowsMpvLaunchArgs uses explicit SubMiner defaults and targets', () => {
|
|
assert.deepEqual(
|
|
buildWindowsMpvLaunchArgs(
|
|
['C:\\a.mkv', 'C:\\b.mkv'],
|
|
[],
|
|
'C:\\SubMiner\\SubMiner.exe',
|
|
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
),
|
|
[
|
|
'--player-operation-mode=pseudo-gui',
|
|
'--force-window=immediate',
|
|
'--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
'--input-ipc-server=\\\\.\\pipe\\subminer-socket',
|
|
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--sub-auto=fuzzy',
|
|
'--sub-file-paths=subs;subtitles',
|
|
'--sid=auto',
|
|
'--secondary-sid=auto',
|
|
'--secondary-sub-visibility=no',
|
|
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
|
'C:\\a.mkv',
|
|
'C:\\b.mkv',
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildWindowsMpvLaunchArgs inserts maximized launch mode before explicit extra args when configured', () => {
|
|
assert.deepEqual(
|
|
buildWindowsMpvLaunchArgs(
|
|
['C:\\video.mkv'],
|
|
['--window-maximized=no'],
|
|
'C:\\SubMiner\\SubMiner.exe',
|
|
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
'maximized',
|
|
),
|
|
[
|
|
'--player-operation-mode=pseudo-gui',
|
|
'--force-window=immediate',
|
|
'--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
'--input-ipc-server=\\\\.\\pipe\\subminer-socket',
|
|
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--sub-auto=fuzzy',
|
|
'--sub-file-paths=subs;subtitles',
|
|
'--sid=auto',
|
|
'--secondary-sid=auto',
|
|
'--secondary-sub-visibility=no',
|
|
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
|
'--window-maximized=yes',
|
|
'--window-maximized=no',
|
|
'C:\\video.mkv',
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildWindowsMpvLaunchArgs keeps shortcut-only launches in idle mode', () => {
|
|
assert.deepEqual(
|
|
buildWindowsMpvLaunchArgs(
|
|
[],
|
|
[],
|
|
'C:\\SubMiner\\SubMiner.exe',
|
|
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
),
|
|
[
|
|
'--player-operation-mode=pseudo-gui',
|
|
'--force-window=immediate',
|
|
'--idle=yes',
|
|
'--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
'--input-ipc-server=\\\\.\\pipe\\subminer-socket',
|
|
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--sub-auto=fuzzy',
|
|
'--sub-file-paths=subs;subtitles',
|
|
'--sid=auto',
|
|
'--secondary-sid=auto',
|
|
'--secondary-sub-visibility=no',
|
|
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildWindowsMpvLaunchArgs mirrors a custom input-ipc-server into script opts', () => {
|
|
assert.deepEqual(
|
|
buildWindowsMpvLaunchArgs(
|
|
['C:\\video.mkv'],
|
|
['--input-ipc-server', '\\\\.\\pipe\\custom-subminer-socket'],
|
|
'C:\\SubMiner\\SubMiner.exe',
|
|
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
),
|
|
[
|
|
'--player-operation-mode=pseudo-gui',
|
|
'--force-window=immediate',
|
|
'--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
'--input-ipc-server=\\\\.\\pipe\\custom-subminer-socket',
|
|
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--sub-auto=fuzzy',
|
|
'--sub-file-paths=subs;subtitles',
|
|
'--sid=auto',
|
|
'--secondary-sid=auto',
|
|
'--secondary-sub-visibility=no',
|
|
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\custom-subminer-socket',
|
|
'--input-ipc-server',
|
|
'\\\\.\\pipe\\custom-subminer-socket',
|
|
'C:\\video.mkv',
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildWindowsMpvLaunchArgs includes socket script opts when plugin entrypoint is present without binary path', () => {
|
|
assert.deepEqual(
|
|
buildWindowsMpvLaunchArgs(
|
|
['C:\\video.mkv'],
|
|
['--input-ipc-server', '\\\\.\\pipe\\custom-subminer-socket'],
|
|
undefined,
|
|
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
),
|
|
[
|
|
'--player-operation-mode=pseudo-gui',
|
|
'--force-window=immediate',
|
|
'--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
'--input-ipc-server=\\\\.\\pipe\\custom-subminer-socket',
|
|
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--sub-auto=fuzzy',
|
|
'--sub-file-paths=subs;subtitles',
|
|
'--sid=auto',
|
|
'--secondary-sid=auto',
|
|
'--secondary-sub-visibility=no',
|
|
'--script-opts=subminer-socket_path=\\\\.\\pipe\\custom-subminer-socket',
|
|
'--input-ipc-server',
|
|
'\\\\.\\pipe\\custom-subminer-socket',
|
|
'C:\\video.mkv',
|
|
],
|
|
);
|
|
});
|
|
|
|
test('launchWindowsMpv reports missing mpv path', async () => {
|
|
const errors: string[] = [];
|
|
const result = await launchWindowsMpv(
|
|
[],
|
|
createDeps({
|
|
showError: (_title, content) => errors.push(content),
|
|
}),
|
|
);
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.mpvPath, '');
|
|
assert.match(errors[0] ?? '', /mpv\.executablePath/i);
|
|
});
|
|
|
|
test('launchWindowsMpv spawns detached mpv with targets', async () => {
|
|
const calls: string[] = [];
|
|
const result = await launchWindowsMpv(
|
|
['C:\\video.mkv'],
|
|
createDeps({
|
|
getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined),
|
|
fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe',
|
|
spawnDetached: async (command, args) => {
|
|
calls.push(command);
|
|
calls.push(args.join('|'));
|
|
},
|
|
}),
|
|
[],
|
|
'C:\\SubMiner\\SubMiner.exe',
|
|
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
);
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.mpvPath, 'C:\\mpv\\mpv.exe');
|
|
assert.deepEqual(calls, [
|
|
'C:\\mpv\\mpv.exe',
|
|
'--player-operation-mode=pseudo-gui|--force-window=immediate|--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua|--input-ipc-server=\\\\.\\pipe\\subminer-socket|--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us|--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us|--sub-auto=fuzzy|--sub-file-paths=subs;subtitles|--sid=auto|--secondary-sid=auto|--secondary-sub-visibility=no|--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket|C:\\video.mkv',
|
|
]);
|
|
});
|
|
|
|
test('launchWindowsMpv skips bundled script when installed plugin is detected', async () => {
|
|
const calls: string[] = [];
|
|
const notifications: string[] = [];
|
|
const result = await launchWindowsMpv(
|
|
['C:\\video.mkv'],
|
|
createDeps({
|
|
getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined),
|
|
fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe',
|
|
spawnDetached: async (command, args) => {
|
|
calls.push(command);
|
|
calls.push(args.join('|'));
|
|
},
|
|
}),
|
|
[],
|
|
'C:\\SubMiner\\SubMiner.exe',
|
|
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
'',
|
|
'normal',
|
|
{
|
|
detectInstalledMpvPlugin: () => ({
|
|
installed: true,
|
|
path: 'C:\\Users\\tester\\AppData\\Roaming\\mpv\\scripts\\subminer\\main.lua',
|
|
version: null,
|
|
source: 'default-config',
|
|
message: null,
|
|
}),
|
|
notifyInstalledPluginDetected: (detection) => {
|
|
notifications.push(detection.path ?? '');
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls[0], 'C:\\mpv\\mpv.exe');
|
|
assert.doesNotMatch(calls[1] ?? '', /--script=C:\\Program Files\\SubMiner/);
|
|
assert.match(calls[1] ?? '', /--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner\.exe/);
|
|
assert.deepEqual(notifications, [
|
|
'C:\\Users\\tester\\AppData\\Roaming\\mpv\\scripts\\subminer\\main.lua',
|
|
]);
|
|
});
|
|
|
|
test('launchWindowsMpv prompts before launch and injects bundled script after legacy plugin removal', async () => {
|
|
const calls: string[] = [];
|
|
const prompts: string[] = [];
|
|
let detectCalls = 0;
|
|
const result = await launchWindowsMpv(
|
|
['C:\\video.mkv'],
|
|
createDeps({
|
|
getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined),
|
|
fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe',
|
|
spawnDetached: async (command, args) => {
|
|
calls.push(command);
|
|
calls.push(args.join('|'));
|
|
},
|
|
}),
|
|
[],
|
|
'C:\\SubMiner\\SubMiner.exe',
|
|
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
|
'',
|
|
'normal',
|
|
{
|
|
detectInstalledMpvPlugin: () => {
|
|
detectCalls += 1;
|
|
return detectCalls === 1
|
|
? {
|
|
installed: true,
|
|
path: 'C:\\Users\\tester\\AppData\\Roaming\\mpv\\scripts\\subminer\\main.lua',
|
|
version: '0.12.0',
|
|
source: 'default-config',
|
|
message: null,
|
|
}
|
|
: {
|
|
installed: false,
|
|
path: null,
|
|
version: null,
|
|
source: null,
|
|
message: null,
|
|
};
|
|
},
|
|
resolveInstalledPluginBeforeLaunch: async (detection) => {
|
|
prompts.push(detection.path ?? '');
|
|
return 'removed' as const;
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(detectCalls, 2);
|
|
assert.deepEqual(prompts, [
|
|
'C:\\Users\\tester\\AppData\\Roaming\\mpv\\scripts\\subminer\\main.lua',
|
|
]);
|
|
assert.equal(calls[0], 'C:\\mpv\\mpv.exe');
|
|
assert.match(
|
|
calls[1] ?? '',
|
|
/--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main\.lua/,
|
|
);
|
|
});
|
|
|
|
test('launchWindowsMpv reports spawn failures with path context', async () => {
|
|
const errors: string[] = [];
|
|
const result = await launchWindowsMpv(
|
|
[],
|
|
createDeps({
|
|
getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined),
|
|
fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe',
|
|
spawnDetached: async () => {
|
|
throw new Error('spawn failed');
|
|
},
|
|
showError: (_title, content) => errors.push(content),
|
|
}),
|
|
);
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.mpvPath, 'C:\\mpv\\mpv.exe');
|
|
assert.match(errors[0] ?? '', /Failed to launch mpv/i);
|
|
assert.match(errors[0] ?? '', /C:\\mpv\\mpv\.exe/i);
|
|
});
|
|
|
|
test('launchWindowsMpv reports async spawn failures with path context', async () => {
|
|
const errors: string[] = [];
|
|
const result = await launchWindowsMpv(
|
|
[],
|
|
createDeps({
|
|
getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined),
|
|
fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe',
|
|
spawnDetached: () => Promise.reject(new Error('async spawn failed')),
|
|
showError: (_title, content) => errors.push(content),
|
|
}),
|
|
);
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.mpvPath, 'C:\\mpv\\mpv.exe');
|
|
assert.match(errors[0] ?? '', /Failed to launch mpv/i);
|
|
assert.match(errors[0] ?? '', /async spawn failed/i);
|
|
});
|