From be4db248614d30eb2267e351d5eff2396579ce70 Mon Sep 17 00:00:00 2001 From: sudacode Date: Mon, 2 Mar 2026 02:45:51 -0800 Subject: [PATCH] make pretty --- ...-auto-pause-config-and-runtime-behavior.md | 1 + ...ause-until-ready-and-single-start-guard.md | 2 + ...to-close-after-successful-subtitle-load.md | 2 + ...-rename-subtitle-to-match-current-video.md | 2 + ...omitan-mecab-and-persistent-local-mecab.md | 2 + ...readings-and-known-token-color-priority.md | 4 + ...hift-to-adjacent-cue-without-seek-jumps.md | 2 + docs/configuration.md | 41 +-- docs/launcher-script.md | 24 +- docs/mpv-plugin.md | 42 +-- docs/plausible.test.ts | 2 +- docs/shortcuts.md | 4 +- launcher/commands/playback-command.ts | 12 +- launcher/config/plugin-runtime-config.ts | 5 +- launcher/jellyfin.ts | 15 +- launcher/main.test.ts | 20 +- src/anki-integration.ts | 3 +- src/cli/args.test.ts | 12 +- src/cli/args.ts | 4 +- src/config/config.test.ts | 5 +- src/config/resolve/subtitle-domains.ts | 6 +- .../services/immersion-tracker/maintenance.ts | 42 +-- .../immersion-tracker/reducer.test.ts | 5 +- .../services/immersion-tracker/reducer.ts | 3 +- .../services/immersion-tracker/session.ts | 11 +- .../immersion-tracker/storage-session.test.ts | 8 +- .../services/immersion-tracker/storage.ts | 6 +- src/core/services/immersion-tracker/types.ts | 6 +- src/core/services/jlpt-vocab.test.ts | 5 +- src/core/services/subtitle-delay-shift.ts | 4 +- src/core/services/tokenizer.test.ts | 1 - src/core/services/tokenizer.ts | 35 ++- .../tokenizer/parser-enrichment-stage.test.ts | 4 +- .../tokenizer/parser-enrichment-stage.ts | 18 +- .../tokenizer/yomitan-parser-runtime.ts | 35 ++- src/main-entry-runtime.test.ts | 10 +- src/main.ts | 12 +- .../composers/mpv-runtime-composer.test.ts | 266 +++++++++--------- src/main/runtime/ipc-mpv-command-main-deps.ts | 3 +- src/main/runtime/jellyfin-cli-list.ts | 2 +- .../subtitle-tokenization-main-deps.test.ts | 32 +-- src/mecab-tokenizer.ts | 13 +- 42 files changed, 395 insertions(+), 336 deletions(-) diff --git a/backlog/tasks/task-77 - Subtitle-hover-auto-pause-config-and-runtime-behavior.md b/backlog/tasks/task-77 - Subtitle-hover-auto-pause-config-and-runtime-behavior.md index 44ee291..f725a4b 100644 --- a/backlog/tasks/task-77 - Subtitle-hover-auto-pause-config-and-runtime-behavior.md +++ b/backlog/tasks/task-77 - Subtitle-hover-auto-pause-config-and-runtime-behavior.md @@ -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. Scope: + - New config key: `subtitleStyle.autoPauseVideoOnHover`. - Default should be enabled. - Hover pause/resume must not unpause if playback was already paused before hover. diff --git a/backlog/tasks/task-78 - Launcher-and-mpv-plugin-auto-start-pause-until-ready-and-single-start-guard.md b/backlog/tasks/task-78 - Launcher-and-mpv-plugin-auto-start-pause-until-ready-and-single-start-guard.md index ffbf98a..073f175 100644 --- a/backlog/tasks/task-78 - Launcher-and-mpv-plugin-auto-start-pause-until-ready-and-single-start-guard.md +++ b/backlog/tasks/task-78 - Launcher-and-mpv-plugin-auto-start-pause-until-ready-and-single-start-guard.md @@ -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. Scope: + - 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`. - Main process signals readiness via mpv script message after tokenized subtitle delivery. @@ -43,6 +44,7 @@ Scope: 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 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. diff --git a/backlog/tasks/task-79 - Jimaku-modal-auto-close-after-successful-subtitle-load.md b/backlog/tasks/task-79 - Jimaku-modal-auto-close-after-successful-subtitle-load.md index 8eee468..7a9f1c8 100644 --- a/backlog/tasks/task-79 - Jimaku-modal-auto-close-after-successful-subtitle-load.md +++ b/backlog/tasks/task-79 - Jimaku-modal-auto-close-after-successful-subtitle-load.md @@ -18,10 +18,12 @@ ordinal: 10000 Fix Jimaku modal UX so selecting a subtitle file closes the modal automatically once subtitle download+load succeeds. Current behavior: + - Subtitle file downloads and loads into mpv. - Jimaku modal remains open until manual close. Expected behavior: + - On successful `jimakuDownloadFile` result, close modal immediately. - Keep error behavior unchanged (stay open + show error). diff --git a/backlog/tasks/task-80 - Jimaku-download-rename-subtitle-to-match-current-video.md b/backlog/tasks/task-80 - Jimaku-download-rename-subtitle-to-match-current-video.md index ab3e5fe..5d7c1fa 100644 --- a/backlog/tasks/task-80 - Jimaku-download-rename-subtitle-to-match-current-video.md +++ b/backlog/tasks/task-80 - Jimaku-download-rename-subtitle-to-match-current-video.md @@ -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. Example: + - Current media: `anime.mkv` - Downloaded subtitle extension: `.srt` - Saved subtitle path: `anime.ja.srt` Scope: + - Apply in Jimaku download IPC path before writing file. - Preserve collision-avoidance behavior (suffix with jimaku entry id/counter when target exists). - Keep mpv load flow unchanged except using renamed path. diff --git a/backlog/tasks/task-81 - Tokenization-performance-disable-yomitan-mecab-and-persistent-local-mecab.md b/backlog/tasks/task-81 - Tokenization-performance-disable-yomitan-mecab-and-persistent-local-mecab.md index 8a38fa7..57367fa 100644 --- a/backlog/tasks/task-81 - Tokenization-performance-disable-yomitan-mecab-and-persistent-local-mecab.md +++ b/backlog/tasks/task-81 - Tokenization-performance-disable-yomitan-mecab-and-persistent-local-mecab.md @@ -16,6 +16,7 @@ ordinal: 9001 Reduce subtitle annotation latency by: + - disabling Yomitan-side MeCab parser requests (`useMecabParser=false`); - 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. @@ -39,6 +40,7 @@ Reduce subtitle annotation latency by: Implemented tokenizer latency optimizations: + - switched Yomitan parse requests to `useMecabParser: false`; - 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; diff --git a/backlog/tasks/task-82 - Subtitle-frequency-highlighting-fixes-for-noisy-readings-and-known-token-color-priority.md b/backlog/tasks/task-82 - Subtitle-frequency-highlighting-fixes-for-noisy-readings-and-known-token-color-priority.md index a91ef20..d7b43b9 100644 --- a/backlog/tasks/task-82 - Subtitle-frequency-highlighting-fixes-for-noisy-readings-and-known-token-color-priority.md +++ b/backlog/tasks/task-82 - Subtitle-frequency-highlighting-fixes-for-noisy-readings-and-known-token-color-priority.md @@ -16,10 +16,12 @@ ordinal: 9002 Address frequency-highlighting regressions: + - 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. Expected behavior: + - known/N+1 color always wins; - if token is frequent and within `topX`, frequency rank label can still appear on hover/metadata. @@ -42,6 +44,7 @@ Expected behavior: Implemented and validated: + - 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; - 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. Related commits: + - `17a417e` (`fix(subtitle): improve frequency highlight reliability`) - `79f37f3` (`fix(subtitle): prioritize known and n+1 colors over frequency`) diff --git a/backlog/tasks/task-83 - Jellyfin-subtitle-delay-shift-to-adjacent-cue-without-seek-jumps.md b/backlog/tasks/task-83 - Jellyfin-subtitle-delay-shift-to-adjacent-cue-without-seek-jumps.md index 9da9d9b..66e7d49 100644 --- a/backlog/tasks/task-83 - Jellyfin-subtitle-delay-shift-to-adjacent-cue-without-seek-jumps.md +++ b/backlog/tasks/task-83 - Jellyfin-subtitle-delay-shift-to-adjacent-cue-without-seek-jumps.md @@ -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). Scope: + - add special commands for next/previous line alignment; - compute delta from active subtitle cue timeline (external subtitle file/URL, including Jellyfin-delivered URLs); - apply `add sub-delay ` and show OSD value; @@ -42,6 +43,7 @@ Scope: Implemented no-jump subtitle-delay alignment 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`; - wired command handling through IPC runtime deps into main runtime; diff --git a/docs/configuration.md b/docs/configuration.md index 162e862..4caca0d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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"`) | ### 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)"`) | | `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. | -| `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) | | `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) | @@ -322,6 +322,7 @@ Set the initial vertical subtitle position (measured from the bottom of the scre | Option | Values | Description | | ---------- | ---------------- | ---------------------------------------------------------------------- | | `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. ### Secondary Subtitles @@ -364,23 +365,23 @@ See `config.example.jsonc` for detailed configuration options and more examples. **Default keybindings:** -| Key | Command | Description | -| ----------------- | ---------------------------- | ------------------------------------- | -| `Space` | `["cycle", "pause"]` | Toggle pause | -| `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track | -| `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track | -| `ArrowRight` | `["seek", 5]` | Seek forward 5 seconds | -| `ArrowLeft` | `["seek", -5]` | Seek backward 5 seconds | -| `ArrowUp` | `["seek", 60]` | Seek forward 60 seconds | -| `ArrowDown` | `["seek", -60]` | Seek backward 60 seconds | -| `Shift+KeyH` | `["sub-seek", -1]` | Jump to previous subtitle | -| `Shift+KeyL` | `["sub-seek", 1]` | Jump to next subtitle | -| `Shift+BracketLeft` | `["__sub-delay-prev-line"]` | Shift subtitle delay to previous 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+KeyL` | `["__play-next-subtitle"]` | Play next subtitle, pause at end | -| `KeyQ` | `["quit"]` | Quit mpv | -| `Ctrl+KeyW` | `["quit"]` | Quit mpv | +| Key | Command | Description | +| -------------------- | ---------------------------- | ------------------------------------- | +| `Space` | `["cycle", "pause"]` | Toggle pause | +| `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track | +| `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track | +| `ArrowRight` | `["seek", 5]` | Seek forward 5 seconds | +| `ArrowLeft` | `["seek", -5]` | Seek backward 5 seconds | +| `ArrowUp` | `["seek", 60]` | Seek forward 60 seconds | +| `ArrowDown` | `["seek", -60]` | Seek backward 60 seconds | +| `Shift+KeyH` | `["sub-seek", -1]` | Jump to previous subtitle | +| `Shift+KeyL` | `["sub-seek", 1]` | Jump to next subtitle | +| `Shift+BracketLeft` | `["__sub-delay-prev-line"]` | Shift subtitle delay to previous 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+KeyL` | `["__play-next-subtitle"]` | Play next subtitle, pause at end | +| `KeyQ` | `["quit"]` | Quit mpv | +| `Ctrl+KeyW` | `["quit"]` | Quit mpv | **Custom keybindings example:** diff --git a/docs/launcher-script.md b/docs/launcher-script.md index c68f30b..0736c39 100644 --- a/docs/launcher-script.md +++ b/docs/launcher-script.md @@ -79,18 +79,18 @@ Use `subminer -h` for command-specific help. ## Options -| Flag | Description | -| ----------------------- | --------------------------------------------------- | -| `-d, --directory` | Video search directory (default: cwd) | -| `-r, --recursive` | Search directories recursively | -| `-R, --rofi` | Use rofi instead of fzf | -| `--start` | Explicitly start overlay after mpv launches | -| `-S, --start-overlay` | Explicitly start overlay after mpv launches | -| `-T, --no-texthooker` | Disable texthooker server | -| `-p, --profile` | mpv profile name (default: `subminer`) | -| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`) | -| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) | -| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) | +| Flag | Description | +| --------------------- | --------------------------------------------------- | +| `-d, --directory` | Video search directory (default: cwd) | +| `-r, --recursive` | Search directories recursively | +| `-R, --rofi` | Use rofi instead of fzf | +| `--start` | Explicitly start overlay after mpv launches | +| `-S, --start-overlay` | Explicitly start overlay after mpv launches | +| `-T, --no-texthooker` | Disable texthooker server | +| `-p, --profile` | mpv profile name (default: `subminer`) | +| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`) | +| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) | +| `--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. diff --git a/docs/mpv-plugin.md b/docs/mpv-plugin.md index 2ccf270..4829d26 100644 --- a/docs/mpv-plugin.md +++ b/docs/mpv-plugin.md @@ -120,27 +120,27 @@ aniskip_button_duration=3 ### Option Reference -| Option | Default | Values | Description | -| ---------------------------- | ----------------------------- | ------------------------------------------ | ---------------------------------------------------------------------- | -| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary | -| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path | -| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server | -| `texthooker_port` | `5174` | 1–65535 | Texthooker server port | -| `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_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 | -| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages | -| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity | -| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection | -| `aniskip_title` | `""` | string | Override title used for lookup | -| `aniskip_season` | `""` | numeric season | Optional season hint | -| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id | -| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed | -| `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_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 | +| Option | Default | Values | Description | +| ------------------------------ | ----------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------- | +| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary | +| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path | +| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server | +| `texthooker_port` | `5174` | 1–65535 | Texthooker server port | +| `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_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 | +| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages | +| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity | +| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection | +| `aniskip_title` | `""` | string | Override title used for lookup | +| `aniskip_season` | `""` | numeric season | Optional season hint | +| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id | +| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed | +| `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_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 | ## Binary Auto-Detection diff --git a/docs/plausible.test.ts b/docs/plausible.test.ts index f9b7915..cac80ce 100644 --- a/docs/plausible.test.ts +++ b/docs/plausible.test.ts @@ -5,7 +5,7 @@ const docsThemePath = new URL('./.vitepress/theme/index.ts', import.meta.url); const docsThemeContents = readFileSync(docsThemePath, 'utf8'); 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("domain: 'subminer.moe'"); expect(docsThemeContents).toContain("endpoint: 'https://worker.subminer.moe'"); diff --git a/docs/shortcuts.md b/docs/shortcuts.md index 9f22b11..79ba27c 100644 --- a/docs/shortcuts.md +++ b/docs/shortcuts.md @@ -46,8 +46,8 @@ These control playback and subtitle display. They require overlay window focus. | `ArrowDown` | Seek backward 60 seconds | | `Shift+H` | Jump to previous subtitle | | `Shift+L` | Jump to next subtitle | -| `Shift+[` | Shift subtitle delay to previous subtitle cue | -| `Shift+]` | Shift subtitle delay to next subtitle cue | +| `Shift+[` | Shift subtitle delay to previous subtitle cue | +| `Shift+]` | Shift subtitle delay to next subtitle cue | | `Ctrl+Shift+H` | Replay current subtitle (play to end, then pause) | | `Ctrl+Shift+L` | Play next subtitle (jump, play to end, then pause) | | `Q` | Quit mpv | diff --git a/launcher/commands/playback-command.ts b/launcher/commands/playback-command.ts index a220d8b..d395365 100644 --- a/launcher/commands/playback-command.ts +++ b/launcher/commands/playback-command.ts @@ -143,11 +143,7 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi pluginRuntimeConfig.autoStartPauseUntilReady; if (shouldPauseUntilOverlayReady) { - log( - 'info', - args.logLevel, - 'Configured to pause mpv until overlay and tokenization are ready', - ); + log('info', args.logLevel, 'Configured to pause mpv until overlay and tokenization are ready'); } startMpv( @@ -198,11 +194,7 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi if (ready) { log('info', args.logLevel, 'MPV IPC socket ready, relying on mpv plugin auto-start'); } else { - log( - 'info', - args.logLevel, - 'MPV IPC socket not ready yet, relying on mpv plugin auto-start', - ); + log('info', args.logLevel, 'MPV IPC socket not ready yet, relying on mpv plugin auto-start'); } } else if (ready) { log( diff --git a/launcher/config/plugin-runtime-config.ts b/launcher/config/plugin-runtime-config.ts index 1ee0a53..dc7f208 100644 --- a/launcher/config/plugin-runtime-config.ts +++ b/launcher/config/plugin-runtime-config.ts @@ -52,7 +52,10 @@ export function parsePluginRuntimeConfigContent( continue; } if (key === 'auto_start_visible_overlay') { - runtimeConfig.autoStartVisibleOverlay = parseBooleanValue('auto_start_visible_overlay', value); + runtimeConfig.autoStartVisibleOverlay = parseBooleanValue( + 'auto_start_visible_overlay', + value, + ); continue; } if (key === 'auto_start_pause_until_ready') { diff --git a/launcher/jellyfin.ts b/launcher/jellyfin.ts index 1bcedce..dab77a1 100644 --- a/launcher/jellyfin.ts +++ b/launcher/jellyfin.ts @@ -239,8 +239,7 @@ export function parseJellyfinPreviewAuthResponse(raw: string): JellyfinPreviewAu const serverUrl = sanitizeServerUrl( typeof candidate.serverUrl === 'string' ? candidate.serverUrl : '', ); - const accessToken = - typeof candidate.accessToken === 'string' ? candidate.accessToken.trim() : ''; + const accessToken = typeof candidate.accessToken === 'string' ? candidate.accessToken.trim() : ''; const userId = typeof candidate.userId === 'string' ? candidate.userId.trim() : ''; if (!serverUrl || !accessToken) return null; @@ -271,9 +270,7 @@ export function readUtf8FileAppendedSince(logPath: string, offsetBytes: number): const buffer = fs.readFileSync(logPath); if (buffer.length === 0) return ''; const normalizedOffset = - Number.isFinite(offsetBytes) && offsetBytes >= 0 - ? Math.floor(offsetBytes) - : 0; + Number.isFinite(offsetBytes) && offsetBytes >= 0 ? Math.floor(offsetBytes) : 0; const startOffset = normalizedOffset > buffer.length ? 0 : normalizedOffset; return buffer.subarray(startOffset).toString('utf8'); } catch { @@ -399,7 +396,9 @@ async function runAppJellyfinCommand( const hasCommandSignal = (output: string): boolean => { 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') { return ( @@ -550,7 +549,9 @@ async function resolveJellyfinSelectionViaApp( } const configuredDefaultLibraryId = session.defaultLibraryId; - const hasConfiguredDefault = libraries.some((library) => library.id === configuredDefaultLibraryId); + const hasConfiguredDefault = libraries.some( + (library) => library.id === configuredDefaultLibraryId, + ); let libraryId = hasConfiguredDefault ? configuredDefaultLibraryId : ''; if (!libraryId) { libraryId = pickLibrary( diff --git a/launcher/main.test.ts b/launcher/main.test.ts index 513c043..cb6b8bc 100644 --- a/launcher/main.test.ts +++ b/launcher/main.test.ts @@ -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."} `); - 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', () => { @@ -385,7 +388,9 @@ test('shouldRetryWithStartForNoRunningInstance matches expected app lifecycle er true, ); assert.equal( - shouldRetryWithStartForNoRunningInstance('Missing Jellyfin session. Run --jellyfin-login first.'), + shouldRetryWithStartForNoRunningInstance( + 'Missing Jellyfin session. Run --jellyfin-login first.', + ), 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', () => { - assert.deepEqual(parseEpisodePathFromDisplay('KONOSUBA S01E03 A Panty Treasure in This Right Hand!'), { - seriesName: 'KONOSUBA', - seasonNumber: 1, - }); + assert.deepEqual( + parseEpisodePathFromDisplay('KONOSUBA S01E03 A Panty Treasure in This Right Hand!'), + { + seriesName: 'KONOSUBA', + seasonNumber: 1, + }, + ); assert.deepEqual(parseEpisodePathFromDisplay('Frieren S2E10 Something'), { seriesName: 'Frieren', seasonNumber: 2, diff --git a/src/anki-integration.ts b/src/anki-integration.ts index 9cf0da8..2c3ae91 100644 --- a/src/anki-integration.ts +++ b/src/anki-integration.ts @@ -86,8 +86,7 @@ function extractFilenameFromMediaPath(rawPath: string): string { } const separatorIndex = trimmedPath.search(/[?#]/); - const pathWithoutQuery = - separatorIndex >= 0 ? trimmedPath.slice(0, separatorIndex) : trimmedPath; + const pathWithoutQuery = separatorIndex >= 0 ? trimmedPath.slice(0, separatorIndex) : trimmedPath; return decodeURIComponentSafe(path.basename(pathWithoutQuery)); } diff --git a/src/cli/args.test.ts b/src/cli/args.test.ts index 2351b4c..d7dcd9a 100644 --- a/src/cli/args.test.ts +++ b/src/cli/args.test.ts @@ -1,6 +1,11 @@ import test from 'node:test'; 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', () => { const args = parseArgs([ @@ -148,10 +153,7 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => { '/tmp/subminer-jf-response.json', ]); assert.equal(jellyfinPreviewAuth.jellyfinPreviewAuth, true); - assert.equal( - jellyfinPreviewAuth.jellyfinResponsePath, - '/tmp/subminer-jf-response.json', - ); + assert.equal(jellyfinPreviewAuth.jellyfinResponsePath, '/tmp/subminer-jf-response.json'); assert.equal(hasExplicitCommand(jellyfinPreviewAuth), true); assert.equal(shouldStartApp(jellyfinPreviewAuth), false); diff --git a/src/cli/args.ts b/src/cli/args.ts index 4ecd900..c4b3b9a 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -240,7 +240,9 @@ export function parseArgs(argv: string[]): CliArgs { if (value === 'true' || value === '1' || value === 'yes') args.jellyfinRecursive = true; if (value === 'false' || value === '0' || value === 'no') args.jellyfinRecursive = false; } 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') { args.jellyfinRecursive = false; } else if (value === 'true' || value === '1' || value === 'yes') { diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 88b2bcb..f744a9a 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -47,7 +47,10 @@ test('loads defaults when config is missing', () => { assert.equal(config.subtitleStyle.textRendering, 'geometricPrecision'); 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.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.immersionTracking.enabled, true); assert.equal(config.immersionTracking.dbPath, ''); diff --git a/src/config/resolve/subtitle-domains.ts b/src/config/resolve/subtitle-domains.ts index e65a2d4..478b091 100644 --- a/src/config/resolve/subtitle-domains.ts +++ b/src/config/resolve/subtitle-domains.ts @@ -99,8 +99,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { if (isObject(src.subtitleStyle)) { const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt; const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks; - const fallbackSubtitleStyleAutoPauseVideoOnHover = - resolved.subtitleStyle.autoPauseVideoOnHover; + const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover; const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor; const fallbackSubtitleStyleHoverTokenBackgroundColor = resolved.subtitleStyle.hoverTokenBackgroundColor; @@ -161,8 +160,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { if (autoPauseVideoOnHover !== undefined) { resolved.subtitleStyle.autoPauseVideoOnHover = autoPauseVideoOnHover; } else if ( - (src.subtitleStyle as { autoPauseVideoOnHover?: unknown }).autoPauseVideoOnHover !== - undefined + (src.subtitleStyle as { autoPauseVideoOnHover?: unknown }).autoPauseVideoOnHover !== undefined ) { resolved.subtitleStyle.autoPauseVideoOnHover = fallbackSubtitleStyleAutoPauseVideoOnHover; warn( diff --git a/src/core/services/immersion-tracker/maintenance.ts b/src/core/services/immersion-tracker/maintenance.ts index 8d9eee8..aa32ab3 100644 --- a/src/core/services/immersion-tracker/maintenance.ts +++ b/src/core/services/immersion-tracker/maintenance.ts @@ -46,23 +46,31 @@ export function pruneRetention( const dayCutoff = nowMs - policy.dailyRollupRetentionMs; const monthCutoff = nowMs - policy.monthlyRollupRetentionMs; - const deletedSessionEvents = (db - .prepare(`DELETE FROM imm_session_events WHERE ts_ms < ?`) - .run(eventCutoff) as { changes: number }).changes; - const deletedTelemetryRows = (db - .prepare(`DELETE FROM imm_session_telemetry WHERE sample_ms < ?`) - .run(telemetryCutoff) as { changes: number }).changes; - const deletedDailyRows = (db - .prepare(`DELETE FROM imm_daily_rollups WHERE rollup_day < ?`) - .run(Math.floor(dayCutoff / DAILY_MS)) as { changes: number }).changes; - const deletedMonthlyRows = (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; + const deletedSessionEvents = ( + db.prepare(`DELETE FROM imm_session_events WHERE ts_ms < ?`).run(eventCutoff) as { + changes: number; + } + ).changes; + const deletedTelemetryRows = ( + db.prepare(`DELETE FROM imm_session_telemetry WHERE sample_ms < ?`).run(telemetryCutoff) as { + changes: number; + } + ).changes; + const deletedDailyRows = ( + db + .prepare(`DELETE FROM imm_daily_rollups WHERE rollup_day < ?`) + .run(Math.floor(dayCutoff / DAILY_MS)) as { changes: number } + ).changes; + const deletedMonthlyRows = ( + 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 { deletedSessionEvents, diff --git a/src/core/services/immersion-tracker/reducer.test.ts b/src/core/services/immersion-tracker/reducer.test.ts index 27949ad..f89a925 100644 --- a/src/core/services/immersion-tracker/reducer.test.ts +++ b/src/core/services/immersion-tracker/reducer.test.ts @@ -17,6 +17,9 @@ test('extractLineVocabulary returns words and unique kanji', () => { new Set(result.words.map((entry) => `${entry.headword}/${entry.word}`)), 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(['你', '好', '猫'])); }); diff --git a/src/core/services/immersion-tracker/reducer.ts b/src/core/services/immersion-tracker/reducer.ts index 2b6a90d..ae1a43f 100644 --- a/src/core/services/immersion-tracker/reducer.ts +++ b/src/core/services/immersion-tracker/reducer.ts @@ -97,7 +97,8 @@ export function extractLineVocabulary(value: string): ExtractedLineVocabulary { if (!cleaned) return { words: [], kanji: [] }; const wordSet = new Set(); - 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) ?? []; for (const rawWord of rawWords) { const normalizedWord = normalizeText(rawWord.toLowerCase()); diff --git a/src/core/services/immersion-tracker/session.ts b/src/core/services/immersion-tracker/session.ts index 4d717b1..be8d660 100644 --- a/src/core/services/immersion-tracker/session.ts +++ b/src/core/services/immersion-tracker/session.ts @@ -19,15 +19,8 @@ export function startSessionRecord( CREATED_DATE, LAST_UPDATE_DATE ) VALUES (?, ?, ?, ?, ?, ?) `, - ) - .run( - sessionUuid, - videoId, - startedAtMs, - SESSION_STATUS_ACTIVE, - startedAtMs, - nowMs, - ); + ) + .run(sessionUuid, videoId, startedAtMs, SESSION_STATUS_ACTIVE, startedAtMs, nowMs); const sessionId = Number(result.lastInsertRowid); return { sessionId, diff --git a/src/core/services/immersion-tracker/storage-session.test.ts b/src/core/services/immersion-tracker/storage-session.test.ts index 8243c8d..66d2d18 100644 --- a/src/core/services/immersion-tracker/storage-session.test.ts +++ b/src/core/services/immersion-tracker/storage-session.test.ts @@ -59,9 +59,7 @@ testIfSqlite('ensureSchema creates immersion core tables', () => { assert.ok(tableNames.has('imm_rollup_state')); const rollupStateRow = db - .prepare( - 'SELECT state_value FROM imm_rollup_state WHERE state_key = ?', - ) + .prepare('SELECT state_value FROM imm_rollup_state WHERE state_key = ?') .get('last_rollup_sample_ms') as { state_value: number; } | null; @@ -188,7 +186,9 @@ testIfSqlite('executeQueuedWrite inserts and upserts word and kanji rows', () => stmts.kanjiUpsertStmt.run('日', 8.0, 11.0); 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 { headword: string; frequency: number; diff --git a/src/core/services/immersion-tracker/storage.ts b/src/core/services/immersion-tracker/storage.ts index 1266e23..3066aa5 100644 --- a/src/core/services/immersion-tracker/storage.ts +++ b/src/core/services/immersion-tracker/storage.ts @@ -426,11 +426,7 @@ export function getOrCreateVideoRecord( LAST_UPDATE_DATE = ? WHERE video_id = ? `, - ).run( - details.canonicalTitle || 'unknown', - Date.now(), - existing.video_id, - ); + ).run(details.canonicalTitle || 'unknown', Date.now(), existing.video_id); return existing.video_id; } diff --git a/src/core/services/immersion-tracker/types.ts b/src/core/services/immersion-tracker/types.ts index 975481c..e7810b1 100644 --- a/src/core/services/immersion-tracker/types.ts +++ b/src/core/services/immersion-tracker/types.ts @@ -129,7 +129,11 @@ interface QueuedKanjiWrite { lastSeen: number; } -export type QueuedWrite = QueuedTelemetryWrite | QueuedEventWrite | QueuedWordWrite | QueuedKanjiWrite; +export type QueuedWrite = + | QueuedTelemetryWrite + | QueuedEventWrite + | QueuedWordWrite + | QueuedKanjiWrite; export interface VideoMetadata { sourceType: number; diff --git a/src/core/services/jlpt-vocab.test.ts b/src/core/services/jlpt-vocab.test.ts index a475112..05e1437 100644 --- a/src/core/services/jlpt-vocab.test.ts +++ b/src/core/services/jlpt-vocab.test.ts @@ -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('鳥'), 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 () => { diff --git a/src/core/services/subtitle-delay-shift.ts b/src/core/services/subtitle-delay-shift.ts index f648daa..797620a 100644 --- a/src/core/services/subtitle-delay-shift.ts +++ b/src/core/services/subtitle-delay-shift.ts @@ -53,7 +53,9 @@ function parseAssStartTimes(content: string): number[] { const starts: number[] = []; const lines = content.split(/\r?\n/); 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; const [hoursRaw, minutesRaw, secondsRaw] = match[1]!.split(':'); if (secondsRaw === undefined) continue; diff --git a/src/core/services/tokenizer.test.ts b/src/core/services/tokenizer.test.ts index 66ea5be..c18cdff 100644 --- a/src/core/services/tokenizer.test.ts +++ b/src/core/services/tokenizer.test.ts @@ -2370,7 +2370,6 @@ test('tokenizeSubtitle keeps frequency enrichment while n+1 is disabled', async assert.equal(frequencyCalls, 1); }); - test('tokenizeSubtitle excludes default non-independent pos2 from N+1 and frequency annotations', async () => { const result = await tokenizeSubtitle( 'になれば', diff --git a/src/core/services/tokenizer.ts b/src/core/services/tokenizer.ts index b2a1fd4..adce5c2 100644 --- a/src/core/services/tokenizer.ts +++ b/src/core/services/tokenizer.ts @@ -92,13 +92,14 @@ interface TokenizerAnnotationOptions { pos2Exclusions: ReadonlySet; } -let parserEnrichmentWorkerRuntimeModulePromise: - | Promise - | null = null; -let annotationStageModulePromise: Promise | null = null; -let parserEnrichmentFallbackModulePromise: - | Promise - | null = null; +let parserEnrichmentWorkerRuntimeModulePromise: Promise< + typeof import('./tokenizer/parser-enrichment-worker-runtime') +> | null = null; +let annotationStageModulePromise: Promise | null = + null; +let parserEnrichmentFallbackModulePromise: Promise< + typeof import('./tokenizer/parser-enrichment-stage') +> | null = null; const DEFAULT_ANNOTATION_POS1_EXCLUSIONS = resolveAnnotationPos1ExclusionSet( DEFAULT_ANNOTATION_POS1_EXCLUSION_CONFIG, ); @@ -106,7 +107,10 @@ const DEFAULT_ANNOTATION_POS2_EXCLUSIONS = resolveAnnotationPos2ExclusionSet( 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) { return () => false; } @@ -126,7 +130,8 @@ async function enrichTokensWithMecabAsync( mecabTokens: MergedToken[] | null, ): Promise { if (!parserEnrichmentWorkerRuntimeModulePromise) { - parserEnrichmentWorkerRuntimeModulePromise = import('./tokenizer/parser-enrichment-worker-runtime'); + parserEnrichmentWorkerRuntimeModulePromise = + import('./tokenizer/parser-enrichment-worker-runtime'); } try { @@ -185,8 +190,7 @@ export function createTokenizerDepsRuntime( getNPlusOneEnabled: options.getNPlusOneEnabled, getJlptEnabled: options.getJlptEnabled, getFrequencyDictionaryEnabled: options.getFrequencyDictionaryEnabled, - getFrequencyDictionaryMatchMode: - options.getFrequencyDictionaryMatchMode ?? (() => 'headword'), + getFrequencyDictionaryMatchMode: options.getFrequencyDictionaryMatchMode ?? (() => 'headword'), getFrequencyRank: options.getFrequencyRank, getMinSentenceWordsForNPlusOne: options.getMinSentenceWordsForNPlusOne ?? (() => 3), getYomitanGroupDebugEnabled: options.getYomitanGroupDebugEnabled ?? (() => false), @@ -348,7 +352,8 @@ function buildYomitanFrequencyRankMap( continue; } 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)) : Number.MAX_SAFE_INTEGER; const current = rankByTerm.get(normalizedTerm); @@ -489,7 +494,11 @@ async function parseWithYomitanInternalParser( normalizedSelectedTokens, frequencyMatchMode, ); - const yomitanFrequencies = await requestYomitanTermFrequencies(termReadingList, deps, logger); + const yomitanFrequencies = await requestYomitanTermFrequencies( + termReadingList, + deps, + logger, + ); return buildYomitanFrequencyRankMap(yomitanFrequencies); })() : Promise.resolve(new Map()); diff --git a/src/core/services/tokenizer/parser-enrichment-stage.test.ts b/src/core/services/tokenizer/parser-enrichment-stage.test.ts index c0e9140..be45e06 100644 --- a/src/core/services/tokenizer/parser-enrichment-stage.test.ts +++ b/src/core/services/tokenizer/parser-enrichment-stage.test.ts @@ -101,7 +101,7 @@ test('enrichTokensWithMecabPos1 avoids repeated active-candidate filter scans', let sentinelFilterCalls = 0; const originalFilter = Array.prototype.filter; - Array.prototype.filter = (function filterWithSentinelCheck( + Array.prototype.filter = function filterWithSentinelCheck( this: unknown[], ...args: any[] ): any[] { @@ -113,7 +113,7 @@ test('enrichTokensWithMecabPos1 avoids repeated active-candidate filter scans', } } return (originalFilter as (...params: any[]) => any[]).apply(this, args); - }) as typeof Array.prototype.filter; + } as typeof Array.prototype.filter; try { const enriched = enrichTokensWithMecabPos1(tokens, mecabTokens); diff --git a/src/core/services/tokenizer/parser-enrichment-stage.ts b/src/core/services/tokenizer/parser-enrichment-stage.ts index 16ea8fd..4782f94 100644 --- a/src/core/services/tokenizer/parser-enrichment-stage.ts +++ b/src/core/services/tokenizer/parser-enrichment-stage.ts @@ -182,7 +182,8 @@ function pickClosestMecabPosMetadataBySurface( startDistance < bestSurfaceMatchDistance || (startDistance === bestSurfaceMatchDistance && (endDistance < bestSurfaceMatchEndDistance || - (endDistance === bestSurfaceMatchEndDistance && candidate.index < bestSurfaceMatchIndex))) + (endDistance === bestSurfaceMatchEndDistance && + candidate.index < bestSurfaceMatchIndex))) ) { bestSurfaceMatchDistance = startDistance; bestSurfaceMatchEndDistance = endDistance; @@ -199,7 +200,8 @@ function pickClosestMecabPosMetadataBySurface( startDistance < bestSurfaceMatchDistance || (startDistance === bestSurfaceMatchDistance && (endDistance < bestSurfaceMatchEndDistance || - (endDistance === bestSurfaceMatchEndDistance && candidate.index < bestSurfaceMatchIndex))) + (endDistance === bestSurfaceMatchEndDistance && + candidate.index < bestSurfaceMatchIndex))) ) { bestSurfaceMatchDistance = startDistance; bestSurfaceMatchEndDistance = endDistance; @@ -274,9 +276,15 @@ function pickClosestMecabPosMetadataByOverlap( const overlappingTokensByMecabOrder = overlappingTokens .slice() .sort((left, right) => left.index - right.index); - const overlapPos1 = joinUniqueTags(overlappingTokensByMecabOrder.map((candidate) => candidate.pos1)); - const overlapPos2 = joinUniqueTags(overlappingTokensByMecabOrder.map((candidate) => candidate.pos2)); - const overlapPos3 = joinUniqueTags(overlappingTokensByMecabOrder.map((candidate) => candidate.pos3)); + const overlapPos1 = joinUniqueTags( + overlappingTokensByMecabOrder.map((candidate) => candidate.pos1), + ); + const overlapPos2 = joinUniqueTags( + overlappingTokensByMecabOrder.map((candidate) => candidate.pos2), + ); + const overlapPos3 = joinUniqueTags( + overlappingTokensByMecabOrder.map((candidate) => candidate.pos3), + ); return { pos1: overlapPos1 ?? bestToken.pos1, diff --git a/src/core/services/tokenizer/yomitan-parser-runtime.ts b/src/core/services/tokenizer/yomitan-parser-runtime.ts index 7b08c0c..fbb4ac5 100644 --- a/src/core/services/tokenizer/yomitan-parser-runtime.ts +++ b/src/core/services/tokenizer/yomitan-parser-runtime.ts @@ -39,7 +39,10 @@ interface YomitanProfileMetadata { const DEFAULT_YOMITAN_SCAN_LENGTH = 40; const yomitanProfileMetadataByWindow = new WeakMap(); -const yomitanFrequencyCacheByWindow = new WeakMap>(); +const yomitanFrequencyCacheByWindow = new WeakMap< + BrowserWindow, + Map +>(); function isObject(value: unknown): value is Record { return Boolean(value && typeof value === 'object'); @@ -87,7 +90,7 @@ function parsePositiveFrequencyString(value: string): number | null { const chunks = numericPrefix.split(','); const normalizedNumber = chunks.length <= 1 - ? chunks[0] ?? '' + ? (chunks[0] ?? '') : chunks.slice(1).every((chunk) => /^\d{3}$/.test(chunk)) ? chunks.join('') : (chunks[0] ?? ''); @@ -145,11 +148,7 @@ function toYomitanTermFrequency(value: unknown): YomitanTermFrequency | null { : Number.MAX_SAFE_INTEGER; const reading = - value.reading === null - ? null - : typeof value.reading === 'string' - ? value.reading - : null; + value.reading === null ? null : typeof value.reading === 'string' ? value.reading : null; const displayValue = typeof displayValueRaw === 'string' ? displayValueRaw : null; 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 seen = new Set(); @@ -174,7 +175,9 @@ function normalizeTermReadingList(termReadingList: YomitanTermReadingPair[]): Yo continue; } 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 ?? ''}`; if (seen.has(key)) { continue; @@ -298,7 +301,9 @@ function groupFrequencyEntriesByPair( const grouped = new Map(); for (const entry of entries) { 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 existing = grouped.get(key); if (existing) { @@ -805,7 +810,11 @@ export async function requestYomitanTermFrequencies( ); if (fallbackFetchResult !== null) { fallbackFetchedEntries = fallbackFetchResult; - cacheFrequencyEntriesForPairs(frequencyCache, fallbackTermReadingList, fallbackFetchedEntries); + cacheFrequencyEntriesForPairs( + frequencyCache, + fallbackTermReadingList, + fallbackFetchedEntries, + ); } for (const pair of missingTermReadingList) { @@ -829,7 +838,9 @@ export async function requestYomitanTermFrequencies( [...missingTermReadingList, ...fallbackTermReadingList].map((pair) => pair.term), ); 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]; } diff --git a/src/main-entry-runtime.test.ts b/src/main-entry-runtime.test.ts index c8d127e..a35f937 100644 --- a/src/main-entry-runtime.test.ts +++ b/src/main-entry-runtime.test.ts @@ -33,7 +33,13 @@ test('sanitizeBackgroundEnv marks background child and keeps warning suppression test('shouldDetachBackgroundLaunch only for first background invocation', () => { assert.equal(shouldDetachBackgroundLaunch(['--background'], {}), true); - assert.equal(shouldDetachBackgroundLaunch(['--background'], { SUBMINER_BACKGROUND_CHILD: '1' }), false); - assert.equal(shouldDetachBackgroundLaunch(['--background'], { ELECTRON_RUN_AS_NODE: '1' }), false); + assert.equal( + shouldDetachBackgroundLaunch(['--background'], { SUBMINER_BACKGROUND_CHILD: '1' }), + false, + ); + assert.equal( + shouldDetachBackgroundLaunch(['--background'], { ELECTRON_RUN_AS_NODE: '1' }), + false, + ); assert.equal(shouldDetachBackgroundLaunch(['--start'], {}), false); }); diff --git a/src/main.ts b/src/main.ts index 35a790a..67cd6c2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -892,7 +892,9 @@ function maybeSignalPluginAutoplayReady( if (typeof pauseProperty === 'number') { 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) { logger.debug( `[autoplay-ready] failed to read pause property for media ${mediaPath}: ${(error as Error).message}`, @@ -1365,7 +1367,10 @@ function shouldInitializeMecabForAnnotations(): boolean { 'subtitle.annotation.nPlusOne', 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( 'subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled, @@ -2992,7 +2997,8 @@ const { showMpvOsd: (text: string) => showMpvOsd(text), replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient), playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient), - shiftSubDelayToAdjacentSubtitle: (direction) => shiftSubtitleDelayToAdjacentCueHandler(direction), + shiftSubDelayToAdjacentSubtitle: (direction) => + shiftSubtitleDelayToAdjacentCueHandler(direction), sendMpvCommand: (rawCommand: (string | number)[]) => sendMpvCommandRuntime(appState.mpvClient, rawCommand), isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected), diff --git a/src/main/runtime/composers/mpv-runtime-composer.test.ts b/src/main/runtime/composers/mpv-runtime-composer.test.ts index 4f6e905..d9f8eb2 100644 --- a/src/main/runtime/composers/mpv-runtime-composer.test.ts +++ b/src/main/runtime/composers/mpv-runtime-composer.test.ts @@ -659,150 +659,144 @@ test('composeMpvRuntimeHandlers does not block first tokenization on dictionary await composed.startTokenizationWarmups(); }); -test( - 'composeMpvRuntimeHandlers shows annotation loading OSD after tokenization-ready when dictionary warmup is still pending', - async () => { - const jlptDeferred = createDeferred(); - const frequencyDeferred = createDeferred(); - const osdMessages: string[] = []; +test('composeMpvRuntimeHandlers shows annotation loading OSD after tokenization-ready when dictionary warmup is still pending', async () => { + const jlptDeferred = createDeferred(); + const frequencyDeferred = createDeferred(); + const osdMessages: string[] = []; - const composed = composeMpvRuntimeHandlers< - { connect: () => void; on: () => void }, - { onTokenizationReady?: (text: string) => void }, - { text: string } - >({ - bindMpvMainEventHandlersMainDeps: { - appState: { - initialArgs: null, - overlayRuntimeInitialized: true, - mpvClient: null, - immersionTracker: null, - subtitleTimingTracker: null, - currentSubText: '', - currentSubAssText: '', - playbackPaused: 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: () => {}, + const composed = composeMpvRuntimeHandlers< + { connect: () => void; on: () => void }, + { onTokenizationReady?: (text: string) => void }, + { text: string } + >({ + bindMpvMainEventHandlersMainDeps: { + appState: { + initialArgs: null, + overlayRuntimeInitialized: true, + mpvClient: null, + immersionTracker: null, + subtitleTimingTracker: null, + currentSubText: '', + currentSubAssText: '', + playbackPaused: null, + previousSecondarySubVisibility: null, }, - mpvClientRuntimeServiceFactoryMainDeps: { - createClient: class { - connect(): void {} - on(): void {} - }, - getSocketPath: () => '/tmp/mpv.sock', - getResolvedConfig: () => ({ auto_start_overlay: false }), - isAutoStartOverlayEnabled: () => false, - setOverlayVisible: () => {}, - isVisibleOverlayVisible: () => false, - getReconnectTimer: () => null, - setReconnectTimer: () => {}, + 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: { + createClient: class { + connect(): void {} + on(): void {} }, - updateMpvSubtitleRenderMetricsMainDeps: { - getCurrentMetrics: () => BASE_METRICS, - setCurrentMetrics: () => {}, - applyPatch: (current, patch) => ({ next: { ...current, ...patch }, changed: true }), - broadcastMetrics: () => {}, + getSocketPath: () => '/tmp/mpv.sock', + getResolvedConfig: () => ({ auto_start_overlay: false }), + isAutoStartOverlayEnabled: () => false, + setOverlayVisible: () => {}, + 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: { - 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, - }, - 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); - }, + 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: { - now: () => 0, - logDebug: () => {}, - logWarn: () => {}, - }, - startBackgroundWarmupsMainDeps: { - getStarted: () => false, - setStarted: () => {}, - isTexthookerOnlyMode: () => false, - ensureYomitanExtensionLoaded: async () => undefined, - shouldWarmupMecab: () => false, - shouldWarmupYomitanExtension: () => false, - shouldWarmupSubtitleDictionaries: () => false, - shouldWarmupJellyfinRemoteSession: () => false, - shouldAutoConnectJellyfinRemote: () => false, - startJellyfinRemoteSession: async () => {}, - }, + }, + warmups: { + launchBackgroundWarmupTaskMainDeps: { + now: () => 0, + logDebug: () => {}, + logWarn: () => {}, }, - }); + 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(); - await new Promise((resolve) => setImmediate(resolve)); - assert.deepEqual(osdMessages, []); + const warmupPromise = composed.startTokenizationWarmups(); + await new Promise((resolve) => setImmediate(resolve)); + assert.deepEqual(osdMessages, []); - await composed.tokenizeSubtitle('first line'); - assert.deepEqual(osdMessages, ['Loading subtitle annotations |']); + await composed.tokenizeSubtitle('first line'); + assert.deepEqual(osdMessages, ['Loading subtitle annotations |']); - jlptDeferred.resolve(); - frequencyDeferred.resolve(); - await warmupPromise; - await new Promise((resolve) => setImmediate(resolve)); + jlptDeferred.resolve(); + frequencyDeferred.resolve(); + await warmupPromise; + await new Promise((resolve) => setImmediate(resolve)); - assert.deepEqual(osdMessages, [ - 'Loading subtitle annotations |', - 'Subtitle annotations loaded', - ]); - }, -); + assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Subtitle annotations loaded']); +}); diff --git a/src/main/runtime/ipc-mpv-command-main-deps.ts b/src/main/runtime/ipc-mpv-command-main-deps.ts index 26db27b..fce5c73 100644 --- a/src/main/runtime/ipc-mpv-command-main-deps.ts +++ b/src/main/runtime/ipc-mpv-command-main-deps.ts @@ -10,8 +10,7 @@ export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler( showMpvOsd: (text: string) => deps.showMpvOsd(text), replayCurrentSubtitle: () => deps.replayCurrentSubtitle(), playNextSubtitle: () => deps.playNextSubtitle(), - shiftSubDelayToAdjacentSubtitle: (direction) => - deps.shiftSubDelayToAdjacentSubtitle(direction), + shiftSubDelayToAdjacentSubtitle: (direction) => deps.shiftSubDelayToAdjacentSubtitle(direction), sendMpvCommand: (command: (string | number)[]) => deps.sendMpvCommand(command), isMpvConnected: () => deps.isMpvConnected(), hasRuntimeOptionsManager: () => deps.hasRuntimeOptionsManager(), diff --git a/src/main/runtime/jellyfin-cli-list.ts b/src/main/runtime/jellyfin-cli-list.ts index f6c3529..7f1ec98 100644 --- a/src/main/runtime/jellyfin-cli-list.ts +++ b/src/main/runtime/jellyfin-cli-list.ts @@ -54,7 +54,7 @@ export function createHandleJellyfinListCommands(deps: { isForced?: boolean; isExternal?: boolean; deliveryUrl?: string | null; - }> + }> >; writeJellyfinPreviewAuth: (responsePath: string, payload: JellyfinPreviewAuthPayload) => void; logInfo: (message: string) => void; diff --git a/src/main/runtime/subtitle-tokenization-main-deps.test.ts b/src/main/runtime/subtitle-tokenization-main-deps.test.ts index e6ffa26..dd92e25 100644 --- a/src/main/runtime/subtitle-tokenization-main-deps.test.ts +++ b/src/main/runtime/subtitle-tokenization-main-deps.test.ts @@ -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']); }); -test( - 'dictionary prewarm shows OSD when loading indicator is requested even if notification predicate is disabled', - async () => { - const osdMessages: string[] = []; +test('dictionary prewarm shows OSD when loading indicator is requested even if notification predicate is disabled', async () => { + const osdMessages: string[] = []; - const prewarm = createPrewarmSubtitleDictionariesMainHandler({ - ensureJlptDictionaryLookup: async () => undefined, - ensureFrequencyDictionaryLookup: async () => undefined, - shouldShowOsdNotification: () => false, - showMpvOsd: (message) => { - osdMessages.push(message); - }, - }); + const prewarm = createPrewarmSubtitleDictionariesMainHandler({ + ensureJlptDictionaryLookup: async () => undefined, + ensureFrequencyDictionaryLookup: async () => undefined, + shouldShowOsdNotification: () => false, + showMpvOsd: (message) => { + osdMessages.push(message); + }, + }); - await prewarm({ showLoadingOsd: true }); + await prewarm({ showLoadingOsd: true }); - assert.deepEqual(osdMessages, [ - 'Loading subtitle annotations |', - 'Subtitle annotations loaded', - ]); - }, -); + assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Subtitle annotations loaded']); +}); test('dictionary prewarm clears loading OSD timer even if notifications are disabled before completion', async () => { const clearedTimers: unknown[] = []; diff --git a/src/mecab-tokenizer.ts b/src/mecab-tokenizer.ts index e64d346..60bd5ca 100644 --- a/src/mecab-tokenizer.ts +++ b/src/mecab-tokenizer.ts @@ -139,7 +139,8 @@ export class MecabTokenizer { ); this.spawnFn = options.spawnFn ?? childProcess.spawn; 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)); } @@ -217,11 +218,7 @@ export class MecabTokenizer { } private retryOrResolveRequest(request: MecabQueuedRequest): void { - if ( - request.retryCount < MecabTokenizer.MAX_RETRY_COUNT && - this.enabled && - this.available - ) { + if (request.retryCount < MecabTokenizer.MAX_RETRY_COUNT && this.enabled && this.available) { this.requestQueue.push({ ...request, retryCount: request.retryCount + 1, @@ -368,7 +365,9 @@ export class MecabTokenizer { this.requestQueue = []; 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) { this.retryOrResolveRequest(request); }