mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
248 lines
13 KiB
Markdown
248 lines
13 KiB
Markdown
# Architecture
|
|
|
|
SubMiner uses a service-oriented Electron architecture with a composition-oriented main process and a modular renderer process.
|
|
|
|
## Goals
|
|
|
|
- Keep behavior stable while reducing coupling.
|
|
- Prefer small, single-purpose units that can be tested in isolation.
|
|
- Keep `main.ts` focused on wiring and state ownership, not implementation detail.
|
|
- Follow Unix-style composability:
|
|
- each service does one job
|
|
- services compose through explicit inputs/outputs
|
|
- orchestration is separate from implementation
|
|
|
|
## Project Structure
|
|
|
|
```text
|
|
src/
|
|
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)
|
|
app-lifecycle.ts # Electron lifecycle event registration
|
|
cli-runtime.ts # CLI command handling and dispatch
|
|
dependencies.ts # Shared dependency builders for IPC/runtime
|
|
ipc-mpv-command.ts # MPV command composition helpers
|
|
ipc-runtime.ts # IPC channel registration and handlers
|
|
overlay-runtime.ts # Overlay window/modal selection and state
|
|
overlay-shortcuts-runtime.ts # Overlay keyboard shortcut handling
|
|
startup.ts # Startup bootstrap flow (argv/env processing)
|
|
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
|
|
cli/ # CLI parsing and help output
|
|
config/ # Config schema, defaults, validation, template generation
|
|
renderer/ # Overlay renderer (modularized UI/runtime)
|
|
window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS)
|
|
jimaku/ # Jimaku API integration helpers
|
|
subsync/ # Subtitle sync (alass/ffsubsync) helpers
|
|
subtitle/ # Subtitle processing utilities
|
|
tokenizers/ # Tokenizer implementations
|
|
token-mergers/ # Token merge strategies
|
|
translators/ # AI translation providers
|
|
```
|
|
|
|
### Service Layer (`src/core/services/`)
|
|
|
|
- **Startup** — `startup-service`, `app-lifecycle-service`, `app-ready-service`
|
|
- **Overlay** — `overlay-manager-service`, `overlay-window-service`, `overlay-visibility-service`, `overlay-bridge-service`, `overlay-runtime-init-service`, `overlay-content-measurement-service`
|
|
- **Shortcuts** — `shortcut-service`, `overlay-shortcut-service`, `overlay-shortcut-handler`, `shortcut-fallback-service`, `numeric-shortcut-service`, `numeric-shortcut-session-service`
|
|
- **MPV** — `mpv-service`, `mpv-control-service`, `mpv-render-metrics-service`, `mpv-transport`, `mpv-protocol`, `mpv-state`, `mpv-properties`
|
|
- **IPC** — `ipc-service`, `ipc-command-service`, `runtime-options-ipc-service`
|
|
- **Mining** — `mining-service`, `field-grouping-service`, `field-grouping-overlay-service`, `anki-jimaku-service`, `anki-jimaku-ipc-service`
|
|
- **Subtitles** — `subtitle-ws-service`, `subtitle-position-service`, `secondary-subtitle-service`, `tokenizer-service`
|
|
- **Integrations** — `jimaku-service`, `subsync-service`, `subsync-runner-service`, `texthooker-service`, `yomitan-extension-loader-service`, `yomitan-settings-service`
|
|
- **Config** — `runtime-config-service`, `cli-command-service`
|
|
|
|
### Renderer Layer (`src/renderer/`)
|
|
|
|
The overlay renderer is split by concern so `renderer.ts` stays focused on bootstrapping, IPC wiring, and module composition.
|
|
|
|
```text
|
|
src/renderer/
|
|
renderer.ts # Entrypoint/orchestration only
|
|
context.ts # Shared runtime context contract
|
|
state.ts # Centralized renderer mutable state
|
|
subtitle-render.ts # Primary/secondary subtitle rendering + style application
|
|
positioning.ts # Visible/invisible positioning + mpv metrics layout
|
|
handlers/
|
|
keyboard.ts # Keybindings, chord handling, modal key routing
|
|
mouse.ts # Hover/drag behavior, selection + observer wiring
|
|
modals/
|
|
jimaku.ts # Jimaku modal flow
|
|
kiku.ts # Kiku field-grouping modal flow
|
|
runtime-options.ts # Runtime options modal flow
|
|
subsync.ts # Manual subsync modal flow
|
|
utils/
|
|
dom.ts # Required DOM lookups + typed handles
|
|
platform.ts # Layer/platform capability detection
|
|
```
|
|
|
|
## Flow Diagram
|
|
|
|
The main process has three layers: `main.ts` delegates to composition modules that wire together domain services. The renderer runs in a separate Electron process, connected through `preload.ts`.
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
classDef entry fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px
|
|
classDef comp fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
classDef svc fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
classDef bridge fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
classDef rend fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
classDef ext fill:#a6da95,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
|
|
Main["main.ts"]:::entry
|
|
|
|
subgraph Comp["Composition — src/main/"]
|
|
Startup["Startup & Lifecycle<br/>startup · app-lifecycle<br/>startup-lifecycle · state"]:::comp
|
|
Wiring["Runtime Wiring<br/>ipc-runtime · cli-runtime<br/>overlay-runtime · subsync-runtime"]:::comp
|
|
end
|
|
|
|
subgraph Svc["Services — src/core/services/"]
|
|
direction LR
|
|
Mpv["MPV Stack<br/>transport · protocol<br/>state · properties"]:::svc
|
|
Overlay["Overlay<br/>manager · window<br/>visibility · bridge"]:::svc
|
|
Mining["Mining & Subtitles<br/>mining · field-grouping<br/>subtitle-ws · tokenizer"]:::svc
|
|
Integrations["Integrations<br/>jimaku · subsync<br/>texthooker · yomitan"]:::svc
|
|
end
|
|
|
|
Bridge(["preload.ts — Electron IPC"]):::bridge
|
|
|
|
subgraph Rend["Renderer — src/renderer/"]
|
|
Orchestration["renderer.ts<br/>orchestration · IPC wiring"]:::rend
|
|
UI["subtitle-render · positioning<br/>handlers · modals"]:::rend
|
|
end
|
|
|
|
subgraph Ext["External Systems"]
|
|
direction LR
|
|
mpv["mpv"]:::ext
|
|
Anki["AnkiConnect"]:::ext
|
|
Jimaku["Jimaku API"]:::ext
|
|
Tracker["Window Tracker"]:::ext
|
|
end
|
|
|
|
Main -->|delegates| Comp
|
|
Startup -->|initializes| Svc
|
|
Wiring -->|dispatches to| Svc
|
|
|
|
Overlay <--> Bridge
|
|
Mining <--> Bridge
|
|
Bridge <--> Orchestration
|
|
Orchestration --> UI
|
|
|
|
Mpv <-->|JSON socket| mpv
|
|
Mining -->|HTTP| Anki
|
|
Integrations -->|HTTP| Jimaku
|
|
Overlay --> Tracker
|
|
|
|
style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
|
|
style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
|
|
style Rend fill:#363a4f,stroke:#494d64,color:#cad3f5
|
|
style Ext fill:#363a4f,stroke:#494d64,color:#cad3f5
|
|
```
|
|
|
|
## Composition Pattern
|
|
|
|
Most runtime code follows a dependency-injection pattern:
|
|
|
|
1. Define a service interface in `src/core/services/*`.
|
|
2. Keep core logic in pure or side-effect-bounded functions.
|
|
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/` and `src/main/runtime/composers/`:
|
|
|
|
- `startup.ts` — argv/env processing and bootstrap flow
|
|
- `app-lifecycle.ts` — Electron lifecycle event registration
|
|
- `startup-lifecycle.ts` — app-ready initialization sequence
|
|
- `state.ts` — centralized application runtime state container
|
|
- `ipc-runtime.ts` — IPC channel registration and handler wiring
|
|
- `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
|
|
|
|
Composer modules share contract conventions via `src/main/runtime/composers/contracts.ts`:
|
|
|
|
- composer input surfaces are declared with `ComposerInputs<T>` so required dependencies cannot be omitted at compile time
|
|
- composer outputs are declared with `ComposerOutputs<T>` to keep result contracts explicit and stable
|
|
- builder return payload extraction should use shared type helpers instead of inline ad-hoc inference
|
|
|
|
This keeps side effects explicit and makes behavior easy to unit-test with fakes.
|
|
|
|
## Program Lifecycle
|
|
|
|
- **Startup:** `startup.ts` parses CLI args and detects the compositor backend. If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.
|
|
- **Initialization:** Once `app.whenReady()` fires, `startup-lifecycle.ts` runs a short critical path first (config reload, keybindings, mpv client, overlay setup, IPC bridge), then schedules non-critical warmups in the background (MeCab availability check, Yomitan extension load, dictionary prewarm, optional Jellyfin remote startup).
|
|
- **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, and keyboard shortcuts all route through the composition layer to domain services, which update state and broadcast to the renderer.
|
|
- **Shutdown:** Electron's `will-quit` triggers service teardown — closes the mpv socket, unregisters shortcuts, stops WebSocket and texthooker servers, destroys the window tracker, and cleans up Anki state.
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
classDef start fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px
|
|
classDef phase fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
classDef decision fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
classDef init fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
classDef runtime fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
classDef shutdown fill:#ed8796,stroke:#363a4f,color:#24273a,stroke-width:1.5px
|
|
|
|
CLI["CLI args & environment"]:::start
|
|
CLI --> Parse["startup.ts<br/>Parse argv · detect backend · resolve config"]:::phase
|
|
Parse --> GenCheck{"--generate-config?"}:::decision
|
|
GenCheck -->|yes| GenExit["Write config template & exit"]:::phase
|
|
GenCheck -->|no| Lifecycle["app-lifecycle.ts<br/>Acquire single-instance lock<br/>Register Electron lifecycle hooks"]:::phase
|
|
Lifecycle -->|"app.whenReady()"| Ready["startup-lifecycle.ts"]:::phase
|
|
|
|
Ready --> Init
|
|
subgraph Init["Initialization"]
|
|
direction LR
|
|
Config["Load config<br/>resolve keybindings"]:::init
|
|
Runtime["Create mpv client<br/>init runtime options"]:::init
|
|
Platform["Start window tracker<br/>WebSocket policy"]:::init
|
|
end
|
|
|
|
Init --> Create["Create overlay window<br/>Establish IPC bridge"]:::phase
|
|
Create --> Warm["Background warmups<br/>MeCab · Yomitan · dictionaries · Jellyfin"]:::phase
|
|
|
|
Warm --> Loop
|
|
subgraph Loop["Runtime — event-driven"]
|
|
direction LR
|
|
Events["mpv · IPC · CLI<br/>shortcut events"]:::runtime
|
|
Dispatch["Route to service<br/>via composition layer"]:::runtime
|
|
State["Update state<br/>broadcast to renderer"]:::runtime
|
|
Events --> Dispatch --> State
|
|
end
|
|
|
|
Loop -->|"app close"| Quit["Electron will-quit"]:::shutdown
|
|
Quit --> Teardown["Close mpv socket · unregister shortcuts<br/>Stop WebSocket & texthooker<br/>Destroy tracker · clean Anki state"]:::shutdown
|
|
|
|
style Init fill:#363a4f,stroke:#494d64,color:#cad3f5
|
|
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
|
|
```
|
|
|
|
## Why This Design
|
|
|
|
- **Smaller blast radius:** changing one feature usually touches one service.
|
|
- **Better testability:** most behavior can be tested without Electron windows/mpv.
|
|
- **Better reviewability:** PRs can be scoped to one subsystem.
|
|
- **Backward compatibility:** CLI flags and IPC channels can remain stable while internals evolve.
|
|
- **Extracted composition root:** TASK-27 refactored `main.ts` into focused modules under `src/main/`, isolating startup, lifecycle, IPC, CLI, and domain-specific runtime wiring.
|
|
- **Split MPV service:** TASK-27.4 separated `mpv.ts` into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), state (`mpv-state.ts`), and properties (`mpv-properties.ts`) layers for improved maintainability.
|
|
|
|
## Extension Rules
|
|
|
|
- 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.
|
|
- For cross-cutting changes, extract-first then refactor internals after parity is verified.
|
|
- When adding new IPC channels or CLI commands, register them in the appropriate `src/main/` module (`ipc-runtime.ts` for IPC, `cli-runtime.ts` for CLI).
|