Compare commits

..

21 Commits

Author SHA1 Message Date
sudacode 1105b18a5a refactor: deduplicate mpv plugin config, fix CSS font fallbacks
- Extract shared getMpvPluginRuntimeConfig() helper to eliminate duplicate inline objects
- Call ensureImmersionTrackerStarted() before markActiveVideoWatched action
- Quote multi-word font names and add sans-serif generic fallback in subtitle sidebar CSS
- Add main-wiring tests asserting deduplication and tracker start ordering
2026-05-19 02:14:16 -07:00
sudacode f4845513f3 feat: add mark-watched action, background app reuse, and N+1 compat
- Add `--mark-watched` CLI flag + mpv session binding; marks video watched, shows OSD, advances playlist
- Launcher detects running background app via `--app-ping` and borrows it instead of owning its lifecycle
- Preserve N+1 highlighting for existing configs with `knownWords.highlightEnabled` set
- Fix `resolveConfiguredShortcuts` to respect explicit `null` overrides (disabling defaults)
- Split session-help modal into focused modules (colors, render, sections, tabs)
2026-05-19 01:30:49 -07:00
sudacode 24b95eda9d test: add 20s timeout to 365d trends dashboard test 2026-05-19 00:19:58 -07:00
sudacode ed85cc4b5e feat: include unconfigured secret paths in config settings snapshot
- Export SECRET_PATHS from registry for reuse
- Populate snapshot with `{ configured: true }` for non-empty secrets not already covered by registered fields
2026-05-19 00:04:38 -07:00
sudacode 222edaf4a0 feat: expand hot-reload to logging, jimaku, subsync, and Anki sub-fields
- Mark logging.level, stats keys, jimaku.*, subsync.*, and granular ankiConnect fields (knownWords, nPlusOne, fields, isLapis, isKiku, behavior) as hot-reloadable
- Refactor classifyConfigHotReloadDiff to path-walk diffing instead of per-key branches
- Wire setLogLevel, invalidateTokenizationCache, refreshSubtitlePrefetch, refreshCurrentSubtitle into hot-reload applied handler
- Exclude ai.* and ankiConnect.ai.* prefixes from config window; hide fields.translation
- Update docs and config.example.jsonc hot-reload annotations
2026-05-18 23:59:08 -07:00
sudacode a299cf6b72 docs: add config subcommand and --config flag to CLI reference 2026-05-18 21:35:02 -07:00
sudacode 0e8260fa5b docs: simplify and restructure installation guide
- Consolidate requirements into a single table with status column
- Rewrite installation.md as a numbered 3-step guide
- Remove verbose platform-specific notes; fold essentials into platform sections
- Trim README quick-start to minimal install/launch commands
2026-05-18 19:25:44 -07:00
sudacode 06d0e3ed18 fix: disable macOS mpv menu shortcuts, buffer latest subtitle IPC state
- Pass --macos-menu-shortcuts=no on Darwin so SubMiner bindings reach mpv
- Replace queued IPC listener with latest-value variant for subtitle channels
- Skip JSONC line/block comments in duplicate-key offset helpers
- Preserve configured Anki note model name in selectPreferredNoteFieldModelName
- Guard known-words deck rename against collision; add chooseKnownWordsDeckRenameValue
- Apply asCssColor on hover token CSS compat reads
2026-05-18 16:03:16 -07:00
sudacode ff4d38e5be migrate subtitle style config to CSS declaration shape
- Flat style keys (fontFamily, fontSize, hoverTokenColor, etc.) consolidated into subtitleStyle.css, secondary.css, and subtitleSidebar.css objects
- Hover token colors migrated to --subtitle-hover-token-color CSS custom properties
- Plugin app-ping now checks result.status (0=running, 1=stopped) to avoid treating transient failures as stopped
- Note-fields note type picker defaults to configured deck's note type before falling back to Kiku/Lapis
- New migration for legacy ankiConnect N+1 config paths
2026-05-18 03:07:39 -07:00
sudacode c7fc328194 fix: transport AppImage args via env and gate restart on app-ping
- Transport Linux AppImage CLI args through SUBMINER_APP_ARGC/ARG_* env vars instead of argv
- Add --app-ping command to probe single-instance lock ownership (exit 0 = running, 1 = not)
- Gate manual restart: poll app-ping until old app releases lock, then until new app owns it
- Preserve user-paused playback when disarming the auto-play-ready gate on restart
- Snapshot subtitles before connection side effects (sub-visibility hide) can suppress them
- Reapply overlay bounds after first show for Hyprland compatibility
2026-05-18 03:07:39 -07:00
sudacode edb1da2993 fix: curl fetch for Linux updater, overlay restart restore, Yomitan late
- Use /usr/bin/curl on Linux for update checks to avoid Electron net-service crashes
- Restore visible overlay on manual restart even when auto-start visibility is disabled
- Reload overlay windows after Yomitan extension loads to fix popup race on startup
2026-05-18 03:07:39 -07:00
sudacode 71ea5ef944 feat(settings): move restart badge inline with option title
- Remove field-meta row (config path, advanced chip) from option rows
- Inline live/restart status badge beside each option label
- Extract getFieldTitleBadges into settings-field-layout module with tests
2026-05-18 03:07:39 -07:00
sudacode b076e8800f fix: normalize anki deck sample size 2026-05-18 03:07:39 -07:00
sudacode 10d9c38037 fix: address follow-up review feedback 2026-05-18 03:07:39 -07:00
sudacode c369841827 fix: address config modal review feedback 2026-05-18 03:07:39 -07:00
sudacode c6537224f2 fix: add missing changelog metadata 2026-05-18 03:07:39 -07:00
sudacode 6ba91780c1 feat(config): unify mpv plugin options under main config and add CSS/Ani
- Replace subminer.conf plugin config with mpv.* fields in config.jsonc
- Add socketPath, backend, autoStartSubMiner, pauseUntilOverlayReady, aniskipEnabled/buttonKey, subminerBinaryPath to mpv config
- Add subtitleSidebar.css field; migrate legacy sidebar appearance fields
- Add paintOrder and WebkitTextStroke to subtitle style options
- Update default subtitle/sidebar fontFamily to CJK-first stack
- Fix overlay visible state surviving mpv y-r restart
- Fix live config saves applying subtitle CSS immediately to open overlays
- Migrate legacy primary/secondary subtitle appearance into subtitleStyle.css on load
- Switch AniSkip button key setting to click-to-learn key capture
2026-05-18 03:07:39 -07:00
sudacode 81830b3372 feat(config): add subtitle CSS editor, nPlusOne.enabled flag, and fix se
- subtitleStyle.css / subtitleStyle.secondary.css replace flat style fields in the settings window
- ankiConnect.nPlusOne.enabled gates known-word cache independently of knownWords.highlightEnabled
- Settings search now covers all categories, narrows on multi-word terms, and hides editor-owned fields
- Default note-type picker to Kiku then Lapis; rename isLapis.sentenceCardModel default to "Lapis"
2026-05-18 03:07:39 -07:00
sudacode 3447103857 fix(launcher): suppress Electron menu diagnostics 2026-05-18 03:07:39 -07:00
sudacode bcbd0173e5 style: format config settings changes 2026-05-18 03:07:39 -07:00
sudacode 0298a066ad feat(config): reorganize settings window and move annotation colors to subtitleStyle
- Reorganize Configuration window into Appearance, Behavior, Anki, Input, and Integration sections
- Add AnkiConnect-backed deck, note-type, and field pickers in the Anki section
- Add click-to-learn keybinding controls
- Move known-word and N+1 highlight colors to subtitleStyle.knownWordColor / subtitleStyle.nPlusOneColor; legacy ankiConnect.knownWords.color and ankiConnect.nPlusOne.nPlusOne keys still accepted with deprecation warnings
- Add deckNames, modelNames, modelFieldNames, and fieldNamesForDeck methods to AnkiConnectClient
- Mark discordPresence.presenceStyle as an enum in the config registry
2026-05-18 03:07:39 -07:00
583 changed files with 9393 additions and 26566 deletions
+23 -23
View File
@@ -4,7 +4,7 @@
# SubMiner
Integrates Yomitan and mpv - on-screen lookups, mine to Anki, and track immersion without leaving the player
Integrates Yomitan with mpv - look up words, mine to Anki, and track your immersion without leaving the player.
[Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
@@ -23,7 +23,7 @@ Integrates Yomitan and mpv - on-screen lookups, mine to Anki, and track immersio
### Dictionary Lookups
Hover over any word and trigger a lookup to get the full Yomitan popup - definitions, pitch accent, and frequency data - without ever leaving mpv.
Yomitan runs inside the overlay. Trigger a lookup on any word for full dictionary popups — definitions, pitch accent, frequency data without ever leaving mpv.
<div align="center">
<img src="docs-site/public/screenshots/yomitan-lookup.png" width="800" alt="Yomitan dictionary popup over annotated subtitles in mpv">
@@ -43,7 +43,7 @@ Create an Anki card with the sentence, audio clip, screenshot, and machine trans
### Reading Annotations
Real-time subtitle annotations with frequency highlighting, JLPT tags, N+1 targeting, and a character name dictionary. Grammar-only tokens and particles render as plain text so you focus on what matters.
Real-time subtitle annotations with frequency highlighting, JLPT tags, N+1 targeting, and a character name dictionary. Known words fade back; new words stand out. Grammar-only tokens render as plain text so you focus on what matters.
<div align="center">
<img src="docs-site/public/screenshots/annotations.png" width="800" alt="Annotated subtitles with frequency coloring, JLPT underlines, and N+1 targets">
@@ -53,7 +53,7 @@ Real-time subtitle annotations with frequency highlighting, JLPT tags, N+1 targe
### Immersion Dashboard
Local stats dashboard tracking watch time, vocabulary growth, mining throughput, session history, and trends. All stored locally, no third-party tracking.
Local stats dashboard watch time, anime library, vocabulary growth, mining throughput, session history, and trends. All stored locally, no third-party tracking.
<div align="center">
<img src="docs-site/public/screenshots/stats-overview.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
@@ -92,11 +92,11 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
</tr>
<tr>
<td><b>alass / ffsubsync</b></td>
<td>Manual subtitle retiming — requires <code>alass</code> or <code>ffsubsync</code> on your <code>PATH</code> (optional; subtitle syncing is disabled without them)</td>
<td>Automatic subtitle retiming — requires <code>alass</code> or <code>ffsubsync</code> on your <code>PATH</code> (optional; subtitle syncing is disabled without them)</td>
</tr>
<tr>
<td><b>WebSocket</b></td>
<td>Plain subtitle feed plus a dedicated annotated feed for texthooker pages and custom tools</td>
<td>Annotated subtitle feed for external clients (texthooker pages, custom tools)</td>
</tr>
</table>
@@ -110,17 +110,16 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
## Requirements
Only **mpv** and Anki+AnkiConnect are required. Everything else is optional but enhances the experience.
Only **mpv** is required. Everything else is optional but enhances the experience.
| Dependency | Status | What it does |
| -------------------- | ----------- | ---------------------------------------- |
| mpv | Required | The video player SubMiner overlays on |
| Anki + AnkiConnect | Required | Card creation from the Yomitan popup |
| ffmpeg | Recommended | Audio clips & screenshots for Anki cards |
| MeCab + mecab-ipadic | Recommended | More precise annotations and filtering |
| yt-dlp | Optional | YouTube playback |
| fzf / rofi | Optional | Video picker in the launcher |
| alass / ffsubsync | Optional | Subtitle sync |
| Dependency | Status | What it does |
| -------------------- | ----------- | ------------------------------------------------- |
| mpv | Required | The video player SubMiner overlays on |
| ffmpeg | Recommended | Audio clips & screenshots for Anki cards |
| MeCab + mecab-ipadic | Recommended | More precise N+1, JLPT, and frequency annotations |
| yt-dlp | Optional | YouTube playback |
| fzf / rofi | Optional | Video picker in the launcher |
| alass / ffsubsync | Optional | Subtitle sync |
<details>
<summary><b>Platform-specific install commands</b></summary>
@@ -197,24 +196,25 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
Run SubMiner and the first-run setup wizard will guide you through importing Yomitan dictionaries and optionally installing the `subminer` command-line launcher.
```bash
# Linux
# Linux (AUR)
subminer app --setup
# macOS — open SubMiner.app, or:
subminer app --setup
```
On **Windows**, just run `SubMiner.exe` and the setup will open automatically on first launch.
On **Windows**, just run `SubMiner.exe` setup opens automatically on first launch.
### 3. Mine
### 3. Play
```bash
subminer video.mkv # launch mpv with SubMiner
subminer /path/to/dir # pick a file with fzf
subminer -R /path/to/dir # pick a file with rofi (Linux only)
subminer video.mkv # play video with overlay
subminer stats # open immersion dashboard
subminer config # open configuration window
subminer --config # open configuration window via flag
```
On **Windows**, use the **SubMiner mpv** shortcut created during setup. Double-click it or drag a video file onto it.
On **Windows**, use the **SubMiner mpv** shortcut created during setup — double-click it or drag a video file onto it.
## Documentation
+193
View File
@@ -0,0 +1,193 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveMacAppBundlePath = resolveMacAppBundlePath;
exports.isMacApplicationsFolderBundle = isMacApplicationsFolderBundle;
exports.isKnownLinuxPackageManagedAppImage = isKnownLinuxPackageManagedAppImage;
exports.isNativeUpdaterSupported = isNativeUpdaterSupported;
exports.configureAutoUpdater = configureAutoUpdater;
exports.createElectronAppUpdater = createElectronAppUpdater;
const node_fs_1 = require("node:fs");
const node_child_process_1 = require("node:child_process");
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const node_util_1 = require("node:util");
const electron_updater_1 = require("electron-updater");
const release_assets_1 = require("./release-assets");
const updaterErrorListeners = new WeakMap();
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
function resolveMacAppBundlePath(execPath) {
const marker = '.app/Contents/MacOS/';
const markerIndex = execPath.indexOf(marker);
if (markerIndex < 0)
return null;
return execPath.slice(0, markerIndex + '.app'.length);
}
async function readMacCodeSignature(appBundlePath) {
try {
const result = await execFileAsync('/usr/bin/codesign', ['-dv', '--verbose=4', appBundlePath], {
encoding: 'utf8',
});
return `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
}
catch {
return null;
}
}
function realpathOrOriginal(filePath) {
try {
return (0, node_fs_1.realpathSync)(filePath);
}
catch {
return filePath;
}
}
function isSameOrInsideDirectory(parentPath, candidatePath) {
const relative = node_path_1.default.relative(parentPath, candidatePath);
return (relative === '' ||
(relative.length > 0 && !relative.startsWith('..') && !node_path_1.default.isAbsolute(relative)));
}
function isMacApplicationsFolderBundle(appBundlePath, homeDir = node_os_1.default.homedir()) {
const resolvedBundlePath = node_path_1.default.resolve(appBundlePath);
return (isSameOrInsideDirectory('/Applications', resolvedBundlePath) ||
isSameOrInsideDirectory(node_path_1.default.join(homeDir, 'Applications'), resolvedBundlePath));
}
function isKnownLinuxPackageManagedAppImage(appImagePath) {
return realpathOrOriginal(appImagePath) === '/opt/SubMiner/SubMiner.AppImage';
}
async function isNativeUpdaterSupported(options) {
if (!options.isPackaged) {
options.log?.('Skipping native updater because this build is not packaged.');
return false;
}
if (options.platform === 'linux') {
options.log?.('Skipping native Linux updater because Linux tray checks use GitHub release assets.');
return false;
}
if (options.platform !== 'darwin') {
options.log?.('Skipping native updater because this platform uses GitHub metadata checks.');
return false;
}
const appBundlePath = resolveMacAppBundlePath(options.execPath);
if (!appBundlePath) {
options.log?.('Skipping native macOS updater because the app bundle path could not be resolved.');
return false;
}
if (!isMacApplicationsFolderBundle(appBundlePath, options.homeDir)) {
options.log?.('Skipping native macOS updater because the app is not installed in an Applications folder.');
return false;
}
const signature = await (options.readCodeSignature ?? readMacCodeSignature)(appBundlePath);
if (!signature) {
options.log?.('Skipping native macOS updater because the app code signature could not be read.');
return false;
}
if (/Signature=adhoc\b/.test(signature) || /TeamIdentifier=not set\b/.test(signature)) {
options.log?.('Skipping native macOS updater because this build is ad-hoc signed.');
return false;
}
return true;
}
function configureAutoUpdater(updater, log = () => { }, channel = 'stable') {
updater.autoDownload = false;
// On macOS this avoids invoking Squirrel until the explicit restart/install step.
updater.autoInstallOnAppQuit = false;
updater.allowPrerelease = channel === 'prerelease';
updater.allowDowngrade = false;
updater.logger = {
info: () => { },
debug: () => { },
warn: (message) => log(message),
error: (message) => log(message),
};
const previousErrorListener = updaterErrorListeners.get(updater);
if (previousErrorListener) {
if (updater.off) {
updater.off('error', previousErrorListener);
}
else {
updater.removeListener?.('error', previousErrorListener);
}
}
if (updater.on) {
const errorListener = (error) => {
const message = error instanceof Error ? error.message : String(error);
log(`Updater error event: ${message}`);
};
updater.on('error', errorListener);
updaterErrorListeners.set(updater, errorListener);
}
return updater;
}
function createElectronAppUpdater(options) {
const getChannel = options.getChannel ?? (() => 'stable');
const updater = configureAutoUpdater(options.updater ?? electron_updater_1.autoUpdater, options.log, getChannel());
if (options.configureHttpExecutor) {
// electron-updater has no public executor hook; keep the macOS cURL override localized.
updater.httpExecutor = options.configureHttpExecutor();
}
if (options.disableDifferentialDownload !== undefined) {
updater.disableDifferentialDownload = options.disableDifferentialDownload;
}
let nativeUpdaterSupported = null;
async function getNativeUpdaterSupported() {
if (!options.isNativeUpdaterSupported)
return true;
if (nativeUpdaterSupported === null) {
nativeUpdaterSupported = Promise.resolve(options.isNativeUpdaterSupported());
}
return nativeUpdaterSupported;
}
return {
async checkForUpdates(channel) {
if (!options.isPackaged) {
return {
available: false,
version: options.currentVersion,
canUpdate: false,
};
}
if (!(await getNativeUpdaterSupported())) {
options.log('Skipping native app update check because native updater is unsupported.');
return {
available: false,
version: options.currentVersion,
canUpdate: false,
};
}
configureAutoUpdater(updater, options.log, channel ?? getChannel());
const result = await updater.checkForUpdates();
const version = result?.updateInfo?.version ?? options.currentVersion;
return {
available: (0, release_assets_1.compareSemverLike)(version, options.currentVersion) > 0,
version,
canUpdate: true,
};
},
async downloadUpdate() {
if (!options.isPackaged) {
options.log('Skipping app update download because this build is not packaged.');
return;
}
if (!(await getNativeUpdaterSupported())) {
options.log('Skipping app update download because native updater is unsupported.');
return;
}
await updater.downloadUpdate();
},
async quitAndInstall() {
if (!options.isPackaged) {
options.log('Skipping app update install because this build is not packaged.');
return;
}
if (!(await getNativeUpdaterSupported())) {
options.log('Skipping app update install because native updater is unsupported.');
return;
}
updater.quitAndInstall(false, true);
},
};
}
//# sourceMappingURL=app-updater.js.map
+31 -135
View File
@@ -15,15 +15,15 @@
"jsonc-parser": "^3.3.1",
"koffi": "^2.15.6",
"libsql": "^0.5.22",
"vscode-json-languageservice": "^5.7.2",
"ws": "^8.19.0",
},
"devDependencies": {
"@types/node": "^24.10.0",
"@types/node": "^25.3.0",
"@types/ws": "^8.18.1",
"electron": "42.2.0",
"electron": "39.8.6",
"electron-builder": "26.8.2",
"esbuild": "^0.25.12",
"eslint": "^10.4.0",
"prettier": "^3.8.1",
"typescript": "^5.9.3",
},
@@ -53,7 +53,7 @@
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
"@electron/get": ["@electron/get@5.0.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA=="],
"@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],
@@ -117,34 +117,10 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
"@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="],
"@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.2.7", "", {}, "sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
@@ -191,21 +167,15 @@
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
@@ -219,16 +189,14 @@
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
"@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
"@xhayper/discord-rpc": ["@xhayper/discord-rpc@1.3.3", "", { "dependencies": { "@discordjs/rest": "^2.6.1", "@vladfrangu/async_event_emitter": "^2.4.7", "discord-api-types": "^0.38.42", "ws": "^8.20.0" } }, "sha512-Ih48GHiua7TtZgKO+f0uZPhCeQqb84fY2qUys/oMh8UbUfiUkUJLVCmd/v2AK0/pV33euh0aqSXo7+9LiPSwGw=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],
"abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
@@ -329,8 +297,6 @@
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
@@ -363,7 +329,7 @@
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron": ["electron@42.2.0", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-b2Tc7sIKiZEl0tBVwFM5GJ+FT5KYhmy9QJHjx8BGVZPVW2SctXWEvrE959ElB56qw7H05dBkhlikDA1DmpaAMw=="],
"electron": ["electron@39.8.6", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-uWX6Jh5LmwL13VwOSKBjebI+ck+03GOwc8V2Sgbmr9pJVJ/cHfli/PkjXuRDr+hq+SLHQuT9mGHSIfScebApRA=="],
"electron-builder": ["electron-builder@26.8.2", "", { "dependencies": { "app-builder-lib": "26.8.2", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-ieiiXPdgH3qrG6lcvy2mtnI5iEmAopmLuVRMSJ5j40weU0tgpNx0OAk9J5X5nnO0j9+KIkxHzwFZVUDk1U3aGw=="],
@@ -381,7 +347,7 @@
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
@@ -401,22 +367,6 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
@@ -427,22 +377,12 @@
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
@@ -467,8 +407,6 @@
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
@@ -507,8 +445,6 @@
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
@@ -517,12 +453,8 @@
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
@@ -543,8 +475,6 @@
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
@@ -559,12 +489,8 @@
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash": ["lodash@4.18.0", "", {}, "sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA=="],
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
@@ -617,8 +543,6 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="],
@@ -639,22 +563,16 @@
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -673,8 +591,6 @@
"postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
@@ -787,15 +703,13 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
"undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="],
@@ -811,12 +725,18 @@
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
"vscode-json-languageservice": ["vscode-json-languageservice@5.7.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-WtKRDtJfFEmLrgtu+ODexOHm/6/krRF0k6t+uvkKIKW1Jh9ZIyxZQwJJwB3qhrEgvAxa37zbUg+vn+UyUK/U2w=="],
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -839,12 +759,14 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@discordjs/rest/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
@@ -853,8 +775,6 @@
"@electron/windows-sign/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
@@ -865,20 +785,6 @@
"@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@types/cacheable-request/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/fs-extra/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/keyv/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/plist/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/responselike/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/ws/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/yauzl/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
@@ -891,6 +797,8 @@
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"electron/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
"electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -903,36 +811,22 @@
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"@types/cacheable-request/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/fs-extra/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/keyv/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/plist/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/responselike/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/ws/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/yauzl/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -943,6 +837,8 @@
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"electron/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Fixed the Jellyfin setup popup login path on Windows by using an IPC bridge, showing immediate login progress, and timing out unreachable server login attempts with an inline error.
-2
View File
@@ -34,13 +34,11 @@ Rules:
How fragments turn into a release:
- At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that.
- The polish step treats pending fragments as the final release outcome, not prerelease history. If a feature is added and then renamed or fixed before the stable cut, ship the final feature bullet instead of separate prerelease-only breaking/fix entries.
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely.
- The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
Prerelease notes:
- prerelease tags like `v0.11.3-beta.1` and `v0.11.3-rc.1` reuse the current pending fragments to generate `release/prerelease-notes.md`
- existing prerelease notes are a reviewed baseline; later prerelease runs should replace stale beta/RC wording with the current outcome instead of appending fix churn
- prerelease note generation does not consume fragments and does not update `CHANGELOG.md` or `docs-site/changelog.md`
- the final stable release is the point where `bun run changelog:build` consumes fragments into the stable changelog and release notes
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: anilist
- Prevent repeated missing-token checks from rapidly exhausting AniList retry attempts or duplicating dead-letter entries for the same episode.
-5
View File
@@ -1,5 +0,0 @@
type: fixed
area: anki
- Made sentence-audio padding opt-in by default, and kept animated AVIF motion aligned when padding is configured by freezing the first frame during leading audio padding.
- Kept multi-line sentence mining aligned when repeated subtitle text appears in the selected history range.
+1 -1
View File
@@ -1,4 +1,4 @@
type: fixed
area: launcher
- Reused an already-running background SubMiner app for launcher-opened videos, closed launcher-owned tray apps after playback ends, and reapplied preferred subtitles for warm launches.
- Reused an already-running background SubMiner app for launcher-opened videos, preserving warmups and keeping the tray app alive after playback closes.
-4
View File
@@ -1,4 +0,0 @@
type: changed
area: release
- Release-note polishing now treats pending fragments and reviewed prerelease notes as a cumulative final outcome, so prerelease-only fixes or breakages collapse into the final user-facing change.
@@ -1,4 +0,0 @@
type: changed
area: dictionary
- Keep character dictionary lookup entries scoped to generated Japanese name aliases instead of surfacing raw romanized/English aliases as separate results, and refresh cached v15 snapshots so old English-name entries are regenerated.
@@ -1,4 +0,0 @@
type: fixed
area: character-dictionary
- Block the character dictionary manager when character dictionary annotations are disabled, and notify through the configured OSD/system notification surfaces.
-4
View File
@@ -1,4 +0,0 @@
type: changed
area: character-dictionary
- Character dictionary entries are now scoped to the current AniList media for name matching and inline portraits, and a new `Ctrl/Cmd+D` manager modal can remove, reorder, or override loaded dictionary entries.
@@ -1,4 +0,0 @@
type: changed
area: character-dictionary
- **Character Dictionary:** Changed the in-app AniList selector to wait for an explicit title search. The search box is prefilled from the current filename guess, so you can edit it before choosing an override.
-7
View File
@@ -1,7 +0,0 @@
type: added
area: subtitles
- Added optional inline AniList portraits for character-name subtitle matches, including automatic refresh of cached character dictionary snapshots that do not contain portrait data.
- Scoped manual AniList overrides by parent media directory, so separate season folders can keep separate character dictionary selections.
- Fixed large character dictionary imports by serving the merged ZIP through a local URL when supported, with a base64 fallback for older bundled Yomitan builds.
- Allowed subtitle overlay data image sources so inline character portraits render instead of showing a broken image icon.
@@ -1,4 +0,0 @@
type: fixed
area: config
- Preserved user config files during legacy config compatibility handling.
+1 -1
View File
@@ -1,4 +1,4 @@
type: changed
area: config
- Reorganized the Settings window into clearer Appearance, Behavior, Anki, input, and integration sections with learned keybinding controls and AnkiConnect-backed deck, field, and note-type pickers.
- Reorganized the Configuration window into clearer Appearance, Behavior, Anki, input, and integration sections with learned keybinding controls and AnkiConnect-backed deck, field, and note-type pickers.
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: settings
- Simplified configuration option rows by hiding raw config paths and placing the live/restart status beside each option title.
+1 -1
View File
@@ -1,4 +1,4 @@
type: fixed
area: config
- Fixed Settings window search so it searches across all categories, narrows on multi-word terms, hides settings owned by richer editors, and no longer shows the Open File button.
- Fixed Configuration window search so it searches across all categories, narrows on multi-word terms, hides settings owned by richer editors, and no longer shows the Open File button.
+5 -5
View File
@@ -1,8 +1,8 @@
type: added
area: config
- Added a dedicated Settings window with launcher entry points via `subminer --settings` and `subminer settings`.
- Fixed the Settings window preload so launcher-opened windows can initialize even when Electron sandboxing is active.
- Kept settings-window startup lightweight by skipping AniList token refresh and automatic update polling.
- Marked safe live config options in the Settings window and expanded hot reload for stats keys, logging level, Jimaku, Subsync, YouTube language defaults, Anki media/sentence/misc field mappings, sentence card model, and selected Anki annotation/runtime options.
- Hid AI and translation fields from the Settings window while keeping them supported in config files.
- Added a dedicated Configuration window with launcher entry points via `subminer --config` and `subminer config`.
- Fixed the Configuration window preload so launcher-opened windows can initialize even when Electron sandboxing is active.
- Kept config-window startup lightweight by skipping AniList token refresh and automatic update polling.
- Marked safe live config options in the Configuration window and expanded hot reload for stats keys, logging level, Jimaku, Subsync, YouTube language defaults, Anki media/sentence/misc field mappings, sentence card model, and selected Anki annotation/runtime options.
- Hid AI and translation fields from the Configuration window while keeping them supported in config files.
@@ -1,4 +0,0 @@
type: changed
area: config
- Defaulted Jellyfin remote-session startup warmup and character-name subtitle highlighting to off.
-4
View File
@@ -1,4 +0,0 @@
type: changed
area: runtime
- Updated the bundled Electron runtime from 39.8.6 to 42.2.0, moving SubMiner back onto a supported Electron release line.
@@ -1,4 +0,0 @@
type: fixed
area: anki
- Fixed animated AVIF word-audio sync so the frozen lead-in matches the word audio duration without adding sentence audio padding a second time.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Primed the first startup subtitle before autoplay resumes so the overlay can render text before video playback begins.
@@ -1,5 +0,0 @@
type: fixed
area: launcher
- Kept launcher-opened videos paused when attaching to an already-running background app until subtitle priming and tokenization readiness complete.
- Moved mpv plugin subtitle auto-selection to pre-load so launch-time subtitle choices are not reset after the video opens.
@@ -1,4 +0,0 @@
type: fixed
area: character-dictionary
- Added surname honorific matches for Japanese localized character aliases embedded in AniList alternative names, such as Korean-source characters with Japanese names in parentheses, and refresh cached snapshots so those aliases are regenerated.
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Hid the visible subtitle overlay as soon as the character dictionary modal opens, including while AniList lookup is still loading or returns no results.
@@ -1,4 +0,0 @@
type: fixed
area: integrations
- Prevented Discord Rich Presence from falling back to Jellyfin stream URLs, and primed Jellyfin playback titles before loading tokenized streams so presence shows the show/episode title
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Refreshed Linux overlay placement after leaving mpv fullscreen so Hyprland keeps the visible overlay aligned to the player.
-5
View File
@@ -1,5 +0,0 @@
type: fixed
area: overlay
- Kept the Hyprland visible overlay stacked above mpv after mpv receives focus from clicks or overlay movement.
- Suspended the visible overlay while the in-player stats window is open, then restored it mouse-passive after stats closes.
@@ -1,4 +0,0 @@
type: fixed
area: desktop
- Fixed Hyprland settings windows opening behind the subtitle overlay by promoting SubMiner and Yomitan settings above the overlay without hiding subtitles.
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Prevented Jellyfin discovery playback from reloading the active item, misreporting paused mpv playback as still playing, retrying startup unpause after playback is paused again, unpausing after a manual `y-t` overlay toggle during startup, repeatedly restoring the overlay from duplicate ready signals, missing delayed Japanese subtitle selection on startup, letting later German/Russian subtitle loads steal the selected Japanese track, and spawning long-lived sidebar ffmpeg extractors against Jellyfin stream URLs.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Fixed Jellyfin discovery resume playback when a remote play command sends `StartPositionTicks: 0` despite saved progress on the item.
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Derived Jellyfin cast device identity from the OS hostname, always reports the client as SubMiner, and ignores legacy configurable Jellyfin client/device identity fields so multiple SubMiner installs no longer share the same remote-session identity.
-11
View File
@@ -1,11 +0,0 @@
type: fixed
area: jellyfin
- Fixed Jellyfin `y-t` overlay hide so the plugin sends an explicit hide command when it knows the overlay is visible, avoiding overlay reloads and paused playback resumes.
- Kept that manual hide sticky across Jellyfin stream redirects that change mpv's path, even when the redirected URL drops mpv's media title.
- Re-armed managed subtitle defaults during those path-changing redirects so Japanese primary subtitles can load on the redirected stream.
- Routed visible-overlay shortcuts and app-side visibility changes back through the mpv plugin so SubMiner overlay toggling stays independent of Jellyfin playback controls.
- Collapsed duplicate visible-overlay toggle events so Hyprland does not process one physical shortcut as hide-then-show.
- Kept passive Linux/Hyprland visible-overlay shows from taking keyboard focus away from mpv/Jellyfin.
- Made Jellyfin external subtitle selection tolerate transient mpv `track-list` read failures and numeric string track IDs so Japanese subtitles are selected after preload on Linux.
- Fixed AppImage-launched Jellyfin playback controls so mpv sends overlay commands to the running SubMiner app-control socket instead of the mounted Electron binary.
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Fixed Jellyfin remote controller visibility and progress syncing for mpv/SubMiner seek jumps, stopped sessions, startup path changes, and Linux websocket reconnect windows.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: stats
- Grouped Jellyfin playback stats under Jellyfin item metadata instead of stream URLs, so watched episodes merge with matching local library titles and keep clean display names.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Improved Jellyfin subtitle timing behavior by preferring default embedded subtitle streams over external sidecars, stripping Jellyfin's server-selected subtitle stream from mpv playback URLs, suppressing mpv's subtitle auto-selection and plugin overlay auto-start while SubMiner stages managed tracks, automatically correcting clear Japanese-vs-English cue timeline offsets, and restoring saved per-stream subtitle delay shifts.
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Keep the Jellyfin discovery tray checkbox in sync on Linux after tray, CLI, or startup remote-session changes, with a visible check mark when Linux tray hosts ignore native checkbox rendering.
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Restarted stale Jellyfin tray discovery sessions when the server no longer lists the SubMiner cast target, avoiding a needless Jellyfin re-login.
-5
View File
@@ -1,5 +0,0 @@
type: fixed
area: jellyfin
- Preserved Jellyfin-visible resume progress when mpv resets its position during playback stop by reusing SubMiner's last known playback position for final progress and stopped reports.
- Kept Jellyfin remote Play and Resume distinct so normal Play starts from the beginning, while Resume starts at Jellyfin's requested position without an early mpv seek race.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: anki
- Fixed Kiku duplicate-card field grouping so local duplicate sentence cards trigger the manual modal or auto merge, modal-open acknowledgement races no longer cancel the flow, and merged card fields follow Kiku's group ordering, sentence-audio, furigana, and tag semantics.
@@ -1,4 +1,4 @@
type: fixed
area: launcher
- Suppressed Electron macOS menu diagnostics from `subminer settings` launcher output.
- Suppressed Electron macOS menu diagnostics from `subminer config` launcher output.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: launcher
- Fixed `subminer app` on Linux so launching the tray app returns control to the terminal immediately instead of waiting for the tray process to exit.
@@ -1,4 +0,0 @@
type: fixed
area: playback
- Fixed managed mpv startup so launcher-owned videos quit SubMiner when playback ends, background/tray sessions stay alive, and pause-until-ready waits for the overlay and tokenization readiness before playback resumes.
@@ -1,4 +0,0 @@
type: fixed
area: mpv
- Pass generated session-action CLI args to the mpv plugin.
@@ -1,7 +0,0 @@
type: fixed
area: logging
- Forward SubMiner `logging.level` into launcher-started and Windows shortcut-started mpv sessions, including mpv log verbosity, plugin script logging, and plugin-launched app logging.
- Add numeric `logging.rotation`, defaulting to 7 days of retained daily app, launcher, and mpv logs.
- Log Windows mpv launch diagnostics, IPC socket connection state, subtitle track summaries, Yomitan extension load state, dictionary counts, and expected/active IPC socket values when plugin auto-start skips due to a socket mismatch.
- Add `logging.files` toggles for app, launcher, and mpv logs, with mpv logs disabled by default unless explicitly enabled for debugging.
@@ -1,4 +0,0 @@
type: fixed
area: shortcuts
- Focus the visible overlay when entering multi-line copy/mine selection so number keys choose the line count on macOS and Windows.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: updater
- Clarified that beta/RC update checks are controlled by `updates.channel`; set it to `"prerelease"` to receive beta/RC updates.
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Kept playback paused for Yomitan lookup popups opened from the subtitle sidebar when popup auto-pause is enabled.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: stats
- Stats: Fixed in-player stats layering so delete confirmations, overlay modals, and update-check dialogs appear above the stats window.
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Fixed Windows managed mpv launches from a background SubMiner instance so the existing warm app receives the start command, retargets the new mpv socket, binds to the player window, and receives startup overlay options.
@@ -1,4 +0,0 @@
type: fixed
area: anki
- Fixed manual clipboard card updates from YouTube playback so generated audio and images use mpv's resolved stream URLs instead of the YouTube page URL.
@@ -1,4 +0,0 @@
type: fixed
area: youtube
- Downloaded selected YouTube primary subtitles to temporary local files so the primary bar and sidebar read the same subtitle source, with temp-file cleanup on reload and quit. Suppressed stale failure notifications by re-checking live mpv subtitle state before reporting primary subtitle load failures.
-1
View File
@@ -1 +0,0 @@
- Fixed frequency annotations for Yomitan single-token compounds with internal particles, such as `目の前`, while keeping pure grammar/kana helper spans unannotated.
-4
View File
@@ -1,4 +0,0 @@
type: changed
area: setup
- Setup: Removed the bundled mpv runtime plugin readiness card; legacy mpv plugin removal still appears when needed.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Kept Jellyfin picker library discovery working when the running app log level is above info.
-5
View File
@@ -1,5 +0,0 @@
type: fixed
area: jellyfin
- Showed the visible subtitle overlay automatically during Jellyfin playback so configured `subtitleStyle` appearance applies to Jellyfin subtitles.
- Injected the bundled mpv plugin when SubMiner auto-launches mpv for Jellyfin playback, restoring mpv-side keybindings without needing overlay focus.
-5
View File
@@ -1,5 +0,0 @@
type: changed
area: anki
- `ankiConnect.nPlusOne.enabled` is no longer implicitly set to `true` when `ankiConnect.knownWords.highlightEnabled` is `true`. Users who rely on known-word highlighting and want N+1 target highlighting must now set `ankiConnect.nPlusOne.enabled: true` explicitly.
- Updated known-word cache docs and examples to recommend expression/word fields and removed legacy-option references from user-facing config docs.
-5
View File
@@ -1,5 +0,0 @@
type: changed
area: updater
- Linux tray "Check for Updates" now installs the new AppImage automatically via `electron-updater`, matching the macOS and Windows tray flow, instead of stopping at a "manual update required" dialog. AppImages managed by a system package (AUR `/opt/SubMiner/SubMiner.AppImage`) and non-AppImage launches (no `APPIMAGE` env) still fall back to the GitHub-asset flow.
- Routed `electron-updater` HTTP through `/usr/bin/curl` on Linux and disabled differential downloads, matching the macOS path, so background update checks stay off Electron's network service.
-4
View File
@@ -1,4 +0,0 @@
type: added
area: logs
- Add sanitized log ZIP exports from the tray menu and `subminer logs -e`, with home-directory usernames redacted from exported log contents.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: launcher
- macOS `subminer settings` launches now exit cleanly after the settings window is closed, returning control to the terminal without requiring Ctrl+C.
+1 -1
View File
@@ -1,4 +1,4 @@
type: fixed
area: updater
- Fixed macOS tray update checks for builds that cannot install native app updates, so newer stable or prerelease GitHub releases are reported instead of incorrectly saying the current build is up to date.
- Fixed tray update checks for builds that cannot install native app updates, showing a manual install message instead of a restart prompt that cannot apply the update.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Kept the macOS visible overlay stable when clicking from the overlay back into mpv.
@@ -1,4 +0,0 @@
type: fixed
area: updater
- macOS update dialogs triggered by `subminer -u` now reliably appear in the foreground. SubMiner now shows the dock icon and activates itself via `osascript` (LaunchServices) before opening the modal alert; `app.focus({ steal: true })` alone was unreliable when SubMiner was reached through single-instance forwarding from the CLI-spawned child, leaving the dialog stranded behind other apps with a bouncing dock icon.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: updater
- Routed macOS supplemental GitHub release lookups through `/usr/bin/curl` instead of Electron `net.fetch`, eliminating the last Electron-networking path from background update checks and avoiding the network-service crashes seen in earlier prereleases.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: release
- Fixed macOS updater metadata mismatches by giving macOS and Windows ZIP release assets distinct build-time filenames.
@@ -1,4 +0,0 @@
type: fixed
area: logging
- Stop repeated MPV IPC socket warning spam while the app waits in the background for mpv to recreate the IPC socket.
-4
View File
@@ -1,4 +0,0 @@
type: added
area: launcher
- Added `mpv.profile` config and settings support for passing an mpv profile to SubMiner-managed mpv launches.
@@ -1,4 +0,0 @@
type: fixed
area: character-dictionary
- Use `subtitleStyle.nameMatchEnabled` as the only switch for character-dictionary sync/builds and hide the legacy `anilist.characterDictionary.enabled` option.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: websocket
- WebSocket: Kept the regular subtitle websocket plain-text only; annotation spans and token metadata now stay on the annotation websocket.
-4
View File
@@ -1,4 +0,0 @@
type: added
area: config
- Added `subtitleStyle.primaryVisibleOnYomitanPopup` to keep hover-mode primary subtitles visible while a Yomitan popup is open.
@@ -1,4 +0,0 @@
type: changed
area: jellyfin
- Removed the Jellyfin setup server presets dropdown; setup now shows a single editable server URL field.
@@ -1,4 +0,0 @@
type: fixed
area: anki
- Sentence cards now refresh the current secondary subtitle before saving, so SelectionText uses the loaded translation instead of repeating the primary subtitle.
@@ -1,4 +0,0 @@
type: added
area: setup
- Setup: Added an Open SubMiner Settings button to first-run setup and moved Finish setup to the right-side action slot.
-4
View File
@@ -1,4 +0,0 @@
type: changed
area: subtitles
- Subsync now always opens the manual picker and the `subsync.defaultMode` config/settings option has been removed.
+1 -1
View File
@@ -1,4 +1,4 @@
type: fixed
area: config
- Fixed live Settings window saves so primary and secondary subtitle CSS declarations apply immediately to open video overlays.
- Fixed live Configuration window saves so primary and secondary subtitle CSS declarations apply immediately to open video overlays.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Fixed subtitle sidebar mining so Yomitan-enriched cards use audio and images from the clicked sidebar line instead of the current primary subtitle line.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: tray
- Fixed the Windows tray "Open SubMiner Setup" action so it opens the setup window after first-run setup is already complete.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: windows
- Windows startup failures now show a native error dialog and write fatal details to the SubMiner app log instead of exiting silently.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: updates
- Windows automatic updates now keep the native `electron-updater`/NSIS install path enabled while routing updater HTTP through main-process fetch, avoiding the delayed app exit seen shortly after launch without requiring `curl.exe`.
-7
View File
@@ -1,7 +0,0 @@
type: fixed
area: linux
- Suppressed false YouTube primary subtitle failure notifications after SubMiner confirms the selected primary track loaded successfully.
- Ensured launcher-managed playback commands create the tray icon even when they attach to an already-running SubMiner process.
- Prevented app-owned YouTube playback from letting the mpv plugin start a second SubMiner process after the launcher already started one.
- Logged Linux tray registration failures with a StatusNotifier/AppIndicator hint and documented the Hyprland tray-host requirement.
+165 -107
View File
@@ -5,6 +5,7 @@
* Copy to %APPDATA%/SubMiner/config.jsonc on Windows, or $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) on Linux/macOS.
*/
{
// ==========================================
// Visible Overlay Auto-Start
// Show the visible subtitle overlay automatically after managed mpv playback starts SubMiner.
@@ -18,7 +19,7 @@
// ==========================================
"texthooker": {
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
"openBrowser": false, // Open the texthooker page in the default browser when the server starts. Values: true | false
"openBrowser": false // Open the texthooker page in the default browser when the server starts. Values: true | false
}, // Configure texthooker startup launch and browser opening behavior.
// ==========================================
@@ -28,7 +29,7 @@
// ==========================================
"websocket": {
"enabled": false, // Built-in subtitle websocket server mode. Values: auto | true | false
"port": 6677, // Built-in subtitle websocket server port.
"port": 6677 // Built-in subtitle websocket server port.
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
// ==========================================
@@ -38,23 +39,17 @@
// ==========================================
"annotationWebsocket": {
"enabled": false, // Annotated subtitle websocket server enabled state. Values: true | false
"port": 6678, // Annotated subtitle websocket server port.
"port": 6678 // Annotated subtitle websocket server port.
}, // Dedicated annotated subtitle websocket for bundled texthooker and token-aware clients.
// ==========================================
// Logging
// Controls logging verbosity.
// Set to debug for full runtime diagnostics.
// Hot-reload: logging.level and logging.files apply live while SubMiner is running.
// Hot-reload: logging.level applies live while SubMiner is running.
// ==========================================
"logging": {
"level": "warn", // Minimum log level for runtime logging. Values: debug | info | warn | error
"rotation": 7, // Number of days of app, launcher, and mpv logs to retain.
"files": {
"app": true, // Write SubMiner app runtime logs. Values: true | false
"launcher": true, // Write launcher command logs. Values: true | false
"mpv": false, // Write mpv player logs. Enable temporarily when debugging mpv/plugin startup. Values: true | false
}, // Files setting.
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
}, // Controls logging verbosity.
// ==========================================
@@ -87,66 +82,66 @@
"leftStickPress": 9, // Raw button index used for controller L3 input.
"rightStickPress": 10, // Raw button index used for controller R3 input.
"leftTrigger": 6, // Raw button index used for controller L2 input.
"rightTrigger": 7, // Raw button index used for controller R2 input.
}, // Semantic button-name reference mapping used for debug output. Updating it does not rewrite existing raw binding descriptors.
"rightTrigger": 7 // Raw button index used for controller R2 input.
}, // Semantic button-name reference mapping used for legacy configs and debug output. Updating it does not rewrite existing raw binding descriptors.
"bindings": {
"toggleLookup": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 0, // Raw button index captured for this discrete controller action.
"buttonIndex": 0 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for toggling lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"closeLookup": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 1, // Raw button index captured for this discrete controller action.
"buttonIndex": 1 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for closing lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"toggleKeyboardOnlyMode": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 3, // Raw button index captured for this discrete controller action.
"buttonIndex": 3 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for toggling keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"mineCard": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 2, // Raw button index captured for this discrete controller action.
"buttonIndex": 2 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for mining the active card. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"quitMpv": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 6, // Raw button index captured for this discrete controller action.
"buttonIndex": 6 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for quitting mpv. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"previousAudio": {
"kind": "none", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
}, // Controller binding descriptor for previous Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"nextAudio": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 5, // Raw button index captured for this discrete controller action.
"buttonIndex": 5 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for next Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"playCurrentAudio": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 4, // Raw button index captured for this discrete controller action.
"buttonIndex": 4 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for playing the current Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"toggleMpvPause": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 9, // Raw button index captured for this discrete controller action.
"buttonIndex": 9 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for toggling mpv play/pause. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"leftStickHorizontal": {
"kind": "axis", // Analog binding input source kind. Values: none | axis
"axisIndex": 0, // Raw axis index captured for this analog controller action.
"dpadFallback": "horizontal", // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
"dpadFallback": "horizontal" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
}, // Axis binding descriptor used for left/right token selection. Use Alt+C learn mode or set a raw axis descriptor manually.
"leftStickVertical": {
"kind": "axis", // Analog binding input source kind. Values: none | axis
"axisIndex": 1, // Raw axis index captured for this analog controller action.
"dpadFallback": "vertical", // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
"dpadFallback": "vertical" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
}, // Axis binding descriptor used for primary popup scrolling. Use Alt+C learn mode or set a raw axis descriptor manually.
"rightStickHorizontal": {
"kind": "axis", // Analog binding input source kind. Values: none | axis
"axisIndex": 3, // Raw axis index captured for this analog controller action.
"dpadFallback": "none", // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
}, // Axis binding descriptor reserved for alternate right-stick mappings. Use Alt+C learn mode or set a raw axis descriptor manually.
"rightStickVertical": {
"kind": "axis", // Analog binding input source kind. Values: none | axis
"axisIndex": 4, // Raw axis index captured for this analog controller action.
"dpadFallback": "none", // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
}, // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
}, // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
"profiles": {}, // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
"profiles": {} // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
// ==========================================
@@ -160,7 +155,7 @@
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
"jellyfinRemoteSession": false, // Warm up Jellyfin remote session at startup. Values: true | false
"jellyfinRemoteSession": true // Warm up Jellyfin remote session at startup. Values: true | false
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
// ==========================================
@@ -172,7 +167,7 @@
"enabled": true, // Run automatic update checks in the background. Values: true | false
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
"notificationType": "system", // How SubMiner announces available updates. Values: system | osd | both | none
"channel": "stable", // Release channel used for update checks. Values: stable | prerelease
"channel": "stable" // Release channel used for update checks. Values: stable | prerelease
}, // Automatic update check behavior.
// ==========================================
@@ -192,13 +187,13 @@
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
"toggleSecondarySub": "CommandOrControl+Shift+V", // Accelerator that toggles the secondary subtitle bar visibility.
"markAudioCard": "CommandOrControl+Shift+A", // Accelerator that marks the last mined card as an audio card.
"openCharacterDictionaryManager": "CommandOrControl+D", // Accelerator that opens the character dictionary manager modal.
"openCharacterDictionary": "CommandOrControl+Alt+A", // Accelerator that opens the character dictionary modal.
"openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
"openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
"openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet.
"openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal.
"openControllerDebug": "Alt+Shift+C", // Accelerator that opens the controller debug modal with live axis/button readouts.
"toggleSubtitleSidebar": "Backslash", // Accelerator that toggles the subtitle sidebar visibility.
"toggleSubtitleSidebar": "Backslash" // Accelerator that toggles the subtitle sidebar visibility.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// ==========================================
@@ -210,76 +205,122 @@
"keybindings": [
{
"key": "Space", // Key setting.
"command": ["cycle", "pause"], // Command setting.
"command": [
"cycle",
"pause"
] // Command setting.
},
{
"key": "KeyF", // Key setting.
"command": ["cycle", "fullscreen"], // Command setting.
"command": [
"cycle",
"fullscreen"
] // Command setting.
},
{
"key": "KeyJ", // Key setting.
"command": ["cycle", "sid"], // Command setting.
"command": [
"cycle",
"sid"
] // Command setting.
},
{
"key": "Shift+KeyJ", // Key setting.
"command": ["cycle", "secondary-sid"], // Command setting.
"command": [
"cycle",
"secondary-sid"
] // Command setting.
},
{
"key": "ArrowRight", // Key setting.
"command": ["seek", 5], // Command setting.
"command": [
"seek",
5
] // Command setting.
},
{
"key": "ArrowLeft", // Key setting.
"command": ["seek", -5], // Command setting.
"command": [
"seek",
-5
] // Command setting.
},
{
"key": "ArrowUp", // Key setting.
"command": ["seek", 60], // Command setting.
"command": [
"seek",
60
] // Command setting.
},
{
"key": "ArrowDown", // Key setting.
"command": ["seek", -60], // Command setting.
"command": [
"seek",
-60
] // Command setting.
},
{
"key": "Shift+KeyH", // Key setting.
"command": ["sub-seek", -1], // Command setting.
"command": [
"sub-seek",
-1
] // Command setting.
},
{
"key": "Shift+KeyL", // Key setting.
"command": ["sub-seek", 1], // Command setting.
"command": [
"sub-seek",
1
] // Command setting.
},
{
"key": "Shift+BracketRight", // Key setting.
"command": ["__sub-delay-next-line"], // Command setting.
"command": [
"__sub-delay-next-line"
] // Command setting.
},
{
"key": "Shift+BracketLeft", // Key setting.
"command": ["__sub-delay-prev-line"], // Command setting.
"command": [
"__sub-delay-prev-line"
] // Command setting.
},
{
"key": "Ctrl+Alt+KeyC", // Key setting.
"command": ["__youtube-picker-open"], // Command setting.
"command": [
"__youtube-picker-open"
] // Command setting.
},
{
"key": "Ctrl+Alt+KeyP", // Key setting.
"command": ["__playlist-browser-open"], // Command setting.
"command": [
"__playlist-browser-open"
] // Command setting.
},
{
"key": "Ctrl+Shift+KeyH", // Key setting.
"command": ["__replay-subtitle"], // Command setting.
"command": [
"__replay-subtitle"
] // Command setting.
},
{
"key": "Ctrl+Shift+KeyL", // Key setting.
"command": ["__play-next-subtitle"], // Command setting.
"command": [
"__play-next-subtitle"
] // Command setting.
},
{
"key": "KeyQ", // Key setting.
"command": ["quit"], // Command setting.
"command": [
"quit"
] // Command setting.
},
{
"key": "Ctrl+KeyW", // Key setting.
"command": ["quit"], // Command setting.
},
"command": [
"quit"
] // Command setting.
}
], // Default and custom keybindings that are merged with built-in defaults.
// ==========================================
@@ -291,19 +332,20 @@
"secondarySub": {
"secondarySubLanguages": [], // Language code priority list used to auto-select a secondary subtitle track when available.
"autoLoadSecondarySub": false, // Automatically load a matching secondary subtitle when the primary subtitle loads. Values: true | false
"defaultMode": "hover", // Default visibility mode for the secondary subtitle bar. Values: hidden | visible | hover
"defaultMode": "hover" // Default visibility mode for the secondary subtitle bar. Values: hidden | visible | hover
}, // Dual subtitle track options.
// ==========================================
// Subtitle Sync
// Auto Subtitle Sync
// Subsync engine and executable paths.
// Hot-reload: subsync changes apply to the next subtitle sync run.
// ==========================================
"subsync": {
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
"alass_path": "", // Optional absolute path to the alass binary used by subsync. Leave empty to auto-discover from PATH.
"ffsubsync_path": "", // Optional absolute path to the ffsubsync binary used by subsync. Leave empty to auto-discover from PATH.
"ffmpeg_path": "", // Optional absolute path to the ffmpeg binary used by subsync. Leave empty to auto-discover from PATH.
"replace": true, // Replace the active subtitle file when sync completes. Values: true | false
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
}, // Subsync engine and executable paths.
// ==========================================
@@ -311,7 +353,7 @@
// Initial vertical subtitle position from the bottom.
// ==========================================
"subtitlePosition": {
"yPercent": 10, // Vertical position of the subtitle overlay expressed as a percentage from the bottom of the screen.
"yPercent": 10 // Vertical position of the subtitle overlay expressed as a percentage from the bottom of the screen.
}, // Initial vertical subtitle position from the bottom.
// ==========================================
@@ -336,15 +378,13 @@
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
"--subtitle-hover-token-background-color": "transparent", // Subtitle hover token background color setting.
"--subtitle-hover-token-background-color": "rgba(54, 58, 79, 0.84)" // Subtitle hover token background color setting.
}, // CSS declaration object applied to primary subtitles after normal subtitle style defaults.
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
"autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
"primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
"nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
"nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
"nPlusOneColor": "#c6a0f6", // Color used for the single N+1 target token subtitle highlight.
"knownWordColor": "#a6da95", // Color used for known-word subtitle highlights.
@@ -353,7 +393,7 @@
"N2": "#f5a97f", // N2 setting.
"N3": "#f9e2af", // N3 setting.
"N4": "#8bd5ca", // N4 setting.
"N5": "#8aadf4", // N5 setting.
"N5": "#8aadf4" // N5 setting.
}, // Jlpt colors setting.
"frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
@@ -362,7 +402,13 @@
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#8bd5ca", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
"bandedColors": [
"#ed8796",
"#f5a97f",
"#f9e2af",
"#8bd5ca",
"#8aadf4"
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
}, // Frequency dictionary setting.
"secondary": {
"css": {
@@ -378,9 +424,9 @@
"font-kerning": "normal", // Font kerning setting.
"text-rendering": "geometricPrecision", // Text rendering setting.
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
}, // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
}, // Secondary setting.
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
} // Secondary setting.
}, // Primary and secondary subtitle styling.
// ==========================================
@@ -393,7 +439,7 @@
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
"pauseVideoOnHover": true, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
"pauseVideoOnHover": false, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
"autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
@@ -405,8 +451,8 @@
"--subtitle-sidebar-timestamp-color": "#a5adcb", // Subtitle sidebar timestamp color setting.
"--subtitle-sidebar-active-line-color": "#f5bde6", // Subtitle sidebar active line color setting.
"--subtitle-sidebar-active-background-color": "rgba(138, 173, 244, 0.22)", // Subtitle sidebar active background color setting.
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)", // Subtitle sidebar hover background color setting.
}, // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)" // Subtitle sidebar hover background color setting.
} // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
// ==========================================
@@ -420,7 +466,7 @@
"model": "openai/gpt-4o-mini", // Default model identifier requested from the shared AI provider.
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // Default system prompt sent with shared AI provider requests.
"requestTimeoutMs": 15000, // Timeout in milliseconds for shared AI provider requests.
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
// ==========================================
@@ -438,22 +484,23 @@
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
"port": 8766, // Bind port for local AnkiConnect proxy.
"upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
"upstreamUrl": "http://127.0.0.1:8765" // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
}, // Proxy setting.
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
"deck": "", // Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to search all decks.
"tags": [
"SubMiner"
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
"fields": {
"word": "Expression", // Card field for the mined word or expression text.
"audio": "ExpressionAudio", // Card field that receives generated sentence audio.
"image": "Picture", // Card field that receives the captured screenshot or animated image.
"sentence": "Sentence", // Card field that receives the source sentence text.
"miscInfo": "MiscInfo", // Card field that receives the miscellaneous info pattern (see ankiConnect.metadata.pattern).
"translation": "SelectionText", // Card field that receives the current selection or translated text.
"translation": "SelectionText" // Card field that receives the current selection or translated text.
}, // Fields setting.
"ai": {
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
"model": "", // Optional model override for Anki AI translation/enrichment flows.
"systemPrompt": "", // Optional system prompt override for Anki AI translation/enrichment flows.
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
}, // Ai setting.
"media": {
"generateAudio": true, // Generate sentence audio for mined cards. Values: true | false
@@ -461,23 +508,20 @@
"imageType": "static", // Image capture type: "static" for a single still frame, "avif" for an animated AVIF. Values: static | avif
"imageFormat": "jpg", // Encoding format used when imageType is "static". Values: jpg | png | webp
"imageQuality": 92, // Quality (0-100) used for lossy static image encoders.
"imageMaxWidth": 0, // Maximum width for static images, in pixels. Set to 0 to preserve the source resolution.
"imageMaxHeight": 0, // Maximum height for static images, in pixels. Set to 0 to preserve the source resolution.
"animatedFps": 10, // Target frame rate for animated AVIF captures.
"animatedMaxWidth": 640, // Maximum width applied to animated AVIF captures.
"animatedMaxHeight": 0, // Maximum height for animated AVIF captures, in pixels. Set to 0 to preserve aspect ratio.
"animatedCrf": 35, // Animated AVIF CRF quality target. Lower values produce larger, higher-quality files.
"syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
"audioPadding": 0, // Seconds of padding appended to both ends of generated sentence audio.
"audioPadding": 0.5, // Seconds of padding appended to both ends of generated sentence audio.
"fallbackDuration": 3, // Fallback clip duration in seconds when subtitle timing data is unavailable.
"maxMediaDuration": 30, // Maximum allowed media clip duration in seconds.
"maxMediaDuration": 30 // Maximum allowed media clip duration in seconds.
}, // Media setting.
"knownWords": {
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Cache matches always receive known-word highlighting even when POS filters suppress other annotation types. Values: headword | surface
"decks": {}, // Decks and expression/word fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word"] }.
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Values: headword | surface
"decks": {} // Decks and fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word", "Word Reading"] }.
}, // Known words setting.
"behavior": {
"overwriteAudio": true, // When updating an existing card, overwrite the audio field instead of skipping it. Values: true | false
@@ -485,24 +529,24 @@
"mediaInsertMode": "append", // Whether new media is appended after or prepended before existing field contents on update. Values: append | prepend
"highlightWord": true, // Bold the mined word inside the sentence field on the saved Anki card. Values: true | false
"notificationType": "osd", // Notification surface used to announce mining and update outcomes. Values: osd | system | both | none
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
}, // Behavior setting.
"nPlusOne": {
"enabled": false, // Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Requires known-word cache data. Values: true | false
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
"minSentenceWords": 3 // Minimum sentence word count required for N+1 targeting (default: 3).
}, // N plus one setting.
"metadata": {
"pattern": "[SubMiner] %f (%t)", // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
"pattern": "[SubMiner] %f (%t)" // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
}, // Metadata setting.
"isLapis": {
"enabled": false, // Enable Lapis-specific mining behaviors and sentence card model targeting. Values: true | false
"sentenceCardModel": "Lapis", // Note type name used by Lapis sentence cards.
"sentenceCardModel": "Lapis" // Note type name used by Lapis sentence cards.
}, // Is lapis setting.
"isKiku": {
"enabled": false, // Enable Kiku-specific mining behaviors (duplicate handling, field grouping). Values: true | false
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
"deleteDuplicateInAuto": true, // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
}, // Is kiku setting.
"deleteDuplicateInAuto": true // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
} // Is kiku setting.
}, // Automatic Anki updates and media generation options.
// ==========================================
@@ -512,10 +556,8 @@
// ==========================================
"jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Base URL of the Jimaku subtitle search API.
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
"maxEntryResults": 10, // Maximum Jimaku search results returned.
"maxEntryResults": 10 // Maximum Jimaku search results returned.
}, // Jimaku API configuration and defaults.
// ==========================================
@@ -524,7 +566,10 @@
// Hot-reload: primarySubLanguages applies to the next YouTube subtitle load.
// ==========================================
"youtube": {
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
"primarySubLanguages": [
"ja",
"jpn"
] // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
}, // Defaults for managed subtitle language preferences and YouTube subtitle loading.
// ==========================================
@@ -537,14 +582,17 @@
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
"characterDictionary": {
"enabled": false, // Enable automatic Yomitan character dictionary sync for currently watched AniList media. Values: true | false
"refreshTtlHours": 168, // Legacy setting; merged character dictionary retention is now usage-based and this value is ignored.
"maxLoaded": 3, // Maximum number of most-recently-used anime snapshots included in the merged Yomitan character dictionary.
"profileScope": "all", // Yomitan profile scope for character dictionary settings updates. Values: all | active
"evictionPolicy": "delete", // Legacy setting; merged character dictionary eviction is usage-based and this value is ignored. Values: disable | delete
"profileScope": "all", // Yomitan profile scope for dictionary enable/disable updates. Values: all | active
"collapsibleSections": {
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
"voicedBy": false, // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
}, // Collapsible sections setting.
}, // Character dictionary setting.
"voicedBy": false // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
} // Collapsible sections setting.
} // Character dictionary setting.
}, // Anilist API credentials and update behavior.
// ==========================================
@@ -555,7 +603,7 @@
// In external-profile mode SubMiner will not import, delete, or modify Yomitan dictionaries/settings.
// ==========================================
"yomitan": {
"externalProfilePath": "", // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
"externalProfilePath": "" // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
}, // Optional external Yomitan profile integration.
// ==========================================
@@ -564,20 +612,18 @@
// Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin.
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.
// Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none.
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
// ==========================================
"mpv": {
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
"launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
"profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
"socketPath": "\\\\.\\pipe\\subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
"backend": "auto", // Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform. Values: auto | hyprland | sway | x11 | macos | windows
"autoStartSubMiner": true, // Start SubMiner in the background when SubMiner-managed mpv loads a file. Values: true | false
"pauseUntilOverlayReady": true, // Pause mpv on visible-overlay auto-start until SubMiner signals subtitle tokenization readiness. Values: true | false
"subminerBinaryPath": "", // Optional SubMiner app binary path passed to the bundled mpv plugin. Leave empty to use the launcher-detected app path.
"aniskipEnabled": true, // Enable AniSkip intro detection and skip markers in the bundled mpv plugin. Values: true | false
"aniskipButtonKey": "TAB", // mpv key used to trigger the AniSkip button while the skip marker is visible.
"aniskipButtonKey": "TAB" // mpv key used to trigger the AniSkip button while the skip marker is visible.
}, // SubMiner-managed mpv launch and bundled plugin options.
// ==========================================
@@ -591,15 +637,27 @@
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
"recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup.
"username": "", // Default Jellyfin username used during CLI login.
"deviceId": "subminer", // Stable device identifier sent on the Jellyfin authentication handshake; primarily internal.
"clientName": "SubMiner", // Client name sent on the Jellyfin authentication handshake; primarily internal.
"clientVersion": "0.1.0", // Client version sent on the Jellyfin authentication handshake; primarily internal.
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
"remoteControlDeviceName": "SubMiner", // Device name reported for Jellyfin remote control sessions.
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions.
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
"directPlayContainers": [
"mkv",
"mp4",
"webm",
"mov",
"flac",
"mp3",
"aac"
], // Container allowlist for direct play decisions.
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
// ==========================================
@@ -611,7 +669,7 @@
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal". Values: default | meme | japanese | minimal
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates.
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
// ==========================================
@@ -636,13 +694,13 @@
"sessionsDays": 0, // Session retention window in days. Use 0 to keep all.
"dailyRollupsDays": 0, // Daily rollup retention window in days. Use 0 to keep all.
"monthlyRollupsDays": 0, // Monthly rollup retention window in days. Use 0 to keep all.
"vacuumIntervalDays": 0, // Minimum days between VACUUM runs. Use 0 to disable.
"vacuumIntervalDays": 0 // Minimum days between VACUUM runs. Use 0 to disable.
}, // Retention setting.
"lifetimeSummaries": {
"global": true, // Maintain global lifetime stats rows. Values: true | false
"anime": true, // Maintain per-anime lifetime stats rows. Values: true | false
"media": true, // Maintain per-media lifetime stats rows. Values: true | false
}, // Lifetime summaries setting.
"media": true // Maintain per-media lifetime stats rows. Values: true | false
} // Lifetime summaries setting.
}, // Enable/disable immersion tracking.
// ==========================================
@@ -655,6 +713,6 @@
"markWatchedKey": "KeyW", // Key code to mark the current video as watched and advance to the next playlist entry.
"serverPort": 6969, // Port for the stats HTTP server.
"autoStartServer": true, // Automatically start the stats server on launch. Values: true | false
"autoOpenBrowser": false, // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
}, // Local immersion stats dashboard served on localhost and available as an in-app overlay.
"autoOpenBrowser": false // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
} // Local immersion stats dashboard served on localhost and available as an in-app overlay.
}
+13 -26
View File
@@ -19,26 +19,18 @@ type VersionManifest = {
versions: Array<{ version: string; path: string }>;
};
function optionalEnv(value: string | undefined): string | undefined {
return value && value !== 'undefined' ? value : undefined;
}
const base = normalizeBase(optionalEnv(process.env.SUBMINER_DOCS_BASE) ?? '/');
const outDir = optionalEnv(process.env.SUBMINER_DOCS_OUT_DIR);
const docsSourceDir = optionalEnv(process.env.SUBMINER_DOCS_SOURCE_DIR) ?? process.cwd();
const channel = normalizeChannel(optionalEnv(process.env.SUBMINER_DOCS_CHANNEL));
const docsVersion = optionalEnv(process.env.SUBMINER_DOCS_VERSION);
const latestStable = optionalEnv(process.env.SUBMINER_DOCS_LATEST_STABLE) ?? 'v0.14.0';
const base = normalizeBase(process.env.SUBMINER_DOCS_BASE ?? '/');
const outDir = process.env.SUBMINER_DOCS_OUT_DIR;
const docsSourceDir = process.env.SUBMINER_DOCS_SOURCE_DIR ?? process.cwd();
const localArchiveDir = resolve(
process.env.SUBMINER_DOCS_LOCAL_ARCHIVE_DIR ??
join(docsSourceDir, '..', '.tmp/docs-versioned-site'),
);
const channel = normalizeChannel(process.env.SUBMINER_DOCS_CHANNEL);
const docsVersion = process.env.SUBMINER_DOCS_VERSION;
const latestStable = process.env.SUBMINER_DOCS_LATEST_STABLE ?? 'v0.14.0';
const versionManifest = parseVersionManifest(process.env.SUBMINER_DOCS_VERSION_MANIFEST);
const versionLinkOrigin =
optionalEnv(process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN) ?? 'production';
function getLocalArchiveDir(): string {
return resolve(
optionalEnv(process.env.SUBMINER_DOCS_LOCAL_ARCHIVE_DIR) ??
join(docsSourceDir, '..', '.tmp/docs-versioned-site'),
);
}
const versionLinkOrigin = process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN ?? 'production';
function normalizeBase(value: string): string {
if (!value || value === '/') return '/';
@@ -51,7 +43,7 @@ function normalizeChannel(value: string | undefined): DocsChannel {
}
function parseVersionManifest(value: string | undefined): VersionManifest {
if (!value || value === 'undefined') {
if (!value) {
return {
latestStable,
channels: [
@@ -226,7 +218,6 @@ function isFile(path: string): boolean {
function archiveFileForPathname(pathname: string): string | null {
if (!shouldHandleLocalVersionRoute(pathname)) return null;
const localArchiveDir = getLocalArchiveDir();
const routePath = decodeURIComponent(pathname).replace(/^\/+/, '');
const filePath = resolve(localArchiveDir, routePath);
if (filePath !== localArchiveDir && !filePath.startsWith(`${localArchiveDir}${sep}`)) {
@@ -243,11 +234,7 @@ function archiveFileForPathname(pathname: string): string | null {
}
function serveLocalArchiveRoute(pathname: string, response: DevServerResponse): boolean {
if (
(optionalEnv(process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN) ?? versionLinkOrigin) !== 'local'
) {
return false;
}
if (versionLinkOrigin !== 'local') return false;
const filePath = archiveFileForPathname(pathname);
if (!filePath) return false;
+2 -3
View File
@@ -4,8 +4,6 @@ SubMiner can sync your watch progress to [AniList](https://anilist.co) automatic
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.
[AniList](https://anilist.co) is a free website for tracking which anime you have watched. An **access token** is a private key SubMiner stores so it can update your list on your behalf — you approve it once during setup, and you never paste a password into SubMiner.
## Setup
AniList integration is opt-in. To enable it:
@@ -98,11 +96,12 @@ All AniList API calls go through a shared rate limiter that enforces a sliding w
| ------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------ |
| `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. Character dictionary sync follows `subtitleStyle.nameMatchEnabled`.
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
+6 -14
View File
@@ -3,13 +3,6 @@
SubMiner uses the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on to create and update Anki cards with sentence context, audio, and screenshots.
This project is built primarily for [Kiku](https://kiku.youyoumu.my.id/) and [Lapis](https://github.com/donkuri/lapis) note types, including sentence-card and field-grouping behavior.
::: tip New to these terms?
- **Anki** is the flashcard app where your study cards live.
- **AnkiConnect** is a free add-on that lets other programs (like SubMiner) talk to Anki over a local connection. SubMiner needs it installed to add or edit cards.
- A **note type** (also called a "model") is the template that defines what a card looks like — for example the Kiku or Lapis templates many Japanese learners use.
- A **field** is one labeled slot in that template, such as `Sentence`, `Expression`, or `Picture`. SubMiner fills these fields when it mines a card.
:::
## Prerequisites
1. Install [Anki](https://apps.ankiweb.net/).
@@ -22,9 +15,9 @@ AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the po
When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, image, and translation fields automatically. Two detection methods are available:
**Proxy mode** (default) — SubMiner runs a local *proxy*: a small middleman server that sits between Yomitan and Anki. Yomitan sends new cards to SubMiner, SubMiner enriches them, then passes them along to Anki. This makes enrichment instant.
**Proxy mode** — SubMiner runs a local AnkiConnect-compatible proxy and intercepts card creation instantly. Recommended when possible.
**Polling mode** (fallback, when the proxy is disabled) — SubMiner asks AnkiConnect every few seconds whether any new cards were added, then enriches them. Simpler setup, but with a short delay (~3 seconds).
**Polling mode** (default) — SubMiner polls AnkiConnect every few seconds for newly added cards. Simpler setup, but with a short delay (~3 seconds).
Use proxy mode if you want immediate enrichment. Use polling mode if your Yomitan instance is external (browser-based) or you prefer minimal configuration.
@@ -37,7 +30,7 @@ In both modes, the enrichment workflow is the same:
5. Writes metadata to the miscInfo field.
Polling mode uses the query `"deck:<ankiConnect.deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks.
Known-word sync scope is controlled by `ankiConnect.knownWords.decks`.
Known-word sync scope is controlled by `ankiConnect.knownWords.decks` (object map), with `ankiConnect.deck` used as legacy fallback.
### Proxy Mode Setup (Yomitan / Texthooker)
@@ -154,13 +147,13 @@ SubMiner uses FFmpeg to generate audio and image media from the video. FFmpeg mu
### Audio
Audio is extracted from the video file using the subtitle's start and end timestamps. Padding is opt-in; keep it at `0` when you want sentence audio to start exactly at the mined sentence.
Audio is extracted from the video file using the subtitle's start and end timestamps, with configurable padding added before and after.
```jsonc
"ankiConnect": {
"media": {
"generateAudio": true,
"audioPadding": 0, // optional seconds before and after subtitle timing
"audioPadding": 0.5, // seconds before and after subtitle timing
"maxMediaDuration": 30 // cap total duration in seconds
}
}
@@ -329,7 +322,6 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
"upstreamUrl": "http://127.0.0.1:8765",
},
"fields": {
"word": "Expression",
"audio": "ExpressionAudio",
"image": "Picture",
"sentence": "Sentence",
@@ -342,7 +334,7 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
"imageType": "static",
"imageFormat": "jpg",
"imageQuality": 92,
"audioPadding": 0,
"audioPadding": 0.5,
"maxMediaDuration": 30,
},
"behavior": {
+1 -1
View File
@@ -273,7 +273,7 @@ For domains migrated to reducer-style transitions (for example AniList token/que
- **Module-level init:** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs all services, and wires dependency injection. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
- **Startup:** If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.
- **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to mpv subtitle/playback properties via `observe_property`), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`.
- **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to 26 properties), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`.
- **Overlay runtime:** `initializeOverlayRuntime()` creates the primary overlay window (interactive Yomitan lookups and subtitle rendering), registers global shortcuts, and sets up bounds tracking via the active window tracker. mpv subtitle suppression is handled by a dedicated `overlay-mpv-sub-visibility` service.
- **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check (with async worker thread), Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, AniList token refresh, and optional AnkiConnect proxy server. Warmup coverage is configurable through `startupWarmups` (including low-power mode that defers all but Yomitan).
- **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, overlay shortcuts, and hot-reload notifications route through runtime handlers/composers. Subtitle text flows through `SubtitlePipeline` (normalize → tokenize → merge), and results are sent to the main overlay renderer and modal surfaces.
-4
View File
@@ -1,9 +1,5 @@
# Changelog
## Unreleased
- **Character Dictionary:** Loaded entries are now scoped to the current AniList media for subtitle name matching and inline portraits. Added a character dictionary manager at `Ctrl/Cmd+D`; AniList overrides now live inside that manager instead of using a separate default shortcut.
## v0.14.0 (2026-05-12)
SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin is injected at runtime only when SubMiner launches mpv — through the `subminer` launcher, the app's managed launch, or the packaged Windows SubMiner mpv shortcut. When you open mpv on its own, SubMiner is not involved and the plugin is never loaded. If you have a legacy global SubMiner plugin under mpv's `scripts` directory, first-run setup detects it and prompts you to remove it before playback starts.
+22 -44
View File
@@ -1,8 +1,6 @@
# Character Dictionary
SubMiner can build a Yomitan-compatible character dictionary from [AniList](https://anilist.co) metadata so that character names in subtitles are recognized, highlighted, and enrichable with context — portraits, roles, voice actors, and biographical detail — without leaving the overlay. (AniList is an online anime/manga database; SubMiner pulls each show's character list from it.)
This is helpful because proper names rarely appear in normal dictionaries, so character names would otherwise be flagged as "unknown" words and clutter your mining. Recognizing them keeps your N+1 highlighting focused on real vocabulary.
SubMiner can build a Yomitan-compatible character dictionary from AniList metadata so that character names in subtitles are recognized, highlighted, and enrichable with context — portraits, roles, voice actors, and biographical detail — without leaving the overlay.
The dictionary is generated per-media, merged across your recently-watched titles, and auto-imported into Yomitan. When a character name appears in a subtitle line, it gets highlighted and becomes available for hover-driven Yomitan profile lookup.
@@ -14,14 +12,14 @@ The feature has three stages: **snapshot**, **merge**, and **match**.
2. **Merge** — SubMiner maintains a most-recently-used list of media IDs (default: 3). Snapshots from those titles are merged into a single Yomitan ZIP — `character-dictionaries/merged.zip` — which is always named "SubMiner Character Dictionary" so Yomitan treats it as a single stable dictionary across rebuilds.
3. **Match** — During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. SubMiner only accepts character entries for the current AniList media when that media ID is known, then flags matching tokens with `isNameMatch` and highlights them in the overlay with a distinct color.
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.
## Enabling the Feature
Character dictionary sync is disabled by default. To turn it on:
1. Authenticate with AniList (see [AniList Integration](/anilist-integration#setup)).
2. Set `subtitleStyle.nameMatchEnabled` to `true` in your config or enable **Name Match Enabled** in Settings.
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.
```jsonc
@@ -29,9 +27,9 @@ Character dictionary sync is disabled by default. To turn it on:
"anilist": {
"enabled": true,
"accessToken": "your-token",
},
"subtitleStyle": {
"nameMatchEnabled": true,
"characterDictionary": {
"enabled": true,
},
},
}
```
@@ -89,29 +87,23 @@ Name matching runs inside Yomitan's scanning pipeline during subtitle tokenizati
1. Yomitan receives subtitle text and scans for dictionary matches.
2. Entries from "SubMiner Character Dictionary" are checked with exact primary-source matching — the token must match the entry's `originalText` with `isPrimary: true` and `matchType: 'exact'`.
3. When the current AniList media ID is known, entries whose embedded media ID belongs to a different title are ignored for name matching and inline portraits.
4. Matched tokens are flagged `isNameMatch: true` and forwarded to the renderer.
5. If `subtitleStyle.nameMatchEnabled` is enabled, the renderer applies the name-match highlight color (default: `#f5bde6`).
6. If `subtitleStyle.nameMatchImagesEnabled` is enabled, the renderer also injects a small circular AniList portrait from the cached snapshot image data.
Older snapshot schema versions are regenerated automatically. Current-version snapshots are normally reused, but when `subtitleStyle.nameMatchImagesEnabled` is enabled SubMiner also checks whether the cached snapshot contains usable character portrait data. If it does not, the snapshot is refreshed so the merged dictionary can include images.
3. Matched tokens are flagged `isNameMatch: true` and forwarded to the renderer.
4. The renderer applies the name-match highlight color (default: `#f5bde6`).
Name matches are visually distinct from [N+1 targeting, frequency highlighting, and JLPT tags](/subtitle-annotations) so you can tell at a glance whether a highlighted word is a character name or a vocabulary target.
**Key settings:**
| Option | Default | Description |
| -------------------------------------- | --------- | ----------------------------------------- |
| `subtitleStyle.nameMatchEnabled` | `false` | Enable dictionary sync and highlighting |
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits beside names |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
| Option | Default | Description |
| -------------------------------- | --------- | ---------------------------------- |
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
## Dictionary Entries
Each character entry in the Yomitan dictionary includes structured content:
- **Name** — the matched Japanese name form
- **Known names** — generated non-honorific Japanese aliases for that character, excluding raw romanized/English aliases from lookup results
- **Name** — native (Japanese) and romanized forms
- **Role badge** — color-coded by role: main (score 100), supporting (90), side (80), background (70)
- **Portrait** — character image from AniList, embedded in the ZIP
- **Description** — biography text from AniList (collapsible)
@@ -136,7 +128,7 @@ The three collapsible sections can be configured to start open or closed:
## Auto-Sync Lifecycle
When `subtitleStyle.nameMatchEnabled` is `true`, SubMiner runs an auto-sync routine whenever the active media changes.
When `characterDictionary.enabled` is `true`, SubMiner runs an auto-sync routine whenever the active media changes.
**Phases:**
@@ -175,13 +167,10 @@ This creates a standalone dictionary ZIP for the target media and saves it along
## Correcting AniList Matches
SubMiner uses `guessit` to infer the anime title from the active filename before searching AniList. Some filenames can still resolve to the wrong title. For example, `Re - ZERO, Starting Life in Another World (2016)` can be misread as a different `Re...` series.
SubMiner uses `guessit` to infer the anime title from the active filename, then searches AniList. Some filenames can still resolve to the wrong title. For example, `Re - ZERO, Starting Life in Another World (2016)` can be misread as a different `Re...` series.
Use the in-app selector or CLI to pin the correct AniList media for the whole series:
- In-app: open the manager with `Ctrl/Cmd+D`, use the **Override** tab/button, edit the prefilled title if needed, then search and choose the correct result.
- CLI: `--dictionary-candidates` still lists matches for the current filename guess.
```bash
# List candidate AniList matches for a file
subminer dictionary --candidates "/path/to/episode.mkv"
@@ -194,20 +183,10 @@ SubMiner.AppImage --dictionary-candidates --dictionary-target "/path/to/episode.
SubMiner.AppImage --dictionary-select --dictionary-anilist-id 21355 --dictionary-target "/path/to/episode.mkv"
# Open the in-app selector from the running app
subminer app --session-action '{"actionId":"openCharacterDictionaryManager"}'
subminer app --open-character-dictionary
```
Manual selections are stored in `character-dictionaries/anilist-overrides.json` using a series key derived from the episode's parent directory plus the filename guess. Later episodes in the same directory use the selected AniList ID automatically, while separate season directories can keep separate overrides and character dictionaries. When the override replaces a previous wrong match, SubMiner removes that stale media ID from the merged dictionary's active set and rebuilds/imports the merged character dictionary.
## Managing Loaded Entries
Open the manager with `Ctrl/Cmd+D` (`shortcuts.openCharacterDictionaryManager`). The manager shows the merged dictionary's active MRU entries, marks the current anime, and lets you adjust eviction priority for the other loaded entries.
- **Remove** drops a non-current entry from the active merged dictionary and rebuilds/imports once.
- **Up/Down** changes MRU order for future eviction without rebuilding.
- **Override** opens the AniList selector for that entry's title so you can replace a saved loaded entry.
The current anime cannot be removed while you are watching it; it stays loaded until playback changes.
Manual selections are stored in `character-dictionaries/anilist-overrides.json` using a series key derived from the filename guess. Later episodes with the same series key use the selected AniList ID automatically. When the override replaces a previous wrong match, SubMiner removes that stale media ID from the merged dictionary's active set and rebuilds/imports the merged character dictionary.
## File Structure
@@ -226,7 +205,7 @@ character-dictionaries/
m170942-va67890.jpg # Voice actor portrait
```
**Snapshot format** (v17): each snapshot contains the media ID, title, entry count, timestamp, an array of Yomitan term entries, and base64-encoded images.
**Snapshot format** (v15): each snapshot contains the media ID, title, entry count, timestamp, an array of Yomitan term entries, and base64-encoded images.
**ZIP structure** follows the Yomitan dictionary format:
@@ -243,13 +222,13 @@ merged.zip
| Option | Default | Description |
| ---------------------------------------------------------------------- | --------- | --------------------------------------------------------------- |
| `anilist.characterDictionary.enabled` | `false` | Enable auto-sync of character dictionary from AniList |
| `anilist.characterDictionary.maxLoaded` | `3` | Number of recent media snapshots kept in the merged dictionary |
| `anilist.characterDictionary.profileScope` | `"all"` | Apply dictionary to `"all"` Yomitan profiles or `"active"` only |
| `anilist.characterDictionary.collapsibleSections.description` | `false` | Start Description section expanded |
| `anilist.characterDictionary.collapsibleSections.characterInformation` | `false` | Start Character Information section expanded |
| `anilist.characterDictionary.collapsibleSections.voicedBy` | `false` | Start Voiced By section expanded |
| `subtitleStyle.nameMatchEnabled` | `false` | Enable character-dictionary sync and name highlighting |
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits beside matched names |
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting in subtitles |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for character-name matches |
## Reference Implementation
@@ -271,10 +250,9 @@ If you work with visual novels or want a standalone dictionary generator indepen
## Troubleshooting
- **Names not highlighting:** Confirm `subtitleStyle.nameMatchEnabled` is `true`. Check that the current media has an AniList entry — SubMiner needs a media ID to fetch characters.
- **Inline portraits missing:** Confirm `subtitleStyle.nameMatchImagesEnabled` is `true`. On the next character dictionary sync, SubMiner refreshes current-version snapshots that do not contain usable cached character portrait data. Portraits still require AniList to return an image and the image download to succeed.
- **Names not highlighting:** Confirm `anilist.characterDictionary.enabled` is `true` and `subtitleStyle.nameMatchEnabled` is `true`. Check that the current media has an AniList entry — SubMiner needs a media ID to fetch characters.
- **Sync seems stuck:** The auto-sync debounces for 800ms after media changes and throttles image downloads at 250ms per image. Large casts (50+ characters) take longer. Check the status bar for the current sync phase.
- **Wrong characters showing:** Open the in-app character dictionary manager (`Ctrl/Cmd+D`) to remove/reorder loaded titles, then use **Override** to correct the active AniList match. You can also run `--dictionary-candidates`, then save the correct media with `--dictionary-select --dictionary-anilist-id <id>`. SubMiner ignores character entries from other loaded titles for subtitle name matching and inline portraits once the current media ID is known.
- **Wrong characters showing:** Open the in-app character dictionary selector (`--open-character-dictionary`) or run `--dictionary-candidates`, then save the correct media with `--dictionary-select --dictionary-anilist-id <id>`. This replaces stale wrong-title entries for that series. If names are only from an older unrelated show, they'll rotate out once you watch enough new titles to push it past `maxLoaded`.
- **Yomitan import fails:** SubMiner waits up to 7 seconds for Yomitan to be ready for mutations. If Yomitan is still loading dictionaries or performing another import, the operation may time out. Restarting the overlay typically resolves this.
- **Portraits missing:** Images are downloaded from AniList CDN during snapshot generation. If the network was unavailable during the initial sync, delete the snapshot file from `character-dictionaries/snapshots/` and let it regenerate.
+310 -287
View File
@@ -8,8 +8,6 @@ outline: [2, 3]
import { withBase } from 'vitepress';
</script>
SubMiner is configured through a single file (`config.jsonc`). Most settings are also editable from the in-app **Settings** window — you rarely need to edit the file by hand. This page is the full reference: it explains the Settings window, where the config file lives, and documents every option grouped by topic. New to SubMiner? The Quick Start below plus the [Settings window](#settings) cover everything most users need.
## Quick Start
For most users, start with this minimal configuration:
@@ -21,7 +19,7 @@ For most users, start with this minimal configuration:
"deck": "YourDeckName",
"knownWords": {
"decks": {
"YourDeckName": ["Word"]
"YourDeckName": ["Word", "Word Reading", "Expression"]
}
},
"fields": {
@@ -33,13 +31,13 @@ For most users, start with this minimal configuration:
}
```
Use the known-word deck map to choose which Anki decks and note fields feed the known-word cache.
`ankiConnect.deck` is still accepted for backward-compatible polling scope and legacy known-word fallback behavior. For known-word cache scope, prefer `ankiConnect.knownWords.decks` with deck-to-fields mapping.
Then customize as needed using the sections below.
## Settings
SubMiner includes a dedicated **Settings** window accessible from the tray menu, the app `--settings` flag, or launcher commands such as `subminer --settings` and `subminer settings`. It is the primary way to configure SubMiner — all changes are written directly to `config.jsonc`, so manual file editing is not required for most users.
SubMiner includes a dedicated **Settings** window accessible from the tray menu, the app `--config` flag, or launcher commands such as `subminer --config` and `subminer config`. It is the primary way to configure SubMiner — all changes are written directly to `config.jsonc`, so manual file editing is not required for most users.
The Settings window groups options by workflow instead of mirroring the raw config-file shape:
@@ -54,7 +52,7 @@ The Settings window groups options by workflow instead of mirroring the raw conf
Each field still writes to its current `config.jsonc` path. For example, subtitle hover pause appears under **Behavior** / playback behavior, but saves to `subtitleStyle.autoPauseVideoOnHover`. Anki-aware fields can query AnkiConnect for deck names, note types, and field names, and keybinding fields use click-to-learn controls instead of raw text boxes.
The Settings window preserves existing JSONC comments, trailing commas, and unrelated keys. Resetting a field removes the explicit config path so the built-in default applies.
The Settings window preserves existing JSONC comments, trailing commas, unrelated keys, and unsupported legacy options. Resetting a field removes the explicit config path so the built-in default applies.
Secret fields do not display stored values. They show whether a value is configured; entering a new value writes it, and reset clears the explicit path. Prefer command-based secret options such as `ai.apiKeyCommand` when available.
@@ -94,10 +92,35 @@ On macOS, these validation warnings also open a native dialog with full details
SubMiner watches the active config file (`config.jsonc` or `config.json`) while running and applies supported updates automatically.
Hot-reloadable settings include subtitle appearance, sidebar controls, keybindings,
logging level, selected source-language preferences, Jimaku/Subsync settings, and
the Anki known-word, N+1, field, sentence-card, and Kiku options listed in the
reference tables below.
Hot-reloadable fields:
- `subtitleStyle`
- `subtitleSidebar`
- `keybindings`
- `shortcuts`
- `secondarySub.defaultMode`
- `stats.toggleKey`
- `stats.markWatchedKey`
- `logging.level`
- `youtube.primarySubLanguages`
- `jimaku.*`
- `subsync.*`
- `ankiConnect.ai.enabled`
- `ankiConnect.behavior.autoUpdateNewCards`
- `ankiConnect.knownWords.highlightEnabled`
- `ankiConnect.knownWords.refreshMinutes`
- `ankiConnect.knownWords.addMinedWordsImmediately`
- `ankiConnect.knownWords.matchMode`
- `ankiConnect.knownWords.decks`
- `ankiConnect.nPlusOne.enabled`
- `ankiConnect.nPlusOne.minSentenceWords`
- `ankiConnect.fields.word`
- `ankiConnect.fields.audio`
- `ankiConnect.fields.image`
- `ankiConnect.fields.sentence`
- `ankiConnect.fields.miscInfo`
- `ankiConnect.isLapis.sentenceCardModel`
- `ankiConnect.isKiku.fieldGrouping`
When these values change, SubMiner applies them live. Invalid config edits are rejected and the previous valid runtime config remains active.
@@ -148,14 +171,14 @@ The configuration file includes several main sections:
**External Integrations**
- [**Jimaku**](#jimaku) - Jimaku API configuration and defaults
- [**Subtitle Sync**](#subtitle-sync) - Sync current subtitle with `alass`/`ffsubsync`
- [**Auto Subtitle Sync**](#auto-subtitle-sync) - Sync current subtitle with `alass`/`ffsubsync`
- [**AniList**](#anilist) - Optional post-watch progress updates
- [**Yomitan**](#yomitan) - Reuse an external read-only Yomitan profile
- [**Yomitan**](#yomitan) - Reuse an external read-only Yomitan profile via `yomitan.externalProfilePath`
- [**Jellyfin**](#jellyfin) - Optional Jellyfin auth, library listing, and playback launch
- [**Discord Rich Presence**](#discord-rich-presence) - Optional Discord activity card updates
- [**Immersion Tracking**](#immersion-tracking) - Track subtitle sessions and mining activity in SQLite
- [**Stats Dashboard**](#stats-dashboard) - Local dashboard and overlay for immersion progress
- [**MPV Launcher**](#mpv-launcher) - mpv executable path, profile, and window launch mode
- [**MPV Launcher**](#mpv-launcher) - mpv executable path and window launch mode
- [**YouTube Playback Settings**](#youtube-playback-settings) - Defaults for YouTube subtitle loading
- [**Updates**](#updates) - Automatic update checks, notifications, and prerelease testing
@@ -168,24 +191,14 @@ Control the minimum log level for runtime output:
```json
{
"logging": {
"level": "warn",
"rotation": 7,
"files": {
"app": true,
"launcher": true,
"mpv": false
}
"level": "info"
}
}
```
| Option | Values | Description |
| ---------------- | ---------------------------------------- | -------------------------------------------------------------------- |
| `level` | `"debug"`, `"info"`, `"warn"`, `"error"` | Minimum log level for runtime logging (default: `"warn"`) |
| `rotation` | positive integer | Number of days of app, launcher, and mpv logs to retain (default: 7) |
| `files.app` | boolean | Write SubMiner app runtime logs (default: `true`) |
| `files.launcher` | boolean | Write launcher command logs (default: `true`) |
| `files.mpv` | boolean | Write mpv player logs. Enable temporarily for mpv/plugin debugging. |
| Option | Values | Description |
| ------- | ---------------------------------------- | --------------------------------------------------------- |
| `level` | `"debug"`, `"info"`, `"warn"`, `"error"` | Minimum log level for runtime logging (default: `"info"`) |
### Updates
@@ -204,7 +217,7 @@ Configure automatic update checks and update notifications:
| Option | Values | Description |
| -------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `updates.enabled` | `true`, `false` | Enable automatic background update checks. Manual tray and `subminer -u` checks are always allowed. |
| `enabled` | `true`, `false` | Enable automatic background update checks. Manual tray and `subminer -u` checks are always allowed. |
| `checkIntervalHours` | number | Minimum hours between automatic update checks. Default `24`. |
| `notificationType` | `"system"` \| `"osd"` \| `"both"` \| `"none"` | How SubMiner announces available updates. Default `"system"`. |
| `channel` | `"stable"` \| `"prerelease"` | Release channel used for update checks. Use `"prerelease"` to test beta/RC releases. |
@@ -215,15 +228,22 @@ Control whether the overlay automatically becomes visible when it connects to mp
```json
{
"auto_start_overlay": true
"auto_start_overlay": false
}
```
| Option | Values | Description |
| -------------------- | --------------- | ----------------------------------------------------- |
| `auto_start_overlay` | `true`, `false` | Auto-show overlay on mpv connection (default: `true`) |
| Option | Values | Description |
| -------------------- | --------------- | ------------------------------------------------------ |
| `auto_start_overlay` | `true`, `false` | Auto-show overlay on mpv connection (default: `false`) |
When you launch through the SubMiner app or the `subminer` wrapper, the launcher reads these settings from this config and injects them into the mpv plugin at runtime — there is no separate plugin config file to edit. `auto_start_overlay` controls whether the visible overlay shows on auto-start. Two related keys in the `mpv` block tune startup behavior: `mpv.autoStartSubMiner` starts the overlay automatically when a file loads, and `mpv.pauseUntilOverlayReady` pauses mpv on visible auto-start until SubMiner signals overlay/tokenization readiness.
The mpv plugin controls startup overlay visibility via `auto_start_visible_overlay` in `subminer.conf`.
For wrapper-driven playback, `subminer.conf` can also enable startup pause gating with
`auto_start_pause_until_ready` (requires `auto_start=yes` + `auto_start_visible_overlay=yes`).
Current plugin defaults in `subminer.conf` are:
- `auto_start=yes`
- `auto_start_visible_overlay=yes`
- `auto_start_pause_until_ready=yes`
On Windows, packaged plugin installs also rewrite the plugin socket path to `\\.\pipe\subminer-socket`.
@@ -238,7 +258,7 @@ Control which startup warmups run in the background versus deferring to first re
"mecab": true,
"yomitanExtension": true,
"subtitleDictionaries": true,
"jellyfinRemoteSession": false
"jellyfinRemoteSession": true
}
}
```
@@ -251,11 +271,11 @@ Control which startup warmups run in the background versus deferring to first re
| `subtitleDictionaries` | `true`, `false` | Warm up JLPT + frequency dictionaries at startup |
| `jellyfinRemoteSession` | `true`, `false` | Warm up Jellyfin remote session at startup (still requires Jellyfin remote auto-connect settings) |
Defaults warm local tokenizer/dictionary work (`true` for `mecab`, `yomitanExtension`, and `subtitleDictionaries`) with `lowPowerMode: false`; Jellyfin remote session warmup is opt-in (`false` by default). Setting a warmup toggle to `false` defers that work until first usage.
Defaults warm everything (`true` for all toggles, `lowPowerMode: false`). Setting a warmup toggle to `false` defers that work until first usage.
### WebSocket Server
The overlay includes a built-in WebSocket server that broadcasts plain subtitle text to connected clients for external processing.
The overlay includes a built-in WebSocket server that broadcasts subtitle text to connected clients (such as texthooker-ui) for external processing.
For endpoint details, payload examples, and client patterns, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
@@ -272,10 +292,10 @@ See `config.example.jsonc` for detailed configuration options.
}
```
| Option | Values | Description |
| ------------------- | ------------------------- | --------------------------------------------------- |
| `websocket.enabled` | `true`, `false`, `"auto"` | Built-in subtitle websocket mode (default: `false`) |
| `websocket.port` | number | WebSocket server port (default: 6677) |
| Option | Values | Description |
| --------- | ------------------------- | --------------------------------------------------- |
| `enabled` | `true`, `false`, `"auto"` | Built-in subtitle websocket mode (default: `false`) |
| `port` | number | WebSocket server port (default: 6677) |
### Annotation WebSocket
@@ -292,10 +312,10 @@ This stream includes subtitle text plus token metadata (N+1, known-word, frequen
}
```
| Option | Values | Description |
| ----------------------------- | --------------- | -------------------------------------------------------------- |
| `annotationWebsocket.enabled` | `true`, `false` | Toggle annotated websocket stream (independent of `websocket`) |
| `annotationWebsocket.port` | number | Annotation websocket port (default: 6678) |
| Option | Values | Description |
| --------- | --------------- | -------------------------------------------------------------- |
| `enabled` | `true`, `false` | Toggle annotated websocket stream (independent of `websocket`) |
| `port` | number | Annotation websocket port (default: 6678) |
### Texthooker
@@ -328,10 +348,10 @@ See `config.example.jsonc` for detailed configuration options.
```json
{
"subtitleStyle": {
"fontColor": "#cad3f5",
"backgroundColor": "transparent",
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"color": "#cad3f5",
"background-color": "transparent",
"font-size": "35px",
"font-weight": "600",
"line-height": "1.35",
@@ -341,15 +361,13 @@ See `config.example.jsonc` for detailed configuration options.
"text-rendering": "geometricPrecision",
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)",
"font-style": "normal",
"backdrop-filter": "blur(6px)",
"--subtitle-hover-token-color": "#f4dbd6",
"--subtitle-hover-token-background-color": "transparent"
"backdrop-filter": "blur(6px)"
},
"secondary": {
"fontColor": "#cad3f5",
"backgroundColor": "transparent",
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"color": "#cad3f5",
"background-color": "transparent",
"font-family": "Inter, Noto Sans, Helvetica Neue, sans-serif",
"font-size": "24px",
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)"
}
@@ -358,60 +376,59 @@ See `config.example.jsonc` for detailed configuration options.
}
```
| Option | Values | Description |
| ---------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `subtitleStyle.css` | object | CSS declaration object applied to primary subtitles after normal style defaults. Use CSS property names such as `font-size`. |
| `secondary.css` | object | CSS declaration object applied to secondary subtitles after normal secondary style defaults. |
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
| `preserveLineBreaks` | boolean | Preserve line breaks in visible overlay subtitle rendering (`false` by default). Enable to mirror mpv line layout. |
| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text, then resume on leave (`true` by default). |
| `autoPauseVideoOnYomitanPopup` | boolean | Pause playback while the Yomitan popup is open, then resume when the popup closes (`true` by default). |
| `primaryVisibleOnYomitanPopup` | boolean | Keep hover-mode primary subtitles visible while the Yomitan popup is open (`true` by default). |
| `nameMatchEnabled` | boolean | Enable character dictionary sync and subtitle token coloring for character-name matches (`false` by default) |
| `nameMatchImagesEnabled` | boolean | Show small cached AniList character portraits beside matched character-name tokens (`false` by default) |
| `nameMatchColor` | string | Hex color used for subtitle tokens matched from the SubMiner character dictionary (default: `#f5bde6`) |
| `knownWordColor` | string | Hex color used for known-word subtitle highlights (default: `#a6da95`) |
| `nPlusOneColor` | string | Hex color used for the single N+1 target subtitle highlight (default: `#c6a0f6`) |
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
| `frequencyDictionary.sourcePath` | string | Path to a local frequency dictionary root. Leave empty or omit to use installed/default frequency-dictionary search paths. |
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`1000` by default) |
| `frequencyDictionary.mode` | string | `"single"` or `"banded"` (`"single"` by default) |
| `frequencyDictionary.matchMode` | string | `"headword"` or `"surface"` (`"headword"` by default) |
| `frequencyDictionary.singleColor` | string | Color used for all highlighted tokens in single mode |
| `frequencyDictionary.bandedColors` | string[] | Array of five hex colors used for ranked bands in banded mode |
| `jlptColors` | object | JLPT level underline colors object (`N1`..`N5`) |
Subtitle CSS custom properties:
| CSS Property | Default | Description |
| ----------------------------------------- | ------------- | --------------------------------------- |
| `--subtitle-hover-token-color` | `#f4dbd6` | Hovered subtitle token text color |
| `--subtitle-hover-token-background-color` | `transparent` | Hovered subtitle token background color |
| Option | Values | Description |
| ---------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------- |
| `fontFamily` | string | CSS font-family value (default: `"Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP"`) |
| `fontSize` | number (px) | Font size in pixels (default: `35`) |
| `fontColor` | string | Any CSS color value (default: `"#cad3f5"`) |
| `css` | object | CSS declarations applied to subtitles after normal style defaults; the settings window writes textbox edits here |
| `fontWeight` | string | CSS font-weight, e.g. `"bold"`, `"normal"`, `"600"` (default: `"600"`) |
| `fontStyle` | string | `"normal"` or `"italic"` (default: `"normal"`) |
| `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"transparent"`) |
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
| `preserveLineBreaks` | boolean | Preserve line breaks in visible overlay subtitle rendering (`false` by default). Enable to mirror mpv line layout. |
| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text, then resume on leave (`true` by default). |
| `autoPauseVideoOnYomitanPopup` | boolean | Pause playback while the Yomitan popup is open, then resume when the popup closes (`true` by default). |
| `hoverTokenColor` | string | Hex color used for hovered subtitle token highlight in mpv (default: catppuccin mauve) |
| `hoverTokenBackgroundColor` | string | CSS color used for hovered subtitle token background highlight; `hoverBackground` is accepted as an alias |
| `nameMatchEnabled` | boolean | Enable subtitle token coloring for matches from the SubMiner character dictionary (`true` by default) |
| `nameMatchColor` | string | Hex color used for subtitle tokens matched from the SubMiner character dictionary (default: `#f5bde6`) |
| `knownWordColor` | string | Hex color used for known-word subtitle highlights (default: `#a6da95`) |
| `nPlusOneColor` | string | Hex color used for the single N+1 target subtitle highlight (default: `#c6a0f6`) |
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
| `frequencyDictionary.sourcePath` | string | Path to a local frequency dictionary root. Leave empty or omit to use installed/default frequency-dictionary search paths. |
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`1000` by default) |
| `frequencyDictionary.mode` | string | `"single"` or `"banded"` (`"single"` by default) |
| `frequencyDictionary.matchMode` | string | `"headword"` or `"surface"` (`"headword"` by default) |
| `frequencyDictionary.singleColor` | string | Color used for all highlighted tokens in single mode |
| `frequencyDictionary.bandedColors` | string[] | Array of five hex colors used for ranked bands in banded mode |
| `jlptColors` | object | JLPT level underline colors object (`N1`..`N5`) |
| `secondary` | object | Override any of the above for secondary subtitles (optional), including `secondary.css` declarations |
The Settings window keeps subtitle color controls separate, then saves CSS textboxes to
the primary subtitle, secondary subtitle, and sidebar CSS objects. The generated example
uses that same CSS declaration shape.
`subtitleStyle.css`, `subtitleStyle.secondary.css`, and `subtitleSidebar.css`. The generated example
uses that same CSS declaration shape; existing top-level style keys such as `fontSize` and
`textShadow` remain supported for hand-written or older configs.
Frequency dictionary highlighting uses the same dictionary file format as JLPT bundle lookups (`term_meta_bank_*.json` under discovered dictionary directories). A token is highlighted when it has a positive integer `frequencyRank` (lower is more common) and the rank is within `topX`.
Lookup behavior:
- Point the source path at a directory containing `term_meta_bank_*.json` for a fully custom source.
- Set `frequencyDictionary.sourcePath` to a directory containing `term_meta_bank_*.json` for a fully custom source.
- If `sourcePath` is missing or empty, SubMiner searches default install/runtime locations for `frequency-dictionary` directories (for example app resources, user data paths, and current working directory).
- In both cases, only terms with a valid `frequencyRank` are used; everything else falls back to no highlighting.
- Match mode controls which token text is used for frequency lookups: `headword` (dictionary form) or `surface` (visible subtitle text).
- `frequencyDictionary.matchMode` controls which token text is used for frequency lookups: `headword` (dictionary form) or `surface` (visible subtitle text).
- Frequency highlighting skips tokens that look like non-lexical SFX/interjection noise (for example kana reduplication or short kana endings like `っ`), even when dictionary ranks exist.
In `single` mode all highlights use `singleColor`; in `banded` mode tokens map to five ascending color bands from most common to least common inside the topX window.
Character-name highlighting is separate from N+1 and frequency highlighting:
- `nameMatchEnabled` controls whether SubMiner syncs the character dictionary and includes character-dictionary name matches in subtitle token metadata and renderer styling.
- `nameMatchImagesEnabled` adds small circular portraits beside matched names using the AniList images already cached with character dictionary snapshots.
- `nameMatchEnabled` controls whether SubMiner includes character-dictionary name matches in subtitle token metadata and renderer styling.
- `nameMatchColor` sets the highlight color for those matched character names.
- Matches come from the bundled SubMiner character dictionary, including AniList-synced merged dictionaries when name matching is enabled.
- Matches come from the bundled SubMiner character dictionary, including AniList-synced merged dictionaries when enabled.
Secondary subtitle styling lives in the secondary subtitle CSS object. Any CSS property not set there falls back to the secondary subtitle defaults, then the normal renderer defaults.
Secondary subtitle defaults: `fontFamily: "Inter, Noto Sans, Helvetica Neue, sans-serif"`, `fontSize: 24`, `fontColor: "#cad3f5"`, `textShadow: "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)"`, `backgroundColor: "transparent"`, `fontWeight: "600"`. Any property not set in `secondary` falls back to the CSS defaults.
**See `config.example.jsonc`** for the complete list of subtitle style configuration options.
@@ -426,38 +443,32 @@ Configure the parsed-subtitle sidebar modal.
"autoOpen": false,
"layout": "overlay",
"toggleKey": "Backslash",
"pauseVideoOnHover": true,
"pauseVideoOnHover": false,
"autoScroll": true,
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"font-size": "16px",
"color": "#cad3f5",
"background-color": "rgba(73, 77, 100, 0.9)",
"--subtitle-sidebar-max-width": "420px"
}
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif",
"fontSize": 16
}
}
```
| Option | Values | Description |
| --------------------------- | ------- | ------------------------------------------------------------------------------------------------------- |
| `subtitleSidebar.enabled` | boolean | Enable subtitle sidebar support (`true` by default) |
| `autoOpen` | boolean | Open sidebar automatically on overlay startup (`false` by default) |
| `layout` | string | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space to mimic browser-like layout |
| `subtitleSidebar.toggleKey` | string | `KeyboardEvent.code` used to open/close the sidebar (default: `"Backslash"`) |
| `pauseVideoOnHover` | boolean | Pause playback while hovering the sidebar cue list (`true` by default) |
| `autoScroll` | boolean | Keep the active cue in view while playback advances |
| `subtitleSidebar.css` | object | CSS declaration object applied to the sidebar. Use CSS properties plus sidebar custom properties below. |
Sidebar CSS custom properties:
| CSS Property | Default | Description |
| -------------------------------------------- | --------------------------- | ---------------------------- |
| `--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 |
| Option | Values | Description |
| --------------------------- | --------- | ------------------------------------------------------------------------------------------------------- |
| `enabled` | boolean | Enable subtitle sidebar support (`true` by default) |
| `autoOpen` | boolean | Open sidebar automatically on overlay startup (`false` by default) |
| `layout` | string | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space to mimic browser-like layout |
| `toggleKey` | string | `KeyboardEvent.code` used to open/close the sidebar (default: `"Backslash"`) |
| `pauseVideoOnHover` | boolean | Pause playback while hovering the sidebar cue list |
| `autoScroll` | boolean | Keep the active cue in view while playback advances |
| `maxWidth` | number | Maximum sidebar width in CSS pixels (default: `420`) |
| `opacity` | number | Sidebar opacity between `0` and `1` (default: `0.95`) |
| `backgroundColor` | string | Sidebar shell background color |
| `textColor` | hex color | Default cue text color |
| `fontFamily` | string | CSS `font-family` value applied to sidebar cue text |
| `fontSize` | number | Base sidebar cue font size in CSS pixels (default: `16`) |
| `timestampColor` | hex color | Cue timestamp color |
| `activeLineColor` | hex color | Active cue text color |
| `activeLineBackgroundColor` | string | Active cue background color |
| `hoverLineBackgroundColor` | string | Hovered cue background color |
The sidebar is only available when the active subtitle source has been parsed into a cue list. Default colors use Catppuccin Macchiato with a semi-transparent shell so the panel stays readable without feeling like an opaque settings dialog.
@@ -472,7 +483,7 @@ For full details on layout modes, behavior, and the keyboard shortcut, see the [
| `N1` | `#ed8796` | JLPT N1 underline color |
| `N2` | `#f5a97f` | JLPT N2 underline color |
| `N3` | `#f9e2af` | JLPT N3 underline color |
| `N4` | `#8bd5ca` | JLPT N4 underline color |
| `N4` | `#a6e3a1` | JLPT N4 underline color |
| `N5` | `#8aadf4` | JLPT N5 underline color |
**Image Quality Notes:**
@@ -521,7 +532,7 @@ See `config.example.jsonc` for detailed configuration options.
| `autoLoadSecondarySub` | `true`, `false` | Auto-detect and load matching secondary subtitle track |
| `defaultMode` | `"hidden"`, `"visible"`, `"hover"` | Initial display mode (default: `"hover"`) |
The secondary-subtitle language list also acts as the fallback secondary-language priority for managed startup subtitle selection on local playback and YouTube playback.
`secondarySub.secondarySubLanguages` also acts as the fallback secondary-language priority for managed startup subtitle selection on local playback and YouTube playback.
**Display modes:**
@@ -610,7 +621,7 @@ See `config.example.jsonc` for detailed configuration options.
"mineSentence": "CommandOrControl+S",
"mineSentenceMultiple": "CommandOrControl+Shift+S",
"markAudioCard": "CommandOrControl+Shift+A",
"openCharacterDictionaryManager": "CommandOrControl+D",
"openCharacterDictionary": "CommandOrControl+Alt+A",
"openRuntimeOptions": "CommandOrControl+Shift+O",
"openSessionHelp": "CommandOrControl+Slash",
"openControllerSelect": "Alt+C",
@@ -622,26 +633,26 @@ See `config.example.jsonc` for detailed configuration options.
}
```
| Option | Values | Description |
| -------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `toggleVisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling visible subtitle overlay (default: `"Alt+Shift+O"`) |
| `copySubtitle` | string \| `null` | Accelerator for copying current subtitle (default: `"CommandOrControl+C"`) |
| `copySubtitleMultiple` | string \| `null` | Accelerator for multi-copy mode (default: `"CommandOrControl+Shift+C"`) |
| `updateLastCardFromClipboard` | string \| `null` | Accelerator for updating card from clipboard (default: `"CommandOrControl+V"`) |
| `triggerFieldGrouping` | string \| `null` | Accelerator for Kiku field grouping on last card (default: `"CommandOrControl+G"`; only active when automatic card updates are disabled) |
| `triggerSubsync` | string \| `null` | Accelerator for running Subsync (default: `"Ctrl+Alt+S"`) |
| `mineSentence` | string \| `null` | Accelerator for creating sentence card from current subtitle (default: `"CommandOrControl+S"`) |
| `mineSentenceMultiple` | string \| `null` | Accelerator for multi-mine sentence card mode (default: `"CommandOrControl+Shift+S"`) |
| `multiCopyTimeoutMs` | number | Timeout in ms for multi-copy/mine digit input (default: `3000`) |
| `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) |
| `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) |
| `openCharacterDictionaryManager` | string \| `null` | Opens the loaded character dictionary manager (default: `"CommandOrControl+D"`) |
| `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) |
| `openSessionHelp` | string \| `null` | Opens the in-overlay session help modal (default: `"CommandOrControl+Slash"`) |
| `openControllerSelect` | string \| `null` | Opens the controller config/remap modal (default: `"Alt+C"`) |
| `openControllerDebug` | string \| `null` | Opens the controller debug modal (default: `"Alt+Shift+C"`) |
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
| `toggleSubtitleSidebar` | string \| `null` | Dispatches the subtitle sidebar toggle action (default: `"Backslash"`). `subtitleSidebar.toggleKey` remains the primary bare-key setting. |
| Option | Values | Description |
| ----------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `toggleVisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling visible subtitle overlay (default: `"Alt+Shift+O"`) |
| `copySubtitle` | string \| `null` | Accelerator for copying current subtitle (default: `"CommandOrControl+C"`) |
| `copySubtitleMultiple` | string \| `null` | Accelerator for multi-copy mode (default: `"CommandOrControl+Shift+C"`) |
| `updateLastCardFromClipboard` | string \| `null` | Accelerator for updating card from clipboard (default: `"CommandOrControl+V"`) |
| `triggerFieldGrouping` | string \| `null` | Accelerator for Kiku field grouping on last card (default: `"CommandOrControl+G"`; only active when `behavior.autoUpdateNewCards` is `false`) |
| `triggerSubsync` | string \| `null` | Accelerator for running Subsync (default: `"Ctrl+Alt+S"`) |
| `mineSentence` | string \| `null` | Accelerator for creating sentence card from current subtitle (default: `"CommandOrControl+S"`) |
| `mineSentenceMultiple` | string \| `null` | Accelerator for multi-mine sentence card mode (default: `"CommandOrControl+Shift+S"`) |
| `multiCopyTimeoutMs` | number | Timeout in ms for multi-copy/mine digit input (default: `3000`) |
| `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) |
| `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) |
| `openCharacterDictionary` | string \| `null` | Opens the character dictionary AniList selector (default: `"CommandOrControl+Alt+A"`) |
| `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) |
| `openSessionHelp` | string \| `null` | Opens the in-overlay session help modal (default: `"CommandOrControl+Slash"`) |
| `openControllerSelect` | string \| `null` | Opens the controller config/remap modal (default: `"Alt+C"`) |
| `openControllerDebug` | string \| `null` | Opens the controller debug modal (default: `"Alt+Shift+C"`) |
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
| `toggleSubtitleSidebar` | string \| `null` | Dispatches the subtitle sidebar toggle action (default: `"Backslash"`). `subtitleSidebar.toggleKey` remains the primary bare-key setting. |
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
@@ -666,7 +677,7 @@ Important behavior:
- Learned bindings are saved under `controller.profiles` for the selected controller id. Global `controller.bindings` remains the fallback for controllers without a profile.
- `Alt+Shift+C` opens the debug modal by default, and you can remap that shortcut through `shortcuts.openControllerDebug`.
- The debug modal shows raw axes/button values plus a ready-to-copy `buttonIndices` config block.
- The button-index map is a semantic reference mapping. Changing it does not rewrite the raw numeric descriptor values already stored under controller bindings.
- `controller.buttonIndices` is a semantic reference/legacy mapping. Changing it does not rewrite the raw numeric descriptor values already stored under `controller.bindings`.
- Turning keyboard-only mode off clears the keyboard-only token highlight state.
- Closing the Yomitan popup clears the temporary native text-selection fill, but keeps controller token selection active.
@@ -755,11 +766,11 @@ If you bind a discrete action to an axis manually, include `direction`:
}
```
Treat the button-index map as reference-only unless you are copying values from the debug modal. Updating it alone does not rewrite the hardcoded raw numeric values already present in controller bindings or controller profiles. If you need a real remap, prefer the `Alt+C` learn flow so both the source and the descriptor shape stay correct.
Treat `controller.buttonIndices` as reference-only unless you are still using legacy semantic bindings or copying values from the debug modal. Updating `controller.buttonIndices` alone does not rewrite the hardcoded raw numeric values already present in `controller.bindings` or `controller.profiles.*.bindings`. If you need a real remap, prefer the `Alt+C` learn flow so both the source and the descriptor shape stay correct.
If you choose to bind `L2` or `R2` manually, set `triggerInputMode` to `analog` and tune `triggerDeadzone` when your controller reports triggers as analog values instead of digital pressed/not-pressed buttons. `auto` accepts either style and remains the default.
If one controller reports non-standard raw button numbers, override that controller profile's button-index map using values from the `Alt+Shift+C` debug modal. Use the global button-index map only when the mapping should apply to every controller without a profile.
If one controller reports non-standard raw button numbers, override `controller.profiles["<controller id>"].buttonIndices` using values from the `Alt+Shift+C` debug modal. Use global `controller.buttonIndices` only when the mapping should apply to every controller without a profile.
If you update this controller documentation or the generated controller examples, run `bun run docs:test` and `bun run docs:build` before merging.
@@ -767,21 +778,21 @@ Tune `scrollPixelsPerSecond`, `horizontalJumpPixels`, deadzones, repeat timing,
### Manual Card Update Shortcuts
When automatic card updates are disabled, new cards are detected but not automatically updated. Use these keyboard shortcuts for manual control:
When `behavior.autoUpdateNewCards` is set to `false`, new cards are detected but not automatically updated. Use these keyboard shortcuts for manual control:
| Shortcut | Action |
| -------------- | ------------------------------------------------------------------------------------------------------------- |
| `Ctrl+C` | Copy the current subtitle line to clipboard (preserves line breaks) |
| `Ctrl+Shift+C` | Enter multi-copy mode. Press `1-9` to copy that many recent lines, or `Esc` to cancel. Timeout: 3 seconds |
| `Ctrl+V` | Update the last added Anki card using subtitles from clipboard |
| `Ctrl+G` | Trigger Kiku duplicate field grouping for the last added card (only when automatic card updates are disabled) |
| `Ctrl+S` | Create a sentence card from the current subtitle line |
| `Ctrl+Shift+S` | Enter multi-mine mode. Press `1-9` to create a sentence card from that many recent lines, or `Esc` to cancel |
| `Ctrl+Shift+V` | Cycle secondary subtitle display mode (hidden → visible → hover) |
| `Ctrl+Shift+A` | Mark the last added Anki card as an audio card (sets IsAudioCard, SentenceAudio, Sentence, Picture) |
| `Ctrl+D` | Open loaded character dictionary manager |
| `Ctrl+Shift+O` | Open runtime options palette (session-only live toggles) |
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist (fixed, not currently configurable) |
| Shortcut | Action |
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `Ctrl+C` | Copy the current subtitle line to clipboard (preserves line breaks) |
| `Ctrl+Shift+C` | Enter multi-copy mode. Press `1-9` to copy that many recent lines, or `Esc` to cancel. Timeout: 3 seconds |
| `Ctrl+V` | Update the last added Anki card using subtitles from clipboard |
| `Ctrl+G` | Trigger Kiku duplicate field grouping for the last added card (only when `behavior.autoUpdateNewCards` is `false`) |
| `Ctrl+S` | Create a sentence card from the current subtitle line |
| `Ctrl+Shift+S` | Enter multi-mine mode. Press `1-9` to create a sentence card from that many recent lines, or `Esc` to cancel |
| `Ctrl+Shift+V` | Cycle secondary subtitle display mode (hidden → visible → hover) |
| `Ctrl+Shift+A` | Mark the last added Anki card as an audio card (sets IsAudioCard, SentenceAudio, Sentence, Picture) |
| `Ctrl+Alt+A` | Open character dictionary AniList selector |
| `Ctrl+Shift+O` | Open runtime options palette (session-only live toggles) |
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist (fixed, not currently configurable) |
**Multi-line copy workflow:**
@@ -820,11 +831,16 @@ When config hot-reload updates shortcut/keybinding/style values, close and reope
Use the runtime options palette to toggle settings live while SubMiner is running. These changes are session-only and reset on restart.
Current runtime options cover automatic card updates, known-word highlighting,
JLPT underlines, frequency highlighting, known-word match mode, and Kiku field
grouping mode.
Current runtime options:
Annotation toggles only apply to new subtitle lines after the toggle. The currently displayed line is not re-tokenized in place.
- `ankiConnect.behavior.autoUpdateNewCards` (`On` / `Off`)
- `ankiConnect.knownWords.highlightEnabled` (`On` / `Off`)
- `subtitleStyle.enableJlpt` (`On` / `Off`)
- `subtitleStyle.frequencyDictionary.enabled` (`On` / `Off`)
- `ankiConnect.knownWords.matchMode` (`headword` / `surface`)
- `ankiConnect.isKiku.fieldGrouping` (`auto` / `manual` / `disabled`)
Annotation toggles (`nPlusOne`, `enableJlpt`, `frequencyDictionary.enabled`) only apply to new subtitle lines after the toggle. The currently displayed line is not re-tokenized in place.
Default shortcut: `Ctrl+Shift+O`
@@ -839,7 +855,8 @@ Palette controls:
### Shared AI Provider
This is the single, shared connection to an OpenAI-compatible LLM endpoint. Configure it **once** here at the top level, and SubMiner reuses it wherever AI is needed (today: Anki translation/enrichment). Per-feature toggles and prompt/model tweaks live in their own sections (for example `ankiConnect.ai`) and inherit this transport.
Shared OpenAI-compatible transport settings live at the top level under `ai`.
Anki reads this provider directly. Legacy subtitle fallback keeps the same provider shape for compatibility, then applies feature-local overrides where supported.
```json
{
@@ -847,26 +864,25 @@ This is the single, shared connection to an OpenAI-compatible LLM endpoint. Conf
"enabled": false,
"apiKey": "",
"apiKeyCommand": "",
"model": "openai/gpt-4o-mini",
"baseUrl": "https://openrouter.ai/api",
"requestTimeoutMs": 15000
}
}
```
| Option | Values | Description |
| ------------------ | -------------------- | ------------------------------------------------------------------------------------ |
| `ai.enabled` | `true`, `false` | Enable shared AI provider features (default: `false`) |
| `apiKey` | string | Static API key for the shared provider |
| `apiKeyCommand` | string | Shell command used to resolve the API key (preferred over a plaintext `apiKey`) |
| `model` | string | Default model identifier requested from the provider (default: `openai/gpt-4o-mini`) |
| `baseUrl` | string (URL) | OpenAI-compatible base URL (default: `https://openrouter.ai/api`) |
| `systemPrompt` | string | Default system prompt sent with requests (default: a translation-engine prompt) |
| `requestTimeoutMs` | integer milliseconds | Shared request timeout (default: `15000`) |
| Option | Values | Description |
| ------------------ | -------------------- | ------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable shared AI provider features |
| `apiKey` | string | Static API key for the shared provider |
| `apiKeyCommand` | string | Shell command used to resolve the API key |
| `baseUrl` | string (URL) | OpenAI-compatible base URL |
| `model` | string | Optional model override for shared provider workflows |
| `systemPrompt` | string | Optional system prompt override for shared provider workflows |
| `requestTimeoutMs` | integer milliseconds | Shared request timeout (default: `15000`) |
SubMiner uses the shared provider for:
- Anki translation/enrichment when Anki AI is enabled
- Anki translation/enrichment when `ankiConnect.ai.enabled` is `true`
### AnkiConnect
@@ -879,7 +895,7 @@ Enable automatic Anki card creation and updates with media generation:
"url": "http://127.0.0.1:8765",
"pollingRate": 3000,
"proxy": {
"enabled": true,
"enabled": false,
"host": "127.0.0.1",
"port": 8766,
"upstreamUrl": "http://127.0.0.1:8765"
@@ -911,7 +927,7 @@ Enable automatic Anki card creation and updates with media generation:
"animatedMaxWidth": 640,
"animatedMaxHeight": 360,
"animatedCrf": 35,
"audioPadding": 0,
"audioPadding": 0.5,
"fallbackDuration": 3,
"maxMediaDuration": 30
},
@@ -940,57 +956,58 @@ This example is intentionally compact. The option table below documents availabl
**Requirements:** [AnkiConnect](https://github.com/FooSoft/anki-connect) plugin must be installed and running in Anki. ffmpeg must be installed for media generation.
| Option | Values | Description |
| ------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ankiConnect.enabled` | `true`, `false` | Enable AnkiConnect integration (default: `true`) |
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
| `pollingRate` | number (ms) | How often to check for new cards in polling mode (default: `3000`; ignored for direct proxy `addNote`/`addNotes` updates) |
| `proxy.enabled` | `true`, `false` | Enable local AnkiConnect-compatible proxy for push-based auto-enrichment (default: `true`) |
| `proxy.host` | string | Bind host for local AnkiConnect proxy (default: `127.0.0.1`) |
| `proxy.port` | number | Bind port for local AnkiConnect proxy (default: `8766`) |
| `proxy.upstreamUrl` | string (URL) | Upstream AnkiConnect URL that proxy forwards to (default: `http://127.0.0.1:8765`) |
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
| `ankiConnect.deck` | string | Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to search all decks. |
| `fields.word` | string | Card field for mined word / expression text (default: `Expression`) |
| `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) |
| `fields.image` | string | Card field for images (default: `Picture`) |
| `fields.sentence` | string | Card field for sentences (default: `Sentence`) |
| `fields.miscInfo` | string | Card field for metadata (default: `"MiscInfo"`, set to `null` to disable) |
| `fields.translation` | string | Card field for sentence-card translation/back text (default: `SelectionText`) |
| `ankiConnect.ai.enabled` | `true`, `false` | Use AI translation for sentence cards. Also auto-attempted when secondary subtitle is missing. |
| `ankiConnect.ai.model` | string | Optional model override for Anki AI translation/enrichment flows. |
| `ankiConnect.ai.systemPrompt` | string | Optional system prompt override for Anki AI translation/enrichment flows. |
| `media.generateAudio` | `true`, `false` | Generate audio clips from video (default: `true`) |
| `media.generateImage` | `true`, `false` | Generate image/animation screenshots (default: `true`) |
| `media.imageType` | `"static"`, `"avif"` | Image type: static screenshot or animated AVIF (default: `"static"`) |
| `media.imageFormat` | `"jpg"`, `"png"`, `"webp"` | Image format (default: `"jpg"`) |
| `media.imageQuality` | number (1-100) | Image quality for JPG/WebP; PNG ignores this (default: `92`) |
| `media.imageMaxWidth` | number (px) | Optional max width for static screenshots. Unset keeps source width. |
| `media.imageMaxHeight` | number (px) | Optional max height for static screenshots. Unset keeps source height. |
| `media.animatedFps` | number (1-60) | FPS for animated AVIF (default: `10`) |
| `media.animatedMaxWidth` | number (px) | Max width for animated AVIF (default: `640`) |
| `media.animatedMaxHeight` | number (px) | Optional max height for animated AVIF. Unset keeps source aspect-constrained height. |
| `media.animatedCrf` | number (0-63) | CRF quality for AVIF; lower = higher quality (default: `35`) |
| `media.syncAnimatedImageToWordAudio` | `true`, `false` | Whether animated AVIF includes an opening frame synced to sentence word-audio timing (default: `true`). |
| `media.audioPadding` | number (seconds) | Optional padding around audio clip timing (default: `0`). Animated AVIF clips freeze the first frame during leading audio padding. |
| `media.fallbackDuration` | number (seconds) | Default duration if timing unavailable (default: `3.0`) |
| `media.maxMediaDuration` | number (seconds) | Max duration for generated media from multi-line copy (default: `30`, `0` to disable) |
| `behavior.overwriteAudio` | `true`, `false` | Replace existing audio on updates; when `false`, new audio is appended/prepended using the configured media insert mode; manual clipboard updates always replace generated sentence audio (default: `true`) |
| `behavior.overwriteImage` | `true`, `false` | Replace existing images on updates; when `false`, new images are appended/prepended using the configured media insert mode (default: `true`) |
| `behavior.mediaInsertMode` | `"append"`, `"prepend"` | Where to insert new media when overwrite is off (default: `"append"`) |
| `behavior.highlightWord` | `true`, `false` | Highlight the word in sentence context (default: `true`) |
| `ankiConnect.knownWords.highlightEnabled` | `true`, `false` | Enable fast local highlighting for words already known in Anki (default: `false`) |
| `ankiConnect.knownWords.addMinedWordsImmediately` | `true`, `false` | Add words from successful mines into the local known-word cache immediately (default: `true`) |
| `ankiConnect.knownWords.matchMode` | `"headword"`, `"surface"` | Matching strategy for known-word highlighting (default: `"headword"`). `headword` uses token headwords; `surface` uses visible subtitle text. |
| `ankiConnect.knownWords.refreshMinutes` | number | Minutes between known-word cache refreshes (default: `1440`) |
| `ankiConnect.knownWords.decks` | object | Deck→fields mapping used for known-word cache query scope (e.g. `{ "Kaishi 1.5k": ["Word"] }`). |
| `ankiConnect.nPlusOne.enabled` | `true`, `false` | Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Independent from `knownWords.highlightEnabled`. Requires known-word cache data (default: `false`). |
| `ankiConnect.nPlusOne.minSentenceWords` | number | Minimum number of words required in a sentence before single unknown-word N+1 highlighting can trigger (default: `3`). |
| `behavior.notificationType` | `"osd"`, `"system"`, `"both"`, `"none"` | Notification type on card update (default: `"osd"`) |
| `behavior.autoUpdateNewCards` | `true`, `false` | Automatically update cards on creation (default: `true`) |
| `metadata.pattern` | string | Format pattern for metadata: `%f`=filename, `%F`=filename+ext, `%t`=time |
| `isLapis` | object | Lapis/shared sentence-card config: `{ enabled, sentenceCardModel }`. Sentence/audio field names are fixed to `Sentence` and `SentenceAudio`. |
| `isKiku` | object | Kiku-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }` (shared sentence/audio/model settings are inherited from `isLapis`) |
| Option | Values | Description |
| ------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `true`) |
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
| `pollingRate` | number (ms) | How often to check for new cards in polling mode (default: `3000`; ignored for direct proxy `addNote`/`addNotes` updates) |
| `proxy.enabled` | `true`, `false` | Enable local AnkiConnect-compatible proxy for push-based auto-enrichment (default: `true`) |
| `proxy.host` | string | Bind host for local AnkiConnect proxy (default: `127.0.0.1`) |
| `proxy.port` | number | Bind port for local AnkiConnect proxy (default: `8766`) |
| `proxy.upstreamUrl` | string (URL) | Upstream AnkiConnect URL that proxy forwards to (default: `http://127.0.0.1:8765`) |
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
| `ankiConnect.deck` | string | Legacy Anki polling/compatibility scope. Newer known-word cache scoping should use `ankiConnect.knownWords.decks`. |
| `ankiConnect.knownWords.decks` | object | Deck→fields mapping for known-word cache queries (for example `{ "Kaishi 1.5k": ["Word", "Word Reading"] }`). |
| `fields.word` | string | Card field for mined word / expression text (default: `Expression`) |
| `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) |
| `fields.image` | string | Card field for images (default: `Picture`) |
| `fields.sentence` | string | Card field for sentences (default: `Sentence`) |
| `fields.miscInfo` | string | Card field for metadata (default: `"MiscInfo"`, set to `null` to disable) |
| `fields.translation` | string | Card field for sentence-card translation/back text (default: `SelectionText`) |
| `ankiConnect.ai.enabled` | `true`, `false` | Use AI translation for sentence cards. Also auto-attempted when secondary subtitle is missing. |
| `ankiConnect.ai.model` | string | Optional model override for Anki AI translation/enrichment flows. |
| `ankiConnect.ai.systemPrompt` | string | Optional system prompt override for Anki AI translation/enrichment flows. |
| `media.generateAudio` | `true`, `false` | Generate audio clips from video (default: `true`) |
| `media.generateImage` | `true`, `false` | Generate image/animation screenshots (default: `true`) |
| `media.imageType` | `"static"`, `"avif"` | Image type: static screenshot or animated AVIF (default: `"static"`) |
| `media.imageFormat` | `"jpg"`, `"png"`, `"webp"` | Image format (default: `"jpg"`) |
| `media.imageQuality` | number (1-100) | Image quality for JPG/WebP; PNG ignores this (default: `92`) |
| `media.imageMaxWidth` | number (px) | Optional max width for static screenshots. Unset keeps source width. |
| `media.imageMaxHeight` | number (px) | Optional max height for static screenshots. Unset keeps source height. |
| `media.animatedFps` | number (1-60) | FPS for animated AVIF (default: `10`) |
| `media.animatedMaxWidth` | number (px) | Max width for animated AVIF (default: `640`) |
| `media.animatedMaxHeight` | number (px) | Optional max height for animated AVIF. Unset keeps source aspect-constrained height. |
| `media.animatedCrf` | number (0-63) | CRF quality for AVIF; lower = higher quality (default: `35`) |
| `media.syncAnimatedImageToWordAudio` | `true`, `false` | Whether animated AVIF includes an opening frame synced to sentence word-audio timing (default: `true`). |
| `media.audioPadding` | number (seconds) | Padding around audio clip timing (default: `0.5`) |
| `media.fallbackDuration` | number (seconds) | Default duration if timing unavailable (default: `3.0`) |
| `media.maxMediaDuration` | number (seconds) | Max duration for generated media from multi-line copy (default: `30`, `0` to disable) |
| `behavior.overwriteAudio` | `true`, `false` | Replace existing audio on updates; when `false`, new audio is appended/prepended per `behavior.mediaInsertMode`; manual clipboard updates always replace generated sentence audio (default: `true`) |
| `behavior.overwriteImage` | `true`, `false` | Replace existing images on updates; when `false`, new images are appended/prepended per `behavior.mediaInsertMode` (default: `true`) |
| `behavior.mediaInsertMode` | `"append"`, `"prepend"` | Where to insert new media when overwrite is off (default: `"append"`) |
| `behavior.highlightWord` | `true`, `false` | Highlight the word in sentence context (default: `true`) |
| `ankiConnect.knownWords.highlightEnabled` | `true`, `false` | Enable fast local highlighting for words already known in Anki (default: `false`) |
| `ankiConnect.knownWords.addMinedWordsImmediately` | `true`, `false` | Add words from successful mines into the local known-word cache immediately (default: `true`) |
| `ankiConnect.knownWords.matchMode` | `"headword"`, `"surface"` | Matching strategy for known-word highlighting (default: `"headword"`). `headword` uses token headwords; `surface` uses visible subtitle text. |
| `ankiConnect.knownWords.refreshMinutes` | number | Minutes between known-word cache refreshes (default: `1440`) |
| `ankiConnect.knownWords.decks` | object | Deck→fields mapping used for known-word cache query scope (e.g. `{ "Kaishi 1.5k": ["Word", "Word Reading"] }`). |
| `ankiConnect.nPlusOne.enabled` | `true`, `false` | Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Independent from `knownWords.highlightEnabled`. Requires known-word cache data (default: `false`). |
| `ankiConnect.nPlusOne.minSentenceWords` | number | Minimum number of words required in a sentence before single unknown-word N+1 highlighting can trigger (default: `3`). |
| `behavior.notificationType` | `"osd"`, `"system"`, `"both"`, `"none"` | Notification type on card update (default: `"osd"`) |
| `behavior.autoUpdateNewCards` | `true`, `false` | Automatically update cards on creation (default: `true`) |
| `metadata.pattern` | string | Format pattern for metadata: `%f`=filename, `%F`=filename+ext, `%t`=time |
| `isLapis` | object | Lapis/shared sentence-card config: `{ enabled, sentenceCardModel }`. Sentence/audio field names are fixed to `Sentence` and `SentenceAudio`. |
| `isKiku` | object | Kiku-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }` (shared sentence/audio/model settings are inherited from `isLapis`) |
`ankiConnect.ai` only controls feature-local enablement plus optional `model` / `systemPrompt` overrides.
API key resolution, base URL, and timeout live under the shared top-level [`ai`](#shared-ai-provider) config.
@@ -1020,21 +1037,21 @@ SubMiner is intentionally built for [Kiku](https://kiku.youyoumu.my.id/) and [La
### N+1 Word Highlighting
When known-word highlighting is enabled, SubMiner builds a local cache of known words from Anki to highlight already learned tokens in subtitle rendering.
When `ankiConnect.knownWords.highlightEnabled` is enabled, SubMiner builds a local cache of known words from Anki to highlight already learned tokens in subtitle rendering.
Known-word cache policy:
- Initial sync runs when the integration starts if the cache is missing or stale.
- The refresh interval controls the minimum time between syncs; between refreshes, cached words are reused without querying Anki.
- `ankiConnect.knownWords.refreshMinutes` controls the minimum time between refreshes; between refreshes, cached words are reused without querying Anki.
- `subtitleStyle.nPlusOneColor` sets the color for the single target token when exactly one eligible unknown word exists.
- The N+1 minimum sentence-word setting controls the token count required before N+1 highlighting can trigger.
- `ankiConnect.nPlusOne.minSentenceWords` sets the minimum token count required in a sentence for N+1 highlighting (default: `3`).
- `subtitleStyle.knownWordColor` sets the known-word highlight color for tokens already in Anki.
- The known-word deck map accepts an object keyed by deck name.
- Prefer expression/word fields such as `Expression` or `Word`. Avoid reading-only fields unless you intentionally want homophone readings to count as known words.
- `ankiConnect.knownWords.decks` accepts an object keyed by deck name. If omitted or empty, it falls back to the legacy `ankiConnect.deck` single-deck scope.
- Cache state is persisted to `known-words-cache.json` under the app `userData` directory.
- The cache is automatically invalidated when the configured scope changes (for example, when deck changes).
- Cache lookups are in-memory. By default, token headwords are matched against cached `Expression` / `Word` values; set known-word matching to `"surface"` for raw subtitle text matching.
- A known-word cache match always receives known-word highlighting, even when part-of-speech filters suppress N+1, frequency, or JLPT annotations for that token.
- Cache lookups are in-memory. By default, token headwords are matched against cached `Expression` / `Word` values; set `ankiConnect.knownWords.matchMode` to `"surface"` for raw subtitle text matching.
- Legacy moved keys under `ankiConnect.nPlusOne` (`highlightEnabled`, `refreshMinutes`, `matchMode`, `decks`, `knownWord`) and older `ankiConnect.behavior.nPlusOne*` keys are deprecated and only kept for backward compatibility.
- Legacy top-level `ankiConnect` migration keys (for example `audioField`, `generateAudio`, `imageType`) are compatibility-only, validated before mapping, and ignored with a warning when invalid.
- If AnkiConnect is unreachable, the cache remains in its previous state and an on-screen/system status message is shown.
- Known-word sync activity is logged at `INFO`/`DEBUG` level with the `anki` logger scope and includes scope, notes returned, and word counts.
@@ -1094,16 +1111,17 @@ Jimaku is rate limited; if you hit a limit, SubMiner will surface the retry dela
Set `openBrowser` to `false` to only print the URL without opening a browser.
### Subtitle Sync
### Auto Subtitle Sync
Sync the active subtitle track from the overlay picker using `alass` or `ffsubsync`. Both are **optional external tools** that must be installed separately and available on your `PATH` (or configured via the path options below).
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
- [`ffsubsync`](https://github.com/smacke/ffsubsync) — audio-based sync using the video file as reference (fallback)
```json
{
"subsync": {
"defaultMode": "auto",
"alass_path": "",
"ffsubsync_path": "",
"ffmpeg_path": "",
@@ -1112,12 +1130,13 @@ Sync the active subtitle track from the overlay picker using `alass` or `ffsubsy
}
```
| Option | Values | Description |
| ---------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `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>`. |
| 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` 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>`. |
Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
Customize it there, or set it to `null` to disable.
@@ -1133,7 +1152,9 @@ AniList integration is opt-in and disabled by default. Enable it to allow SubMin
"accessToken": "",
"characterDictionary": {
"enabled": false,
"refreshTtlHours": 168,
"maxLoaded": 3,
"evictionPolicy": "delete",
"profileScope": "all",
"collapsibleSections": {
"description": false,
@@ -1145,15 +1166,18 @@ AniList integration is opt-in and disabled by default. Enable it to allow SubMin
}
```
| Option | Values | Description |
| -------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |
| `anilist.enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
| `accessToken` | string | Optional explicit AniList access token override (default: empty string) |
| `characterDictionary.maxLoaded` | number | Maximum number of most-recently-used AniList media snapshots included in the merged dictionary (default: `3`) |
| `characterDictionary.collapsibleSections.description` | `true`, `false` | Open the Description section by default in generated dictionary entries |
| `characterDictionary.collapsibleSections.characterInformation` | `true`, `false` | Open the Character Information section by default in generated dictionary entries |
| `characterDictionary.collapsibleSections.voicedBy` | `true`, `false` | Open the Voiced by section by default in generated dictionary entries |
| `characterDictionary.profileScope` | `"all"`, `"active"` | Apply dictionary settings updates to all Yomitan profiles or only active profile |
| Option | Values | Description |
| -------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
| `accessToken` | string | Optional explicit AniList access token override (default: empty string) |
| `characterDictionary.enabled` | `true`, `false` | Enable automatic import/update of the merged SubMiner character dictionary for recent AniList media |
| `characterDictionary.refreshTtlHours` | number | Legacy compatibility setting. Parsed and preserved, but merged dictionary retention is now usage-based |
| `characterDictionary.maxLoaded` | number | Maximum number of most-recently-used AniList media snapshots included in the merged dictionary (default: `3`) |
| `characterDictionary.evictionPolicy` | `"delete"`, `"disable"` | Legacy compatibility setting. Parsed and preserved, but merged dictionary eviction is now usage-based |
| `characterDictionary.collapsibleSections.description` | `true`, `false` | Open the Description section by default in generated dictionary entries |
| `characterDictionary.collapsibleSections.characterInformation` | `true`, `false` | Open the Character Information section by default in generated dictionary entries |
| `characterDictionary.collapsibleSections.voicedBy` | `true`, `false` | Open the Voiced by section by default in generated dictionary entries |
| `characterDictionary.profileScope` | `"all"`, `"active"` | Apply dictionary settings updates to all Yomitan profiles or only active profile |
When `enabled` is `true` and `accessToken` is empty, SubMiner opens an AniList setup helper window. Keep `enabled` as `false` to disable all AniList setup/update behavior.
@@ -1176,7 +1200,7 @@ Current post-watch behavior:
Setup flow details:
1. Set `anilist.enabled` to `true`.
2. Leave the AniList access-token field empty and restart SubMiner (or run `--anilist-setup`) to trigger setup.
2. Leave `anilist.accessToken` empty and restart SubMiner (or run `--anilist-setup`) to trigger setup.
3. Approve access in AniList.
4. Callback flow returns to SubMiner via `subminer://anilist-setup?...`, and SubMiner stores the token automatically.
- Encryption backend: Linux defaults to `gnome-libsecret`.
@@ -1184,7 +1208,7 @@ Setup flow details:
Token + detection notes:
- The AniList access token can be set directly in config; when blank, SubMiner uses the locally stored encrypted token from setup.
- `anilist.accessToken` can be set directly in config; when blank, SubMiner uses the locally stored encrypted token from setup.
- Detection quality is best when `guessit` is installed and available on `PATH`.
- When `guessit` cannot parse or is missing, SubMiner falls back automatically to internal filename parsing.
@@ -1220,7 +1244,7 @@ External-profile mode behavior:
- SubMiner does not open its own Yomitan settings window in this mode.
- SubMiner does not import, delete, or update dictionaries/settings in the external profile.
- SubMiner character-dictionary features are fully disabled in this mode, including auto-sync, manual generation, and subtitle-side character-dictionary annotations.
- First-run setup does not require any internal dictionaries while this mode is configured. If you later launch without an external Yomitan profile, setup will require at least one internal Yomitan dictionary unless SubMiner already finds one.
- First-run setup does not require any internal dictionaries while this mode is configured. If you later launch without `yomitan.externalProfilePath`, setup will require at least one internal Yomitan dictionary unless SubMiner already finds one.
### Jellyfin
@@ -1236,6 +1260,7 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
"remoteControlEnabled": true,
"remoteControlAutoConnect": true,
"autoAnnounce": false,
"remoteControlDeviceName": "SubMiner",
"defaultLibraryId": "",
"directPlayPreferred": true,
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"],
@@ -1244,23 +1269,27 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
}
```
| Option | Values | Description |
| -------------------------- | --------------- | ------------------------------------------------------------------------------------------------------ |
| `jellyfin.enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
| `serverUrl` | string (URL) | Jellyfin server base URL |
| `recentServers` | string[] | Recent Jellyfin server URLs shown in setup; entries are trimmed, deduped, and capped at 5 |
| `username` | string | Default username used by `--jellyfin-login` |
| `defaultLibraryId` | string | Default library id for `--jellyfin-items` when CLI value is omitted |
| `remoteControlEnabled` | `true`, `false` | Enable Jellyfin cast/remote-control session support |
| `remoteControlAutoConnect` | `true`, `false` | Auto-connect Jellyfin remote session on app startup (requires Jellyfin integration and remote control) |
| `autoAnnounce` | `true`, `false` | Auto-run cast-target visibility announce check on connect (default: `false`) |
| `pullPictures` | `true`, `false` | Enable poster/icon fetching for launcher Jellyfin pickers |
| `iconCacheDir` | string | Cache directory for launcher-fetched Jellyfin poster icons |
| `directPlayPreferred` | `true`, `false` | Prefer direct stream URLs before transcoding |
| `directPlayContainers` | string[] | Container allowlist for direct play decisions |
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
| Option | Values | Description |
| -------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------ |
| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
| `serverUrl` | string (URL) | Jellyfin server base URL |
| `recentServers` | string[] | Recent Jellyfin server URLs shown in setup; entries are trimmed, deduped, and capped at 5 |
| `username` | string | Default username used by `--jellyfin-login` |
| `deviceId` | string | Client device id sent in auth headers (default: `subminer`) |
| `clientName` | string | Client name sent in auth headers (default: `SubMiner`) |
| `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) |
| `defaultLibraryId` | string | Default library id for `--jellyfin-items` when CLI value is omitted |
| `remoteControlEnabled` | `true`, `false` | Enable Jellyfin cast/remote-control session support |
| `remoteControlAutoConnect` | `true`, `false` | Auto-connect Jellyfin remote session on app startup (requires `jellyfin.enabled` and `remoteControlEnabled`) |
| `autoAnnounce` | `true`, `false` | Auto-run cast-target visibility announce check on connect (default: `false`) |
| `remoteControlDeviceName` | string | Device name shown in Jellyfin cast/device lists |
| `pullPictures` | `true`, `false` | Enable poster/icon fetching for launcher Jellyfin pickers |
| `iconCacheDir` | string | Cache directory for launcher-fetched Jellyfin poster icons |
| `directPlayPreferred` | `true`, `false` | Prefer direct stream URLs before transcoding |
| `directPlayContainers` | string[] | Container allowlist for direct play decisions |
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted storage after login/setup. SubMiner reports the Jellyfin client as `SubMiner`, derives the Jellyfin device id and visible device name from the OS hostname, and owns the client version internally. The Settings window also hides low-level default library fields (`defaultLibraryId`) so normal setup stays focused on server, auth, playback, and remote-control behavior.
Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted storage after login/setup. The legacy `jellyfin.accessToken` and `jellyfin.userId` config keys are not resolver-backed settings in the current runtime. The Settings window also hides low-level client identity and default library fields (`deviceId`, `clientName`, `clientVersion`, and `defaultLibraryId`) so normal setup stays focused on server, auth, playback, and remote-control behavior.
- On Linux, token storage defaults to `gnome-libsecret` for `safeStorage`. Override with `--password-store=<backend>` on launcher/app invocations when needed.
@@ -1275,9 +1304,7 @@ Launcher subcommands:
See [Jellyfin Integration](/jellyfin-integration) for the full setup and cast-to-device guide.
Jellyfin remote auto-connect runs only when Jellyfin integration, remote control, and remote auto-connect are all enabled.
Jellyfin playback auto-launched through SubMiner loads the mpv plugin the same way regular playback does, and shows the visible subtitle overlay automatically so `subtitleStyle` applies to subtitles selected from Jellyfin.
Jellyfin remote auto-connect runs only when all three are `true`: `jellyfin.enabled`, `jellyfin.remoteControlEnabled`, and `jellyfin.remoteControlAutoConnect`.
When Jellyfin is enabled with a server URL and SubMiner is running, the tray menu also shows a `Jellyfin Discovery` checkbox. It starts or stops discovery for the current runtime session only and does not write config. Starting discovery still requires a valid stored or environment-provided Jellyfin auth session.
@@ -1296,12 +1323,12 @@ Discord Rich Presence is enabled by default. SubMiner publishes a polished activ
}
```
| Option | Values | Description |
| ------------------------- | ------------------------------------------------ | ---------------------------------------------------------- |
| `discordPresence.enabled` | `true`, `false` | Enable Discord Rich Presence updates (default: `true`) |
| `presenceStyle` | `"default"`, `"meme"`, `"japanese"`, `"minimal"` | Card text preset (default: `"default"`) |
| `updateIntervalMs` | number | Minimum interval between activity updates in milliseconds |
| `debounceMs` | number | Debounce window for bursty playback events in milliseconds |
| Option | Values | Description |
| ------------------ | ------------------------------------------------ | ---------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable Discord Rich Presence updates (default: `true`) |
| `presenceStyle` | `"default"`, `"meme"`, `"japanese"`, `"minimal"` | Card text preset (default: `"default"`) |
| `updateIntervalMs` | number | Minimum interval between activity updates in milliseconds |
| `debounceMs` | number | Debounce window for bursty playback events in milliseconds |
Setup steps:
@@ -1363,7 +1390,7 @@ Enable or disable local immersion analytics stored in SQLite for mined subtitles
| Option | Values | Description |
| ------------------------------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `immersionTracking.enabled` | `true`, `false` | Enable immersion tracking. Defaults to `true`. |
| `enabled` | `true`, `false` | Enable immersion tracking. Defaults to `true`. |
| `dbPath` | string | Optional SQLite database path. Leave empty to use default app-data path at `<config dir>/immersion.sqlite`. |
| `batchSize` | integer (`1`-`10000`) | Buffered writes per transaction. Default `25`. |
| `flushIntervalMs` | integer (`50`-`60000`) | Maximum queue delay before flush. Default `500ms`. |
@@ -1416,7 +1443,7 @@ Configure the local stats UI served from SubMiner and the in-app stats overlay t
| Option | Values | Description |
| ----------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| `stats.toggleKey` | Electron key code | Overlay-local key code used to toggle the stats overlay. Default `Backquote`. |
| `toggleKey` | Electron key code | Overlay-local key code used to toggle the stats overlay. Default `Backquote`. |
| `serverPort` | integer | Localhost port for the browser stats UI. Default `6969`. |
| `autoStartServer` | `true`, `false` | Start the local stats HTTP server automatically once immersion tracking is active. Default `true`. |
| `autoOpenBrowser` | `true`, `false` | When `subminer stats` starts the server on demand, also open the dashboard in your default browser. Default `false`. |
@@ -1430,13 +1457,12 @@ Usage notes:
### MPV Launcher
Configure the mpv executable, profile, and window state for SubMiner-managed mpv launches (launcher playback, Windows `--launch-mpv`, and Jellyfin idle mpv startup):
Configure the mpv executable and window state for SubMiner-managed mpv launches (launcher playback, Windows `--launch-mpv`, and Jellyfin idle mpv startup):
```json
{
"mpv": {
"executablePath": "",
"profile": "",
"launchMode": "normal"
}
}
@@ -1445,11 +1471,8 @@ Configure the mpv executable, profile, and window state for SubMiner-managed mpv
| Option | Values | Description |
| ---------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `executablePath` | string | Absolute path to `mpv.exe` for Windows launch flows. Leave empty to auto-discover from `SUBMINER_MPV_PATH` or `PATH` (default `""`) |
| `profile` | string | mpv profile name passed as `--profile=<name>`. Leave empty to pass no profile (default `""`) |
| `launchMode` | `"normal"` \| `"maximized"` \| `"fullscreen"` | Window state when SubMiner spawns mpv (default `"normal"`) |
If `mpv.profile` is configured and the launcher also receives `--profile`, SubMiner passes both as a comma-separated mpv profile list.
Launch mode behavior:
- **`normal`** — mpv opens at its default window size with no extra flags.
@@ -1478,14 +1501,14 @@ Current launcher behavior:
- If YouTube/mpv already exposes an authoritative matching subtitle track, SubMiner reuses it; otherwise it downloads and injects only the missing side.
- SubMiner loads the primary subtitle plus a best-effort secondary subtitle.
- Playback waits only for primary subtitle readiness; secondary failures do not block playback.
- English secondary subtitles are selected from the secondary-subtitle language list when primary language matches are unavailable.
- English secondary subtitles are selected from `secondarySub.secondarySubLanguages` when primary language matches are unavailable.
- Native mpv secondary subtitle rendering stays hidden during this flow so the SubMiner overlay remains the visible secondary subtitle surface.
- If primary subtitle loading fails, use `Ctrl+Alt+C` to open the subtitle modal and pick a track.
Language targets are derived from subtitle config:
- primary track: `youtube.primarySubLanguages` (falls back to `["ja","jpn"]`)
- secondary track: secondary-subtitle language list (falls back to English when empty)
- secondary track: `secondarySub.secondarySubLanguages` (falls back to English when empty)
- Local playback uses the same priorities after mpv reports subtitle track metadata, so sidecar/internal mixed sets can override an incorrect initial `sid=auto` pick.
- Tracks are resolved and loaded before mpv starts; the older launcher mode switch has been removed.
+2 -2
View File
@@ -1,6 +1,6 @@
# Feature Demos
Short recordings of SubMiner's key features and integrations from real playback sessions. A few terms you'll see below: _Yomitan_ is the pop-up dictionary used for word lookups, _Jimaku_ is a community subtitle database, _alass_ and _ffsubsync_ are tools that retime subtitles to match the audio, _Jellyfin_ is a self-hosted media server, and a _texthooker_ is a web page that mirrors the current subtitle as selectable text for browser-based tools.
Short recordings of SubMiner's key features and integrations from real playback sessions.
<script setup>
import { withBase } from 'vitepress';
@@ -25,7 +25,7 @@ Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner aut
## Subtitle Download & Sync
Search and download subtitles from Jimaku, then retime them with alass or ffsubsync — all from within SubMiner.
Search and download subtitles from Jimaku, then automatically synchronize them with alass or ffsubsync — all from within SubMiner.
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/subtitle-sync-poster.jpg?v=${v}`)">
<source :src="withBase(`/assets/demos/subtitle-sync.webm?v=${v}`)" type="video/webm" />
+3 -8
View File
@@ -68,15 +68,10 @@ make dev-watch-macos # same as dev-watch, forcing --bac
```
For mpv-plugin-driven testing without exporting `SUBMINER_BINARY_PATH` each run, set a one-time
dev binary path with `mpv.subminerBinaryPath` in your SubMiner config. The launcher injects it into
the mpv plugin at runtime:
dev binary path in `~/.config/mpv/script-opts/subminer.conf`:
```json
{
"mpv": {
"subminerBinaryPath": "/absolute/path/to/SubMiner/scripts/subminer-dev.sh"
}
}
```ini
binary_path=/absolute/path/to/SubMiner/scripts/subminer-dev.sh
```
## Testing
-6
View File
@@ -2,14 +2,8 @@
SubMiner can log your watching and mining activity to a local SQLite database, then surface it in the built-in stats dashboard. Tracking is enabled by default and can be turned off if you do not want local analytics.
"Immersion" here means time spent watching and reading native Japanese content. **All data stays on your computer** — nothing is uploaded anywhere. (SQLite is just a single-file database; you do not need to install or manage anything.)
When enabled, SubMiner records per-session statistics (watch time, subtitle lines seen, words encountered, cards mined) and maintains exact lifetime summary tables plus daily/monthly rollups. You can view that data in SubMiner's stats UI or query the database directly with any SQLite tool.
::: tip For most users
Just leave tracking on and use the built-in [Stats Dashboard](#stats-dashboard). The retention, performance, SQL, and schema sections further down are reference material for advanced users who want to inspect or tune the database — you can safely skip them.
:::
Episode completion for local `watched` state uses the shared `DEFAULT_MIN_WATCH_RATIO` (`85%`) value from `src/shared/watch-threshold.ts`.
## Enabling
+1 -1
View File
@@ -66,7 +66,7 @@ features:
src: /assets/subtitle-download.svg
alt: Subtitle download icon
title: Subtitle Download & Sync
details: Search and pull subtitles from Jimaku, then retime subtitles with alass or ffsubsync — all from the overlay.
details: Search and pull subtitles from Jimaku, then auto-sync timing with alass or ffsubsync — all from the overlay.
link: /jimaku-integration
linkText: Jimaku integration
- icon:
+5 -7
View File
@@ -1,7 +1,5 @@
# Installation
SubMiner is a desktop app that draws an interactive layer — an **overlay** — on top of the [mpv](https://mpv.io) video player. As you watch native Japanese media, you can click or hover any word in the subtitles to look it up, then turn it into an Anki flashcard without pausing to switch apps. Building flashcards from real content you're watching is called **sentence mining**, and it's what SubMiner is built for. It bundles its own copy of **Yomitan** (a pop-up dictionary) and talks to **AnkiConnect** (an add-on that lets other programs add cards to Anki) so cards get filled in automatically.
Three steps to get started:
1. **Install requirements** — mpv and a few optional extras
@@ -24,7 +22,7 @@ Only **mpv** is strictly required to run SubMiner. Everything else enhances the
| ffmpegthumbnailer | Optional | Video thumbnail generation for the picker. |
| guessit | Optional | Better AniSkip title/season/episode parsing. |
| alass | Optional | Subtitle sync engine (preferred). Disabled without alass or ffsubsync. |
| ffsubsync | Optional | Audio-based subtitle sync engine. Disabled without alass or ffsubsync. |
| ffsubsync | Optional | Subtitle sync engine (fallback). Disabled without alass or ffsubsync. |
| fuse2 | Linux only | Required to run the AppImage. |
### Linux
@@ -94,7 +92,7 @@ pip install ffsubsync
### macOS
macOS 11 (Big Sur) or later. Accessibility permission — the macOS setting that lets one app observe and position another app's windows — is required so the overlay can follow the mpv window (see [step 2](#macos-dmg)).
macOS 10.13 or later. Accessibility permission is required for window tracking (see [step 2](#macos-dmg)).
```bash
brew install mpv ffmpeg
@@ -173,7 +171,7 @@ If you prefer to install it manually, see [manual launcher install](#manual-laun
Download the latest installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
- `SubMiner-<version>.exe` — installer (recommended)
- `SubMiner-<version>-win.zip` — portable fallback
- `SubMiner-<version>.zip` — portable fallback
Make sure `mpv.exe` is on your `PATH`, or set `mpv.executablePath` in the config during first-run setup.
@@ -302,9 +300,9 @@ subminer --update
SubMiner verifies AppImage, launcher, and rofi theme downloads against `SHA256SUMS.txt`. If the binary is in a protected path, SubMiner shows the exact command to run rather than elevating itself.
The tray "Check for Updates" entry installs the new app automatically on Linux, macOS, and Windows. On Linux it replaces the running `.AppImage` in place via `electron-updater`; AppImages managed by a system package (for example the AUR `/opt/SubMiner/SubMiner.AppImage`) are skipped so the package manager stays in charge.
On Linux, `subminer -u` performs the AppImage update from the launcher process directly.
`subminer -u` also performs the AppImage update directly from the launcher process, which is useful when SubMiner is not currently running.
On macOS, tray update checks can also update the app automatically through Electron's built-in updater.
## How It All Fits Together
+161 -94
View File
@@ -1,118 +1,185 @@
# Jellyfin Integration
[Jellyfin](https://jellyfin.org) is a free, self-hosted media server — think of it as your own private streaming service for video you own. If you keep your anime on a Jellyfin server, SubMiner can play episodes through mpv with the full mining overlay.
SubMiner includes an optional Jellyfin CLI integration for:
::: tip Who needs this?
This page is only relevant if you already run (or have access to) a Jellyfin server. If you watch local files or YouTube, you can skip it. The in-app setup window (`subminer jellyfin`) is the easiest starting point.
:::
SubMiner can act as a **cast-to-device target** for Jellyfin (similar to jellyfin-mpv-shim). Sign in once, turn on discovery, and SubMiner shows up in the "Play on…" / cast menu of any Jellyfin app — web, phone, or TV. Pick an episode, cast it to SubMiner, and it plays in SubMiner's mpv window with the full overlay and Yomitan click-to-lookup.
This is the recommended way to use Jellyfin with SubMiner. A terminal-only option is covered in [Launcher playback](#launcher-playback) at the end.
- authenticating against a server
- listing libraries and media items
- launching item playback in the connected mpv instance
- receiving Jellyfin remote cast-to-device playback events in-app
- opening an in-app setup window for server selection and authentication
- toggling Jellyfin cast discovery from the tray once configured
## Requirements
- A Jellyfin server plus your username and password
- SubMiner installed and running (see [Installation](/installation))
- On Linux, the session token is stored with `gnome-libsecret` by default
- Jellyfin server URL and user credentials
- For `--jellyfin-play`: connected mpv IPC socket (`--start` or existing mpv plugin workflow)
- On Linux, token encryption defaults to `gnome-libsecret`; pass `--password-store=<backend>` to override.
## Quick start
## Setup
### 1. Start SubMiner
Launch SubMiner so it's running in the system tray.
### 2. Sign in to your server
Open the tray menu and click **Configure Jellyfin**. In the window that opens, enter your **Server URL** (for example `http://127.0.0.1:8096`), **Username**, and **Password**, then click **Login**.
On success, SubMiner:
- saves an encrypted session token — your password is never stored,
- turns the Jellyfin integration on, and
- remembers the server and username for next time.
Reopen this window any time to switch servers or **Logout**.
### 3. Turn on discovery
Discovery is what makes SubMiner appear as a cast target. Two ways to enable it:
- **For the current session** — open the tray menu and tick **Jellyfin Discovery**. (This item appears once you've signed in.)
- **Automatically on every launch** — already on by default. After your first sign-in, SubMiner auto-connects to Jellyfin at startup, so the cast target is ready without touching the tray. You can change this under [Settings](#settings).
### 4. Cast from any Jellyfin app
In the Jellyfin web UI or mobile app, start playing something, open the **cast / "Play on"** menu, and pick your device — SubMiner appears there named after your computer's hostname. Playback opens in SubMiner.
From then on, pause / resume / seek / stop and audio or subtitle track changes you make in the Jellyfin app are mirrored in SubMiner, and your watch progress syncs back to Jellyfin (now-playing and resume position).
## What happens during playback
- **mpv launches automatically.** If mpv isn't already running when you cast, SubMiner starts it with SubMiner defaults and the bundled mpv plugin, so keybindings work right away.
- **The overlay is managed by SubMiner,** so your configured `subtitleStyle` controls how subtitles look. Use the [overlay-toggle shortcut](/shortcuts) to hide it for a session.
- **Resume works.** If Jellyfin has a saved position for the item, SubMiner seeks there on load.
- **Direct play first.** When the source allows it and the container is in your direct-play allowlist, SubMiner streams the original file; otherwise it requests a transcoded stream from Jellyfin.
- **Japanese subtitles are auto-selected,** preferring Jellyfin's default and embedded tracks over external sidecar files when several match.
- **Subtitle timing is corrected when possible.** SubMiner removes Jellyfin's server-selected subtitle stream from the mpv load URL, suppresses the mpv plugin's one-shot subtitle auto-selection and overlay auto-start for managed Jellyfin loads, stages downloaded subtitle tracks without letting mpv auto-switch between tracks, then selects the Japanese track once after applying any saved or inferred timing delay. When Jellyfin provides both Japanese and English subtitle files, SubMiner compares their cue timelines and applies a global delay if one track is clearly offset. Manual delay shifts you make with SubMiner's adjacent-cue controls are saved per item and subtitle track, then restored the next time you select that track.
## Settings
All Jellyfin options live under **Settings → Integrations → Jellyfin** (open settings from the tray's **Open SubMiner Settings**). The ones that matter for casting:
| Setting | Default | What it does |
| ------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
| **Enabled** | Off | Turns the Jellyfin integration on. Switched on for you when you sign in. |
| **Server Url** | — | Your Jellyfin server. Filled in when you sign in. |
| **Remote Control Enabled** | On | Lets SubMiner act as a cast target. |
| **Remote Control Auto Connect** | On | Connects to Jellyfin at startup so discovery is automatic. Turn off if you'd rather start it from the tray each time. |
| **Auto Announce** | Off | Re-broadcasts visibility on connect. Enable if your device is slow to appear in the cast menu. |
Prefer editing the config file? The same keys live under `jellyfin` in `config.jsonc`:
1. Set base config values (`config.jsonc`):
```jsonc
{
"jellyfin": {
"enabled": true,
"serverUrl": "http://127.0.0.1:8096",
"recentServers": ["http://127.0.0.1:8096"],
"username": "your-user",
"remoteControlEnabled": true,
"remoteControlAutoConnect": true,
"autoAnnounce": false,
"remoteControlDeviceName": "SubMiner",
"defaultLibraryId": "",
"pullPictures": false,
"iconCacheDir": "/tmp/subminer-jellyfin-icons",
"directPlayPreferred": true,
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"],
"transcodeVideoCodec": "h264",
},
}
```
See [Configuration](/configuration) for the full list (transcode codec, direct-play containers, default library, and more).
## Troubleshooting
**SubMiner doesn't appear in the cast menu**
- Make sure SubMiner is running.
- Make sure you're signed in — reopen **Configure Jellyfin** and log in again if your token expired.
- Make sure discovery is on (tray **Jellyfin Discovery**, or **Remote Control Auto Connect** in settings).
- Make sure SubMiner and the Jellyfin client point at the same server.
**Casting starts but nothing plays**
- Confirm the item plays normally in another Jellyfin client.
- If mpv was closed, give it a moment — SubMiner launches it on demand and retries.
**SubMiner keeps disconnecting**
- Check server/network stability and whether the session token has expired.
## Security notes
- The Jellyfin session (access token + user ID) is kept in SubMiner's local encrypted token storage. Your password is used only to log in and is never saved.
- Treat the token storage and your `config.jsonc` as secrets — don't commit them.
- Advanced/headless: the `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID` environment variables can supply a session without the sign-in window.
## Launcher playback
If you'd rather stay in the terminal, the `subminer` launcher can browse and play Jellyfin media directly, without casting from a Jellyfin app:
2. Authenticate:
```bash
subminer jellyfin -p # alias: subminer jf -p
subminer jellyfin
subminer jellyfin -l \
--server http://127.0.0.1:8096 \
--username your-user \
--password 'your-password'
```
This opens an fzf picker (add `-R` for rofi) to browse your libraries and episodes, then plays the selected item in SubMiner's mpv with the same overlay, resume, and subtitle behavior described above. Sign in first (step 2) so the launcher can reach your server. See [Launcher Script](/launcher-script) for the rest of the launcher's features.
`subminer jellyfin` opens the setup window. It offers the configured server, recent servers, and a manual server URL field. Successful login keeps the window open, stores the Jellyfin session token in encrypted storage, updates the configured server/username/client metadata, and refreshes recent servers. Passwords are never stored.
3. List libraries:
```bash
SubMiner.AppImage --jellyfin-libraries
```
Launcher wrapper equivalent for interactive playback flow:
```bash
subminer jellyfin -p
```
Launcher wrapper for Jellyfin cast discovery mode (background app + tray):
```bash
subminer jellyfin -d
```
After Jellyfin is enabled with a server URL and SubMiner is already running, the tray menu shows `Jellyfin Discovery`. Use that checkbox to start or stop discovery for the current runtime session without changing config. If the stored login session is missing or expired, starting discovery shows a warning and setup remains the path to refresh credentials. It does not survive app restart.
Stop discovery session/app:
```bash
subminer app --stop
```
`subminer jf ...` is an alias for `subminer jellyfin ...`.
To clear saved session credentials:
```bash
subminer jellyfin --logout
```
4. List items in a library:
```bash
SubMiner.AppImage --jellyfin-items --jellyfin-library-id LIBRARY_ID --jellyfin-search term
```
Optional listing controls:
- `--jellyfin-recursive=true|false` (default: true)
- `--jellyfin-include-item-types=Series,Season,Folder,CollectionFolder,Movie,...`
These are used by the launcher picker flow to:
- keep root search focused on shows/folders/movies (exclude episode rows)
- browse selected anime/show directories as folder-or-file lists
- recurse for playable files only after selecting a folder
5. Start playback:
```bash
SubMiner.AppImage --start
SubMiner.AppImage --jellyfin-play --jellyfin-item-id ITEM_ID
```
Optional stream overrides:
- `--jellyfin-audio-stream-index N`
- `--jellyfin-subtitle-stream-index N`
## Playback Behavior
- Direct play is attempted first when:
- `jellyfin.directPlayPreferred=true`
- media source supports direct stream
- source container matches `jellyfin.directPlayContainers`
- If direct play is not selected/available, SubMiner requests a Jellyfin transcoded stream (`master.m3u8`) using `jellyfin.transcodeVideoCodec`.
- Resume position (`PlaybackPositionTicks`) is applied via mpv seek.
- Media title is set in mpv as `[Jellyfin/<mode>] <title>`.
## Cast To Device Mode (jellyfin-mpv-shim style)
When SubMiner is running with a valid Jellyfin session, it can appear as a
remote playback target in Jellyfin's cast-to-device menu.
### Requirements
- `jellyfin.enabled=true`
- valid `jellyfin.serverUrl` and Jellyfin auth session (env override or stored login session)
- `jellyfin.remoteControlEnabled=true` (default)
- `jellyfin.remoteControlAutoConnect=true` (default) for startup auto-connect
- `jellyfin.autoAnnounce=false` by default (`true` enables auto announce/visibility check logs on connect)
### Behavior
- SubMiner connects to Jellyfin remote websocket and posts playback capabilities.
- Startup auto-connect still requires `remoteControlAutoConnect=true`; the tray `Jellyfin Discovery` checkbox can start discovery later even when startup auto-connect is disabled.
- `Play` events open media in mpv with the same defaults used by `--jellyfin-play`.
- If mpv IPC is not connected at cast time, SubMiner auto-launches mpv in idle mode with SubMiner defaults and retries playback.
- `Playstate` events map to mpv pause/resume/seek/stop controls.
- Stream selection commands (`SetAudioStreamIndex`, `SetSubtitleStreamIndex`) are mapped to mpv track selection.
- SubMiner reports start/progress/stop timeline updates back to Jellyfin so now-playing and resume state stay synchronized.
- `--jellyfin-remote-announce` forces an immediate capability re-broadcast and logs whether server sessions can see the device.
### Troubleshooting
- Device not visible in Jellyfin cast menu:
- ensure SubMiner is running
- ensure session token is valid (`--jellyfin-login` again if needed)
- ensure `remoteControlEnabled` is true
- use tray `Jellyfin Discovery` or `subminer jellyfin -d` to start discovery
- Cast command received but playback does not start:
- verify mpv IPC can connect (`--start` flow)
- verify item is playable from normal `--jellyfin-play --jellyfin-item-id ...`
- Frequent reconnects:
- check Jellyfin server/network stability and token expiration
## Failure Handling
User-visible errors are shown through CLI logs and mpv OSD for:
- invalid credentials
- expired/invalid token
- server/network errors
- missing library/item identifiers
- no playable source
- mpv not connected for playback
## Security Notes and Limitations
- Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted token storage after login/setup.
- Launcher wrappers support `--password-store=<backend>` and forward it through to the app process.
- Optional environment overrides are supported: `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID`.
- Treat both token storage and config files as secrets and avoid committing them.
- Password is used only for login and is not stored.
- Optional setup UI is available via `--jellyfin`; all actions are also available via CLI flags.
- `subminer` wrapper uses Jellyfin subcommands (`subminer jellyfin ...`, alias `subminer jf ...`). Use `SubMiner.AppImage` for direct `--jellyfin-libraries` and `--jellyfin-items`.
- For direct app CLI usage (`SubMiner.AppImage ...`), `--jellyfin-server` can override server URL for login/play flows without editing config.
+1 -5
View File
@@ -1,10 +1,6 @@
# Jimaku Integration
[Jimaku](https://jimaku.cc) is a community-driven subtitle repository for anime — a shared online library of subtitle files contributed by other learners. 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.
::: tip Prerequisite: a free API key
You need a Jimaku account and an API key (a personal access string) before this feature works. Create an account at [jimaku.cc](https://jimaku.cc), copy your key, and add it to your config as shown under [Configuration](#configuration) below. Without a key, the search modal will report "Jimaku API key not set."
:::
[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
-3
View File
@@ -64,7 +64,6 @@ subminer video.mkv # play a specific file (default plugin c
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
subminer --backend x11 video.mkv # Force x11 backend for a specific file
subminer -u # check for SubMiner updates
subminer logs -e # export sanitized log ZIP
subminer stats # open immersion dashboard
subminer stats -b # start background stats daemon
```
@@ -78,8 +77,6 @@ subminer stats -b # start background stats daemon
| `subminer stats -b` | Start or reuse background stats daemon (non-blocking) |
| `subminer stats cleanup` | Backfill vocabulary metadata and prune stale rows |
| `subminer doctor` | Dependency + config + socket diagnostics |
| `subminer settings` | Open the SubMiner settings window |
| `subminer logs -e` | Export a sanitized log ZIP and print its path |
| `subminer config path` | Print active config file path |
| `subminer config show` | Print active config contents |
| `subminer mpv status` | Check mpv socket readiness |
+112 -81
View File
@@ -4,15 +4,74 @@ This guide walks through the sentence mining loop — from watching a video to c
## Overview
*Sentence mining* means turning real sentences you encounter while watching native video into Anki flashcards, so you learn vocabulary in the context where you actually met it. SubMiner automates the tedious parts of that loop.
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.
SubMiner runs as a transparent overlay on top of mpv (the video player). As subtitles play, the overlay displays them as interactive text. You hover a word, trigger a Yomitan dictionary lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, an audio clip, and a screenshot to that card — no manual copy-pasting or screen capturing.
## Subtitle Delivery Path (Startup + Runtime)
> **Yomitan** is the popup dictionary that shows definitions when you hover or scan a word. **AnkiConnect** is the add-on that lets SubMiner talk to Anki. Both are set up during installation — see [Anki Integration](/anki-integration) if you have not configured them yet.
SubMiner prioritizes subtitle responsiveness over heavy initialization:
1. The first subtitle render is **plain text first** (no tokenization wait).
2. Tokenized enrichment (word spans, known-word flags, JLPT/frequency metadata) is applied right after parsing completes.
3. Under rapid subtitle churn, SubMiner uses a **latest-only tokenization queue** so stale lines are dropped instead of building lag.
4. MeCab, Yomitan extension load, and dictionary prewarm run as background warmups after overlay initialization (configurable via `startupWarmups`, including low-power mode).
This keeps early playback snappy and avoids mpv-side sluggishness while startup work completes.
## Overlay Model
SubMiner uses one overlay window with modal surfaces.
### Primary Subtitle Layer
The visible overlay renders subtitles as tokenized hoverable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
- Word-level hover targets for Yomitan lookup
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
- Auto pause/resume while the Yomitan popup is open (enabled by default via `subtitleStyle.autoPauseVideoOnYomitanPopup`)
- Right-click to pause/resume
- Right-click + drag to reposition subtitles
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
- **Reading annotations** — known words, N+1 targets, character-name matches, JLPT levels, and frequency hits can all be visually highlighted
Toggle visibility with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
### Secondary Subtitle Bar
The secondary subtitle bar is a compact top-strip region in the same overlay window for translation/context visibility while keeping primary reading flow below. It mirrors your configured secondary subtitle preference and can be independently shown or hidden.
It is controlled by `secondarySub` configuration and shares lifecycle with the main overlay window.
### Modal Surfaces
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
## Looking Up Words
1. Hover over the subtitle area — the overlay activates pointer events.
2. Hover the word you want. SubMiner keeps per-token boundaries so Yomitan can target that token cleanly.
3. Trigger Yomitan lookup with your configured lookup key/modifier (for example `Shift` if that is how your Yomitan profile is set up).
4. Yomitan opens its lookup popup for the hovered token.
5. From the popup, add the word to Anki.
### Controller Workflow
With a gamepad connected and keyboard-only mode enabled, the full mining loop works without a mouse or keyboard:
1. **Navigate** — push the left stick left/right to move the token highlight across subtitle words.
2. **Look up** — press `A` to trigger Yomitan lookup on the highlighted word.
3. **Browse the popup** — push the left stick up/down to smooth-scroll through the Yomitan popup, or use the right stick for larger jumps.
4. **Cycle audio** — press `R1` to move to the next dictionary audio entry, `L1` to play the current one.
5. **Mine** — press `X` to create an Anki card for the current sentence (same as `Ctrl+S`).
6. **Close** — press `B` to dismiss the Yomitan popup and return to subtitle navigation.
7. **Pause/resume** — press `L3` (left stick click) to toggle mpv pause at any time.
After controller support is enabled, the controller and keyboard can be used interchangeably — switching mid-session is seamless. Toggle keyboard-only mode on or off with `Y` on the controller.
See [Usage — Controller Support](/usage#controller-support) for setup details and [Configuration — Controller Support](/configuration#controller-support) for the full mapping and tuning options.
## Creating Anki Cards
There are four ways to create or enrich cards, depending on your workflow.
There are three ways to create cards, depending on your workflow.
### 1. Auto-Update from Yomitan
@@ -21,11 +80,11 @@ This is the most common flow. Yomitan creates a card in Anki, and SubMiner enric
1. Hover a word, then trigger Yomitan lookup → Yomitan popup appears.
2. Click the Anki icon in Yomitan to add the word.
3. SubMiner receives or detects the new card:
- **Proxy mode** (default, `ankiConnect.proxy.enabled: true`): immediate enrich after a successful `addNote` / `addNotes` is pushed through the local proxy.
- **Polling mode** (fallback, when the proxy is disabled): detects new cards via AnkiConnect polling (`ankiConnect.pollingRate`, default 3 seconds).
- **Proxy mode** (`ankiConnect.proxy.enabled: true`): immediate enrich after successful `addNote` / `addNotes`.
- **Polling mode** (default): detects via AnkiConnect polling (`ankiConnect.pollingRate`, default 3 seconds).
4. SubMiner updates the card with:
- **Sentence**: The current subtitle line.
- **Audio**: Extracted from the video using the subtitle's start/end timing (plus optional configured padding).
- **Audio**: Extracted from the video using the subtitle's start/end timing (plus configurable padding).
- **Image**: A screenshot or animated clip from the current playback position.
- **Translation**: From the secondary subtitle track, or generated via AI if configured.
- **MiscInfo**: Metadata like filename and timestamp.
@@ -72,88 +131,55 @@ After adding a word via Yomitan, press the audio card shortcut to overwrite the
Audio card marking requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration — Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
:::
### Field Grouping (Kiku)
## Secondary Subtitles
If you mine the same word from different sentences, SubMiner can merge the cards instead of creating duplicates. This feature is designed for use with [Kiku](https://github.com/youyoumu/kiku) and similar note types that support grouped fields.
1. You add a word via Yomitan.
2. SubMiner detects the new card and checks if a card with the same expression already exists.
3. If a duplicate is found (this requires `ankiConnect.isKiku.fieldGrouping` to be set to `"auto"` or `"manual"`; it defaults to `"disabled"`):
- **Auto mode** (`ankiConnect.isKiku.fieldGrouping: "auto"`): Merges automatically. Both sentences, audio clips, and images are combined into the existing card. The duplicate is optionally deleted.
- **Manual mode** (`ankiConnect.isKiku.fieldGrouping: "manual"`): A modal appears showing both cards side by side. You choose which card to keep and preview the merged result before confirming.
See [Anki Integration — Field Grouping](/anki-integration#field-grouping-kiku) for configuration options, merge behavior, and modal keyboard shortcuts.
## Overlay Model
SubMiner uses one overlay window with modal surfaces. It carries two subtitle bars — a primary reading bar and a secondary translation/context bar — plus modal dialogs that open on top.
Toggle the entire overlay window with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
### Primary Subtitle Layer
The primary bar renders subtitles as tokenized hoverable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
- Word-level hover targets for Yomitan lookup
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
- Auto pause/resume while the Yomitan popup is open (enabled by default via `subtitleStyle.autoPauseVideoOnYomitanPopup`)
- Right-click to pause/resume
- Right-click + drag to reposition subtitles
- **Reading annotations** — known words, N+1 targets, character-name matches, JLPT levels, and frequency hits can all be visually highlighted
### Secondary Subtitle Bar
The secondary bar is a compact top-strip region in the same overlay window. It shows a secondary subtitle track (typically English) for translation/context while keeping the primary reading flow below. It is useful for:
SubMiner can display a secondary subtitle track (typically English) alongside the primary Japanese subtitles. This is useful for:
- Quick comprehension checks without leaving the mining flow.
- Auto-populating the translation field on mined cards — when a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
It is controlled by `secondarySub` configuration and shares its lifecycle with the main overlay window. Cycle which track feeds it with `Shift+J`.
- Auto-populating the translation field on mined cards.
### Display Modes
Both the primary and secondary subtitle bars share the same three visibility modes, and each can be changed independently at runtime:
Cycle through modes with the configured shortcut:
- **Hidden** — the bar is not shown.
- **Visible** — the bar is always shown.
- **Hover** — the bar is revealed only while you hover over the overlay.
- **Hidden**: Secondary subtitle not shown.
- **Visible**: Always displayed below the primary subtitle.
- **Hover**: Only shown when you hover over the primary subtitle.
By default the **primary** bar is `visible` (`subtitleStyle.primaryDefaultMode`) and the **secondary** bar is `hover` (`secondarySub.defaultMode`).
When a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
Cycle each bar's mode at runtime with its own shortcut:
## Field Grouping (Kiku)
| Shortcut | Action | Config key |
| -------------------- | -------------------------------------------------------- | ------------------------------ |
| `V` | Cycle primary subtitle mode (hidden → visible → hover) | overlay-local |
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
If you mine the same word from different sentences, SubMiner can merge the cards instead of creating duplicates. This feature is designed for use with [Kiku](https://github.com/youyoumu/kiku) and similar note types that support grouped fields.
### Modal Surfaces
### How It Works
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
1. You add a word via Yomitan.
2. SubMiner detects the new card and checks if a card with the same expression already exists.
3. If a duplicate is found:
- **Auto mode** (`fieldGrouping: "auto"`): Merges automatically. Both sentences, audio clips, and images are combined into the existing card. The duplicate is optionally deleted.
- **Manual mode** (`fieldGrouping: "manual"`): A modal appears showing both cards side by side. You choose which card to keep and preview the merged result before confirming.
## Looking Up Words
See [Anki Integration — Field Grouping](/anki-integration#field-grouping-kiku) for configuration options, merge behavior, and modal keyboard shortcuts.
1. Hover over the subtitle area — the overlay activates pointer events.
2. Hover the word you want. SubMiner keeps per-token boundaries so Yomitan can target that token cleanly.
3. Trigger Yomitan lookup with your configured lookup key/modifier (for example `Shift` if that is how your Yomitan profile is set up).
4. Yomitan opens its lookup popup for the hovered token.
5. From the popup, add the word to Anki.
## Jimaku Subtitle Search
### Controller Workflow
SubMiner integrates with [Jimaku](https://jimaku.cc) to search and download subtitle files for anime directly from the overlay.
With a gamepad connected and keyboard-only mode enabled, the full mining loop works without a mouse or keyboard:
1. Open the Jimaku modal via the configured shortcut (`Ctrl+Shift+J` by default).
2. SubMiner auto-fills the search from the current video filename (title, season, episode).
3. Browse matching entries and select a subtitle file to download.
4. The subtitle is loaded into mpv as a new track.
1. **Navigate** — push the left stick left/right to move the token highlight across subtitle words.
2. **Look up** — press `A` to trigger Yomitan lookup on the highlighted word.
3. **Browse the popup** — push the left stick up/down to smooth-scroll through the Yomitan popup, or use the right stick for larger jumps.
4. **Cycle audio** — press `R1` to move to the next dictionary audio entry, `L1` to play the current one.
5. **Mine** — press `X` to create an Anki card for the current sentence (same as `Ctrl+S`).
6. **Close** — press `B` to dismiss the Yomitan popup and return to subtitle navigation.
7. **Pause/resume** — press `L3` (left stick click) to toggle mpv pause at any time.
Requires an internet connection. An API key (`jimaku.apiKey`) is optional but recommended for higher rate limits.
After controller support is enabled, the controller and keyboard can be used interchangeably — switching mid-session is seamless. Toggle keyboard-only mode on or off with `Y` on the controller.
## Texthooker
See [Usage — Controller Support](/usage#controller-support) for setup details and [Configuration — Controller Support](/configuration#controller-support) for the full mapping and tuning options.
SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) that serves a texthooker UI. This allows external tools — such as a browser-based Yomitan instance — to receive subtitle text in real time.
The texthooker page displays the current subtitle and updates as new lines arrive. This is useful if you prefer to do lookups in a browser rather than through the overlay's built-in Yomitan.
If you want to build your own browser client, websocket consumer, or automation relay, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
## Subtitle Sync (Subsync)
@@ -164,24 +190,29 @@ If your subtitle file is out of sync with the audio, SubMiner can resynchronize
3. For alass, select a reference subtitle track from the video.
4. SubMiner runs the sync and reloads the corrected subtitle.
For remote streams, including Jellyfin playback, the modal only offers alass. Jellyfin subtitle URLs are cached as temporary subtitle files so alass can read them, but the video stream is not downloaded. ffsubsync needs direct access to the local media file and is unavailable for stream URLs.
Install the sync tools separately — see [Troubleshooting](/troubleshooting#subtitle-sync-subsync) if the tools are not found.
## Texthooker
## N+1 Word Highlighting
SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) that serves a texthooker UI. This allows external tools — such as a browser-based Yomitan instance — to receive subtitle text in real time.
When enabled, SubMiner cross-references your Anki decks to highlight known words in the overlay, making true N+1 sentences (exactly one unknown word) easy to spot during immersion.
The texthooker page displays the current subtitle and updates as new lines arrive. This is useful if you prefer to do lookups in a browser rather than through the overlay's built-in Yomitan.
See [Subtitle Annotations — N+1](/subtitle-annotations#n1-word-highlighting) for configuration options and color settings.
If you want to build your own browser client, websocket consumer, or automation relay, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
## Immersion Tracking
## Related Features
SubMiner can log your watching and mining activity to a local SQLite database and expose it in the built-in stats dashboard — session times, words seen, cards mined, and daily/monthly rollups.
These features support the mining loop but have their own dedicated pages:
Enable it in your config:
- **[Jimaku subtitle search](/jimaku-integration)** — search and download anime subtitle files directly from the overlay (`Ctrl+Shift+J` by default), then load them into mpv.
- **[N+1 word highlighting](/subtitle-annotations#n1-word-highlighting)** — cross-reference your Anki decks to highlight known words, making true N+1 sentences (exactly one unknown word) easy to spot during immersion.
- **[Immersion tracking](/immersion-tracking)** — log watching and mining activity to a local database and view session times, words seen, and cards mined in the built-in stats dashboard.
```jsonc
"immersionTracking": {
"enabled": true,
"dbPath": "" // leave empty to use the default location
}
```
Open the dashboard in the overlay with `stats.toggleKey` (default: `` ` ``), launch it in a browser with `subminer stats`, keep a dedicated background server alive with `subminer stats -b`, stop that background server with `subminer stats -s`, or visit `http://127.0.0.1:6969` directly if the local stats server is already running. The dashboard covers overview totals, anime progress, session detail, and vocabulary drill-down from the same local immersion database.
See [Immersion Tracking](/immersion-tracking) for dashboard details, schema, and retention settings.
Next: [Anki Integration](/anki-integration) — field mapping, media generation, and card enrichment configuration.
+101 -41
View File
@@ -1,10 +1,6 @@
# MPV Plugin
**What this is:** mpv is the video player SubMiner overlays subtitles on. The SubMiner mpv plugin is a small Lua script that runs *inside* mpv and gives you in-player keybindings to control the SubMiner overlay (start/stop/toggle, skip intro, etc.) without leaving the player window.
**Who needs this page:** Most users never touch the plugin directly — SubMiner-managed launches (the app, the `subminer` launcher, or the Windows shortcut) inject the bundled plugin automatically for that session, so there is nothing to install into mpv's global `scripts` directory. Read on if you launch mpv from another tool and want SubMiner's in-player controls, or you want to script mpv against SubMiner.
The plugin ships as a modular Lua package under `plugin/subminer/` (entry point `init.lua`, which loads `main.lua` and sibling modules). Earlier releases shipped a single global `main.lua`; runtime loading replaces it.
The SubMiner mpv plugin (`subminer/main.lua`) provides in-player keybindings to control the overlay without leaving mpv. SubMiner-managed launches inject the bundled runtime plugin, so users do not need to install it into mpv's global `scripts` directory.
## Runtime Loading
@@ -27,45 +23,22 @@ input-ipc-server=\\.\pipe\subminer-socket
## Keybindings
Most plugin actions use a `y` chord prefix — press `y`, then the second key (a "chord"):
All keybindings use a `y` chord prefix — press `y`, then the second key:
| Chord | Action |
| ---------------- | -------------------------------------- |
| `y-y` | Open menu |
| `y-s` | Start overlay |
| `y-S` | Stop overlay |
| `y-t` | Toggle visible overlay |
| `y-o` | Open settings window |
| `y-r` | Restart overlay |
| `y-c` | Check status |
| `y-h` | Open session help / keybinding modal |
| `v` | Toggle primary subtitle bar visibility |
| `TAB` (default) | Skip intro (AniSkip) |
The AniSkip key is **not** a `y` chord. It defaults to `TAB` and is configurable via `mpv.aniskipButtonKey`. The legacy `y-k` chord still works as a fallback unless you remap the AniSkip key onto it.
| Chord | Action |
| ----- | ---------------------- |
| `y-y` | Open menu |
| `y-s` | Start overlay |
| `y-S` | Stop overlay |
| `y-t` | Toggle visible overlay |
| `v` | Toggle primary subtitle bar visibility |
| `y-o` | Open settings window |
| `y-r` | Restart overlay |
| `y-c` | Check status |
| `y-k` | Skip intro (AniSkip) |
The bare `v` binding is a forced mpv binding. It overrides mpv's default primary subtitle visibility toggle and routes the action to SubMiner's primary subtitle bar instead.
## Shared Shortcuts (Session Bindings)
The `y-*` chords above are built into the plugin. Everything else you configure under [`shortcuts.*`](/shortcuts) — plus any custom [`keybindings`](/configuration) and the stats toggle/mark-watched keys — is **injected into mpv at runtime**, so the same shortcut works both inside mpv and in the SubMiner overlay. You do not edit any mpv config to enable them.
How it works:
1. The SubMiner app compiles your configured shortcuts, custom keybindings, and stats keys into a normalized list and writes it to `session-bindings.json` in the SubMiner config directory.
2. On load, the plugin reads that file and registers each entry as a forced mpv key binding, translating each accelerator into the matching mpv key name.
3. When a binding fires, the plugin either runs a SubMiner action (by invoking the SubMiner binary with the corresponding CLI flag, e.g. `--mine-sentence`) or runs a raw mpv command, depending on what the shortcut maps to.
Because the bindings come from the same configuration the overlay uses, you maintain one set of shortcuts for both surfaces.
Live updates: changing a shortcut in the app rewrites `session-bindings.json` and sends the plugin a `subminer-reload-session-bindings` script message, so mpv re-registers the bindings immediately — no mpv restart required.
Notes:
- Accelerators are normalized per platform — `CommandOrControl` resolves to `Cmd` on macOS and `Ctrl` elsewhere.
- Multi-line actions (`copySubtitleMultiple`, `mineSentenceMultiple`) register temporary `1``9` digit follow-up bindings after the trigger key, with `Esc` to cancel.
- If two shortcuts compile to the same key, or an accelerator can't be mapped to an mpv key, the app logs a warning and skips that binding instead of registering a broken one.
## Menu
Press `y-y` to open an interactive menu in mpv's OSD:
@@ -82,6 +55,93 @@ SubMiner:
Select an item by pressing its number.
## Configuration
For advanced/manual runtime use, create or edit `~/.config/mpv/script-opts/subminer.conf`:
```ini
# Path to SubMiner binary. Leave empty for auto-detection.
binary_path=
# MPV IPC socket path. Must match input-ipc-server in mpv.conf.
socket_path=/tmp/subminer-socket
# Enable the texthooker WebSocket server.
texthooker_enabled=yes
# Port for the texthooker server.
texthooker_port=5174
# Window manager backend: auto, hyprland, sway, x11, macos, windows.
backend=auto
# Start the overlay automatically when a file is loaded.
# Runs only when mpv input-ipc-server matches socket_path.
auto_start=yes
# Show the visible overlay on auto-start.
# Runs only when mpv input-ipc-server matches socket_path.
auto_start_visible_overlay=yes
# Pause mpv on visible auto-start until SubMiner signals overlay/tokenization readiness.
# Requires auto_start=yes and auto_start_visible_overlay=yes.
auto_start_pause_until_ready=yes
# Show OSD messages for overlay status changes.
osd_messages=yes
# Logging level: debug, info, warn, error.
log_level=info
# Enable AniSkip intro detection/markers.
aniskip_enabled=yes
# Optional title override (launcher fills from guessit when available).
aniskip_title=
# Optional season override (launcher fills from guessit when available).
aniskip_season=
# Optional MAL ID override. Leave blank to resolve from media title.
aniskip_mal_id=
# Optional episode override. Leave blank to detect from filename/title.
aniskip_episode=
# Show OSD skip button while inside intro range.
aniskip_show_button=yes
# OSD label + keybinding for intro skip action.
aniskip_button_text=You can skip by pressing %s
aniskip_button_key=y-k
aniskip_button_duration=3
```
### Option Reference
| Option | Default | Values | Description |
| ------------------------------ | ----------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------- |
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
| `texthooker_port` | `5174` | 165535 | Texthooker server port |
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
| `auto_start` | `yes` | `yes` / `no` | Auto-start overlay on file load when mpv socket matches `socket_path` |
| `auto_start_visible_overlay` | `yes` | `yes` / `no` | Show visible layer on auto-start when mpv socket matches `socket_path` |
| `auto_start_pause_until_ready` | `yes` | `yes` / `no` | Pause mpv on visible auto-start; resume when SubMiner signals tokenization-ready |
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection |
| `aniskip_title` | `""` | string | Override title used for lookup |
| `aniskip_season` | `""` | numeric season | Optional season hint |
| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id |
| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed |
| `aniskip_payload` | `""` | JSON / base64-encoded JSON | Optional pre-fetched AniSkip payload for this media. When set, plugin skips network lookup |
| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt |
| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) |
| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) |
| `aniskip_button_duration` | `3` | float seconds | OSD hint duration |
## Binary Auto-Detection
When `binary_path` is empty, the plugin searches platform-specific locations:
@@ -158,7 +218,7 @@ script-message subminer-start backend=hyprland socket=/custom/path texthooker=no
- If the payload is absent or invalid, lookup falls back to title/MAL-based async fetch.
- Install `guessit` for best detection quality (`python3 -m pip install --user guessit`).
- If OP interval exists, plugin adds `AniSkip Intro Start` and `AniSkip Intro End` chapters.
- At intro start, plugin shows an OSD hint for the first 3 seconds (`You can skip by pressing TAB` by default; the key reflects `mpv.aniskipButtonKey`).
- At intro start, plugin shows an OSD hint for the first 3 seconds (`You can skip by pressing y-k` by default).
- Use `script-message subminer-aniskip-refresh` after changing media metadata/options to retry lookup.
## Lifecycle
+165 -107
View File
@@ -5,6 +5,7 @@
* Copy to %APPDATA%/SubMiner/config.jsonc on Windows, or $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) on Linux/macOS.
*/
{
// ==========================================
// Visible Overlay Auto-Start
// Show the visible subtitle overlay automatically after managed mpv playback starts SubMiner.
@@ -18,7 +19,7 @@
// ==========================================
"texthooker": {
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
"openBrowser": false, // Open the texthooker page in the default browser when the server starts. Values: true | false
"openBrowser": false // Open the texthooker page in the default browser when the server starts. Values: true | false
}, // Configure texthooker startup launch and browser opening behavior.
// ==========================================
@@ -28,7 +29,7 @@
// ==========================================
"websocket": {
"enabled": false, // Built-in subtitle websocket server mode. Values: auto | true | false
"port": 6677, // Built-in subtitle websocket server port.
"port": 6677 // Built-in subtitle websocket server port.
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
// ==========================================
@@ -38,23 +39,17 @@
// ==========================================
"annotationWebsocket": {
"enabled": false, // Annotated subtitle websocket server enabled state. Values: true | false
"port": 6678, // Annotated subtitle websocket server port.
"port": 6678 // Annotated subtitle websocket server port.
}, // Dedicated annotated subtitle websocket for bundled texthooker and token-aware clients.
// ==========================================
// Logging
// Controls logging verbosity.
// Set to debug for full runtime diagnostics.
// Hot-reload: logging.level and logging.files apply live while SubMiner is running.
// Hot-reload: logging.level applies live while SubMiner is running.
// ==========================================
"logging": {
"level": "warn", // Minimum log level for runtime logging. Values: debug | info | warn | error
"rotation": 7, // Number of days of app, launcher, and mpv logs to retain.
"files": {
"app": true, // Write SubMiner app runtime logs. Values: true | false
"launcher": true, // Write launcher command logs. Values: true | false
"mpv": false, // Write mpv player logs. Enable temporarily when debugging mpv/plugin startup. Values: true | false
}, // Files setting.
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
}, // Controls logging verbosity.
// ==========================================
@@ -87,66 +82,66 @@
"leftStickPress": 9, // Raw button index used for controller L3 input.
"rightStickPress": 10, // Raw button index used for controller R3 input.
"leftTrigger": 6, // Raw button index used for controller L2 input.
"rightTrigger": 7, // Raw button index used for controller R2 input.
}, // Semantic button-name reference mapping used for debug output. Updating it does not rewrite existing raw binding descriptors.
"rightTrigger": 7 // Raw button index used for controller R2 input.
}, // Semantic button-name reference mapping used for legacy configs and debug output. Updating it does not rewrite existing raw binding descriptors.
"bindings": {
"toggleLookup": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 0, // Raw button index captured for this discrete controller action.
"buttonIndex": 0 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for toggling lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"closeLookup": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 1, // Raw button index captured for this discrete controller action.
"buttonIndex": 1 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for closing lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"toggleKeyboardOnlyMode": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 3, // Raw button index captured for this discrete controller action.
"buttonIndex": 3 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for toggling keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"mineCard": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 2, // Raw button index captured for this discrete controller action.
"buttonIndex": 2 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for mining the active card. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"quitMpv": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 6, // Raw button index captured for this discrete controller action.
"buttonIndex": 6 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for quitting mpv. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"previousAudio": {
"kind": "none", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
}, // Controller binding descriptor for previous Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"nextAudio": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 5, // Raw button index captured for this discrete controller action.
"buttonIndex": 5 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for next Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"playCurrentAudio": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 4, // Raw button index captured for this discrete controller action.
"buttonIndex": 4 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for playing the current Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"toggleMpvPause": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
"buttonIndex": 9, // Raw button index captured for this discrete controller action.
"buttonIndex": 9 // Raw button index captured for this discrete controller action.
}, // Controller binding descriptor for toggling mpv play/pause. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
"leftStickHorizontal": {
"kind": "axis", // Analog binding input source kind. Values: none | axis
"axisIndex": 0, // Raw axis index captured for this analog controller action.
"dpadFallback": "horizontal", // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
"dpadFallback": "horizontal" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
}, // Axis binding descriptor used for left/right token selection. Use Alt+C learn mode or set a raw axis descriptor manually.
"leftStickVertical": {
"kind": "axis", // Analog binding input source kind. Values: none | axis
"axisIndex": 1, // Raw axis index captured for this analog controller action.
"dpadFallback": "vertical", // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
"dpadFallback": "vertical" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
}, // Axis binding descriptor used for primary popup scrolling. Use Alt+C learn mode or set a raw axis descriptor manually.
"rightStickHorizontal": {
"kind": "axis", // Analog binding input source kind. Values: none | axis
"axisIndex": 3, // Raw axis index captured for this analog controller action.
"dpadFallback": "none", // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
}, // Axis binding descriptor reserved for alternate right-stick mappings. Use Alt+C learn mode or set a raw axis descriptor manually.
"rightStickVertical": {
"kind": "axis", // Analog binding input source kind. Values: none | axis
"axisIndex": 4, // Raw axis index captured for this analog controller action.
"dpadFallback": "none", // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
}, // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
}, // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
"profiles": {}, // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
"profiles": {} // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
// ==========================================
@@ -160,7 +155,7 @@
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
"jellyfinRemoteSession": false, // Warm up Jellyfin remote session at startup. Values: true | false
"jellyfinRemoteSession": true // Warm up Jellyfin remote session at startup. Values: true | false
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
// ==========================================
@@ -172,7 +167,7 @@
"enabled": true, // Run automatic update checks in the background. Values: true | false
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
"notificationType": "system", // How SubMiner announces available updates. Values: system | osd | both | none
"channel": "stable", // Release channel used for update checks. Values: stable | prerelease
"channel": "stable" // Release channel used for update checks. Values: stable | prerelease
}, // Automatic update check behavior.
// ==========================================
@@ -192,13 +187,13 @@
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
"toggleSecondarySub": "CommandOrControl+Shift+V", // Accelerator that toggles the secondary subtitle bar visibility.
"markAudioCard": "CommandOrControl+Shift+A", // Accelerator that marks the last mined card as an audio card.
"openCharacterDictionaryManager": "CommandOrControl+D", // Accelerator that opens the character dictionary manager modal.
"openCharacterDictionary": "CommandOrControl+Alt+A", // Accelerator that opens the character dictionary modal.
"openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
"openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
"openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet.
"openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal.
"openControllerDebug": "Alt+Shift+C", // Accelerator that opens the controller debug modal with live axis/button readouts.
"toggleSubtitleSidebar": "Backslash", // Accelerator that toggles the subtitle sidebar visibility.
"toggleSubtitleSidebar": "Backslash" // Accelerator that toggles the subtitle sidebar visibility.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// ==========================================
@@ -210,76 +205,122 @@
"keybindings": [
{
"key": "Space", // Key setting.
"command": ["cycle", "pause"], // Command setting.
"command": [
"cycle",
"pause"
] // Command setting.
},
{
"key": "KeyF", // Key setting.
"command": ["cycle", "fullscreen"], // Command setting.
"command": [
"cycle",
"fullscreen"
] // Command setting.
},
{
"key": "KeyJ", // Key setting.
"command": ["cycle", "sid"], // Command setting.
"command": [
"cycle",
"sid"
] // Command setting.
},
{
"key": "Shift+KeyJ", // Key setting.
"command": ["cycle", "secondary-sid"], // Command setting.
"command": [
"cycle",
"secondary-sid"
] // Command setting.
},
{
"key": "ArrowRight", // Key setting.
"command": ["seek", 5], // Command setting.
"command": [
"seek",
5
] // Command setting.
},
{
"key": "ArrowLeft", // Key setting.
"command": ["seek", -5], // Command setting.
"command": [
"seek",
-5
] // Command setting.
},
{
"key": "ArrowUp", // Key setting.
"command": ["seek", 60], // Command setting.
"command": [
"seek",
60
] // Command setting.
},
{
"key": "ArrowDown", // Key setting.
"command": ["seek", -60], // Command setting.
"command": [
"seek",
-60
] // Command setting.
},
{
"key": "Shift+KeyH", // Key setting.
"command": ["sub-seek", -1], // Command setting.
"command": [
"sub-seek",
-1
] // Command setting.
},
{
"key": "Shift+KeyL", // Key setting.
"command": ["sub-seek", 1], // Command setting.
"command": [
"sub-seek",
1
] // Command setting.
},
{
"key": "Shift+BracketRight", // Key setting.
"command": ["__sub-delay-next-line"], // Command setting.
"command": [
"__sub-delay-next-line"
] // Command setting.
},
{
"key": "Shift+BracketLeft", // Key setting.
"command": ["__sub-delay-prev-line"], // Command setting.
"command": [
"__sub-delay-prev-line"
] // Command setting.
},
{
"key": "Ctrl+Alt+KeyC", // Key setting.
"command": ["__youtube-picker-open"], // Command setting.
"command": [
"__youtube-picker-open"
] // Command setting.
},
{
"key": "Ctrl+Alt+KeyP", // Key setting.
"command": ["__playlist-browser-open"], // Command setting.
"command": [
"__playlist-browser-open"
] // Command setting.
},
{
"key": "Ctrl+Shift+KeyH", // Key setting.
"command": ["__replay-subtitle"], // Command setting.
"command": [
"__replay-subtitle"
] // Command setting.
},
{
"key": "Ctrl+Shift+KeyL", // Key setting.
"command": ["__play-next-subtitle"], // Command setting.
"command": [
"__play-next-subtitle"
] // Command setting.
},
{
"key": "KeyQ", // Key setting.
"command": ["quit"], // Command setting.
"command": [
"quit"
] // Command setting.
},
{
"key": "Ctrl+KeyW", // Key setting.
"command": ["quit"], // Command setting.
},
"command": [
"quit"
] // Command setting.
}
], // Default and custom keybindings that are merged with built-in defaults.
// ==========================================
@@ -291,19 +332,20 @@
"secondarySub": {
"secondarySubLanguages": [], // Language code priority list used to auto-select a secondary subtitle track when available.
"autoLoadSecondarySub": false, // Automatically load a matching secondary subtitle when the primary subtitle loads. Values: true | false
"defaultMode": "hover", // Default visibility mode for the secondary subtitle bar. Values: hidden | visible | hover
"defaultMode": "hover" // Default visibility mode for the secondary subtitle bar. Values: hidden | visible | hover
}, // Dual subtitle track options.
// ==========================================
// Subtitle Sync
// Auto Subtitle Sync
// Subsync engine and executable paths.
// Hot-reload: subsync changes apply to the next subtitle sync run.
// ==========================================
"subsync": {
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
"alass_path": "", // Optional absolute path to the alass binary used by subsync. Leave empty to auto-discover from PATH.
"ffsubsync_path": "", // Optional absolute path to the ffsubsync binary used by subsync. Leave empty to auto-discover from PATH.
"ffmpeg_path": "", // Optional absolute path to the ffmpeg binary used by subsync. Leave empty to auto-discover from PATH.
"replace": true, // Replace the active subtitle file when sync completes. Values: true | false
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
}, // Subsync engine and executable paths.
// ==========================================
@@ -311,7 +353,7 @@
// Initial vertical subtitle position from the bottom.
// ==========================================
"subtitlePosition": {
"yPercent": 10, // Vertical position of the subtitle overlay expressed as a percentage from the bottom of the screen.
"yPercent": 10 // Vertical position of the subtitle overlay expressed as a percentage from the bottom of the screen.
}, // Initial vertical subtitle position from the bottom.
// ==========================================
@@ -336,15 +378,13 @@
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
"--subtitle-hover-token-background-color": "transparent", // Subtitle hover token background color setting.
"--subtitle-hover-token-background-color": "rgba(54, 58, 79, 0.84)" // Subtitle hover token background color setting.
}, // CSS declaration object applied to primary subtitles after normal subtitle style defaults.
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
"autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
"primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
"nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
"nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
"nPlusOneColor": "#c6a0f6", // Color used for the single N+1 target token subtitle highlight.
"knownWordColor": "#a6da95", // Color used for known-word subtitle highlights.
@@ -353,7 +393,7 @@
"N2": "#f5a97f", // N2 setting.
"N3": "#f9e2af", // N3 setting.
"N4": "#8bd5ca", // N4 setting.
"N5": "#8aadf4", // N5 setting.
"N5": "#8aadf4" // N5 setting.
}, // Jlpt colors setting.
"frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
@@ -362,7 +402,13 @@
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#8bd5ca", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
"bandedColors": [
"#ed8796",
"#f5a97f",
"#f9e2af",
"#8bd5ca",
"#8aadf4"
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
}, // Frequency dictionary setting.
"secondary": {
"css": {
@@ -378,9 +424,9 @@
"font-kerning": "normal", // Font kerning setting.
"text-rendering": "geometricPrecision", // Text rendering setting.
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
}, // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
}, // Secondary setting.
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
} // Secondary setting.
}, // Primary and secondary subtitle styling.
// ==========================================
@@ -393,7 +439,7 @@
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
"pauseVideoOnHover": true, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
"pauseVideoOnHover": false, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
"autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
@@ -405,8 +451,8 @@
"--subtitle-sidebar-timestamp-color": "#a5adcb", // Subtitle sidebar timestamp color setting.
"--subtitle-sidebar-active-line-color": "#f5bde6", // Subtitle sidebar active line color setting.
"--subtitle-sidebar-active-background-color": "rgba(138, 173, 244, 0.22)", // Subtitle sidebar active background color setting.
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)", // Subtitle sidebar hover background color setting.
}, // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)" // Subtitle sidebar hover background color setting.
} // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
// ==========================================
@@ -420,7 +466,7 @@
"model": "openai/gpt-4o-mini", // Default model identifier requested from the shared AI provider.
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // Default system prompt sent with shared AI provider requests.
"requestTimeoutMs": 15000, // Timeout in milliseconds for shared AI provider requests.
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
// ==========================================
@@ -438,22 +484,23 @@
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
"port": 8766, // Bind port for local AnkiConnect proxy.
"upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
"upstreamUrl": "http://127.0.0.1:8765" // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
}, // Proxy setting.
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
"deck": "", // Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to search all decks.
"tags": [
"SubMiner"
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
"fields": {
"word": "Expression", // Card field for the mined word or expression text.
"audio": "ExpressionAudio", // Card field that receives generated sentence audio.
"image": "Picture", // Card field that receives the captured screenshot or animated image.
"sentence": "Sentence", // Card field that receives the source sentence text.
"miscInfo": "MiscInfo", // Card field that receives the miscellaneous info pattern (see ankiConnect.metadata.pattern).
"translation": "SelectionText", // Card field that receives the current selection or translated text.
"translation": "SelectionText" // Card field that receives the current selection or translated text.
}, // Fields setting.
"ai": {
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
"model": "", // Optional model override for Anki AI translation/enrichment flows.
"systemPrompt": "", // Optional system prompt override for Anki AI translation/enrichment flows.
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
}, // Ai setting.
"media": {
"generateAudio": true, // Generate sentence audio for mined cards. Values: true | false
@@ -461,23 +508,20 @@
"imageType": "static", // Image capture type: "static" for a single still frame, "avif" for an animated AVIF. Values: static | avif
"imageFormat": "jpg", // Encoding format used when imageType is "static". Values: jpg | png | webp
"imageQuality": 92, // Quality (0-100) used for lossy static image encoders.
"imageMaxWidth": 0, // Maximum width for static images, in pixels. Set to 0 to preserve the source resolution.
"imageMaxHeight": 0, // Maximum height for static images, in pixels. Set to 0 to preserve the source resolution.
"animatedFps": 10, // Target frame rate for animated AVIF captures.
"animatedMaxWidth": 640, // Maximum width applied to animated AVIF captures.
"animatedMaxHeight": 0, // Maximum height for animated AVIF captures, in pixels. Set to 0 to preserve aspect ratio.
"animatedCrf": 35, // Animated AVIF CRF quality target. Lower values produce larger, higher-quality files.
"syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
"audioPadding": 0, // Seconds of padding appended to both ends of generated sentence audio.
"audioPadding": 0.5, // Seconds of padding appended to both ends of generated sentence audio.
"fallbackDuration": 3, // Fallback clip duration in seconds when subtitle timing data is unavailable.
"maxMediaDuration": 30, // Maximum allowed media clip duration in seconds.
"maxMediaDuration": 30 // Maximum allowed media clip duration in seconds.
}, // Media setting.
"knownWords": {
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Cache matches always receive known-word highlighting even when POS filters suppress other annotation types. Values: headword | surface
"decks": {}, // Decks and expression/word fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word"] }.
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Values: headword | surface
"decks": {} // Decks and fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word", "Word Reading"] }.
}, // Known words setting.
"behavior": {
"overwriteAudio": true, // When updating an existing card, overwrite the audio field instead of skipping it. Values: true | false
@@ -485,24 +529,24 @@
"mediaInsertMode": "append", // Whether new media is appended after or prepended before existing field contents on update. Values: append | prepend
"highlightWord": true, // Bold the mined word inside the sentence field on the saved Anki card. Values: true | false
"notificationType": "osd", // Notification surface used to announce mining and update outcomes. Values: osd | system | both | none
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
}, // Behavior setting.
"nPlusOne": {
"enabled": false, // Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Requires known-word cache data. Values: true | false
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
"minSentenceWords": 3 // Minimum sentence word count required for N+1 targeting (default: 3).
}, // N plus one setting.
"metadata": {
"pattern": "[SubMiner] %f (%t)", // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
"pattern": "[SubMiner] %f (%t)" // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
}, // Metadata setting.
"isLapis": {
"enabled": false, // Enable Lapis-specific mining behaviors and sentence card model targeting. Values: true | false
"sentenceCardModel": "Lapis", // Note type name used by Lapis sentence cards.
"sentenceCardModel": "Lapis" // Note type name used by Lapis sentence cards.
}, // Is lapis setting.
"isKiku": {
"enabled": false, // Enable Kiku-specific mining behaviors (duplicate handling, field grouping). Values: true | false
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
"deleteDuplicateInAuto": true, // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
}, // Is kiku setting.
"deleteDuplicateInAuto": true // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
} // Is kiku setting.
}, // Automatic Anki updates and media generation options.
// ==========================================
@@ -512,10 +556,8 @@
// ==========================================
"jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Base URL of the Jimaku subtitle search API.
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
"maxEntryResults": 10, // Maximum Jimaku search results returned.
"maxEntryResults": 10 // Maximum Jimaku search results returned.
}, // Jimaku API configuration and defaults.
// ==========================================
@@ -524,7 +566,10 @@
// Hot-reload: primarySubLanguages applies to the next YouTube subtitle load.
// ==========================================
"youtube": {
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
"primarySubLanguages": [
"ja",
"jpn"
] // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
}, // Defaults for managed subtitle language preferences and YouTube subtitle loading.
// ==========================================
@@ -537,14 +582,17 @@
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
"characterDictionary": {
"enabled": false, // Enable automatic Yomitan character dictionary sync for currently watched AniList media. Values: true | false
"refreshTtlHours": 168, // Legacy setting; merged character dictionary retention is now usage-based and this value is ignored.
"maxLoaded": 3, // Maximum number of most-recently-used anime snapshots included in the merged Yomitan character dictionary.
"profileScope": "all", // Yomitan profile scope for character dictionary settings updates. Values: all | active
"evictionPolicy": "delete", // Legacy setting; merged character dictionary eviction is usage-based and this value is ignored. Values: disable | delete
"profileScope": "all", // Yomitan profile scope for dictionary enable/disable updates. Values: all | active
"collapsibleSections": {
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
"voicedBy": false, // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
}, // Collapsible sections setting.
}, // Character dictionary setting.
"voicedBy": false // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
} // Collapsible sections setting.
} // Character dictionary setting.
}, // Anilist API credentials and update behavior.
// ==========================================
@@ -555,7 +603,7 @@
// In external-profile mode SubMiner will not import, delete, or modify Yomitan dictionaries/settings.
// ==========================================
"yomitan": {
"externalProfilePath": "", // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
"externalProfilePath": "" // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
}, // Optional external Yomitan profile integration.
// ==========================================
@@ -564,20 +612,18 @@
// Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin.
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.
// Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none.
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
// ==========================================
"mpv": {
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
"launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
"profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
"socketPath": "\\\\.\\pipe\\subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
"backend": "auto", // Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform. Values: auto | hyprland | sway | x11 | macos | windows
"autoStartSubMiner": true, // Start SubMiner in the background when SubMiner-managed mpv loads a file. Values: true | false
"pauseUntilOverlayReady": true, // Pause mpv on visible-overlay auto-start until SubMiner signals subtitle tokenization readiness. Values: true | false
"subminerBinaryPath": "", // Optional SubMiner app binary path passed to the bundled mpv plugin. Leave empty to use the launcher-detected app path.
"aniskipEnabled": true, // Enable AniSkip intro detection and skip markers in the bundled mpv plugin. Values: true | false
"aniskipButtonKey": "TAB", // mpv key used to trigger the AniSkip button while the skip marker is visible.
"aniskipButtonKey": "TAB" // mpv key used to trigger the AniSkip button while the skip marker is visible.
}, // SubMiner-managed mpv launch and bundled plugin options.
// ==========================================
@@ -591,15 +637,27 @@
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
"recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup.
"username": "", // Default Jellyfin username used during CLI login.
"deviceId": "subminer", // Stable device identifier sent on the Jellyfin authentication handshake; primarily internal.
"clientName": "SubMiner", // Client name sent on the Jellyfin authentication handshake; primarily internal.
"clientVersion": "0.1.0", // Client version sent on the Jellyfin authentication handshake; primarily internal.
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
"remoteControlDeviceName": "SubMiner", // Device name reported for Jellyfin remote control sessions.
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions.
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
"directPlayContainers": [
"mkv",
"mp4",
"webm",
"mov",
"flac",
"mp3",
"aac"
], // Container allowlist for direct play decisions.
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
// ==========================================
@@ -611,7 +669,7 @@
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal". Values: default | meme | japanese | minimal
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates.
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
// ==========================================
@@ -636,13 +694,13 @@
"sessionsDays": 0, // Session retention window in days. Use 0 to keep all.
"dailyRollupsDays": 0, // Daily rollup retention window in days. Use 0 to keep all.
"monthlyRollupsDays": 0, // Monthly rollup retention window in days. Use 0 to keep all.
"vacuumIntervalDays": 0, // Minimum days between VACUUM runs. Use 0 to disable.
"vacuumIntervalDays": 0 // Minimum days between VACUUM runs. Use 0 to disable.
}, // Retention setting.
"lifetimeSummaries": {
"global": true, // Maintain global lifetime stats rows. Values: true | false
"anime": true, // Maintain per-anime lifetime stats rows. Values: true | false
"media": true, // Maintain per-media lifetime stats rows. Values: true | false
}, // Lifetime summaries setting.
"media": true // Maintain per-media lifetime stats rows. Values: true | false
} // Lifetime summaries setting.
}, // Enable/disable immersion tracking.
// ==========================================
@@ -655,6 +713,6 @@
"markWatchedKey": "KeyW", // Key code to mark the current video as watched and advance to the next playlist entry.
"serverPort": 6969, // Port for the stats HTTP server.
"autoStartServer": true, // Automatically start the stats server on launch. Values: true | false
"autoOpenBrowser": false, // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
}, // Local immersion stats dashboard served on localhost and available as an in-app overlay.
"autoOpenBrowser": false // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
} // Local immersion stats dashboard served on localhost and available as an in-app overlay.
}

Some files were not shown because too many files have changed in this diff Show More