refactor: extract additional main runtime dependency builders

This commit is contained in:
2026-02-20 00:10:36 -08:00
parent df380ed1ca
commit 5476d44005
21 changed files with 1299 additions and 110 deletions

View File

@@ -0,0 +1,87 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler,
createBuildHandleAnilistSetupProtocolUrlMainDepsHandler,
createBuildNotifyAnilistSetupMainDepsHandler,
createBuildRegisterSubminerProtocolClientMainDepsHandler,
} from './anilist-setup-protocol-main-deps';
test('notify anilist setup main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildNotifyAnilistSetupMainDepsHandler({
hasMpvClient: () => true,
showMpvOsd: (message) => calls.push(`osd:${message}`),
showDesktopNotification: (title) => calls.push(`notify:${title}`),
logInfo: (message) => calls.push(`log:${message}`),
})();
assert.equal(deps.hasMpvClient(), true);
deps.showMpvOsd('ok');
deps.showDesktopNotification('SubMiner', { body: 'x' });
deps.logInfo('done');
assert.deepEqual(calls, ['osd:ok', 'notify:SubMiner', 'log:done']);
});
test('consume anilist setup token main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler({
consumeAnilistSetupCallbackUrl: () => true,
saveToken: () => calls.push('save'),
setCachedToken: () => calls.push('cache'),
setResolvedState: () => calls.push('resolved'),
setSetupPageOpened: () => calls.push('opened'),
onSuccess: () => calls.push('success'),
closeWindow: () => calls.push('close'),
})();
assert.equal(
deps.consumeAnilistSetupCallbackUrl({
rawUrl: 'subminer://anilist-setup',
saveToken: () => {},
setCachedToken: () => {},
setResolvedState: () => {},
setSetupPageOpened: () => {},
onSuccess: () => {},
closeWindow: () => {},
}),
true,
);
deps.saveToken('token');
deps.setCachedToken('token');
deps.setResolvedState(Date.now());
deps.setSetupPageOpened(true);
deps.onSuccess();
deps.closeWindow();
assert.deepEqual(calls, ['save', 'cache', 'resolved', 'opened', 'success', 'close']);
});
test('handle anilist setup protocol url main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildHandleAnilistSetupProtocolUrlMainDepsHandler({
consumeAnilistSetupTokenFromUrl: () => true,
logWarn: (message) => calls.push(`warn:${message}`),
})();
assert.equal(deps.consumeAnilistSetupTokenFromUrl('subminer://anilist-setup'), true);
deps.logWarn('missing', null);
assert.deepEqual(calls, ['warn:missing']);
});
test('register subminer protocol client main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildRegisterSubminerProtocolClientMainDepsHandler({
isDefaultApp: () => true,
getArgv: () => ['electron', 'entry.js'],
execPath: '/tmp/electron',
resolvePath: (value) => `/abs/${value}`,
setAsDefaultProtocolClient: () => true,
logWarn: (message) => calls.push(`warn:${message}`),
})();
assert.equal(deps.isDefaultApp(), true);
assert.deepEqual(deps.getArgv(), ['electron', 'entry.js']);
assert.equal(deps.execPath, '/tmp/electron');
assert.equal(deps.resolvePath('entry.js'), '/abs/entry.js');
assert.equal(deps.setAsDefaultProtocolClient('subminer'), true);
});

View File

@@ -0,0 +1,64 @@
import type {
createConsumeAnilistSetupTokenFromUrlHandler,
createHandleAnilistSetupProtocolUrlHandler,
createNotifyAnilistSetupHandler,
createRegisterSubminerProtocolClientHandler,
} from './anilist-setup-protocol';
type NotifyAnilistSetupMainDeps = Parameters<typeof createNotifyAnilistSetupHandler>[0];
type ConsumeAnilistSetupTokenMainDeps = Parameters<
typeof createConsumeAnilistSetupTokenFromUrlHandler
>[0];
type HandleAnilistSetupProtocolUrlMainDeps = Parameters<
typeof createHandleAnilistSetupProtocolUrlHandler
>[0];
type RegisterSubminerProtocolClientMainDeps = Parameters<
typeof createRegisterSubminerProtocolClientHandler
>[0];
export function createBuildNotifyAnilistSetupMainDepsHandler(deps: NotifyAnilistSetupMainDeps) {
return (): NotifyAnilistSetupMainDeps => ({
hasMpvClient: () => deps.hasMpvClient(),
showMpvOsd: (message: string) => deps.showMpvOsd(message),
showDesktopNotification: (title: string, options: { body: string }) =>
deps.showDesktopNotification(title, options),
logInfo: (message: string) => deps.logInfo(message),
});
}
export function createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler(
deps: ConsumeAnilistSetupTokenMainDeps,
) {
return (): ConsumeAnilistSetupTokenMainDeps => ({
consumeAnilistSetupCallbackUrl: (input) => deps.consumeAnilistSetupCallbackUrl(input),
saveToken: (token: string) => deps.saveToken(token),
setCachedToken: (token: string) => deps.setCachedToken(token),
setResolvedState: (resolvedAt: number) => deps.setResolvedState(resolvedAt),
setSetupPageOpened: (opened: boolean) => deps.setSetupPageOpened(opened),
onSuccess: () => deps.onSuccess(),
closeWindow: () => deps.closeWindow(),
});
}
export function createBuildHandleAnilistSetupProtocolUrlMainDepsHandler(
deps: HandleAnilistSetupProtocolUrlMainDeps,
) {
return (): HandleAnilistSetupProtocolUrlMainDeps => ({
consumeAnilistSetupTokenFromUrl: (rawUrl: string) => deps.consumeAnilistSetupTokenFromUrl(rawUrl),
logWarn: (message: string, details: unknown) => deps.logWarn(message, details),
});
}
export function createBuildRegisterSubminerProtocolClientMainDepsHandler(
deps: RegisterSubminerProtocolClientMainDeps,
) {
return (): RegisterSubminerProtocolClientMainDeps => ({
isDefaultApp: () => deps.isDefaultApp(),
getArgv: () => deps.getArgv(),
execPath: deps.execPath,
resolvePath: (value: string) => deps.resolvePath(value),
setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) =>
deps.setAsDefaultProtocolClient(scheme, path, args),
logWarn: (message: string, details?: unknown) => deps.logWarn(message, details),
});
}

