docs: apply M PLUS 1 and Manrope font defaults for docs content

This commit is contained in:
2026-03-25 23:52:29 -07:00
parent 508864bcbb
commit 61d15f9431
21 changed files with 593 additions and 78 deletions

View File

@@ -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' },
],
},

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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=<backend>` (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

View File

@@ -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.

View File

@@ -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 <youtube-url>` 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.

View File

@@ -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

View File

@@ -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 `<name>_retimed.<ext>`. |
@@ -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.

View File

@@ -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 |

View File

@@ -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)');

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

View File

@@ -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.
// ==========================================

View File

@@ -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

View File

@@ -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

View File

@@ -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)