mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor(main): extract anilist/mpv runtime composers
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
---
|
||||
id: TASK-71
|
||||
title: Split main.ts into domain runtime modules round 2
|
||||
status: In Progress
|
||||
assignee: []
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-02-18 11:35'
|
||||
updated_date: '2026-02-21 03:40'
|
||||
updated_date: '2026-02-21 04:57'
|
||||
labels:
|
||||
- architecture
|
||||
- refactor
|
||||
@@ -45,14 +46,48 @@ priority: high
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 `src/main.ts` responsibilities reduced to composition/wiring concerns
|
||||
- [ ] #2 Extracted runtime modules have focused interfaces and isolated tests
|
||||
- [ ] #3 No CLI/IPC regressions in existing test suite
|
||||
- [ ] #4 Docs reflect new module boundaries
|
||||
- [x] #1 `src/main.ts` responsibilities reduced to composition/wiring concerns
|
||||
- [x] #2 Extracted runtime modules have focused interfaces and isolated tests
|
||||
- [x] #3 No CLI/IPC regressions in existing test suite
|
||||
- [x] #4 Docs reflect new module boundaries
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1) Extract AniList tracking composition from src/main.ts (refresh/guess/probe/retry/update handler assembly) into src/main/runtime/composers/anilist-tracking-composer.ts with focused seam tests.
|
||||
2) Extract MPV runtime composition from src/main.ts (bind event handlers, MPV client factory, subtitle render metrics, tokenizer warmups) into src/main/runtime/composers/mpv-runtime-composer.ts with focused seam tests.
|
||||
3) Rewire src/main.ts to consume both composers while preserving existing local helper contracts and downstream behavior.
|
||||
4) Update architecture docs to reflect composition boundary under src/main/runtime/composers/*.
|
||||
5) Run verification gate (build + check:main-fanin + test:core:dist), then record TASK-71 notes/AC/DoD and finalize if green.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
2026-02-21: extracted two remaining high-noise main-process composition clusters from `src/main.ts` into dedicated runtime composers: `src/main/runtime/composers/anilist-tracking-composer.ts` (AniList media tracking/probe/retry/update assembly) and `src/main/runtime/composers/mpv-runtime-composer.ts` (MPV event binding/factory/metrics/tokenizer/warmups assembly).
|
||||
|
||||
Rewired `src/main.ts` to consume composer outputs while preserving existing helper contracts/call sites (`refreshAnilistClientSecretState`, `processNextAnilistRetryUpdate`, `maybeRunAnilistPostWatchUpdate`, `createMpvClientRuntimeService`, `updateMpvSubtitleRenderMetrics`, `tokenizeSubtitle`, `startBackgroundWarmups`).
|
||||
|
||||
Added focused seam tests: `src/main/runtime/composers/anilist-tracking-composer.test.ts` and `src/main/runtime/composers/mpv-runtime-composer.test.ts`.
|
||||
|
||||
Added `src/main/runtime/composers/index.ts` barrel and updated main imports to keep runtime fan-in strict gate green.
|
||||
|
||||
Docs updated for new boundaries in `docs/architecture.md` and `docs/development.md`.
|
||||
|
||||
Verification: `bun run build`, `bun test src/main/runtime/composers/anilist-tracking-composer.test.ts src/main/runtime/composers/mpv-runtime-composer.test.ts`, `bun run check:main-fanin:strict` (85 import lines / 10 unique runtime paths, pass), and `bun run test:fast` (pass).
|
||||
|
||||
`src/main.ts` LOC reduced from 2956 to 2875 in this pass.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Completed TASK-71 round 2 by extracting AniList tracking and MPV runtime assembly clusters out of `src/main.ts` into dedicated composer modules with focused seam tests, then rewiring the composition root to consume those modules without changing runtime behavior. Added a composers barrel to reduce runtime import fan-in and updated architecture/development docs to reflect composer-first boundaries; verification gates (`build`, `check:main-fanin:strict`, composer tests, `test:fast`) all pass.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
## Definition of Done
|
||||
<!-- DOD:BEGIN -->
|
||||
- [ ] #1 `bun run test:fast` passes
|
||||
- [ ] #2 Architecture docs updated
|
||||
- [x] #1 `bun run test:fast` passes
|
||||
- [x] #2 Architecture docs updated
|
||||
<!-- DOD:END -->
|
||||
|
||||
@@ -16,7 +16,7 @@ SubMiner uses a service-oriented Electron architecture with a composition-orient
|
||||
|
||||
```text
|
||||
src/
|
||||
main.ts # Entry point — delegates to src/main/ composition modules
|
||||
main.ts # Entry point — delegates to runtime composers/domain modules
|
||||
preload.ts # Electron preload bridge
|
||||
types.ts # Shared type definitions
|
||||
main/ # Composition root modules (extracted from main.ts)
|
||||
@@ -31,6 +31,9 @@ src/
|
||||
startup-lifecycle.ts # App-ready initialization sequence
|
||||
state.ts # Application runtime state container
|
||||
subsync-runtime.ts # Subsync command orchestration
|
||||
runtime/
|
||||
composers/ # Composition assembly clusters consumed by main.ts
|
||||
domains/ # Domain barrel exports for runtime services
|
||||
core/
|
||||
services/ # ~60 focused service modules (see below)
|
||||
utils/ # Pure helpers and coercion/config utilities
|
||||
@@ -154,7 +157,7 @@ Most runtime code follows a dependency-injection pattern:
|
||||
3. Build runtime deps in `src/main/` composition modules; extract an adapter/helper only when it adds meaningful behavior or reuse.
|
||||
4. Call the service from lifecycle/command wiring points.
|
||||
|
||||
The composition root (`src/main.ts`) delegates to focused modules in `src/main/`:
|
||||
The composition root (`src/main.ts`) delegates to focused modules in `src/main/` and `src/main/runtime/composers/`:
|
||||
|
||||
- `startup.ts` — argv/env processing and bootstrap flow
|
||||
- `app-lifecycle.ts` — Electron lifecycle event registration
|
||||
@@ -164,6 +167,8 @@ The composition root (`src/main.ts`) delegates to focused modules in `src/main/`
|
||||
- `cli-runtime.ts` — CLI command parsing and dispatch
|
||||
- `overlay-runtime.ts` — overlay window selection and modal state management
|
||||
- `subsync-runtime.ts` — subsync command orchestration
|
||||
- `runtime/composers/anilist-tracking-composer.ts` — AniList media tracking/probe/retry wiring
|
||||
- `runtime/composers/mpv-runtime-composer.ts` — MPV event/factory/tokenizer/warmup wiring
|
||||
|
||||
This keeps side effects explicit and makes behavior easy to unit-test with fakes.
|
||||
|
||||
@@ -228,7 +233,7 @@ flowchart TD
|
||||
|
||||
## Extension Rules
|
||||
|
||||
- Add behavior to an existing service in `src/core/services/*` or create a new focused module in `src/main/` for composition-level logic — not as ad-hoc logic in `main.ts`.
|
||||
- Add behavior to an existing service in `src/core/services/*` or create a focused composition module in `src/main/` / `src/main/runtime/composers/` — not as ad-hoc logic in `main.ts`.
|
||||
- Keep service APIs explicit and narrowly scoped.
|
||||
- Prefer additive changes that preserve existing CLI flags and IPC channel behavior.
|
||||
- Add/update unit tests for each service extraction or behavior change.
|
||||
|
||||
@@ -115,7 +115,7 @@ Run `make help` for a full list of targets. Key ones:
|
||||
|
||||
- To add or change a config option, update `src/config/definitions.ts` first. Defaults, runtime-option metadata, and generated `config.example.jsonc` are derived from this centralized source.
|
||||
- Overlay window/visibility state is owned by `src/core/services/overlay-manager.ts`.
|
||||
- Main process composition is now split across `src/main/` modules (`startup.ts`, `app-lifecycle.ts`, `startup-lifecycle.ts`, `state.ts`, `ipc-runtime.ts`, `cli-runtime.ts`, `overlay-runtime.ts`, `subsync-runtime.ts`).
|
||||
- Main process composition is split across `src/main/` modules plus focused runtime composers under `src/main/runtime/composers/*` (for example AniList tracking and MPV runtime assembly clusters).
|
||||
- Runtime domain imports for `src/main.ts` should route through `src/main/runtime/domains/*`; shared domain access point is `src/main/runtime/registry.ts`.
|
||||
- Linux packaged desktop launches pass `--background` using electron-builder `build.linux.executableArgs` in `package.json`.
|
||||
- MPV service has been split into transport, protocol, state, and properties layers in `src/core/services/`.
|
||||
|
||||
@@ -31,3 +31,4 @@ Read first. Keep concise.
|
||||
| `opencode-task95-config-20260221T031843Z-m4k9` | `opencode-task95-config` | `Implement TASK-95 config extraction for src/config/service.ts` | `done` | `docs/subagents/agents/opencode-task95-config-20260221T031843Z-m4k9.md` | `2026-02-21T03:26:57Z` |
|
||||
| `codex-task95-anki-20260221T031836Z-6f3e` | `codex-task95-anki` | `Implement TASK-95 anki-integration extraction for field-grouping merge collaborator` | `done` | `docs/subagents/agents/codex-task95-anki-20260221T031836Z-6f3e.md` | `2026-02-21T03:26:55Z` |
|
||||
| `opencode-task-94-20260221T033647Z-7ou2` | `opencode-task-94` | `Finish TASK-94 thin composition root refactor and close acceptance criteria` | `done` | `docs/subagents/agents/opencode-task-94-20260221T033647Z-7ou2.md` | `2026-02-21T04:12:45Z` |
|
||||
| `codex-task71-round2-20260221T043541Z-k9t3` | `codex-task71-round2` | `Execute TASK-71 round 2 split of main.ts into domain runtime modules` | `done` | `docs/subagents/agents/codex-task71-round2-20260221T043541Z-k9t3.md` | `2026-02-21T04:57:00Z` |
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Agent: codex-task71-round2-20260221T043541Z-k9t3
|
||||
|
||||
- alias: codex-task71-round2
|
||||
- mission: Execute TASK-71 round 2 by further splitting main.ts wiring into domain runtime modules with focused tests/docs updates
|
||||
- status: done
|
||||
- branch: main
|
||||
- started_at: 2026-02-21T04:35:41Z
|
||||
- heartbeat_minutes: 5
|
||||
|
||||
## Current Work (newest first)
|
||||
|
||||
- [2026-02-21T04:57:00Z] completed: TASK-71 moved to Done in Backlog MCP; AC + DoD checked; notes/final summary recorded.
|
||||
- [2026-02-21T04:55:00Z] verify: `bun run build`, `bun run check:main-fanin:strict`, `bun run test:fast` all passing.
|
||||
- [2026-02-21T04:52:00Z] progress: added `src/main/runtime/composers/index.ts` barrel and rewired `src/main.ts` composer imports to recover strict fan-in gate (85 lines / 10 unique paths).
|
||||
- [2026-02-21T04:49:00Z] progress: rewired `src/main.ts` AniList + MPV assembly clusters to `composeAnilistTrackingHandlers` + `composeMpvRuntimeHandlers`; added architecture/development docs updates.
|
||||
- [2026-02-21T04:44:00Z] progress: parallel subagents delivered new composers + seam tests for AniList tracking and MPV runtime clusters.
|
||||
- [2026-02-21T04:35:41Z] intent: load TASK-71 context from Backlog MCP, create plan via writing-plans, execute via executing-plans, no commit.
|
||||
- [2026-02-21T04:35:41Z] planned files: `src/main.ts`, `src/main/runtime/composers/*`, `src/main/runtime/domains/*`, focused tests, `docs/architecture.md`, Backlog TASK-71 metadata via MCP tools only.
|
||||
- [2026-02-21T04:35:41Z] assumptions: existing dirty tree includes prior TASK-94/TASK-95 work; preserve unrelated edits.
|
||||
|
||||
## Files Touched
|
||||
|
||||
- `docs/subagents/agents/codex-task71-round2-20260221T043541Z-k9t3.md`
|
||||
- `docs/subagents/INDEX.md`
|
||||
- `docs/subagents/collaboration.md`
|
||||
- `docs/plans/2026-02-21-task-71-round2-main-runtime-modules.md`
|
||||
- `src/main.ts`
|
||||
- `src/main/runtime/composers/anilist-tracking-composer.ts`
|
||||
- `src/main/runtime/composers/anilist-tracking-composer.test.ts`
|
||||
- `src/main/runtime/composers/mpv-runtime-composer.ts`
|
||||
- `src/main/runtime/composers/mpv-runtime-composer.test.ts`
|
||||
- `src/main/runtime/composers/index.ts`
|
||||
- `docs/architecture.md`
|
||||
- `docs/development.md`
|
||||
|
||||
## Assumptions
|
||||
|
||||
- User requested direct plan+execution flow; no extra approval gate needed.
|
||||
|
||||
## Open Questions / Blockers
|
||||
|
||||
- none
|
||||
|
||||
## Next Step
|
||||
|
||||
- Handoff complete; await user direction (optional commit/push).
|
||||
@@ -29,3 +29,5 @@ Shared notes. Append-only.
|
||||
- [2026-02-21T03:36:58Z] [opencode-task-94-20260221T033647Z-7ou2|opencode-task-94] starting TASK-94 finish pass: pull backlog context, write+execute plan via writing-plans/executing-plans, and close remaining AC without commit.
|
||||
- [2026-02-21T04:11:57Z] [opencode-task-94-20260221T033647Z-7ou2|opencode-task-94] extracted IPC/shortcuts/startup-lifecycle/app-ready clusters behind composer modules, rewired `src/main.ts`, added focused composer tests, and revalidated build + `check:main-fanin` + `test:core:dist`.
|
||||
- [2026-02-21T04:12:45Z] [opencode-task-94-20260221T033647Z-7ou2|opencode-task-94] TASK-94 finalized in Backlog MCP: AC checklist complete, notes+final summary recorded, status moved to Done.
|
||||
- [2026-02-21T04:35:41Z] [codex-task71-round2-20260221T043541Z-k9t3|codex-task71-round2] overlap note: starting TASK-71 round 2 follow-up on `src/main.ts` + `src/main/runtime/composers/*` + docs; preserving prior TASK-94/TASK-95 edits.
|
||||
- [2026-02-21T04:57:00Z] [codex-task71-round2-20260221T043541Z-k9t3|codex-task71-round2] completed TASK-71: extracted AniList tracking + MPV runtime composition into new composers, added seam tests, rewired `main.ts` + composer barrel, strict fan-in green, and finalized Backlog task as Done.
|
||||
|
||||
346
src/main.ts
346
src/main.ts
@@ -425,12 +425,16 @@ import {
|
||||
} from './config';
|
||||
import { resolveConfigDir } from './config/path-resolution';
|
||||
import { createMainRuntimeRegistry } from './main/runtime/registry';
|
||||
import { composeAnilistSetupHandlers } from './main/runtime/composers/anilist-setup-composer';
|
||||
import { composeJellyfinRemoteHandlers } from './main/runtime/composers/jellyfin-remote-composer';
|
||||
import { composeIpcRuntimeHandlers } from './main/runtime/composers/ipc-runtime-composer';
|
||||
import { composeShortcutRuntimes } from './main/runtime/composers/shortcuts-runtime-composer';
|
||||
import { composeStartupLifecycleHandlers } from './main/runtime/composers/startup-lifecycle-composer';
|
||||
import { composeAppReadyRuntime } from './main/runtime/composers/app-ready-composer';
|
||||
import {
|
||||
composeAnilistSetupHandlers,
|
||||
composeAnilistTrackingHandlers,
|
||||
composeAppReadyRuntime,
|
||||
composeIpcRuntimeHandlers,
|
||||
composeJellyfinRemoteHandlers,
|
||||
composeMpvRuntimeHandlers,
|
||||
composeShortcutRuntimes,
|
||||
composeStartupLifecycleHandlers,
|
||||
} from './main/runtime/composers';
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal');
|
||||
@@ -1496,8 +1500,19 @@ function openJellyfinSetupWindow(): void {
|
||||
createOpenJellyfinSetupWindowHandler(buildOpenJellyfinSetupWindowMainDepsHandler())();
|
||||
}
|
||||
|
||||
const buildRefreshAnilistClientSecretStateMainDepsHandler =
|
||||
createBuildRefreshAnilistClientSecretStateMainDepsHandler({
|
||||
const {
|
||||
refreshAnilistClientSecretState,
|
||||
getCurrentAnilistMediaKey,
|
||||
resetAnilistMediaTracking,
|
||||
getAnilistMediaGuessRuntimeState,
|
||||
setAnilistMediaGuessRuntimeState,
|
||||
resetAnilistMediaGuessState,
|
||||
maybeProbeAnilistDuration,
|
||||
ensureAnilistMediaGuess,
|
||||
processNextAnilistRetryUpdate,
|
||||
maybeRunAnilistPostWatchUpdate,
|
||||
} = composeAnilistTrackingHandlers({
|
||||
refreshClientSecretMainDeps: {
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig),
|
||||
getCachedAccessToken: () => anilistCachedAccessToken,
|
||||
@@ -1519,24 +1534,11 @@ const buildRefreshAnilistClientSecretStateMainDepsHandler =
|
||||
openAnilistSetupWindow();
|
||||
},
|
||||
now: () => Date.now(),
|
||||
});
|
||||
const refreshAnilistClientSecretStateMainDeps =
|
||||
buildRefreshAnilistClientSecretStateMainDepsHandler();
|
||||
const refreshAnilistClientSecretState = createRefreshAnilistClientSecretStateHandler(
|
||||
refreshAnilistClientSecretStateMainDeps,
|
||||
);
|
||||
|
||||
const buildGetCurrentAnilistMediaKeyMainDepsHandler =
|
||||
createBuildGetCurrentAnilistMediaKeyMainDepsHandler({
|
||||
},
|
||||
getCurrentMediaKeyMainDeps: {
|
||||
getCurrentMediaPath: () => appState.currentMediaPath,
|
||||
});
|
||||
const getCurrentAnilistMediaKeyMainDeps = buildGetCurrentAnilistMediaKeyMainDepsHandler();
|
||||
const getCurrentAnilistMediaKey = createGetCurrentAnilistMediaKeyHandler(
|
||||
getCurrentAnilistMediaKeyMainDeps,
|
||||
);
|
||||
|
||||
const buildResetAnilistMediaTrackingMainDepsHandler =
|
||||
createBuildResetAnilistMediaTrackingMainDepsHandler({
|
||||
},
|
||||
resetMediaTrackingMainDeps: {
|
||||
setMediaKey: (value) => {
|
||||
anilistCurrentMediaKey = value;
|
||||
},
|
||||
@@ -1552,28 +1554,15 @@ const buildResetAnilistMediaTrackingMainDepsHandler =
|
||||
setLastDurationProbeAtMs: (value) => {
|
||||
anilistLastDurationProbeAtMs = value;
|
||||
},
|
||||
});
|
||||
const resetAnilistMediaTrackingMainDeps = buildResetAnilistMediaTrackingMainDepsHandler();
|
||||
const resetAnilistMediaTracking = createResetAnilistMediaTrackingHandler(
|
||||
resetAnilistMediaTrackingMainDeps,
|
||||
);
|
||||
|
||||
const buildGetAnilistMediaGuessRuntimeStateMainDepsHandler =
|
||||
createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler({
|
||||
},
|
||||
getMediaGuessRuntimeStateMainDeps: {
|
||||
getMediaKey: () => anilistCurrentMediaKey,
|
||||
getMediaDurationSec: () => anilistCurrentMediaDurationSec,
|
||||
getMediaGuess: () => anilistCurrentMediaGuess,
|
||||
getMediaGuessPromise: () => anilistCurrentMediaGuessPromise,
|
||||
getLastDurationProbeAtMs: () => anilistLastDurationProbeAtMs,
|
||||
});
|
||||
const getAnilistMediaGuessRuntimeStateMainDeps =
|
||||
buildGetAnilistMediaGuessRuntimeStateMainDepsHandler();
|
||||
const getAnilistMediaGuessRuntimeState = createGetAnilistMediaGuessRuntimeStateHandler(
|
||||
getAnilistMediaGuessRuntimeStateMainDeps,
|
||||
);
|
||||
|
||||
const buildSetAnilistMediaGuessRuntimeStateMainDepsHandler =
|
||||
createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler({
|
||||
},
|
||||
setMediaGuessRuntimeStateMainDeps: {
|
||||
setMediaKey: (value) => {
|
||||
anilistCurrentMediaKey = value;
|
||||
},
|
||||
@@ -1589,29 +1578,16 @@ const buildSetAnilistMediaGuessRuntimeStateMainDepsHandler =
|
||||
setLastDurationProbeAtMs: (value) => {
|
||||
anilistLastDurationProbeAtMs = value;
|
||||
},
|
||||
});
|
||||
const setAnilistMediaGuessRuntimeStateMainDeps =
|
||||
buildSetAnilistMediaGuessRuntimeStateMainDepsHandler();
|
||||
const setAnilistMediaGuessRuntimeState = createSetAnilistMediaGuessRuntimeStateHandler(
|
||||
setAnilistMediaGuessRuntimeStateMainDeps,
|
||||
);
|
||||
|
||||
const buildResetAnilistMediaGuessStateMainDepsHandler =
|
||||
createBuildResetAnilistMediaGuessStateMainDepsHandler({
|
||||
},
|
||||
resetMediaGuessStateMainDeps: {
|
||||
setMediaGuess: (value) => {
|
||||
anilistCurrentMediaGuess = value;
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
anilistCurrentMediaGuessPromise = value;
|
||||
},
|
||||
});
|
||||
const resetAnilistMediaGuessStateMainDeps = buildResetAnilistMediaGuessStateMainDepsHandler();
|
||||
const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler(
|
||||
resetAnilistMediaGuessStateMainDeps,
|
||||
);
|
||||
|
||||
const buildMaybeProbeAnilistDurationMainDepsHandler =
|
||||
createBuildMaybeProbeAnilistDurationMainDepsHandler({
|
||||
},
|
||||
maybeProbeDurationMainDeps: {
|
||||
getState: () => getAnilistMediaGuessRuntimeState(),
|
||||
setState: (state) => {
|
||||
setAnilistMediaGuessRuntimeState(state);
|
||||
@@ -1620,14 +1596,8 @@ const buildMaybeProbeAnilistDurationMainDepsHandler =
|
||||
now: () => Date.now(),
|
||||
requestMpvDuration: async () => appState.mpvClient?.requestProperty('duration'),
|
||||
logWarn: (message, error) => logger.warn(message, error),
|
||||
});
|
||||
const maybeProbeAnilistDurationMainDeps = buildMaybeProbeAnilistDurationMainDepsHandler();
|
||||
const maybeProbeAnilistDuration = createMaybeProbeAnilistDurationHandler(
|
||||
maybeProbeAnilistDurationMainDeps,
|
||||
);
|
||||
|
||||
const buildEnsureAnilistMediaGuessMainDepsHandler =
|
||||
createBuildEnsureAnilistMediaGuessMainDepsHandler({
|
||||
},
|
||||
ensureMediaGuessMainDeps: {
|
||||
getState: () => getAnilistMediaGuessRuntimeState(),
|
||||
setState: (state) => {
|
||||
setAnilistMediaGuessRuntimeState(state);
|
||||
@@ -1637,22 +1607,8 @@ const buildEnsureAnilistMediaGuessMainDepsHandler =
|
||||
getCurrentMediaPath: () => appState.currentMediaPath,
|
||||
getCurrentMediaTitle: () => appState.currentMediaTitle,
|
||||
guessAnilistMediaInfo: (mediaPath, mediaTitle) => guessAnilistMediaInfo(mediaPath, mediaTitle),
|
||||
});
|
||||
const ensureAnilistMediaGuessMainDeps = buildEnsureAnilistMediaGuessMainDepsHandler();
|
||||
const ensureAnilistMediaGuess = createEnsureAnilistMediaGuessHandler(
|
||||
ensureAnilistMediaGuessMainDeps,
|
||||
);
|
||||
|
||||
const rememberAnilistAttemptedUpdate = (key: string): void => {
|
||||
rememberAnilistAttemptedUpdateKey(
|
||||
anilistAttemptedUpdateKeys,
|
||||
key,
|
||||
ANILIST_MAX_ATTEMPTED_UPDATE_KEYS,
|
||||
);
|
||||
};
|
||||
|
||||
const buildProcessNextAnilistRetryUpdateMainDepsHandler =
|
||||
createBuildProcessNextAnilistRetryUpdateMainDepsHandler({
|
||||
},
|
||||
processNextRetryUpdateMainDeps: {
|
||||
nextReady: () => anilistUpdateQueue.nextReady(),
|
||||
refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(),
|
||||
setLastAttemptAt: (value) => {
|
||||
@@ -1675,14 +1631,8 @@ const buildProcessNextAnilistRetryUpdateMainDepsHandler =
|
||||
},
|
||||
logInfo: (message) => logger.info(message),
|
||||
now: () => Date.now(),
|
||||
});
|
||||
const processNextAnilistRetryUpdateMainDeps = buildProcessNextAnilistRetryUpdateMainDepsHandler();
|
||||
const processNextAnilistRetryUpdate = createProcessNextAnilistRetryUpdateHandler(
|
||||
processNextAnilistRetryUpdateMainDeps,
|
||||
);
|
||||
|
||||
const buildMaybeRunAnilistPostWatchUpdateMainDepsHandler =
|
||||
createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler({
|
||||
},
|
||||
maybeRunPostWatchUpdateMainDeps: {
|
||||
getInFlight: () => anilistUpdateInFlight,
|
||||
setInFlight: (value) => {
|
||||
anilistUpdateInFlight = value;
|
||||
@@ -1721,11 +1671,16 @@ const buildMaybeRunAnilistPostWatchUpdateMainDepsHandler =
|
||||
logWarn: (message) => logger.warn(message),
|
||||
minWatchSeconds: ANILIST_UPDATE_MIN_WATCH_SECONDS,
|
||||
minWatchRatio: ANILIST_UPDATE_MIN_WATCH_RATIO,
|
||||
});
|
||||
const maybeRunAnilistPostWatchUpdateMainDeps = buildMaybeRunAnilistPostWatchUpdateMainDepsHandler();
|
||||
const maybeRunAnilistPostWatchUpdate = createMaybeRunAnilistPostWatchUpdateHandler(
|
||||
maybeRunAnilistPostWatchUpdateMainDeps,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const rememberAnilistAttemptedUpdate = (key: string): void => {
|
||||
rememberAnilistAttemptedUpdateKey(
|
||||
anilistAttemptedUpdateKeys,
|
||||
key,
|
||||
ANILIST_MAX_ATTEMPTED_UPDATE_KEYS,
|
||||
);
|
||||
};
|
||||
|
||||
const buildLoadSubtitlePositionMainDepsHandler = createBuildLoadSubtitlePositionMainDepsHandler({
|
||||
loadSubtitlePositionCore: () =>
|
||||
@@ -2043,8 +1998,16 @@ function handleInitialArgs(): void {
|
||||
handleInitialArgsRuntimeHandler();
|
||||
}
|
||||
|
||||
const buildBindMpvMainEventHandlersMainDepsHandler =
|
||||
createBuildBindMpvMainEventHandlersMainDepsHandler({
|
||||
const {
|
||||
bindMpvClientEventHandlers,
|
||||
createMpvClientRuntimeService: createMpvClientRuntimeServiceHandler,
|
||||
updateMpvSubtitleRenderMetrics: updateMpvSubtitleRenderMetricsHandler,
|
||||
tokenizeSubtitle,
|
||||
createMecabTokenizerAndCheck,
|
||||
prewarmSubtitleDictionaries,
|
||||
startBackgroundWarmups,
|
||||
} = composeMpvRuntimeHandlers<ReturnType<typeof createTokenizerDepsRuntime>, SubtitleData>({
|
||||
bindMpvMainEventHandlersMainDeps: {
|
||||
appState,
|
||||
getQuitOnDisconnectArmed: () => jellyfinPlayQuitOnDisconnectArmed,
|
||||
scheduleQuitCheck: (callback) => {
|
||||
@@ -2090,15 +2053,9 @@ const buildBindMpvMainEventHandlersMainDepsHandler =
|
||||
updateSubtitleRenderMetrics: (patch) => {
|
||||
updateMpvSubtitleRenderMetrics(patch as Partial<MpvSubtitleRenderMetrics>);
|
||||
},
|
||||
});
|
||||
const bindMpvMainEventHandlersMainDeps = buildBindMpvMainEventHandlersMainDepsHandler();
|
||||
const bindMpvClientEventHandlers = createBindMpvMainEventHandlersHandler(
|
||||
bindMpvMainEventHandlersMainDeps,
|
||||
);
|
||||
|
||||
const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
|
||||
createBuildMpvClientRuntimeServiceFactoryDepsHandler({
|
||||
createClient: MpvIpcClient,
|
||||
},
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||
createClient: MpvIpcClient as unknown as new (socketPath: string, options: unknown) => unknown,
|
||||
getSocketPath: () => appState.mpvSocketPath,
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
isAutoStartOverlayEnabled: () => appState.autoStartOverlay,
|
||||
@@ -2110,17 +2067,8 @@ const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => {
|
||||
appState.reconnectTimer = timer;
|
||||
},
|
||||
bindEventHandlers: (client) => bindMpvClientEventHandlers(client),
|
||||
});
|
||||
|
||||
function createMpvClientRuntimeService(): MpvIpcClient {
|
||||
return createMpvClientRuntimeServiceFactory(
|
||||
buildMpvClientRuntimeServiceFactoryMainDepsHandler(),
|
||||
)();
|
||||
}
|
||||
|
||||
const buildUpdateMpvSubtitleRenderMetricsMainDepsHandler =
|
||||
createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler({
|
||||
},
|
||||
updateMpvSubtitleRenderMetricsMainDeps: {
|
||||
getCurrentMetrics: () => appState.mpvSubtitleRenderMetrics,
|
||||
setCurrentMetrics: (metrics) => {
|
||||
appState.mpvSubtitleRenderMetrics = metrics;
|
||||
@@ -2129,109 +2077,83 @@ const buildUpdateMpvSubtitleRenderMetricsMainDepsHandler =
|
||||
broadcastMetrics: (metrics) => {
|
||||
broadcastToOverlayWindows('mpv-subtitle-render-metrics:set', metrics);
|
||||
},
|
||||
});
|
||||
const updateMpvSubtitleRenderMetricsMainDeps = buildUpdateMpvSubtitleRenderMetricsMainDepsHandler();
|
||||
const updateMpvSubtitleRenderMetricsRuntime = createUpdateMpvSubtitleRenderMetricsHandler(
|
||||
updateMpvSubtitleRenderMetricsMainDeps,
|
||||
);
|
||||
|
||||
function updateMpvSubtitleRenderMetrics(patch: Partial<MpvSubtitleRenderMetrics>): void {
|
||||
updateMpvSubtitleRenderMetricsRuntime(patch);
|
||||
}
|
||||
|
||||
const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler({
|
||||
getYomitanExt: () => appState.yomitanExt,
|
||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||
setYomitanParserWindow: (window) => {
|
||||
appState.yomitanParserWindow = window as BrowserWindow | null;
|
||||
},
|
||||
getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
|
||||
setYomitanParserReadyPromise: (promise) => {
|
||||
appState.yomitanParserReadyPromise = promise;
|
||||
tokenizer: {
|
||||
buildTokenizerDepsMainDeps: {
|
||||
getYomitanExt: () => appState.yomitanExt,
|
||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||
setYomitanParserWindow: (window) => {
|
||||
appState.yomitanParserWindow = window as BrowserWindow | null;
|
||||
},
|
||||
getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
|
||||
setYomitanParserReadyPromise: (promise) => {
|
||||
appState.yomitanParserReadyPromise = promise;
|
||||
},
|
||||
getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
|
||||
setYomitanParserInitPromise: (promise) => {
|
||||
appState.yomitanParserInitPromise = promise;
|
||||
},
|
||||
isKnownWord: (text) => Boolean(appState.ankiIntegration?.isKnownWord(text)),
|
||||
recordLookup: (hit) => {
|
||||
appState.immersionTracker?.recordLookup(hit);
|
||||
},
|
||||
getKnownWordMatchMode: () =>
|
||||
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
||||
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
||||
getMinSentenceWordsForNPlusOne: () =>
|
||||
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
||||
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
||||
getJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
||||
getFrequencyDictionaryEnabled: () =>
|
||||
getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||
getFrequencyRank: (text) => appState.frequencyRankLookup(text),
|
||||
getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||
},
|
||||
createTokenizerRuntimeDeps: (deps) =>
|
||||
createTokenizerDepsRuntime(deps as Parameters<typeof createTokenizerDepsRuntime>[0]),
|
||||
tokenizeSubtitle: (text, deps) => tokenizeSubtitleCore(text, deps),
|
||||
createMecabTokenizerAndCheckMainDeps: {
|
||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||
setMecabTokenizer: (tokenizer) => {
|
||||
appState.mecabTokenizer = tokenizer as MecabTokenizer | null;
|
||||
},
|
||||
createMecabTokenizer: () => new MecabTokenizer(),
|
||||
checkAvailability: async (tokenizer) => (tokenizer as MecabTokenizer).checkAvailability(),
|
||||
},
|
||||
prewarmSubtitleDictionariesMainDeps: {
|
||||
ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
||||
ensureFrequencyDictionaryLookup: () =>
|
||||
frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
||||
},
|
||||
},
|
||||
getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
|
||||
setYomitanParserInitPromise: (promise) => {
|
||||
appState.yomitanParserInitPromise = promise;
|
||||
warmups: {
|
||||
launchBackgroundWarmupTaskMainDeps: {
|
||||
now: () => Date.now(),
|
||||
logDebug: (message) => logger.debug(message),
|
||||
logWarn: (message) => logger.warn(message),
|
||||
},
|
||||
startBackgroundWarmupsMainDeps: {
|
||||
getStarted: () => backgroundWarmupsStarted,
|
||||
setStarted: (started) => {
|
||||
backgroundWarmupsStarted = started;
|
||||
},
|
||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
|
||||
shouldAutoConnectJellyfinRemote: () => getResolvedConfig().jellyfin.remoteControlAutoConnect,
|
||||
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
||||
},
|
||||
},
|
||||
isKnownWord: (text) => Boolean(appState.ankiIntegration?.isKnownWord(text)),
|
||||
recordLookup: (hit) => {
|
||||
appState.immersionTracker?.recordLookup(hit);
|
||||
},
|
||||
getKnownWordMatchMode: () =>
|
||||
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
||||
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
||||
getMinSentenceWordsForNPlusOne: () => getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
||||
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
||||
getJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
||||
getFrequencyDictionaryEnabled: () =>
|
||||
getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||
getFrequencyRank: (text) => appState.frequencyRankLookup(text),
|
||||
getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||
});
|
||||
|
||||
const buildCreateMecabTokenizerAndCheckMainDepsHandler =
|
||||
createCreateMecabTokenizerAndCheckMainHandler({
|
||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||
setMecabTokenizer: (tokenizer) => {
|
||||
appState.mecabTokenizer = tokenizer;
|
||||
},
|
||||
createMecabTokenizer: () => new MecabTokenizer(),
|
||||
checkAvailability: async (tokenizer) => tokenizer.checkAvailability(),
|
||||
});
|
||||
const createMecabTokenizerAndCheckHandler = buildCreateMecabTokenizerAndCheckMainDepsHandler;
|
||||
|
||||
const buildPrewarmSubtitleDictionariesMainDepsHandler =
|
||||
createPrewarmSubtitleDictionariesMainHandler({
|
||||
ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
||||
ensureFrequencyDictionaryLookup: () =>
|
||||
frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
||||
});
|
||||
const prewarmSubtitleDictionariesHandler = buildPrewarmSubtitleDictionariesMainDepsHandler;
|
||||
|
||||
async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
|
||||
await jlptDictionaryRuntime.ensureJlptDictionaryLookup();
|
||||
await frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup();
|
||||
return tokenizeSubtitleCore(text, createTokenizerDepsRuntime(buildTokenizerDepsHandler()));
|
||||
function createMpvClientRuntimeService(): MpvIpcClient {
|
||||
return createMpvClientRuntimeServiceHandler() as MpvIpcClient;
|
||||
}
|
||||
|
||||
async function createMecabTokenizerAndCheck(): Promise<void> {
|
||||
await createMecabTokenizerAndCheckHandler();
|
||||
function updateMpvSubtitleRenderMetrics(patch: Partial<MpvSubtitleRenderMetrics>): void {
|
||||
updateMpvSubtitleRenderMetricsHandler(patch);
|
||||
}
|
||||
|
||||
async function prewarmSubtitleDictionaries(): Promise<void> {
|
||||
await prewarmSubtitleDictionariesHandler();
|
||||
}
|
||||
|
||||
const buildLaunchBackgroundWarmupTaskMainDepsHandler =
|
||||
createBuildLaunchBackgroundWarmupTaskMainDepsHandler({
|
||||
now: () => Date.now(),
|
||||
logDebug: (message) => logger.debug(message),
|
||||
logWarn: (message) => logger.warn(message),
|
||||
});
|
||||
const launchBackgroundWarmupTaskMainDeps = buildLaunchBackgroundWarmupTaskMainDepsHandler();
|
||||
const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskHandler(
|
||||
launchBackgroundWarmupTaskMainDeps,
|
||||
);
|
||||
|
||||
const buildStartBackgroundWarmupsMainDepsHandler = createBuildStartBackgroundWarmupsMainDepsHandler(
|
||||
{
|
||||
getStarted: () => backgroundWarmupsStarted,
|
||||
setStarted: (started) => {
|
||||
backgroundWarmupsStarted = started;
|
||||
},
|
||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||
launchTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
|
||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||
shouldAutoConnectJellyfinRemote: () => getResolvedConfig().jellyfin.remoteControlAutoConnect,
|
||||
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
||||
},
|
||||
);
|
||||
const startBackgroundWarmupsMainDeps = buildStartBackgroundWarmupsMainDepsHandler();
|
||||
const startBackgroundWarmups = createStartBackgroundWarmupsHandler(startBackgroundWarmupsMainDeps);
|
||||
|
||||
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
|
||||
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
|
||||
setOverlayWindowBounds: (layer, geometry) =>
|
||||
|
||||
237
src/main/runtime/composers/anilist-tracking-composer.test.ts
Normal file
237
src/main/runtime/composers/anilist-tracking-composer.test.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { AnilistMediaGuess } from '../../../core/services/anilist/anilist-updater';
|
||||
import { composeAnilistTrackingHandlers } from './anilist-tracking-composer';
|
||||
|
||||
test('composeAnilistTrackingHandlers returns callable handlers and forwards calls to deps', async () => {
|
||||
const refreshSavedTokens: string[] = [];
|
||||
let refreshCachedToken: string | null = null;
|
||||
|
||||
let mediaKeyState: string | null = 'media-key';
|
||||
let mediaDurationSecState: number | null = null;
|
||||
let mediaGuessState: AnilistMediaGuess | null = null;
|
||||
let mediaGuessPromiseState: Promise<AnilistMediaGuess | null> | null = null;
|
||||
let lastDurationProbeAtMsState = 0;
|
||||
let requestMpvDurationCalls = 0;
|
||||
let guessAnilistMediaInfoCalls = 0;
|
||||
|
||||
let retryUpdateCalls = 0;
|
||||
let maybeRunUpdateCalls = 0;
|
||||
|
||||
const composed = composeAnilistTrackingHandlers({
|
||||
refreshClientSecretMainDeps: {
|
||||
getResolvedConfig: () => ({ anilist: { accessToken: 'refresh-token' } }),
|
||||
isAnilistTrackingEnabled: () => true,
|
||||
getCachedAccessToken: () => refreshCachedToken,
|
||||
setCachedAccessToken: (token) => {
|
||||
refreshCachedToken = token;
|
||||
},
|
||||
saveStoredToken: (token) => {
|
||||
refreshSavedTokens.push(token);
|
||||
},
|
||||
loadStoredToken: () => null,
|
||||
setClientSecretState: () => {},
|
||||
getAnilistSetupPageOpened: () => false,
|
||||
setAnilistSetupPageOpened: () => {},
|
||||
openAnilistSetupWindow: () => {},
|
||||
now: () => 100,
|
||||
},
|
||||
getCurrentMediaKeyMainDeps: {
|
||||
getCurrentMediaPath: () => ' media-key ',
|
||||
},
|
||||
resetMediaTrackingMainDeps: {
|
||||
setMediaKey: (value) => {
|
||||
mediaKeyState = value;
|
||||
},
|
||||
setMediaDurationSec: (value) => {
|
||||
mediaDurationSecState = value;
|
||||
},
|
||||
setMediaGuess: (value) => {
|
||||
mediaGuessState = value;
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
mediaGuessPromiseState = value;
|
||||
},
|
||||
setLastDurationProbeAtMs: (value) => {
|
||||
lastDurationProbeAtMsState = value;
|
||||
},
|
||||
},
|
||||
getMediaGuessRuntimeStateMainDeps: {
|
||||
getMediaKey: () => mediaKeyState,
|
||||
getMediaDurationSec: () => mediaDurationSecState,
|
||||
getMediaGuess: () => mediaGuessState,
|
||||
getMediaGuessPromise: () => mediaGuessPromiseState,
|
||||
getLastDurationProbeAtMs: () => lastDurationProbeAtMsState,
|
||||
},
|
||||
setMediaGuessRuntimeStateMainDeps: {
|
||||
setMediaKey: (value) => {
|
||||
mediaKeyState = value;
|
||||
},
|
||||
setMediaDurationSec: (value) => {
|
||||
mediaDurationSecState = value;
|
||||
},
|
||||
setMediaGuess: (value) => {
|
||||
mediaGuessState = value;
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
mediaGuessPromiseState = value;
|
||||
},
|
||||
setLastDurationProbeAtMs: (value) => {
|
||||
lastDurationProbeAtMsState = value;
|
||||
},
|
||||
},
|
||||
resetMediaGuessStateMainDeps: {
|
||||
setMediaGuess: (value) => {
|
||||
mediaGuessState = value;
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
mediaGuessPromiseState = value;
|
||||
},
|
||||
},
|
||||
maybeProbeDurationMainDeps: {
|
||||
getState: () => ({
|
||||
mediaKey: mediaKeyState,
|
||||
mediaDurationSec: mediaDurationSecState,
|
||||
mediaGuess: mediaGuessState,
|
||||
mediaGuessPromise: mediaGuessPromiseState,
|
||||
lastDurationProbeAtMs: lastDurationProbeAtMsState,
|
||||
}),
|
||||
setState: (state) => {
|
||||
mediaKeyState = state.mediaKey;
|
||||
mediaDurationSecState = state.mediaDurationSec;
|
||||
mediaGuessState = state.mediaGuess;
|
||||
mediaGuessPromiseState = state.mediaGuessPromise;
|
||||
lastDurationProbeAtMsState = state.lastDurationProbeAtMs;
|
||||
},
|
||||
durationRetryIntervalMs: 0,
|
||||
now: () => 1000,
|
||||
requestMpvDuration: async () => {
|
||||
requestMpvDurationCalls += 1;
|
||||
return 120;
|
||||
},
|
||||
logWarn: () => {},
|
||||
},
|
||||
ensureMediaGuessMainDeps: {
|
||||
getState: () => ({
|
||||
mediaKey: mediaKeyState,
|
||||
mediaDurationSec: mediaDurationSecState,
|
||||
mediaGuess: mediaGuessState,
|
||||
mediaGuessPromise: mediaGuessPromiseState,
|
||||
lastDurationProbeAtMs: lastDurationProbeAtMsState,
|
||||
}),
|
||||
setState: (state) => {
|
||||
mediaKeyState = state.mediaKey;
|
||||
mediaDurationSecState = state.mediaDurationSec;
|
||||
mediaGuessState = state.mediaGuess;
|
||||
mediaGuessPromiseState = state.mediaGuessPromise;
|
||||
lastDurationProbeAtMsState = state.lastDurationProbeAtMs;
|
||||
},
|
||||
resolveMediaPathForJimaku: (value) => value,
|
||||
getCurrentMediaPath: () => '/tmp/media.mkv',
|
||||
getCurrentMediaTitle: () => 'Episode title',
|
||||
guessAnilistMediaInfo: async () => {
|
||||
guessAnilistMediaInfoCalls += 1;
|
||||
return { title: 'Episode title', episode: 7, source: 'guessit' };
|
||||
},
|
||||
},
|
||||
processNextRetryUpdateMainDeps: {
|
||||
nextReady: () => ({ key: 'retry-key', title: 'Retry title', episode: 1 }),
|
||||
refreshRetryQueueState: () => {},
|
||||
setLastAttemptAt: () => {},
|
||||
setLastError: () => {},
|
||||
refreshAnilistClientSecretState: async () => 'retry-token',
|
||||
updateAnilistPostWatchProgress: async () => {
|
||||
retryUpdateCalls += 1;
|
||||
return { status: 'updated', message: 'ok' };
|
||||
},
|
||||
markSuccess: () => {},
|
||||
rememberAttemptedUpdateKey: () => {},
|
||||
markFailure: () => {},
|
||||
logInfo: () => {},
|
||||
now: () => 1,
|
||||
},
|
||||
maybeRunPostWatchUpdateMainDeps: {
|
||||
getInFlight: () => false,
|
||||
setInFlight: () => {},
|
||||
getResolvedConfig: () => ({ tracking: true }),
|
||||
isAnilistTrackingEnabled: () => true,
|
||||
getCurrentMediaKey: () => 'media-key',
|
||||
hasMpvClient: () => true,
|
||||
getTrackedMediaKey: () => 'media-key',
|
||||
resetTrackedMedia: () => {},
|
||||
getWatchedSeconds: () => 500,
|
||||
maybeProbeAnilistDuration: async () => 600,
|
||||
ensureAnilistMediaGuess: async () => ({
|
||||
title: 'Episode title',
|
||||
episode: 2,
|
||||
source: 'guessit',
|
||||
}),
|
||||
hasAttemptedUpdateKey: () => false,
|
||||
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
|
||||
refreshAnilistClientSecretState: async () => 'run-token',
|
||||
enqueueRetry: () => {},
|
||||
markRetryFailure: () => {},
|
||||
markRetrySuccess: () => {},
|
||||
refreshRetryQueueState: () => {},
|
||||
updateAnilistPostWatchProgress: async () => {
|
||||
maybeRunUpdateCalls += 1;
|
||||
return { status: 'updated', message: 'updated from maybeRun' };
|
||||
},
|
||||
rememberAttemptedUpdateKey: () => {},
|
||||
showMpvOsd: () => {},
|
||||
logInfo: () => {},
|
||||
logWarn: () => {},
|
||||
minWatchSeconds: 10,
|
||||
minWatchRatio: 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.refreshAnilistClientSecretState, 'function');
|
||||
assert.equal(typeof composed.getCurrentAnilistMediaKey, 'function');
|
||||
assert.equal(typeof composed.resetAnilistMediaTracking, 'function');
|
||||
assert.equal(typeof composed.getAnilistMediaGuessRuntimeState, 'function');
|
||||
assert.equal(typeof composed.setAnilistMediaGuessRuntimeState, 'function');
|
||||
assert.equal(typeof composed.resetAnilistMediaGuessState, 'function');
|
||||
assert.equal(typeof composed.maybeProbeAnilistDuration, 'function');
|
||||
assert.equal(typeof composed.ensureAnilistMediaGuess, 'function');
|
||||
assert.equal(typeof composed.processNextAnilistRetryUpdate, 'function');
|
||||
assert.equal(typeof composed.maybeRunAnilistPostWatchUpdate, 'function');
|
||||
|
||||
const refreshed = await composed.refreshAnilistClientSecretState({ force: true });
|
||||
assert.equal(refreshed, 'refresh-token');
|
||||
assert.deepEqual(refreshSavedTokens, ['refresh-token']);
|
||||
|
||||
assert.equal(composed.getCurrentAnilistMediaKey(), 'media-key');
|
||||
composed.resetAnilistMediaTracking('next-key');
|
||||
assert.equal(mediaKeyState, 'next-key');
|
||||
assert.equal(mediaDurationSecState, null);
|
||||
|
||||
composed.setAnilistMediaGuessRuntimeState({
|
||||
mediaKey: 'media-key',
|
||||
mediaDurationSec: 90,
|
||||
mediaGuess: { title: 'Known', episode: 3, source: 'fallback' },
|
||||
mediaGuessPromise: null,
|
||||
lastDurationProbeAtMs: 11,
|
||||
});
|
||||
assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaDurationSec, 90);
|
||||
|
||||
composed.resetAnilistMediaGuessState();
|
||||
assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaGuess, null);
|
||||
|
||||
mediaKeyState = 'media-key';
|
||||
mediaDurationSecState = null;
|
||||
const probedDuration = await composed.maybeProbeAnilistDuration('media-key');
|
||||
assert.equal(probedDuration, 120);
|
||||
assert.equal(requestMpvDurationCalls, 1);
|
||||
|
||||
mediaGuessState = null;
|
||||
await composed.ensureAnilistMediaGuess('media-key');
|
||||
assert.equal(guessAnilistMediaInfoCalls, 1);
|
||||
|
||||
const retryResult = await composed.processNextAnilistRetryUpdate();
|
||||
assert.deepEqual(retryResult, { ok: true, message: 'ok' });
|
||||
assert.equal(retryUpdateCalls, 1);
|
||||
|
||||
await composed.maybeRunAnilistPostWatchUpdate();
|
||||
assert.equal(maybeRunUpdateCalls, 1);
|
||||
});
|
||||
128
src/main/runtime/composers/anilist-tracking-composer.ts
Normal file
128
src/main/runtime/composers/anilist-tracking-composer.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
createBuildEnsureAnilistMediaGuessMainDepsHandler,
|
||||
createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler,
|
||||
createBuildGetCurrentAnilistMediaKeyMainDepsHandler,
|
||||
createBuildMaybeProbeAnilistDurationMainDepsHandler,
|
||||
createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler,
|
||||
createBuildProcessNextAnilistRetryUpdateMainDepsHandler,
|
||||
createBuildRefreshAnilistClientSecretStateMainDepsHandler,
|
||||
createBuildResetAnilistMediaGuessStateMainDepsHandler,
|
||||
createBuildResetAnilistMediaTrackingMainDepsHandler,
|
||||
createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler,
|
||||
createEnsureAnilistMediaGuessHandler,
|
||||
createGetAnilistMediaGuessRuntimeStateHandler,
|
||||
createGetCurrentAnilistMediaKeyHandler,
|
||||
createMaybeProbeAnilistDurationHandler,
|
||||
createMaybeRunAnilistPostWatchUpdateHandler,
|
||||
createProcessNextAnilistRetryUpdateHandler,
|
||||
createRefreshAnilistClientSecretStateHandler,
|
||||
createResetAnilistMediaGuessStateHandler,
|
||||
createResetAnilistMediaTrackingHandler,
|
||||
createSetAnilistMediaGuessRuntimeStateHandler,
|
||||
} from '../domains/anilist';
|
||||
|
||||
export type AnilistTrackingComposerOptions = {
|
||||
refreshClientSecretMainDeps: Parameters<
|
||||
typeof createBuildRefreshAnilistClientSecretStateMainDepsHandler
|
||||
>[0];
|
||||
getCurrentMediaKeyMainDeps: Parameters<
|
||||
typeof createBuildGetCurrentAnilistMediaKeyMainDepsHandler
|
||||
>[0];
|
||||
resetMediaTrackingMainDeps: Parameters<
|
||||
typeof createBuildResetAnilistMediaTrackingMainDepsHandler
|
||||
>[0];
|
||||
getMediaGuessRuntimeStateMainDeps: Parameters<
|
||||
typeof createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler
|
||||
>[0];
|
||||
setMediaGuessRuntimeStateMainDeps: Parameters<
|
||||
typeof createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler
|
||||
>[0];
|
||||
resetMediaGuessStateMainDeps: Parameters<
|
||||
typeof createBuildResetAnilistMediaGuessStateMainDepsHandler
|
||||
>[0];
|
||||
maybeProbeDurationMainDeps: Parameters<
|
||||
typeof createBuildMaybeProbeAnilistDurationMainDepsHandler
|
||||
>[0];
|
||||
ensureMediaGuessMainDeps: Parameters<typeof createBuildEnsureAnilistMediaGuessMainDepsHandler>[0];
|
||||
processNextRetryUpdateMainDeps: Parameters<
|
||||
typeof createBuildProcessNextAnilistRetryUpdateMainDepsHandler
|
||||
>[0];
|
||||
maybeRunPostWatchUpdateMainDeps: Parameters<
|
||||
typeof createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler
|
||||
>[0];
|
||||
};
|
||||
|
||||
export type AnilistTrackingComposerResult = {
|
||||
refreshAnilistClientSecretState: ReturnType<typeof createRefreshAnilistClientSecretStateHandler>;
|
||||
getCurrentAnilistMediaKey: ReturnType<typeof createGetCurrentAnilistMediaKeyHandler>;
|
||||
resetAnilistMediaTracking: ReturnType<typeof createResetAnilistMediaTrackingHandler>;
|
||||
getAnilistMediaGuessRuntimeState: ReturnType<
|
||||
typeof createGetAnilistMediaGuessRuntimeStateHandler
|
||||
>;
|
||||
setAnilistMediaGuessRuntimeState: ReturnType<
|
||||
typeof createSetAnilistMediaGuessRuntimeStateHandler
|
||||
>;
|
||||
resetAnilistMediaGuessState: ReturnType<typeof createResetAnilistMediaGuessStateHandler>;
|
||||
maybeProbeAnilistDuration: ReturnType<typeof createMaybeProbeAnilistDurationHandler>;
|
||||
ensureAnilistMediaGuess: ReturnType<typeof createEnsureAnilistMediaGuessHandler>;
|
||||
processNextAnilistRetryUpdate: ReturnType<typeof createProcessNextAnilistRetryUpdateHandler>;
|
||||
maybeRunAnilistPostWatchUpdate: ReturnType<typeof createMaybeRunAnilistPostWatchUpdateHandler>;
|
||||
};
|
||||
|
||||
export function composeAnilistTrackingHandlers(
|
||||
options: AnilistTrackingComposerOptions,
|
||||
): AnilistTrackingComposerResult {
|
||||
const refreshAnilistClientSecretState = createRefreshAnilistClientSecretStateHandler(
|
||||
createBuildRefreshAnilistClientSecretStateMainDepsHandler(
|
||||
options.refreshClientSecretMainDeps,
|
||||
)(),
|
||||
);
|
||||
const getCurrentAnilistMediaKey = createGetCurrentAnilistMediaKeyHandler(
|
||||
createBuildGetCurrentAnilistMediaKeyMainDepsHandler(options.getCurrentMediaKeyMainDeps)(),
|
||||
);
|
||||
const resetAnilistMediaTracking = createResetAnilistMediaTrackingHandler(
|
||||
createBuildResetAnilistMediaTrackingMainDepsHandler(options.resetMediaTrackingMainDeps)(),
|
||||
);
|
||||
const getAnilistMediaGuessRuntimeState = createGetAnilistMediaGuessRuntimeStateHandler(
|
||||
createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler(
|
||||
options.getMediaGuessRuntimeStateMainDeps,
|
||||
)(),
|
||||
);
|
||||
const setAnilistMediaGuessRuntimeState = createSetAnilistMediaGuessRuntimeStateHandler(
|
||||
createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler(
|
||||
options.setMediaGuessRuntimeStateMainDeps,
|
||||
)(),
|
||||
);
|
||||
const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler(
|
||||
createBuildResetAnilistMediaGuessStateMainDepsHandler(options.resetMediaGuessStateMainDeps)(),
|
||||
);
|
||||
const maybeProbeAnilistDuration = createMaybeProbeAnilistDurationHandler(
|
||||
createBuildMaybeProbeAnilistDurationMainDepsHandler(options.maybeProbeDurationMainDeps)(),
|
||||
);
|
||||
const ensureAnilistMediaGuess = createEnsureAnilistMediaGuessHandler(
|
||||
createBuildEnsureAnilistMediaGuessMainDepsHandler(options.ensureMediaGuessMainDeps)(),
|
||||
);
|
||||
const processNextAnilistRetryUpdate = createProcessNextAnilistRetryUpdateHandler(
|
||||
createBuildProcessNextAnilistRetryUpdateMainDepsHandler(
|
||||
options.processNextRetryUpdateMainDeps,
|
||||
)(),
|
||||
);
|
||||
const maybeRunAnilistPostWatchUpdate = createMaybeRunAnilistPostWatchUpdateHandler(
|
||||
createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler(
|
||||
options.maybeRunPostWatchUpdateMainDeps,
|
||||
)(),
|
||||
);
|
||||
|
||||
return {
|
||||
refreshAnilistClientSecretState,
|
||||
getCurrentAnilistMediaKey,
|
||||
resetAnilistMediaTracking,
|
||||
getAnilistMediaGuessRuntimeState,
|
||||
setAnilistMediaGuessRuntimeState,
|
||||
resetAnilistMediaGuessState,
|
||||
maybeProbeAnilistDuration,
|
||||
ensureAnilistMediaGuess,
|
||||
processNextAnilistRetryUpdate,
|
||||
maybeRunAnilistPostWatchUpdate,
|
||||
};
|
||||
}
|
||||
8
src/main/runtime/composers/index.ts
Normal file
8
src/main/runtime/composers/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './anilist-setup-composer';
|
||||
export * from './anilist-tracking-composer';
|
||||
export * from './app-ready-composer';
|
||||
export * from './ipc-runtime-composer';
|
||||
export * from './jellyfin-remote-composer';
|
||||
export * from './mpv-runtime-composer';
|
||||
export * from './shortcuts-runtime-composer';
|
||||
export * from './startup-lifecycle-composer';
|
||||
216
src/main/runtime/composers/mpv-runtime-composer.test.ts
Normal file
216
src/main/runtime/composers/mpv-runtime-composer.test.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { MpvSubtitleRenderMetrics } from '../../../types';
|
||||
import { composeMpvRuntimeHandlers } from './mpv-runtime-composer';
|
||||
|
||||
const BASE_METRICS: MpvSubtitleRenderMetrics = {
|
||||
subPos: 100,
|
||||
subFontSize: 36,
|
||||
subScale: 1,
|
||||
subMarginY: 0,
|
||||
subMarginX: 0,
|
||||
subFont: '',
|
||||
subSpacing: 0,
|
||||
subBold: false,
|
||||
subItalic: false,
|
||||
subBorderSize: 0,
|
||||
subShadowOffset: 0,
|
||||
subAssOverride: 'yes',
|
||||
subScaleByWindow: true,
|
||||
subUseMargins: true,
|
||||
osdHeight: 0,
|
||||
osdDimensions: null,
|
||||
};
|
||||
|
||||
test('composeMpvRuntimeHandlers returns callable handlers and forwards to injected deps', async () => {
|
||||
const calls: string[] = [];
|
||||
let started = false;
|
||||
let metrics = BASE_METRICS;
|
||||
|
||||
class FakeMpvClient {
|
||||
connected = false;
|
||||
|
||||
constructor(
|
||||
public socketPath: string,
|
||||
public options: unknown,
|
||||
) {
|
||||
const autoStartOverlay = (options as { autoStartOverlay: boolean }).autoStartOverlay;
|
||||
calls.push(`create-client:${socketPath}`);
|
||||
calls.push(`auto-start:${String(autoStartOverlay)}`);
|
||||
}
|
||||
|
||||
on(): void {}
|
||||
|
||||
connect(): void {
|
||||
this.connected = true;
|
||||
calls.push('client-connect');
|
||||
}
|
||||
}
|
||||
|
||||
const composed = composeMpvRuntimeHandlers<
|
||||
{ isKnownWord: (text: string) => boolean },
|
||||
{ text: string }
|
||||
>({
|
||||
bindMpvMainEventHandlersMainDeps: {
|
||||
appState: {
|
||||
initialArgs: null,
|
||||
overlayRuntimeInitialized: true,
|
||||
mpvClient: null,
|
||||
immersionTracker: null,
|
||||
subtitleTimingTracker: null,
|
||||
currentSubText: '',
|
||||
currentSubAssText: '',
|
||||
previousSecondarySubVisibility: null,
|
||||
},
|
||||
getQuitOnDisconnectArmed: () => false,
|
||||
scheduleQuitCheck: () => {},
|
||||
quitApp: () => {},
|
||||
reportJellyfinRemoteStopped: () => {},
|
||||
maybeRunAnilistPostWatchUpdate: async () => {},
|
||||
logSubtitleTimingError: () => {},
|
||||
broadcastToOverlayWindows: () => {},
|
||||
onSubtitleChange: () => {},
|
||||
updateCurrentMediaPath: () => {},
|
||||
getCurrentAnilistMediaKey: () => null,
|
||||
resetAnilistMediaTracking: () => {},
|
||||
maybeProbeAnilistDuration: () => {},
|
||||
ensureAnilistMediaGuess: () => {},
|
||||
syncImmersionMediaState: () => {},
|
||||
updateCurrentMediaTitle: () => {},
|
||||
resetAnilistMediaGuessState: () => {},
|
||||
reportJellyfinRemoteProgress: () => {},
|
||||
updateSubtitleRenderMetrics: () => {},
|
||||
},
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||
createClient: FakeMpvClient,
|
||||
getSocketPath: () => '/tmp/mpv.sock',
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
isAutoStartOverlayEnabled: () => true,
|
||||
setOverlayVisible: () => {},
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||
isVisibleOverlayVisible: () => false,
|
||||
getReconnectTimer: () => null,
|
||||
setReconnectTimer: () => {},
|
||||
},
|
||||
updateMpvSubtitleRenderMetricsMainDeps: {
|
||||
getCurrentMetrics: () => metrics,
|
||||
setCurrentMetrics: (next) => {
|
||||
metrics = next;
|
||||
calls.push('set-metrics');
|
||||
},
|
||||
applyPatch: (current, patch) => {
|
||||
calls.push('apply-metrics-patch');
|
||||
return { next: { ...current, ...patch }, changed: true };
|
||||
},
|
||||
broadcastMetrics: () => {
|
||||
calls.push('broadcast-metrics');
|
||||
},
|
||||
},
|
||||
tokenizer: {
|
||||
buildTokenizerDepsMainDeps: {
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => {},
|
||||
getYomitanParserReadyPromise: () => null,
|
||||
setYomitanParserReadyPromise: () => {},
|
||||
getYomitanParserInitPromise: () => null,
|
||||
setYomitanParserInitPromise: () => {},
|
||||
isKnownWord: (text) => text === 'known',
|
||||
recordLookup: () => {},
|
||||
getKnownWordMatchMode: () => 'exact',
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => null,
|
||||
getJlptEnabled: () => true,
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyRank: () => null,
|
||||
getYomitanGroupDebugEnabled: () => false,
|
||||
getMecabTokenizer: () => null,
|
||||
},
|
||||
createTokenizerRuntimeDeps: (deps) => {
|
||||
calls.push('create-tokenizer-runtime-deps');
|
||||
return { isKnownWord: (text: string) => deps.isKnownWord(text) };
|
||||
},
|
||||
tokenizeSubtitle: async (text, deps) => {
|
||||
calls.push(`tokenize:${text}`);
|
||||
deps.isKnownWord('known');
|
||||
return { text };
|
||||
},
|
||||
createMecabTokenizerAndCheckMainDeps: {
|
||||
getMecabTokenizer: () => ({ id: 'mecab' }),
|
||||
setMecabTokenizer: () => {},
|
||||
createMecabTokenizer: () => ({ id: 'mecab' }),
|
||||
checkAvailability: async () => {
|
||||
calls.push('check-mecab');
|
||||
},
|
||||
},
|
||||
prewarmSubtitleDictionariesMainDeps: {
|
||||
ensureJlptDictionaryLookup: async () => {
|
||||
calls.push('prewarm-jlpt');
|
||||
},
|
||||
ensureFrequencyDictionaryLookup: async () => {
|
||||
calls.push('prewarm-frequency');
|
||||
},
|
||||
},
|
||||
},
|
||||
warmups: {
|
||||
launchBackgroundWarmupTaskMainDeps: {
|
||||
now: () => 100,
|
||||
logDebug: () => {
|
||||
calls.push('warmup-debug');
|
||||
},
|
||||
logWarn: () => {
|
||||
calls.push('warmup-warn');
|
||||
},
|
||||
},
|
||||
startBackgroundWarmupsMainDeps: {
|
||||
getStarted: () => started,
|
||||
setStarted: (next) => {
|
||||
started = next;
|
||||
calls.push(`set-started:${String(next)}`);
|
||||
},
|
||||
isTexthookerOnlyMode: () => false,
|
||||
ensureYomitanExtensionLoaded: async () => {
|
||||
calls.push('warmup-yomitan');
|
||||
},
|
||||
shouldAutoConnectJellyfinRemote: () => false,
|
||||
startJellyfinRemoteSession: async () => {
|
||||
calls.push('warmup-jellyfin');
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.bindMpvClientEventHandlers, 'function');
|
||||
assert.equal(typeof composed.createMpvClientRuntimeService, 'function');
|
||||
assert.equal(typeof composed.updateMpvSubtitleRenderMetrics, 'function');
|
||||
assert.equal(typeof composed.tokenizeSubtitle, 'function');
|
||||
assert.equal(typeof composed.createMecabTokenizerAndCheck, 'function');
|
||||
assert.equal(typeof composed.prewarmSubtitleDictionaries, 'function');
|
||||
assert.equal(typeof composed.launchBackgroundWarmupTask, 'function');
|
||||
assert.equal(typeof composed.startBackgroundWarmups, 'function');
|
||||
|
||||
const client = composed.createMpvClientRuntimeService() as FakeMpvClient;
|
||||
assert.equal(client.connected, true);
|
||||
|
||||
composed.updateMpvSubtitleRenderMetrics({ subPos: 90 });
|
||||
const tokenized = await composed.tokenizeSubtitle('subtitle text');
|
||||
await composed.createMecabTokenizerAndCheck();
|
||||
await composed.prewarmSubtitleDictionaries();
|
||||
composed.startBackgroundWarmups();
|
||||
|
||||
assert.deepEqual(tokenized, { text: 'subtitle text' });
|
||||
assert.equal(metrics.subPos, 90);
|
||||
assert.ok(calls.includes('create-client:/tmp/mpv.sock'));
|
||||
assert.ok(calls.includes('auto-start:true'));
|
||||
assert.ok(calls.includes('client-connect'));
|
||||
assert.ok(calls.includes('apply-metrics-patch'));
|
||||
assert.ok(calls.includes('set-metrics'));
|
||||
assert.ok(calls.includes('broadcast-metrics'));
|
||||
assert.ok(calls.includes('create-tokenizer-runtime-deps'));
|
||||
assert.ok(calls.includes('tokenize:subtitle text'));
|
||||
assert.ok(calls.includes('check-mecab'));
|
||||
assert.ok(calls.includes('prewarm-jlpt'));
|
||||
assert.ok(calls.includes('prewarm-frequency'));
|
||||
assert.ok(calls.includes('set-started:true'));
|
||||
assert.ok(calls.includes('warmup-yomitan'));
|
||||
});
|
||||
142
src/main/runtime/composers/mpv-runtime-composer.ts
Normal file
142
src/main/runtime/composers/mpv-runtime-composer.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { createBindMpvMainEventHandlersHandler } from '../mpv-main-event-bindings';
|
||||
import { createBuildBindMpvMainEventHandlersMainDepsHandler } from '../mpv-main-event-main-deps';
|
||||
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from '../mpv-client-runtime-service-main-deps';
|
||||
import { createMpvClientRuntimeServiceFactory } from '../mpv-client-runtime-service';
|
||||
import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from '../mpv-subtitle-render-metrics-main-deps';
|
||||
import { createUpdateMpvSubtitleRenderMetricsHandler } from '../mpv-subtitle-render-metrics';
|
||||
import {
|
||||
createBuildTokenizerDepsMainHandler,
|
||||
createCreateMecabTokenizerAndCheckMainHandler,
|
||||
createPrewarmSubtitleDictionariesMainHandler,
|
||||
} from '../subtitle-tokenization-main-deps';
|
||||
import {
|
||||
createBuildLaunchBackgroundWarmupTaskMainDepsHandler,
|
||||
createBuildStartBackgroundWarmupsMainDepsHandler,
|
||||
} from '../startup-warmups-main-deps';
|
||||
import {
|
||||
createLaunchBackgroundWarmupTaskHandler as createLaunchBackgroundWarmupTaskFromStartup,
|
||||
createStartBackgroundWarmupsHandler as createStartBackgroundWarmupsFromStartup,
|
||||
} from '../startup-warmups';
|
||||
|
||||
type BindMpvMainEventHandlersMainDeps = Parameters<
|
||||
typeof createBuildBindMpvMainEventHandlersMainDepsHandler
|
||||
>[0];
|
||||
type MpvClientRuntimeServiceFactoryMainDeps = Omit<
|
||||
Parameters<typeof createBuildMpvClientRuntimeServiceFactoryDepsHandler>[0],
|
||||
'bindEventHandlers'
|
||||
>;
|
||||
type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<
|
||||
typeof createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler
|
||||
>[0];
|
||||
type BuildTokenizerDepsMainDeps = Parameters<typeof createBuildTokenizerDepsMainHandler>[0];
|
||||
type TokenizerMainDeps = ReturnType<ReturnType<typeof createBuildTokenizerDepsMainHandler>>;
|
||||
type CreateMecabTokenizerAndCheckMainDeps = Parameters<
|
||||
typeof createCreateMecabTokenizerAndCheckMainHandler
|
||||
>[0];
|
||||
type PrewarmSubtitleDictionariesMainDeps = Parameters<
|
||||
typeof createPrewarmSubtitleDictionariesMainHandler
|
||||
>[0];
|
||||
type LaunchBackgroundWarmupTaskMainDeps = Parameters<
|
||||
typeof createBuildLaunchBackgroundWarmupTaskMainDepsHandler
|
||||
>[0];
|
||||
type StartBackgroundWarmupsMainDeps = Omit<
|
||||
Parameters<typeof createBuildStartBackgroundWarmupsMainDepsHandler>[0],
|
||||
'launchTask' | 'createMecabTokenizerAndCheck' | 'prewarmSubtitleDictionaries'
|
||||
>;
|
||||
|
||||
export type MpvRuntimeComposerOptions<TTokenizerRuntimeDeps, TTokenizedSubtitle> = {
|
||||
bindMpvMainEventHandlersMainDeps: BindMpvMainEventHandlersMainDeps;
|
||||
mpvClientRuntimeServiceFactoryMainDeps: MpvClientRuntimeServiceFactoryMainDeps;
|
||||
updateMpvSubtitleRenderMetricsMainDeps: UpdateMpvSubtitleRenderMetricsMainDeps;
|
||||
tokenizer: {
|
||||
buildTokenizerDepsMainDeps: BuildTokenizerDepsMainDeps;
|
||||
createTokenizerRuntimeDeps: (deps: TokenizerMainDeps) => TTokenizerRuntimeDeps;
|
||||
tokenizeSubtitle: (text: string, deps: TTokenizerRuntimeDeps) => Promise<TTokenizedSubtitle>;
|
||||
createMecabTokenizerAndCheckMainDeps: CreateMecabTokenizerAndCheckMainDeps;
|
||||
prewarmSubtitleDictionariesMainDeps: PrewarmSubtitleDictionariesMainDeps;
|
||||
};
|
||||
warmups: {
|
||||
launchBackgroundWarmupTaskMainDeps: LaunchBackgroundWarmupTaskMainDeps;
|
||||
startBackgroundWarmupsMainDeps: StartBackgroundWarmupsMainDeps;
|
||||
};
|
||||
};
|
||||
|
||||
export type MpvRuntimeComposerResult<TTokenizedSubtitle> = {
|
||||
bindMpvClientEventHandlers: ReturnType<typeof createBindMpvMainEventHandlersHandler>;
|
||||
createMpvClientRuntimeService: () => unknown;
|
||||
updateMpvSubtitleRenderMetrics: ReturnType<typeof createUpdateMpvSubtitleRenderMetricsHandler>;
|
||||
tokenizeSubtitle: (text: string) => Promise<TTokenizedSubtitle>;
|
||||
createMecabTokenizerAndCheck: () => Promise<void>;
|
||||
prewarmSubtitleDictionaries: () => Promise<void>;
|
||||
launchBackgroundWarmupTask: ReturnType<typeof createLaunchBackgroundWarmupTaskFromStartup>;
|
||||
startBackgroundWarmups: ReturnType<typeof createStartBackgroundWarmupsFromStartup>;
|
||||
};
|
||||
|
||||
export function composeMpvRuntimeHandlers<TTokenizerRuntimeDeps, TTokenizedSubtitle>(
|
||||
options: MpvRuntimeComposerOptions<TTokenizerRuntimeDeps, TTokenizedSubtitle>,
|
||||
): MpvRuntimeComposerResult<TTokenizedSubtitle> {
|
||||
const bindMpvMainEventHandlersMainDeps = createBuildBindMpvMainEventHandlersMainDepsHandler(
|
||||
options.bindMpvMainEventHandlersMainDeps,
|
||||
)();
|
||||
const bindMpvClientEventHandlers = createBindMpvMainEventHandlersHandler(
|
||||
bindMpvMainEventHandlersMainDeps,
|
||||
);
|
||||
|
||||
const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
|
||||
createBuildMpvClientRuntimeServiceFactoryDepsHandler({
|
||||
...options.mpvClientRuntimeServiceFactoryMainDeps,
|
||||
bindEventHandlers: (client) => bindMpvClientEventHandlers(client as never),
|
||||
});
|
||||
const createMpvClientRuntimeService = (): unknown =>
|
||||
createMpvClientRuntimeServiceFactory(
|
||||
buildMpvClientRuntimeServiceFactoryMainDepsHandler() as never,
|
||||
)();
|
||||
|
||||
const updateMpvSubtitleRenderMetrics = createUpdateMpvSubtitleRenderMetricsHandler(
|
||||
createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler(
|
||||
options.updateMpvSubtitleRenderMetricsMainDeps,
|
||||
)(),
|
||||
);
|
||||
|
||||
const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler(
|
||||
options.tokenizer.buildTokenizerDepsMainDeps,
|
||||
);
|
||||
const createMecabTokenizerAndCheck = createCreateMecabTokenizerAndCheckMainHandler(
|
||||
options.tokenizer.createMecabTokenizerAndCheckMainDeps,
|
||||
);
|
||||
const prewarmSubtitleDictionaries = createPrewarmSubtitleDictionariesMainHandler(
|
||||
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
|
||||
);
|
||||
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
|
||||
await prewarmSubtitleDictionaries();
|
||||
return options.tokenizer.tokenizeSubtitle(
|
||||
text,
|
||||
options.tokenizer.createTokenizerRuntimeDeps(buildTokenizerDepsHandler()),
|
||||
);
|
||||
};
|
||||
|
||||
const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskFromStartup(
|
||||
createBuildLaunchBackgroundWarmupTaskMainDepsHandler(
|
||||
options.warmups.launchBackgroundWarmupTaskMainDeps,
|
||||
)(),
|
||||
);
|
||||
const startBackgroundWarmups = createStartBackgroundWarmupsFromStartup(
|
||||
createBuildStartBackgroundWarmupsMainDepsHandler({
|
||||
...options.warmups.startBackgroundWarmupsMainDeps,
|
||||
launchTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||
})(),
|
||||
);
|
||||
|
||||
return {
|
||||
bindMpvClientEventHandlers: (client) => bindMpvClientEventHandlers(client),
|
||||
createMpvClientRuntimeService,
|
||||
updateMpvSubtitleRenderMetrics: (patch) => updateMpvSubtitleRenderMetrics(patch),
|
||||
tokenizeSubtitle,
|
||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||
launchBackgroundWarmupTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user