View File

@@ -0,0 +1,84 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createBuildHandleJellyfinAuthCommandsMainDepsHandler,
createBuildHandleJellyfinListCommandsMainDepsHandler,
createBuildHandleJellyfinPlayCommandMainDepsHandler,
createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler,
} from './jellyfin-cli-main-deps';
test('jellyfin auth commands main deps builder maps callbacks', async () => {
const calls: string[] = [];
const deps = createBuildHandleJellyfinAuthCommandsMainDepsHandler({
patchRawConfig: () => calls.push('patch'),
authenticateWithPassword: async () => ({}) as never,
logInfo: (message) => calls.push(`info:${message}`),
})();
deps.patchRawConfig({ jellyfin: {} });
await deps.authenticateWithPassword('', '', '', {
deviceId: '',
clientName: '',
clientVersion: '',
});
deps.logInfo('ok');
assert.deepEqual(calls, ['patch', 'info:ok']);
});
test('jellyfin list commands main deps builder maps callbacks', async () => {
const calls: string[] = [];
const deps = createBuildHandleJellyfinListCommandsMainDepsHandler({
listJellyfinLibraries: async () => {
calls.push('libraries');
return [];
},
listJellyfinItems: async () => {
calls.push('items');
return [];
},
listJellyfinSubtitleTracks: async () => {
calls.push('subtitles');
return [];
},
logInfo: (message) => calls.push(`info:${message}`),
})();
await deps.listJellyfinLibraries({} as never, {} as never);
await deps.listJellyfinItems({} as never, {} as never, { libraryId: '', limit: 1 });
await deps.listJellyfinSubtitleTracks({} as never, {} as never, 'id');
deps.logInfo('done');
assert.deepEqual(calls, ['libraries', 'items', 'subtitles', 'info:done']);
});
test('jellyfin play command main deps builder maps callbacks', async () => {
const calls: string[] = [];
const deps = createBuildHandleJellyfinPlayCommandMainDepsHandler({
playJellyfinItemInMpv: async () => {
calls.push('play');
},
logWarn: (message) => calls.push(`warn:${message}`),
})();
await deps.playJellyfinItemInMpv({} as never);
deps.logWarn('missing');
assert.deepEqual(calls, ['play', 'warn:missing']);
});
test('jellyfin remote announce main deps builder maps callbacks', async () => {
const calls: string[] = [];
const session = { advertiseNow: async () => true };
const deps = createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler({
startJellyfinRemoteSession: async () => {
calls.push('start');
},
getRemoteSession: () => session,
logInfo: (message) => calls.push(`info:${message}`),
logWarn: (message) => calls.push(`warn:${message}`),
})();
await deps.startJellyfinRemoteSession();
assert.equal(deps.getRemoteSession(), session);
deps.logInfo('visible');
deps.logWarn('not-visible');
assert.deepEqual(calls, ['start', 'info:visible', 'warn:not-visible']);
});

View File

