mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
47e78ff698
- Show visible overlay automatically during Jellyfin playback so subtitleStyle applies - Inject bundled mpv plugin on auto-launch so keybindings work without overlay focus - Group Jellyfin playback stats under item metadata (jellyfin://host/item/id) instead of stream URLs so episodes merge with matching local titles - Mark ffsubsync unavailable in subsync modal for remote media paths - Drain queued second-instance commands even when onReady throws
194 lines
6.3 KiB
TypeScript
194 lines
6.3 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
createEnsureMpvConnectedForJellyfinPlaybackHandler,
|
|
createLaunchMpvIdleForJellyfinPlaybackHandler,
|
|
createWaitForMpvConnectedHandler,
|
|
} from './jellyfin-remote-connection';
|
|
|
|
test('createWaitForMpvConnectedHandler connects and waits for readiness', async () => {
|
|
let connected = false;
|
|
let nowMs = 0;
|
|
const waitForConnected = createWaitForMpvConnectedHandler({
|
|
getMpvClient: () => ({
|
|
connected,
|
|
connect: () => {
|
|
connected = true;
|
|
},
|
|
}),
|
|
now: () => nowMs,
|
|
sleep: async () => {
|
|
nowMs += 100;
|
|
},
|
|
});
|
|
|
|
const ready = await waitForConnected(500);
|
|
assert.equal(ready, true);
|
|
});
|
|
|
|
test('createLaunchMpvIdleForJellyfinPlaybackHandler builds expected mpv args', () => {
|
|
const spawnedArgs: string[][] = [];
|
|
const logs: string[] = [];
|
|
const launch = createLaunchMpvIdleForJellyfinPlaybackHandler({
|
|
getSocketPath: () => '/tmp/subminer.sock',
|
|
getLaunchMode: () => 'maximized',
|
|
platform: 'darwin',
|
|
execPath: '/Applications/SubMiner.app/Contents/MacOS/SubMiner',
|
|
getRuntimePluginEntrypoint: () =>
|
|
'/Applications/SubMiner.app/Contents/Resources/plugin/subminer/main.lua',
|
|
defaultMpvLogPath: '/tmp/mp.log',
|
|
defaultMpvArgs: ['--sid=auto'],
|
|
removeSocketPath: () => {},
|
|
spawnMpv: (args) => {
|
|
spawnedArgs.push(args);
|
|
return {
|
|
on: () => {},
|
|
unref: () => {},
|
|
};
|
|
},
|
|
logWarn: (message) => logs.push(message),
|
|
logInfo: (message) => logs.push(message),
|
|
});
|
|
|
|
launch();
|
|
assert.equal(spawnedArgs.length, 1);
|
|
assert.ok(spawnedArgs[0]!.includes('--window-maximized=yes'));
|
|
assert.ok(spawnedArgs[0]!.includes('--idle=yes'));
|
|
assert.ok(
|
|
spawnedArgs[0]!.includes(
|
|
'--script=/Applications/SubMiner.app/Contents/Resources/plugin/subminer/main.lua',
|
|
),
|
|
);
|
|
assert.ok(spawnedArgs[0]!.some((arg) => arg.includes('--input-ipc-server=/tmp/subminer.sock')));
|
|
assert.ok(logs.some((entry) => entry.includes('Launched mpv for Jellyfin playback')));
|
|
});
|
|
|
|
test('createLaunchMpvIdleForJellyfinPlaybackHandler forwards runtime plugin config', () => {
|
|
const spawnedArgs: string[][] = [];
|
|
const launch = createLaunchMpvIdleForJellyfinPlaybackHandler({
|
|
getSocketPath: () => '/tmp/subminer.sock',
|
|
getLaunchMode: () => 'normal',
|
|
platform: 'linux',
|
|
execPath: '/opt/SubMiner/SubMiner.AppImage',
|
|
getPluginRuntimeConfig: () => ({
|
|
socketPath: '/tmp/ignored-config.sock',
|
|
binaryPath: '/custom/SubMiner.AppImage',
|
|
backend: 'x11',
|
|
autoStart: true,
|
|
autoStartVisibleOverlay: false,
|
|
autoStartPauseUntilReady: false,
|
|
texthookerEnabled: false,
|
|
aniskipEnabled: true,
|
|
aniskipButtonKey: 'F8',
|
|
}),
|
|
defaultMpvLogPath: '/tmp/mp.log',
|
|
defaultMpvArgs: ['--sid=auto'],
|
|
removeSocketPath: () => {},
|
|
spawnMpv: (args) => {
|
|
spawnedArgs.push(args);
|
|
return {
|
|
on: () => {},
|
|
unref: () => {},
|
|
};
|
|
},
|
|
logWarn: () => {},
|
|
logInfo: () => {},
|
|
});
|
|
|
|
launch();
|
|
const scriptOpts = spawnedArgs[0]?.find((arg) => arg.startsWith('--script-opts='));
|
|
assert.match(scriptOpts ?? '', /subminer-binary_path=\/custom\/SubMiner\.AppImage/);
|
|
assert.match(scriptOpts ?? '', /subminer-socket_path=\/tmp\/subminer\.sock/);
|
|
assert.match(scriptOpts ?? '', /subminer-backend=x11/);
|
|
assert.match(scriptOpts ?? '', /subminer-auto_start=yes/);
|
|
assert.match(scriptOpts ?? '', /subminer-auto_start_visible_overlay=no/);
|
|
assert.match(scriptOpts ?? '', /subminer-auto_start_pause_until_ready=no/);
|
|
assert.match(scriptOpts ?? '', /subminer-texthooker_enabled=no/);
|
|
assert.match(scriptOpts ?? '', /subminer-aniskip_enabled=yes/);
|
|
assert.match(scriptOpts ?? '', /subminer-aniskip_button_key=F8/);
|
|
});
|
|
|
|
test('createLaunchMpvIdleForJellyfinPlaybackHandler skips bundled script when installed plugin exists', () => {
|
|
const spawnedArgs: string[][] = [];
|
|
const launch = createLaunchMpvIdleForJellyfinPlaybackHandler({
|
|
getSocketPath: () => '/tmp/subminer.sock',
|
|
getLaunchMode: () => 'normal',
|
|
platform: 'linux',
|
|
execPath: '/opt/SubMiner/SubMiner.AppImage',
|
|
getRuntimePluginEntrypoint: () => '/opt/SubMiner/plugin/subminer/main.lua',
|
|
getInstalledPluginDetection: () => ({
|
|
installed: true,
|
|
path: '/home/tester/.config/mpv/scripts/subminer/main.lua',
|
|
version: '0.1.0',
|
|
source: 'default-config',
|
|
message: null,
|
|
}),
|
|
defaultMpvLogPath: '/tmp/mp.log',
|
|
defaultMpvArgs: ['--sid=auto'],
|
|
removeSocketPath: () => {},
|
|
spawnMpv: (args) => {
|
|
spawnedArgs.push(args);
|
|
return {
|
|
on: () => {},
|
|
unref: () => {},
|
|
};
|
|
},
|
|
logWarn: () => {},
|
|
logInfo: () => {},
|
|
});
|
|
|
|
launch();
|
|
assert.equal(
|
|
spawnedArgs[0]?.some((arg) => arg.startsWith('--script=/opt/SubMiner/plugin/subminer')),
|
|
false,
|
|
);
|
|
assert.ok(spawnedArgs[0]?.some((arg) => arg.startsWith('--script-opts=')));
|
|
});
|
|
|
|
test('createEnsureMpvConnectedForJellyfinPlaybackHandler auto-launches once', async () => {
|
|
let autoLaunchInFlight: Promise<boolean> | null = null;
|
|
let launchCalls = 0;
|
|
let waitCalls = 0;
|
|
let mpvClient: { connected: boolean; connect: () => void } | null = null;
|
|
let resolveAutoLaunchPromise: (value: boolean) => void = () => {};
|
|
const autoLaunchPromise = new Promise<boolean>((resolve) => {
|
|
resolveAutoLaunchPromise = resolve;
|
|
});
|
|
|
|
const ensureConnected = createEnsureMpvConnectedForJellyfinPlaybackHandler({
|
|
getMpvClient: () => mpvClient,
|
|
setMpvClient: (client) => {
|
|
mpvClient = client;
|
|
},
|
|
createMpvClient: () => ({
|
|
connected: false,
|
|
connect: () => {},
|
|
}),
|
|
waitForMpvConnected: async (timeoutMs) => {
|
|
waitCalls += 1;
|
|
if (timeoutMs === 3000) return false;
|
|
return await autoLaunchPromise;
|
|
},
|
|
launchMpvIdleForJellyfinPlayback: () => {
|
|
launchCalls += 1;
|
|
},
|
|
getAutoLaunchInFlight: () => autoLaunchInFlight,
|
|
setAutoLaunchInFlight: (promise) => {
|
|
autoLaunchInFlight = promise;
|
|
},
|
|
connectTimeoutMs: 3000,
|
|
autoLaunchTimeoutMs: 20000,
|
|
});
|
|
|
|
const firstPromise = ensureConnected();
|
|
const secondPromise = ensureConnected();
|
|
resolveAutoLaunchPromise(true);
|
|
const first = await firstPromise;
|
|
const second = await secondPromise;
|
|
|
|
assert.equal(first, true);
|
|
assert.equal(second, true);
|
|
assert.equal(launchCalls, 1);
|
|
assert.equal(waitCalls >= 2, true);
|
|
});
|