mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-01 07:21:33 -07:00
feat(anime): add anime browser powered by Aniyomi extensions
- Add `subminer anime` / `--anime` and a tray entry to open a browser that searches installed Aniyomi extension sources, shows cover art and episodes, and plays into mpv with overlay/mining attached - Add an Extensions tab to add repos and install/update/remove sources, and per-source settings for sources needing config - Support searching all sources at once with streaming, per-source results and status - Prefer Japanese audio/subtitle tracks from the source and keep the primary subtitle slot reserved for Japanese - Fix window/tray/Dock handling so the browser and mpv can be switched between without quitting the app or losing the Dock icon - Add anime.repos, anime.extensionsDir, anime.preferredQuality config keys (no bundled repos or discovery)
This commit is contained in:
@@ -327,6 +327,7 @@ const sidebar: DefaultTheme.SidebarItem[] = [
|
||||
{ text: 'Anki', link: '/anki-integration' },
|
||||
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
||||
{ text: 'YouTube', link: '/youtube-integration' },
|
||||
{ text: 'Anime Browser', link: '/anime-browser' },
|
||||
{ text: 'Jimaku', link: '/jimaku-integration' },
|
||||
{ text: 'TsukiHime', link: '/tsukihime-integration' },
|
||||
{ text: 'AniList', link: '/anilist-integration' },
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 0;
|
||||
background: var(--vp-c-bg);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 24px 64px rgba(0, 0, 0, 0.2);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 24px 64px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -62,7 +64,9 @@
|
||||
font-family: var(--tui-font-mono);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color 180ms ease, color 180ms ease;
|
||||
transition:
|
||||
border-color 180ms ease,
|
||||
color 180ms ease;
|
||||
}
|
||||
|
||||
.mermaid-modal__close:hover {
|
||||
|
||||
@@ -10,7 +10,5 @@ test('status line file path formats version archive home without trailing slash'
|
||||
});
|
||||
|
||||
test('status line file path keeps normal docs routes as markdown files', () => {
|
||||
expect(formatStatusLineFilePath('/v/0.12.0/configuration')).toBe(
|
||||
'v/0.12.0/configuration.md',
|
||||
);
|
||||
expect(formatStatusLineFilePath('/v/0.12.0/configuration')).toBe('v/0.12.0/configuration.md');
|
||||
});
|
||||
|
||||
@@ -22,17 +22,8 @@
|
||||
:root {
|
||||
--tui-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||
--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;
|
||||
'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;
|
||||
|
||||
/* Theme-specific values — overridden in .dark below */
|
||||
@@ -96,8 +87,11 @@ button,
|
||||
.VPFeature,
|
||||
.VPNavBarMenuLink,
|
||||
.VPSidebarItem .text {
|
||||
transition: color var(--tui-transition), background var(--tui-transition),
|
||||
border-color var(--tui-transition), opacity var(--tui-transition);
|
||||
transition:
|
||||
color var(--tui-transition),
|
||||
background var(--tui-transition),
|
||||
border-color var(--tui-transition),
|
||||
opacity var(--tui-transition);
|
||||
}
|
||||
|
||||
/* === Nav bar === */
|
||||
@@ -218,8 +212,7 @@ 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;
|
||||
font-family: var(--tui-font-mono), 'M PLUS 1', 'Noto Sans CJK JP', 'Noto Sans JP', monospace;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
@@ -228,8 +221,7 @@ 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;
|
||||
font-family: var(--tui-font-mono), 'M PLUS 1', 'Noto Sans CJK JP', 'Noto Sans JP', monospace;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
@@ -273,7 +265,9 @@ button,
|
||||
.vp-doc a {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--tui-link-underline);
|
||||
transition: border-color var(--tui-transition), color var(--tui-transition);
|
||||
transition:
|
||||
border-color var(--tui-transition),
|
||||
color var(--tui-transition);
|
||||
}
|
||||
|
||||
.vp-doc a:hover {
|
||||
@@ -312,20 +306,45 @@ button,
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
.vp-doc .custom-block.tip { border-color: var(--vp-c-brand-1); }
|
||||
.vp-doc .custom-block.tip::before { content: '-- tip'; color: var(--vp-c-brand-1); }
|
||||
.vp-doc .custom-block.tip {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
.vp-doc .custom-block.tip::before {
|
||||
content: '-- tip';
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.vp-doc .custom-block.info { border-color: var(--vp-c-brand-2); }
|
||||
.vp-doc .custom-block.info::before { content: '-- info'; color: var(--vp-c-brand-2); }
|
||||
.vp-doc .custom-block.info {
|
||||
border-color: var(--vp-c-brand-2);
|
||||
}
|
||||
.vp-doc .custom-block.info::before {
|
||||
content: '-- info';
|
||||
color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.vp-doc .custom-block.warning { border-color: var(--vp-c-warning-1); }
|
||||
.vp-doc .custom-block.warning::before { content: '-- warning'; color: var(--vp-c-warning-1); }
|
||||
.vp-doc .custom-block.warning {
|
||||
border-color: var(--vp-c-warning-1);
|
||||
}
|
||||
.vp-doc .custom-block.warning::before {
|
||||
content: '-- warning';
|
||||
color: var(--vp-c-warning-1);
|
||||
}
|
||||
|
||||
.vp-doc .custom-block.danger { border-color: var(--vp-c-danger-1); }
|
||||
.vp-doc .custom-block.danger::before { content: '-- danger'; color: var(--vp-c-danger-1); }
|
||||
.vp-doc .custom-block.danger {
|
||||
border-color: var(--vp-c-danger-1);
|
||||
}
|
||||
.vp-doc .custom-block.danger::before {
|
||||
content: '-- danger';
|
||||
color: var(--vp-c-danger-1);
|
||||
}
|
||||
|
||||
.vp-doc .custom-block.details { border-color: var(--vp-c-divider); }
|
||||
.vp-doc .custom-block.details::before { content: '-- details'; color: var(--vp-c-text-2); }
|
||||
.vp-doc .custom-block.details {
|
||||
border-color: var(--vp-c-divider);
|
||||
}
|
||||
.vp-doc .custom-block.details::before {
|
||||
content: '-- details';
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.vp-doc .custom-block .custom-block-title {
|
||||
font-family: var(--tui-font-mono);
|
||||
@@ -413,11 +432,15 @@ button,
|
||||
}
|
||||
|
||||
@keyframes tui-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* === Statusline === */
|
||||
.tui-statusline {
|
||||
position: fixed;
|
||||
@@ -457,7 +480,7 @@ button,
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.tui-statusline__mode[data-mode="HOME"] {
|
||||
.tui-statusline__mode[data-mode='HOME'] {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
@@ -561,7 +584,9 @@ body {
|
||||
.VPFeatures .VPFeature {
|
||||
border-radius: 8px !important;
|
||||
border: 1px solid var(--vp-c-divider) !important;
|
||||
transition: border-color var(--tui-transition), background var(--tui-transition),
|
||||
transition:
|
||||
border-color var(--tui-transition),
|
||||
background var(--tui-transition),
|
||||
transform var(--tui-transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -672,11 +697,7 @@ body {
|
||||
transform: translateX(-50%);
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
var(--tui-hero-glow) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
background: radial-gradient(ellipse at center, var(--tui-hero-glow) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Anime Browser
|
||||
|
||||
Search anime sources, pick an episode, and play it in mpv with SubMiner's overlay
|
||||
and mining tools attached — the same way a local file or a Jellyfin stream works.
|
||||
|
||||
Open it with `subminer anime`, with `SubMiner.AppImage --anime`, or from
|
||||
**Browse Anime** in the tray menu. The window stays open while you watch, so you
|
||||
can queue the next episode without reopening it.
|
||||
|
||||
While the window is open, SubMiner shows a tray icon and — on macOS — appears
|
||||
in the Cmd+Tab switcher and the Dock (macOS ties the two together), so you can
|
||||
flip between the browser and mpv. SubMiner normally hides itself from the Dock
|
||||
because the subtitle overlay needs that to float above fullscreen video; it
|
||||
hides again when the window closes during playback.
|
||||
|
||||
Launching an episode starts a full SubMiner playback session, the same as
|
||||
playing a local file: the overlay and mining tools attach, and the tray icon
|
||||
stays available. In standalone `subminer anime` mode, closing the window while
|
||||
a video is playing leaves playback running — reopen the browser from the tray
|
||||
(**Browse Anime**). The app only exits with the window when nothing is playing.
|
||||
|
||||
## How it works
|
||||
|
||||
SubMiner does not implement any anime source itself. It runs **Aniyomi extension
|
||||
APKs** through a bundled JVM sidecar ([M-Extension-Server][mes]), asks the
|
||||
selected extension to resolve an episode, and hands the resulting URL to mpv.
|
||||
|
||||
```
|
||||
extension APK → bridge (JVM) → { url, headers } → mpv → SubMiner overlay
|
||||
```
|
||||
|
||||
Because the extension resolves the stream, whichever sources you install decide
|
||||
what is available. SubMiner only hosts them.
|
||||
|
||||
## Installing extensions
|
||||
|
||||
**SubMiner ships no extension repositories and bundles no sources.** There is no
|
||||
default repository, no suggested list, and no discovery. Until you add one, the
|
||||
browser has nothing to search — that is deliberate, and it is what keeps SubMiner
|
||||
a neutral host rather than a distributor.
|
||||
|
||||
There are two ways to add extensions.
|
||||
|
||||
### From a repository
|
||||
|
||||
The window has three tabs — **Browse**, **Extensions**, and **Source settings** —
|
||||
and each one fills the window, so a long extension list is not squeezed in above
|
||||
the search results.
|
||||
|
||||
Open the **Extensions** tab, paste a repository index URL, and choose
|
||||
**Add repository**. The URL must be `https` and point at a `.json` index file —
|
||||
`index.min.json` is the common Aniyomi name, but repositories are free to publish
|
||||
under another one (for example `video.min.json`). Anything else is rejected
|
||||
immediately rather than failing later. Everything before the file name is treated
|
||||
as the repository root, so `.apk` and icon URLs are resolved relative to it.
|
||||
|
||||
Extensions your repositories offer but you do not have appear under
|
||||
**Available**, each with **Install**. Repositories are stored in config under
|
||||
`anime.repos`, so you can also manage them there and keep them in a dotfile.
|
||||
|
||||
### Managing what is installed
|
||||
|
||||
The Extensions tab opens with an **Installed** section listing everything in the
|
||||
extensions directory, with the sources each one provides and a **Remove**
|
||||
button. It is built from the directory rather than from a repository, so an
|
||||
extension you dropped in by hand — or one whose repository you have since
|
||||
removed — is still listed and still removable.
|
||||
|
||||
**Update** appears next to an extension a configured repository still carries;
|
||||
it downloads the current version over the existing APK.
|
||||
|
||||
### From a file
|
||||
|
||||
Drop Aniyomi `.apk` files into the extensions directory, shown at the top of the
|
||||
Extensions tab. It defaults to `<userData>/anime-extensions` — on macOS,
|
||||
`~/Library/Application Support/SubMiner/anime-extensions` — and can be moved with
|
||||
`anime.extensionsDir`.
|
||||
|
||||
A single APK may provide several sources; each appears separately in the
|
||||
**Source** picker. Extensions that fail to load are listed in the Installed
|
||||
section with the reason, so a broken APK is visible rather than silently
|
||||
missing.
|
||||
|
||||
An extension that fails to load is skipped rather than blocking the others, so
|
||||
one bad APK will not hide the rest.
|
||||
|
||||
## Searching every source at once
|
||||
|
||||
With more than one source installed, the **Source** picker gains an
|
||||
**All sources** entry. Searching with it selected runs the query against every
|
||||
installed source at once, and each source's results appear the moment that
|
||||
source answers — a fast source is on screen while a slow one is still
|
||||
resolving. The status bar counts sources as they finish
|
||||
(`Searching… 3/5 sources · 42 results`).
|
||||
|
||||
Each cover is labelled with the source it came from, and opening one always
|
||||
queries that source, whatever the picker says afterwards.
|
||||
|
||||
A source that errors is named in the status bar and the rest still show their
|
||||
results; one extension that needs a login cannot blank the grid. If every
|
||||
source fails, the first error is shown in full.
|
||||
|
||||
Typing a new search while one is still running simply starts over: results
|
||||
from the superseded search are discarded, even if its sources answer late.
|
||||
|
||||
Source settings belong to a single extension, so the **Source settings** tab
|
||||
asks you to pick one while **All sources** is selected.
|
||||
|
||||
## Settings
|
||||
|
||||
| Key | Purpose |
|
||||
| ------------------------ | -------------------------------------------------------------------- |
|
||||
| `anime.repos` | Repository index URLs. Empty by default. |
|
||||
| `anime.extensionsDir` | Where APKs are read from. Empty uses `<userData>/anime-extensions`. |
|
||||
| `anime.preferredQuality` | Preferred stream label, matched as a substring (for example `1080`). |
|
||||
|
||||
## Source settings
|
||||
|
||||
Most extensions need configuration before they return anything — a server
|
||||
address and credentials, a preferred quality, a language filter. Open the
|
||||
**Source settings** tab to edit them. Changes save as you make them and
|
||||
persist across restarts in `<userData>/anime-source-preferences.json`.
|
||||
|
||||
Each save is handed back to the extension, so it can react: the Jellyfin source
|
||||
logs in when the address and password land, then fills in its media-library
|
||||
picker. Password-like fields are masked. Because that file can hold
|
||||
credentials, it is written with owner-only permissions.
|
||||
|
||||
## The bridge
|
||||
|
||||
The first launch downloads a platform bundle (~130 MB) containing the server and
|
||||
a matching Java runtime, so no system JDK is required. It is verified against a
|
||||
pinned SHA-256 before running, unpacked into `<userData>/anime-bridge`, and
|
||||
reused after that. Progress appears in the banner at the top of the window.
|
||||
|
||||
The bridge stays running while the window is open. Resolved video URLs point at
|
||||
its own loopback proxy so the extension's cookies and headers apply, which means
|
||||
those URLs stop working once it exits — the window keeps it alive for the whole
|
||||
session.
|
||||
|
||||
Two known limits:
|
||||
|
||||
- There is no Android WebView, so extensions that need one (typically for
|
||||
Cloudflare challenges) will fail with an error from the source.
|
||||
- Bundles are published for macOS (arm64, x64), Linux (x64), and Windows (x64).
|
||||
Other platforms are unsupported.
|
||||
|
||||
## Playback
|
||||
|
||||
Selecting an episode resolves the best available stream, applies the source's
|
||||
required headers as mpv `http-header-fields`, and loads it. The headers are
|
||||
readable back off mpv, so Anki card audio and screenshots fetch correctly too.
|
||||
|
||||
### Japanese audio, and switching tracks
|
||||
|
||||
Sources often return a dub and the original audio as two separate entries — or
|
||||
as two audio tracks of one stream — and the dub is frequently listed first.
|
||||
SubMiner always aims at the Japanese audio:
|
||||
|
||||
- Entries labelled as a dub are skipped as long as another entry exists. This
|
||||
outranks `anime.preferredQuality`: a 1080p dub is the wrong file, not a better
|
||||
one. If every entry is a dub, it still plays.
|
||||
- mpv's `alang` is set to `ja,jpn,jp,japanese` before the file loads, so a
|
||||
stream carrying several audio tracks starts on the Japanese one. With no
|
||||
Japanese track, mpv falls back to the first one as usual.
|
||||
- Any audio or subtitle tracks the extension supplies separately are added to
|
||||
mpv with `audio-add` / `sub-add`, tagged with their language, and the
|
||||
Japanese one is selected.
|
||||
|
||||
The primary subtitle slot is reserved for Japanese — it is what the overlay
|
||||
mines. A source that only carries, say, English subtitles does not get them
|
||||
promoted to primary; instead the track is added with a normalized language tag
|
||||
(`English` → `en`), and the regular [dual-subtitle settings](configuration.md)
|
||||
apply: with `secondarySub.autoLoadSecondarySub` enabled and the language listed
|
||||
in `secondarySub.secondarySubLanguages`, it is picked up as the secondary
|
||||
subtitle, exactly as it would be for a local file.
|
||||
|
||||
Every track is added, including the ones that are not selected, so all of them
|
||||
appear in mpv's track menu and can be switched by hand while watching
|
||||
(`#` cycles audio, `j` cycles subtitles by default).
|
||||
|
||||
[mes]: https://github.com/1Selxo/M-Extension-Server
|
||||
@@ -45,9 +45,9 @@ Results are cached per file for the app session; only definitive "no intro found
|
||||
|
||||
You can trigger AniSkip actions from mpv script-messages:
|
||||
|
||||
| Command | Effect |
|
||||
| ------- | ------ |
|
||||
| `script-message subminer-skip-intro` | Skip to the intro end immediately (same as pressing the key) |
|
||||
| Command | Effect |
|
||||
| ----------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `script-message subminer-skip-intro` | Skip to the intro end immediately (same as pressing the key) |
|
||||
| `script-message subminer-aniskip-refresh` | Force a fresh lookup for the current file, discarding any cached result |
|
||||
|
||||
These are handled by the SubMiner app over the IPC socket.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## v0.19.0 (2026-07-29)
|
||||
|
||||
**Added**
|
||||
|
||||
- Anki Maturity Highlighting: Known-word subtitle highlights can now be colored by Anki card maturity (new, learning, young, mature), similar to asbplayer. Tier thresholds and colors are configurable, with a runtime toggle and an updated help legend.
|
||||
- Post-Playback Menu: After a watch-history episode ends, the fzf/rofi launcher returns to that series with options to play the previous or next episode, rewatch, pick another episode, or quit. The pre-playback series menu now offers the previous episode too.
|
||||
- Delete Library Entries: The stats Library detail view can now delete an entire title in one step (episodes, sessions, subtitle lines, rollups, cover art, and vocabulary counts). Delete progress is now shown app-wide via a progress bar and status toast instead of disappearing when you switch tabs.
|
||||
@@ -10,9 +11,11 @@
|
||||
- TsukiHime Subtitle Downloads: Added subtitle downloads for the current video via TsukiHime, loading Japanese as the primary track and your configured secondary language directly into mpv.
|
||||
|
||||
**Changed**
|
||||
|
||||
- Clipboard-Video Shortcut: The "append clipboard video to queue" shortcut is now configurable.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- AniList Season Resolution: Season 2+ files now resolve to the correct AniList entry instead of silently falling back to season 1 (which mismatched character dictionaries and watch progress). Manual overrides now stay scoped per season, fix both the dictionary and progress tracking together, and also correct per-season cover art.
|
||||
- Subtitle Annotation Accuracy: Fixed several annotation edge cases, including inconsistent POS exclusions on merged quote-particle tokens, dropped annotations on supplementary-plane kanji, katakana punctuation wrongly treated as noise, and certain kanji vocabulary losing N+1 highlighting eligibility.
|
||||
- AnkiConnect Proxy Port Conflict: Video startup no longer crashes when another process already holds the configured AnkiConnect proxy port; a notification now explains how to resolve it.
|
||||
@@ -28,6 +31,7 @@
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
**Internal**
|
||||
|
||||
- Added a golden-file regression test corpus for the tokenizer/annotation pipeline, plus scripts to record new fixtures and diff against stock Yomitan.
|
||||
- Consolidated renderer modal state handling into a descriptor registry.
|
||||
- Consolidated CI quality checks (PR, stable, and prerelease) into one reusable workflow with mpv plugin tests and dependency audits.
|
||||
@@ -44,10 +48,12 @@
|
||||
<h2>v0.18.0 (2026-07-10)</h2>
|
||||
|
||||
**Added**
|
||||
|
||||
- Sentence Audio Normalization: Generated sentence audio is now normalized to -23 LUFS by default, and clips mined from playback mirror mpv's software volume curve with a limiter to prevent clipping. Both behaviors are configurable independently.
|
||||
- Watch History Command: Added `subminer -H` / `--history` to browse watch history, replay or continue episodes, or pick one via fzf or rofi, with cover art shown in the rofi picker.
|
||||
|
||||
**Changed**
|
||||
|
||||
- Fzf Preview Layout: Moved fzf previews below launcher menus, giving long titles and metadata more room.
|
||||
- Known-Word Highlighting: Now compares subtitle and Anki-card readings, preventing false matches between homographs and unrelated words that share a reading, while still supporting matching across kana and kanji spellings.
|
||||
- Annotation Filtering: Standalone suffix tokens (e.g. さん, れる) are now excluded from JLPT/frequency/N+1 highlighting by default, matching how particles and interjections are treated; configurable via the pos2 exclusion setting.
|
||||
@@ -55,6 +61,7 @@
|
||||
- Stats Trend Charts: Overhauled with persisted title visibility, per-chart title limits, "top" and "most recent" ranking modes, an option to show or hide empty days, calendar-aligned periods, and value-sorted tooltips.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- Background Stats Server: `subminer app` background launches now auto-start the stats server when enabled, and skip startup if one is already running.
|
||||
- Character Name Highlighting: Character dictionaries now split unspaced native names more reliably, and portraits, highlights, and hover lookup survive punctuation, unmatched text, and competing dictionary matches without incorrectly splitting longer words.
|
||||
- Highlighting Coverage: Frequency/JLPT highlighting and vocabulary stats now include content adverbs (e.g. 確かに, やはり) and kanji nouns MeCab tags as non-independent (e.g. 日, 点, 以外), while still suppressing interjections, pronouns, and grammar fragments; lexicalized kana expressions like かといって keep their annotations.
|
||||
@@ -68,6 +75,7 @@
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
**Internal**
|
||||
|
||||
- Test lanes moved to `scripts/test-lanes.ts` with per-directory discovery and isolated per-file timeouts; CI now covers previously orphaned stats, scripts, plugin process-retry, and runtime-compat suites, plus a new stats lane in the change-verification workflow.
|
||||
|
||||
</details>
|
||||
@@ -80,15 +88,18 @@
|
||||
<h2>v0.17.2 (2026-06-28)</h2>
|
||||
|
||||
**Fixed**
|
||||
|
||||
- YouTube Background Cache: Fixed Windows YouTube background media cache startup for YouTube URLs opened directly in mpv, including resolved stream URLs when mpv still exposes the original YouTube playlist entry, so queued Anki media updates can append audio and images after the cache finishes.
|
||||
- YouTube Subtitle Picker: Manual subtitle picker requests now show an immediate configured notification while SubMiner probes tracks and opens the modal. Subtitle download progress is replaced with a transient success notification after tracks load.
|
||||
|
||||
<h2>v0.17.1 (2026-06-27)</h2>
|
||||
|
||||
**Added**
|
||||
|
||||
- YouTube Media Cache Mode: Adds `youtube.mediaCache.mode` with `direct` and `background` options. Background mode uses a yt-dlp cache download when direct stream extraction is unreliable — creates a text-only card immediately, queues media updates for mined notes, and fills audio/image fields once the download finishes. Progress is announced via overlay/OSD notifications. Downloads are capped at 720p by default (`youtube.mediaCache.maxHeight`). Switching back to direct mode cancels any in-flight background download.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- Log Export: Fixed log filenames to use the local date so exports around UTC midnight include the current day's logs rather than stale prior-day files. Expanded export redaction to mask IPs, emails, auth and cookie headers, yt-dlp cookie arguments, URL credentials, token/key/password fields, and signed YouTube media URL parameters.
|
||||
- YouTube Card Media: Improved media generation reliability by sending safer ffmpeg options for resolved streams and skipping stale stream maps (including cached YouTube files). Hardened background cache downloads with IPv4 and extractor retry flags; failed downloads now notify the user and clear queued media updates instead of leaving them silently pending. Stale background cache files are cleaned on startup and before each new download.
|
||||
|
||||
@@ -174,6 +185,7 @@
|
||||
<summary>Internal changes</summary>
|
||||
|
||||
**Internal**
|
||||
|
||||
- **Build**: `make deps` now initializes git submodules before installing dependencies on a fresh source checkout.
|
||||
- **Release Tooling**: Release notes now credit contributors and first-time authors resolved from changelog fragments via git and the GitHub API.
|
||||
- **Changelog Guidance**: PR fragment guidance updated to preserve separate-outcome fragments while directing contributors to consolidate same-PR follow-up notes before adding churn.
|
||||
@@ -188,9 +200,11 @@
|
||||
<h2>v0.15.2 (2026-06-02)</h2>
|
||||
|
||||
**Changed**
|
||||
|
||||
- Yomitan: Updated the bundled Yomitan build to the latest vendored revision.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- Anki - Animated AVIF: Clip timing no longer starts or ends early; word-audio lead-in and clip duration are now aligned to frame boundaries.
|
||||
- Overlay (Hyprland): Fixed fullscreen overlay alignment - modal, stats, and sidebar content no longer shift below the mpv window.
|
||||
- Overlay (macOS): Subtitle bars are now interactive immediately after autoplay starts with "wait for overlay to be ready" enabled, without requiring a manual click.
|
||||
|
||||
+10
-10
@@ -38,7 +38,7 @@ flowchart TB
|
||||
|
||||
## Runtime Sockets
|
||||
|
||||
The renderer↔main bridge above lives *inside* the Electron app. A separate set of OS sockets connects the app to the other runtimes - mpv and the launcher/plugin. These carry no renderer payloads and bypass the contract/validator layer; they are command and property channels between processes.
|
||||
The renderer↔main bridge above lives _inside_ the Electron app. A separate set of OS sockets connects the app to the other runtimes - mpv and the launcher/plugin. These carry no renderer payloads and bypass the contract/validator layer; they are command and property channels between processes.
|
||||
|
||||
- **mpv IPC socket** (`/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows): the `MpvIpcClient` in the main process connects here to send JSON commands and subscribe to playback/subtitle properties via `observe_property`. Created by mpv's `--input-ipc-server`.
|
||||
- **App control socket** (`/tmp/subminer-control-<uid>-<hash>.sock`, or a named pipe on Windows): the launcher and the mpv plugin send CLI-style commands (`--start`, `--show-visible-overlay`, `--texthooker`) to a running app here. It also dedupes a second `subminer` invocation into the existing instance instead of launching twice.
|
||||
@@ -69,15 +69,15 @@ How these sockets are established during launch is covered in [Playback Startup
|
||||
|
||||
## Core Surfaces
|
||||
|
||||
| File | Role |
|
||||
| --- | --- |
|
||||
| `src/shared/ipc/contracts.ts` | Canonical channel names and payload type contracts. Single source of truth for both processes. |
|
||||
| `src/shared/ipc/validators.ts` | Runtime payload parsers and type guards. Every `invoke` payload is validated here before the handler runs. |
|
||||
| `src/preload.ts` | Renderer-side bridge. Exposes a typed API surface to the renderer - only approved channels are accessible. |
|
||||
| `src/main/ipc-runtime.ts` | Main-process handler registration and routing. Wires validated channels to domain handlers. |
|
||||
| `src/core/services/ipc.ts` | Service-level invoke handling. Applies guardrails (validation, error wrapping) before calling domain logic. |
|
||||
| `src/core/services/anki-jimaku-ipc.ts` | Integration-specific IPC boundary for Anki and Jimaku operations. |
|
||||
| `src/main/cli-runtime.ts` | CLI/runtime command boundary. Handles commands that originate from the launcher or mpv plugin rather than the renderer. |
|
||||
| File | Role |
|
||||
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `src/shared/ipc/contracts.ts` | Canonical channel names and payload type contracts. Single source of truth for both processes. |
|
||||
| `src/shared/ipc/validators.ts` | Runtime payload parsers and type guards. Every `invoke` payload is validated here before the handler runs. |
|
||||
| `src/preload.ts` | Renderer-side bridge. Exposes a typed API surface to the renderer - only approved channels are accessible. |
|
||||
| `src/main/ipc-runtime.ts` | Main-process handler registration and routing. Wires validated channels to domain handlers. |
|
||||
| `src/core/services/ipc.ts` | Service-level invoke handling. Applies guardrails (validation, error wrapping) before calling domain logic. |
|
||||
| `src/core/services/anki-jimaku-ipc.ts` | Integration-specific IPC boundary for Anki and Jimaku operations. |
|
||||
| `src/main/cli-runtime.ts` | CLI/runtime command boundary. Handles commands that originate from the launcher or mpv plugin rather than the renderer. |
|
||||
|
||||
## Contract Rules
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ If no files match the current episode filter, a "Show all files" button lets you
|
||||
|
||||
### 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 |
|
||||
| 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 |
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -41,26 +41,26 @@ Add a `jimaku` section to your `config.jsonc`:
|
||||
"apiKeyCommand": "cat ~/.jimaku_key",
|
||||
"apiBaseUrl": "https://jimaku.cc",
|
||||
"languagePreference": "ja",
|
||||
"maxEntryResults": 10
|
||||
}
|
||||
"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. |
|
||||
| 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"
|
||||
}
|
||||
"openJimaku": "Ctrl+Shift+J",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+14
-14
@@ -31,20 +31,20 @@ input-ipc-server=\\.\pipe\subminer-socket
|
||||
|
||||
The plugin reads options from `script-opts` with the `subminer-` prefix (for example `--script-opts=subminer-backend=hyprland`). Managed launches inject these automatically from your SubMiner config; the shipped `subminer.conf` is intentionally empty so command-line opts always win.
|
||||
|
||||
| Option | Default | Description |
|
||||
| -------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------- |
|
||||
| `binary_path` | `""` | Path to the SubMiner binary; empty enables [auto-detection](#binary-auto-detection) |
|
||||
| `socket_path` | platform default | mpv IPC socket path (`/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows) |
|
||||
| `texthooker_enabled` | `no` | Start the texthooker server with the overlay |
|
||||
| `texthooker_port` | `5174` | Texthooker server port |
|
||||
| `backend` | `auto` | Window backend (`auto`, `hyprland`, `sway`, `x11`, `macos`) |
|
||||
| `auto_start` | `no` | Start the overlay app on `file-loaded` (managed launches set this from `mpv.autoStartSubMiner`) |
|
||||
| `auto_start_visible_overlay` | `no` | Show the visible overlay on auto-start (from `auto_start_overlay` in config) |
|
||||
| `overlay_loading_osd` | `no` | Show an OSD loading spinner while the overlay starts |
|
||||
| `auto_start_pause_until_ready` | `yes` | Keep mpv paused until the overlay reports tokenization-ready |
|
||||
| `auto_start_pause_until_ready_timeout_seconds` | `30` | Timeout before resuming playback anyway |
|
||||
| `osd_messages` | `yes` | Show plugin OSD status messages |
|
||||
| `log_level` | `info` | Plugin log verbosity |
|
||||
| Option | Default | Description |
|
||||
| ---------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `binary_path` | `""` | Path to the SubMiner binary; empty enables [auto-detection](#binary-auto-detection) |
|
||||
| `socket_path` | platform default | mpv IPC socket path (`/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows) |
|
||||
| `texthooker_enabled` | `no` | Start the texthooker server with the overlay |
|
||||
| `texthooker_port` | `5174` | Texthooker server port |
|
||||
| `backend` | `auto` | Window backend (`auto`, `hyprland`, `sway`, `x11`, `macos`) |
|
||||
| `auto_start` | `no` | Start the overlay app on `file-loaded` (managed launches set this from `mpv.autoStartSubMiner`) |
|
||||
| `auto_start_visible_overlay` | `no` | Show the visible overlay on auto-start (from `auto_start_overlay` in config) |
|
||||
| `overlay_loading_osd` | `no` | Show an OSD loading spinner while the overlay starts |
|
||||
| `auto_start_pause_until_ready` | `yes` | Keep mpv paused until the overlay reports tokenization-ready |
|
||||
| `auto_start_pause_until_ready_timeout_seconds` | `30` | Timeout before resuming playback anyway |
|
||||
| `osd_messages` | `yes` | Show plugin OSD status messages |
|
||||
| `log_level` | `info` | Plugin log verbosity |
|
||||
|
||||
## Keybindings
|
||||
|
||||
|
||||
@@ -607,10 +607,22 @@
|
||||
"deleteDuplicateInAuto": true // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
|
||||
}, // Is kiku setting.
|
||||
"lapisKiku": {
|
||||
"wordCardKind": "word-and-sentence" // Card-type flag SubMiner marks on Kiku/Lapis word cards: word-and-sentence (IsWordAndSentenceCard), click (IsClickCard), sentence (IsSentenceCard), audio (IsAudioCard), or none to leave the flags untouched. Only applies when isKiku.enabled or isLapis.enabled; the other card-type flags are cleared so a note never claims two kinds. Cards mined with the dedicated sentence/audio actions keep their own flag. Values: word-and-sentence | click | sentence | audio | none
|
||||
"wordCardKind": "word-and-sentence" // Card-type flag SubMiner marks on Kiku/Lapis word cards. Only one flag is set at a time; the others are cleared. Requires isKiku.enabled or isLapis.enabled. Values: word-and-sentence | click | sentence | audio | none
|
||||
} // Lapis kiku setting.
|
||||
}, // Automatic Anki updates and media generation options.
|
||||
|
||||
// ==========================================
|
||||
// Anime Browser
|
||||
// Anime browser sources. SubMiner ships no extension repositories and bundles no sources;
|
||||
// add a repository index URL here (or drop .apk files in the extensions directory) to have any.
|
||||
// Hot-reload: anime changes apply the next time the anime browser opens.
|
||||
// ==========================================
|
||||
"anime": {
|
||||
"extensionsDir": "", // Directory holding Aniyomi extension .apk files. Empty uses <userData>/anime-extensions.
|
||||
"repos": [], // Extension repository index URLs (any https .json index, e.g. https://.../index.min.json). Empty by default; SubMiner ships no repositories.
|
||||
"preferredQuality": "" // Preferred stream quality label, matched as a substring (for example: 1080). Empty uses the source order.
|
||||
}, // Anime browser sources. SubMiner ships no extension repositories and bundles no sources;
|
||||
|
||||
// ==========================================
|
||||
// Jimaku
|
||||
// Jimaku API configuration and defaults.
|
||||
|
||||
+18
-18
@@ -12,10 +12,10 @@ All shortcuts are configurable in `config.jsonc` under `shortcuts` and `keybindi
|
||||
|
||||
## App-Wide Shortcuts
|
||||
|
||||
| Shortcut | Action | Scope | Configurable |
|
||||
| ------------- | ---------------------- | -------------------------------------------- | -------------------------------------- |
|
||||
| `Alt+Shift+O` | Toggle visible overlay | Works while the overlay or mpv has focus | `shortcuts.toggleVisibleOverlayGlobal` |
|
||||
| `Alt+Shift+Y` | Open Yomitan settings | OS-global (registered with the OS) | Fixed (not configurable) |
|
||||
| Shortcut | Action | Scope | Configurable |
|
||||
| ------------- | ---------------------- | ---------------------------------------- | -------------------------------------- |
|
||||
| `Alt+Shift+O` | Toggle visible overlay | Works while the overlay or mpv has focus | `shortcuts.toggleVisibleOverlayGlobal` |
|
||||
| `Alt+Shift+Y` | Open Yomitan settings | OS-global (registered with the OS) | Fixed (not configurable) |
|
||||
|
||||
::: tip
|
||||
`Alt+Shift+O` is dispatched by the overlay window and the mpv plugin, so it works from either surface without OS registration. Only `Alt+Shift+Y` is registered with the OS; if it conflicts with another application, that binding cannot be changed. All `shortcuts.*` keys hot-reload - no restart needed.
|
||||
@@ -75,21 +75,21 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle
|
||||
|
||||
## Subtitle & Feature Shortcuts
|
||||
|
||||
| Shortcut | Action | Config key |
|
||||
| ------------------ | -------------------------------------------------------- | ------------------------------------------ |
|
||||
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
|
||||
| `Ctrl/Cmd+D` | Open loaded character dictionary manager | `shortcuts.openCharacterDictionaryManager` |
|
||||
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
||||
| `Ctrl/Cmd+/` | Open session help modal | `shortcuts.openSessionHelp` |
|
||||
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
||||
| `Ctrl+Shift+T` | Open TsukiHime subtitle search modal (EN/JA tabs) | `shortcuts.openTsukihime` |
|
||||
| `Ctrl/Cmd+N` | Toggle overlay notification history panel | `shortcuts.toggleNotificationHistory` |
|
||||
| `Ctrl+Alt+C` | Open the manual YouTube subtitle picker | `keybindings` |
|
||||
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
||||
| `Ctrl/Cmd+A` | Append clipboard video path to mpv playlist | `shortcuts.appendClipboardVideoToQueue` |
|
||||
| Shortcut | Action | Config key |
|
||||
| ------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
|
||||
| `Ctrl/Cmd+D` | Open loaded character dictionary manager | `shortcuts.openCharacterDictionaryManager` |
|
||||
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
||||
| `Ctrl/Cmd+/` | Open session help modal | `shortcuts.openSessionHelp` |
|
||||
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
||||
| `Ctrl+Shift+T` | Open TsukiHime subtitle search modal (EN/JA tabs) | `shortcuts.openTsukihime` |
|
||||
| `Ctrl/Cmd+N` | Toggle overlay notification history panel | `shortcuts.toggleNotificationHistory` |
|
||||
| `Ctrl+Alt+C` | Open the manual YouTube subtitle picker | `keybindings` |
|
||||
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
||||
| `Ctrl/Cmd+A` | Append clipboard video path to mpv playlist | `shortcuts.appendClipboardVideoToQueue` |
|
||||
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` (overlay) / `shortcuts.toggleSubtitleSidebar` (mpv session binding) |
|
||||
| `` ` `` | Toggle stats overlay | `stats.toggleKey` |
|
||||
| `W` | Mark current video watched and advance to next in queue | `stats.markWatchedKey` |
|
||||
| `` ` `` | Toggle stats overlay | `stats.toggleKey` |
|
||||
| `W` | Mark current video watched and advance to next in queue | `stats.markWatchedKey` |
|
||||
|
||||
`shortcuts.openAnimetosho` remains accepted as a deprecated alias for `shortcuts.openTsukihime`. The current name takes precedence when both are configured.
|
||||
|
||||
|
||||
@@ -62,23 +62,23 @@ Styling lives under the `css` object, using CSS property names and CSS custom pr
|
||||
| `pauseVideoOnHover` | boolean | `true` | Pause playback while hovering the cue list |
|
||||
| `autoScroll` | boolean | `true` | Keep the active cue in view during playback |
|
||||
|
||||
| `css` property | Default | Description |
|
||||
| ------------------------------------------- | --------------------------- | ---------------------------- |
|
||||
| `font-family` | `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP` | Cue text font family |
|
||||
| `color` | `#cad3f5` | Default cue text color |
|
||||
| `background-color` | `rgba(73, 77, 100, 0.9)` | Sidebar shell background color |
|
||||
| `font-size` | `16px` | Base cue font size |
|
||||
| `opacity` | `0.95` | Sidebar opacity between `0` and `1` |
|
||||
| `--subtitle-sidebar-max-width` | `420px` | Maximum sidebar width |
|
||||
| `--subtitle-sidebar-timestamp-color` | `#a5adcb` | Cue timestamp color |
|
||||
| `--subtitle-sidebar-active-line-color` | `#f5bde6` | Active cue text color |
|
||||
| `--subtitle-sidebar-active-background-color`| `rgba(138, 173, 244, 0.22)` | Active cue background color |
|
||||
| `--subtitle-sidebar-hover-background-color` | `rgba(54, 58, 79, 0.84)` | Hovered cue background color |
|
||||
| `css` property | Default | Description |
|
||||
| -------------------------------------------- | --------------------------------------------------------------- | ----------------------------------- |
|
||||
| `font-family` | `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP` | Cue text font family |
|
||||
| `color` | `#cad3f5` | Default cue text color |
|
||||
| `background-color` | `rgba(73, 77, 100, 0.9)` | Sidebar shell background color |
|
||||
| `font-size` | `16px` | Base cue font size |
|
||||
| `opacity` | `0.95` | Sidebar opacity between `0` and `1` |
|
||||
| `--subtitle-sidebar-max-width` | `420px` | Maximum sidebar width |
|
||||
| `--subtitle-sidebar-timestamp-color` | `#a5adcb` | Cue timestamp color |
|
||||
| `--subtitle-sidebar-active-line-color` | `#f5bde6` | Active cue text color |
|
||||
| `--subtitle-sidebar-active-background-color` | `rgba(138, 173, 244, 0.22)` | Active cue background color |
|
||||
| `--subtitle-sidebar-hover-background-color` | `rgba(54, 58, 79, 0.84)` | Hovered cue background color |
|
||||
|
||||
## Keyboard Shortcut
|
||||
|
||||
| Key | Action | Config key |
|
||||
| --- | ----------------------- | ------------------------------ |
|
||||
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` |
|
||||
| Key | Action | Config key |
|
||||
| --- | ----------------------- | --------------------------- |
|
||||
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` |
|
||||
|
||||
The toggle is overlay-local and only opens when SubMiner has a parsed cue list for the active subtitle source. See [Keyboard Shortcuts](/shortcuts) for the full shortcut reference.
|
||||
|
||||
@@ -70,6 +70,7 @@ subminer https://youtu.be/... # Play a YouTube URL
|
||||
subminer stats # Open the immersion stats dashboard
|
||||
subminer doctor # Check dependencies, config, and the mpv socket
|
||||
subminer settings # Open the SubMiner settings window
|
||||
subminer anime # Open the anime browser window
|
||||
subminer app --setup # Re-open first-run setup
|
||||
subminer -u # Check for updates
|
||||
```
|
||||
@@ -131,6 +132,7 @@ SubMiner.AppImage --open-tsukihime # Open TsukiHime subtitle search
|
||||
SubMiner.AppImage --yomitan # Open Yomitan settings
|
||||
SubMiner.AppImage --settings # Open the SubMiner settings window
|
||||
SubMiner.AppImage --jellyfin # Open the Jellyfin setup window
|
||||
SubMiner.AppImage --anime # Open the anime browser window
|
||||
SubMiner.AppImage --dictionary # Generate a character dictionary ZIP
|
||||
SubMiner.AppImage --start --dev # Enable app/dev mode
|
||||
SubMiner.AppImage --start --log-level debug # Verbose logging without dev mode
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Who this page is for:** developers and tinkerers who want to consume SubMiner's live subtitle stream from their own tools - a browser tab, an automation script, or another mpv plugin. If you just want subtitles in a browser tab for Yomitan, skip to [Texthooker Integration Guide](#texthooker-integration-guide); the rest is reference for building custom clients.
|
||||
|
||||
A *texthooker* is a page/tool that receives the text currently on screen so a dictionary extension (like Yomitan) can look words up. SubMiner ships its own texthooker UI and also broadcasts subtitle text over local WebSockets that any client can connect to.
|
||||
A _texthooker_ is a page/tool that receives the text currently on screen so a dictionary extension (like Yomitan) can look words up. SubMiner ships its own texthooker UI and also broadcasts subtitle text over local WebSockets that any client can connect to.
|
||||
|
||||
SubMiner exposes a small set of local integration surfaces for browser tools, automation helpers, and mpv-driven workflows:
|
||||
|
||||
@@ -15,12 +15,12 @@ This page documents those integration points and shows how to build custom consu
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Surface | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `websocket` | `ws://127.0.0.1:6677` | Basic subtitle broadcast stream |
|
||||
| `annotationWebsocket` | `ws://127.0.0.1:6678` | Structured stream with token metadata |
|
||||
| `texthooker` | `http://127.0.0.1:5174` | Local texthooker UI with injected websocket config |
|
||||
| mpv plugin | `script-message subminer-*` | Start/stop/toggle/status automation inside mpv |
|
||||
| Surface | Default | Purpose |
|
||||
| --------------------- | --------------------------- | -------------------------------------------------- |
|
||||
| `websocket` | `ws://127.0.0.1:6677` | Basic subtitle broadcast stream |
|
||||
| `annotationWebsocket` | `ws://127.0.0.1:6678` | Structured stream with token metadata |
|
||||
| `texthooker` | `http://127.0.0.1:5174` | Local texthooker UI with injected websocket config |
|
||||
| mpv plugin | `script-message subminer-*` | Start/stop/toggle/status automation inside mpv |
|
||||
|
||||
## Enable and Configure the Services
|
||||
|
||||
@@ -30,16 +30,16 @@ SubMiner's integration ports are configured in `config.jsonc`. All three service
|
||||
{
|
||||
"websocket": {
|
||||
"enabled": "auto",
|
||||
"port": 6677
|
||||
"port": 6677,
|
||||
},
|
||||
"annotationWebsocket": {
|
||||
"enabled": true,
|
||||
"port": 6678
|
||||
"port": 6678,
|
||||
},
|
||||
"texthooker": {
|
||||
"launchAtStartup": true,
|
||||
"openBrowser": false
|
||||
}
|
||||
"openBrowser": false,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -79,12 +79,12 @@ When a client connects, SubMiner immediately sends the latest subtitle payload i
|
||||
|
||||
#### Field reference
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `version` | number | Current websocket payload version. Today this is `1`. |
|
||||
| `text` | string | Raw subtitle text. |
|
||||
| Field | Type | Notes |
|
||||
| ---------- | ------ | ---------------------------------------------------------------------------------------------- |
|
||||
| `version` | number | Current websocket payload version. Today this is `1`. |
|
||||
| `text` | string | Raw subtitle text. |
|
||||
| `sentence` | string | Plain subtitle text with line breaks represented as `<br>`. No annotation spans or attributes. |
|
||||
| `tokens` | array | Always empty on the basic subtitle websocket. |
|
||||
| `tokens` | array | Always empty on the basic subtitle websocket. |
|
||||
|
||||
### 2. Annotation WebSocket
|
||||
|
||||
@@ -127,22 +127,22 @@ In practice, if you are building a new client, prefer `annotationWebsocket` unle
|
||||
|
||||
Each annotation token may include:
|
||||
|
||||
| Token field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `surface` | string | Display text for the token |
|
||||
| `reading` | string | Kana reading when available |
|
||||
| `headword` | string | Dictionary headword when available |
|
||||
| `startPos` / `endPos` | number | Character offsets in the subtitle text |
|
||||
| `partOfSpeech` | string | SubMiner token POS label |
|
||||
| `isMerged` | boolean | Whether this token represents merged content |
|
||||
| `isKnown` | boolean | Marked known by SubMiner's known-word logic |
|
||||
| `isNPlusOneTarget` | boolean | True when the token is the sentence's N+1 target |
|
||||
| `isNameMatch` | boolean | True for prioritized character-name matches |
|
||||
| `frequencyRank` | number | Frequency rank when available |
|
||||
| `jlptLevel` | string | JLPT level when available |
|
||||
| `className` | string | CSS-ready class list derived from token state |
|
||||
| `frequencyRankLabel` | string or `null` | Preformatted rank label for UIs |
|
||||
| `jlptLevelLabel` | string or `null` | Preformatted JLPT label for UIs |
|
||||
| Token field | Type | Notes |
|
||||
| --------------------- | ---------------- | ------------------------------------------------ |
|
||||
| `surface` | string | Display text for the token |
|
||||
| `reading` | string | Kana reading when available |
|
||||
| `headword` | string | Dictionary headword when available |
|
||||
| `startPos` / `endPos` | number | Character offsets in the subtitle text |
|
||||
| `partOfSpeech` | string | SubMiner token POS label |
|
||||
| `isMerged` | boolean | Whether this token represents merged content |
|
||||
| `isKnown` | boolean | Marked known by SubMiner's known-word logic |
|
||||
| `isNPlusOneTarget` | boolean | True when the token is the sentence's N+1 target |
|
||||
| `isNameMatch` | boolean | True for prioritized character-name matches |
|
||||
| `frequencyRank` | number | Frequency rank when available |
|
||||
| `jlptLevel` | string | JLPT level when available |
|
||||
| `className` | string | CSS-ready class list derived from token state |
|
||||
| `frequencyRankLabel` | string or `null` | Preformatted rank label for UIs |
|
||||
| `jlptLevelLabel` | string or `null` | Preformatted JLPT label for UIs |
|
||||
|
||||
### 3. HTML markup conventions
|
||||
|
||||
|
||||
@@ -110,8 +110,8 @@ Background cache downloads are capped at 720p by default (`youtube.mediaCache.ma
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description |
|
||||
| --------------------- | ---------- | ------------------------------------------------------------------------------------- |
|
||||
| Option | Type | Description |
|
||||
| --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `primarySubLanguages` | `string[]` | Languages that count as a satisfactory primary subtitle (default `["ja", "jpn"]`). Used by the "primary subtitle missing" notification and by managed local/playlist subtitle selection. |
|
||||
|
||||
YouTube auto-selection itself always picks a Japanese track first (manual over auto), then falls back to any manual track — `primarySubLanguages` does not change which YouTube track is auto-picked.
|
||||
@@ -130,11 +130,11 @@ YouTube secondary selection is fixed: SubMiner always tries an English track (ma
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description |
|
||||
| ----------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Option | Type | Description |
|
||||
| ----------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `secondarySubLanguages` | `string[]` | Extra language codes (e.g. `["eng", "en"]`) used when auto-selecting a secondary track for local/Jellyfin sidecar files. Default is empty (`[]`). Not used for YouTube. |
|
||||
| `autoLoadSecondarySub` | `boolean` | Auto-detect and load a matching secondary sidecar track for local files (default: `false`). Not used for YouTube. |
|
||||
| `defaultMode` | `"hidden"` / `"visible"` / `"hover"` | Initial display mode for secondary subtitles (default: `"hover"`) |
|
||||
| `defaultMode` | `"hidden"` / `"visible"` / `"hover"` | Initial display mode for secondary subtitles (default: `"hover"`) |
|
||||
|
||||
These settings come from `config.jsonc` (or built-in defaults); there are no CLI flags or environment variables for subtitle language selection.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user