@@ -0,0 +1,63 @@
import type {
createHandleJellyfinAuthCommands,
} from './jellyfin-cli-auth';
import type {
createHandleJellyfinListCommands,
} from './jellyfin-cli-list';
import type {
createHandleJellyfinPlayCommand,
} from './jellyfin-cli-play';
import type {
createHandleJellyfinRemoteAnnounceCommand,
} from './jellyfin-cli-remote-announce';
type HandleJellyfinAuthCommandsMainDeps = Parameters<typeof createHandleJellyfinAuthCommands>[0];
type HandleJellyfinListCommandsMainDeps = Parameters<typeof createHandleJellyfinListCommands>[0];
type HandleJellyfinPlayCommandMainDeps = Parameters<typeof createHandleJellyfinPlayCommand>[0];
type HandleJellyfinRemoteAnnounceCommandMainDeps = Parameters<
typeof createHandleJellyfinRemoteAnnounceCommand
>[0];
export function createBuildHandleJellyfinAuthCommandsMainDepsHandler(
deps: HandleJellyfinAuthCommandsMainDeps,
) {
return (): HandleJellyfinAuthCommandsMainDeps => ({
patchRawConfig: (patch) => deps.patchRawConfig(patch),
authenticateWithPassword: (serverUrl, username, password, clientInfo) =>
deps.authenticateWithPassword(serverUrl, username, password, clientInfo),
logInfo: (message: string) => deps.logInfo(message),
});
}
export function createBuildHandleJellyfinListCommandsMainDepsHandler(
deps: HandleJellyfinListCommandsMainDeps,
) {
return (): HandleJellyfinListCommandsMainDeps => ({
listJellyfinLibraries: (session, clientInfo) => deps.listJellyfinLibraries(session, clientInfo),
listJellyfinItems: (session, clientInfo, params) =>
deps.listJellyfinItems(session, clientInfo, params),
listJellyfinSubtitleTracks: (session, clientInfo, itemId) =>
deps.listJellyfinSubtitleTracks(session, clientInfo, itemId),
logInfo: (message: string) => deps.logInfo(message),
});
}
export function createBuildHandleJellyfinPlayCommandMainDepsHandler(
deps: HandleJellyfinPlayCommandMainDeps,
) {
return (): HandleJellyfinPlayCommandMainDeps => ({
playJellyfinItemInMpv: (params) => deps.playJellyfinItemInMpv(params),
logWarn: (message: string) => deps.logWarn(message),
});
}
export function createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler(
deps: HandleJellyfinRemoteAnnounceCommandMainDeps,
) {
return (): HandleJellyfinRemoteAnnounceCommandMainDeps => ({
startJellyfinRemoteSession: () => deps.startJellyfinRemoteSession(),
getRemoteSession: () => deps.getRemoteSession(),
logInfo: (message: string) => deps.logInfo(message),
logWarn: (message: string) => deps.logWarn(message),
});
}

View File

@@ -0,0 +1,26 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createBuildGetJellyfinClientInfoMainDepsHandler,
createBuildGetResolvedJellyfinConfigMainDepsHandler,
} from './jellyfin-client-info-main-deps';
test('get resolved jellyfin config main deps builder maps callbacks', () => {
const resolved = { jellyfin: { url: 'https://example.com' } };
const deps = createBuildGetResolvedJellyfinConfigMainDepsHandler({
getResolvedConfig: () => resolved as never,
})();
assert.equal(deps.getResolvedConfig(), resolved);
});
test('get jellyfin client info main deps builder maps callbacks', () => {
const configured = { clientName: 'Configured' };
const defaults = { clientName: 'Default' };
const deps = createBuildGetJellyfinClientInfoMainDepsHandler({
getResolvedJellyfinConfig: () => configured as never,
getDefaultJellyfinConfig: () => defaults as never,
})();
assert.equal(deps.getResolvedJellyfinConfig(), configured);
assert.equal(deps.getDefaultJellyfinConfig(), defaults);
});

View File

@@ -0,0 +1,24 @@
import type {
createGetJellyfinClientInfoHandler,
createGetResolvedJellyfinConfigHandler,
} from './jellyfin-client-info';
type GetResolvedJellyfinConfigMainDeps = Parameters<typeof createGetResolvedJellyfinConfigHandler>[0];
type GetJellyfinClientInfoMainDeps = Parameters<typeof createGetJellyfinClientInfoHandler>[0];
export function createBuildGetResolvedJellyfinConfigMainDepsHandler(
deps: GetResolvedJellyfinConfigMainDeps,
) {
return (): GetResolvedJellyfinConfigMainDeps => ({
getResolvedConfig: () => deps.getResolvedConfig(),
});
}
export function createBuildGetJellyfinClientInfoMainDepsHandler(
deps: GetJellyfinClientInfoMainDeps,
) {
return (): GetJellyfinClientInfoMainDeps => ({
getResolvedJellyfinConfig: () => deps.getResolvedJellyfinConfig(),
getDefaultJellyfinConfig: () => deps.getDefaultJellyfinConfig(),
});
}

View File

@@ -0,0 +1,72 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { CliArgs } from '../../cli/args';
import { createBuildRunJellyfinCommandMainDepsHandler } from './jellyfin-command-dispatch-main-deps';
test('run jellyfin command main deps builder maps callbacks', async () => {
const calls: string[] = [];
const args = { raw: [] } as unknown as CliArgs;
const config = {
serverUrl: 'http://localhost:8096',
accessToken: 'token',
userId: 'uid',
username: 'alice',
};
const clientInfo = { clientName: 'SubMiner' };
const deps = createBuildRunJellyfinCommandMainDepsHandler({
getJellyfinConfig: () => config,
defaultServerUrl: 'http://127.0.0.1:8096',
getJellyfinClientInfo: () => clientInfo,
handleAuthCommands: async () => {
calls.push('auth');
return false;
},
handleRemoteAnnounceCommand: async () => {
calls.push('remote');
return false;
},
handleListCommands: async () => {
calls.push('list');
return false;
},
handlePlayCommand: async () => {
calls.push('play');
return true;
},
})();
assert.equal(deps.getJellyfinConfig(), config);
assert.equal(deps.defaultServerUrl, 'http://127.0.0.1:8096');
assert.equal(deps.getJellyfinClientInfo(config), clientInfo);
await deps.handleAuthCommands({
args,
jellyfinConfig: config,
serverUrl: config.serverUrl,
clientInfo,
});
await deps.handleRemoteAnnounceCommand(args);
await deps.handleListCommands({
args,
session: {
serverUrl: config.serverUrl,
accessToken: config.accessToken,
userId: config.userId,
username: config.username,
},
clientInfo,
jellyfinConfig: config,
});
await deps.handlePlayCommand({
args,
session: {
serverUrl: config.serverUrl,
accessToken: config.accessToken,
userId: config.userId,
username: config.username,
},
clientInfo,
jellyfinConfig: config,
});
assert.deepEqual(calls, ['auth', 'remote', 'list', 'play']);
});

