Compare commits

..

22 Commits

Author SHA1 Message Date
sudacode f7abcedd75 chore: prepare 0.15.0-beta.6 prerelease 2026-05-25 03:25:34 -07:00
sudacode 807c0ff3db Add inline character portraits and dictionary search workflow (#83) 2026-05-25 03:16:25 -07:00
sudacode 7e6f9672cf fix: suppress overlay subtitle immediately when character dictionary modal opens (#84) 2026-05-25 02:30:33 -07:00
sudacode 9fe13601fb Launch macOS app background-detached when no args passed
- Add `launchAppBackgroundDetached` that spawns with `--start --background` and `SUBMINER_BACKGROUND_CHILD=1`
- On darwin with empty appArgs, use detached background launch instead of inherited process
- Add `extraEnv` param to `launchAppCommandDetached` for env injection
- Inject deps into `runAppPassthroughCommand` for testability
- Bump vendor/subminer-yomitan submodule
2026-05-25 02:12:41 -07:00
sudacode 920cbab1bc Fix Windows mpv handoff and tray setup (#82) 2026-05-25 01:34:01 -07:00
sudacode 17d97f0b7e fix: rename Windows ZIPs and fix macOS manual update checks (#81) 2026-05-24 23:47:02 -07:00
sudacode 10463e7348 chore: prepare 0.15.0-beta.5 prerelease 2026-05-24 19:11:02 -07:00
sudacode e9abbd5f05 style: format youtube runtime files 2026-05-24 19:10:58 -07:00
sudacode d6ff50455a fix(changelog): summarize prerelease notes as net outcome 2026-05-24 19:10:53 -07:00
sudacode b1bdeabca8 fix(jellyfin): show overlay, inject plugin, and fix stats title on playback (#77)
* fix(jellyfin): show overlay, inject plugin, and fix stats title on playb

- Show visible overlay automatically during Jellyfin playback so subtitleStyle applies
- Inject bundled mpv plugin on auto-launch so keybindings work without overlay focus
- Group Jellyfin playback stats under item metadata (jellyfin://host/item/id) instead of stream URLs so episodes merge with matching local titles
- Mark ffsubsync unavailable in subsync modal for remote media paths
- Drain queued second-instance commands even when onReady throws

* fix(overlay): stabilize macOS focus handoff and sidebar Yomitan pause

- Keep overlay visible during macOS foreground probe after overlay blur
- Hold sidebar hover-pause while a Yomitan lookup popup remains open

* fix(jellyfin): fix discovery loop, device identity, tray state, and Disc

- Derive device identity from OS hostname; remove legacy configurable client/device fields
- Prevent discovery playback from reloading active item, misreporting pause state, and duplicate overlay restores
- Restart stale tray discovery sessions without re-login when server drops SubMiner cast target
- Sync tray discovery checkbox state on Linux after CLI/startup/remote-session changes
- Stop Discord presence falling back to stream URLs; prime title before tokenized stream loads
- Fix picker library discovery when log level is above info
- Fix config.example.jsonc trailing commas and array formatting

* docs(release): trim and consolidate prerelease notes for 0.15.0

- Remove breaking changes section and several redundant bullet points
- Consolidate per-platform updater notes into a single entry
- Normalize em-dash separators to hyphens in section headers

* fix(config): remove trailing commas from config.example.jsonc

- Strip trailing commas throughout both config.example.jsonc copies
- Reformat inline arrays to multi-line for JSON strictness
- Update Jellyfin subtitle preload and playback launch tests and impl

* fix(tokenizer): preserve known-word highlight when POS filters suppress

- Known-word cache matches now set isKnown=true even for tokens excluded by POS filters
- POS exclusion gate suppresses N+1, frequency, and JLPT only; known status is computed before the gate
- Jellyfin subtitle preload continues after cleanup failures instead of aborting
- Update config docs and option description to document the known-word bypass behavior

* fix(jellyfin): send explicit hide/show overlay instead of toggle

- Track overlay visibility in plugin state; y-t uses explicit hide/show commands when state is known
- Prevent paused Jellyfin playback from resuming on overlay hide
- Fix subtitle cache cleanup to only remove dirs after successful cleanup

* fix(jellyfin): fix remote progress sync, seek reporting, and startup sto

- arm active playback before loadfile with loadedMediaPath: null to suppress premature stop events
- force immediate progress report on seek-like position jumps at the mpv time-pos level
- send positionTicks and failed=false in reportStopped payload
- remove EventName from HTTP timeline payloads (websocket-only field)
- add startup grace window to drop stop events before media finishes loading

* fix(jellyfin): fix overlay toggle sync, redirect reload, and AppImage bi

- Sync visible-overlay state back to plugin via script messages to avoid toggle/hide drift
- Collapse duplicate toggle events within 250ms to prevent hide-then-show on single keypress
- Preserve manual hide across Jellyfin path-changing redirects even when media-title drops
- Rearm managed subtitle defaults on path-changing redirects
- Route toggleVisibleOverlay session binding through plugin toggle instead of app-side IPC
- Show Linux/Hyprland overlay passively (showInactive) to avoid stealing mpv keyboard focus
- Fix AppImage binary resolution to prefer $APPIMAGE env over mounted inner binary
- Add stats window layer management so delete/update dialogs appear above stats window
- Fix Jellyfin remote progress sync during Linux websocket reconnect windows

* Fix CodeRabbit review feedback

* fix(jellyfin): subtitle timing, resume progress, and overlay sync

- Add per-stream subtitle delay persistence and auto timeline-offset correction
- Strip server-selected subtitle stream from mpv load URL; suppress plugin subtitle rearm and auto-start during app-managed preload
- Fix resume position lost when mpv resets on stop; use last known position for final progress/stopped reports
- Keep Play vs Resume distinct to avoid early seek race on normal play
- Fix discovery resume when remote play sends StartPositionTicks=0 despite saved progress
- Deduplicate show/hide overlay commands using recorded visibility state
- Rewrite docs-site Jellyfin page around cast-to-device UX

* test: update lifecycle cleanup assertion

* fix: clear aborted playback state, fix overlay passthrough, and guard du

- Reset app_managed_playback_pending on lifecycle cleanup to prevent state leak into next item
- Record visible overlay action only after command succeeds, not before
- Non-native passive overlay now always click-through on re-show (fix isNonNativePassiveOverlay ordering)
- Defer activeParsedSubtitleMediaPath assignment until after prefetch completes
- Move autoplay gate release into the hide branch of toggleVisibleOverlay
- Clear active Jellyfin playback when stopping media that never loaded
- Reset managed subtitle delay and delay key when no external tracks are available
- Await async removeDir in subtitle cache cleanup
- Guard duplicate delete clicks in MediaDetailView and SessionsTab with refs
- Escape key in DeleteConfirmDialog now calls stopPropagation and stopImmediatePropagation
2026-05-24 18:40:56 -07:00
sudacode da3c971ee6 fix: delegate multi-line digit selection to visible overlay (#78) 2026-05-24 00:39:23 -07:00
sudacode c02edc90cc docs: audit and refresh user-facing and internal docs
Cross-check every config key, shortcut, default, and command against the
current source and fix the drift (mpv.socketPath, auto_start_overlay
default, AniSkip TAB key, JLPT N4 color, secondary-sub font/defaults,
secondary-sub language behavior, modular mpv plugin layout, and more).
Add plain-language intros and first-use definitions across onboarding and
integration pages so non-technical readers can follow along.

Internal docs/: fix stale module paths in architecture/domains.md, add
missing contract entry points and catalog rows, and bump verified dates.
Remove the obsolete docs/plans/ directory (its only plan shipped in
0.15.0) and reframe planning.md so plans live with the work, not in docs/.
2026-05-23 21:21:16 -07:00
sudacode 4d1a20d69b feat(config): surface optional anki/jimaku keys in example config
Add ankiConnect.deck, jimaku.apiKey, and jimaku.apiKeyCommand to the
defaults so they appear in the generated config.example.jsonc, and
change the static/animated image maxima (imageMaxWidth, imageMaxHeight,
animatedMaxHeight) from undefined to 0 so they render too. The resize
paths already treat 0 as "no limit", so this is behavior-neutral and
just improves discoverability of these previously-undocumented keys.
2026-05-23 21:21:07 -07:00
sudacode 7e86c4ea3d feat(launcher): add mpv.profile config option for managed launches (#80) 2026-05-23 15:14:19 -07:00
sudacode c4f99fec2f upgrade Electron 39→42 and fix Hyprland overlay z-order/placement (#79) 2026-05-22 23:22:51 -07:00
sudacode c6328eef09 chore: prepare 0.15.0-beta.4 prerelease 2026-05-21 20:57:31 -07:00
sudacode dc52bc2fba feat(config): add configuration window (#70) 2026-05-21 04:16:21 -07:00
sudacode a54f03f0cd Fix Jellyfin Login (#76) 2026-05-20 00:46:11 -07:00
sudacode 799cce6991 fix(docs): correct versioned nav links and local dev version routing (#74) 2026-05-18 01:07:17 -07:00
sudacode 6b2cb002ac [codex] add versioned Pages deployment (#73) 2026-05-17 19:54:59 -07:00
sudacode e84674e3b5 feat(macos): configuration window + curl-backed macOS updater (#71) 2026-05-17 02:23:44 -07:00
sudacode 6ca5cede3e feat(launcher): add --version / -v flag to print app version
- Exits early with `SubMiner <version>` output, no app binary required
- Maps `-v` at root level without conflicting with `stats cleanup -v`
- Adds `version: boolean` to `Args` type and default args
- Documents flag in docs-site and adds changelog fragment
2026-05-16 22:47:57 -07:00
591 changed files with 40151 additions and 4701 deletions
+76
View File
@@ -0,0 +1,76 @@
name: Docs Pages
on:
workflow_dispatch:
push:
branches:
- main
tags:
- 'v*'
paths:
- 'docs-site/**'
- 'scripts/docs-versioning.ts'
- 'scripts/build-versioned-docs.ts'
- '.github/workflows/docs-pages.yml'
- 'package.json'
- 'bun.lock'
concurrency:
group: docs-pages-production
cancel-in-progress: false
jobs:
deploy:
if: ${{ github.ref_type != 'tag' || !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Guard stable docs tag shape
id: tag_guard
if: github.ref_type == 'tag'
run: |
if [[ ! "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::notice::Skipping non-stable docs tag ${{ github.ref_name }}"
echo "stable_tag=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "stable_tag=true" >> "$GITHUB_OUTPUT"
- name: Setup Bun
if: steps.tag_guard.outputs.stable_tag != 'false'
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Install dependencies
if: steps.tag_guard.outputs.stable_tag != 'false'
run: |
bun install --frozen-lockfile
cd docs-site && bun install --frozen-lockfile
- name: Cache versioned docs archives
if: steps.tag_guard.outputs.stable_tag != 'false'
uses: actions/cache@v4
with:
path: .tmp/docs-versioned-archive-cache
key: docs-versioned-archives-${{ runner.os }}-${{ hashFiles('docs-site/.vitepress/**', 'docs-site/public/assets/fonts/**', 'docs-site/package.json', 'docs-site/bun.lock', 'scripts/build-versioned-docs.ts', 'scripts/docs-versioning.ts') }}
- name: Test docs
if: steps.tag_guard.outputs.stable_tag != 'false'
run: bun run docs:test
- name: Build versioned docs
if: steps.tag_guard.outputs.stable_tag != 'false'
run: bun run docs:build:versioned
- name: Deploy docs to Cloudflare Pages
if: steps.tag_guard.outputs.stable_tag != 'false'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy .tmp/docs-versioned-site --project-name "${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME }}" --branch main
+17 -1
View File
@@ -1,4 +1,4 @@
.PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-windows uninstall uninstall-linux uninstall-macos uninstall-windows print-dirs pretty lint ensure-bun generate-config generate-example-config dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop
.PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-windows uninstall uninstall-linux uninstall-macos uninstall-windows print-dirs pretty lint ensure-bun generate-config generate-example-config dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop docs-test docs-build docs-build-versioned docs-dev
APP_NAME := subminer
THEME_SOURCE := assets/themes/subminer.rasi
@@ -62,6 +62,10 @@ help:
" dev-watch-macos Start watch loop with forced macOS tracker backend" \
" dev-toggle Toggle overlay in a running local Electron app" \
" dev-stop Stop a running local Electron app" \
" docs-test Run docs tests" \
" docs-build Build the docs site" \
" docs-build-versioned Build production versioned docs site" \
" docs-dev Start the docs dev server" \
" install-linux Install Linux wrapper/theme/app artifacts" \
" install-macos Install macOS wrapper/theme/app artifacts" \
" install-windows Print Windows packaging/install guidance" \
@@ -200,6 +204,18 @@ dev-toggle: ensure-bun
dev-stop: ensure-bun
@bun run electron . --stop
docs-test: ensure-bun
@bun run docs:test
docs-build: ensure-bun
@bun run docs:build
docs-build-versioned: ensure-bun
@bun run docs:build:versioned
docs-dev: ensure-bun
@bun run docs:dev
install-linux: build-launcher
@printf '%s\n' "[INFO] Installing Linux wrapper/theme artifacts"
+43 -93
View File
@@ -4,7 +4,7 @@
# SubMiner
Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv.
Integrates Yomitan and mpv - on-screen lookups, mine to Anki, and track 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 @@ Look up words with Yomitan, export to Anki in one key, track your immersion —
### Dictionary Lookups
Yomitan runs inside the overlay. Trigger a lookup on any word for full dictionary popups — definitions, pitch accent, frequency data without ever leaving mpv.
Hover over any word and trigger a lookup to get the full Yomitan popup - definitions, pitch accent, and 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. Known words fade back; new words stand out. Grammar-only tokens 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. Grammar-only tokens and particles 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 watch time, anime library, vocabulary growth, mining throughput, session history, and trends. All stored locally, no third-party tracking.
Local stats dashboard tracking watch time, 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>Automatic subtitle retiming — requires <code>alass</code> or <code>ffsubsync</code> on your <code>PATH</code> (optional; subtitle syncing is disabled without them)</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>
</tr>
<tr>
<td><b>WebSocket</b></td>
<td>Annotated subtitle feed for external clients (texthooker pages, custom tools)</td>
<td>Plain subtitle feed plus a dedicated annotated feed for texthooker pages and custom tools</td>
</tr>
</table>
@@ -110,65 +110,36 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
## Requirements
| | Required | Recommended | Optional |
| -------------- | --------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — | — |
| **Processing** | — | `ffmpeg` (audio clips & screenshots) | `mecab` + `mecab-ipadic` (annotation POS filtering), `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
| **Media** | — | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
| **Selection** | — | — | `fzf` / `rofi` |
Only **mpv** and Anki+AnkiConnect are required. Everything else is optional but enhances the experience.
> [!TIP]
> `ffmpeg` is not strictly required to run SubMiner, but without it audio clips and screenshots will not be attached to Anki cards. Most users will want it installed.
> [!NOTE]
> [`bun`](https://bun.sh) is required if building from source or using the CLI wrapper: `subminer`. Pre-built releases (AppImage, DMG, installer) do not require it.
**Platform-specific:**
| Linux | macOS | Windows |
| ------------------------------------------------------------ | ------------------------ | ------------- |
| Hyprland (`hyprctl`) · X11/Xwayland (`xdotool` + `xwininfo`) | Accessibility permission | No extra deps |
> [!NOTE]
> **Wayland support is compositor-specific.** Wayland has no universal API for window positioning and each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Hyprland is the only native Wayland backend supported currenlty. All other Linux compositors require both mpv and SubMiner to run under X11 or Xwayland. The launcher detects your compositor and configures this automatically.
| 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 |
<details>
<summary><b>Arch Linux</b></summary>
<summary><b>Platform-specific install commands</b></summary>
**Arch Linux:**
```bash
paru -S --needed mpv ffmpeg
# Optional
paru -S --needed mecab-git mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo
# Optional: subtitle sync (install at least one for subtitle syncing to work)
paru -S --needed alass python-ffsubsync
# X11 / Xwayland (required for non-Hyprland compositors)
paru -S --needed xdotool xorg-xwininfo
sudo pacman -S --needed mpv ffmpeg mecab mecab-ipadic
```
</details>
<details>
<summary><b>macOS</b></summary>
**macOS:**
```bash
brew install mpv ffmpeg
# Optional
brew install mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
# Optional: subtitle sync (install at least one for subtitle syncing to work)
brew install alass
pip install ffsubsync
brew install mpv ffmpeg mecab mecab-ipadic
```
Grant Accessibility permission to SubMiner in **System Settings > Privacy & Security > Accessibility**.
**Windows:** Install [mpv](https://mpv.io/installation/) and [ffmpeg](https://ffmpeg.org/download.html) and ensure both are on `PATH`.
</details>
<details>
<summary><b>Windows</b></summary>
Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on your `PATH`.
Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for additional metadata enrichment.
See the [full requirements list](https://docs.subminer.moe/installation#1-install-requirements) for optional dependencies.
</details>
@@ -176,7 +147,7 @@ Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download
## Quick Start
### 1. Install
### 1. Install SubMiner
<details>
<summary><b>Arch Linux (AUR)</b></summary>
@@ -185,12 +156,6 @@ Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download
paru -S subminer-bin
```
Or manually:
```bash
git clone https://aur.archlinux.org/subminer-bin.git && cd subminer-bin && makepkg -si
```
</details>
<details>
@@ -199,40 +164,24 @@ git clone https://aur.archlinux.org/subminer-bin.git && cd subminer-bin && makep
```bash
mkdir -p ~/.local/bin
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage \
&& chmod +x ~/.local/bin/SubMiner.AppImage
&& chmod +x ~/.local/bin/SubMiner.AppImage
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer \
&& chmod +x ~/.local/bin/subminer
&& chmod +x ~/.local/bin/subminer
```
> [!NOTE]
> The `subminer` wrapper uses a [Bun](https://bun.sh) shebang. First-run setup can optionally install Bun and the launcher into an existing writable PATH directory.
</details>
<details>
<summary><b>macOS</b></summary>
<summary><b>macOS (DMG)</b></summary>
Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
Also download the `subminer` launcher (recommended):
```bash
mkdir -p ~/.local/bin
curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o ~/.local/bin/subminer \
&& chmod +x ~/.local/bin/subminer
```
> [!NOTE]
> The `subminer` launcher uses a [Bun](https://bun.sh) shebang. First-run setup can optionally install Bun and the launcher into an existing writable PATH directory. Make sure `~/.local/bin` is on your PATH before installing there.
Download the latest DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
</details>
<details>
<summary><b>Windows</b></summary>
Download the latest installer (`.exe`) [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`.
**Note:** On Windows the recommended way to run playback is with the **SubMiner mpv** shortcut created during first-run setup. First-run setup can also optionally install Bun and a `subminer.cmd` command shim to your user PATH, so new terminals can run `subminer` without adding `SubMiner.exe` to PATH.
Download and run the latest installer (`.exe`) from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest).
</details>
@@ -243,28 +192,29 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
</details>
### 2. First Launch
### 2. Launch & Set Up
Run SubMiner and the first-run setup wizard will guide you through importing Yomitan dictionaries and optionally installing the `subminer` command-line launcher.
```bash
subminer app --setup # launch the first-run setup wizard
# Linux
subminer app --setup
# macOS — open SubMiner.app, or:
subminer app --setup
```
SubMiner creates a default config, starts in the system tray, and opens a setup popup that walks you through installing Yomitan dictionaries. The setup popup can also optionally install Bun and the `subminer` command-line launcher; those choices do not block setup completion.
> [!NOTE]
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
On **Windows**, just run `SubMiner.exe` and the setup will open automatically on first launch.
### 3. Mine
```bash
subminer video.mkv # play video with overlay
subminer --start video.mkv # explicit overlay start
subminer stats # open immersion dashboard
subminer stats -b # stats daemon in background
subminer stats -s # stop background stats daemon
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)
```
On **Windows**, use the **SubMiner mpv** shortcut created during first-run setup — double-click it to open mpv, or drag a video file onto it. You can also run `SubMiner.exe --launch-mpv` from a terminal.
On **Windows**, use the **SubMiner mpv** shortcut created during setup. Double-click it or drag a video file onto it.
## Documentation
+135 -20
View File
@@ -18,11 +18,12 @@
"ws": "^8.19.0",
},
"devDependencies": {
"@types/node": "^25.3.0",
"@types/node": "^24.10.0",
"@types/ws": "^8.18.1",
"electron": "39.8.6",
"electron": "42.2.0",
"electron-builder": "26.8.2",
"esbuild": "^0.25.12",
"eslint": "^10.4.0",
"prettier": "^3.8.1",
"typescript": "^5.9.3",
},
@@ -52,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@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/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/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=="],
@@ -116,10 +117,34 @@
"@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=="],
@@ -166,15 +191,21 @@
"@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@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
"@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
@@ -194,6 +225,10 @@
"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=="],
@@ -294,6 +329,8 @@
"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=="],
@@ -326,7 +363,7 @@
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"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": ["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-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=="],
@@ -344,7 +381,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@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
@@ -364,6 +401,22 @@
"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=="],
@@ -374,12 +427,22 @@
"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=="],
@@ -404,6 +467,8 @@
"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=="],
@@ -442,6 +507,8 @@
"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=="],
@@ -450,8 +517,12 @@
"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=="],
@@ -472,6 +543,8 @@
"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=="],
@@ -486,8 +559,12 @@
"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=="],
@@ -540,6 +617,8 @@
"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=="],
@@ -560,16 +639,22 @@
"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=="],
@@ -588,6 +673,8 @@
"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=="],
@@ -700,13 +787,15 @@
"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@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
"undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="],
@@ -726,6 +815,8 @@
"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=="],
@@ -748,14 +839,12 @@
"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=="],
@@ -764,6 +853,8 @@
"@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=="],
@@ -774,6 +865,20 @@
"@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=="],
@@ -786,8 +891,6 @@
"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=="],
@@ -800,22 +903,36 @@
"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=="],
@@ -826,8 +943,6 @@
"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
@@ -0,0 +1,4 @@
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,11 +34,13 @@ 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
+5
View File
@@ -0,0 +1,5 @@
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.
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: config
- Settings: Changed the AniSkip button key setting to use click-to-learn key capture instead of raw text entry.
+4
View File
@@ -0,0 +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.
+4
View File
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,7 @@
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.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: config
- Updated the generated example config to use the same CSS declaration paths written by the Settings window for subtitle and sidebar appearance.
@@ -0,0 +1,4 @@
type: fixed
area: config
- Preserved user config files during legacy config compatibility handling.
+4
View File
@@ -0,0 +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.
+4
View File
@@ -0,0 +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.
+8
View File
@@ -0,0 +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.
@@ -0,0 +1,4 @@
type: changed
area: config
- Defaulted Jellyfin remote-session startup warmup and character-name subtitle highlighting to off.
+4
View File
@@ -0,0 +1,4 @@
type: docs
area: docs
- Published stable docs at the site root with current development docs under `/main/`.
+4
View File
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Primed the first startup subtitle before autoplay resumes so the overlay can render text before video playback begins.
@@ -0,0 +1,5 @@
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.
@@ -0,0 +1,4 @@
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.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: subtitles
- Kept frequency highlighting for determiner-led noun compounds like `その場` while still filtering standalone determiners.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,5 @@
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.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
type: fixed
area: jellyfin
- Fixed Jellyfin discovery resume playback when a remote play command sends `StartPositionTicks: 0` despite saved progress on the item.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,11 @@
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.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,5 @@
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.
@@ -0,0 +1,4 @@
type: fixed
area: launcher
- Suppressed Electron macOS menu diagnostics from `subminer settings` launcher output.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: shortcuts
- Disabled native mpv menu shortcuts during managed macOS playback so configured SubMiner shortcuts also work while mpv has focus.
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
type: fixed
area: updater
- Clarified that beta/RC update checks are controlled by `updates.channel`; set it to `"prerelease"` to receive beta/RC updates.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
type: fixed
area: stats
- Stats: Fixed in-player stats layering so delete confirmations, overlay modals, and update-check dialogs appear above the stats window.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: docs
- Fixed versioned docs navigation so archived pages keep local links under the selected version, the version switcher no longer nests targets under the current archive path, local dev version routes serve warmed archive files instead of redirecting to production or falling through to VitePress 404s, and internal README files do not break archived builds.
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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.
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: setup
- Setup: Removed the bundled mpv runtime plugin readiness card; legacy mpv plugin removal still appears when needed.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: jellyfin
- Kept Jellyfin picker library discovery working when the running app log level is above info.
+5
View File
@@ -0,0 +1,5 @@
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.
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: config
- Config: Moved known-word and N+1 annotation colors to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`; legacy Anki color keys are still accepted with warnings.
+4
View File
@@ -0,0 +1,4 @@
type: added
area: launcher
- Added `subminer --version` and `subminer -v` to print the installed SubMiner app version.
+5
View File
@@ -0,0 +1,5 @@
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
@@ -0,0 +1,4 @@
type: fixed
area: updater
- Fixed Linux automatic update checks to avoid Electron networking, preventing native Electron network-service crashes during video startup.
+4
View File
@@ -0,0 +1,4 @@
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 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.
- 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.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Kept the macOS visible overlay stable when clicking from the overlay back into mpv.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
type: fixed
area: release
- Fixed macOS updater metadata mismatches by giving macOS and Windows ZIP release assets distinct build-time filenames.
+4
View File
@@ -0,0 +1,4 @@
type: added
area: launcher
- Managed bundled mpv plugin startup options from SubMiner config.
+4
View File
@@ -0,0 +1,4 @@
type: added
area: launcher
- Added `mpv.profile` config and settings support for passing an mpv profile to SubMiner-managed mpv launches.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Wired configured session shortcuts, including `stats.markWatchedKey`, through mpv so custom add/remove changes work while mpv has focus.
+3 -1
View File
@@ -1,4 +1,6 @@
type: fixed
area: updates
- Kept signed macOS app updates on the native updater path while preventing eager Squirrel install checks before the user confirms restart.
- Restored the standard macOS `electron-updater`/Squirrel update path and routed supplemental GitHub updater requests through Electron networking instead of Node fetch.
- macOS update checks now skip local build-output apps outside Applications before touching Squirrel, and macOS tray checks no longer perform the supplemental GitHub asset lookup.
- macOS `electron-updater` metadata and full ZIP downloads now use `/usr/bin/curl` under the hood to avoid the Electron network crash seen during tray update checks while preserving Squirrel installation.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: config
- Defaulted the note-fields note type picker to the configured Anki deck's note type when available, then exact `Kiku`, then exact `Lapis`, otherwise leaving it blank for manual selection.
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: config
- Config: Preserved N+1 subtitle highlighting for existing configs that already enabled known-word highlighting, while keeping N+1 disabled by default for new configs unless `ankiConnect.nPlusOne.enabled` is set.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Kept the visible overlay and subtitle stream alive after restarting SubMiner from the mpv `y-r` shortcut by transporting Linux AppImage control args safely, restoring mpv subtitle visibility during shutdown, snapshotting subtitles before overlay suppression resumes, reapplying Linux overlay bounds after the restarted window maps, allowing Hyprland to resize the visible overlay window, and preserving user-paused playback while readiness gates clear.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: websocket
- WebSocket: Kept the regular subtitle websocket plain-text only; annotation spans and token metadata now stay on the annotation websocket.
@@ -0,0 +1,4 @@
type: changed
area: jellyfin
- Removed the Jellyfin setup server presets dropdown; setup now shows a single editable server URL field.
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
type: changed
area: subtitles
- Subsync now always opens the manual picker and the `subsync.defaultMode` config/settings option has been removed.
@@ -0,0 +1,4 @@
type: changed
area: config
- Config: Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css` and `subtitleStyle.secondary.css`.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: config
- Migrated legacy subtitle hover token colors into `subtitleStyle.css` instead of leaving `hoverTokenColor` or `hoverTokenBackgroundColor` behind.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: config
- Migrated legacy primary and secondary subtitle appearance options into `subtitleStyle.css` automatically when loading config files.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: config
- Fixed live Settings window saves so primary and secondary subtitle CSS declarations apply immediately to open video overlays.
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: config
- Added `subtitleSidebar.css`, migrated legacy sidebar appearance fields into it, and updated subtitle font defaults to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`.
+4
View File
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
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`.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed Yomitan popups not opening when playback/overlay startup races the Yomitan extension load.
+7
View File
@@ -0,0 +1,7 @@
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.
+140 -117
View File
@@ -7,10 +7,11 @@
{
// ==========================================
// Overlay Auto-Start
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
// Visible Overlay Auto-Start
// Show the visible subtitle overlay automatically after managed mpv playback starts SubMiner.
// SubMiner can still auto-start in the background when this is false.
// ==========================================
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
"auto_start_overlay": true, // Show the visible subtitle overlay automatically when the bundled mpv plugin starts SubMiner. Values: true | false
// ==========================================
// Texthooker Server
@@ -18,7 +19,7 @@
// ==========================================
"texthooker": {
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
"openBrowser": false // Open browser setting. 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.
// ==========================================
@@ -45,6 +46,7 @@
// Logging
// Controls logging verbosity.
// Set to debug for full runtime diagnostics.
// Hot-reload: logging.level applies live while SubMiner is running.
// ==========================================
"logging": {
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
@@ -153,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": true // Warm up Jellyfin remote session at startup. Values: true | false
"jellyfinRemoteSession": false // Warm up Jellyfin remote session at startup. Values: true | false
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
// ==========================================
@@ -174,24 +176,24 @@
// Hot-reload: shortcut changes apply live and update the session help modal on reopen.
// ==========================================
"shortcuts": {
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
"triggerFieldGrouping": "CommandOrControl+G", // Trigger field grouping setting.
"triggerSubsync": "Ctrl+Alt+S", // Trigger subsync setting.
"mineSentence": "CommandOrControl+S", // Mine sentence setting.
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Mine sentence multiple setting.
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Global accelerator that toggles overlay visibility from anywhere on the system. Use null to disable.
"copySubtitle": "CommandOrControl+C", // Accelerator that copies the current subtitle line to the clipboard.
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Accelerator that copies consecutive subtitle lines while the multi-copy window stays open.
"updateLastCardFromClipboard": "CommandOrControl+V", // Accelerator that updates the last mined Anki card using the current clipboard contents.
"triggerFieldGrouping": "CommandOrControl+G", // Accelerator that triggers Kiku field grouping on duplicate cards.
"triggerSubsync": "Ctrl+Alt+S", // Accelerator that triggers subsync against the active subtitle file.
"mineSentence": "CommandOrControl+S", // Accelerator that mines the current sentence as a new Anki card.
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Accelerator that mines consecutive sentences while the multi-mine window stays open.
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
"openCharacterDictionary": "CommandOrControl+Alt+A", // Open character dictionary setting.
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
"openSessionHelp": "CommandOrControl+Slash", // Open session help setting.
"openControllerSelect": "Alt+C", // Open controller select setting.
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
"toggleSubtitleSidebar": "Backslash" // Toggle subtitle sidebar setting.
"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.
"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.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// ==========================================
@@ -328,20 +330,20 @@
// Hot-reload: defaultMode updates live while SubMiner is running.
// ==========================================
"secondarySub": {
"secondarySubLanguages": [], // Secondary sub languages setting.
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
"defaultMode": "hover" // Default mode setting.
"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
}, // Dual subtitle track options.
// ==========================================
// Auto Subtitle Sync
// 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": "", // Alass path setting.
"ffsubsync_path": "", // Ffsubsync path setting.
"ffmpeg_path": "", // Ffmpeg path setting.
"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
}, // Subsync engine and executable paths.
@@ -350,7 +352,7 @@
// Initial vertical subtitle position from the bottom.
// ==========================================
"subtitlePosition": {
"yPercent": 10 // Y percent setting.
"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.
// ==========================================
@@ -360,29 +362,32 @@
// ==========================================
"subtitleStyle": {
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"color": "#cad3f5", // Color setting.
"background-color": "transparent", // Background color setting.
"font-size": "35px", // Font size setting.
"font-weight": "600", // Font weight setting.
"font-style": "normal", // Font style setting.
"line-height": "1.35", // Line height setting.
"letter-spacing": "-0.01em", // Letter spacing setting.
"word-spacing": "0", // Word spacing setting.
"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.
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
"--subtitle-hover-token-background-color": "transparent" // 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
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
"nameMatchEnabled": false, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
"nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched 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.
"fontFamily": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"fontSize": 35, // Font size setting.
"fontColor": "#cad3f5", // Font color setting.
"fontWeight": "600", // Font weight setting.
"lineHeight": 1.35, // Line height setting.
"letterSpacing": "-0.01em", // Letter spacing setting.
"wordSpacing": 0, // Word spacing setting.
"fontKerning": "normal", // Font kerning setting.
"textRendering": "geometricPrecision", // Text rendering setting.
"textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"fontStyle": "normal", // Font style setting.
"backgroundColor": "transparent", // Background color setting.
"backdropFilter": "blur(6px)", // Backdrop filter setting.
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
"knownWordColor": "#a6da95", // Known word color setting.
"nPlusOneColor": "#c6a0f6", // Color used for the single N+1 target token subtitle highlight.
"knownWordColor": "#a6da95", // Color used for known-word subtitle highlights.
"jlptColors": {
"N1": "#ed8796", // N1 setting.
"N2": "#f5a97f", // N2 setting.
@@ -406,19 +411,21 @@
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
}, // Frequency dictionary setting.
"secondary": {
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
"fontSize": 24, // Font size setting.
"fontColor": "#cad3f5", // Font color setting.
"lineHeight": 1.35, // Line height setting.
"letterSpacing": "-0.01em", // Letter spacing setting.
"wordSpacing": 0, // Word spacing setting.
"fontKerning": "normal", // Font kerning setting.
"textRendering": "geometricPrecision", // Text rendering setting.
"textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"backgroundColor": "transparent", // Background color setting.
"backdropFilter": "blur(6px)", // Backdrop filter setting.
"fontWeight": "600", // Font weight setting.
"fontStyle": "normal" // Font style setting.
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"color": "#cad3f5", // Color setting.
"background-color": "transparent", // Background color setting.
"font-size": "24px", // Font size setting.
"font-weight": "600", // Font weight setting.
"font-style": "normal", // Font style setting.
"line-height": "1.35", // Line height setting.
"letter-spacing": "-0.01em", // Letter spacing setting.
"word-spacing": "0", // Word spacing setting.
"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.
}, // Primary and secondary subtitle styling.
@@ -432,18 +439,20 @@
"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": false, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
"pauseVideoOnHover": true, // 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
"maxWidth": 420, // Maximum sidebar width in CSS pixels.
"opacity": 0.95, // Base opacity applied to the sidebar shell.
"backgroundColor": "rgba(73, 77, 100, 0.9)", // Background color for the subtitle sidebar shell.
"textColor": "#cad3f5", // Default cue text color in the subtitle sidebar.
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif", // Font family used for subtitle sidebar cue text.
"fontSize": 16, // Base font size for subtitle sidebar cue text in CSS pixels.
"timestampColor": "#a5adcb", // Timestamp color in the subtitle sidebar.
"activeLineColor": "#f5bde6", // Text color for the active subtitle cue.
"activeLineBackgroundColor": "rgba(138, 173, 244, 0.22)", // Background color for the active subtitle cue.
"hoverLineBackgroundColor": "rgba(54, 58, 79, 0.84)" // Background color for hovered subtitle cues.
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"color": "#cad3f5", // Color setting.
"background-color": "rgba(73, 77, 100, 0.9)", // Background color setting.
"font-size": "16px", // Font size setting.
"opacity": "0.95", // Opacity setting.
"--subtitle-sidebar-max-width": "420px", // Subtitle sidebar max width setting.
"--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.
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
// ==========================================
@@ -454,22 +463,22 @@
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
"model": "openai/gpt-4o-mini", // Model setting.
"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.", // System prompt setting.
"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.
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
// ==========================================
// AnkiConnect Integration
// Automatic Anki updates and media generation options.
// Hot-reload: ankiConnect.ai.enabled updates live while SubMiner is running.
// Hot-reload: ankiConnect.ai.enabled, knownWords, nPlusOne, fields.word/audio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, and isKiku.fieldGrouping update live while SubMiner is running.
// Shared AI provider transport settings are read from top-level ai and typically require restart.
// Most other AnkiConnect settings still require restart.
// ==========================================
"ankiConnect": {
"enabled": true, // Enable AnkiConnect integration. Values: true | false
"url": "http://127.0.0.1:8765", // Url setting.
"url": "http://127.0.0.1:8765", // Base URL of the AnkiConnect HTTP server.
"pollingRate": 3000, // Polling interval in milliseconds.
"proxy": {
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
@@ -480,13 +489,14 @@
"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.
"fields": {
"word": "Expression", // Card field for the mined word or expression text.
"audio": "ExpressionAudio", // Audio setting.
"image": "Picture", // Image setting.
"sentence": "Sentence", // Sentence setting.
"miscInfo": "MiscInfo", // Misc info setting.
"translation": "SelectionText" // Translation setting.
"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.
}, // Fields setting.
"ai": {
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
@@ -494,59 +504,64 @@
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
}, // Ai setting.
"media": {
"generateAudio": true, // Generate audio setting. Values: true | false
"generateImage": true, // Generate image setting. Values: true | false
"imageType": "static", // Image type setting.
"imageFormat": "jpg", // Image format setting.
"imageQuality": 92, // Image quality setting.
"animatedFps": 10, // Animated fps setting.
"animatedMaxWidth": 640, // Animated max width setting.
"animatedCrf": 35, // Animated crf setting.
"generateAudio": true, // Generate sentence audio for mined cards. Values: true | false
"generateImage": true, // Generate screenshot or animated image for mined cards. Values: true | false
"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.5, // Audio padding setting.
"fallbackDuration": 3, // Fallback duration setting.
"maxMediaDuration": 30 // Max media duration setting.
"audioPadding": 0, // 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.
}, // 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. 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"] }.
"color": "#a6da95" // Color used for known-word highlights.
"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 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, // Overwrite audio setting. Values: true | false
"overwriteImage": true, // Overwrite image setting. Values: true | false
"mediaInsertMode": "append", // Media insert mode setting.
"highlightWord": true, // Highlight word setting. Values: true | false
"notificationType": "osd", // Notification type setting.
"overwriteAudio": true, // When updating an existing card, overwrite the audio field instead of skipping it. Values: true | false
"overwriteImage": true, // When updating an existing card, overwrite the image field instead of skipping it. Values: true | false
"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
}, // Behavior setting.
"nPlusOne": {
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
"nPlusOne": "#c6a0f6" // Color used for the single N+1 target token highlight.
"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).
}, // N plus one setting.
"metadata": {
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
"pattern": "[SubMiner] %f (%t)" // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
}, // Metadata setting.
"isLapis": {
"enabled": false, // Enabled setting. Values: true | false
"sentenceCardModel": "Japanese sentences" // Sentence card model setting.
"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.
}, // Is lapis setting.
"isKiku": {
"enabled": false, // Enabled setting. Values: true | false
"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 // Delete duplicate in auto setting. Values: true | false
"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.
// ==========================================
// Jimaku
// Jimaku API configuration and defaults.
// Hot-reload: Jimaku changes apply to the next Jimaku request.
// ==========================================
"jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
"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.
}, // Jimaku API configuration and defaults.
@@ -554,6 +569,7 @@
// ==========================================
// YouTube Playback Settings
// Defaults for managed subtitle language preferences and YouTube subtitle loading.
// Hot-reload: primarySubLanguages applies to the next YouTube subtitle load.
// ==========================================
"youtube": {
"primarySubLanguages": [
@@ -598,14 +614,25 @@
// ==========================================
// MPV Launcher
// Optional mpv.exe override for Windows playback entry points.
// SubMiner-managed mpv launch and bundled plugin options.
// 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
}, // Optional mpv.exe override for Windows playback entry points.
"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": "/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.
}, // SubMiner-managed mpv launch and bundled plugin options.
// ==========================================
// Jellyfin
@@ -618,14 +645,10 @@
"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", // Device id setting.
"clientName": "SubMiner", // Client name setting.
"clientVersion": "0.1.0", // Client version setting.
"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
@@ -648,7 +671,7 @@
// ==========================================
"discordPresence": {
"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".
"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.
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
+355 -65
View File
@@ -1,3 +1,7 @@
import { existsSync, readFileSync, statSync } from 'node:fs';
import { extname, join, posix, resolve, sep } from 'node:path';
import type { DefaultTheme, HeadConfig, TransformContext, UserConfig } from 'vitepress';
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
@@ -7,20 +11,358 @@ const PLAUSIBLE_INIT_SCRIPT = [
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
].join('\n');
function pageToCanonicalHref(page: string): string | null {
type DocsChannel = 'stable-root' | 'stable-archive' | 'main';
type VersionManifest = {
latestStable: string;
channels: Array<{ label: string; path: string }>;
versions: Array<{ version: string; path: string }>;
};
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 = process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN ?? 'production';
function normalizeBase(value: string): string {
if (!value || value === '/') return '/';
return `/${value.replace(/^\/+|\/+$/g, '')}/`;
}
function normalizeChannel(value: string | undefined): DocsChannel {
if (value === 'main' || value === 'stable-archive') return value;
return 'stable-root';
}
function parseVersionManifest(value: string | undefined): VersionManifest {
if (!value) {
return {
latestStable,
channels: [
{ label: 'Latest stable', path: '/' },
{ label: 'main', path: '/main/' },
],
versions: [{ version: latestStable, path: `/v/${latestStable.replace(/^v/, '')}/` }],
};
}
return JSON.parse(value) as VersionManifest;
}
function withDocsBase(path: string): string {
if (/^[a-z]+:\/\//i.test(path)) return path;
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
if (base === '/') return normalizedPath;
return `${base.replace(/\/$/, '')}${normalizedPath}`;
}
function pageToRoute(page: string): string | null {
if (page === '404.md') return null;
const route = page
.replace(/(^|\/)index\.md$/, '')
.replace(/\.md$/, '')
.replace(/\/$/, '');
return route ? `${DOCS_HOSTNAME}/${route}` : `${DOCS_HOSTNAME}/`;
return route ? `/${route}` : '/';
}
export default {
function pageToCanonicalHref(page: string): string | null {
const route = pageToRoute(page);
if (!route) return null;
if (channel === 'main') {
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
}
if (channel === 'stable-archive' && docsVersion !== latestStable) {
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
}
return route === '/' ? `${DOCS_HOSTNAME}/` : `${DOCS_HOSTNAME}${route}`;
}
function canonicalRouteWithBase(route: string): string {
const routeWithBase = withDocsBase(route);
return route === '/' ? routeWithBase : routeWithBase.replace(/\/$/, '');
}
function transformPageHead({ page }: TransformContext): HeadConfig[] {
const href = pageToCanonicalHref(page);
const head: HeadConfig[] = href ? [['link', { rel: 'canonical', href }]] : [];
if (channel === 'main') {
head.push(['meta', { name: 'robots', content: 'noindex,follow' }]);
}
return head;
}
function linkToPagePath(link: string): string | null {
if (!link.startsWith('/') || link.startsWith('/v/') || link.startsWith('/main/')) {
return null;
}
const withoutHash = link.split('#')[0] ?? '/';
const withoutQuery = withoutHash.split('?')[0] ?? '/';
const route = withoutQuery.replace(/^\/+|\/+$/g, '');
return route ? `${route}.md` : 'index.md';
}
function hasPageForLink(link: string): boolean {
const pagePath = linkToPagePath(link);
if (!pagePath) return true;
return existsSync(join(docsSourceDir, pagePath));
}
function filterNav(items: DefaultTheme.NavItem[]): DefaultTheme.NavItem[] {
return items
.map((item) => {
if ('items' in item && item.items) {
return { ...item, items: filterNav(item.items as DefaultTheme.NavItem[]) };
}
if ('link' in item && item.link && !hasPageForLink(item.link)) {
return null;
}
return item;
})
.filter((item): item is DefaultTheme.NavItem => Boolean(item));
}
function filterSidebar(items: DefaultTheme.SidebarItem[]): DefaultTheme.SidebarItem[] {
return items
.map((item) => {
const filteredChildren = item.items ? filterSidebar(item.items) : undefined;
if (item.link && !hasPageForLink(item.link)) return null;
if (item.items && filteredChildren?.length === 0 && !item.link) return null;
return { ...item, items: filteredChildren };
})
.filter((item): item is DefaultTheme.SidebarItem => Boolean(item));
}
function versionSwitchLink(path: string): string {
if (/^[a-z]+:\/\//i.test(path)) return path;
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
if (versionLinkOrigin === 'local') return localVersionSwitchLink(normalizedPath);
return `${DOCS_HOSTNAME}${normalizedPath}`;
}
function localVersionSwitchLink(path: string): string {
if (base === '/') return path;
const basePath = base.replace(/\/$/, '');
const targetPath = path === '/' ? '/' : path.replace(/\/$/, '');
const relativePath = posix.relative(basePath, targetPath) || '.';
return path.endsWith('/') ? `${relativePath}/` : relativePath;
}
function shouldHandleLocalVersionRoute(pathname: string): boolean {
if (base !== '/' || channel !== 'stable-root') return false;
return /^\/main(?:\/|$)/.test(pathname) || /^\/v\/[^/]+(?:\/|$)/.test(pathname);
}
function contentTypeForPath(path: string): string {
switch (extname(path)) {
case '.css':
return 'text/css; charset=utf-8';
case '.gif':
return 'image/gif';
case '.ico':
return 'image/x-icon';
case '.jpg':
case '.jpeg':
return 'image/jpeg';
case '.js':
case '.mjs':
return 'text/javascript; charset=utf-8';
case '.json':
case '.jsonc':
return 'application/json; charset=utf-8';
case '.mp4':
return 'video/mp4';
case '.png':
return 'image/png';
case '.svg':
return 'image/svg+xml';
case '.ttf':
return 'font/ttf';
case '.webm':
return 'video/webm';
case '.woff':
return 'font/woff';
case '.woff2':
return 'font/woff2';
case '.xml':
return 'application/xml; charset=utf-8';
default:
return 'text/html; charset=utf-8';
}
}
function isFile(path: string): boolean {
try {
return statSync(path).isFile();
} catch {
return false;
}
}
function archiveFileForPathname(pathname: string): string | null {
if (!shouldHandleLocalVersionRoute(pathname)) return null;
const routePath = decodeURIComponent(pathname).replace(/^\/+/, '');
const filePath = resolve(localArchiveDir, routePath);
if (filePath !== localArchiveDir && !filePath.startsWith(`${localArchiveDir}${sep}`)) {
return null;
}
const candidates = pathname.endsWith('/')
? [join(filePath, 'index.html')]
: extname(filePath)
? [filePath]
: [`${filePath}.html`, join(filePath, 'index.html')];
return candidates.find(isFile) ?? null;
}
function serveLocalArchiveRoute(pathname: string, response: DevServerResponse): boolean {
if (versionLinkOrigin !== 'local') return false;
const filePath = archiveFileForPathname(pathname);
if (!filePath) return false;
response.statusCode = 200;
response.setHeader('Content-Type', contentTypeForPath(filePath));
response.end(readFileSync(filePath));
return true;
}
type DevServerResponse = {
statusCode: number;
setHeader(name: string, value: string): void;
end(chunk?: string | Uint8Array): void;
};
const versionItems = [
{
text: `Latest stable (${versionManifest.latestStable})`,
link: versionSwitchLink('/'),
target: '_self',
noIcon: true,
},
...versionManifest.channels
.filter((entry) => entry.label !== 'Latest stable')
.map((entry) => ({
text: entry.label,
link: versionSwitchLink(entry.path),
target: '_self',
noIcon: true,
})),
...versionManifest.versions.map((entry) => ({
text: entry.version,
link: versionSwitchLink(entry.path),
target: '_self',
noIcon: true,
})),
];
const nav: DefaultTheme.NavItem[] = [
{ text: 'Home', link: '/' },
{ text: 'Get Started', link: '/installation' },
{ text: 'Mining', link: '/mining-workflow' },
{ text: 'Configuration', link: '/configuration' },
{ text: 'Changelog', link: '/changelog' },
{ text: 'Troubleshooting', link: '/troubleshooting' },
{ text: docsVersion ?? (channel === 'main' ? 'main' : latestStable), items: versionItems },
];
const sidebar: DefaultTheme.SidebarItem[] = [
{
text: 'Getting Started',
items: [
{ text: 'Overview', link: '/' },
{ text: 'Installation', link: '/installation' },
{ text: 'Usage', link: '/usage' },
{ text: 'Mining Workflow', link: '/mining-workflow' },
{ text: 'Launcher Script', link: '/launcher-script' },
],
},
{
text: 'Reference',
items: [
{ text: 'Configuration', link: '/configuration' },
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
{ text: 'Troubleshooting', link: '/troubleshooting' },
],
},
{
text: 'Integrations',
items: [
{ text: 'MPV Plugin', link: '/mpv-plugin' },
{ text: 'Anki', link: '/anki-integration' },
{ text: 'Jellyfin', link: '/jellyfin-integration' },
{ text: 'YouTube', link: '/youtube-integration' },
{ text: 'Jimaku', link: '/jimaku-integration' },
{ text: 'AniList', link: '/anilist-integration' },
{ text: 'Character Dictionary', link: '/character-dictionary' },
],
},
{
text: 'Development',
items: [
{ text: 'Building & Testing', link: '/development' },
{ text: 'Architecture', link: '/architecture' },
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
{ text: 'Changelog', link: '/changelog' },
],
},
];
const config: UserConfig = {
title: 'SubMiner Docs',
description:
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
base,
...(outDir ? { outDir } : {}),
vite: {
plugins: [
{
name: 'subminer-docs-local-version-redirects',
configureServer(server) {
server.middlewares.use((request, response, next) => {
const requestUrl = new URL(request.url ?? '/', 'http://localhost');
if (serveLocalArchiveRoute(requestUrl.pathname, response)) {
return;
}
if (!shouldHandleLocalVersionRoute(requestUrl.pathname)) {
next();
return;
}
response.statusCode = 302;
response.setHeader(
'Location',
`${DOCS_HOSTNAME}${requestUrl.pathname}${requestUrl.search}`,
);
response.end();
});
},
},
],
},
head: [
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
[
@@ -31,13 +373,13 @@ export default {
},
],
['script', {}, PLAUSIBLE_INIT_SCRIPT],
['link', { rel: 'icon', href: '/favicon.ico', sizes: 'any' }],
['link', { rel: 'icon', href: withDocsBase('/favicon.ico'), sizes: 'any' }],
[
'link',
{
rel: 'icon',
type: 'image/png',
href: '/favicon-32x32.png',
href: withDocsBase('/favicon-32x32.png'),
sizes: '32x32',
},
],
@@ -46,7 +388,7 @@ export default {
{
rel: 'icon',
type: 'image/png',
href: '/favicon-16x16.png',
href: withDocsBase('/favicon-16x16.png'),
sizes: '16x16',
},
],
@@ -54,7 +396,7 @@ export default {
'link',
{
rel: 'apple-touch-icon',
href: '/apple-touch-icon.png',
href: withDocsBase('/apple-touch-icon.png'),
sizes: '180x180',
},
],
@@ -70,12 +412,9 @@ export default {
);
},
},
transformHead({ page }) {
const href = pageToCanonicalHref(page);
return href ? [['link', { rel: 'canonical', href }]] : [];
},
transformHead: transformPageHead,
lastUpdated: true,
srcExclude: ['subagents/**'],
srcExclude: ['subagents/**', 'README.md'],
markdown: {
theme: {
light: 'catppuccin-latte',
@@ -88,59 +427,8 @@ export default {
dark: '/assets/SubMiner.png',
},
siteTitle: 'SubMiner Docs',
nav: [
{ text: 'Home', link: '/' },
{ text: 'Get Started', link: '/installation' },
{ text: 'Mining', link: '/mining-workflow' },
{ text: 'Configuration', link: '/configuration' },
{ text: 'Changelog', link: '/changelog' },
{ text: 'Troubleshooting', link: '/troubleshooting' },
],
sidebar: [
{
text: 'Getting Started',
items: [
{ text: 'Overview', link: '/' },
{ text: 'Installation', link: '/installation' },
{ text: 'Usage', link: '/usage' },
{ text: 'Mining Workflow', link: '/mining-workflow' },
{ text: 'Launcher Script', link: '/launcher-script' },
],
},
{
text: 'Reference',
items: [
{ text: 'Configuration', link: '/configuration' },
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
{ text: 'Troubleshooting', link: '/troubleshooting' },
],
},
{
text: 'Integrations',
items: [
{ text: 'MPV Plugin', link: '/mpv-plugin' },
{ text: 'Anki', link: '/anki-integration' },
{ text: 'Jellyfin', link: '/jellyfin-integration' },
{ text: 'YouTube', link: '/youtube-integration' },
{ text: 'Jimaku', link: '/jimaku-integration' },
{ text: 'AniList', link: '/anilist-integration' },
{ text: 'Character Dictionary', link: '/character-dictionary' },
],
},
{
text: 'Development',
items: [
{ text: 'Building & Testing', link: '/development' },
{ text: 'Architecture', link: '/architecture' },
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
{ text: 'Changelog', link: '/changelog' },
],
},
],
nav: filterNav(nav),
sidebar: filterSidebar(sidebar),
search: {
provider: 'local',
},
@@ -159,3 +447,5 @@ export default {
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
},
};
export default config;
@@ -1,6 +1,7 @@
<script setup>
import { useRoute, useData } from 'vitepress';
import { computed } from 'vue';
import { formatStatusLineFilePath } from '../status-line';
const route = useRoute();
const { page, frontmatter } = useData();
@@ -12,8 +13,7 @@ const mode = computed(() => {
});
const filePath = computed(() => {
const path = route.path;
return path === '/' ? 'index.md' : `${path.replace(/^\//, '')}.md`;
return formatStatusLineFilePath(route.path);
});
const section = computed(() => {
@@ -0,0 +1,16 @@
import { expect, test } from 'bun:test';
import { formatStatusLineFilePath } from './status-line';
test('status line file path formats root home as index markdown', () => {
expect(formatStatusLineFilePath('/')).toBe('index.md');
});
test('status line file path formats version archive home without trailing slash', () => {
expect(formatStatusLineFilePath('/v/0.12.0/')).toBe('v/0.12.0.md');
});
test('status line file path keeps normal docs routes as markdown files', () => {
expect(formatStatusLineFilePath('/v/0.12.0/configuration')).toBe(
'v/0.12.0/configuration.md',
);
});
@@ -0,0 +1,4 @@
export function formatStatusLineFilePath(routePath: string): string {
if (routePath === '/') return 'index.md';
return `${routePath.replace(/^\/|\/$/g, '')}.md`;
}
+2 -2
View File
@@ -5,7 +5,7 @@
@font-face {
font-family: 'M PLUS 1';
src: url('/assets/fonts/Mplus1-Medium.ttf') format('truetype');
src: url('../../public/assets/fonts/Mplus1-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -13,7 +13,7 @@
@font-face {
font-family: 'Manrope Default';
src: url('/assets/fonts/manrope-latin-600-normal.ttf') format('truetype');
src: url('../../public/assets/fonts/manrope-latin-600-normal.ttf') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
+12 -5
View File
@@ -30,9 +30,16 @@ bun run docs:dev
## Cloudflare Pages
- Git repo: `ksyasuda/SubMiner`
- Root directory: `docs-site`
- Build command: `bun run docs:build`
- Build output directory: `.vitepress/dist`
- Build watch paths: `docs-site/*`
- Production branch: `main`
- Automatic production and preview deployments: disabled
- Custom domain: `docs.subminer.moe` attached to Production
- Deployment path: GitHub Actions direct upload with Wrangler
Cloudflare Pages watch paths use a single `*` wildcard for monorepo subdirectories. `docs-site/*` matches nested files under the docs site; `docs-site/**` can cause docs-only pushes to be skipped.
The public docs root is stable-only:
- `/` serves the latest stable release docs.
- `/main/` serves development docs from `main` and is marked `noindex,follow`.
- `/v/<version>/` serves stable release archives.
- Prerelease tags do not update the docs site.
Keep Cloudflare Git auto-deploy disabled. The production deploy is `.github/workflows/docs-pages.yml`, which uploads `.tmp/docs-versioned-site` with `--branch main` so tag-triggered runs update Production instead of creating preview deployments.
+2
View File
@@ -4,6 +4,8 @@ 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:
+13 -5
View File
@@ -3,6 +3,13 @@
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/).
@@ -15,9 +22,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** — SubMiner runs a local AnkiConnect-compatible proxy and intercepts card creation instantly. Recommended when possible.
**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.
**Polling mode** (default) — SubMiner polls AnkiConnect every few seconds for newly added cards. Simpler setup, but with a short delay (~3 seconds).
**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).
Use proxy mode if you want immediate enrichment. Use polling mode if your Yomitan instance is external (browser-based) or you prefer minimal configuration.
@@ -147,13 +154,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, with configurable padding added before and after.
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.
```jsonc
"ankiConnect": {
"media": {
"generateAudio": true,
"audioPadding": 0.5, // seconds before and after subtitle timing
"audioPadding": 0, // optional seconds before and after subtitle timing
"maxMediaDuration": 30 // cap total duration in seconds
}
}
@@ -322,6 +329,7 @@ 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",
@@ -334,7 +342,7 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
"imageType": "static",
"imageFormat": "jpg",
"imageQuality": 92,
"audioPadding": 0.5,
"audioPadding": 0,
"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 26 properties), 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 mpv subtitle/playback properties via `observe_property`), 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.
+24 -12
View File
@@ -1,6 +1,8 @@
# Character Dictionary
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.
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.
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.
@@ -88,22 +90,27 @@ 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. Matched tokens are flagged `isNameMatch: true` and forwarded to the renderer.
4. The renderer applies the name-match highlight color (default: `#f5bde6`).
4. If `subtitleStyle.nameMatchEnabled` is enabled, the renderer applies the name-match highlight color (default: `#f5bde6`).
5. 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.
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` | `true` | Toggle character-name highlighting |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
| Option | Default | Description |
| -------------------------------------- | --------- | ----------------------------------------- |
| `subtitleStyle.nameMatchEnabled` | `false` | Toggle character-name highlighting |
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits beside names |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
## Dictionary Entries
Each character entry in the Yomitan dictionary includes structured content:
- **Name** — native (Japanese) and romanized forms
- **Name** — the matched Japanese name form
- **Known names** — generated non-honorific Japanese aliases for that character, excluding raw romanized/English aliases from lookup results
- **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)
@@ -167,10 +174,13 @@ 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, 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.
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.
Use the in-app selector or CLI to pin the correct AniList media for the whole series:
- In-app: open the selector with `Ctrl/Cmd+Alt+A` or `--open-character-dictionary`, 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"
@@ -186,7 +196,7 @@ SubMiner.AppImage --dictionary-select --dictionary-anilist-id 21355 --dictionary
subminer app --open-character-dictionary
```
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.
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.
## File Structure
@@ -205,7 +215,7 @@ character-dictionaries/
m170942-va67890.jpg # Voice actor portrait
```
**Snapshot format** (v15): each snapshot contains the media ID, title, entry count, timestamp, an array of Yomitan term entries, and base64-encoded images.
**Snapshot format** (v16): 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:
@@ -228,7 +238,8 @@ merged.zip
| `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` | `true` | Toggle character-name highlighting in subtitles |
| `subtitleStyle.nameMatchEnabled` | `false` | Toggle character-name highlighting in subtitles |
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits beside matched names |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for character-name matches |
## Reference Implementation
@@ -251,8 +262,9 @@ If you work with visual novels or want a standalone dictionary generator indepen
## Troubleshooting
- **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.
- **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.
- **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 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`.
- **Wrong characters showing:** Open the in-app character dictionary selector (`--open-character-dictionary`), edit the search title, and select the right AniList entry. You can also 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.
+165 -108
View File
@@ -4,9 +4,11 @@ outline: [2, 3]
# Configuration
Settings are stored in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc` when `XDG_CONFIG_HOME` is unset).
On Windows, the default path is `%APPDATA%\SubMiner\config.jsonc`.
When both files exist, SubMiner prefers `config.jsonc` over `config.json`.
<script setup>
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
@@ -35,9 +37,38 @@ For most users, start with this minimal configuration:
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.
The Settings window groups options by workflow instead of mirroring the raw config-file shape:
- Appearance
- Behavior
- Mining & Anki
- Playback & Sources
- Input
- Integrations
- Tracking & App
- Advanced
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, 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.
Saving validates the candidate config before writing. Live-reloadable changes are applied immediately; other changes return a restart-required banner in the window.
## Configuration File
See [config.example.jsonc](/config.example.jsonc) for a comprehensive example configuration file with all available options, default values, and detailed comments. Only include the options you want to customize in your config file.
The Settings window writes to `config.jsonc` directly, so most users do not need to edit the file by hand. The config file and the option reference below are provided for advanced use, scripting, or cases where you prefer editing config directly.
Settings are stored in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc` when `XDG_CONFIG_HOME` is unset).
On Windows, the default path is `%APPDATA%\SubMiner\config.jsonc`.
When both files exist, SubMiner prefers `config.jsonc` over `config.json`.
See [config.example.jsonc](/config.example.jsonc) for a comprehensive example with all available options, default values, and detailed comments. Only include the options you want to customize in your config file.
Generate a fresh default config from the centralized config registry:
@@ -70,7 +101,28 @@ Hot-reloadable fields:
- `keybindings`
- `shortcuts`
- `secondarySub.defaultMode`
- `ankiConnect.ai`
- `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.
@@ -78,6 +130,7 @@ Restart-required changes:
- Any other config sections still require restart.
- Shared top-level `ai` provider settings still require restart.
- AnkiConnect transport/proxy/media/deck/tag fields still require restart unless listed above.
- SubMiner shows an on-screen/system notification listing restart-required sections when they change.
### Configuration Options Overview
@@ -120,14 +173,14 @@ The configuration file includes several main sections:
**External Integrations**
- [**Jimaku**](#jimaku) - Jimaku API configuration and defaults
- [**Auto Subtitle Sync**](#auto-subtitle-sync) - Sync current subtitle with `alass`/`ffsubsync`
- [**Subtitle Sync**](#subtitle-sync) - Sync current subtitle with `alass`/`ffsubsync`
- [**AniList**](#anilist) - Optional post-watch progress updates
- [**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 and window launch mode
- [**MPV Launcher**](#mpv-launcher) - mpv executable path, profile, and window launch mode
- [**YouTube Playback Settings**](#youtube-playback-settings) - Defaults for YouTube subtitle loading
- [**Updates**](#updates) - Automatic update checks, notifications, and prerelease testing
@@ -177,22 +230,15 @@ Control whether the overlay automatically becomes visible when it connects to mp
```json
{
"auto_start_overlay": false
"auto_start_overlay": true
}
```
| Option | Values | Description |
| -------------------- | --------------- | ------------------------------------------------------ |
| `auto_start_overlay` | `true`, `false` | Auto-show overlay on mpv connection (default: `false`) |
| Option | Values | Description |
| -------------------- | --------------- | ----------------------------------------------------- |
| `auto_start_overlay` | `true`, `false` | Auto-show overlay on mpv connection (default: `true`) |
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`
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.
On Windows, packaged plugin installs also rewrite the plugin socket path to `\\.\pipe\subminer-socket`.
@@ -207,7 +253,7 @@ Control which startup warmups run in the background versus deferring to first re
"mecab": true,
"yomitanExtension": true,
"subtitleDictionaries": true,
"jellyfinRemoteSession": true
"jellyfinRemoteSession": false
}
}
```
@@ -220,11 +266,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 everything (`true` for all toggles, `lowPowerMode: false`). Setting a warmup toggle to `false` defers that work until first usage.
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.
### WebSocket Server
The overlay includes a built-in WebSocket server that broadcasts subtitle text to connected clients (such as texthooker-ui) for external processing.
The overlay includes a built-in WebSocket server that broadcasts plain subtitle text to connected clients for external processing.
For endpoint details, payload examples, and client patterns, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
@@ -297,57 +343,68 @@ See `config.example.jsonc` for detailed configuration options.
```json
{
"subtitleStyle": {
"fontFamily": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"fontSize": 35,
"fontColor": "#cad3f5",
"fontWeight": "600",
"lineHeight": 1.35,
"letterSpacing": "-0.01em",
"wordSpacing": 0,
"fontKerning": "normal",
"textRendering": "geometricPrecision",
"textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)",
"fontStyle": "normal",
"backgroundColor": "transparent",
"backdropFilter": "blur(6px)",
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"font-size": "35px",
"font-weight": "600",
"line-height": "1.35",
"letter-spacing": "-0.01em",
"word-spacing": "0",
"font-kerning": "normal",
"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)"
},
"secondary": {
"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"
"backgroundColor": "transparent",
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"font-size": "24px",
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)"
}
}
}
}
```
| 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"`) |
| `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`) |
| `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 |
| `nPlusOneColor` | string | Existing n+1 highlight color (default: `#c6a0f6`) |
| `knownWordColor` | string | Existing known-word highlight color (default: `#a6da95`) |
| `jlptColors` | object | JLPT level underline colors object (`N1`..`N5`) |
| `secondary` | object | Override any of the above for secondary subtitles (optional) |
| 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 (default: `"transparent"`); `hoverBackground` is accepted as an alias |
| `nameMatchEnabled` | boolean | Enable subtitle token coloring for matches from the SubMiner character dictionary (`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`) |
| `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
`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`.
@@ -364,10 +421,11 @@ In `single` mode all highlights use `singleColor`; in `banded` mode tokens map t
Character-name highlighting is separate from N+1 and frequency highlighting:
- `nameMatchEnabled` controls whether SubMiner 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.
- `nameMatchColor` sets the highlight color for those matched character names.
- Matches come from the bundled SubMiner character dictionary, including AniList-synced merged dictionaries when enabled.
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.
Secondary subtitle defaults: `fontFamily: "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP"`, `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.
@@ -382,9 +440,9 @@ Configure the parsed-subtitle sidebar modal.
"autoOpen": false,
"layout": "overlay",
"toggleKey": "Backslash",
"pauseVideoOnHover": false,
"pauseVideoOnHover": true,
"autoScroll": true,
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif",
"fontFamily": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"fontSize": 16
}
}
@@ -396,7 +454,7 @@ Configure the parsed-subtitle sidebar modal.
| `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 |
| `pauseVideoOnHover` | boolean | Pause playback while hovering the sidebar cue list (`true` by default) |
| `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`) |
@@ -422,7 +480,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` | `#a6e3a1` | JLPT N4 underline color |
| `N4` | `#8bd5ca` | JLPT N4 underline color |
| `N5` | `#8aadf4` | JLPT N5 underline color |
**Image Quality Notes:**
@@ -794,8 +852,7 @@ Palette controls:
### Shared AI Provider
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.
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.
```json
{
@@ -803,21 +860,22 @@ Anki reads this provider directly. Legacy subtitle fallback keeps the same provi
"enabled": false,
"apiKey": "",
"apiKeyCommand": "",
"model": "openai/gpt-4o-mini",
"baseUrl": "https://openrouter.ai/api",
"requestTimeoutMs": 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`) |
| Option | Values | Description |
| ------------------ | -------------------- | ------------------------------------------------------------------------------------ |
| `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`) |
SubMiner uses the shared provider for:
@@ -834,7 +892,7 @@ Enable automatic Anki card creation and updates with media generation:
"url": "http://127.0.0.1:8765",
"pollingRate": 3000,
"proxy": {
"enabled": false,
"enabled": true,
"host": "127.0.0.1",
"port": 8766,
"upstreamUrl": "http://127.0.0.1:8765"
@@ -866,7 +924,7 @@ Enable automatic Anki card creation and updates with media generation:
"animatedMaxWidth": 640,
"animatedMaxHeight": 360,
"animatedCrf": 35,
"audioPadding": 0.5,
"audioPadding": 0,
"fallbackDuration": 3,
"maxMediaDuration": 30
},
@@ -928,7 +986,7 @@ This example is intentionally compact. The option table below documents availabl
| `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.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 per `behavior.mediaInsertMode`; manual clipboard updates always replace generated sentence audio (default: `true`) |
@@ -937,11 +995,10 @@ This example is intentionally compact. The option table below documents availabl
| `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.color` | hex color string | Text color for tokens already found in the local known-word cache (default: `"#a6da95"`). |
| `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.nPlusOne` | hex color string | Text color for the single target token to study when exactly one unknown candidate exists in a sentence (default: `"#c6a0f6"`). |
| `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`) |
@@ -983,13 +1040,14 @@ Known-word cache policy:
- Initial sync runs when the integration starts if the cache is missing or stale.
- `ankiConnect.knownWords.refreshMinutes` controls the minimum time between refreshes; between refreshes, cached words are reused without querying Anki.
- `ankiConnect.nPlusOne.nPlusOne` sets the color for the single target token when exactly one eligible unknown word exists.
- `subtitleStyle.nPlusOneColor` sets the color for the single target token when exactly one eligible unknown word exists.
- `ankiConnect.nPlusOne.minSentenceWords` sets the minimum token count required in a sentence for N+1 highlighting (default: `3`).
- `ankiConnect.knownWords.color` sets the known-word highlight color for tokens already in Anki.
- `subtitleStyle.knownWordColor` sets the known-word highlight color for tokens already in Anki.
- `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 `ankiConnect.knownWords.matchMode` 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.
- 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.
@@ -1022,12 +1080,12 @@ To refresh roughly once per day, set:
`deleteDuplicateInAuto` controls whether `auto` mode deletes the duplicate after merge (default: `true`). In `manual` mode, the popup asks each time whether to delete the duplicate.
When the manual merge popup opens, SubMiner pauses playback and closes any open Yomitan popup first so the merge flow can take focus.
<video controls playsinline preload="metadata" poster="/assets/kiku-integration-poster.jpg" style="width: 100%; max-width: 960px;">
<source :src="'/assets/kiku-integration.webm'" type="video/webm" />
<video controls playsinline preload="metadata" :poster="withBase('/assets/kiku-integration-poster.jpg')" style="width: 100%; max-width: 960px;">
<source :src="withBase('/assets/kiku-integration.webm')" type="video/webm" />
Your browser does not support the video tag.
</video>
<a :href="'/assets/kiku-integration.webm'" target="_blank" rel="noreferrer">Open demo in a new tab</a>
<a :href="withBase('/assets/kiku-integration.webm')" target="_blank" rel="noreferrer">Open demo in a new tab</a>
## External Integrations
@@ -1051,17 +1109,16 @@ 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.
### Auto Subtitle Sync
### Subtitle Sync
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.
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).
- [`alass`](https://github.com/kaegi/alass) — fast, audio-independent sync using a secondary subtitle as reference
- [`ffsubsync`](https://github.com/smacke/ffsubsync) — audio-based sync using the video file as reference (fallback)
- [`ffsubsync`](https://github.com/smacke/ffsubsync) — audio-based sync using the video file as reference
```json
{
"subsync": {
"defaultMode": "auto",
"alass_path": "",
"ffsubsync_path": "",
"ffmpeg_path": "",
@@ -1070,13 +1127,12 @@ Sync the active subtitle track using `alass` (preferred) or `ffsubsync`. Both ar
}
```
| 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>`. |
| 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>`. |
Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
Customize it there, or set it to `null` to disable.
@@ -1200,7 +1256,6 @@ 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"],
@@ -1215,21 +1270,17 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
| `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.
Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted storage after login/setup. The legacy `jellyfin.accessToken`, `jellyfin.userId`, `jellyfin.clientName`, `jellyfin.deviceId`, `jellyfin.clientVersion`, and `jellyfin.remoteControlDeviceName` config keys are not resolver-backed settings in the current runtime. 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.
- On Linux, token storage defaults to `gnome-libsecret` for `safeStorage`. Override with `--password-store=<backend>` on launcher/app invocations when needed.
@@ -1246,6 +1297,8 @@ See [Jellyfin Integration](/jellyfin-integration) for the full setup and cast-to
Jellyfin remote auto-connect runs only when all three are `true`: `jellyfin.enabled`, `jellyfin.remoteControlEnabled`, and `jellyfin.remoteControlAutoConnect`.
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.
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.
### Discord Rich Presence
@@ -1397,12 +1450,13 @@ Usage notes:
### MPV Launcher
Configure the mpv executable and window state for SubMiner-managed mpv launches (launcher playback, Windows `--launch-mpv`, and Jellyfin idle mpv startup):
Configure the mpv executable, profile, and window state for SubMiner-managed mpv launches (launcher playback, Windows `--launch-mpv`, and Jellyfin idle mpv startup):
```json
{
"mpv": {
"executablePath": "",
"profile": "",
"launchMode": "normal"
}
}
@@ -1411,8 +1465,11 @@ Configure the mpv executable and window state for SubMiner-managed mpv launches
| 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.
+18 -16
View File
@@ -1,8 +1,10 @@
# Feature Demos
Short recordings of SubMiner's key features and integrations from real playback sessions.
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.
<script setup>
import { withBase } from 'vitepress';
const v = '20260301-1';
</script>
@@ -10,11 +12,11 @@ const v = '20260301-1';
Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner automatically attaches the sentence, a timing-accurate audio clip, a screenshot, and a translation.
<video controls playsinline preload="metadata" :poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`">
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
<video controls playsinline preload="metadata" :poster="withBase(`/assets/minecard-poster.jpg?v=${v}`)">
<source :src="withBase(`/assets/minecard.webm?v=${v}`)" type="video/webm" />
<source :src="withBase(`/assets/minecard.mp4?v=${v}`)" type="video/mp4" />
<a :href="withBase(`/assets/minecard.webm?v=${v}`)" target="_blank" rel="noreferrer">
<img :src="withBase(`/assets/minecard.webp?v=${v}`)" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
</a>
</video>
@@ -23,11 +25,11 @@ Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner aut
## Subtitle Download & Sync
Search and download subtitles from Jimaku, then automatically synchronize them with alass or ffsubsync — all from within SubMiner.
Search and download subtitles from Jimaku, then retime them with alass or ffsubsync — all from within SubMiner.
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/subtitle-sync-poster.jpg?v=${v}`">
<source :src="`/assets/demos/subtitle-sync.webm?v=${v}`" type="video/webm" />
<source :src="`/assets/demos/subtitle-sync.mp4?v=${v}`" type="video/mp4" />
<!-- <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" />
<source :src="withBase(`/assets/demos/subtitle-sync.mp4?v=${v}`)" type="video/mp4" />
</video> -->
::: info VIDEO COMING SOON
@@ -37,9 +39,9 @@ Search and download subtitles from Jimaku, then automatically synchronize them w
Browse your Jellyfin library, cast to devices, and launch playback directly from SubMiner. Watch progress syncs back to your Jellyfin server.
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/jellyfin-poster.jpg?v=${v}`">
<source :src="`/assets/demos/jellyfin.webm?v=${v}`" type="video/webm" />
<source :src="`/assets/demos/jellyfin.mp4?v=${v}`" type="video/mp4" />
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/jellyfin-poster.jpg?v=${v}`)">
<source :src="withBase(`/assets/demos/jellyfin.webm?v=${v}`)" type="video/webm" />
<source :src="withBase(`/assets/demos/jellyfin.mp4?v=${v}`)" type="video/mp4" />
</video> -->
::: info VIDEO COMING SOON
@@ -49,9 +51,9 @@ Browse your Jellyfin library, cast to devices, and launch playback directly from
Open subtitles in an external texthooker page for use with browser-based tools and extensions alongside the overlay.
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/texthooker-poster.jpg?v=${v}`">
<source :src="`/assets/demos/texthooker.webm?v=${v}`" type="video/webm" />
<source :src="`/assets/demos/texthooker.mp4?v=${v}`" type="video/mp4" />
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/texthooker-poster.jpg?v=${v}`)">
<source :src="withBase(`/assets/demos/texthooker.webm?v=${v}`)" type="video/webm" />
<source :src="withBase(`/assets/demos/texthooker.mp4?v=${v}`)" type="video/mp4" />
</video> -->
::: info VIDEO COMING SOON
+28 -14
View File
@@ -68,10 +68,15 @@ 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 in `~/.config/mpv/script-opts/subminer.conf`:
dev binary path with `mpv.subminerBinaryPath` in your SubMiner config. The launcher injects it into
the mpv plugin at runtime:
```ini
binary_path=/absolute/path/to/SubMiner/scripts/subminer-dev.sh
```json
{
"mpv": {
"subminerBinaryPath": "/absolute/path/to/SubMiner/scripts/subminer-dev.sh"
}
}
```
## Testing
@@ -113,6 +118,14 @@ bun run docs:test
bun run docs:build
```
For production docs routing, run the versioned build:
```bash
bun run docs:build:versioned
```
The versioned build writes `.tmp/docs-versioned-site` with latest stable docs at `/`, development docs at `/main/`, and stable archives under `/v/<version>/`. Prerelease tags are skipped. Public assets from `docs-site/public/assets` are shared from root `/assets/` so large demo media is not duplicated into every version archive; generated VitePress CSS and JS assets stay under each version route. Stale `.tmp/docs-versioned-archive-cache` generations are pruned after a successful build, and intermediate `.tmp/docs-versioned-build` workspaces are removed.
Focused commands:
```bash
@@ -154,6 +167,7 @@ bun run format:check:src
- `make pretty` runs the maintained Prettier allowlist only (`format:src`).
- `bun run format:check:src` checks the same scoped set without writing changes.
- `bun run format` remains the broad repo-wide Prettier command; use it intentionally.
## Config Generation
```bash
@@ -197,17 +211,17 @@ Use Cloudflare's single `*` wildcard syntax for watch paths. `docs-site/*` cover
Run `make help` for a full list of targets. Key ones:
| Target | Description |
| ---------------------- | ---------------------------------------------------------------- |
| `make build` | Build platform package for detected OS |
| `make build-launcher` | Generate Bun launcher wrapper at `dist/launcher/subminer` |
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
| `make deps` | Install JS dependencies (root + stats + texthooker-ui) |
| `make pretty` | Run scoped Prettier formatting for maintained source/config files |
| `make generate-config` | Generate default config from centralized registry |
| `make build-linux` | Convenience wrapper for Linux packaging |
| `make build-macos` | Convenience wrapper for signed macOS packaging |
| `make build-macos-unsigned` | Convenience wrapper for unsigned macOS packaging |
| Target | Description |
| --------------------------- | ----------------------------------------------------------------- |
| `make build` | Build platform package for detected OS |
| `make build-launcher` | Generate Bun launcher wrapper at `dist/launcher/subminer` |
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
| `make deps` | Install JS dependencies (root + stats + texthooker-ui) |
| `make pretty` | Run scoped Prettier formatting for maintained source/config files |
| `make generate-config` | Generate default config from centralized registry |
| `make build-linux` | Convenience wrapper for Linux packaging |
| `make build-macos` | Convenience wrapper for signed macOS packaging |
| `make build-macos-unsigned` | Convenience wrapper for unsigned macOS packaging |
## Contributor Notes
+14 -4
View File
@@ -8,6 +8,7 @@ const installationContents = readFileSync(new URL('./installation.md', import.me
const mpvPluginContents = readFileSync(new URL('./mpv-plugin.md', import.meta.url), 'utf8');
const developmentContents = readFileSync(new URL('./development.md', import.meta.url), 'utf8');
const changelogContents = readFileSync(new URL('./changelog.md', import.meta.url), 'utf8');
const docsPackageContents = readFileSync(new URL('./package.json', import.meta.url), 'utf8');
const ankiIntegrationContents = readFileSync(
new URL('./anki-integration.md', import.meta.url),
'utf8',
@@ -37,13 +38,13 @@ test('docs reflect current launcher and release surfaces', () => {
expect(mpvPluginContents).toContain('\\\\.\\pipe\\subminer-socket');
expect(readmeContents).toContain('Root directory: `docs-site`');
expect(readmeContents).toContain('Build output directory: `.vitepress/dist`');
expect(readmeContents).toContain('Build watch paths: `docs-site/*`');
expect(readmeContents).toContain('Automatic production and preview deployments: disabled');
expect(readmeContents).toContain('/main/');
expect(readmeContents).toContain('GitHub Actions direct upload with Wrangler');
expect(developmentContents).not.toContain('../subminer-docs');
expect(developmentContents).toContain('bun run docs:build');
expect(developmentContents).toContain('bun run docs:test');
expect(developmentContents).toContain('Build watch paths: `docs-site/*`');
expect(developmentContents).toContain('bun run docs:build:versioned');
expect(developmentContents).not.toContain('test:subtitle:dist');
expect(developmentContents).toContain('bun run build:win');
@@ -57,6 +58,15 @@ test('docs reflect current launcher and release surfaces', () => {
expect(changelogContents).toContain('v0.5.1 (2026-03-09)');
});
test('docs dev server links version navigation to local dev routes', () => {
expect(docsPackageContents).toContain('scripts/build-versioned-docs.ts');
expect(docsPackageContents).toContain(
'SUBMINER_DOCS_VERSION_LINK_ORIGIN=local bun run ../scripts/build-versioned-docs.ts',
);
expect(docsPackageContents).toContain('SUBMINER_DOCS_VERSION_LINK_ORIGIN=local');
expect(docsPackageContents).toContain('SUBMINER_DOCS_VERSION_MANIFEST');
});
test('docs changelog keeps the current minor release headings aligned with the root changelog', () => {
const docsHeadings = extractCurrentMinorHeadings(changelogContents);
expect(docsHeadings.length).toBeGreaterThan(0);
+6
View File
@@ -2,8 +2,14 @@
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

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