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
|
id: TASK-71
|
||||||
title: Split main.ts into domain runtime modules round 2
|
title: Split main.ts into domain runtime modules round 2
|
||||||
status: In Progress
|
status: Done
|
||||||
assignee: []
|
assignee:
|
||||||
|
- codex
|
||||||
created_date: '2026-02-18 11:35'
|
created_date: '2026-02-18 11:35'
|
||||||
updated_date: '2026-02-21 03:40'
|
updated_date: '2026-02-21 04:57'
|
||||||
labels:
|
labels:
|
||||||
- architecture
|
- architecture
|
||||||
- refactor
|
- refactor
|
||||||
@@ -45,14 +46,48 @@ priority: high
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
<!-- AC:BEGIN -->
|
<!-- AC:BEGIN -->
|
||||||
- [ ] #1 `src/main.ts` responsibilities reduced to composition/wiring concerns
|
- [x] #1 `src/main.ts` responsibilities reduced to composition/wiring concerns
|
||||||
- [ ] #2 Extracted runtime modules have focused interfaces and isolated tests
|
- [x] #2 Extracted runtime modules have focused interfaces and isolated tests
|
||||||
- [ ] #3 No CLI/IPC regressions in existing test suite
|
- [x] #3 No CLI/IPC regressions in existing test suite
|
||||||
- [ ] #4 Docs reflect new module boundaries
|
- [x] #4 Docs reflect new module boundaries
|
||||||
<!-- AC:END -->
|
<!-- 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
|
## Definition of Done
|
||||||
<!-- DOD:BEGIN -->
|
<!-- DOD:BEGIN -->
|
||||||
- [ ] #1 `bun run test:fast` passes
|
- [x] #1 `bun run test:fast` passes
|
||||||
- [ ] #2 Architecture docs updated
|
- [x] #2 Architecture docs updated
|
||||||
<!-- DOD:END -->
|
<!-- DOD:END -->
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ SubMiner uses a service-oriented Electron architecture with a composition-orient
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
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
|
preload.ts # Electron preload bridge
|
||||||
types.ts # Shared type definitions
|
types.ts # Shared type definitions
|
||||||
main/ # Composition root modules (extracted from main.ts)
|
main/ # Composition root modules (extracted from main.ts)
|
||||||
@@ -31,6 +31,9 @@ src/
|
|||||||
startup-lifecycle.ts # App-ready initialization sequence
|
startup-lifecycle.ts # App-ready initialization sequence
|
||||||
state.ts # Application runtime state container
|
state.ts # Application runtime state container
|
||||||
subsync-runtime.ts # Subsync command orchestration
|
subsync-runtime.ts # Subsync command orchestration
|
||||||
|
runtime/
|
||||||
|
composers/ # Composition assembly clusters consumed by main.ts
|
||||||
|
domains/ # Domain barrel exports for runtime services
|
||||||
core/
|
core/
|
||||||
services/ # ~60 focused service modules (see below)
|
services/ # ~60 focused service modules (see below)
|
||||||
utils/ # Pure helpers and coercion/config utilities
|
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.
|
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.
|
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
|
- `startup.ts` — argv/env processing and bootstrap flow
|
||||||
- `app-lifecycle.ts` — Electron lifecycle event registration
|
- `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
|
- `cli-runtime.ts` — CLI command parsing and dispatch
|
||||||
- `overlay-runtime.ts` — overlay window selection and modal state management
|
- `overlay-runtime.ts` — overlay window selection and modal state management
|
||||||
- `subsync-runtime.ts` — subsync command orchestration
|
- `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.
|
This keeps side effects explicit and makes behavior easy to unit-test with fakes.
|
||||||
|
|
||||||
@@ -228,7 +233,7 @@ flowchart TD
|
|||||||
|
|
||||||
## Extension Rules
|
## 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.
|
- Keep service APIs explicit and narrowly scoped.
|
||||||
- Prefer additive changes that preserve existing CLI flags and IPC channel behavior.
|
- Prefer additive changes that preserve existing CLI flags and IPC channel behavior.
|
||||||
- Add/update unit tests for each service extraction or behavior change.
|
- 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.
|
- 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`.
|
- 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`.
|
- 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`.
|
- 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/`.
|
- 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` |
|
| `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` |
|
| `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` |
|
| `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-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: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: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.
|
||||||
|
|||||||
266
src/main.ts
266
src/main.ts
@@ -425,12 +425,16 @@ import {
|
|||||||
} from './config';
|
} from './config';
|
||||||
import { resolveConfigDir } from './config/path-resolution';
|
import { resolveConfigDir } from './config/path-resolution';
|
||||||
import { createMainRuntimeRegistry } from './main/runtime/registry';
|
import { createMainRuntimeRegistry } from './main/runtime/registry';
|
||||||
import { composeAnilistSetupHandlers } from './main/runtime/composers/anilist-setup-composer';
|
import {
|
||||||
import { composeJellyfinRemoteHandlers } from './main/runtime/composers/jellyfin-remote-composer';
|
composeAnilistSetupHandlers,
|
||||||
import { composeIpcRuntimeHandlers } from './main/runtime/composers/ipc-runtime-composer';
|
composeAnilistTrackingHandlers,
|
||||||
import { composeShortcutRuntimes } from './main/runtime/composers/shortcuts-runtime-composer';
|
composeAppReadyRuntime,
|
||||||
import { composeStartupLifecycleHandlers } from './main/runtime/composers/startup-lifecycle-composer';
|
composeIpcRuntimeHandlers,
|
||||||
import { composeAppReadyRuntime } from './main/runtime/composers/app-ready-composer';
|
composeJellyfinRemoteHandlers,
|
||||||
|
composeMpvRuntimeHandlers,
|
||||||
|
composeShortcutRuntimes,
|
||||||
|
composeStartupLifecycleHandlers,
|
||||||
|
} from './main/runtime/composers';
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal');
|
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal');
|
||||||
@@ -1496,8 +1500,19 @@ function openJellyfinSetupWindow(): void {
|
|||||||
createOpenJellyfinSetupWindowHandler(buildOpenJellyfinSetupWindowMainDepsHandler())();
|
createOpenJellyfinSetupWindowHandler(buildOpenJellyfinSetupWindowMainDepsHandler())();
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildRefreshAnilistClientSecretStateMainDepsHandler =
|
const {
|
||||||
createBuildRefreshAnilistClientSecretStateMainDepsHandler({
|
refreshAnilistClientSecretState,
|
||||||
|
getCurrentAnilistMediaKey,
|
||||||
|
resetAnilistMediaTracking,
|
||||||
|
getAnilistMediaGuessRuntimeState,
|
||||||
|
setAnilistMediaGuessRuntimeState,
|
||||||
|
resetAnilistMediaGuessState,
|
||||||
|
maybeProbeAnilistDuration,
|
||||||
|
ensureAnilistMediaGuess,
|
||||||
|
processNextAnilistRetryUpdate,
|
||||||
|
maybeRunAnilistPostWatchUpdate,
|
||||||
|
} = composeAnilistTrackingHandlers({
|
||||||
|
refreshClientSecretMainDeps: {
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig),
|
isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig),
|
||||||
getCachedAccessToken: () => anilistCachedAccessToken,
|
getCachedAccessToken: () => anilistCachedAccessToken,
|
||||||
@@ -1519,24 +1534,11 @@ const buildRefreshAnilistClientSecretStateMainDepsHandler =
|
|||||||
openAnilistSetupWindow();
|
openAnilistSetupWindow();
|
||||||
},
|
},
|
||||||
now: () => Date.now(),
|
now: () => Date.now(),
|
||||||
});
|
},
|
||||||
const refreshAnilistClientSecretStateMainDeps =
|
getCurrentMediaKeyMainDeps: {
|
||||||
buildRefreshAnilistClientSecretStateMainDepsHandler();
|
|
||||||
const refreshAnilistClientSecretState = createRefreshAnilistClientSecretStateHandler(
|
|
||||||
refreshAnilistClientSecretStateMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildGetCurrentAnilistMediaKeyMainDepsHandler =
|
|
||||||
createBuildGetCurrentAnilistMediaKeyMainDepsHandler({
|
|
||||||
getCurrentMediaPath: () => appState.currentMediaPath,
|
getCurrentMediaPath: () => appState.currentMediaPath,
|
||||||
});
|
},
|
||||||
const getCurrentAnilistMediaKeyMainDeps = buildGetCurrentAnilistMediaKeyMainDepsHandler();
|
resetMediaTrackingMainDeps: {
|
||||||
const getCurrentAnilistMediaKey = createGetCurrentAnilistMediaKeyHandler(
|
|
||||||
getCurrentAnilistMediaKeyMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildResetAnilistMediaTrackingMainDepsHandler =
|
|
||||||
createBuildResetAnilistMediaTrackingMainDepsHandler({
|
|
||||||
setMediaKey: (value) => {
|
setMediaKey: (value) => {
|
||||||
anilistCurrentMediaKey = value;
|
anilistCurrentMediaKey = value;
|
||||||
},
|
},
|
||||||
@@ -1552,28 +1554,15 @@ const buildResetAnilistMediaTrackingMainDepsHandler =
|
|||||||
setLastDurationProbeAtMs: (value) => {
|
setLastDurationProbeAtMs: (value) => {
|
||||||
anilistLastDurationProbeAtMs = value;
|
anilistLastDurationProbeAtMs = value;
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
const resetAnilistMediaTrackingMainDeps = buildResetAnilistMediaTrackingMainDepsHandler();
|
getMediaGuessRuntimeStateMainDeps: {
|
||||||
const resetAnilistMediaTracking = createResetAnilistMediaTrackingHandler(
|
|
||||||
resetAnilistMediaTrackingMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildGetAnilistMediaGuessRuntimeStateMainDepsHandler =
|
|
||||||
createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler({
|
|
||||||
getMediaKey: () => anilistCurrentMediaKey,
|
getMediaKey: () => anilistCurrentMediaKey,
|
||||||
getMediaDurationSec: () => anilistCurrentMediaDurationSec,
|
getMediaDurationSec: () => anilistCurrentMediaDurationSec,
|
||||||
getMediaGuess: () => anilistCurrentMediaGuess,
|
getMediaGuess: () => anilistCurrentMediaGuess,
|
||||||
getMediaGuessPromise: () => anilistCurrentMediaGuessPromise,
|
getMediaGuessPromise: () => anilistCurrentMediaGuessPromise,
|
||||||
getLastDurationProbeAtMs: () => anilistLastDurationProbeAtMs,
|
getLastDurationProbeAtMs: () => anilistLastDurationProbeAtMs,
|
||||||
});
|
},
|
||||||
const getAnilistMediaGuessRuntimeStateMainDeps =
|
setMediaGuessRuntimeStateMainDeps: {
|
||||||
buildGetAnilistMediaGuessRuntimeStateMainDepsHandler();
|
|
||||||
const getAnilistMediaGuessRuntimeState = createGetAnilistMediaGuessRuntimeStateHandler(
|
|
||||||
getAnilistMediaGuessRuntimeStateMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildSetAnilistMediaGuessRuntimeStateMainDepsHandler =
|
|
||||||
createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler({
|
|
||||||
setMediaKey: (value) => {
|
setMediaKey: (value) => {
|
||||||
anilistCurrentMediaKey = value;
|
anilistCurrentMediaKey = value;
|
||||||
},
|
},
|
||||||
@@ -1589,29 +1578,16 @@ const buildSetAnilistMediaGuessRuntimeStateMainDepsHandler =
|
|||||||
setLastDurationProbeAtMs: (value) => {
|
setLastDurationProbeAtMs: (value) => {
|
||||||
anilistLastDurationProbeAtMs = value;
|
anilistLastDurationProbeAtMs = value;
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
const setAnilistMediaGuessRuntimeStateMainDeps =
|
resetMediaGuessStateMainDeps: {
|
||||||
buildSetAnilistMediaGuessRuntimeStateMainDepsHandler();
|
|
||||||
const setAnilistMediaGuessRuntimeState = createSetAnilistMediaGuessRuntimeStateHandler(
|
|
||||||
setAnilistMediaGuessRuntimeStateMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildResetAnilistMediaGuessStateMainDepsHandler =
|
|
||||||
createBuildResetAnilistMediaGuessStateMainDepsHandler({
|
|
||||||
setMediaGuess: (value) => {
|
setMediaGuess: (value) => {
|
||||||
anilistCurrentMediaGuess = value;
|
anilistCurrentMediaGuess = value;
|
||||||
},
|
},
|
||||||
setMediaGuessPromise: (value) => {
|
setMediaGuessPromise: (value) => {
|
||||||
anilistCurrentMediaGuessPromise = value;
|
anilistCurrentMediaGuessPromise = value;
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
const resetAnilistMediaGuessStateMainDeps = buildResetAnilistMediaGuessStateMainDepsHandler();
|
maybeProbeDurationMainDeps: {
|
||||||
const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler(
|
|
||||||
resetAnilistMediaGuessStateMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildMaybeProbeAnilistDurationMainDepsHandler =
|
|
||||||
createBuildMaybeProbeAnilistDurationMainDepsHandler({
|
|
||||||
getState: () => getAnilistMediaGuessRuntimeState(),
|
getState: () => getAnilistMediaGuessRuntimeState(),
|
||||||
setState: (state) => {
|
setState: (state) => {
|
||||||
setAnilistMediaGuessRuntimeState(state);
|
setAnilistMediaGuessRuntimeState(state);
|
||||||
@@ -1620,14 +1596,8 @@ const buildMaybeProbeAnilistDurationMainDepsHandler =
|
|||||||
now: () => Date.now(),
|
now: () => Date.now(),
|
||||||
requestMpvDuration: async () => appState.mpvClient?.requestProperty('duration'),
|
requestMpvDuration: async () => appState.mpvClient?.requestProperty('duration'),
|
||||||
logWarn: (message, error) => logger.warn(message, error),
|
logWarn: (message, error) => logger.warn(message, error),
|
||||||
});
|
},
|
||||||
const maybeProbeAnilistDurationMainDeps = buildMaybeProbeAnilistDurationMainDepsHandler();
|
ensureMediaGuessMainDeps: {
|
||||||
const maybeProbeAnilistDuration = createMaybeProbeAnilistDurationHandler(
|
|
||||||
maybeProbeAnilistDurationMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildEnsureAnilistMediaGuessMainDepsHandler =
|
|
||||||
createBuildEnsureAnilistMediaGuessMainDepsHandler({
|
|
||||||
getState: () => getAnilistMediaGuessRuntimeState(),
|
getState: () => getAnilistMediaGuessRuntimeState(),
|
||||||
setState: (state) => {
|
setState: (state) => {
|
||||||
setAnilistMediaGuessRuntimeState(state);
|
setAnilistMediaGuessRuntimeState(state);
|
||||||
@@ -1637,22 +1607,8 @@ const buildEnsureAnilistMediaGuessMainDepsHandler =
|
|||||||
getCurrentMediaPath: () => appState.currentMediaPath,
|
getCurrentMediaPath: () => appState.currentMediaPath,
|
||||||
getCurrentMediaTitle: () => appState.currentMediaTitle,
|
getCurrentMediaTitle: () => appState.currentMediaTitle,
|
||||||
guessAnilistMediaInfo: (mediaPath, mediaTitle) => guessAnilistMediaInfo(mediaPath, mediaTitle),
|
guessAnilistMediaInfo: (mediaPath, mediaTitle) => guessAnilistMediaInfo(mediaPath, mediaTitle),
|
||||||
});
|
},
|
||||||
const ensureAnilistMediaGuessMainDeps = buildEnsureAnilistMediaGuessMainDepsHandler();
|
processNextRetryUpdateMainDeps: {
|
||||||
const ensureAnilistMediaGuess = createEnsureAnilistMediaGuessHandler(
|
|
||||||
ensureAnilistMediaGuessMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rememberAnilistAttemptedUpdate = (key: string): void => {
|
|
||||||
rememberAnilistAttemptedUpdateKey(
|
|
||||||
anilistAttemptedUpdateKeys,
|
|
||||||
key,
|
|
||||||
ANILIST_MAX_ATTEMPTED_UPDATE_KEYS,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildProcessNextAnilistRetryUpdateMainDepsHandler =
|
|
||||||
createBuildProcessNextAnilistRetryUpdateMainDepsHandler({
|
|
||||||
nextReady: () => anilistUpdateQueue.nextReady(),
|
nextReady: () => anilistUpdateQueue.nextReady(),
|
||||||
refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(),
|
refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(),
|
||||||
setLastAttemptAt: (value) => {
|
setLastAttemptAt: (value) => {
|
||||||
@@ -1675,14 +1631,8 @@ const buildProcessNextAnilistRetryUpdateMainDepsHandler =
|
|||||||
},
|
},
|
||||||
logInfo: (message) => logger.info(message),
|
logInfo: (message) => logger.info(message),
|
||||||
now: () => Date.now(),
|
now: () => Date.now(),
|
||||||
});
|
},
|
||||||
const processNextAnilistRetryUpdateMainDeps = buildProcessNextAnilistRetryUpdateMainDepsHandler();
|
maybeRunPostWatchUpdateMainDeps: {
|
||||||
const processNextAnilistRetryUpdate = createProcessNextAnilistRetryUpdateHandler(
|
|
||||||
processNextAnilistRetryUpdateMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildMaybeRunAnilistPostWatchUpdateMainDepsHandler =
|
|
||||||
createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler({
|
|
||||||
getInFlight: () => anilistUpdateInFlight,
|
getInFlight: () => anilistUpdateInFlight,
|
||||||
setInFlight: (value) => {
|
setInFlight: (value) => {
|
||||||
anilistUpdateInFlight = value;
|
anilistUpdateInFlight = value;
|
||||||
@@ -1721,11 +1671,16 @@ const buildMaybeRunAnilistPostWatchUpdateMainDepsHandler =
|
|||||||
logWarn: (message) => logger.warn(message),
|
logWarn: (message) => logger.warn(message),
|
||||||
minWatchSeconds: ANILIST_UPDATE_MIN_WATCH_SECONDS,
|
minWatchSeconds: ANILIST_UPDATE_MIN_WATCH_SECONDS,
|
||||||
minWatchRatio: ANILIST_UPDATE_MIN_WATCH_RATIO,
|
minWatchRatio: ANILIST_UPDATE_MIN_WATCH_RATIO,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const maybeRunAnilistPostWatchUpdateMainDeps = buildMaybeRunAnilistPostWatchUpdateMainDepsHandler();
|
|
||||||
const maybeRunAnilistPostWatchUpdate = createMaybeRunAnilistPostWatchUpdateHandler(
|
const rememberAnilistAttemptedUpdate = (key: string): void => {
|
||||||
maybeRunAnilistPostWatchUpdateMainDeps,
|
rememberAnilistAttemptedUpdateKey(
|
||||||
|
anilistAttemptedUpdateKeys,
|
||||||
|
key,
|
||||||
|
ANILIST_MAX_ATTEMPTED_UPDATE_KEYS,
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const buildLoadSubtitlePositionMainDepsHandler = createBuildLoadSubtitlePositionMainDepsHandler({
|
const buildLoadSubtitlePositionMainDepsHandler = createBuildLoadSubtitlePositionMainDepsHandler({
|
||||||
loadSubtitlePositionCore: () =>
|
loadSubtitlePositionCore: () =>
|
||||||
@@ -2043,8 +1998,16 @@ function handleInitialArgs(): void {
|
|||||||
handleInitialArgsRuntimeHandler();
|
handleInitialArgsRuntimeHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildBindMpvMainEventHandlersMainDepsHandler =
|
const {
|
||||||
createBuildBindMpvMainEventHandlersMainDepsHandler({
|
bindMpvClientEventHandlers,
|
||||||
|
createMpvClientRuntimeService: createMpvClientRuntimeServiceHandler,
|
||||||
|
updateMpvSubtitleRenderMetrics: updateMpvSubtitleRenderMetricsHandler,
|
||||||
|
tokenizeSubtitle,
|
||||||
|
createMecabTokenizerAndCheck,
|
||||||
|
prewarmSubtitleDictionaries,
|
||||||
|
startBackgroundWarmups,
|
||||||
|
} = composeMpvRuntimeHandlers<ReturnType<typeof createTokenizerDepsRuntime>, SubtitleData>({
|
||||||
|
bindMpvMainEventHandlersMainDeps: {
|
||||||
appState,
|
appState,
|
||||||
getQuitOnDisconnectArmed: () => jellyfinPlayQuitOnDisconnectArmed,
|
getQuitOnDisconnectArmed: () => jellyfinPlayQuitOnDisconnectArmed,
|
||||||
scheduleQuitCheck: (callback) => {
|
scheduleQuitCheck: (callback) => {
|
||||||
@@ -2090,15 +2053,9 @@ const buildBindMpvMainEventHandlersMainDepsHandler =
|
|||||||
updateSubtitleRenderMetrics: (patch) => {
|
updateSubtitleRenderMetrics: (patch) => {
|
||||||
updateMpvSubtitleRenderMetrics(patch as Partial<MpvSubtitleRenderMetrics>);
|
updateMpvSubtitleRenderMetrics(patch as Partial<MpvSubtitleRenderMetrics>);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
const bindMpvMainEventHandlersMainDeps = buildBindMpvMainEventHandlersMainDepsHandler();
|
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||||
const bindMpvClientEventHandlers = createBindMpvMainEventHandlersHandler(
|
createClient: MpvIpcClient as unknown as new (socketPath: string, options: unknown) => unknown,
|
||||||
bindMpvMainEventHandlersMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
|
|
||||||
createBuildMpvClientRuntimeServiceFactoryDepsHandler({
|
|
||||||
createClient: MpvIpcClient,
|
|
||||||
getSocketPath: () => appState.mpvSocketPath,
|
getSocketPath: () => appState.mpvSocketPath,
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
isAutoStartOverlayEnabled: () => appState.autoStartOverlay,
|
isAutoStartOverlayEnabled: () => appState.autoStartOverlay,
|
||||||
@@ -2110,17 +2067,8 @@ const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
|
|||||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => {
|
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => {
|
||||||
appState.reconnectTimer = timer;
|
appState.reconnectTimer = timer;
|
||||||
},
|
},
|
||||||
bindEventHandlers: (client) => bindMpvClientEventHandlers(client),
|
},
|
||||||
});
|
updateMpvSubtitleRenderMetricsMainDeps: {
|
||||||
|
|
||||||
function createMpvClientRuntimeService(): MpvIpcClient {
|
|
||||||
return createMpvClientRuntimeServiceFactory(
|
|
||||||
buildMpvClientRuntimeServiceFactoryMainDepsHandler(),
|
|
||||||
)();
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildUpdateMpvSubtitleRenderMetricsMainDepsHandler =
|
|
||||||
createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler({
|
|
||||||
getCurrentMetrics: () => appState.mpvSubtitleRenderMetrics,
|
getCurrentMetrics: () => appState.mpvSubtitleRenderMetrics,
|
||||||
setCurrentMetrics: (metrics) => {
|
setCurrentMetrics: (metrics) => {
|
||||||
appState.mpvSubtitleRenderMetrics = metrics;
|
appState.mpvSubtitleRenderMetrics = metrics;
|
||||||
@@ -2129,17 +2077,9 @@ const buildUpdateMpvSubtitleRenderMetricsMainDepsHandler =
|
|||||||
broadcastMetrics: (metrics) => {
|
broadcastMetrics: (metrics) => {
|
||||||
broadcastToOverlayWindows('mpv-subtitle-render-metrics:set', metrics);
|
broadcastToOverlayWindows('mpv-subtitle-render-metrics:set', metrics);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
const updateMpvSubtitleRenderMetricsMainDeps = buildUpdateMpvSubtitleRenderMetricsMainDepsHandler();
|
tokenizer: {
|
||||||
const updateMpvSubtitleRenderMetricsRuntime = createUpdateMpvSubtitleRenderMetricsHandler(
|
buildTokenizerDepsMainDeps: {
|
||||||
updateMpvSubtitleRenderMetricsMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
function updateMpvSubtitleRenderMetrics(patch: Partial<MpvSubtitleRenderMetrics>): void {
|
|
||||||
updateMpvSubtitleRenderMetricsRuntime(patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler({
|
|
||||||
getYomitanExt: () => appState.yomitanExt,
|
getYomitanExt: () => appState.yomitanExt,
|
||||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||||
setYomitanParserWindow: (window) => {
|
setYomitanParserWindow: (window) => {
|
||||||
@@ -2160,7 +2100,8 @@ const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler({
|
|||||||
getKnownWordMatchMode: () =>
|
getKnownWordMatchMode: () =>
|
||||||
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
||||||
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
||||||
getMinSentenceWordsForNPlusOne: () => getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
getMinSentenceWordsForNPlusOne: () =>
|
||||||
|
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
||||||
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
||||||
getJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
getJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
||||||
getFrequencyDictionaryEnabled: () =>
|
getFrequencyDictionaryEnabled: () =>
|
||||||
@@ -2168,69 +2109,50 @@ const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler({
|
|||||||
getFrequencyRank: (text) => appState.frequencyRankLookup(text),
|
getFrequencyRank: (text) => appState.frequencyRankLookup(text),
|
||||||
getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
|
getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||||
});
|
},
|
||||||
|
createTokenizerRuntimeDeps: (deps) =>
|
||||||
const buildCreateMecabTokenizerAndCheckMainDepsHandler =
|
createTokenizerDepsRuntime(deps as Parameters<typeof createTokenizerDepsRuntime>[0]),
|
||||||
createCreateMecabTokenizerAndCheckMainHandler({
|
tokenizeSubtitle: (text, deps) => tokenizeSubtitleCore(text, deps),
|
||||||
|
createMecabTokenizerAndCheckMainDeps: {
|
||||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||||
setMecabTokenizer: (tokenizer) => {
|
setMecabTokenizer: (tokenizer) => {
|
||||||
appState.mecabTokenizer = tokenizer;
|
appState.mecabTokenizer = tokenizer as MecabTokenizer | null;
|
||||||
},
|
},
|
||||||
createMecabTokenizer: () => new MecabTokenizer(),
|
createMecabTokenizer: () => new MecabTokenizer(),
|
||||||
checkAvailability: async (tokenizer) => tokenizer.checkAvailability(),
|
checkAvailability: async (tokenizer) => (tokenizer as MecabTokenizer).checkAvailability(),
|
||||||
});
|
},
|
||||||
const createMecabTokenizerAndCheckHandler = buildCreateMecabTokenizerAndCheckMainDepsHandler;
|
prewarmSubtitleDictionariesMainDeps: {
|
||||||
|
|
||||||
const buildPrewarmSubtitleDictionariesMainDepsHandler =
|
|
||||||
createPrewarmSubtitleDictionariesMainHandler({
|
|
||||||
ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
||||||
ensureFrequencyDictionaryLookup: () =>
|
ensureFrequencyDictionaryLookup: () =>
|
||||||
frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
||||||
});
|
},
|
||||||
const prewarmSubtitleDictionariesHandler = buildPrewarmSubtitleDictionariesMainDepsHandler;
|
},
|
||||||
|
warmups: {
|
||||||
async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
|
launchBackgroundWarmupTaskMainDeps: {
|
||||||
await jlptDictionaryRuntime.ensureJlptDictionaryLookup();
|
|
||||||
await frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup();
|
|
||||||
return tokenizeSubtitleCore(text, createTokenizerDepsRuntime(buildTokenizerDepsHandler()));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createMecabTokenizerAndCheck(): Promise<void> {
|
|
||||||
await createMecabTokenizerAndCheckHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function prewarmSubtitleDictionaries(): Promise<void> {
|
|
||||||
await prewarmSubtitleDictionariesHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildLaunchBackgroundWarmupTaskMainDepsHandler =
|
|
||||||
createBuildLaunchBackgroundWarmupTaskMainDepsHandler({
|
|
||||||
now: () => Date.now(),
|
now: () => Date.now(),
|
||||||
logDebug: (message) => logger.debug(message),
|
logDebug: (message) => logger.debug(message),
|
||||||
logWarn: (message) => logger.warn(message),
|
logWarn: (message) => logger.warn(message),
|
||||||
});
|
},
|
||||||
const launchBackgroundWarmupTaskMainDeps = buildLaunchBackgroundWarmupTaskMainDepsHandler();
|
startBackgroundWarmupsMainDeps: {
|
||||||
const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskHandler(
|
|
||||||
launchBackgroundWarmupTaskMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildStartBackgroundWarmupsMainDepsHandler = createBuildStartBackgroundWarmupsMainDepsHandler(
|
|
||||||
{
|
|
||||||
getStarted: () => backgroundWarmupsStarted,
|
getStarted: () => backgroundWarmupsStarted,
|
||||||
setStarted: (started) => {
|
setStarted: (started) => {
|
||||||
backgroundWarmupsStarted = started;
|
backgroundWarmupsStarted = started;
|
||||||
},
|
},
|
||||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||||
launchTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
|
||||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
|
||||||
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
|
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
|
||||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
|
||||||
shouldAutoConnectJellyfinRemote: () => getResolvedConfig().jellyfin.remoteControlAutoConnect,
|
shouldAutoConnectJellyfinRemote: () => getResolvedConfig().jellyfin.remoteControlAutoConnect,
|
||||||
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
||||||
},
|
},
|
||||||
);
|
},
|
||||||
const startBackgroundWarmupsMainDeps = buildStartBackgroundWarmupsMainDepsHandler();
|
});
|
||||||
const startBackgroundWarmups = createStartBackgroundWarmupsHandler(startBackgroundWarmupsMainDeps);
|
|
||||||
|
function createMpvClientRuntimeService(): MpvIpcClient {
|
||||||
|
return createMpvClientRuntimeServiceHandler() as MpvIpcClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMpvSubtitleRenderMetrics(patch: Partial<MpvSubtitleRenderMetrics>): void {
|
||||||
|
updateMpvSubtitleRenderMetricsHandler(patch);
|
||||||
|
}
|
||||||
|
|
||||||
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
|
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
|
||||||
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
|
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
|
||||||
|
|||||||
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