View File

@@ -0,0 +1,55 @@
import type { CliArgs } from '../../cli/args';
type JellyfinConfigBase = {
serverUrl?: string;
accessToken?: string;
userId?: string;
username?: string;
};
type JellyfinSession = {
serverUrl: string;
accessToken: string;
userId: string;
username: string;
};
export type RunJellyfinCommandMainDeps<TClientInfo, TConfig extends JellyfinConfigBase> = {
getJellyfinConfig: () => TConfig;
defaultServerUrl: string;
getJellyfinClientInfo: (config: TConfig) => TClientInfo;
handleAuthCommands: (params: {
args: CliArgs;
jellyfinConfig: TConfig;
serverUrl: string;
clientInfo: TClientInfo;
}) => Promise<boolean>;
handleRemoteAnnounceCommand: (args: CliArgs) => Promise<boolean>;
handleListCommands: (params: {
args: CliArgs;
session: JellyfinSession;
clientInfo: TClientInfo;
jellyfinConfig: TConfig;
}) => Promise<boolean>;
handlePlayCommand: (params: {
args: CliArgs;
session: JellyfinSession;
clientInfo: TClientInfo;
jellyfinConfig: TConfig;
}) => Promise<boolean>;
};
export function createBuildRunJellyfinCommandMainDepsHandler<
TClientInfo,
TConfig extends JellyfinConfigBase,
>(deps: RunJellyfinCommandMainDeps<TClientInfo, TConfig>) {
return (): RunJellyfinCommandMainDeps<TClientInfo, TConfig> => ({
getJellyfinConfig: () => deps.getJellyfinConfig(),
defaultServerUrl: deps.defaultServerUrl,
getJellyfinClientInfo: (config: TConfig) => deps.getJellyfinClientInfo(config),
handleAuthCommands: (params) => deps.handleAuthCommands(params),
handleRemoteAnnounceCommand: (args: CliArgs) => deps.handleRemoteAnnounceCommand(args),
handleListCommands: (params) => deps.handleListCommands(params),
handlePlayCommand: (params) => deps.handlePlayCommand(params),
});
}

View File

@@ -0,0 +1,88 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createBuildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler,
createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler,
createBuildWaitForMpvConnectedMainDepsHandler,
} from './jellyfin-remote-connection-main-deps';
test('wait for mpv connected main deps builder maps callbacks', async () => {
const calls: string[] = [];
const client = { connected: false, connect: () => calls.push('connect') };
const deps = createBuildWaitForMpvConnectedMainDepsHandler({
getMpvClient: () => client,
now: () => 123,
sleep: async () => {
calls.push('sleep');
},
})();
assert.equal(deps.getMpvClient(), client);
assert.equal(deps.now(), 123);
await deps.sleep(10);
assert.deepEqual(calls, ['sleep']);
});
test('launch mpv for jellyfin main deps builder maps callbacks', () => {
const calls: string[] = [];
const proc = {
on: () => {},
unref: () => {
calls.push('unref');
},
};
const deps = createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler({
getSocketPath: () => '/tmp/mpv.sock',
platform: 'darwin',
execPath: '/tmp/subminer',
defaultMpvLogPath: '/tmp/mpv.log',
defaultMpvArgs: ['--no-config'],
removeSocketPath: (socketPath) => calls.push(`rm:${socketPath}`),
spawnMpv: (args) => {
calls.push(`spawn:${args.join(' ')}`);
return proc;
},
logWarn: (message) => calls.push(`warn:${message}`),
logInfo: (message) => calls.push(`info:${message}`),
})();
assert.equal(deps.getSocketPath(), '/tmp/mpv.sock');
assert.equal(deps.platform, 'darwin');
assert.equal(deps.execPath, '/tmp/subminer');
assert.equal(deps.defaultMpvLogPath, '/tmp/mpv.log');
assert.deepEqual(deps.defaultMpvArgs, ['--no-config']);
deps.removeSocketPath('/tmp/mpv.sock');
deps.spawnMpv(['--idle=yes']);
deps.logInfo('launched');
deps.logWarn('bad', null);
assert.deepEqual(calls, ['rm:/tmp/mpv.sock', 'spawn:--idle=yes', 'info:launched', 'warn:bad']);
});
test('ensure mpv connected for jellyfin main deps builder maps callbacks', async () => {
const calls: string[] = [];
const client = { connected: true, connect: () => {} };
const waitPromise = Promise.resolve(true);
const inFlight = Promise.resolve(false);
const deps = createBuildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler({
getMpvClient: () => client,
setMpvClient: () => calls.push('set-client'),
createMpvClient: () => client,
waitForMpvConnected: () => waitPromise,
launchMpvIdleForJellyfinPlayback: () => calls.push('launch'),
getAutoLaunchInFlight: () => inFlight,
setAutoLaunchInFlight: () => calls.push('set-in-flight'),
connectTimeoutMs: 7000,
autoLaunchTimeoutMs: 15000,
})();
assert.equal(deps.getMpvClient(), client);
deps.setMpvClient(client);
assert.equal(deps.createMpvClient(), client);
assert.equal(await deps.waitForMpvConnected(1), true);
deps.launchMpvIdleForJellyfinPlayback();
assert.equal(deps.getAutoLaunchInFlight(), inFlight);
deps.setAutoLaunchInFlight(null);
assert.equal(deps.connectTimeoutMs, 7000);
assert.equal(deps.autoLaunchTimeoutMs, 15000);
assert.deepEqual(calls, ['set-client', 'launch', 'set-in-flight']);
});

