mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-02 06:22:42 -08:00
make pretty
This commit is contained in:
@@ -18,6 +18,7 @@ ordinal: 8000
|
|||||||
Add a user-facing subtitle config option to pause mpv playback when the cursor hovers subtitle text and resume playback when the cursor leaves.
|
Add a user-facing subtitle config option to pause mpv playback when the cursor hovers subtitle text and resume playback when the cursor leaves.
|
||||||
|
|
||||||
Scope:
|
Scope:
|
||||||
|
|
||||||
- New config key: `subtitleStyle.autoPauseVideoOnHover`.
|
- New config key: `subtitleStyle.autoPauseVideoOnHover`.
|
||||||
- Default should be enabled.
|
- Default should be enabled.
|
||||||
- Hover pause/resume must not unpause if playback was already paused before hover.
|
- Hover pause/resume must not unpause if playback was already paused before hover.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ ordinal: 9000
|
|||||||
Add startup gating behavior for wrapper + mpv plugin flow so playback starts paused when visible overlay auto-start is enabled, then auto-resumes only after subtitle tokenization is ready.
|
Add startup gating behavior for wrapper + mpv plugin flow so playback starts paused when visible overlay auto-start is enabled, then auto-resumes only after subtitle tokenization is ready.
|
||||||
|
|
||||||
Scope:
|
Scope:
|
||||||
|
|
||||||
- Plugin option `auto_start_pause_until_ready` (default `yes`).
|
- Plugin option `auto_start_pause_until_ready` (default `yes`).
|
||||||
- Launcher reads plugin runtime config and starts mpv paused when `auto_start=yes`, `auto_start_visible_overlay=yes`, and `auto_start_pause_until_ready=yes`.
|
- Launcher reads plugin runtime config and starts mpv paused when `auto_start=yes`, `auto_start_visible_overlay=yes`, and `auto_start_pause_until_ready=yes`.
|
||||||
- Main process signals readiness via mpv script message after tokenized subtitle delivery.
|
- Main process signals readiness via mpv script message after tokenized subtitle delivery.
|
||||||
@@ -43,6 +44,7 @@ Scope:
|
|||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
|
||||||
Implemented startup pause gate across launcher/plugin/main runtime:
|
Implemented startup pause gate across launcher/plugin/main runtime:
|
||||||
|
|
||||||
- Added plugin runtime config parsing in launcher (`auto_start`, `auto_start_visible_overlay`, `auto_start_pause_until_ready`) and mpv start-paused behavior for eligible runs.
|
- Added plugin runtime config parsing in launcher (`auto_start`, `auto_start_visible_overlay`, `auto_start_pause_until_ready`) and mpv start-paused behavior for eligible runs.
|
||||||
- Added plugin auto-play gate state, timeout fallback, and readiness release via `subminer-autoplay-ready` script message.
|
- Added plugin auto-play gate state, timeout fallback, and readiness release via `subminer-autoplay-ready` script message.
|
||||||
- Added main-process readiness signaling after tokenization delivery, including unpause fallback command path.
|
- Added main-process readiness signaling after tokenization delivery, including unpause fallback command path.
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ ordinal: 10000
|
|||||||
Fix Jimaku modal UX so selecting a subtitle file closes the modal automatically once subtitle download+load succeeds.
|
Fix Jimaku modal UX so selecting a subtitle file closes the modal automatically once subtitle download+load succeeds.
|
||||||
|
|
||||||
Current behavior:
|
Current behavior:
|
||||||
|
|
||||||
- Subtitle file downloads and loads into mpv.
|
- Subtitle file downloads and loads into mpv.
|
||||||
- Jimaku modal remains open until manual close.
|
- Jimaku modal remains open until manual close.
|
||||||
|
|
||||||
Expected behavior:
|
Expected behavior:
|
||||||
|
|
||||||
- On successful `jimakuDownloadFile` result, close modal immediately.
|
- On successful `jimakuDownloadFile` result, close modal immediately.
|
||||||
- Keep error behavior unchanged (stay open + show error).
|
- Keep error behavior unchanged (stay open + show error).
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ ordinal: 11000
|
|||||||
When user selects a Jimaku subtitle, save subtitle with filename derived from currently playing media filename instead of Jimaku release filename.
|
When user selects a Jimaku subtitle, save subtitle with filename derived from currently playing media filename instead of Jimaku release filename.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
- Current media: `anime.mkv`
|
- Current media: `anime.mkv`
|
||||||
- Downloaded subtitle extension: `.srt`
|
- Downloaded subtitle extension: `.srt`
|
||||||
- Saved subtitle path: `anime.ja.srt`
|
- Saved subtitle path: `anime.ja.srt`
|
||||||
|
|
||||||
Scope:
|
Scope:
|
||||||
|
|
||||||
- Apply in Jimaku download IPC path before writing file.
|
- Apply in Jimaku download IPC path before writing file.
|
||||||
- Preserve collision-avoidance behavior (suffix with jimaku entry id/counter when target exists).
|
- Preserve collision-avoidance behavior (suffix with jimaku entry id/counter when target exists).
|
||||||
- Keep mpv load flow unchanged except using renamed path.
|
- Keep mpv load flow unchanged except using renamed path.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ ordinal: 9001
|
|||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
|
||||||
Reduce subtitle annotation latency by:
|
Reduce subtitle annotation latency by:
|
||||||
|
|
||||||
- disabling Yomitan-side MeCab parser requests (`useMecabParser=false`);
|
- disabling Yomitan-side MeCab parser requests (`useMecabParser=false`);
|
||||||
- initializing local MeCab only when POS-dependent annotations are enabled (N+1 / JLPT / frequency);
|
- initializing local MeCab only when POS-dependent annotations are enabled (N+1 / JLPT / frequency);
|
||||||
- replacing per-line local MeCab process spawning with a persistent parser process that auto-shuts down after idle time and restarts on demand.
|
- replacing per-line local MeCab process spawning with a persistent parser process that auto-shuts down after idle time and restarts on demand.
|
||||||
@@ -39,6 +40,7 @@ Reduce subtitle annotation latency by:
|
|||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
|
||||||
Implemented tokenizer latency optimizations:
|
Implemented tokenizer latency optimizations:
|
||||||
|
|
||||||
- switched Yomitan parse requests to `useMecabParser: false`;
|
- switched Yomitan parse requests to `useMecabParser: false`;
|
||||||
- added annotation-aware MeCab initialization gating in runtime warmup flow;
|
- added annotation-aware MeCab initialization gating in runtime warmup flow;
|
||||||
- added persistent local MeCab process (default idle shutdown: 30s) with queued requests, retry-on-process-end, idle auto-shutdown, and automatic restart on new work;
|
- added persistent local MeCab process (default idle shutdown: 30s) with queued requests, retry-on-process-end, idle auto-shutdown, and automatic restart on new work;
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ ordinal: 9002
|
|||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
|
||||||
Address frequency-highlighting regressions:
|
Address frequency-highlighting regressions:
|
||||||
|
|
||||||
- tokens like `断じて` missed rank assignment when Yomitan merged-token reading was truncated/noisy;
|
- tokens like `断じて` missed rank assignment when Yomitan merged-token reading was truncated/noisy;
|
||||||
- known/N+1 tokens were incorrectly colored by frequency color instead of known/N+1 color.
|
- known/N+1 tokens were incorrectly colored by frequency color instead of known/N+1 color.
|
||||||
|
|
||||||
Expected behavior:
|
Expected behavior:
|
||||||
|
|
||||||
- known/N+1 color always wins;
|
- known/N+1 color always wins;
|
||||||
- if token is frequent and within `topX`, frequency rank label can still appear on hover/metadata.
|
- if token is frequent and within `topX`, frequency rank label can still appear on hover/metadata.
|
||||||
|
|
||||||
@@ -42,6 +44,7 @@ Expected behavior:
|
|||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
|
||||||
Implemented and validated:
|
Implemented and validated:
|
||||||
|
|
||||||
- tokenizer now normalizes selected Yomitan merged-token readings by appending missing trailing kana suffixes when safe (`headword === surface`);
|
- tokenizer now normalizes selected Yomitan merged-token readings by appending missing trailing kana suffixes when safe (`headword === surface`);
|
||||||
- frequency lookup now does lazy fallback: requests `{term, reading}` first, and only requests `{term, reading: null}` for misses;
|
- frequency lookup now does lazy fallback: requests `{term, reading}` first, and only requests `{term, reading: null}` for misses;
|
||||||
- this removes eager `(term, null)` payload inflation on medium-frequency lines and reduces extension RPC payload/load;
|
- this removes eager `(term, null)` payload inflation on medium-frequency lines and reduces extension RPC payload/load;
|
||||||
@@ -50,6 +53,7 @@ Implemented and validated:
|
|||||||
- added regression tests covering noisy-reading fallback, lazy fallback-query behavior, and renderer class/label precedence.
|
- added regression tests covering noisy-reading fallback, lazy fallback-query behavior, and renderer class/label precedence.
|
||||||
|
|
||||||
Related commits:
|
Related commits:
|
||||||
|
|
||||||
- `17a417e` (`fix(subtitle): improve frequency highlight reliability`)
|
- `17a417e` (`fix(subtitle): improve frequency highlight reliability`)
|
||||||
- `79f37f3` (`fix(subtitle): prioritize known and n+1 colors over frequency`)
|
- `79f37f3` (`fix(subtitle): prioritize known and n+1 colors over frequency`)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ ordinal: 9003
|
|||||||
Add keybinding-friendly special commands that shift `sub-delay` to align current subtitle start with next/previous cue start, without `sub-seek` probing (avoid playback jump).
|
Add keybinding-friendly special commands that shift `sub-delay` to align current subtitle start with next/previous cue start, without `sub-seek` probing (avoid playback jump).
|
||||||
|
|
||||||
Scope:
|
Scope:
|
||||||
|
|
||||||
- add special commands for next/previous line alignment;
|
- add special commands for next/previous line alignment;
|
||||||
- compute delta from active subtitle cue timeline (external subtitle file/URL, including Jellyfin-delivered URLs);
|
- compute delta from active subtitle cue timeline (external subtitle file/URL, including Jellyfin-delivered URLs);
|
||||||
- apply `add sub-delay <delta>` and show OSD value;
|
- apply `add sub-delay <delta>` and show OSD value;
|
||||||
@@ -42,6 +43,7 @@ Scope:
|
|||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
|
||||||
Implemented no-jump subtitle-delay alignment commands:
|
Implemented no-jump subtitle-delay alignment commands:
|
||||||
|
|
||||||
- added `__sub-delay-next-line` and `__sub-delay-prev-line` special commands;
|
- added `__sub-delay-next-line` and `__sub-delay-prev-line` special commands;
|
||||||
- added `createShiftSubtitleDelayToAdjacentCueHandler` to parse cue start times from active external subtitle source and apply `add sub-delay` delta from current `sub-start`;
|
- added `createShiftSubtitleDelayToAdjacentCueHandler` to parse cue start times from active external subtitle source and apply `add sub-delay` delta from current `sub-start`;
|
||||||
- wired command handling through IPC runtime deps into main runtime;
|
- wired command handling through IPC runtime deps into main runtime;
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ Control the minimum log level for runtime output:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Values | Description |
|
| Option | Values | Description |
|
||||||
| ------- | ----------------------------------- | ------------------------------------------------ |
|
| ------- | ---------------------------------------- | --------------------------------------------------------- |
|
||||||
| `level` | `"debug"`, `"info"`, `"warn"`, `"error"` | Minimum log level for runtime logging (default: `"info"`) |
|
| `level` | `"debug"`, `"info"`, `"warn"`, `"error"` | Minimum log level for runtime logging (default: `"info"`) |
|
||||||
|
|
||||||
### Auto-Start Overlay
|
### Auto-Start Overlay
|
||||||
@@ -258,7 +258,7 @@ See `config.example.jsonc` for detailed configuration options.
|
|||||||
| `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"rgb(30, 32, 48, 0.88)"`) |
|
| `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"rgb(30, 32, 48, 0.88)"`) |
|
||||||
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
|
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
|
||||||
| `preserveLineBreaks` | boolean | Preserve line breaks in visible overlay subtitle rendering (`false` by default). Enable to mirror mpv line layout. |
|
| `preserveLineBreaks` | boolean | Preserve line breaks in visible overlay subtitle rendering (`false` by default). Enable to mirror mpv line layout. |
|
||||||
| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text, then resume on leave (`true` by default). |
|
| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text, then resume on leave (`true` by default). |
|
||||||
| `hoverTokenColor` | string | Hex color used for hovered subtitle token highlight in mpv (default: catppuccin mauve) |
|
| `hoverTokenColor` | string | Hex color used for hovered subtitle token highlight in mpv (default: catppuccin mauve) |
|
||||||
| `hoverTokenBackgroundColor` | string | CSS color used for hovered subtitle token background highlight (default: semi-transparent dark) |
|
| `hoverTokenBackgroundColor` | string | CSS color used for hovered subtitle token background highlight (default: semi-transparent dark) |
|
||||||
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
|
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
|
||||||
@@ -322,6 +322,7 @@ Set the initial vertical subtitle position (measured from the bottom of the scre
|
|||||||
| Option | Values | Description |
|
| Option | Values | Description |
|
||||||
| ---------- | ---------------- | ---------------------------------------------------------------------- |
|
| ---------- | ---------------- | ---------------------------------------------------------------------- |
|
||||||
| `yPercent` | number (0 - 100) | Distance from the bottom as a percent of screen height (default: `10`) |
|
| `yPercent` | number (0 - 100) | Distance from the bottom as a percent of screen height (default: `10`) |
|
||||||
|
|
||||||
In the overlay, you can fine-tune subtitle position at runtime with `Right-click + drag` on subtitle text.
|
In the overlay, you can fine-tune subtitle position at runtime with `Right-click + drag` on subtitle text.
|
||||||
|
|
||||||
### Secondary Subtitles
|
### Secondary Subtitles
|
||||||
@@ -364,23 +365,23 @@ See `config.example.jsonc` for detailed configuration options and more examples.
|
|||||||
|
|
||||||
**Default keybindings:**
|
**Default keybindings:**
|
||||||
|
|
||||||
| Key | Command | Description |
|
| Key | Command | Description |
|
||||||
| ----------------- | ---------------------------- | ------------------------------------- |
|
| -------------------- | ---------------------------- | ------------------------------------- |
|
||||||
| `Space` | `["cycle", "pause"]` | Toggle pause |
|
| `Space` | `["cycle", "pause"]` | Toggle pause |
|
||||||
| `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track |
|
| `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track |
|
||||||
| `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track |
|
| `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track |
|
||||||
| `ArrowRight` | `["seek", 5]` | Seek forward 5 seconds |
|
| `ArrowRight` | `["seek", 5]` | Seek forward 5 seconds |
|
||||||
| `ArrowLeft` | `["seek", -5]` | Seek backward 5 seconds |
|
| `ArrowLeft` | `["seek", -5]` | Seek backward 5 seconds |
|
||||||
| `ArrowUp` | `["seek", 60]` | Seek forward 60 seconds |
|
| `ArrowUp` | `["seek", 60]` | Seek forward 60 seconds |
|
||||||
| `ArrowDown` | `["seek", -60]` | Seek backward 60 seconds |
|
| `ArrowDown` | `["seek", -60]` | Seek backward 60 seconds |
|
||||||
| `Shift+KeyH` | `["sub-seek", -1]` | Jump to previous subtitle |
|
| `Shift+KeyH` | `["sub-seek", -1]` | Jump to previous subtitle |
|
||||||
| `Shift+KeyL` | `["sub-seek", 1]` | Jump to next subtitle |
|
| `Shift+KeyL` | `["sub-seek", 1]` | Jump to next subtitle |
|
||||||
| `Shift+BracketLeft` | `["__sub-delay-prev-line"]` | Shift subtitle delay to previous cue |
|
| `Shift+BracketLeft` | `["__sub-delay-prev-line"]` | Shift subtitle delay to previous cue |
|
||||||
| `Shift+BracketRight` | `["__sub-delay-next-line"]` | Shift subtitle delay to next cue |
|
| `Shift+BracketRight` | `["__sub-delay-next-line"]` | Shift subtitle delay to next cue |
|
||||||
| `Ctrl+Shift+KeyH` | `["__replay-subtitle"]` | Replay current subtitle, pause at end |
|
| `Ctrl+Shift+KeyH` | `["__replay-subtitle"]` | Replay current subtitle, pause at end |
|
||||||
| `Ctrl+Shift+KeyL` | `["__play-next-subtitle"]` | Play next subtitle, pause at end |
|
| `Ctrl+Shift+KeyL` | `["__play-next-subtitle"]` | Play next subtitle, pause at end |
|
||||||
| `KeyQ` | `["quit"]` | Quit mpv |
|
| `KeyQ` | `["quit"]` | Quit mpv |
|
||||||
| `Ctrl+KeyW` | `["quit"]` | Quit mpv |
|
| `Ctrl+KeyW` | `["quit"]` | Quit mpv |
|
||||||
|
|
||||||
**Custom keybindings example:**
|
**Custom keybindings example:**
|
||||||
|
|
||||||
|
|||||||
@@ -79,18 +79,18 @@ Use `subminer <subcommand> -h` for command-specific help.
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
| ----------------------- | --------------------------------------------------- |
|
| --------------------- | --------------------------------------------------- |
|
||||||
| `-d, --directory` | Video search directory (default: cwd) |
|
| `-d, --directory` | Video search directory (default: cwd) |
|
||||||
| `-r, --recursive` | Search directories recursively |
|
| `-r, --recursive` | Search directories recursively |
|
||||||
| `-R, --rofi` | Use rofi instead of fzf |
|
| `-R, --rofi` | Use rofi instead of fzf |
|
||||||
| `--start` | Explicitly start overlay after mpv launches |
|
| `--start` | Explicitly start overlay after mpv launches |
|
||||||
| `-S, --start-overlay` | Explicitly start overlay after mpv launches |
|
| `-S, --start-overlay` | Explicitly start overlay after mpv launches |
|
||||||
| `-T, --no-texthooker` | Disable texthooker server |
|
| `-T, --no-texthooker` | Disable texthooker server |
|
||||||
| `-p, --profile` | mpv profile name (default: `subminer`) |
|
| `-p, --profile` | mpv profile name (default: `subminer`) |
|
||||||
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`) |
|
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`) |
|
||||||
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) |
|
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) |
|
||||||
| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) |
|
| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) |
|
||||||
|
|
||||||
With default plugin settings (`auto_start=yes`, `auto_start_visible_overlay=yes`, `auto_start_pause_until_ready=yes`), explicit start flags are usually unnecessary.
|
With default plugin settings (`auto_start=yes`, `auto_start_visible_overlay=yes`, `auto_start_pause_until_ready=yes`), explicit start flags are usually unnecessary.
|
||||||
|
|
||||||
|
|||||||
@@ -120,27 +120,27 @@ aniskip_button_duration=3
|
|||||||
|
|
||||||
### Option Reference
|
### Option Reference
|
||||||
|
|
||||||
| Option | Default | Values | Description |
|
| Option | Default | Values | Description |
|
||||||
| ---------------------------- | ----------------------------- | ------------------------------------------ | ---------------------------------------------------------------------- |
|
| ------------------------------ | ----------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------- |
|
||||||
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
|
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
|
||||||
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
|
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
|
||||||
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
|
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
|
||||||
| `texthooker_port` | `5174` | 1–65535 | Texthooker server port |
|
| `texthooker_port` | `5174` | 1–65535 | Texthooker server port |
|
||||||
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
|
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
|
||||||
| `auto_start` | `yes` | `yes` / `no` | Auto-start overlay on file load when mpv socket matches `socket_path` |
|
| `auto_start` | `yes` | `yes` / `no` | Auto-start overlay on file load when mpv socket matches `socket_path` |
|
||||||
| `auto_start_visible_overlay` | `yes` | `yes` / `no` | Show visible layer on auto-start when mpv socket matches `socket_path` |
|
| `auto_start_visible_overlay` | `yes` | `yes` / `no` | Show visible layer on auto-start when mpv socket matches `socket_path` |
|
||||||
| `auto_start_pause_until_ready` | `yes` | `yes` / `no` | Pause mpv on visible auto-start; resume when SubMiner signals tokenization-ready |
|
| `auto_start_pause_until_ready` | `yes` | `yes` / `no` | Pause mpv on visible auto-start; resume when SubMiner signals tokenization-ready |
|
||||||
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
|
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
|
||||||
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
|
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
|
||||||
| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection |
|
| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection |
|
||||||
| `aniskip_title` | `""` | string | Override title used for lookup |
|
| `aniskip_title` | `""` | string | Override title used for lookup |
|
||||||
| `aniskip_season` | `""` | numeric season | Optional season hint |
|
| `aniskip_season` | `""` | numeric season | Optional season hint |
|
||||||
| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id |
|
| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id |
|
||||||
| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed |
|
| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed |
|
||||||
| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt |
|
| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt |
|
||||||
| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) |
|
| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) |
|
||||||
| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) |
|
| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) |
|
||||||
| `aniskip_button_duration` | `3` | float seconds | OSD hint duration |
|
| `aniskip_button_duration` | `3` | float seconds | OSD hint duration |
|
||||||
|
|
||||||
## Binary Auto-Detection
|
## Binary Auto-Detection
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const docsThemePath = new URL('./.vitepress/theme/index.ts', import.meta.url);
|
|||||||
const docsThemeContents = readFileSync(docsThemePath, 'utf8');
|
const docsThemeContents = readFileSync(docsThemePath, 'utf8');
|
||||||
|
|
||||||
test('docs theme configures plausible tracker for subminer.moe via worker.subminer.moe', () => {
|
test('docs theme configures plausible tracker for subminer.moe via worker.subminer.moe', () => {
|
||||||
expect(docsThemeContents).toContain("@plausible-analytics/tracker");
|
expect(docsThemeContents).toContain('@plausible-analytics/tracker');
|
||||||
expect(docsThemeContents).toContain('const { init } = await import');
|
expect(docsThemeContents).toContain('const { init } = await import');
|
||||||
expect(docsThemeContents).toContain("domain: 'subminer.moe'");
|
expect(docsThemeContents).toContain("domain: 'subminer.moe'");
|
||||||
expect(docsThemeContents).toContain("endpoint: 'https://worker.subminer.moe'");
|
expect(docsThemeContents).toContain("endpoint: 'https://worker.subminer.moe'");
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ These control playback and subtitle display. They require overlay window focus.
|
|||||||
| `ArrowDown` | Seek backward 60 seconds |
|
| `ArrowDown` | Seek backward 60 seconds |
|
||||||
| `Shift+H` | Jump to previous subtitle |
|
| `Shift+H` | Jump to previous subtitle |
|
||||||
| `Shift+L` | Jump to next subtitle |
|
| `Shift+L` | Jump to next subtitle |
|
||||||
| `Shift+[` | Shift subtitle delay to previous subtitle cue |
|
| `Shift+[` | Shift subtitle delay to previous subtitle cue |
|
||||||
| `Shift+]` | Shift subtitle delay to next subtitle cue |
|
| `Shift+]` | Shift subtitle delay to next subtitle cue |
|
||||||
| `Ctrl+Shift+H` | Replay current subtitle (play to end, then pause) |
|
| `Ctrl+Shift+H` | Replay current subtitle (play to end, then pause) |
|
||||||
| `Ctrl+Shift+L` | Play next subtitle (jump, play to end, then pause) |
|
| `Ctrl+Shift+L` | Play next subtitle (jump, play to end, then pause) |
|
||||||
| `Q` | Quit mpv |
|
| `Q` | Quit mpv |
|
||||||
|
|||||||
@@ -143,11 +143,7 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi
|
|||||||
pluginRuntimeConfig.autoStartPauseUntilReady;
|
pluginRuntimeConfig.autoStartPauseUntilReady;
|
||||||
|
|
||||||
if (shouldPauseUntilOverlayReady) {
|
if (shouldPauseUntilOverlayReady) {
|
||||||
log(
|
log('info', args.logLevel, 'Configured to pause mpv until overlay and tokenization are ready');
|
||||||
'info',
|
|
||||||
args.logLevel,
|
|
||||||
'Configured to pause mpv until overlay and tokenization are ready',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startMpv(
|
startMpv(
|
||||||
@@ -198,11 +194,7 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi
|
|||||||
if (ready) {
|
if (ready) {
|
||||||
log('info', args.logLevel, 'MPV IPC socket ready, relying on mpv plugin auto-start');
|
log('info', args.logLevel, 'MPV IPC socket ready, relying on mpv plugin auto-start');
|
||||||
} else {
|
} else {
|
||||||
log(
|
log('info', args.logLevel, 'MPV IPC socket not ready yet, relying on mpv plugin auto-start');
|
||||||
'info',
|
|
||||||
args.logLevel,
|
|
||||||
'MPV IPC socket not ready yet, relying on mpv plugin auto-start',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (ready) {
|
} else if (ready) {
|
||||||
log(
|
log(
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ export function parsePluginRuntimeConfigContent(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (key === 'auto_start_visible_overlay') {
|
if (key === 'auto_start_visible_overlay') {
|
||||||
runtimeConfig.autoStartVisibleOverlay = parseBooleanValue('auto_start_visible_overlay', value);
|
runtimeConfig.autoStartVisibleOverlay = parseBooleanValue(
|
||||||
|
'auto_start_visible_overlay',
|
||||||
|
value,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (key === 'auto_start_pause_until_ready') {
|
if (key === 'auto_start_pause_until_ready') {
|
||||||
|
|||||||
@@ -239,8 +239,7 @@ export function parseJellyfinPreviewAuthResponse(raw: string): JellyfinPreviewAu
|
|||||||
const serverUrl = sanitizeServerUrl(
|
const serverUrl = sanitizeServerUrl(
|
||||||
typeof candidate.serverUrl === 'string' ? candidate.serverUrl : '',
|
typeof candidate.serverUrl === 'string' ? candidate.serverUrl : '',
|
||||||
);
|
);
|
||||||
const accessToken =
|
const accessToken = typeof candidate.accessToken === 'string' ? candidate.accessToken.trim() : '';
|
||||||
typeof candidate.accessToken === 'string' ? candidate.accessToken.trim() : '';
|
|
||||||
const userId = typeof candidate.userId === 'string' ? candidate.userId.trim() : '';
|
const userId = typeof candidate.userId === 'string' ? candidate.userId.trim() : '';
|
||||||
if (!serverUrl || !accessToken) return null;
|
if (!serverUrl || !accessToken) return null;
|
||||||
|
|
||||||
@@ -271,9 +270,7 @@ export function readUtf8FileAppendedSince(logPath: string, offsetBytes: number):
|
|||||||
const buffer = fs.readFileSync(logPath);
|
const buffer = fs.readFileSync(logPath);
|
||||||
if (buffer.length === 0) return '';
|
if (buffer.length === 0) return '';
|
||||||
const normalizedOffset =
|
const normalizedOffset =
|
||||||
Number.isFinite(offsetBytes) && offsetBytes >= 0
|
Number.isFinite(offsetBytes) && offsetBytes >= 0 ? Math.floor(offsetBytes) : 0;
|
||||||
? Math.floor(offsetBytes)
|
|
||||||
: 0;
|
|
||||||
const startOffset = normalizedOffset > buffer.length ? 0 : normalizedOffset;
|
const startOffset = normalizedOffset > buffer.length ? 0 : normalizedOffset;
|
||||||
return buffer.subarray(startOffset).toString('utf8');
|
return buffer.subarray(startOffset).toString('utf8');
|
||||||
} catch {
|
} catch {
|
||||||
@@ -399,7 +396,9 @@ async function runAppJellyfinCommand(
|
|||||||
|
|
||||||
const hasCommandSignal = (output: string): boolean => {
|
const hasCommandSignal = (output: string): boolean => {
|
||||||
if (label === 'jellyfin-libraries') {
|
if (label === 'jellyfin-libraries') {
|
||||||
return output.includes('Jellyfin library:') || output.includes('No Jellyfin libraries found.');
|
return (
|
||||||
|
output.includes('Jellyfin library:') || output.includes('No Jellyfin libraries found.')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (label === 'jellyfin-items') {
|
if (label === 'jellyfin-items') {
|
||||||
return (
|
return (
|
||||||
@@ -550,7 +549,9 @@ async function resolveJellyfinSelectionViaApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const configuredDefaultLibraryId = session.defaultLibraryId;
|
const configuredDefaultLibraryId = session.defaultLibraryId;
|
||||||
const hasConfiguredDefault = libraries.some((library) => library.id === configuredDefaultLibraryId);
|
const hasConfiguredDefault = libraries.some(
|
||||||
|
(library) => library.id === configuredDefaultLibraryId,
|
||||||
|
);
|
||||||
let libraryId = hasConfiguredDefault ? configuredDefaultLibraryId : '';
|
let libraryId = hasConfiguredDefault ? configuredDefaultLibraryId : '';
|
||||||
if (!libraryId) {
|
if (!libraryId) {
|
||||||
libraryId = pickLibrary(
|
libraryId = pickLibrary(
|
||||||
|
|||||||
@@ -333,7 +333,10 @@ test('parseJellyfinErrorFromAppOutput extracts main runtime error lines', () =>
|
|||||||
[subminer] - 2026-03-01 13:10:34 - ERROR - [main] runJellyfinCommand failed: {"message":"Missing Jellyfin password."}
|
[subminer] - 2026-03-01 13:10:34 - ERROR - [main] runJellyfinCommand failed: {"message":"Missing Jellyfin password."}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
assert.equal(parsed, '[main] runJellyfinCommand failed: {"message":"Missing Jellyfin password."}');
|
assert.equal(
|
||||||
|
parsed,
|
||||||
|
'[main] runJellyfinCommand failed: {"message":"Missing Jellyfin password."}',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parseJellyfinPreviewAuthResponse parses valid structured response payload', () => {
|
test('parseJellyfinPreviewAuthResponse parses valid structured response payload', () => {
|
||||||
@@ -385,7 +388,9 @@ test('shouldRetryWithStartForNoRunningInstance matches expected app lifecycle er
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldRetryWithStartForNoRunningInstance('Missing Jellyfin session. Run --jellyfin-login first.'),
|
shouldRetryWithStartForNoRunningInstance(
|
||||||
|
'Missing Jellyfin session. Run --jellyfin-login first.',
|
||||||
|
),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -407,10 +412,13 @@ test('readUtf8FileAppendedSince treats offset as bytes and survives multibyte lo
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('parseEpisodePathFromDisplay extracts series and season from episode display titles', () => {
|
test('parseEpisodePathFromDisplay extracts series and season from episode display titles', () => {
|
||||||
assert.deepEqual(parseEpisodePathFromDisplay('KONOSUBA S01E03 A Panty Treasure in This Right Hand!'), {
|
assert.deepEqual(
|
||||||
seriesName: 'KONOSUBA',
|
parseEpisodePathFromDisplay('KONOSUBA S01E03 A Panty Treasure in This Right Hand!'),
|
||||||
seasonNumber: 1,
|
{
|
||||||
});
|
seriesName: 'KONOSUBA',
|
||||||
|
seasonNumber: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
assert.deepEqual(parseEpisodePathFromDisplay('Frieren S2E10 Something'), {
|
assert.deepEqual(parseEpisodePathFromDisplay('Frieren S2E10 Something'), {
|
||||||
seriesName: 'Frieren',
|
seriesName: 'Frieren',
|
||||||
seasonNumber: 2,
|
seasonNumber: 2,
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ function extractFilenameFromMediaPath(rawPath: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const separatorIndex = trimmedPath.search(/[?#]/);
|
const separatorIndex = trimmedPath.search(/[?#]/);
|
||||||
const pathWithoutQuery =
|
const pathWithoutQuery = separatorIndex >= 0 ? trimmedPath.slice(0, separatorIndex) : trimmedPath;
|
||||||
separatorIndex >= 0 ? trimmedPath.slice(0, separatorIndex) : trimmedPath;
|
|
||||||
return decodeURIComponentSafe(path.basename(pathWithoutQuery));
|
return decodeURIComponentSafe(path.basename(pathWithoutQuery));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { hasExplicitCommand, parseArgs, shouldRunSettingsOnlyStartup, shouldStartApp } from './args';
|
import {
|
||||||
|
hasExplicitCommand,
|
||||||
|
parseArgs,
|
||||||
|
shouldRunSettingsOnlyStartup,
|
||||||
|
shouldStartApp,
|
||||||
|
} from './args';
|
||||||
|
|
||||||
test('parseArgs parses booleans and value flags', () => {
|
test('parseArgs parses booleans and value flags', () => {
|
||||||
const args = parseArgs([
|
const args = parseArgs([
|
||||||
@@ -148,10 +153,7 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => {
|
|||||||
'/tmp/subminer-jf-response.json',
|
'/tmp/subminer-jf-response.json',
|
||||||
]);
|
]);
|
||||||
assert.equal(jellyfinPreviewAuth.jellyfinPreviewAuth, true);
|
assert.equal(jellyfinPreviewAuth.jellyfinPreviewAuth, true);
|
||||||
assert.equal(
|
assert.equal(jellyfinPreviewAuth.jellyfinResponsePath, '/tmp/subminer-jf-response.json');
|
||||||
jellyfinPreviewAuth.jellyfinResponsePath,
|
|
||||||
'/tmp/subminer-jf-response.json',
|
|
||||||
);
|
|
||||||
assert.equal(hasExplicitCommand(jellyfinPreviewAuth), true);
|
assert.equal(hasExplicitCommand(jellyfinPreviewAuth), true);
|
||||||
assert.equal(shouldStartApp(jellyfinPreviewAuth), false);
|
assert.equal(shouldStartApp(jellyfinPreviewAuth), false);
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,9 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
if (value === 'true' || value === '1' || value === 'yes') args.jellyfinRecursive = true;
|
if (value === 'true' || value === '1' || value === 'yes') args.jellyfinRecursive = true;
|
||||||
if (value === 'false' || value === '0' || value === 'no') args.jellyfinRecursive = false;
|
if (value === 'false' || value === '0' || value === 'no') args.jellyfinRecursive = false;
|
||||||
} else if (arg === '--jellyfin-recursive') {
|
} else if (arg === '--jellyfin-recursive') {
|
||||||
const value = readValue(argv[i + 1])?.trim().toLowerCase();
|
const value = readValue(argv[i + 1])
|
||||||
|
?.trim()
|
||||||
|
.toLowerCase();
|
||||||
if (value === 'false' || value === '0' || value === 'no') {
|
if (value === 'false' || value === '0' || value === 'no') {
|
||||||
args.jellyfinRecursive = false;
|
args.jellyfinRecursive = false;
|
||||||
} else if (value === 'true' || value === '1' || value === 'yes') {
|
} else if (value === 'true' || value === '1' || value === 'yes') {
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ test('loads defaults when config is missing', () => {
|
|||||||
assert.equal(config.subtitleStyle.textRendering, 'geometricPrecision');
|
assert.equal(config.subtitleStyle.textRendering, 'geometricPrecision');
|
||||||
assert.equal(config.subtitleStyle.textShadow, '0 3px 10px rgba(0,0,0,0.69)');
|
assert.equal(config.subtitleStyle.textShadow, '0 3px 10px rgba(0,0,0,0.69)');
|
||||||
assert.equal(config.subtitleStyle.backdropFilter, 'blur(6px)');
|
assert.equal(config.subtitleStyle.backdropFilter, 'blur(6px)');
|
||||||
assert.equal(config.subtitleStyle.secondary.fontFamily, 'Inter, Noto Sans, Helvetica Neue, sans-serif');
|
assert.equal(
|
||||||
|
config.subtitleStyle.secondary.fontFamily,
|
||||||
|
'Inter, Noto Sans, Helvetica Neue, sans-serif',
|
||||||
|
);
|
||||||
assert.equal(config.subtitleStyle.secondary.fontColor, '#cad3f5');
|
assert.equal(config.subtitleStyle.secondary.fontColor, '#cad3f5');
|
||||||
assert.equal(config.immersionTracking.enabled, true);
|
assert.equal(config.immersionTracking.enabled, true);
|
||||||
assert.equal(config.immersionTracking.dbPath, '');
|
assert.equal(config.immersionTracking.dbPath, '');
|
||||||
|
|||||||
@@ -99,8 +99,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
if (isObject(src.subtitleStyle)) {
|
if (isObject(src.subtitleStyle)) {
|
||||||
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
|
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
|
||||||
const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks;
|
const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks;
|
||||||
const fallbackSubtitleStyleAutoPauseVideoOnHover =
|
const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover;
|
||||||
resolved.subtitleStyle.autoPauseVideoOnHover;
|
|
||||||
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
|
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
|
||||||
const fallbackSubtitleStyleHoverTokenBackgroundColor =
|
const fallbackSubtitleStyleHoverTokenBackgroundColor =
|
||||||
resolved.subtitleStyle.hoverTokenBackgroundColor;
|
resolved.subtitleStyle.hoverTokenBackgroundColor;
|
||||||
@@ -161,8 +160,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
if (autoPauseVideoOnHover !== undefined) {
|
if (autoPauseVideoOnHover !== undefined) {
|
||||||
resolved.subtitleStyle.autoPauseVideoOnHover = autoPauseVideoOnHover;
|
resolved.subtitleStyle.autoPauseVideoOnHover = autoPauseVideoOnHover;
|
||||||
} else if (
|
} else if (
|
||||||
(src.subtitleStyle as { autoPauseVideoOnHover?: unknown }).autoPauseVideoOnHover !==
|
(src.subtitleStyle as { autoPauseVideoOnHover?: unknown }).autoPauseVideoOnHover !== undefined
|
||||||
undefined
|
|
||||||
) {
|
) {
|
||||||
resolved.subtitleStyle.autoPauseVideoOnHover = fallbackSubtitleStyleAutoPauseVideoOnHover;
|
resolved.subtitleStyle.autoPauseVideoOnHover = fallbackSubtitleStyleAutoPauseVideoOnHover;
|
||||||
warn(
|
warn(
|
||||||
|
|||||||
@@ -46,23 +46,31 @@ export function pruneRetention(
|
|||||||
const dayCutoff = nowMs - policy.dailyRollupRetentionMs;
|
const dayCutoff = nowMs - policy.dailyRollupRetentionMs;
|
||||||
const monthCutoff = nowMs - policy.monthlyRollupRetentionMs;
|
const monthCutoff = nowMs - policy.monthlyRollupRetentionMs;
|
||||||
|
|
||||||
const deletedSessionEvents = (db
|
const deletedSessionEvents = (
|
||||||
.prepare(`DELETE FROM imm_session_events WHERE ts_ms < ?`)
|
db.prepare(`DELETE FROM imm_session_events WHERE ts_ms < ?`).run(eventCutoff) as {
|
||||||
.run(eventCutoff) as { changes: number }).changes;
|
changes: number;
|
||||||
const deletedTelemetryRows = (db
|
}
|
||||||
.prepare(`DELETE FROM imm_session_telemetry WHERE sample_ms < ?`)
|
).changes;
|
||||||
.run(telemetryCutoff) as { changes: number }).changes;
|
const deletedTelemetryRows = (
|
||||||
const deletedDailyRows = (db
|
db.prepare(`DELETE FROM imm_session_telemetry WHERE sample_ms < ?`).run(telemetryCutoff) as {
|
||||||
.prepare(`DELETE FROM imm_daily_rollups WHERE rollup_day < ?`)
|
changes: number;
|
||||||
.run(Math.floor(dayCutoff / DAILY_MS)) as { changes: number }).changes;
|
}
|
||||||
const deletedMonthlyRows = (db
|
).changes;
|
||||||
.prepare(`DELETE FROM imm_monthly_rollups WHERE rollup_month < ?`)
|
const deletedDailyRows = (
|
||||||
.run(toMonthKey(monthCutoff)) as { changes: number }).changes;
|
db
|
||||||
const deletedEndedSessions = (db
|
.prepare(`DELETE FROM imm_daily_rollups WHERE rollup_day < ?`)
|
||||||
.prepare(
|
.run(Math.floor(dayCutoff / DAILY_MS)) as { changes: number }
|
||||||
`DELETE FROM imm_sessions WHERE ended_at_ms IS NOT NULL AND ended_at_ms < ?`,
|
).changes;
|
||||||
)
|
const deletedMonthlyRows = (
|
||||||
.run(telemetryCutoff) as { changes: number }).changes;
|
db
|
||||||
|
.prepare(`DELETE FROM imm_monthly_rollups WHERE rollup_month < ?`)
|
||||||
|
.run(toMonthKey(monthCutoff)) as { changes: number }
|
||||||
|
).changes;
|
||||||
|
const deletedEndedSessions = (
|
||||||
|
db
|
||||||
|
.prepare(`DELETE FROM imm_sessions WHERE ended_at_ms IS NOT NULL AND ended_at_ms < ?`)
|
||||||
|
.run(telemetryCutoff) as { changes: number }
|
||||||
|
).changes;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deletedSessionEvents,
|
deletedSessionEvents,
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ test('extractLineVocabulary returns words and unique kanji', () => {
|
|||||||
new Set(result.words.map((entry) => `${entry.headword}/${entry.word}`)),
|
new Set(result.words.map((entry) => `${entry.headword}/${entry.word}`)),
|
||||||
new Set(['hello/hello', '你好/你好', '猫/猫']),
|
new Set(['hello/hello', '你好/你好', '猫/猫']),
|
||||||
);
|
);
|
||||||
assert.equal(result.words.every((entry) => entry.reading === ''), true);
|
assert.equal(
|
||||||
|
result.words.every((entry) => entry.reading === ''),
|
||||||
|
true,
|
||||||
|
);
|
||||||
assert.deepEqual(new Set(result.kanji), new Set(['你', '好', '猫']));
|
assert.deepEqual(new Set(result.kanji), new Set(['你', '好', '猫']));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ export function extractLineVocabulary(value: string): ExtractedLineVocabulary {
|
|||||||
if (!cleaned) return { words: [], kanji: [] };
|
if (!cleaned) return { words: [], kanji: [] };
|
||||||
|
|
||||||
const wordSet = new Set<string>();
|
const wordSet = new Set<string>();
|
||||||
const tokenPattern = /[A-Za-z0-9']+|[\u3040-\u30ff]+|[\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df]+/g;
|
const tokenPattern =
|
||||||
|
/[A-Za-z0-9']+|[\u3040-\u30ff]+|[\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df]+/g;
|
||||||
const rawWords = cleaned.match(tokenPattern) ?? [];
|
const rawWords = cleaned.match(tokenPattern) ?? [];
|
||||||
for (const rawWord of rawWords) {
|
for (const rawWord of rawWords) {
|
||||||
const normalizedWord = normalizeText(rawWord.toLowerCase());
|
const normalizedWord = normalizeText(rawWord.toLowerCase());
|
||||||
|
|||||||
@@ -19,15 +19,8 @@ export function startSessionRecord(
|
|||||||
CREATED_DATE, LAST_UPDATE_DATE
|
CREATED_DATE, LAST_UPDATE_DATE
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.run(
|
.run(sessionUuid, videoId, startedAtMs, SESSION_STATUS_ACTIVE, startedAtMs, nowMs);
|
||||||
sessionUuid,
|
|
||||||
videoId,
|
|
||||||
startedAtMs,
|
|
||||||
SESSION_STATUS_ACTIVE,
|
|
||||||
startedAtMs,
|
|
||||||
nowMs,
|
|
||||||
);
|
|
||||||
const sessionId = Number(result.lastInsertRowid);
|
const sessionId = Number(result.lastInsertRowid);
|
||||||
return {
|
return {
|
||||||
sessionId,
|
sessionId,
|
||||||
|
|||||||
@@ -59,9 +59,7 @@ testIfSqlite('ensureSchema creates immersion core tables', () => {
|
|||||||
assert.ok(tableNames.has('imm_rollup_state'));
|
assert.ok(tableNames.has('imm_rollup_state'));
|
||||||
|
|
||||||
const rollupStateRow = db
|
const rollupStateRow = db
|
||||||
.prepare(
|
.prepare('SELECT state_value FROM imm_rollup_state WHERE state_key = ?')
|
||||||
'SELECT state_value FROM imm_rollup_state WHERE state_key = ?',
|
|
||||||
)
|
|
||||||
.get('last_rollup_sample_ms') as {
|
.get('last_rollup_sample_ms') as {
|
||||||
state_value: number;
|
state_value: number;
|
||||||
} | null;
|
} | null;
|
||||||
@@ -188,7 +186,9 @@ testIfSqlite('executeQueuedWrite inserts and upserts word and kanji rows', () =>
|
|||||||
stmts.kanjiUpsertStmt.run('日', 8.0, 11.0);
|
stmts.kanjiUpsertStmt.run('日', 8.0, 11.0);
|
||||||
|
|
||||||
const wordRow = db
|
const wordRow = db
|
||||||
.prepare('SELECT headword, frequency, first_seen, last_seen FROM imm_words WHERE headword = ?')
|
.prepare(
|
||||||
|
'SELECT headword, frequency, first_seen, last_seen FROM imm_words WHERE headword = ?',
|
||||||
|
)
|
||||||
.get('猫') as {
|
.get('猫') as {
|
||||||
headword: string;
|
headword: string;
|
||||||
frequency: number;
|
frequency: number;
|
||||||
|
|||||||
@@ -426,11 +426,7 @@ export function getOrCreateVideoRecord(
|
|||||||
LAST_UPDATE_DATE = ?
|
LAST_UPDATE_DATE = ?
|
||||||
WHERE video_id = ?
|
WHERE video_id = ?
|
||||||
`,
|
`,
|
||||||
).run(
|
).run(details.canonicalTitle || 'unknown', Date.now(), existing.video_id);
|
||||||
details.canonicalTitle || 'unknown',
|
|
||||||
Date.now(),
|
|
||||||
existing.video_id,
|
|
||||||
);
|
|
||||||
return existing.video_id;
|
return existing.video_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,11 @@ interface QueuedKanjiWrite {
|
|||||||
lastSeen: number;
|
lastSeen: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueuedWrite = QueuedTelemetryWrite | QueuedEventWrite | QueuedWordWrite | QueuedKanjiWrite;
|
export type QueuedWrite =
|
||||||
|
| QueuedTelemetryWrite
|
||||||
|
| QueuedEventWrite
|
||||||
|
| QueuedWordWrite
|
||||||
|
| QueuedKanjiWrite;
|
||||||
|
|
||||||
export interface VideoMetadata {
|
export interface VideoMetadata {
|
||||||
sourceType: number;
|
sourceType: number;
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ test('createJlptVocabularyLookup loads JLPT bank entries and resolves known leve
|
|||||||
assert.equal(lookup('猫'), 'N5');
|
assert.equal(lookup('猫'), 'N5');
|
||||||
assert.equal(lookup('犬'), 'N5');
|
assert.equal(lookup('犬'), 'N5');
|
||||||
assert.equal(lookup('鳥'), null);
|
assert.equal(lookup('鳥'), null);
|
||||||
assert.equal(logs.some((entry) => entry.includes('JLPT dictionary loaded from')), true);
|
assert.equal(
|
||||||
|
logs.some((entry) => entry.includes('JLPT dictionary loaded from')),
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('createJlptVocabularyLookup does not require synchronous fs APIs', async () => {
|
test('createJlptVocabularyLookup does not require synchronous fs APIs', async () => {
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ function parseAssStartTimes(content: string): number[] {
|
|||||||
const starts: number[] = [];
|
const starts: number[] = [];
|
||||||
const lines = content.split(/\r?\n/);
|
const lines = content.split(/\r?\n/);
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const match = line.match(/^Dialogue:[^,]*,(\d+:\d{2}:\d{2}\.\d{1,2}),\d+:\d{2}:\d{2}\.\d{1,2},/);
|
const match = line.match(
|
||||||
|
/^Dialogue:[^,]*,(\d+:\d{2}:\d{2}\.\d{1,2}),\d+:\d{2}:\d{2}\.\d{1,2},/,
|
||||||
|
);
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
const [hoursRaw, minutesRaw, secondsRaw] = match[1]!.split(':');
|
const [hoursRaw, minutesRaw, secondsRaw] = match[1]!.split(':');
|
||||||
if (secondsRaw === undefined) continue;
|
if (secondsRaw === undefined) continue;
|
||||||
|
|||||||
@@ -2370,7 +2370,6 @@ test('tokenizeSubtitle keeps frequency enrichment while n+1 is disabled', async
|
|||||||
assert.equal(frequencyCalls, 1);
|
assert.equal(frequencyCalls, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('tokenizeSubtitle excludes default non-independent pos2 from N+1 and frequency annotations', async () => {
|
test('tokenizeSubtitle excludes default non-independent pos2 from N+1 and frequency annotations', async () => {
|
||||||
const result = await tokenizeSubtitle(
|
const result = await tokenizeSubtitle(
|
||||||
'になれば',
|
'になれば',
|
||||||
|
|||||||
@@ -92,13 +92,14 @@ interface TokenizerAnnotationOptions {
|
|||||||
pos2Exclusions: ReadonlySet<string>;
|
pos2Exclusions: ReadonlySet<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let parserEnrichmentWorkerRuntimeModulePromise:
|
let parserEnrichmentWorkerRuntimeModulePromise: Promise<
|
||||||
| Promise<typeof import('./tokenizer/parser-enrichment-worker-runtime')>
|
typeof import('./tokenizer/parser-enrichment-worker-runtime')
|
||||||
| null = null;
|
> | null = null;
|
||||||
let annotationStageModulePromise: Promise<typeof import('./tokenizer/annotation-stage')> | null = null;
|
let annotationStageModulePromise: Promise<typeof import('./tokenizer/annotation-stage')> | null =
|
||||||
let parserEnrichmentFallbackModulePromise:
|
null;
|
||||||
| Promise<typeof import('./tokenizer/parser-enrichment-stage')>
|
let parserEnrichmentFallbackModulePromise: Promise<
|
||||||
| null = null;
|
typeof import('./tokenizer/parser-enrichment-stage')
|
||||||
|
> | null = null;
|
||||||
const DEFAULT_ANNOTATION_POS1_EXCLUSIONS = resolveAnnotationPos1ExclusionSet(
|
const DEFAULT_ANNOTATION_POS1_EXCLUSIONS = resolveAnnotationPos1ExclusionSet(
|
||||||
DEFAULT_ANNOTATION_POS1_EXCLUSION_CONFIG,
|
DEFAULT_ANNOTATION_POS1_EXCLUSION_CONFIG,
|
||||||
);
|
);
|
||||||
@@ -106,7 +107,10 @@ const DEFAULT_ANNOTATION_POS2_EXCLUSIONS = resolveAnnotationPos2ExclusionSet(
|
|||||||
DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG,
|
DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG,
|
||||||
);
|
);
|
||||||
|
|
||||||
function getKnownWordLookup(deps: TokenizerServiceDeps, options: TokenizerAnnotationOptions): (text: string) => boolean {
|
function getKnownWordLookup(
|
||||||
|
deps: TokenizerServiceDeps,
|
||||||
|
options: TokenizerAnnotationOptions,
|
||||||
|
): (text: string) => boolean {
|
||||||
if (!options.nPlusOneEnabled) {
|
if (!options.nPlusOneEnabled) {
|
||||||
return () => false;
|
return () => false;
|
||||||
}
|
}
|
||||||
@@ -126,7 +130,8 @@ async function enrichTokensWithMecabAsync(
|
|||||||
mecabTokens: MergedToken[] | null,
|
mecabTokens: MergedToken[] | null,
|
||||||
): Promise<MergedToken[]> {
|
): Promise<MergedToken[]> {
|
||||||
if (!parserEnrichmentWorkerRuntimeModulePromise) {
|
if (!parserEnrichmentWorkerRuntimeModulePromise) {
|
||||||
parserEnrichmentWorkerRuntimeModulePromise = import('./tokenizer/parser-enrichment-worker-runtime');
|
parserEnrichmentWorkerRuntimeModulePromise =
|
||||||
|
import('./tokenizer/parser-enrichment-worker-runtime');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -185,8 +190,7 @@ export function createTokenizerDepsRuntime(
|
|||||||
getNPlusOneEnabled: options.getNPlusOneEnabled,
|
getNPlusOneEnabled: options.getNPlusOneEnabled,
|
||||||
getJlptEnabled: options.getJlptEnabled,
|
getJlptEnabled: options.getJlptEnabled,
|
||||||
getFrequencyDictionaryEnabled: options.getFrequencyDictionaryEnabled,
|
getFrequencyDictionaryEnabled: options.getFrequencyDictionaryEnabled,
|
||||||
getFrequencyDictionaryMatchMode:
|
getFrequencyDictionaryMatchMode: options.getFrequencyDictionaryMatchMode ?? (() => 'headword'),
|
||||||
options.getFrequencyDictionaryMatchMode ?? (() => 'headword'),
|
|
||||||
getFrequencyRank: options.getFrequencyRank,
|
getFrequencyRank: options.getFrequencyRank,
|
||||||
getMinSentenceWordsForNPlusOne: options.getMinSentenceWordsForNPlusOne ?? (() => 3),
|
getMinSentenceWordsForNPlusOne: options.getMinSentenceWordsForNPlusOne ?? (() => 3),
|
||||||
getYomitanGroupDebugEnabled: options.getYomitanGroupDebugEnabled ?? (() => false),
|
getYomitanGroupDebugEnabled: options.getYomitanGroupDebugEnabled ?? (() => false),
|
||||||
@@ -348,7 +352,8 @@ function buildYomitanFrequencyRankMap(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const dictionaryPriority =
|
const dictionaryPriority =
|
||||||
typeof frequency.dictionaryPriority === 'number' && Number.isFinite(frequency.dictionaryPriority)
|
typeof frequency.dictionaryPriority === 'number' &&
|
||||||
|
Number.isFinite(frequency.dictionaryPriority)
|
||||||
? Math.max(0, Math.floor(frequency.dictionaryPriority))
|
? Math.max(0, Math.floor(frequency.dictionaryPriority))
|
||||||
: Number.MAX_SAFE_INTEGER;
|
: Number.MAX_SAFE_INTEGER;
|
||||||
const current = rankByTerm.get(normalizedTerm);
|
const current = rankByTerm.get(normalizedTerm);
|
||||||
@@ -489,7 +494,11 @@ async function parseWithYomitanInternalParser(
|
|||||||
normalizedSelectedTokens,
|
normalizedSelectedTokens,
|
||||||
frequencyMatchMode,
|
frequencyMatchMode,
|
||||||
);
|
);
|
||||||
const yomitanFrequencies = await requestYomitanTermFrequencies(termReadingList, deps, logger);
|
const yomitanFrequencies = await requestYomitanTermFrequencies(
|
||||||
|
termReadingList,
|
||||||
|
deps,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
return buildYomitanFrequencyRankMap(yomitanFrequencies);
|
return buildYomitanFrequencyRankMap(yomitanFrequencies);
|
||||||
})()
|
})()
|
||||||
: Promise.resolve(new Map<string, number>());
|
: Promise.resolve(new Map<string, number>());
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ test('enrichTokensWithMecabPos1 avoids repeated active-candidate filter scans',
|
|||||||
|
|
||||||
let sentinelFilterCalls = 0;
|
let sentinelFilterCalls = 0;
|
||||||
const originalFilter = Array.prototype.filter;
|
const originalFilter = Array.prototype.filter;
|
||||||
Array.prototype.filter = (function filterWithSentinelCheck(
|
Array.prototype.filter = function filterWithSentinelCheck(
|
||||||
this: unknown[],
|
this: unknown[],
|
||||||
...args: any[]
|
...args: any[]
|
||||||
): any[] {
|
): any[] {
|
||||||
@@ -113,7 +113,7 @@ test('enrichTokensWithMecabPos1 avoids repeated active-candidate filter scans',
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (originalFilter as (...params: any[]) => any[]).apply(this, args);
|
return (originalFilter as (...params: any[]) => any[]).apply(this, args);
|
||||||
}) as typeof Array.prototype.filter;
|
} as typeof Array.prototype.filter;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const enriched = enrichTokensWithMecabPos1(tokens, mecabTokens);
|
const enriched = enrichTokensWithMecabPos1(tokens, mecabTokens);
|
||||||
|
|||||||
@@ -182,7 +182,8 @@ function pickClosestMecabPosMetadataBySurface(
|
|||||||
startDistance < bestSurfaceMatchDistance ||
|
startDistance < bestSurfaceMatchDistance ||
|
||||||
(startDistance === bestSurfaceMatchDistance &&
|
(startDistance === bestSurfaceMatchDistance &&
|
||||||
(endDistance < bestSurfaceMatchEndDistance ||
|
(endDistance < bestSurfaceMatchEndDistance ||
|
||||||
(endDistance === bestSurfaceMatchEndDistance && candidate.index < bestSurfaceMatchIndex)))
|
(endDistance === bestSurfaceMatchEndDistance &&
|
||||||
|
candidate.index < bestSurfaceMatchIndex)))
|
||||||
) {
|
) {
|
||||||
bestSurfaceMatchDistance = startDistance;
|
bestSurfaceMatchDistance = startDistance;
|
||||||
bestSurfaceMatchEndDistance = endDistance;
|
bestSurfaceMatchEndDistance = endDistance;
|
||||||
@@ -199,7 +200,8 @@ function pickClosestMecabPosMetadataBySurface(
|
|||||||
startDistance < bestSurfaceMatchDistance ||
|
startDistance < bestSurfaceMatchDistance ||
|
||||||
(startDistance === bestSurfaceMatchDistance &&
|
(startDistance === bestSurfaceMatchDistance &&
|
||||||
(endDistance < bestSurfaceMatchEndDistance ||
|
(endDistance < bestSurfaceMatchEndDistance ||
|
||||||
(endDistance === bestSurfaceMatchEndDistance && candidate.index < bestSurfaceMatchIndex)))
|
(endDistance === bestSurfaceMatchEndDistance &&
|
||||||
|
candidate.index < bestSurfaceMatchIndex)))
|
||||||
) {
|
) {
|
||||||
bestSurfaceMatchDistance = startDistance;
|
bestSurfaceMatchDistance = startDistance;
|
||||||
bestSurfaceMatchEndDistance = endDistance;
|
bestSurfaceMatchEndDistance = endDistance;
|
||||||
@@ -274,9 +276,15 @@ function pickClosestMecabPosMetadataByOverlap(
|
|||||||
const overlappingTokensByMecabOrder = overlappingTokens
|
const overlappingTokensByMecabOrder = overlappingTokens
|
||||||
.slice()
|
.slice()
|
||||||
.sort((left, right) => left.index - right.index);
|
.sort((left, right) => left.index - right.index);
|
||||||
const overlapPos1 = joinUniqueTags(overlappingTokensByMecabOrder.map((candidate) => candidate.pos1));
|
const overlapPos1 = joinUniqueTags(
|
||||||
const overlapPos2 = joinUniqueTags(overlappingTokensByMecabOrder.map((candidate) => candidate.pos2));
|
overlappingTokensByMecabOrder.map((candidate) => candidate.pos1),
|
||||||
const overlapPos3 = joinUniqueTags(overlappingTokensByMecabOrder.map((candidate) => candidate.pos3));
|
);
|
||||||
|
const overlapPos2 = joinUniqueTags(
|
||||||
|
overlappingTokensByMecabOrder.map((candidate) => candidate.pos2),
|
||||||
|
);
|
||||||
|
const overlapPos3 = joinUniqueTags(
|
||||||
|
overlappingTokensByMecabOrder.map((candidate) => candidate.pos3),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pos1: overlapPos1 ?? bestToken.pos1,
|
pos1: overlapPos1 ?? bestToken.pos1,
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ interface YomitanProfileMetadata {
|
|||||||
|
|
||||||
const DEFAULT_YOMITAN_SCAN_LENGTH = 40;
|
const DEFAULT_YOMITAN_SCAN_LENGTH = 40;
|
||||||
const yomitanProfileMetadataByWindow = new WeakMap<BrowserWindow, YomitanProfileMetadata>();
|
const yomitanProfileMetadataByWindow = new WeakMap<BrowserWindow, YomitanProfileMetadata>();
|
||||||
const yomitanFrequencyCacheByWindow = new WeakMap<BrowserWindow, Map<string, YomitanTermFrequency[]>>();
|
const yomitanFrequencyCacheByWindow = new WeakMap<
|
||||||
|
BrowserWindow,
|
||||||
|
Map<string, YomitanTermFrequency[]>
|
||||||
|
>();
|
||||||
|
|
||||||
function isObject(value: unknown): value is Record<string, unknown> {
|
function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
return Boolean(value && typeof value === 'object');
|
return Boolean(value && typeof value === 'object');
|
||||||
@@ -87,7 +90,7 @@ function parsePositiveFrequencyString(value: string): number | null {
|
|||||||
const chunks = numericPrefix.split(',');
|
const chunks = numericPrefix.split(',');
|
||||||
const normalizedNumber =
|
const normalizedNumber =
|
||||||
chunks.length <= 1
|
chunks.length <= 1
|
||||||
? chunks[0] ?? ''
|
? (chunks[0] ?? '')
|
||||||
: chunks.slice(1).every((chunk) => /^\d{3}$/.test(chunk))
|
: chunks.slice(1).every((chunk) => /^\d{3}$/.test(chunk))
|
||||||
? chunks.join('')
|
? chunks.join('')
|
||||||
: (chunks[0] ?? '');
|
: (chunks[0] ?? '');
|
||||||
@@ -145,11 +148,7 @@ function toYomitanTermFrequency(value: unknown): YomitanTermFrequency | null {
|
|||||||
: Number.MAX_SAFE_INTEGER;
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
const reading =
|
const reading =
|
||||||
value.reading === null
|
value.reading === null ? null : typeof value.reading === 'string' ? value.reading : null;
|
||||||
? null
|
|
||||||
: typeof value.reading === 'string'
|
|
||||||
? value.reading
|
|
||||||
: null;
|
|
||||||
const displayValue = typeof displayValueRaw === 'string' ? displayValueRaw : null;
|
const displayValue = typeof displayValueRaw === 'string' ? displayValueRaw : null;
|
||||||
const displayValueParsed = value.displayValueParsed === true;
|
const displayValueParsed = value.displayValueParsed === true;
|
||||||
|
|
||||||
@@ -164,7 +163,9 @@ function toYomitanTermFrequency(value: unknown): YomitanTermFrequency | null {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTermReadingList(termReadingList: YomitanTermReadingPair[]): YomitanTermReadingPair[] {
|
function normalizeTermReadingList(
|
||||||
|
termReadingList: YomitanTermReadingPair[],
|
||||||
|
): YomitanTermReadingPair[] {
|
||||||
const normalized: YomitanTermReadingPair[] = [];
|
const normalized: YomitanTermReadingPair[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|
||||||
@@ -174,7 +175,9 @@ function normalizeTermReadingList(termReadingList: YomitanTermReadingPair[]): Yo
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const reading =
|
const reading =
|
||||||
typeof pair.reading === 'string' && pair.reading.trim().length > 0 ? pair.reading.trim() : null;
|
typeof pair.reading === 'string' && pair.reading.trim().length > 0
|
||||||
|
? pair.reading.trim()
|
||||||
|
: null;
|
||||||
const key = `${term}\u0000${reading ?? ''}`;
|
const key = `${term}\u0000${reading ?? ''}`;
|
||||||
if (seen.has(key)) {
|
if (seen.has(key)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -298,7 +301,9 @@ function groupFrequencyEntriesByPair(
|
|||||||
const grouped = new Map<string, YomitanTermFrequency[]>();
|
const grouped = new Map<string, YomitanTermFrequency[]>();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const reading =
|
const reading =
|
||||||
typeof entry.reading === 'string' && entry.reading.trim().length > 0 ? entry.reading.trim() : null;
|
typeof entry.reading === 'string' && entry.reading.trim().length > 0
|
||||||
|
? entry.reading.trim()
|
||||||
|
: null;
|
||||||
const key = makeTermReadingCacheKey(entry.term.trim(), reading);
|
const key = makeTermReadingCacheKey(entry.term.trim(), reading);
|
||||||
const existing = grouped.get(key);
|
const existing = grouped.get(key);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -805,7 +810,11 @@ export async function requestYomitanTermFrequencies(
|
|||||||
);
|
);
|
||||||
if (fallbackFetchResult !== null) {
|
if (fallbackFetchResult !== null) {
|
||||||
fallbackFetchedEntries = fallbackFetchResult;
|
fallbackFetchedEntries = fallbackFetchResult;
|
||||||
cacheFrequencyEntriesForPairs(frequencyCache, fallbackTermReadingList, fallbackFetchedEntries);
|
cacheFrequencyEntriesForPairs(
|
||||||
|
frequencyCache,
|
||||||
|
fallbackTermReadingList,
|
||||||
|
fallbackFetchedEntries,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pair of missingTermReadingList) {
|
for (const pair of missingTermReadingList) {
|
||||||
@@ -829,7 +838,9 @@ export async function requestYomitanTermFrequencies(
|
|||||||
[...missingTermReadingList, ...fallbackTermReadingList].map((pair) => pair.term),
|
[...missingTermReadingList, ...fallbackTermReadingList].map((pair) => pair.term),
|
||||||
);
|
);
|
||||||
const cachedResult = buildCachedResult();
|
const cachedResult = buildCachedResult();
|
||||||
const unmatchedEntries = allFetchedEntries.filter((entry) => !queriedTerms.has(entry.term.trim()));
|
const unmatchedEntries = allFetchedEntries.filter(
|
||||||
|
(entry) => !queriedTerms.has(entry.term.trim()),
|
||||||
|
);
|
||||||
return [...cachedResult, ...unmatchedEntries];
|
return [...cachedResult, ...unmatchedEntries];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ test('sanitizeBackgroundEnv marks background child and keeps warning suppression
|
|||||||
|
|
||||||
test('shouldDetachBackgroundLaunch only for first background invocation', () => {
|
test('shouldDetachBackgroundLaunch only for first background invocation', () => {
|
||||||
assert.equal(shouldDetachBackgroundLaunch(['--background'], {}), true);
|
assert.equal(shouldDetachBackgroundLaunch(['--background'], {}), true);
|
||||||
assert.equal(shouldDetachBackgroundLaunch(['--background'], { SUBMINER_BACKGROUND_CHILD: '1' }), false);
|
assert.equal(
|
||||||
assert.equal(shouldDetachBackgroundLaunch(['--background'], { ELECTRON_RUN_AS_NODE: '1' }), false);
|
shouldDetachBackgroundLaunch(['--background'], { SUBMINER_BACKGROUND_CHILD: '1' }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldDetachBackgroundLaunch(['--background'], { ELECTRON_RUN_AS_NODE: '1' }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
assert.equal(shouldDetachBackgroundLaunch(['--start'], {}), false);
|
assert.equal(shouldDetachBackgroundLaunch(['--start'], {}), false);
|
||||||
});
|
});
|
||||||
|
|||||||
12
src/main.ts
12
src/main.ts
@@ -892,7 +892,9 @@ function maybeSignalPluginAutoplayReady(
|
|||||||
if (typeof pauseProperty === 'number') {
|
if (typeof pauseProperty === 'number') {
|
||||||
return pauseProperty !== 0;
|
return pauseProperty !== 0;
|
||||||
}
|
}
|
||||||
logger.debug(`[autoplay-ready] unrecognized pause property for media ${mediaPath}: ${String(pauseProperty)}`);
|
logger.debug(
|
||||||
|
`[autoplay-ready] unrecognized pause property for media ${mediaPath}: ${String(pauseProperty)}`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`[autoplay-ready] failed to read pause property for media ${mediaPath}: ${(error as Error).message}`,
|
`[autoplay-ready] failed to read pause property for media ${mediaPath}: ${(error as Error).message}`,
|
||||||
@@ -1365,7 +1367,10 @@ function shouldInitializeMecabForAnnotations(): boolean {
|
|||||||
'subtitle.annotation.nPlusOne',
|
'subtitle.annotation.nPlusOne',
|
||||||
config.ankiConnect.nPlusOne.highlightEnabled,
|
config.ankiConnect.nPlusOne.highlightEnabled,
|
||||||
);
|
);
|
||||||
const jlptEnabled = getRuntimeBooleanOption('subtitle.annotation.jlpt', config.subtitleStyle.enableJlpt);
|
const jlptEnabled = getRuntimeBooleanOption(
|
||||||
|
'subtitle.annotation.jlpt',
|
||||||
|
config.subtitleStyle.enableJlpt,
|
||||||
|
);
|
||||||
const frequencyEnabled = getRuntimeBooleanOption(
|
const frequencyEnabled = getRuntimeBooleanOption(
|
||||||
'subtitle.annotation.frequency',
|
'subtitle.annotation.frequency',
|
||||||
config.subtitleStyle.frequencyDictionary.enabled,
|
config.subtitleStyle.frequencyDictionary.enabled,
|
||||||
@@ -2992,7 +2997,8 @@ const {
|
|||||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||||
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
||||||
playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient),
|
playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient),
|
||||||
shiftSubDelayToAdjacentSubtitle: (direction) => shiftSubtitleDelayToAdjacentCueHandler(direction),
|
shiftSubDelayToAdjacentSubtitle: (direction) =>
|
||||||
|
shiftSubtitleDelayToAdjacentCueHandler(direction),
|
||||||
sendMpvCommand: (rawCommand: (string | number)[]) =>
|
sendMpvCommand: (rawCommand: (string | number)[]) =>
|
||||||
sendMpvCommandRuntime(appState.mpvClient, rawCommand),
|
sendMpvCommandRuntime(appState.mpvClient, rawCommand),
|
||||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
||||||
|
|||||||
@@ -659,150 +659,144 @@ test('composeMpvRuntimeHandlers does not block first tokenization on dictionary
|
|||||||
await composed.startTokenizationWarmups();
|
await composed.startTokenizationWarmups();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('composeMpvRuntimeHandlers shows annotation loading OSD after tokenization-ready when dictionary warmup is still pending', async () => {
|
||||||
'composeMpvRuntimeHandlers shows annotation loading OSD after tokenization-ready when dictionary warmup is still pending',
|
const jlptDeferred = createDeferred();
|
||||||
async () => {
|
const frequencyDeferred = createDeferred();
|
||||||
const jlptDeferred = createDeferred();
|
const osdMessages: string[] = [];
|
||||||
const frequencyDeferred = createDeferred();
|
|
||||||
const osdMessages: string[] = [];
|
|
||||||
|
|
||||||
const composed = composeMpvRuntimeHandlers<
|
const composed = composeMpvRuntimeHandlers<
|
||||||
{ connect: () => void; on: () => void },
|
{ connect: () => void; on: () => void },
|
||||||
{ onTokenizationReady?: (text: string) => void },
|
{ onTokenizationReady?: (text: string) => void },
|
||||||
{ text: string }
|
{ text: string }
|
||||||
>({
|
>({
|
||||||
bindMpvMainEventHandlersMainDeps: {
|
bindMpvMainEventHandlersMainDeps: {
|
||||||
appState: {
|
appState: {
|
||||||
initialArgs: null,
|
initialArgs: null,
|
||||||
overlayRuntimeInitialized: true,
|
overlayRuntimeInitialized: true,
|
||||||
mpvClient: null,
|
mpvClient: null,
|
||||||
immersionTracker: null,
|
immersionTracker: null,
|
||||||
subtitleTimingTracker: null,
|
subtitleTimingTracker: null,
|
||||||
currentSubText: '',
|
currentSubText: '',
|
||||||
currentSubAssText: '',
|
currentSubAssText: '',
|
||||||
playbackPaused: null,
|
playbackPaused: null,
|
||||||
previousSecondarySubVisibility: null,
|
previousSecondarySubVisibility: null,
|
||||||
},
|
|
||||||
getQuitOnDisconnectArmed: () => false,
|
|
||||||
scheduleQuitCheck: () => {},
|
|
||||||
quitApp: () => {},
|
|
||||||
reportJellyfinRemoteStopped: () => {},
|
|
||||||
syncOverlayMpvSubtitleSuppression: () => {},
|
|
||||||
maybeRunAnilistPostWatchUpdate: async () => {},
|
|
||||||
logSubtitleTimingError: () => {},
|
|
||||||
broadcastToOverlayWindows: () => {},
|
|
||||||
onSubtitleChange: () => {},
|
|
||||||
refreshDiscordPresence: () => {},
|
|
||||||
ensureImmersionTrackerInitialized: () => {},
|
|
||||||
updateCurrentMediaPath: () => {},
|
|
||||||
restoreMpvSubVisibility: () => {},
|
|
||||||
getCurrentAnilistMediaKey: () => null,
|
|
||||||
resetAnilistMediaTracking: () => {},
|
|
||||||
maybeProbeAnilistDuration: () => {},
|
|
||||||
ensureAnilistMediaGuess: () => {},
|
|
||||||
syncImmersionMediaState: () => {},
|
|
||||||
updateCurrentMediaTitle: () => {},
|
|
||||||
resetAnilistMediaGuessState: () => {},
|
|
||||||
reportJellyfinRemoteProgress: () => {},
|
|
||||||
updateSubtitleRenderMetrics: () => {},
|
|
||||||
},
|
},
|
||||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
getQuitOnDisconnectArmed: () => false,
|
||||||
createClient: class {
|
scheduleQuitCheck: () => {},
|
||||||
connect(): void {}
|
quitApp: () => {},
|
||||||
on(): void {}
|
reportJellyfinRemoteStopped: () => {},
|
||||||
},
|
syncOverlayMpvSubtitleSuppression: () => {},
|
||||||
getSocketPath: () => '/tmp/mpv.sock',
|
maybeRunAnilistPostWatchUpdate: async () => {},
|
||||||
getResolvedConfig: () => ({ auto_start_overlay: false }),
|
logSubtitleTimingError: () => {},
|
||||||
isAutoStartOverlayEnabled: () => false,
|
broadcastToOverlayWindows: () => {},
|
||||||
setOverlayVisible: () => {},
|
onSubtitleChange: () => {},
|
||||||
isVisibleOverlayVisible: () => false,
|
refreshDiscordPresence: () => {},
|
||||||
getReconnectTimer: () => null,
|
ensureImmersionTrackerInitialized: () => {},
|
||||||
setReconnectTimer: () => {},
|
updateCurrentMediaPath: () => {},
|
||||||
|
restoreMpvSubVisibility: () => {},
|
||||||
|
getCurrentAnilistMediaKey: () => null,
|
||||||
|
resetAnilistMediaTracking: () => {},
|
||||||
|
maybeProbeAnilistDuration: () => {},
|
||||||
|
ensureAnilistMediaGuess: () => {},
|
||||||
|
syncImmersionMediaState: () => {},
|
||||||
|
updateCurrentMediaTitle: () => {},
|
||||||
|
resetAnilistMediaGuessState: () => {},
|
||||||
|
reportJellyfinRemoteProgress: () => {},
|
||||||
|
updateSubtitleRenderMetrics: () => {},
|
||||||
|
},
|
||||||
|
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||||
|
createClient: class {
|
||||||
|
connect(): void {}
|
||||||
|
on(): void {}
|
||||||
},
|
},
|
||||||
updateMpvSubtitleRenderMetricsMainDeps: {
|
getSocketPath: () => '/tmp/mpv.sock',
|
||||||
getCurrentMetrics: () => BASE_METRICS,
|
getResolvedConfig: () => ({ auto_start_overlay: false }),
|
||||||
setCurrentMetrics: () => {},
|
isAutoStartOverlayEnabled: () => false,
|
||||||
applyPatch: (current, patch) => ({ next: { ...current, ...patch }, changed: true }),
|
setOverlayVisible: () => {},
|
||||||
broadcastMetrics: () => {},
|
isVisibleOverlayVisible: () => false,
|
||||||
|
getReconnectTimer: () => null,
|
||||||
|
setReconnectTimer: () => {},
|
||||||
|
},
|
||||||
|
updateMpvSubtitleRenderMetricsMainDeps: {
|
||||||
|
getCurrentMetrics: () => BASE_METRICS,
|
||||||
|
setCurrentMetrics: () => {},
|
||||||
|
applyPatch: (current, patch) => ({ next: { ...current, ...patch }, changed: true }),
|
||||||
|
broadcastMetrics: () => {},
|
||||||
|
},
|
||||||
|
tokenizer: {
|
||||||
|
buildTokenizerDepsMainDeps: {
|
||||||
|
getYomitanExt: () => null,
|
||||||
|
getYomitanParserWindow: () => null,
|
||||||
|
setYomitanParserWindow: () => {},
|
||||||
|
getYomitanParserReadyPromise: () => null,
|
||||||
|
setYomitanParserReadyPromise: () => {},
|
||||||
|
getYomitanParserInitPromise: () => null,
|
||||||
|
setYomitanParserInitPromise: () => {},
|
||||||
|
isKnownWord: () => false,
|
||||||
|
recordLookup: () => {},
|
||||||
|
getKnownWordMatchMode: () => 'headword',
|
||||||
|
getNPlusOneEnabled: () => false,
|
||||||
|
getMinSentenceWordsForNPlusOne: () => 3,
|
||||||
|
getJlptLevel: () => null,
|
||||||
|
getJlptEnabled: () => true,
|
||||||
|
getFrequencyDictionaryEnabled: () => true,
|
||||||
|
getFrequencyDictionaryMatchMode: () => 'headword',
|
||||||
|
getFrequencyRank: () => null,
|
||||||
|
getYomitanGroupDebugEnabled: () => false,
|
||||||
|
getMecabTokenizer: () => null,
|
||||||
},
|
},
|
||||||
tokenizer: {
|
createTokenizerRuntimeDeps: (deps) =>
|
||||||
buildTokenizerDepsMainDeps: {
|
deps as unknown as { onTokenizationReady?: (text: string) => void },
|
||||||
getYomitanExt: () => null,
|
tokenizeSubtitle: async (text, deps) => {
|
||||||
getYomitanParserWindow: () => null,
|
deps.onTokenizationReady?.(text);
|
||||||
setYomitanParserWindow: () => {},
|
return { text };
|
||||||
getYomitanParserReadyPromise: () => null,
|
},
|
||||||
setYomitanParserReadyPromise: () => {},
|
createMecabTokenizerAndCheckMainDeps: {
|
||||||
getYomitanParserInitPromise: () => null,
|
getMecabTokenizer: () => null,
|
||||||
setYomitanParserInitPromise: () => {},
|
setMecabTokenizer: () => {},
|
||||||
isKnownWord: () => false,
|
createMecabTokenizer: () => ({ id: 'mecab' }),
|
||||||
recordLookup: () => {},
|
checkAvailability: async () => {},
|
||||||
getKnownWordMatchMode: () => 'headword',
|
},
|
||||||
getNPlusOneEnabled: () => false,
|
prewarmSubtitleDictionariesMainDeps: {
|
||||||
getMinSentenceWordsForNPlusOne: () => 3,
|
ensureJlptDictionaryLookup: async () => jlptDeferred.promise,
|
||||||
getJlptLevel: () => null,
|
ensureFrequencyDictionaryLookup: async () => frequencyDeferred.promise,
|
||||||
getJlptEnabled: () => true,
|
showMpvOsd: (message) => {
|
||||||
getFrequencyDictionaryEnabled: () => true,
|
osdMessages.push(message);
|
||||||
getFrequencyDictionaryMatchMode: () => 'headword',
|
|
||||||
getFrequencyRank: () => null,
|
|
||||||
getYomitanGroupDebugEnabled: () => false,
|
|
||||||
getMecabTokenizer: () => null,
|
|
||||||
},
|
|
||||||
createTokenizerRuntimeDeps: (deps) =>
|
|
||||||
deps as unknown as { onTokenizationReady?: (text: string) => void },
|
|
||||||
tokenizeSubtitle: async (text, deps) => {
|
|
||||||
deps.onTokenizationReady?.(text);
|
|
||||||
return { text };
|
|
||||||
},
|
|
||||||
createMecabTokenizerAndCheckMainDeps: {
|
|
||||||
getMecabTokenizer: () => null,
|
|
||||||
setMecabTokenizer: () => {},
|
|
||||||
createMecabTokenizer: () => ({ id: 'mecab' }),
|
|
||||||
checkAvailability: async () => {},
|
|
||||||
},
|
|
||||||
prewarmSubtitleDictionariesMainDeps: {
|
|
||||||
ensureJlptDictionaryLookup: async () => jlptDeferred.promise,
|
|
||||||
ensureFrequencyDictionaryLookup: async () => frequencyDeferred.promise,
|
|
||||||
showMpvOsd: (message) => {
|
|
||||||
osdMessages.push(message);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
warmups: {
|
},
|
||||||
launchBackgroundWarmupTaskMainDeps: {
|
warmups: {
|
||||||
now: () => 0,
|
launchBackgroundWarmupTaskMainDeps: {
|
||||||
logDebug: () => {},
|
now: () => 0,
|
||||||
logWarn: () => {},
|
logDebug: () => {},
|
||||||
},
|
logWarn: () => {},
|
||||||
startBackgroundWarmupsMainDeps: {
|
|
||||||
getStarted: () => false,
|
|
||||||
setStarted: () => {},
|
|
||||||
isTexthookerOnlyMode: () => false,
|
|
||||||
ensureYomitanExtensionLoaded: async () => undefined,
|
|
||||||
shouldWarmupMecab: () => false,
|
|
||||||
shouldWarmupYomitanExtension: () => false,
|
|
||||||
shouldWarmupSubtitleDictionaries: () => false,
|
|
||||||
shouldWarmupJellyfinRemoteSession: () => false,
|
|
||||||
shouldAutoConnectJellyfinRemote: () => false,
|
|
||||||
startJellyfinRemoteSession: async () => {},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
startBackgroundWarmupsMainDeps: {
|
||||||
|
getStarted: () => false,
|
||||||
|
setStarted: () => {},
|
||||||
|
isTexthookerOnlyMode: () => false,
|
||||||
|
ensureYomitanExtensionLoaded: async () => undefined,
|
||||||
|
shouldWarmupMecab: () => false,
|
||||||
|
shouldWarmupYomitanExtension: () => false,
|
||||||
|
shouldWarmupSubtitleDictionaries: () => false,
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => false,
|
||||||
|
shouldAutoConnectJellyfinRemote: () => false,
|
||||||
|
startJellyfinRemoteSession: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const warmupPromise = composed.startTokenizationWarmups();
|
const warmupPromise = composed.startTokenizationWarmups();
|
||||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||||
assert.deepEqual(osdMessages, []);
|
assert.deepEqual(osdMessages, []);
|
||||||
|
|
||||||
await composed.tokenizeSubtitle('first line');
|
await composed.tokenizeSubtitle('first line');
|
||||||
assert.deepEqual(osdMessages, ['Loading subtitle annotations |']);
|
assert.deepEqual(osdMessages, ['Loading subtitle annotations |']);
|
||||||
|
|
||||||
jlptDeferred.resolve();
|
jlptDeferred.resolve();
|
||||||
frequencyDeferred.resolve();
|
frequencyDeferred.resolve();
|
||||||
await warmupPromise;
|
await warmupPromise;
|
||||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
assert.deepEqual(osdMessages, [
|
assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Subtitle annotations loaded']);
|
||||||
'Loading subtitle annotations |',
|
});
|
||||||
'Subtitle annotations loaded',
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(
|
|||||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||||
replayCurrentSubtitle: () => deps.replayCurrentSubtitle(),
|
replayCurrentSubtitle: () => deps.replayCurrentSubtitle(),
|
||||||
playNextSubtitle: () => deps.playNextSubtitle(),
|
playNextSubtitle: () => deps.playNextSubtitle(),
|
||||||
shiftSubDelayToAdjacentSubtitle: (direction) =>
|
shiftSubDelayToAdjacentSubtitle: (direction) => deps.shiftSubDelayToAdjacentSubtitle(direction),
|
||||||
deps.shiftSubDelayToAdjacentSubtitle(direction),
|
|
||||||
sendMpvCommand: (command: (string | number)[]) => deps.sendMpvCommand(command),
|
sendMpvCommand: (command: (string | number)[]) => deps.sendMpvCommand(command),
|
||||||
isMpvConnected: () => deps.isMpvConnected(),
|
isMpvConnected: () => deps.isMpvConnected(),
|
||||||
hasRuntimeOptionsManager: () => deps.hasRuntimeOptionsManager(),
|
hasRuntimeOptionsManager: () => deps.hasRuntimeOptionsManager(),
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function createHandleJellyfinListCommands(deps: {
|
|||||||
isForced?: boolean;
|
isForced?: boolean;
|
||||||
isExternal?: boolean;
|
isExternal?: boolean;
|
||||||
deliveryUrl?: string | null;
|
deliveryUrl?: string | null;
|
||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
writeJellyfinPreviewAuth: (responsePath: string, payload: JellyfinPreviewAuthPayload) => void;
|
writeJellyfinPreviewAuth: (responsePath: string, payload: JellyfinPreviewAuthPayload) => void;
|
||||||
logInfo: (message: string) => void;
|
logInfo: (message: string) => void;
|
||||||
|
|||||||
@@ -167,28 +167,22 @@ test('dictionary prewarm can show OSD while awaiting background-started load', a
|
|||||||
assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Subtitle annotations loaded']);
|
assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Subtitle annotations loaded']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('dictionary prewarm shows OSD when loading indicator is requested even if notification predicate is disabled', async () => {
|
||||||
'dictionary prewarm shows OSD when loading indicator is requested even if notification predicate is disabled',
|
const osdMessages: string[] = [];
|
||||||
async () => {
|
|
||||||
const osdMessages: string[] = [];
|
|
||||||
|
|
||||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||||
ensureJlptDictionaryLookup: async () => undefined,
|
ensureJlptDictionaryLookup: async () => undefined,
|
||||||
ensureFrequencyDictionaryLookup: async () => undefined,
|
ensureFrequencyDictionaryLookup: async () => undefined,
|
||||||
shouldShowOsdNotification: () => false,
|
shouldShowOsdNotification: () => false,
|
||||||
showMpvOsd: (message) => {
|
showMpvOsd: (message) => {
|
||||||
osdMessages.push(message);
|
osdMessages.push(message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prewarm({ showLoadingOsd: true });
|
await prewarm({ showLoadingOsd: true });
|
||||||
|
|
||||||
assert.deepEqual(osdMessages, [
|
assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Subtitle annotations loaded']);
|
||||||
'Loading subtitle annotations |',
|
});
|
||||||
'Subtitle annotations loaded',
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test('dictionary prewarm clears loading OSD timer even if notifications are disabled before completion', async () => {
|
test('dictionary prewarm clears loading OSD timer even if notifications are disabled before completion', async () => {
|
||||||
const clearedTimers: unknown[] = [];
|
const clearedTimers: unknown[] = [];
|
||||||
|
|||||||
@@ -139,7 +139,8 @@ export class MecabTokenizer {
|
|||||||
);
|
);
|
||||||
this.spawnFn = options.spawnFn ?? childProcess.spawn;
|
this.spawnFn = options.spawnFn ?? childProcess.spawn;
|
||||||
this.execSyncFn = options.execSyncFn ?? childProcess.execSync;
|
this.execSyncFn = options.execSyncFn ?? childProcess.execSync;
|
||||||
this.setTimeoutFn = options.setTimeoutFn ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
this.setTimeoutFn =
|
||||||
|
options.setTimeoutFn ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
||||||
this.clearTimeoutFn = options.clearTimeoutFn ?? ((timer) => clearTimeout(timer));
|
this.clearTimeoutFn = options.clearTimeoutFn ?? ((timer) => clearTimeout(timer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,11 +218,7 @@ export class MecabTokenizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private retryOrResolveRequest(request: MecabQueuedRequest): void {
|
private retryOrResolveRequest(request: MecabQueuedRequest): void {
|
||||||
if (
|
if (request.retryCount < MecabTokenizer.MAX_RETRY_COUNT && this.enabled && this.available) {
|
||||||
request.retryCount < MecabTokenizer.MAX_RETRY_COUNT &&
|
|
||||||
this.enabled &&
|
|
||||||
this.available
|
|
||||||
) {
|
|
||||||
this.requestQueue.push({
|
this.requestQueue.push({
|
||||||
...request,
|
...request,
|
||||||
retryCount: request.retryCount + 1,
|
retryCount: request.retryCount + 1,
|
||||||
@@ -368,7 +365,9 @@ export class MecabTokenizer {
|
|||||||
this.requestQueue = [];
|
this.requestQueue = [];
|
||||||
|
|
||||||
if (pending.length > 0) {
|
if (pending.length > 0) {
|
||||||
log.warn(`MeCab parser process ended during active work (${reason}); retrying pending request(s).`);
|
log.warn(
|
||||||
|
`MeCab parser process ended during active work (${reason}); retrying pending request(s).`,
|
||||||
|
);
|
||||||
for (const request of pending) {
|
for (const request of pending) {
|
||||||
this.retryOrResolveRequest(request);
|
this.retryOrResolveRequest(request);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user