From 5476d44005cfc3e446d364284f6febe21c6274ca Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 20 Feb 2026 00:10:36 -0800 Subject: [PATCH] refactor: extract additional main runtime dependency builders --- docs/subagents/INDEX.md | 2 +- .../codex-task85-20260219T233711Z-46hc.md | 26 ++ src/main.ts | 352 ++++++++++++------ .../anilist-setup-protocol-main-deps.test.ts | 87 +++++ .../anilist-setup-protocol-main-deps.ts | 64 ++++ .../runtime/jellyfin-cli-main-deps.test.ts | 84 +++++ src/main/runtime/jellyfin-cli-main-deps.ts | 63 ++++ .../jellyfin-client-info-main-deps.test.ts | 26 ++ .../runtime/jellyfin-client-info-main-deps.ts | 24 ++ ...ellyfin-command-dispatch-main-deps.test.ts | 72 ++++ .../jellyfin-command-dispatch-main-deps.ts | 55 +++ ...llyfin-remote-connection-main-deps.test.ts | 88 +++++ .../jellyfin-remote-connection-main-deps.ts | 45 +++ .../mpv-jellyfin-defaults-main-deps.test.ts | 25 ++ .../mpv-jellyfin-defaults-main-deps.ts | 24 ++ .../overlay-bootstrap-main-deps.test.ts | 30 ++ .../runtime/overlay-bootstrap-main-deps.ts | 22 ++ ...lay-runtime-main-actions-main-deps.test.ts | 78 ++++ .../overlay-runtime-main-actions-main-deps.ts | 89 +++++ .../runtime-bootstrap-main-deps.test.ts | 99 +++++ .../runtime/runtime-bootstrap-main-deps.ts | 54 +++ 21 files changed, 1299 insertions(+), 110 deletions(-) create mode 100644 src/main/runtime/anilist-setup-protocol-main-deps.test.ts create mode 100644 src/main/runtime/anilist-setup-protocol-main-deps.ts create mode 100644 src/main/runtime/jellyfin-cli-main-deps.test.ts create mode 100644 src/main/runtime/jellyfin-cli-main-deps.ts create mode 100644 src/main/runtime/jellyfin-client-info-main-deps.test.ts create mode 100644 src/main/runtime/jellyfin-client-info-main-deps.ts create mode 100644 src/main/runtime/jellyfin-command-dispatch-main-deps.test.ts create mode 100644 src/main/runtime/jellyfin-command-dispatch-main-deps.ts create mode 100644 src/main/runtime/jellyfin-remote-connection-main-deps.test.ts create mode 100644 src/main/runtime/jellyfin-remote-connection-main-deps.ts create mode 100644 src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts create mode 100644 src/main/runtime/mpv-jellyfin-defaults-main-deps.ts create mode 100644 src/main/runtime/overlay-bootstrap-main-deps.test.ts create mode 100644 src/main/runtime/overlay-bootstrap-main-deps.ts create mode 100644 src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts create mode 100644 src/main/runtime/overlay-runtime-main-actions-main-deps.ts create mode 100644 src/main/runtime/runtime-bootstrap-main-deps.test.ts create mode 100644 src/main/runtime/runtime-bootstrap-main-deps.ts diff --git a/docs/subagents/INDEX.md b/docs/subagents/INDEX.md index 5e437d0..617c919 100644 --- a/docs/subagents/INDEX.md +++ b/docs/subagents/INDEX.md @@ -6,7 +6,7 @@ Read first. Keep concise. | ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- | | `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` | | `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` | -| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T07:35:01Z` | +| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T08:00:02Z` | | `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` | | `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` | | `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` | diff --git a/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md b/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md index 09b5723..7c1c070 100644 --- a/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md +++ b/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md @@ -9,6 +9,32 @@ ## Current Work (newest first) +- [2026-02-20T08:00:02Z] progress: extracted Jellyfin command-dispatch deps assembly into `src/main/runtime/jellyfin-command-dispatch-main-deps.ts` (`createBuildRunJellyfinCommandMainDepsHandler`) and rewired `runJellyfinCommand` construction in `src/main.ts`. +- [2026-02-20T08:00:02Z] progress: added parity tests in `src/main/runtime/jellyfin-command-dispatch-main-deps.test.ts`; `src/main.ts` now 2909 LOC. +- [2026-02-20T08:00:02Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-command-dispatch-main-deps.test.js dist/main/runtime/jellyfin-command-dispatch.test.js dist/main/runtime/jellyfin-cli-main-deps.test.js` pass (8/8). +- [2026-02-20T07:58:40Z] progress: extracted Jellyfin CLI handler deps assembly into `src/main/runtime/jellyfin-cli-main-deps.ts` (auth/list/play/remote-announce builders) and rewired those handler construction sites in `src/main.ts`. +- [2026-02-20T07:58:40Z] progress: added parity tests in `src/main/runtime/jellyfin-cli-main-deps.test.ts`; `src/main.ts` now 2905 LOC. +- [2026-02-20T07:58:40Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-cli-main-deps.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (16/16). +- [2026-02-20T07:57:16Z] progress: extracted AniList setup protocol deps assembly into `src/main/runtime/anilist-setup-protocol-main-deps.ts` (notify, consume-token, protocol-url, protocol-client builders) and rewired those handler construction sites in `src/main.ts`. +- [2026-02-20T07:57:16Z] progress: added parity tests in `src/main/runtime/anilist-setup-protocol-main-deps.test.ts`; `src/main.ts` now 2882 LOC. +- [2026-02-20T07:57:16Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/anilist-setup-protocol-main-deps.test.js dist/main/runtime/anilist-setup-protocol.test.js dist/main/runtime/jellyfin-client-info-main-deps.test.js` pass (6/6). +- [2026-02-20T07:52:56Z] progress: extracted Jellyfin client-info deps assembly into `src/main/runtime/jellyfin-client-info-main-deps.ts` (resolved-config + client-info builders) and rewired those handler construction sites in `src/main.ts`. +- [2026-02-20T07:52:56Z] progress: added parity tests in `src/main/runtime/jellyfin-client-info-main-deps.test.ts`; `src/main.ts` now 2859 LOC. +- [2026-02-20T07:52:56Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-client-info-main-deps.test.js dist/main/runtime/jellyfin-client-info.test.js dist/main/runtime/mpv-jellyfin-defaults-main-deps.test.js` pass (5/5). +- [2026-02-20T07:51:58Z] progress: extracted MPV/Jellyfin defaults deps assembly into `src/main/runtime/mpv-jellyfin-defaults-main-deps.ts` (apply-defaults + default-socket-path builders) and rewired those constructor sites in `src/main.ts`. +- [2026-02-20T07:51:58Z] progress: added parity tests in `src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts`; `src/main.ts` now 2848 LOC. +- [2026-02-20T07:51:58Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/mpv-jellyfin-defaults-main-deps.test.js dist/main/runtime/mpv-jellyfin-defaults.test.js dist/main/runtime/jellyfin-remote-connection-main-deps.test.js` pass (5/5). +- [2026-02-20T07:46:45Z] progress: extracted Jellyfin remote-connection deps assembly into `src/main/runtime/jellyfin-remote-connection-main-deps.ts` (wait, launch, ensure builders) and rewired those constructor sites in `src/main.ts`. +- [2026-02-20T07:46:45Z] progress: added parity tests in `src/main/runtime/jellyfin-remote-connection-main-deps.test.ts`; `src/main.ts` now 2837 LOC. +- [2026-02-20T07:46:45Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-remote-connection-main-deps.test.js dist/main/runtime/jellyfin-remote-connection.test.js dist/main/runtime/jellyfin-remote-main-deps.test.js` pass (8/8). +- [2026-02-20T07:44:09Z] progress: extracted runtime-options/overlay-action deps assembly into `src/main/runtime/overlay-runtime-main-actions-main-deps.ts` (get-state, restore-secondary-sub, broadcast, send-active-overlay, debug-visualization, open-palette builders) and rewired handler construction in `src/main.ts`. +- [2026-02-20T07:44:09Z] progress: added parity tests in `src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts`; `src/main.ts` now 2821 LOC. +- [2026-02-20T07:44:09Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/overlay-runtime-main-actions-main-deps.test.js dist/main/runtime/overlay-runtime-main-actions.test.js dist/main/runtime/overlay-bootstrap-main-deps.test.js` pass (10/10). +- [2026-02-20T07:42:00Z] progress: extracted overlay bootstrap deps assembly into `src/main/runtime/overlay-bootstrap-main-deps.ts` (`createBuildOverlayContentMeasurementStoreMainDepsHandler`, `createBuildOverlayModalRuntimeMainDepsHandler`) and rewired overlay measurement/modal constructor wiring in `src/main.ts`. +- [2026-02-20T07:42:00Z] progress: added parity tests in `src/main/runtime/overlay-bootstrap-main-deps.test.ts`; `src/main.ts` now 2794 LOC. +- [2026-02-20T07:42:00Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/overlay-bootstrap-main-deps.test.js dist/core/services/overlay-content-measurement.test.js dist/main/runtime/runtime-bootstrap-main-deps.test.js` pass (8/8). +- [2026-02-20T07:40:24Z] progress: after push of commit `561f7b3`, extracted bootstrap runtime deps assembly into `src/main/runtime/runtime-bootstrap-main-deps.ts` (immersion media, AniList state, config-derived, subsync builders) and rewired those constructor sites in `src/main.ts`. +- [2026-02-20T07:40:24Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/runtime-bootstrap-main-deps.test.js dist/main/runtime/immersion-media.test.js dist/main/runtime/anilist-state.test.js` pass (10/10). - [2026-02-20T07:35:01Z] progress: extracted subtitle processing controller deps assembly into `src/main/runtime/subtitle-processing-main-deps.ts` (`createBuildSubtitleProcessingControllerMainDepsHandler`) and rewired `subtitleProcessingController` construction in `src/main.ts`. - [2026-02-20T07:35:01Z] progress: added parity tests in `src/main/runtime/subtitle-processing-main-deps.test.ts`; `src/main.ts` now 2775 LOC. - [2026-02-20T07:35:01Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/subtitle-processing-main-deps.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/jellyfin-remote-main-deps.test.js` pass (9/9). diff --git a/src/main.ts b/src/main.ts index 308006e..7d7769c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -94,6 +94,12 @@ import { createNotifyAnilistSetupHandler, createRegisterSubminerProtocolClientHandler, } from './main/runtime/anilist-setup-protocol'; +import { + createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler, + createBuildHandleAnilistSetupProtocolUrlMainDepsHandler, + createBuildNotifyAnilistSetupMainDepsHandler, + createBuildRegisterSubminerProtocolClientMainDepsHandler, +} from './main/runtime/anilist-setup-protocol-main-deps'; import { createRefreshAnilistClientSecretStateHandler } from './main/runtime/anilist-token-refresh'; import { createHandleJellyfinRemoteGeneralCommand, @@ -114,11 +120,26 @@ import { createBuildReportJellyfinRemoteStoppedMainDepsHandler, } from './main/runtime/jellyfin-remote-main-deps'; import { createBuildSubtitleProcessingControllerMainDepsHandler } from './main/runtime/subtitle-processing-main-deps'; +import { + createBuildAnilistStateRuntimeMainDepsHandler, + createBuildConfigDerivedRuntimeMainDepsHandler, + createBuildImmersionMediaRuntimeMainDepsHandler, + createBuildMainSubsyncRuntimeMainDepsHandler, +} from './main/runtime/runtime-bootstrap-main-deps'; +import { + createBuildOverlayContentMeasurementStoreMainDepsHandler, + createBuildOverlayModalRuntimeMainDepsHandler, +} from './main/runtime/overlay-bootstrap-main-deps'; import { createEnsureMpvConnectedForJellyfinPlaybackHandler, createLaunchMpvIdleForJellyfinPlaybackHandler, createWaitForMpvConnectedHandler, } from './main/runtime/jellyfin-remote-connection'; +import { + createBuildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler, + createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler, + createBuildWaitForMpvConnectedMainDepsHandler, +} from './main/runtime/jellyfin-remote-connection-main-deps'; import { buildJellyfinSetupFormHtml, createOpenJellyfinSetupWindowHandler, @@ -156,14 +177,29 @@ import { createRunJellyfinCommandHandler } from './main/runtime/jellyfin-command import { createHandleJellyfinListCommands } from './main/runtime/jellyfin-cli-list'; import { createHandleJellyfinPlayCommand } from './main/runtime/jellyfin-cli-play'; import { createHandleJellyfinRemoteAnnounceCommand } from './main/runtime/jellyfin-cli-remote-announce'; +import { + createBuildHandleJellyfinAuthCommandsMainDepsHandler, + createBuildHandleJellyfinListCommandsMainDepsHandler, + createBuildHandleJellyfinPlayCommandMainDepsHandler, + createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler, +} from './main/runtime/jellyfin-cli-main-deps'; +import { createBuildRunJellyfinCommandMainDepsHandler } from './main/runtime/jellyfin-command-dispatch-main-deps'; import { createGetJellyfinClientInfoHandler, createGetResolvedJellyfinConfigHandler, } from './main/runtime/jellyfin-client-info'; +import { + createBuildGetJellyfinClientInfoMainDepsHandler, + createBuildGetResolvedJellyfinConfigMainDepsHandler, +} from './main/runtime/jellyfin-client-info-main-deps'; import { createApplyJellyfinMpvDefaultsHandler, createGetDefaultSocketPathHandler, } from './main/runtime/mpv-jellyfin-defaults'; +import { + createBuildApplyJellyfinMpvDefaultsMainDepsHandler, + createBuildGetDefaultSocketPathMainDepsHandler, +} from './main/runtime/mpv-jellyfin-defaults-main-deps'; import { createBuildMediaRuntimeMainDepsHandler } from './main/runtime/media-runtime-main-deps'; import { createBuildDictionaryRootsMainHandler, @@ -271,6 +307,14 @@ import { createSendToActiveOverlayWindowHandler, createSetOverlayDebugVisualizationEnabledHandler, } from './main/runtime/overlay-runtime-main-actions'; +import { + createBuildBroadcastRuntimeOptionsChangedMainDepsHandler, + createBuildGetRuntimeOptionsStateMainDepsHandler, + createBuildOpenRuntimeOptionsPaletteMainDepsHandler, + createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler, + createBuildSendToActiveOverlayWindowMainDepsHandler, + createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler, +} from './main/runtime/overlay-runtime-main-actions-main-deps'; import { createHandleMpvCommandFromIpcHandler, createRunSubsyncManualFromIpcHandler, @@ -486,10 +530,14 @@ let jellyfinMpvAutoLaunchInFlight: Promise | null = null; let backgroundWarmupsStarted = false; let yomitanLoadInFlight: Promise | null = null; -const applyJellyfinMpvDefaultsHandler = createApplyJellyfinMpvDefaultsHandler({ +const buildApplyJellyfinMpvDefaultsMainDepsHandler = + createBuildApplyJellyfinMpvDefaultsMainDepsHandler({ sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client as never, command), jellyfinLangPref: JELLYFIN_LANG_PREF, }); +const applyJellyfinMpvDefaultsHandler = createApplyJellyfinMpvDefaultsHandler( + buildApplyJellyfinMpvDefaultsMainDepsHandler(), +); function applyJellyfinMpvDefaults(client: MpvIpcClient): void { applyJellyfinMpvDefaultsHandler(client); @@ -549,9 +597,12 @@ const appLogger = { }, }; -const getDefaultSocketPathHandler = createGetDefaultSocketPathHandler({ +const buildGetDefaultSocketPathMainDepsHandler = createBuildGetDefaultSocketPathMainDepsHandler({ platform: process.platform, }); +const getDefaultSocketPathHandler = createGetDefaultSocketPathHandler( + buildGetDefaultSocketPathMainDepsHandler(), +); function getDefaultSocketPath(): string { return getDefaultSocketPathHandler(); @@ -570,19 +621,24 @@ process.on('SIGTERM', () => { }); const overlayManager = createOverlayManager(); -const overlayContentMeasurementStore = createOverlayContentMeasurementStore({ +const buildOverlayContentMeasurementStoreMainDepsHandler = + createBuildOverlayContentMeasurementStoreMainDepsHandler({ now: () => Date.now(), warn: (message: string) => logger.warn(message), }); -const overlayModalRuntime = createOverlayModalRuntimeService({ +const buildOverlayModalRuntimeMainDepsHandler = createBuildOverlayModalRuntimeMainDepsHandler({ getMainWindow: () => overlayManager.getMainWindow(), getInvisibleWindow: () => overlayManager.getInvisibleWindow(), }); +const overlayContentMeasurementStore = createOverlayContentMeasurementStore( + buildOverlayContentMeasurementStoreMainDepsHandler(), +); +const overlayModalRuntime = createOverlayModalRuntimeService(buildOverlayModalRuntimeMainDepsHandler()); const appState = createAppState({ mpvSocketPath: getDefaultSocketPath(), texthookerPort: DEFAULT_TEXTHOOKER_PORT, }); -const immersionMediaRuntime = createImmersionMediaRuntime({ +const buildImmersionMediaRuntimeMainDepsHandler = createBuildImmersionMediaRuntimeMainDepsHandler({ getResolvedConfig: () => getResolvedConfig(), defaultImmersionDbPath: DEFAULT_IMMERSION_DB_PATH, getTracker: () => appState.immersionTracker, @@ -592,7 +648,7 @@ const immersionMediaRuntime = createImmersionMediaRuntime({ logDebug: (message) => logger.debug(message), logInfo: (message) => logger.info(message), }); -const anilistStateRuntime = createAnilistStateRuntime({ +const buildAnilistStateRuntimeMainDepsHandler = createBuildAnilistStateRuntimeMainDepsHandler({ getClientSecretState: () => appState.anilistClientSecretState, setClientSecretState: (next) => { appState.anilistClientSecretState = next; @@ -607,7 +663,7 @@ const anilistStateRuntime = createAnilistStateRuntime({ anilistCachedAccessToken = null; }, }); -const configDerivedRuntime = createConfigDerivedRuntime({ +const buildConfigDerivedRuntimeMainDepsHandler = createBuildConfigDerivedRuntimeMainDepsHandler({ getResolvedConfig: () => getResolvedConfig(), getRuntimeOptionsManager: () => appState.runtimeOptionsManager, platform: process.platform, @@ -615,7 +671,7 @@ const configDerivedRuntime = createConfigDerivedRuntime({ defaultJimakuMaxEntryResults: DEFAULT_CONFIG.jimaku.maxEntryResults, defaultJimakuApiBaseUrl: DEFAULT_CONFIG.jimaku.apiBaseUrl, }); -const subsyncRuntime = createMainSubsyncRuntime({ +const buildMainSubsyncRuntimeMainDepsHandler = createBuildMainSubsyncRuntimeMainDepsHandler({ getMpvClient: () => appState.mpvClient, getResolvedConfig: () => getResolvedConfig(), getSubsyncInProgress: () => appState.subsyncInProgress, @@ -629,6 +685,10 @@ const subsyncRuntime = createMainSubsyncRuntime({ }); }, }); +const immersionMediaRuntime = createImmersionMediaRuntime(buildImmersionMediaRuntimeMainDepsHandler()); +const anilistStateRuntime = createAnilistStateRuntime(buildAnilistStateRuntimeMainDepsHandler()); +const configDerivedRuntime = createConfigDerivedRuntime(buildConfigDerivedRuntimeMainDepsHandler()); +const subsyncRuntime = createMainSubsyncRuntime(buildMainSubsyncRuntimeMainDepsHandler()); let appTray: Tray | null = null; const buildSubtitleProcessingControllerMainDepsHandler = createBuildSubtitleProcessingControllerMainDepsHandler({ @@ -953,9 +1013,12 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService( })(), ); -const getRuntimeOptionsStateHandler = createGetRuntimeOptionsStateHandler({ +const buildGetRuntimeOptionsStateMainDepsHandler = createBuildGetRuntimeOptionsStateMainDepsHandler({ getRuntimeOptionsManager: () => appState.runtimeOptionsManager, }); +const getRuntimeOptionsStateHandler = createGetRuntimeOptionsStateHandler( + buildGetRuntimeOptionsStateMainDepsHandler(), +); function getRuntimeOptionsState(): RuntimeOptionState[] { return getRuntimeOptionsStateHandler(); @@ -965,10 +1028,12 @@ function getOverlayWindows(): BrowserWindow[] { return overlayManager.getOverlayWindows(); } -const restorePreviousSecondarySubVisibilityHandler = createRestorePreviousSecondarySubVisibilityHandler( - { +const buildRestorePreviousSecondarySubVisibilityMainDepsHandler = + createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler({ getMpvClient: () => appState.mpvClient, - }, + }); +const restorePreviousSecondarySubVisibilityHandler = createRestorePreviousSecondarySubVisibilityHandler( + buildRestorePreviousSecondarySubVisibilityMainDepsHandler(), ); function restorePreviousSecondarySubVisibility(): void { @@ -979,20 +1044,28 @@ function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void { overlayManager.broadcastToOverlayWindows(channel, ...args); } -const broadcastRuntimeOptionsChangedHandler = createBroadcastRuntimeOptionsChangedHandler({ - broadcastRuntimeOptionsChangedRuntime, - getRuntimeOptionsState: () => getRuntimeOptionsState(), - broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args), -}); +const buildBroadcastRuntimeOptionsChangedMainDepsHandler = + createBuildBroadcastRuntimeOptionsChangedMainDepsHandler({ + broadcastRuntimeOptionsChangedRuntime, + getRuntimeOptionsState: () => getRuntimeOptionsState(), + broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args), + }); +const broadcastRuntimeOptionsChangedHandler = createBroadcastRuntimeOptionsChangedHandler( + buildBroadcastRuntimeOptionsChangedMainDepsHandler(), +); function broadcastRuntimeOptionsChanged(): void { broadcastRuntimeOptionsChangedHandler(); } -const sendToActiveOverlayWindowHandler = createSendToActiveOverlayWindowHandler({ - sendToActiveOverlayWindowRuntime: (channel, payload, runtimeOptions) => - overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions), -}); +const buildSendToActiveOverlayWindowMainDepsHandler = + createBuildSendToActiveOverlayWindowMainDepsHandler({ + sendToActiveOverlayWindowRuntime: (channel, payload, runtimeOptions) => + overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions), + }); +const sendToActiveOverlayWindowHandler = createSendToActiveOverlayWindowHandler( + buildSendToActiveOverlayWindowMainDepsHandler(), +); function sendToActiveOverlayWindow( channel: string, @@ -1002,24 +1075,30 @@ function sendToActiveOverlayWindow( return sendToActiveOverlayWindowHandler(channel, payload, runtimeOptions); } -const setOverlayDebugVisualizationEnabledHandler = createSetOverlayDebugVisualizationEnabledHandler( - { +const buildSetOverlayDebugVisualizationEnabledMainDepsHandler = + createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler({ setOverlayDebugVisualizationEnabledRuntime, getCurrentEnabled: () => appState.overlayDebugVisualizationEnabled, setCurrentEnabled: (next) => { appState.overlayDebugVisualizationEnabled = next; }, broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args), - }, + }); +const setOverlayDebugVisualizationEnabledHandler = createSetOverlayDebugVisualizationEnabledHandler( + buildSetOverlayDebugVisualizationEnabledMainDepsHandler(), ); function setOverlayDebugVisualizationEnabled(enabled: boolean): void { setOverlayDebugVisualizationEnabledHandler(enabled); } -const openRuntimeOptionsPaletteHandler = createOpenRuntimeOptionsPaletteHandler({ - openRuntimeOptionsPaletteRuntime: () => overlayModalRuntime.openRuntimeOptionsPalette(), -}); +const buildOpenRuntimeOptionsPaletteMainDepsHandler = + createBuildOpenRuntimeOptionsPaletteMainDepsHandler({ + openRuntimeOptionsPaletteRuntime: () => overlayModalRuntime.openRuntimeOptionsPalette(), + }); +const openRuntimeOptionsPaletteHandler = createOpenRuntimeOptionsPaletteHandler( + buildOpenRuntimeOptionsPaletteMainDepsHandler(), +); function openRuntimeOptionsPalette(): void { openRuntimeOptionsPaletteHandler(); @@ -1029,62 +1108,80 @@ function getResolvedConfig() { return configService.getConfig(); } -const getResolvedJellyfinConfigHandler = createGetResolvedJellyfinConfigHandler({ +const buildGetResolvedJellyfinConfigMainDepsHandler = + createBuildGetResolvedJellyfinConfigMainDepsHandler({ getResolvedConfig: () => getResolvedConfig(), }); +const getResolvedJellyfinConfigHandler = createGetResolvedJellyfinConfigHandler( + buildGetResolvedJellyfinConfigMainDepsHandler(), +); function getResolvedJellyfinConfig() { return getResolvedJellyfinConfigHandler(); } -const getJellyfinClientInfoHandler = createGetJellyfinClientInfoHandler({ +const buildGetJellyfinClientInfoMainDepsHandler = createBuildGetJellyfinClientInfoMainDepsHandler({ getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(), getDefaultJellyfinConfig: () => DEFAULT_CONFIG.jellyfin, }); +const getJellyfinClientInfoHandler = createGetJellyfinClientInfoHandler( + buildGetJellyfinClientInfoMainDepsHandler(), +); function getJellyfinClientInfo(config = getResolvedJellyfinConfig()) { return getJellyfinClientInfoHandler(config); } -const waitForMpvConnected = createWaitForMpvConnectedHandler({ +const buildWaitForMpvConnectedMainDepsHandler = createBuildWaitForMpvConnectedMainDepsHandler({ getMpvClient: () => appState.mpvClient, now: () => Date.now(), sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)), }); +const waitForMpvConnected = createWaitForMpvConnectedHandler( + buildWaitForMpvConnectedMainDepsHandler(), +); -const launchMpvIdleForJellyfinPlayback = createLaunchMpvIdleForJellyfinPlaybackHandler({ - getSocketPath: () => appState.mpvSocketPath, - platform: process.platform, - execPath: process.execPath, - defaultMpvLogPath: DEFAULT_MPV_LOG_PATH, - defaultMpvArgs: MPV_JELLYFIN_DEFAULT_ARGS, - removeSocketPath: (socketPath) => { - fs.rmSync(socketPath, { force: true }); - }, - spawnMpv: (args) => - spawn('mpv', args, { - detached: true, - stdio: 'ignore', - }), - logWarn: (message, error) => logger.warn(message, error), - logInfo: (message) => logger.info(message), -}); +const buildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler = + createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler({ + getSocketPath: () => appState.mpvSocketPath, + platform: process.platform, + execPath: process.execPath, + defaultMpvLogPath: DEFAULT_MPV_LOG_PATH, + defaultMpvArgs: MPV_JELLYFIN_DEFAULT_ARGS, + removeSocketPath: (socketPath) => { + fs.rmSync(socketPath, { force: true }); + }, + spawnMpv: (args) => + spawn('mpv', args, { + detached: true, + stdio: 'ignore', + }), + logWarn: (message, error) => logger.warn(message, error), + logInfo: (message) => logger.info(message), + }); +const launchMpvIdleForJellyfinPlayback = createLaunchMpvIdleForJellyfinPlaybackHandler( + buildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler(), +); -const ensureMpvConnectedForJellyfinPlayback = createEnsureMpvConnectedForJellyfinPlaybackHandler({ - getMpvClient: () => appState.mpvClient, - setMpvClient: (client) => { - appState.mpvClient = client as MpvIpcClient | null; - }, - createMpvClient: () => createMpvClientRuntimeService(), - waitForMpvConnected: (timeoutMs) => waitForMpvConnected(timeoutMs), - launchMpvIdleForJellyfinPlayback: () => launchMpvIdleForJellyfinPlayback(), - getAutoLaunchInFlight: () => jellyfinMpvAutoLaunchInFlight, - setAutoLaunchInFlight: (promise) => { - jellyfinMpvAutoLaunchInFlight = promise; - }, - connectTimeoutMs: JELLYFIN_MPV_CONNECT_TIMEOUT_MS, - autoLaunchTimeoutMs: JELLYFIN_MPV_AUTO_LAUNCH_TIMEOUT_MS, -}); +const buildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler = + createBuildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler({ + getMpvClient: () => appState.mpvClient, + setMpvClient: (client) => { + appState.mpvClient = client as MpvIpcClient | null; + }, + createMpvClient: () => createMpvClientRuntimeService(), + waitForMpvConnected: (timeoutMs) => waitForMpvConnected(timeoutMs), + launchMpvIdleForJellyfinPlayback: () => launchMpvIdleForJellyfinPlayback(), + getAutoLaunchInFlight: () => jellyfinMpvAutoLaunchInFlight, + setAutoLaunchInFlight: (promise) => { + jellyfinMpvAutoLaunchInFlight = promise; + }, + connectTimeoutMs: JELLYFIN_MPV_CONNECT_TIMEOUT_MS, + autoLaunchTimeoutMs: JELLYFIN_MPV_AUTO_LAUNCH_TIMEOUT_MS, + }); +const ensureMpvConnectedForJellyfinPlayback = createEnsureMpvConnectedForJellyfinPlaybackHandler( + buildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler(), +); const preloadJellyfinExternalSubtitles = createPreloadJellyfinExternalSubtitlesHandler({ listJellyfinSubtitleTracks: (session, clientInfo, itemId) => @@ -1143,7 +1240,8 @@ const playJellyfinItemInMpv = createPlayJellyfinItemInMpvHandler({ }, }); -const handleJellyfinAuthCommands = createHandleJellyfinAuthCommands({ +const buildHandleJellyfinAuthCommandsMainDepsHandler = + createBuildHandleJellyfinAuthCommandsMainDepsHandler({ patchRawConfig: (patch) => { configService.patchRawConfig(patch); }, @@ -1151,8 +1249,12 @@ const handleJellyfinAuthCommands = createHandleJellyfinAuthCommands({ authenticateWithPasswordRuntime(serverUrl, username, password, clientInfo), logInfo: (message) => logger.info(message), }); +const handleJellyfinAuthCommands = createHandleJellyfinAuthCommands( + buildHandleJellyfinAuthCommandsMainDepsHandler(), +); -const handleJellyfinListCommands = createHandleJellyfinListCommands({ +const buildHandleJellyfinListCommandsMainDepsHandler = + createBuildHandleJellyfinListCommandsMainDepsHandler({ listJellyfinLibraries: (session, clientInfo) => listJellyfinLibrariesRuntime(session, clientInfo), listJellyfinItems: (session, clientInfo, params) => listJellyfinItemsRuntime(session, clientInfo, params), @@ -1160,19 +1262,31 @@ const handleJellyfinListCommands = createHandleJellyfinListCommands({ listJellyfinSubtitleTracksRuntime(session, clientInfo, itemId), logInfo: (message) => logger.info(message), }); +const handleJellyfinListCommands = createHandleJellyfinListCommands( + buildHandleJellyfinListCommandsMainDepsHandler(), +); -const handleJellyfinPlayCommand = createHandleJellyfinPlayCommand({ +const buildHandleJellyfinPlayCommandMainDepsHandler = createBuildHandleJellyfinPlayCommandMainDepsHandler( + { playJellyfinItemInMpv: (params) => playJellyfinItemInMpv(params as Parameters[0]), logWarn: (message) => logger.warn(message), -}); + }, +); +const handleJellyfinPlayCommand = createHandleJellyfinPlayCommand( + buildHandleJellyfinPlayCommandMainDepsHandler(), +); -const handleJellyfinRemoteAnnounceCommand = createHandleJellyfinRemoteAnnounceCommand({ +const buildHandleJellyfinRemoteAnnounceCommandMainDepsHandler = + createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler({ startJellyfinRemoteSession: () => startJellyfinRemoteSession(), getRemoteSession: () => appState.jellyfinRemoteSession, logInfo: (message) => logger.info(message), logWarn: (message) => logger.warn(message), }); +const handleJellyfinRemoteAnnounceCommand = createHandleJellyfinRemoteAnnounceCommand( + buildHandleJellyfinRemoteAnnounceCommandMainDepsHandler(), +); const startJellyfinRemoteSession = createStartJellyfinRemoteSessionHandler({ getJellyfinConfig: () => getResolvedJellyfinConfig(), @@ -1201,7 +1315,7 @@ const stopJellyfinRemoteSession = createStopJellyfinRemoteSessionHandler({ }, }); -const runJellyfinCommand = createRunJellyfinCommandHandler({ +const buildRunJellyfinCommandMainDepsHandler = createBuildRunJellyfinCommandMainDepsHandler({ getJellyfinConfig: () => getResolvedJellyfinConfig(), defaultServerUrl: DEFAULT_CONFIG.jellyfin.serverUrl, getJellyfinClientInfo: (jellyfinConfig) => getJellyfinClientInfo(jellyfinConfig), @@ -1210,56 +1324,76 @@ const runJellyfinCommand = createRunJellyfinCommandHandler({ handleListCommands: (params) => handleJellyfinListCommands(params), handlePlayCommand: (params) => handleJellyfinPlayCommand(params), }); +const runJellyfinCommand = createRunJellyfinCommandHandler( + buildRunJellyfinCommandMainDepsHandler(), +); -const notifyAnilistSetup = createNotifyAnilistSetupHandler({ +const buildNotifyAnilistSetupMainDepsHandler = createBuildNotifyAnilistSetupMainDepsHandler({ hasMpvClient: () => Boolean(appState.mpvClient), showMpvOsd: (message) => showMpvOsd(message), showDesktopNotification: (title, options) => showDesktopNotification(title, options), logInfo: (message) => logger.info(message), }); +const notifyAnilistSetup = createNotifyAnilistSetupHandler( + buildNotifyAnilistSetupMainDepsHandler(), +); -const consumeAnilistSetupTokenFromUrl = createConsumeAnilistSetupTokenFromUrlHandler({ - consumeAnilistSetupCallbackUrl, - saveToken: (token) => anilistTokenStore.saveToken(token), - setCachedToken: (token) => { - anilistCachedAccessToken = token; - }, - setResolvedState: (resolvedAt) => { - anilistStateRuntime.setClientSecretState({ - status: 'resolved', - source: 'stored', - message: 'saved token from AniList login', - resolvedAt, - errorAt: null, - }); - }, - setSetupPageOpened: (opened) => { - appState.anilistSetupPageOpened = opened; - }, - onSuccess: () => { - notifyAnilistSetup('AniList login success'); - }, - closeWindow: () => { - if (appState.anilistSetupWindow && !appState.anilistSetupWindow.isDestroyed()) { - appState.anilistSetupWindow.close(); - } - }, -}); +const buildConsumeAnilistSetupTokenFromUrlMainDepsHandler = + createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler({ + consumeAnilistSetupCallbackUrl, + saveToken: (token) => anilistTokenStore.saveToken(token), + setCachedToken: (token) => { + anilistCachedAccessToken = token; + }, + setResolvedState: (resolvedAt) => { + anilistStateRuntime.setClientSecretState({ + status: 'resolved', + source: 'stored', + message: 'saved token from AniList login', + resolvedAt, + errorAt: null, + }); + }, + setSetupPageOpened: (opened) => { + appState.anilistSetupPageOpened = opened; + }, + onSuccess: () => { + notifyAnilistSetup('AniList login success'); + }, + closeWindow: () => { + if (appState.anilistSetupWindow && !appState.anilistSetupWindow.isDestroyed()) { + appState.anilistSetupWindow.close(); + } + }, + }); +const consumeAnilistSetupTokenFromUrl = createConsumeAnilistSetupTokenFromUrlHandler( + buildConsumeAnilistSetupTokenFromUrlMainDepsHandler(), +); -const handleAnilistSetupProtocolUrl = createHandleAnilistSetupProtocolUrlHandler({ - consumeAnilistSetupTokenFromUrl: (rawUrl) => consumeAnilistSetupTokenFromUrl(rawUrl), - logWarn: (message, details) => logger.warn(message, details), -}); +const buildHandleAnilistSetupProtocolUrlMainDepsHandler = + createBuildHandleAnilistSetupProtocolUrlMainDepsHandler({ + consumeAnilistSetupTokenFromUrl: (rawUrl) => consumeAnilistSetupTokenFromUrl(rawUrl), + logWarn: (message, details) => logger.warn(message, details), + }); +const handleAnilistSetupProtocolUrl = createHandleAnilistSetupProtocolUrlHandler( + buildHandleAnilistSetupProtocolUrlMainDepsHandler(), +); -const registerSubminerProtocolClient = createRegisterSubminerProtocolClientHandler({ - isDefaultApp: () => Boolean(process.defaultApp), - getArgv: () => process.argv, - execPath: process.execPath, - resolvePath: (value) => path.resolve(value), - setAsDefaultProtocolClient: (scheme, appPath, args) => - appPath ? app.setAsDefaultProtocolClient(scheme, appPath, args) : app.setAsDefaultProtocolClient(scheme), - logWarn: (message, details) => logger.warn(message, details), -}); +const buildRegisterSubminerProtocolClientMainDepsHandler = + createBuildRegisterSubminerProtocolClientMainDepsHandler({ + isDefaultApp: () => Boolean(process.defaultApp), + getArgv: () => process.argv, + execPath: process.execPath, + resolvePath: (value) => path.resolve(value), + setAsDefaultProtocolClient: (scheme, appPath, args) => + appPath + ? app.setAsDefaultProtocolClient(scheme, appPath, args) + : app.setAsDefaultProtocolClient(scheme), + logWarn: (message, details) => logger.warn(message, details), + }); +const registerSubminerProtocolClient = createRegisterSubminerProtocolClientHandler( + buildRegisterSubminerProtocolClientMainDepsHandler(), +); function openAnilistSetupWindow(): void { createOpenAnilistSetupWindowHandler({ diff --git a/src/main/runtime/anilist-setup-protocol-main-deps.test.ts b/src/main/runtime/anilist-setup-protocol-main-deps.test.ts new file mode 100644 index 0000000..a6c4c06 --- /dev/null +++ b/src/main/runtime/anilist-setup-protocol-main-deps.test.ts @@ -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); +}); diff --git a/src/main/runtime/anilist-setup-protocol-main-deps.ts b/src/main/runtime/anilist-setup-protocol-main-deps.ts new file mode 100644 index 0000000..b028a2a --- /dev/null +++ b/src/main/runtime/anilist-setup-protocol-main-deps.ts @@ -0,0 +1,64 @@ +import type { + createConsumeAnilistSetupTokenFromUrlHandler, + createHandleAnilistSetupProtocolUrlHandler, + createNotifyAnilistSetupHandler, + createRegisterSubminerProtocolClientHandler, +} from './anilist-setup-protocol'; + +type NotifyAnilistSetupMainDeps = Parameters[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), + }); +} diff --git a/src/main/runtime/jellyfin-cli-main-deps.test.ts b/src/main/runtime/jellyfin-cli-main-deps.test.ts new file mode 100644 index 0000000..1854447 --- /dev/null +++ b/src/main/runtime/jellyfin-cli-main-deps.test.ts @@ -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']); +}); diff --git a/src/main/runtime/jellyfin-cli-main-deps.ts b/src/main/runtime/jellyfin-cli-main-deps.ts new file mode 100644 index 0000000..7294b35 --- /dev/null +++ b/src/main/runtime/jellyfin-cli-main-deps.ts @@ -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[0]; +type HandleJellyfinListCommandsMainDeps = Parameters[0]; +type HandleJellyfinPlayCommandMainDeps = Parameters[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), + }); +} diff --git a/src/main/runtime/jellyfin-client-info-main-deps.test.ts b/src/main/runtime/jellyfin-client-info-main-deps.test.ts new file mode 100644 index 0000000..8505644 --- /dev/null +++ b/src/main/runtime/jellyfin-client-info-main-deps.test.ts @@ -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); +}); diff --git a/src/main/runtime/jellyfin-client-info-main-deps.ts b/src/main/runtime/jellyfin-client-info-main-deps.ts new file mode 100644 index 0000000..d530697 --- /dev/null +++ b/src/main/runtime/jellyfin-client-info-main-deps.ts @@ -0,0 +1,24 @@ +import type { + createGetJellyfinClientInfoHandler, + createGetResolvedJellyfinConfigHandler, +} from './jellyfin-client-info'; + +type GetResolvedJellyfinConfigMainDeps = Parameters[0]; +type GetJellyfinClientInfoMainDeps = Parameters[0]; + +export function createBuildGetResolvedJellyfinConfigMainDepsHandler( + deps: GetResolvedJellyfinConfigMainDeps, +) { + return (): GetResolvedJellyfinConfigMainDeps => ({ + getResolvedConfig: () => deps.getResolvedConfig(), + }); +} + +export function createBuildGetJellyfinClientInfoMainDepsHandler( + deps: GetJellyfinClientInfoMainDeps, +) { + return (): GetJellyfinClientInfoMainDeps => ({ + getResolvedJellyfinConfig: () => deps.getResolvedJellyfinConfig(), + getDefaultJellyfinConfig: () => deps.getDefaultJellyfinConfig(), + }); +} diff --git a/src/main/runtime/jellyfin-command-dispatch-main-deps.test.ts b/src/main/runtime/jellyfin-command-dispatch-main-deps.test.ts new file mode 100644 index 0000000..0a51697 --- /dev/null +++ b/src/main/runtime/jellyfin-command-dispatch-main-deps.test.ts @@ -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']); +}); diff --git a/src/main/runtime/jellyfin-command-dispatch-main-deps.ts b/src/main/runtime/jellyfin-command-dispatch-main-deps.ts new file mode 100644 index 0000000..33a4994 --- /dev/null +++ b/src/main/runtime/jellyfin-command-dispatch-main-deps.ts @@ -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 = { + getJellyfinConfig: () => TConfig; + defaultServerUrl: string; + getJellyfinClientInfo: (config: TConfig) => TClientInfo; + handleAuthCommands: (params: { + args: CliArgs; + jellyfinConfig: TConfig; + serverUrl: string; + clientInfo: TClientInfo; + }) => Promise; + handleRemoteAnnounceCommand: (args: CliArgs) => Promise; + handleListCommands: (params: { + args: CliArgs; + session: JellyfinSession; + clientInfo: TClientInfo; + jellyfinConfig: TConfig; + }) => Promise; + handlePlayCommand: (params: { + args: CliArgs; + session: JellyfinSession; + clientInfo: TClientInfo; + jellyfinConfig: TConfig; + }) => Promise; +}; + +export function createBuildRunJellyfinCommandMainDepsHandler< + TClientInfo, + TConfig extends JellyfinConfigBase, +>(deps: RunJellyfinCommandMainDeps) { + return (): RunJellyfinCommandMainDeps => ({ + 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), + }); +} diff --git a/src/main/runtime/jellyfin-remote-connection-main-deps.test.ts b/src/main/runtime/jellyfin-remote-connection-main-deps.test.ts new file mode 100644 index 0000000..79de86a --- /dev/null +++ b/src/main/runtime/jellyfin-remote-connection-main-deps.test.ts @@ -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']); +}); diff --git a/src/main/runtime/jellyfin-remote-connection-main-deps.ts b/src/main/runtime/jellyfin-remote-connection-main-deps.ts new file mode 100644 index 0000000..563ecc1 --- /dev/null +++ b/src/main/runtime/jellyfin-remote-connection-main-deps.ts @@ -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, + }); +} diff --git a/src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts b/src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts new file mode 100644 index 0000000..301f16d --- /dev/null +++ b/src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts @@ -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'); +}); diff --git a/src/main/runtime/mpv-jellyfin-defaults-main-deps.ts b/src/main/runtime/mpv-jellyfin-defaults-main-deps.ts new file mode 100644 index 0000000..f79624e --- /dev/null +++ b/src/main/runtime/mpv-jellyfin-defaults-main-deps.ts @@ -0,0 +1,24 @@ +import type { + createApplyJellyfinMpvDefaultsHandler, + createGetDefaultSocketPathHandler, +} from './mpv-jellyfin-defaults'; + +type ApplyJellyfinMpvDefaultsMainDeps = Parameters[0]; +type GetDefaultSocketPathMainDeps = Parameters[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, + }); +} diff --git a/src/main/runtime/overlay-bootstrap-main-deps.test.ts b/src/main/runtime/overlay-bootstrap-main-deps.test.ts new file mode 100644 index 0000000..1e897a2 --- /dev/null +++ b/src/main/runtime/overlay-bootstrap-main-deps.test.ts @@ -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); +}); diff --git a/src/main/runtime/overlay-bootstrap-main-deps.ts b/src/main/runtime/overlay-bootstrap-main-deps.ts new file mode 100644 index 0000000..dfea96c --- /dev/null +++ b/src/main/runtime/overlay-bootstrap-main-deps.ts @@ -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(), + }); +} diff --git a/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts b/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts new file mode 100644 index 0000000..4e50aef --- /dev/null +++ b/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts @@ -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']); +}); diff --git a/src/main/runtime/overlay-runtime-main-actions-main-deps.ts b/src/main/runtime/overlay-runtime-main-actions-main-deps.ts new file mode 100644 index 0000000..683c4ee --- /dev/null +++ b/src/main/runtime/overlay-runtime-main-actions-main-deps.ts @@ -0,0 +1,89 @@ +import { + createBroadcastRuntimeOptionsChangedHandler, + createGetRuntimeOptionsStateHandler, + createOpenRuntimeOptionsPaletteHandler, + createRestorePreviousSecondarySubVisibilityHandler, + createSendToActiveOverlayWindowHandler, + createSetOverlayDebugVisualizationEnabledHandler, +} from './overlay-runtime-main-actions'; + +type GetRuntimeOptionsStateMainDeps = Parameters[0]; +type RestorePreviousSecondarySubVisibilityMainDeps = Parameters< + typeof createRestorePreviousSecondarySubVisibilityHandler +>[0]; +type BroadcastRuntimeOptionsChangedMainDeps = Parameters< + typeof createBroadcastRuntimeOptionsChangedHandler +>[0]; +type SendToActiveOverlayWindowMainDeps = Parameters[0]; +type SetOverlayDebugVisualizationEnabledMainDeps = Parameters< + typeof createSetOverlayDebugVisualizationEnabledHandler +>[0]; +type OpenRuntimeOptionsPaletteMainDeps = Parameters[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(), + }); +} diff --git a/src/main/runtime/runtime-bootstrap-main-deps.test.ts b/src/main/runtime/runtime-bootstrap-main-deps.test.ts new file mode 100644 index 0000000..8b53c16 --- /dev/null +++ b/src/main/runtime/runtime-bootstrap-main-deps.test.ts @@ -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']); +}); diff --git a/src/main/runtime/runtime-bootstrap-main-deps.ts b/src/main/runtime/runtime-bootstrap-main-deps.ts new file mode 100644 index 0000000..015f55f --- /dev/null +++ b/src/main/runtime/runtime-bootstrap-main-deps.ts @@ -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), + }); +}