View File

@@ -0,0 +1,45 @@
import type {
EnsureMpvConnectedDeps,
LaunchMpvForJellyfinDeps,
WaitForMpvConnectedDeps,
} from './jellyfin-remote-connection';
export function createBuildWaitForMpvConnectedMainDepsHandler(deps: WaitForMpvConnectedDeps) {
return (): WaitForMpvConnectedDeps => ({
getMpvClient: () => deps.getMpvClient(),
now: () => deps.now(),
sleep: (delayMs: number) => deps.sleep(delayMs),
});
}
export function createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler(
deps: LaunchMpvForJellyfinDeps,
) {
return (): LaunchMpvForJellyfinDeps => ({
getSocketPath: () => deps.getSocketPath(),
platform: deps.platform,
execPath: deps.execPath,
defaultMpvLogPath: deps.defaultMpvLogPath,
defaultMpvArgs: deps.defaultMpvArgs,
removeSocketPath: (socketPath: string) => deps.removeSocketPath(socketPath),
spawnMpv: (args: string[]) => deps.spawnMpv(args),
logWarn: (message: string, error: unknown) => deps.logWarn(message, error),
logInfo: (message: string) => deps.logInfo(message),
});
}
export function createBuildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler(
deps: EnsureMpvConnectedDeps,
) {
return (): EnsureMpvConnectedDeps => ({
getMpvClient: () => deps.getMpvClient(),
setMpvClient: (client) => deps.setMpvClient(client),
createMpvClient: () => deps.createMpvClient(),
waitForMpvConnected: (timeoutMs: number) => deps.waitForMpvConnected(timeoutMs),
launchMpvIdleForJellyfinPlayback: () => deps.launchMpvIdleForJellyfinPlayback(),
getAutoLaunchInFlight: () => deps.getAutoLaunchInFlight(),
setAutoLaunchInFlight: (promise) => deps.setAutoLaunchInFlight(promise),
connectTimeoutMs: deps.connectTimeoutMs,
autoLaunchTimeoutMs: deps.autoLaunchTimeoutMs,
});
}

View File

@@ -0,0 +1,25 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createBuildApplyJellyfinMpvDefaultsMainDepsHandler,
createBuildGetDefaultSocketPathMainDepsHandler,
} from './mpv-jellyfin-defaults-main-deps';
test('apply jellyfin mpv defaults main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildApplyJellyfinMpvDefaultsMainDepsHandler({
sendMpvCommandRuntime: (_client, command) => calls.push(command.join(':')),
jellyfinLangPref: 'ja,jp',
})();
deps.sendMpvCommandRuntime({}, ['set_property', 'aid', 'auto']);
assert.equal(deps.jellyfinLangPref, 'ja,jp');
assert.deepEqual(calls, ['set_property:aid:auto']);
});
test('get default socket path main deps builder maps platform', () => {
const deps = createBuildGetDefaultSocketPathMainDepsHandler({
platform: 'darwin',
})();
assert.equal(deps.platform, 'darwin');
});

View File

@@ -0,0 +1,24 @@
import type {
createApplyJellyfinMpvDefaultsHandler,
createGetDefaultSocketPathHandler,
} from './mpv-jellyfin-defaults';
type ApplyJellyfinMpvDefaultsMainDeps = Parameters<typeof createApplyJellyfinMpvDefaultsHandler>[0];
type GetDefaultSocketPathMainDeps = Parameters<typeof createGetDefaultSocketPathHandler>[0];
export function createBuildApplyJellyfinMpvDefaultsMainDepsHandler(
deps: ApplyJellyfinMpvDefaultsMainDeps,
) {
return (): ApplyJellyfinMpvDefaultsMainDeps => ({
sendMpvCommandRuntime: (client, command) => deps.sendMpvCommandRuntime(client, command),
jellyfinLangPref: deps.jellyfinLangPref,
});
}
export function createBuildGetDefaultSocketPathMainDepsHandler(
deps: GetDefaultSocketPathMainDeps,
) {
return (): GetDefaultSocketPathMainDeps => ({
platform: deps.platform,
});
}

View File

@@ -0,0 +1,30 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createBuildOverlayContentMeasurementStoreMainDepsHandler,
createBuildOverlayModalRuntimeMainDepsHandler,
} from './overlay-bootstrap-main-deps';
test('overlay content measurement store main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildOverlayContentMeasurementStoreMainDepsHandler({
now: () => 42,
warn: (message) => calls.push(`warn:${message}`),
})();
assert.equal(deps.now(), 42);
deps.warn('bad payload');
assert.deepEqual(calls, ['warn:bad payload']);
});
test('overlay modal runtime main deps builder maps window resolvers', () => {
const mainWindow = { id: 'main' };
const invisibleWindow = { id: 'invisible' };
const deps = createBuildOverlayModalRuntimeMainDepsHandler({
getMainWindow: () => mainWindow as never,
getInvisibleWindow: () => invisibleWindow as never,
})();
assert.equal(deps.getMainWindow(), mainWindow);
assert.equal(deps.getInvisibleWindow(), invisibleWindow);
});

