diff --git a/docs-site/.vitepress/config.ts b/docs-site/.vitepress/config.ts index 8a1bb2f..62e74b0 100644 --- a/docs-site/.vitepress/config.ts +++ b/docs-site/.vitepress/config.ts @@ -84,8 +84,9 @@ export default { { text: 'MPV Plugin', link: '/mpv-plugin' }, { text: 'Anki', link: '/anki-integration' }, { text: 'Jellyfin', link: '/jellyfin-integration' }, - { text: 'Jimaku', link: '/configuration#jimaku' }, - { text: 'AniList', link: '/configuration#anilist' }, + { text: 'YouTube', link: '/youtube-integration' }, + { text: 'Jimaku', link: '/jimaku-integration' }, + { text: 'AniList', link: '/anilist-integration' }, { text: 'Character Dictionary', link: '/character-dictionary' }, ], }, diff --git a/docs-site/.vitepress/theme/index.ts b/docs-site/.vitepress/theme/index.ts index 78e0b16..1b21d17 100644 --- a/docs-site/.vitepress/theme/index.ts +++ b/docs-site/.vitepress/theme/index.ts @@ -134,6 +134,11 @@ async function getMermaid() { startOnLoad: false, securityLevel: 'loose', theme: 'base', + flowchart: { + padding: 16, + nodeSpacing: 30, + rankSpacing: 40, + }, themeVariables: { background: '#24273a', primaryColor: '#363a4f', diff --git a/docs-site/.vitepress/theme/mermaid-modal.css b/docs-site/.vitepress/theme/mermaid-modal.css index c702e0f..d54e8d1 100644 --- a/docs-site/.vitepress/theme/mermaid-modal.css +++ b/docs-site/.vitepress/theme/mermaid-modal.css @@ -1,6 +1,14 @@ .mermaid-interactive { cursor: zoom-in; transition: outline-color 180ms ease; + overflow-x: auto; + overflow-y: hidden; + text-align: center; +} + +.mermaid-interactive svg { + display: inline-block; + min-width: 0; } .mermaid-interactive:focus-visible { diff --git a/docs-site/.vitepress/theme/tui-theme.css b/docs-site/.vitepress/theme/tui-theme.css index 4800edf..a98a239 100644 --- a/docs-site/.vitepress/theme/tui-theme.css +++ b/docs-site/.vitepress/theme/tui-theme.css @@ -2,23 +2,50 @@ @import '@fontsource/jetbrains-mono/500.css'; @import '@fontsource/jetbrains-mono/600.css'; @import '@fontsource/jetbrains-mono/700.css'; -@import '@fontsource/manrope/400.css'; -@import '@fontsource/manrope/500.css'; -@import '@fontsource/manrope/600.css'; -@import '@fontsource/manrope/700.css'; -@import '@fontsource/manrope/800.css'; + +@font-face { + font-family: 'M PLUS 1'; + src: url('/assets/fonts/Mplus1-Medium.ttf') format('truetype'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Manrope Default'; + src: url('/assets/fonts/manrope-latin-600-normal.ttf') format('truetype'); + font-weight: 600; + font-style: normal; + font-display: swap; +} :root { --tui-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace; - --tui-font-body: 'Manrope', system-ui, sans-serif; + --tui-font-body: + 'Manrope Default', + 'M PLUS 1', + 'Manrope', + 'Noto Sans CJK JP', + 'Noto Sans JP', + 'Hiragino Kaku Gothic ProN', + 'Meiryo', + 'Yu Gothic', + 'Hiragino Sans', + system-ui, + sans-serif; --tui-transition: 180ms ease; } :root { --vp-font-family-base: var(--tui-font-body); + --vp-font-family-heading: var(--tui-font-body); --vp-font-family-mono: var(--tui-font-mono); } +.vp-doc { + font-family: var(--vp-font-family-base); +} + /* === Selection === */ ::selection { background: hsla(267, 83%, 80%, 0.22); @@ -132,7 +159,7 @@ button, .vp-doc h2, .vp-doc h3, .vp-doc h4 { - font-family: var(--tui-font-mono); + font-family: var(--vp-font-family-heading); } .vp-doc h1 { @@ -172,6 +199,8 @@ button, background: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-divider); color: var(--vp-c-brand-1); + font-family: var(--tui-font-mono), 'M PLUS 1', 'Noto Sans CJK JP', 'Noto Sans JP', + monospace; } /* === Code blocks === */ @@ -179,6 +208,8 @@ button, border-radius: 0; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-alt) !important; + font-family: var(--tui-font-mono), 'M PLUS 1', 'Noto Sans CJK JP', 'Noto Sans JP', + monospace; } .vp-doc div[class*='language-']::before { diff --git a/docs-site/anilist-integration.md b/docs-site/anilist-integration.md new file mode 100644 index 0000000..a9cdd5a --- /dev/null +++ b/docs-site/anilist-integration.md @@ -0,0 +1,150 @@ +# AniList Integration + +SubMiner can sync your watch progress to [AniList](https://anilist.co) automatically. When you finish an episode, SubMiner detects the title and episode number from the filename, finds the matching AniList entry, and updates your progress via the GraphQL API. Failed updates are retried with exponential backoff in the background. + +AniList data also powers two additional features: [cover art](#cover-art) for the stats dashboard and the [Character Dictionary](/character-dictionary) for in-overlay name lookup. + +## Setup + +AniList integration is opt-in. To enable it: + +1. Set `anilist.enabled` to `true` in your config. +2. Leave `anilist.accessToken` empty and restart SubMiner (or run `--anilist-setup`). +3. Approve access in the AniList authorization page. +4. The callback returns to SubMiner via the `subminer://anilist-setup?...` protocol URL, and SubMiner stores the token automatically. + +```jsonc +{ + "anilist": { + "enabled": true, + "accessToken": "" + } +} +``` + +The access token is encrypted at rest using Electron's `safeStorage` API. On Linux this defaults to `gnome-libsecret`; override the backend with `--password-store=` (for example `--password-store=basic_text`). + +If the embedded auth UI fails to render, SubMiner opens the authorize URL in your default browser and shows fallback instructions in-app. + +::: tip +You can also set `anilist.accessToken` directly in config to skip the setup flow entirely. When blank, SubMiner uses the locally stored encrypted token. +::: + +## How Tracking Works + +SubMiner monitors playback and triggers an AniList progress update when an episode is considered "watched" -- at least 85% of the episode duration viewed and a minimum of 10 minutes watched. + +The update flow: + +1. **Title detection** -- SubMiner extracts the anime title, season, and episode number from the media filename. It tries [`guessit`](https://github.com/guessit-io/guessit) first for accurate parsing, then falls back to an internal filename parser if guessit is unavailable. +2. **AniList search** -- The detected title is searched against the AniList GraphQL API. SubMiner picks the best match by comparing titles (romaji, English, native) and filtering by episode count. +3. **Progress check** -- SubMiner fetches your current list entry for the matched media. If your recorded progress already meets or exceeds the detected episode, the update is skipped. +4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`. + +```mermaid +flowchart TB + classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef action fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef result fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef ext fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px + + Play["Media Plays"]:::step + Detect["Episode Detected"]:::action + Queue["Update Queue"]:::action + Rate["Rate Limiter"]:::enrich + GQL["GraphQL Mutation"]:::ext + Done["Progress Updated"]:::result + + Play --> Detect + Detect --> Queue + Queue --> Rate + Rate --> GQL + GQL --> Done +``` + +## Update Queue and Retry + +Failed AniList updates are persisted to a retry queue on disk and retried with exponential backoff. + +| Parameter | Value | +| --- | --- | +| Initial backoff | 30 seconds | +| Maximum backoff | 6 hours | +| Maximum attempts | 8 | +| Queue capacity | 500 items | + +After 8 failed attempts, the update is moved to a dead-letter queue and no longer retried automatically. The queue is persisted across restarts so no updates are lost if SubMiner exits before a retry succeeds. + +Use `--anilist-retry-queue` to manually process one ready item from the queue. + +## Cover Art + +SubMiner fetches cover art from AniList for display in the stats dashboard. When a new video starts playing, the cover art fetcher: + +1. Checks the local database for cached art. +2. If missing, parses the media title (guessit then fallback) and searches the AniList API. +3. Downloads the cover image from the AniList CDN and caches it locally (both URL and blob). +4. Stores AniList metadata (romaji/English titles, total episodes) alongside the cover for dashboard display. + +A no-match result is cached for 5 minutes before SubMiner retries, preventing repeated API calls for unrecognized media. + +## Rate Limiting + +All AniList API calls go through a shared rate limiter that enforces a sliding window of 20 requests per minute. The limiter also reads AniList's `X-RateLimit-Remaining` and `Retry-After` response headers and pauses requests when the server signals throttling. This applies to both episode tracking and cover art fetching. + +## Configuration Reference + +```jsonc +{ + "anilist": { + "enabled": true, + "accessToken": "", + "characterDictionary": { + "enabled": false, + "maxLoaded": 3, + "profileScope": "all", + "collapsibleSections": { + "description": false, + "characterInformation": false, + "voicedBy": false + } + } + } +} +``` + +| Option | Values | Description | +| --- | --- | --- | +| `enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) | +| `accessToken` | string | Explicit AniList access token override; when blank, SubMiner uses the stored encrypted token (default: `""`) | +| `characterDictionary.enabled` | `true`, `false` | Enable auto-sync of the merged character dictionary from AniList (default: `false`) | +| `characterDictionary.maxLoaded` | number | Number of recent media snapshots kept in the merged dictionary (default: `3`) | +| `characterDictionary.profileScope` | `"all"`, `"active"` | Apply dictionary to all Yomitan profiles or only the active one | +| `characterDictionary.collapsibleSections.*` | `true`, `false` | Control which dictionary entry sections start expanded | + +See the [Character Dictionary](/character-dictionary) page for full details on the character dictionary feature, including name generation, matching, auto-sync lifecycle, and dictionary entry format. + +## CLI Commands + +| Command | Description | +| --- | --- | +| `--anilist-setup` | Open AniList setup/auth flow helper window | +| `--anilist-status` | Print current token resolution state and retry queue counters | +| `--anilist-logout` | Clear stored AniList token from local persisted state | +| `--anilist-retry-queue` | Process one ready retry queue item immediately | + +## Troubleshooting + +- **Updates not triggering:** Confirm `anilist.enabled` is `true`. SubMiner requires at least 85% of the episode watched and a minimum of 10 minutes. Short episodes or partial watches will not trigger an update. +- **Wrong episode or title matched:** Detection quality is best when `guessit` is installed and on your `PATH`. Without it, SubMiner falls back to internal filename parsing which can be less accurate with unusual naming conventions. +- **Token issues:** Run `--anilist-status` to check token state. If the token is invalid or expired, run `--anilist-setup` or `--anilist-logout` and re-authenticate. +- **Updates failing repeatedly:** Run `--anilist-status` to see retry queue counters. Items that fail 8 times are moved to the dead-letter queue. Check network connectivity and AniList API status. +- **Cover art missing:** Cover art is fetched on a best-effort basis using title matching. If the filename is hard to parse, the search may return no results. The fetcher retries after 5 minutes. +- **Encryption unavailable on Linux:** If you see warnings about safeStorage, try `--password-store=basic_text` as a workaround, or ensure your desktop keyring (gnome-keyring, KWallet) is running. + +## Related + +- [Character Dictionary](/character-dictionary) -- AniList-powered character name dictionary for Yomitan +- [Configuration Reference](/configuration) -- full config options +- [Jellyfin Integration](/jellyfin-integration) -- media server integration diff --git a/docs-site/architecture.md b/docs-site/architecture.md index 5463cac..97a01ac 100644 --- a/docs-site/architecture.md +++ b/docs-site/architecture.md @@ -32,6 +32,7 @@ plugin/ # state · messages · hover · ui · options · environment · log # binary · aniskip · aniskip_match) src/ + ai/ # AI translation provider utilities (client, config) main-entry.ts # Background-mode bootstrap wrapper before loading main.js main.ts # Entry point — delegates to runtime composers/domain modules preload.ts # Electron preload bridge @@ -134,7 +135,7 @@ src/renderer/ The main process orchestrates a single primary overlay window plus modal surfaces: `main.ts` delegates to composition modules that wire together domain services. Subtitle layers (primary + secondary bar) are rendered in the same overlay renderer process, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough. ```mermaid -flowchart LR +flowchart TB classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold classDef comp fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef svc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px @@ -144,29 +145,22 @@ flowchart LR classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px subgraph ExtRt["External Runtimes"] + direction LR Launcher["Launcher CLI"]:::extrt Plugin["mpv Plugin"]:::extrt end - subgraph Ext["External Systems"] - mpvExt["mpv"]:::ext - AnkiExt["AnkiConnect"]:::ext - JimakuExt["Jimaku"]:::ext - TrackerExt["Window Tracker"]:::ext - AnilistExt["AniList"]:::ext - JellyfinExt["Jellyfin"]:::ext - DiscordExt["Discord"]:::ext - end - Main["main.ts"]:::entry subgraph Comp["Composition"] + direction LR Startup["Startup & Lifecycle"]:::comp Wiring["Runtime Wiring"]:::comp Composers["Domain Composers"]:::comp end subgraph Svc["Services"] + direction LR Mpv["MPV Stack"]:::svc OverlaySvc["Overlay Manager"]:::svc Mining["Mining & Subtitles"]:::svc @@ -179,16 +173,31 @@ flowchart LR Bridge(["preload.ts"]):::bridge subgraph Rend["Renderer"] + direction LR OverlayWin["Overlay Window"]:::rend UI["Subtitles & Modals"]:::rend end + subgraph Ext["External Systems"] + direction LR + mpvExt["mpv"]:::ext + AnkiExt["AnkiConnect"]:::ext + JimakuExt["Jimaku"]:::ext + TrackerExt["Window Tracker"]:::ext + AnilistExt["AniList"]:::ext + JellyfinExt["Jellyfin"]:::ext + DiscordExt["Discord"]:::ext + end + Launcher -->|"CLI"| Main Plugin -->|"IPC"| mpvExt Main --> Comp Comp --> Svc + Svc --> Bridge + Bridge --> Rend + mpvExt <-->|"socket"| Mpv AnkiExt <-->|"HTTP"| AnkiProxy JimakuExt <-->|"HTTP"| Integrations @@ -197,10 +206,6 @@ flowchart LR JellyfinExt <-->|"HTTP"| Tracking DiscordExt <-->|"RPC"| Integrations - OverlaySvc & Mining --> Bridge - Bridge --> OverlayWin - OverlayWin --> UI - style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5 style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5 style Rend fill:#363a4f,stroke:#494d64,color:#cad3f5 @@ -273,7 +278,7 @@ For domains migrated to reducer-style transitions (for example AniList token/que - **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, stops the AnkiConnect proxy server, and cleans Anki/AniList state. ```mermaid -flowchart LR +flowchart TB classDef start fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold classDef phase fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px @@ -285,7 +290,7 @@ flowchart LR CLI["CLI + Environment"]:::start CLI --> Init["Module Init"]:::phase Init --> Parse["Parse argv"]:::phase - Parse --> GenCheck{"--generate\nconfig?"}:::decision + Parse --> GenCheck{"--generate-config?"}:::decision GenCheck -->|"yes"| GenExit["Write & exit"]:::phase GenCheck -->|"no"| Lock["Acquire lock"]:::phase @@ -300,21 +305,14 @@ flowchart LR OverlayInit --> MainWin["Create window"]:::init OverlayInit --> Shortcuts["Register shortcuts"]:::init - MainWin & Shortcuts --> Warmups["Background Warmups"]:::phase + MainWin & Shortcuts --> Warmups - subgraph WarmupGroup[" "] - direction TB - W1["MeCab"]:::warmup - W2["Yomitan"]:::warmup - W3["Dictionaries"]:::warmup - W4["Jellyfin"]:::warmup - W5["Discord"]:::warmup - W6["AniList"]:::warmup - W7["Anki Proxy"]:::warmup - W1 ~~~ W2 ~~~ W3 ~~~ W4 ~~~ W5 ~~~ W6 ~~~ W7 + subgraph Warmups["Background Warmups (parallel)"] + direction LR + W1["MeCab"]:::warmup ~~~ W2["Yomitan"]:::warmup ~~~ W3["Dictionaries"]:::warmup ~~~ W4["Jellyfin"]:::warmup ~~~ W5["Discord"]:::warmup ~~~ W6["AniList"]:::warmup ~~~ W7["Anki Proxy"]:::warmup end - Warmups --> WarmupGroup + Warmups --> Loop subgraph Loop["Event Loop"] direction TB @@ -324,19 +322,53 @@ flowchart LR Pipeline --> Broadcast["State + Renderer"]:::runtime end - WarmupGroup --> Loop - - style WarmupGroup fill:transparent,stroke:none + style Warmups fill:#363a4f,stroke:#494d64,color:#cad3f5 Loop -->|"quit"| Quit["Shutdown"]:::shutdown - Quit --> T1["UI cleanup"]:::shutdown - Quit --> T2["Socket + server teardown"]:::shutdown - Quit --> T3["Flush tracking + state"]:::shutdown + subgraph Cleanup[" "] + direction LR + T1["UI cleanup"]:::shutdown + T2["Socket + server teardown"]:::shutdown + T3["Flush tracking + state"]:::shutdown + end + Quit --> Cleanup + + style Cleanup fill:transparent,stroke:none style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5 ``` +## Subtitle Prefetch Pipeline + +SubMiner can pre-tokenize upcoming subtitle lines before they appear on screen. When an external subtitle file (SRT, VTT, or ASS) is detected on the active track, the `SubtitlePrefetchService` parses all cues via the `SubtitleCueParser`, identifies a priority window of upcoming lines based on the current playback position, and tokenizes them in the background through the same pipeline used for live subtitles. Results are stored directly into the `SubtitleProcessingController` cache, so when a subtitle actually appears during playback, it hits a warm cache and renders in ~30-50ms instead of ~200-320ms. + +The prefetcher yields to live subtitle processing (which always takes priority over background work) and re-computes its priority window on seek. Cache invalidation events (e.g. marking a word as known) trigger re-prefetching of the current window to keep results fresh. + +```mermaid +flowchart TB + classDef phase fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef init fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px + + SubFile["External Sub File"]:::init + Parse["Cue Parser"]:::phase + Window["Upcoming Lines"]:::phase + Tokenize["Pre-tokenize"]:::warmup + Cache["Token Cache"]:::runtime + Appear["Subtitle Appears"]:::init + Hit["Cache Hit"]:::runtime + Render["Fast Render"]:::runtime + + SubFile --> Parse --> Window --> Tokenize --> Cache + Appear --> Hit --> Render + Cache -.->|"warm"| Hit + + style SubFile stroke-width:2px + style Render stroke-width:2px +``` + ## Why This Design - **Smaller blast radius:** changing one feature usually touches one service. diff --git a/docs-site/changelog.md b/docs-site/changelog.md index 00ac450..b5412d7 100644 --- a/docs-site/changelog.md +++ b/docs-site/changelog.md @@ -1,5 +1,12 @@ # Changelog +## v0.9.2 (2026-03-25) +- Fixed overlay pointer tracking so Windows click-through toggles immediately when the cursor enters or leaves subtitle regions. +- Fixed Windows overlay window tracking on scaled displays by converting native tracked window bounds to Electron DIP coordinates. +- Fixed Windows direct `--youtube-play` startup so MPV boots reliably, stays paused until the app-owned subtitle flow is ready, and reuses an already-running SubMiner instance. +- Fixed standalone Windows `--youtube-play` sessions so closing MPV fully exits SubMiner instead of leaving hidden overlay windows behind. +- Fixed `subminer ` on Linux so the YouTube playback flow waits for Yomitan to load before creating the overlay window. + ## v0.9.1 (2026-03-24) - Reduced packaged release size by excluding duplicate `extraResources` payload and pruning docs, tests, sourcemaps, and other source-only files from Electron bundles. - Restored controller navigation and lookup/mining controls while the subtitle sidebar is open, while keeping true modal dialogs blocking controller actions. diff --git a/docs-site/character-dictionary.md b/docs-site/character-dictionary.md index 6d89ec4..01fe8ec 100644 --- a/docs-site/character-dictionary.md +++ b/docs-site/character-dictionary.md @@ -32,7 +32,7 @@ The feature has three stages: **snapshot**, **merge**, and **match**. 3. **Match** — During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. Tokens that match a character entry are flagged with `isNameMatch` and highlighted in the overlay with a distinct color. ```mermaid -flowchart LR +flowchart TB classDef api fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef store fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef build fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px @@ -59,7 +59,7 @@ flowchart LR Character dictionary sync is disabled by default. To turn it on: -1. Authenticate with AniList (see [AniList configuration](/configuration#anilist)). +1. Authenticate with AniList (see [AniList Integration](/anilist-integration#setup)). 2. Set `anilist.characterDictionary.enabled` to `true` in your config. 3. Start watching — SubMiner will generate a snapshot for the current media and import the merged dictionary into Yomitan automatically. @@ -274,5 +274,5 @@ If you work with visual novels or want a standalone dictionary generator indepen ## Related - [Subtitle Annotations](/subtitle-annotations) — how name matches interact with N+1, frequency, and JLPT layers -- [AniList Configuration](/configuration#anilist) — authentication and AniList settings +- [AniList Integration](/anilist-integration) — authentication, episode tracking, and AniList settings - [Configuration Reference](/configuration) — full config options diff --git a/docs-site/configuration.md b/docs-site/configuration.md index 85ad227..d06654d 100644 --- a/docs-site/configuration.md +++ b/docs-site/configuration.md @@ -768,7 +768,6 @@ Anki reads this provider directly. Legacy subtitle fallback keeps the same provi SubMiner uses the shared provider for: - Anki translation/enrichment when `ankiConnect.ai.enabled` is `true` -- Legacy subtitle fallback compatibility when `youtubeSubgen.fixWithAi` is `true` ### AnkiConnect @@ -999,7 +998,10 @@ Set `openBrowser` to `false` to only print the URL without opening a browser. ### Auto Subtitle Sync -Sync the active subtitle track using `alass` (preferred) or `ffsubsync`: +Sync the active subtitle track using `alass` (preferred) or `ffsubsync`. Both are **optional external tools** that must be installed separately and available on your `PATH` (or configured via the path options below). Subtitle syncing is silently skipped if neither is found. + +- [`alass`](https://github.com/kaegi/alass) — fast, audio-independent sync using a secondary subtitle as reference +- [`ffsubsync`](https://github.com/smacke/ffsubsync) — audio-based sync using the video file as reference (fallback) ```json { @@ -1016,8 +1018,8 @@ Sync the active subtitle track using `alass` (preferred) or `ffsubsync`: | Option | Values | Description | | ---------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `defaultMode` | `"auto"`, `"manual"` | `auto`: try `alass` against secondary subtitle, then fallback to `ffsubsync`; `manual`: open overlay picker | -| `alass_path` | string path | Path to `alass` executable. Empty or `null` falls back to `/usr/bin/alass`. | -| `ffsubsync_path` | string path | Path to `ffsubsync` executable. Empty or `null` falls back to `/usr/bin/ffsubsync`. | +| `alass_path` | string path | Path to `alass` executable. Empty or `null` resolves from `PATH`. `alass` must be installed separately. | +| `ffsubsync_path` | string path | Path to `ffsubsync` executable. Empty or `null` resolves from `PATH`. `ffsubsync` must be installed separately. | | `ffmpeg_path` | string path | Path to `ffmpeg` (used for internal subtitle extraction). Empty or `null` falls back to `/usr/bin/ffmpeg`. | | `replace` | `true`, `false` | When `true` (default), overwrite the active subtitle file on successful sync. When `false`, write `_retimed.`. | @@ -1332,7 +1334,7 @@ Set defaults used by the `subminer` launcher for YouTube subtitle loading: ```json { - "youtubeSubgen": { + "youtube": { "primarySubLanguages": ["ja", "jpn"] } } @@ -1354,7 +1356,7 @@ Current launcher behavior: Language targets are derived from subtitle config: -- primary track: `youtubeSubgen.primarySubLanguages` (falls back to `["ja","jpn"]`) +- primary track: `youtube.primarySubLanguages` (falls back to `["ja","jpn"]`) - secondary track: `secondarySub.secondarySubLanguages` (falls back to English when empty) - Tracks are resolved and loaded before mpv starts; the older launcher mode switch has been removed. diff --git a/docs-site/development.md b/docs-site/development.md index 62c78be..5607dd3 100644 --- a/docs-site/development.md +++ b/docs-site/development.md @@ -231,13 +231,6 @@ Run `make help` for a full list of targets. Key ones: | `SUBMINER_ROFI_THEME` | Override rofi theme path for launcher picker | | `SUBMINER_LOG_LEVEL` | Override app logger level (`debug`, `info`, `warn`, `error`) | | `SUBMINER_MPV_LOG` | Override mpv/app shared log file path | -| `SUBMINER_WHISPER_BIN` | Override legacy `youtubeSubgen.whisperBin` fallback compatibility path | -| `SUBMINER_WHISPER_MODEL` | Override legacy `youtubeSubgen.whisperModel` fallback compatibility path | -| `SUBMINER_WHISPER_VAD_MODEL` | Override legacy `youtubeSubgen.whisperVadModel` fallback compatibility path | -| `SUBMINER_WHISPER_THREADS` | Override legacy `youtubeSubgen.whisperThreads` fallback compatibility value | -| `SUBMINER_YT_SUBGEN_OUT_DIR` | Override legacy fallback subtitle output directory | -| `SUBMINER_YT_SUBGEN_AUDIO_FORMAT` | Override extraction format used by legacy fallback subtitle path | -| `SUBMINER_YT_SUBGEN_KEEP_TEMP` | Set to `1` to keep legacy fallback subtitle workspace | | `SUBMINER_JIMAKU_API_KEY` | Override Jimaku API key for launcher subtitle downloads | | `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command used to resolve Jimaku API key at runtime | | `SUBMINER_JIMAKU_API_BASE_URL` | Override Jimaku API base URL | diff --git a/docs-site/docs-sync.test.ts b/docs-site/docs-sync.test.ts index 94786d8..6e7bf0b 100644 --- a/docs-site/docs-sync.test.ts +++ b/docs-site/docs-sync.test.ts @@ -40,6 +40,8 @@ test('docs reflect current launcher and release surfaces', () => { expect(ankiIntegrationContents).not.toContain('alwaysUseAiTranslation'); expect(ankiIntegrationContents).not.toContain('targetLanguage'); expect(configurationContents).not.toContain('youtubeSubgen": {\n "mode"'); + expect(configurationContents).not.toContain('youtubeSubgen.primarySubLanguages'); + expect(configurationContents).toContain('youtube.primarySubLanguages'); expect(configurationContents).toContain('### Shared AI Provider'); expect(changelogContents).toContain('## v0.5.1 (2026-03-09)'); diff --git a/docs-site/installation.md b/docs-site/installation.md index 040cece..a2aff07 100644 --- a/docs-site/installation.md +++ b/docs-site/installation.md @@ -34,8 +34,8 @@ | chafa | Thumbnail previews in fzf | | ffmpegthumbnailer | Generate video thumbnails for picker | | guessit | Better AniSkip title/season/episode parsing for file playback | -| alass | Subtitle sync engine (preferred) | -| ffsubsync | Subtitle sync engine (fallback) | +| alass | Subtitle sync engine (preferred) — must be on `PATH` or set `subsync.alass_path` in config; subtitle syncing is disabled without it or ffsubsync | +| ffsubsync | Subtitle sync engine (fallback) — must be on `PATH` or set `subsync.ffsubsync_path` in config; subtitle syncing is disabled without it or alass | ## Linux diff --git a/docs-site/ipc-contracts.md b/docs-site/ipc-contracts.md index eb0f328..28039bf 100644 --- a/docs-site/ipc-contracts.md +++ b/docs-site/ipc-contracts.md @@ -9,7 +9,7 @@ The contract system enforces this by making channel names, payload shapes, and v Renderer-initiated calls (`invoke`) pass through four boundaries before reaching a service. Fire-and-forget messages (`send`) follow the same path but skip the response leg. Malformed payloads are caught at the validator and never reach domain code. ```mermaid -flowchart LR +flowchart TB classDef rend fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef bridge fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef valid fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px diff --git a/docs-site/jimaku-integration.md b/docs-site/jimaku-integration.md new file mode 100644 index 0000000..4d74bd6 --- /dev/null +++ b/docs-site/jimaku-integration.md @@ -0,0 +1,136 @@ +# Jimaku Integration + +[Jimaku](https://jimaku.cc) is a community-driven subtitle repository for anime. SubMiner integrates with the Jimaku API so you can search, browse, and download Japanese subtitle files directly from the overlay — no alt-tabbing or manual file management required. Downloaded subtitles are loaded into mpv immediately. + +## How It Works + +The Jimaku integration runs through an in-overlay modal accessible via a keyboard shortcut (`Ctrl+Shift+J` by default). + +When you open the modal, SubMiner parses the current video filename to extract a title, season, and episode number. Common naming conventions are supported — `S01E03`, `1x03`, `E03`, and dash-separated episode numbers all work. If the filename yields a high-confidence match (title + episode), SubMiner auto-searches immediately. + +From there: + +1. **Search** — SubMiner queries the Jimaku API with the parsed title. Results appear as a list of anime entries (Japanese and English names). +2. **Browse entries** — Select an entry to load its available subtitle files, filtered by episode if one was detected. +3. **Browse files** — Files show name, size, and last-modified date. If a language preference is configured, files are sorted accordingly (e.g., Japanese-tagged files first). +4. **Download** — Selecting a file downloads it to the same directory as the video (or a temp directory for remote/streamed media) and loads it into mpv as a new subtitle track. + +If no files match the current episode filter, a "Show all files" button lets you broaden the search to all episodes for that entry. + +### Modal Keyboard Shortcuts + +| Key | Action | +| --- | --- | +| `Enter` (in text field) | Search | +| `Enter` (in list) | Select entry / download file | +| `Arrow Up` / `Arrow Down` | Navigate entries or files | +| `Escape` | Close modal | + +### Flow + +```mermaid +flowchart TD + classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef action fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef result fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px + classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px + + Open["Open Jimaku modal (Ctrl+Shift+J)"]:::step + Parse["Auto-fill title, season, episode from filename"]:::enrich + Search["Search Jimaku API"]:::action + Entries["Browse matching entries"]:::action + Files["Browse subtitle files"]:::action + Download["Download selected file"]:::action + Load["Load subtitle into mpv"]:::result + + Open --> Parse + Parse --> Search + Search --> Entries + Entries --> Files + Files --> Download + Download --> Load +``` + +## Configuration + +Add a `jimaku` section to your `config.jsonc`: + +```jsonc +{ + "jimaku": { + "apiKey": "YOUR_API_KEY", + "apiKeyCommand": "cat ~/.jimaku_key", + "apiBaseUrl": "https://jimaku.cc", + "languagePreference": "ja", + "maxEntryResults": 10 + } +} +``` + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `jimaku.apiKey` | `string` | — | Jimaku API key (plaintext). Mutually exclusive with `apiKeyCommand`. | +| `jimaku.apiKeyCommand` | `string` | — | Shell command that prints the API key to stdout. Useful for secret managers (e.g., `pass jimaku/api-key`). | +| `jimaku.apiBaseUrl` | `string` | `"https://jimaku.cc"` | Base URL for the Jimaku API. Only change this if using a mirror or local instance. | +| `jimaku.languagePreference` | `"ja"` \| `"en"` \| `"none"` | `"ja"` | Sort subtitle files by language tag. `"ja"` pushes Japanese-tagged files to the top; `"en"` does the same for English. `"none"` preserves the API order. | +| `jimaku.maxEntryResults` | `number` | `10` | Maximum number of anime entries returned per search. | + +The keyboard shortcut is configured separately under `shortcuts`: + +```jsonc +{ + "shortcuts": { + "openJimaku": "Ctrl+Shift+J" + } +} +``` + +### API Key + +An API key is required to use the Jimaku integration. You can get one from [jimaku.cc](https://jimaku.cc). There are two ways to provide it: + +- **`apiKey`** — set the key directly in config. Simple, but the key is stored in plaintext. +- **`apiKeyCommand`** — a shell command that outputs the key. Runs with a 10-second timeout. Preferred if you use a secret manager like `pass`, `gpg`, or a keychain tool. + +If both are set, `apiKey` takes priority. + +## Filename Parsing + +SubMiner extracts media info from the current video path to pre-fill the search fields. The parser handles: + +- **Season + episode patterns:** `S01E03`, `1x03` +- **Episode-only patterns:** `E03`, `EP03`, or dash-separated numbers like `Title - 03 -` +- **Bracket tags:** `[SubGroup]`, `[1080p]`, `[HEVC]` — stripped before title extraction +- **Year tags:** `(2024)` — stripped +- **Dots and underscores:** treated as spaces +- **Remote/streamed URLs:** SubMiner checks URL query parameters (`title`, `name`, `q`) and path segments to extract a meaningful title + +If the parser produces a high-confidence result (title + episode both detected), the search runs automatically when the modal opens. Otherwise, you can adjust the fields manually before searching. + +## Troubleshooting + +**"Jimaku API key not set"** + +Configure `jimaku.apiKey` or `jimaku.apiKeyCommand` in your config. If using `apiKeyCommand`, verify the command works in your shell: it should print the key and exit cleanly. + +**"Jimaku request failed" or HTTP 429** + +The Jimaku API has rate limits. If you see 429 errors, wait for the retry duration shown in the OSD message and try again. An API key provides higher rate limits. + +**No entries found** + +Try simplifying the title — remove season/episode qualifiers and search with just the anime name. Jimaku's search matches against its own database of anime titles, so the exact spelling matters. + +**No files found for this episode** + +The entry may not have per-episode files, or files may be named differently. Click "Show all files" to see everything available for the entry. + +**Downloaded subtitle not loading** + +Verify mpv is running and connected via IPC. SubMiner loads the subtitle by issuing a `sub-add` command over the mpv socket. If mpv is not connected, the download succeeds but the subtitle cannot be loaded. + +## Related + +- [Configuration Reference](/configuration#jimaku) — full config section +- [Mining Workflow](/mining-workflow#jimaku-subtitle-search) — how Jimaku fits into the sentence mining loop +- [Troubleshooting](/troubleshooting#jimaku) — additional error guidance diff --git a/docs-site/mining-workflow.md b/docs-site/mining-workflow.md index 0d17b65..d410786 100644 --- a/docs-site/mining-workflow.md +++ b/docs-site/mining-workflow.md @@ -7,7 +7,7 @@ This guide walks through the sentence mining loop — from watching a video to c SubMiner runs as a transparent overlay on top of mpv. As subtitles play, the overlay displays them as interactive text. You hover a word, trigger Yomitan lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, audio clip, and screenshot. ```mermaid -flowchart LR +flowchart TB classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef action fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef result fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px diff --git a/docs-site/public/assets/fonts/Mplus1-Medium.ttf b/docs-site/public/assets/fonts/Mplus1-Medium.ttf new file mode 100644 index 0000000..be9f8a0 Binary files /dev/null and b/docs-site/public/assets/fonts/Mplus1-Medium.ttf differ diff --git a/docs-site/public/assets/fonts/manrope-latin-600-normal.ttf b/docs-site/public/assets/fonts/manrope-latin-600-normal.ttf new file mode 100644 index 0000000..34c1284 Binary files /dev/null and b/docs-site/public/assets/fonts/manrope-latin-600-normal.ttf differ diff --git a/docs-site/public/config.example.jsonc b/docs-site/public/config.example.jsonc index b94e0c2..2f5c223 100644 --- a/docs-site/public/config.example.jsonc +++ b/docs-site/public/config.example.jsonc @@ -417,20 +417,11 @@ // YouTube Playback Settings // Defaults for SubMiner YouTube subtitle loading and languages. // ========================================== - "youtubeSubgen": { - "whisperBin": "", // Legacy compatibility path kept for external subtitle fallback tools; not used by default. - "whisperModel": "", // Legacy compatibility model path kept for external subtitle fallback tooling; not used by default. - "whisperVadModel": "", // Legacy compatibility VAD path kept for external subtitle fallback tooling; not used by default. - "whisperThreads": 4, // Legacy thread tuning for subtitle fallback tooling; not used by default. - "fixWithAi": false, // Legacy subtitle fallback post-processing switch kept for compatibility; use is currently disabled by default. Values: true | false - "ai": { - "model": "", // Optional model override for legacy subtitle fallback post-processing; not used by default. - "systemPrompt": "" // Optional system prompt override for legacy subtitle fallback post-processing; not used by default. - }, // Ai setting. + "youtube": { "primarySubLanguages": [ "ja", "jpn" - ] // Comma-separated primary subtitle language priority used by the launcher. + ] // Comma-separated primary subtitle language priority for YouTube auto-loading. }, // Defaults for SubMiner YouTube subtitle loading and languages. // ========================================== diff --git a/docs-site/troubleshooting.md b/docs-site/troubleshooting.md index cadca60..e3a599c 100644 --- a/docs-site/troubleshooting.md +++ b/docs-site/troubleshooting.md @@ -252,13 +252,24 @@ Resume playback and wait for the next subtitle to appear, then try mining again. ## Subtitle Sync (Subsync) +Both **alass** and **ffsubsync** are optional external dependencies. Subtitle syncing requires at least one of them to be installed. + **"Configured alass executable not found"** Install alass or configure the path: -- **Arch Linux (AUR)**: `yay -S alass-git` +- **Arch Linux (AUR)**: `paru -S alass` +- **Cargo**: `cargo install alass-cli` - Set the path: `subsync.alass_path` in your config. +**"Configured ffsubsync executable not found"** + +Install ffsubsync or configure the path: + +- **Arch Linux (AUR)**: `paru -S python-ffsubsync` +- **pip**: `pip install ffsubsync` +- Must be on `PATH` or configured via `subsync.ffsubsync_path` in your config. + **"Subtitle synchronization failed"** SubMiner tries alass first, then falls back to ffsubsync. If both fail: @@ -266,6 +277,7 @@ SubMiner tries alass first, then falls back to ffsubsync. If both fail: - Ensure the reference subtitle track exists in the video (alass requires a source track). - Check that `ffmpeg` is available (used to extract the internal subtitle track). - Try running the sync tool manually to see detailed error output. +- ffsubsync requires local files and cannot handle remote media streams (e.g., streaming URLs). ## Jimaku diff --git a/docs-site/usage.md b/docs-site/usage.md index 40de30b..b5287db 100644 --- a/docs-site/usage.md +++ b/docs-site/usage.md @@ -234,9 +234,9 @@ Notes: - Press `Ctrl+Alt+C` during active YouTube playback to open the manual YouTube subtitle picker and retry track selection. - For YouTube URLs, `subminer` probes available YouTube subtitle tracks, reuses existing authoritative tracks when available, and downloads only missing sides. - Native mpv secondary subtitle rendering stays hidden so the overlay remains the visible secondary subtitle surface. -- Primary subtitle target languages come from `youtubeSubgen.primarySubLanguages` (defaults to `["ja","jpn"]`). +- Primary subtitle target languages come from `youtube.primarySubLanguages` (defaults to `["ja","jpn"]`). - Secondary target languages come from `secondarySub.secondarySubLanguages` (defaults to English if unset). -- Configure defaults in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`) under `youtubeSubgen` and `secondarySub`. +- Configure defaults in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`) under `youtube` and `secondarySub`. ## Controller Support diff --git a/docs-site/youtube-integration.md b/docs-site/youtube-integration.md new file mode 100644 index 0000000..81e58f0 --- /dev/null +++ b/docs-site/youtube-integration.md @@ -0,0 +1,145 @@ +# YouTube Integration + +SubMiner auto-loads Japanese subtitles when you play a YouTube URL, giving you the same sentence-mining overlay experience as local video files. It probes available subtitle tracks via `yt-dlp`, selects the best primary and secondary tracks, downloads them, and loads them into mpv before playback resumes. + +## Requirements + +- **yt-dlp** must be installed and on `PATH` (or set `SUBMINER_YTDLP_BIN` to its path) +- mpv with `--input-ipc-server` configured (handled automatically by the `subminer` launcher) + +## How It Works + +When SubMiner detects a YouTube URL (or `ytsearch:` target), it pauses mpv at startup and runs a subtitle pipeline before resuming playback: + +1. **Probe** --- `yt-dlp --dump-single-json` extracts all available subtitle tracks (manual uploads and auto-generated captions) along with video metadata. +2. **Discover** --- Each track is normalized into a `YoutubeTrackOption` with language code, kind (`manual` or `auto`), display label, and direct download URL. +3. **Select** --- SubMiner picks the best primary track (Japanese, preferring manual over auto) and secondary track (English, preferring manual over auto). +4. **Download** --- Selected tracks are fetched via direct URL when available, falling back to `yt-dlp --write-subs` / `--write-auto-subs`. YouTube TimedText XML formats (`srv1`/`srv2`/`srv3`) are converted to VTT on the fly. Auto-generated VTT captions are normalized to remove rolling-caption duplication. +5. **Load** --- Subtitle files are injected into mpv via `sub-add`. Playback resumes once the primary track is ready; secondary failures do not block. + +## Pipeline Diagram + +```mermaid +flowchart TD + classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a + classDef action fill:#8aadf4,stroke:#494d64,color:#24273a + classDef result fill:#a6da95,stroke:#494d64,color:#24273a + classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a + classDef ext fill:#eed49f,stroke:#494d64,color:#24273a + + A[YouTube URL detected]:::step + B[yt-dlp probe]:::ext + C[Track discovery]:::action + D{Auto or manual selection?}:::step + E[Auto-select best tracks]:::action + F[Manual picker — Ctrl+Alt+C]:::action + G[Download subtitle files]:::action + H[Convert TimedText to VTT]:::enrich + I[Normalize auto-caption duplicates]:::enrich + K[sub-add into mpv]:::action + L[Overlay renders subtitles]:::result + + A --> B + B --> C + C --> D + D -- startup --> E + D -- user request --> F + E --> G + F --> G + G --> H + H --> I + I --> K + K --> L +``` + +## Auto-Load Flow + +On startup with a YouTube URL: + +1. mpv launches paused. +2. SubMiner calls `yt-dlp --dump-single-json` to probe all subtitle tracks. +3. Tracks are split into **manual** (human-uploaded) and **auto** (machine-generated) categories. +4. The selection algorithm picks: + - **Primary**: first Japanese manual track, then Japanese auto track, then any manual track, then first available track. + - **Secondary**: first English manual track, then English auto track (excluding the primary). +5. If mpv already exposes an authoritative matching track, SubMiner reuses it instead of downloading again. +6. Missing tracks are downloaded to a temp directory and loaded via `sub-add`. +7. Playback unpauses once the primary subtitle is ready. + +## Manual Subtitle Picker + +Press **Ctrl+Alt+C** during YouTube playback to open the subtitle picker overlay. This lets you: + +- Browse all discovered tracks (manual and auto-generated) +- Select different primary and secondary tracks +- Retry track loading if the auto-load failed or picked the wrong track + +The picker displays each track with its language, kind (manual/auto), and title when available. + +## Subtitle Format Handling + +SubMiner handles several YouTube subtitle formats transparently: + +| Format | Handling | +| ------ | -------- | +| `srt`, `vtt` | Used directly (preferred for manual tracks) | +| `srv1`, `srv2`, `srv3` | YouTube TimedText XML --- converted to VTT automatically | +| Auto-generated VTT | Normalized to remove rolling-caption text duplication | + +For auto-generated tracks, SubMiner prefers `srv3` > `srv2` > `srv1` > `vtt` (TimedText XML produces cleaner output). For manual tracks, `srt` > `vtt` is preferred. + +## Configuration Reference + +### Primary Subtitle Languages + +```jsonc +{ + "youtube": { + "primarySubLanguages": ["ja", "jpn"] + } +} +``` + +| Option | Type | Description | +| ------ | ---- | ----------- | +| `primarySubLanguages` | `string[]` | Language priority for YouTube primary subtitle auto-loading (default `["ja", "jpn"]`) | + +### Secondary Subtitle Languages + +Secondary track selection uses the shared `secondarySub` config: + +```jsonc +{ + "secondarySub": { + "secondarySubLanguages": ["eng", "en"], + "autoLoadSecondarySub": true, + "defaultMode": "hover" + } +} +``` + +| Option | Type | Description | +| ------ | ---- | ----------- | +| `secondarySubLanguages` | `string[]` | Language codes for secondary subtitle auto-loading (default: English) | +| `autoLoadSecondarySub` | `boolean` | Auto-detect and load matching secondary track | +| `defaultMode` | `"hidden"` / `"visible"` / `"hover"` | Initial display mode for secondary subtitles (default: `"hover"`) | + +Precedence: CLI flag > environment variable > `config.jsonc` > built-in default. + +## Limitations and Troubleshooting + +- **No subtitles found**: The video may not have Japanese subtitles. Open the picker with `Ctrl+Alt+C` to see all available tracks. +- **yt-dlp not found**: Install `yt-dlp` and ensure it is on `PATH`, or set `SUBMINER_YTDLP_BIN` to the binary path. +- **Probe timeout**: `yt-dlp` has a 15-second timeout per operation. Slow connections or rate-limited IPs may hit this. Retry or update `yt-dlp`. +- **Auto-caption quality**: YouTube auto-generated captions vary in quality. Manual subtitles (when available) are always preferred. +- **`ytsearch:` targets**: `subminer ytsearch:"keyword"` plays the first search result. Subtitle availability depends on the matched video. +- **Secondary subtitle fails**: Secondary track failures never block playback. The primary subtitle loads independently. +- **Native mpv secondary rendering**: Stays hidden during YouTube flows so the SubMiner overlay remains the visible secondary subtitle surface. + +## Related Pages + +- [Usage --- YouTube Playback](/usage#youtube-playback) +- [Configuration --- YouTube Playback Settings](/configuration#youtube-playback-settings) +- [Configuration --- Secondary Subtitle](/configuration#secondary-subtitle) +- [Keyboard Shortcuts](/shortcuts) +- [Jellyfin Integration](/jellyfin-integration)