View File

@@ -0,0 +1,22 @@
import type { OverlayWindowResolver } from '../overlay-runtime';
type OverlayContentMeasurementStoreMainDeps = {
now: () => number;
warn: (message: string) => void;
};
export function createBuildOverlayContentMeasurementStoreMainDepsHandler(
deps: OverlayContentMeasurementStoreMainDeps,
) {
return (): OverlayContentMeasurementStoreMainDeps => ({
now: () => deps.now(),
warn: (message: string) => deps.warn(message),
});
}
export function createBuildOverlayModalRuntimeMainDepsHandler(deps: OverlayWindowResolver) {
return (): OverlayWindowResolver => ({
getMainWindow: () => deps.getMainWindow(),
getInvisibleWindow: () => deps.getInvisibleWindow(),
});
}

View File

@@ -0,0 +1,78 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createBuildBroadcastRuntimeOptionsChangedMainDepsHandler,
createBuildGetRuntimeOptionsStateMainDepsHandler,
createBuildOpenRuntimeOptionsPaletteMainDepsHandler,
createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler,
createBuildSendToActiveOverlayWindowMainDepsHandler,
createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler,
} from './overlay-runtime-main-actions-main-deps';
test('get runtime options state main deps builder maps callbacks', () => {
const manager = { listOptions: () => [] };
const deps = createBuildGetRuntimeOptionsStateMainDepsHandler({
getRuntimeOptionsManager: () => manager,
})();
assert.equal(deps.getRuntimeOptionsManager(), manager);
});
test('restore secondary sub visibility main deps builder maps callbacks', () => {
const deps = createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler({
getMpvClient: () => ({ connected: true, restorePreviousSecondarySubVisibility: () => {} }),
})();
assert.equal(deps.getMpvClient()?.connected, true);
});
test('broadcast runtime options changed main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildBroadcastRuntimeOptionsChangedMainDepsHandler({
broadcastRuntimeOptionsChangedRuntime: () => calls.push('broadcast-runtime'),
getRuntimeOptionsState: () => [],
broadcastToOverlayWindows: (channel) => calls.push(channel),
})();
deps.broadcastRuntimeOptionsChangedRuntime(() => [], () => {});
deps.broadcastToOverlayWindows('runtime-options:changed');
assert.deepEqual(deps.getRuntimeOptionsState(), []);
assert.deepEqual(calls, ['broadcast-runtime', 'runtime-options:changed']);
});
test('send to active overlay window main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildSendToActiveOverlayWindowMainDepsHandler({
sendToActiveOverlayWindowRuntime: () => {
calls.push('send');
return true;
},
})();
assert.equal(deps.sendToActiveOverlayWindowRuntime('x'), true);
assert.deepEqual(calls, ['send']);
});
test('set overlay debug visualization main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler({
setOverlayDebugVisualizationEnabledRuntime: () => calls.push('set-runtime'),
getCurrentEnabled: () => false,
setCurrentEnabled: () => calls.push('set-current'),
broadcastToOverlayWindows: () => calls.push('broadcast'),
})();
deps.setOverlayDebugVisualizationEnabledRuntime(false, true, () => {}, () => {});
assert.equal(deps.getCurrentEnabled(), false);
deps.setCurrentEnabled(true);
deps.broadcastToOverlayWindows('overlay:debug');
assert.deepEqual(calls, ['set-runtime', 'set-current', 'broadcast']);
});
test('open runtime options palette main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildOpenRuntimeOptionsPaletteMainDepsHandler({
openRuntimeOptionsPaletteRuntime: () => calls.push('open'),
})();
deps.openRuntimeOptionsPaletteRuntime();
assert.deepEqual(calls, ['open']);
});

View File

@@ -0,0 +1,89 @@
import {
createBroadcastRuntimeOptionsChangedHandler,
createGetRuntimeOptionsStateHandler,
createOpenRuntimeOptionsPaletteHandler,
createRestorePreviousSecondarySubVisibilityHandler,
createSendToActiveOverlayWindowHandler,
createSetOverlayDebugVisualizationEnabledHandler,
} from './overlay-runtime-main-actions';
type GetRuntimeOptionsStateMainDeps = Parameters<typeof createGetRuntimeOptionsStateHandler>[0];
type RestorePreviousSecondarySubVisibilityMainDeps = Parameters<
typeof createRestorePreviousSecondarySubVisibilityHandler
>[0];
type BroadcastRuntimeOptionsChangedMainDeps = Parameters<
typeof createBroadcastRuntimeOptionsChangedHandler
>[0];
type SendToActiveOverlayWindowMainDeps = Parameters<typeof createSendToActiveOverlayWindowHandler>[0];
type SetOverlayDebugVisualizationEnabledMainDeps = Parameters<
typeof createSetOverlayDebugVisualizationEnabledHandler
>[0];
type OpenRuntimeOptionsPaletteMainDeps = Parameters<typeof createOpenRuntimeOptionsPaletteHandler>[0];
export function createBuildGetRuntimeOptionsStateMainDepsHandler(
deps: GetRuntimeOptionsStateMainDeps,
) {
return (): GetRuntimeOptionsStateMainDeps => ({
getRuntimeOptionsManager: () => deps.getRuntimeOptionsManager(),
});
}
export function createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler(
deps: RestorePreviousSecondarySubVisibilityMainDeps,
) {
return (): RestorePreviousSecondarySubVisibilityMainDeps => ({
getMpvClient: () => deps.getMpvClient(),
});
}
export function createBuildBroadcastRuntimeOptionsChangedMainDepsHandler(
deps: BroadcastRuntimeOptionsChangedMainDeps,
) {
return (): BroadcastRuntimeOptionsChangedMainDeps => ({
broadcastRuntimeOptionsChangedRuntime: (getRuntimeOptionsState, broadcastToOverlayWindows) =>
deps.broadcastRuntimeOptionsChangedRuntime(getRuntimeOptionsState, broadcastToOverlayWindows),
getRuntimeOptionsState: () => deps.getRuntimeOptionsState(),
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) =>
deps.broadcastToOverlayWindows(channel, ...args),
});
}
export function createBuildSendToActiveOverlayWindowMainDepsHandler(
deps: SendToActiveOverlayWindowMainDeps,
) {
return (): SendToActiveOverlayWindowMainDeps => ({
sendToActiveOverlayWindowRuntime: (channel, payload, runtimeOptions) =>
deps.sendToActiveOverlayWindowRuntime(channel, payload, runtimeOptions),
});
}
export function createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler(
deps: SetOverlayDebugVisualizationEnabledMainDeps,
) {
return (): SetOverlayDebugVisualizationEnabledMainDeps => ({
setOverlayDebugVisualizationEnabledRuntime: (
currentEnabled,
nextEnabled,
setCurrentEnabled,
broadcastToOverlayWindows,
) =>
deps.setOverlayDebugVisualizationEnabledRuntime(
currentEnabled,
nextEnabled,
setCurrentEnabled,
broadcastToOverlayWindows,
),
getCurrentEnabled: () => deps.getCurrentEnabled(),
setCurrentEnabled: (enabled: boolean) => deps.setCurrentEnabled(enabled),
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) =>
deps.broadcastToOverlayWindows(channel, ...args),
});
}
export function createBuildOpenRuntimeOptionsPaletteMainDepsHandler(
deps: OpenRuntimeOptionsPaletteMainDeps,
) {
return (): OpenRuntimeOptionsPaletteMainDeps => ({
openRuntimeOptionsPaletteRuntime: () => deps.openRuntimeOptionsPaletteRuntime(),
});
}

View File

@@ -0,0 +1,99 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createBuildAnilistStateRuntimeMainDepsHandler,
createBuildConfigDerivedRuntimeMainDepsHandler,
createBuildImmersionMediaRuntimeMainDepsHandler,
createBuildMainSubsyncRuntimeMainDepsHandler,
} from './runtime-bootstrap-main-deps';
test('immersion media runtime main deps builder maps callbacks', async () => {
const calls: string[] = [];
const deps = createBuildImmersionMediaRuntimeMainDepsHandler({
getResolvedConfig: () => ({ immersionTracking: { dbPath: '/tmp/db.sqlite' } }),
defaultImmersionDbPath: '/tmp/default.sqlite',
getTracker: () => ({ handleMediaChange: () => calls.push('track') }),
getMpvClient: () => ({ connected: true }),
getCurrentMediaPath: () => '/tmp/media.mkv',
getCurrentMediaTitle: () => 'Title',
sleep: async () => {
calls.push('sleep');
},
seedWaitMs: 25,
seedAttempts: 3,
logDebug: (message) => calls.push(`debug:${message}`),
logInfo: (message) => calls.push(`info:${message}`),
})();
assert.equal(deps.defaultImmersionDbPath, '/tmp/default.sqlite');
assert.deepEqual(deps.getResolvedConfig(), { immersionTracking: { dbPath: '/tmp/db.sqlite' } });
assert.deepEqual(deps.getMpvClient(), { connected: true });
assert.equal(deps.getCurrentMediaPath(), '/tmp/media.mkv');
assert.equal(deps.getCurrentMediaTitle(), 'Title');
assert.equal(deps.seedWaitMs, 25);
assert.equal(deps.seedAttempts, 3);
await deps.sleep?.(1);
deps.logDebug('a');
deps.logInfo('b');
assert.deepEqual(calls, ['sleep', 'debug:a', 'info:b']);
});
test('anilist state runtime main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildAnilistStateRuntimeMainDepsHandler({
getClientSecretState: () => ({ status: 'resolved' } as never),
setClientSecretState: () => calls.push('set-client'),
getRetryQueueState: () => ({ pending: 1 } as never),
setRetryQueueState: () => calls.push('set-queue'),
getUpdateQueueSnapshot: () => ({ pending: 2 } as never),
clearStoredToken: () => calls.push('clear-stored'),
clearCachedAccessToken: () => calls.push('clear-cached'),
})();
assert.deepEqual(deps.getClientSecretState(), { status: 'resolved' });
assert.deepEqual(deps.getRetryQueueState(), { pending: 1 });
assert.deepEqual(deps.getUpdateQueueSnapshot(), { pending: 2 });
deps.setClientSecretState({} as never);
deps.setRetryQueueState({} as never);
deps.clearStoredToken();
deps.clearCachedAccessToken();
assert.deepEqual(calls, ['set-client', 'set-queue', 'clear-stored', 'clear-cached']);
});
test('config derived runtime main deps builder maps callbacks', () => {
const deps = createBuildConfigDerivedRuntimeMainDepsHandler({
getResolvedConfig: () => ({ jimaku: {} } as never),
getRuntimeOptionsManager: () => null,
platform: 'darwin',
defaultJimakuLanguagePreference: 'ja',
defaultJimakuMaxEntryResults: 20,
defaultJimakuApiBaseUrl: 'https://api.example.com',
})();
assert.deepEqual(deps.getResolvedConfig(), { jimaku: {} });
assert.equal(deps.getRuntimeOptionsManager(), null);
assert.equal(deps.platform, 'darwin');
assert.equal(deps.defaultJimakuLanguagePreference, 'ja');
assert.equal(deps.defaultJimakuMaxEntryResults, 20);
assert.equal(deps.defaultJimakuApiBaseUrl, 'https://api.example.com');
});
test('main subsync runtime main deps builder maps callbacks', () => {
const calls: string[] = [];
const deps = createBuildMainSubsyncRuntimeMainDepsHandler({
getMpvClient: () => ({ connected: true }) as never,
getResolvedConfig: () => ({ subsync: {} } as never),
getSubsyncInProgress: () => true,
setSubsyncInProgress: () => calls.push('set-progress'),
showMpvOsd: (text) => calls.push(`osd:${text}`),
openManualPicker: () => calls.push('open-picker'),
})();
assert.deepEqual(deps.getMpvClient(), { connected: true });
assert.deepEqual(deps.getResolvedConfig(), { subsync: {} });
assert.equal(deps.getSubsyncInProgress(), true);
deps.setSubsyncInProgress(false);
deps.showMpvOsd('ready');
deps.openManualPicker({} as never);
assert.deepEqual(calls, ['set-progress', 'osd:ready', 'open-picker']);
});

View File

@@ -0,0 +1,54 @@
import type { AnilistStateRuntimeDeps } from './anilist-state';
import type { ConfigDerivedRuntimeDeps } from './config-derived';
import type { ImmersionMediaRuntimeDeps } from './immersion-media';
import type { MainSubsyncRuntimeDeps } from './subsync-runtime';
export function createBuildImmersionMediaRuntimeMainDepsHandler(deps: ImmersionMediaRuntimeDeps) {
return (): ImmersionMediaRuntimeDeps => ({
getResolvedConfig: () => deps.getResolvedConfig(),
defaultImmersionDbPath: deps.defaultImmersionDbPath,
getTracker: () => deps.getTracker(),
getMpvClient: () => deps.getMpvClient(),
getCurrentMediaPath: () => deps.getCurrentMediaPath(),
getCurrentMediaTitle: () => deps.getCurrentMediaTitle(),
sleep: deps.sleep,
seedWaitMs: deps.seedWaitMs,
seedAttempts: deps.seedAttempts,
logDebug: (message: string) => deps.logDebug(message),
logInfo: (message: string) => deps.logInfo(message),
});
}
export function createBuildAnilistStateRuntimeMainDepsHandler(deps: AnilistStateRuntimeDeps) {
return (): AnilistStateRuntimeDeps => ({
getClientSecretState: () => deps.getClientSecretState(),
setClientSecretState: (next) => deps.setClientSecretState(next),
getRetryQueueState: () => deps.getRetryQueueState(),
setRetryQueueState: (next) => deps.setRetryQueueState(next),
getUpdateQueueSnapshot: () => deps.getUpdateQueueSnapshot(),
clearStoredToken: () => deps.clearStoredToken(),
clearCachedAccessToken: () => deps.clearCachedAccessToken(),
});
}
export function createBuildConfigDerivedRuntimeMainDepsHandler(deps: ConfigDerivedRuntimeDeps) {
return (): ConfigDerivedRuntimeDeps => ({
getResolvedConfig: () => deps.getResolvedConfig(),
getRuntimeOptionsManager: () => deps.getRuntimeOptionsManager(),
platform: deps.platform,
defaultJimakuLanguagePreference: deps.defaultJimakuLanguagePreference,
defaultJimakuMaxEntryResults: deps.defaultJimakuMaxEntryResults,
defaultJimakuApiBaseUrl: deps.defaultJimakuApiBaseUrl,
});
}
export function createBuildMainSubsyncRuntimeMainDepsHandler(deps: MainSubsyncRuntimeDeps) {
return (): MainSubsyncRuntimeDeps => ({
getMpvClient: () => deps.getMpvClient(),
getResolvedConfig: () => deps.getResolvedConfig(),
getSubsyncInProgress: () => deps.getSubsyncInProgress(),
setSubsyncInProgress: (inProgress: boolean) => deps.setSubsyncInProgress(inProgress),
showMpvOsd: (text: string) => deps.showMpvOsd(text),
openManualPicker: (payload) => deps.openManualPicker(payload),
});
}