Compare commits

..

86 Commits

Author SHA1 Message Date
sudacode 6d56f72755 feat(plugin): route restart feedback through playback feedback surface
- Show overlay loading OSD spinner during restart and keep it alive until overlay reports ready
- Route restart progress/completion through notify_playback_feedback so overlay/both modes display in overlay
- Delay "Restarted successfully" until show-visible-overlay completes
- Add test-plugin-restart-feedback.lua covering the restart feedback flow
2026-06-10 01:55:16 -07:00
sudacode a26f10fff0 docs: document --playback-feedback CLI flag
- Add flag to usage.md CLI reference and notes section
- Add cross-reference in configuration.md notification surface section
2026-06-10 01:09:49 -07:00
sudacode 50b6226a7b feat(aniskip): route skip prompts and results through playback feedback
- Add --playback-feedback CLI flag; overlay/both modes show AniSkip hint and skip result on overlay instead of raw OSD
- Wire notify_playback_feedback through process.lua and the full deps chain to showPlaybackFeedback
- Fallback to OSD when binary unavailable or osd_messages opt enabled
2026-06-10 00:12:02 -07:00
sudacode 2a4fdb74e4 fix(notifications): stabilize overlay startup and macOS hover passthroug
- Skip re-append for cards already in stack to avoid replaying enter animation
- Track enter animation end to remove `entering` class after first render
- Gate mouseenter interactive state on cards with explicit actions only
- Bind interactive hover only to action buttons and their close buttons
- Add regression tests for passive vs action-bearing notification hover
2026-06-10 00:09:06 -07:00
sudacode 5cfb2c75ba fix(anki): honor runtime AnkiConnect URL override in Open in Anki fallba
- Use getEffectiveAnkiConnectConfig for fallback client URL resolution
- Fall back to DEFAULT_CONFIG.ankiConnect.url when effective URL is unset
2026-06-10 00:09:06 -07:00
sudacode a15fb39847 feat(notifications): add Open in Anki action and in-place progress updat
- Add openNoteInBrowser to AnkiConnectClient via guiBrowse IPC
- Add Open in Anki action button to mined-card overlay notifications and history entries
- Fall back to a direct AnkiConnectClient when the live integration is unavailable
- Embed notification images as base64 data URIs so history panel shows thumbnails
- Update same-id progress notifications in place to avoid spinner flicker
- Thread noteId through IPC overlay notification action payload
2026-06-10 00:09:06 -07:00
sudacode ce52973765 fix(shortcuts): use CommandOrControl+N for notification history toggle
- Replaces Ctrl+N default with CommandOrControl+N so the binding works on macOS (Cmd+N) and Windows/Linux (Ctrl+N)
- Updates config example, docs, changelog, and default config to match
- Adds !important to notification stack z-index to prevent stacking-context overrides
2026-06-10 00:09:05 -07:00
sudacode 361c0f1334 fix(notifications): gate overlay delivery on visible overlay; default to
- Default notificationType fallback changed from 'overlay' to 'osd'
- isVisibleOverlayContentReady guards on overlay visible + window ready
- All overlay hide paths dismiss loading OSD notification
- notifyConfiguredStatus falls back to desktop when overlay not ready
- anilist deps builder preserves undefined optional callbacks as undefined
- settingsEnumValues field added to ConfigOptionRegistryEntry
- Drop !important from z-index; lower yomitan popup z-index below notification stack
2026-06-10 00:09:05 -07:00
sudacode 740a5b07cb fix(update): separate in-flight dedup keys for manual check vs install
- manual:check and manual:install now tracked independently; install no longer reuses a check's in-flight promise
- add toggleNotificationHistory (Ctrl+N) shortcut to config example and docs
2026-06-10 00:09:05 -07:00
sudacode cf16587547 feat(overlay): add loading OSD spinner and queue notifications until ren
- Show mpv OSD spinner from start-file until subminer-overlay-loading-ready; force-shown for visible-overlay startup regardless of osd_messages setting
- Gate non-macOS overlay visibility on content-ready so first subtitle line is immediately hoverable and clickable
- Queue startup notifications in main process until overlay window finishes loading; upsert progress cards by id to avoid cold-start floods
- Defer background warmups until after overlay runtime init so queued notifications can deliver promptly
- Preserve character dictionary checking/building/importing/ready phases as distinct history entries; route building and importing to system notifications when notificationType is both
2026-06-10 00:09:05 -07:00
sudacode 8111deac44 feat(notifications): add notification history panel and overlay UX fixes
- New toggleNotificationHistory (Ctrl+N) session-scoped history panel; slides in from same edge as notification stack
- Overlay error/recovery toast follows notifications.overlayPosition; stack and history side seeded at startup
- Cold managed background startup initializes tray and visible overlay shell before tokenization warmups finish
- Add Update button to overlay update-available notifications
- Fix Ctrl+S sentence-card flow: only Anki progress notification, no duplicate status toast
- Fix overlay notification close/actions clickability above subtitle bars on Linux
- Increase pause-until-ready default timeout from 15s to 30s
2026-06-10 00:07:57 -07:00
sudacode a34ec049a2 fix(startup): release autoplay gate before first subtitle line
- Send synthetic `__warm__` payload when no current subtitle exists so the gate can release without waiting for a subtitle event that can't fire while paused
- Visible-overlay readiness accepts `__warm__` once the overlay is content-ready, rejects it otherwise
- Autoplay gate self-retries via scheduled polling when signal target isn't ready, removing reliance on an external flush event
- Skip duplicate desktop notification when overlay or startup sequencer already delivered it
2026-06-10 00:06:24 -07:00
sudacode 1fe9bdc198 fix(startup): signal autoplay gate from subtitle resolve; dedupe dict sy
- Add onResolvedSubtitle callback to resolveCurrentSubtitleForRenderer so the startup overlay-ready gate fires after the initial subtitle resolves
- Guard scheduleCharacterDictionarySync behind a last-path check so duplicate MPV media-path events don't re-trigger sync for the same video
2026-06-10 00:06:24 -07:00
sudacode f675ef5b02 feat(notifications): auto-dismiss loading OSD on overlay visibility chan
- add dismissOverlayLoadingOsd dep and call it on hide/show paths (macOS only)
- simplify notification card styles: remove accent bar, flatten gradient bg, tweak spacing
- fix test CSS path to use __dirname instead of process.cwd()
2026-06-10 00:06:24 -07:00
sudacode 486f682563 fix(notifications): reserve grid space for overlay thumbnail so it can't overlap text
The thumbnail was 100px wide but its grid column only reserved 56px, so on
macOS the image spilled ~44px into the content column and overlapped the
title/body. Reserve a minmax(0, 100px) track for the image and make the
image fluid (width: 100%; max-width: 100px; aspect-ratio) so it shrinks to
fit on narrow notifications instead of overlapping the text.
2026-06-10 00:06:24 -07:00
sudacode b177a2fb4d feat(notifications): add enter/leave animations and DOM-reuse for overla
- Direction-aware slide animations (right/left/top) on enter and leave
- Cards keyed by id so re-renders reuse elements; enter animation only plays once
- Exit animates via .leaving class then removes; fallback timer guards missing animationend
- Respects prefers-reduced-motion
2026-06-10 00:06:24 -07:00
sudacode d07a3b7d55 fix(notifications): widen overlay notification thumbnail to 100px 2026-06-10 00:06:24 -07:00
sudacode e84825754b fix(notifications): show thumbnail image in overlay mined-card notificat
- Share generated notification icon between overlay and system notification paths
- Add `image` field to OverlayNotificationPayload; render as IMG with has-image layout
- Widen overlay stack to 420px; enlarge card padding and min-height for image variant
- Show OSD message after successful anilist retry when attempt key already handled
2026-06-10 00:06:24 -07:00
sudacode 88fa9ba8b5 refactor(notifications): extract routing predicates and fix pre-overlay
- Extract shouldShowOsd/Overlay/Desktop into notification-routing.ts (was duplicated in 3 files)
- Add resolveOverlayReadinessNotificationType: preserves system channel when overlay not ready (both→osd-system, system→system, overlay→osd)
- Route overlay loading status through showConfiguredStatusNotification instead of raw OSD
2026-06-10 00:06:24 -07:00
sudacode 144373db52 feat(notifications): add overlay notifications with position config
- Add Catppuccin Macchiato overlay notification stack with 3s transient timeout
- Add `notifications.overlayPosition` config (top-left | top | top-right)
- Route startup tokenization and subtitle annotation status through configured surfaces
- Deduplicate rapid subtitle mode toggle notifications
- Change `both` to mean overlay + system; add `osd-system` as legacy alias for old behavior
- Keep `osd`/`osd-system` as config-file-only legacy values; Settings UI offers overlay/system/both/none
2026-06-10 00:06:24 -07:00
sudacode c09d009a3e docs: add ani-skip to credits table in README 2026-06-10 00:01:51 -07:00
sudacode 2007e28be8 feat(aniskip): move intro detection from mpv plugin to app runtime (#117) 2026-06-09 23:55:43 -07:00
sudacode d5bfdcae7b fix(stats): repair legacy combined-season anime rows on startup (#116) 2026-06-09 12:41:07 -07:00
sudacode 311f1e8ee5 feat(stats): speed up session maintenance and improve stats UI (#111) 2026-06-08 02:20:52 -07:00
sudacode e6a16a069b fix(anilist): mark entry completed when final episode is reached (#115) 2026-06-07 23:45:09 -07:00
Autumn (Bee) af67c53dd6 [codex] Restart Jellyfin remote session after setup login (#112) 2026-06-06 11:52:16 -07:00
sudacode ea79e331fa build: make deps initializes submodules before installing JS deps
- Add `submodules` target that runs `git submodule update --init --recursive`
- `deps` now depends on `submodules` so fresh checkouts work out of the box
- Update docs to replace manual install steps with `make deps`
- Fix Windows build-from-source steps to include stats and submodule init
2026-06-06 01:25:01 -07:00
sudacode ee89b0c8a9 feat(release): add contributor attribution to release notes (#114) 2026-06-06 01:07:47 -07:00
sudacode f2fd58cd2b docs(changelog): require reconciled fragments, not just new ones (#113) 2026-06-06 00:55:34 -07:00
sudacode 1280a30216 chore(release): prepare 0.15.2 2026-06-02 23:45:03 -07:00
sudacode a80ed72b2d docs: replace em-dashes with hyphens across docs-site 2026-06-02 23:36:44 -07:00
sudacode 4cc6c12dc7 chore(vendor): update subminer-yomitan submodule (#109) 2026-06-02 00:37:45 -07:00
sudacode 425004879a fix(anki): align animated AVIF clip bounds to frame boundaries (#108) 2026-06-01 15:20:06 -07:00
sudacode 76f99e6518 fix(overlay): correct Hyprland fullscreen overlay alignment on Linux (#107) 2026-06-01 02:12:16 -07:00
sudacode f1e260e996 fix(overlay): fix macOS overlay interactivity and focus after autoplay (#106) 2026-06-01 01:34:27 -07:00
sudacode 54e90754ef chore: release 0.15.1 2026-05-31 22:40:40 -07:00
sudacode 487143802a feat(keybindings): add mouse button support for mpv keybindings (#103) 2026-05-31 22:22:38 -07:00
sudacode e6a004ab8b Fix Windows mpv shortcut attachment to background app (#105) 2026-05-31 21:46:00 -07:00
sudacode b510c54875 fix(overlay): restore mpv focus and pointer state on macOS (#104) 2026-05-31 21:25:04 -07:00
sudacode e1ea464bc9 fix(overlay): Linux X11/XWayland stacking, stale pause state, multi-copy selector (#101) 2026-05-31 20:59:18 -07:00
sudacode b46b8dfa41 chore: add issue forms and expand PR template (#100) 2026-05-30 23:50:00 -07:00
sudacode ca067a6ccf Add FUNDING.yml 2026-05-30 20:15:58 -07:00
sudacode d719b346e0 fix(overlay): use Lua dispatch syntax for Hyprland 0.55+ Lua configs (#99) 2026-05-29 00:13:31 -07:00
sudacode a1da3dcdc8 docs(troubleshooting): fix Hyprland rules, add character dictionary + see also
Rewrite Hyprland overlay window-rule guidance with current Lua (hl.window_rule)
config and legacy hyprland.conf syntax, and note SubMiner's automatic hyprctl
placement. Add a Character Dictionary troubleshooting section (no AniList auth
required) and a See Also index linking each feature's own troubleshooting page.
2026-05-28 23:53:07 -07:00
sudacode 9927ef1581 docs(character-dictionary): correct auth requirement and add portrait do
- AniList auth not required for character dictionary; uses public GraphQL
- Document nameMatchImagesEnabled and inline portrait behavior
- Clarify AniList auth is only for watch-progress sync
- Delete stale release/release-notes.md
2026-05-28 23:30:19 -07:00
sudacode 791c993870 docs: reformat changelog entries as nested bullet lists
- Convert flat prose entries in CHANGELOG.md and docs-site/changelog.md to bold headers + sub-bullets
- Scope artifact uploads in release/prerelease workflows to `latest*.yml` instead of `*.yml`
- Update release-notes and RELEASING docs to match
- Adjust workflow tests for new nested bullet format
2026-05-28 22:53:22 -07:00
sudacode 38dbce517c chore(release): 0.15.0 2026-05-28 19:46:05 -07:00
sudacode 889dc9c009 docs: reconcile docs-site with current config schema and defaults
- sidebar: migrate flat props to css object (font-family, color, bg, custom vars)
- frequencyDictionary.topX default: 1000 → 10000
- text-shadow default: updated to outline-style multi-shadow
- anki: reset ai model/prompt, imageMaxWidth/Height, animatedMaxHeight to 0; isLapis defaults
- troubleshooting: log default warn (not info), css["font-size"] usage
- shortcuts: add W markWatchedKey; clarify keybindings vs built-in overlay actions
- websocket: clarify all services off by default, fix enabled semantics
- usage: --anilist → --anilist-setup
- AGENTS.md: add Docs Upkeep trigger map, clarify CLAUDE.md symlink, expand PR notes
2026-05-28 19:21:58 -07:00
sudacode 097021072d chore(release): 0.15.0-beta.12 2026-05-28 02:16:55 -07:00
sudacode 91c8eb8faf feat(changelog): add nested bullet format for release notes
- Prompt now requests short top-level bullets with nested change/benefit/action sub-bullets in release-notes mode
- CHANGELOG mode keeps single-line bullets unchanged
- Tests assert new prompt constraints are present
2026-05-28 02:08:44 -07:00
sudacode eed0a6a243 feat: use cached annotations on subtitle change and skip pre-warmed cues (#97) 2026-05-28 00:50:41 -07:00
sudacode d33009d4a3 style: update subtitle text shadow, JLPT underlines, and frequency topX default (#96)
* style: update subtitle text shadow, JLPT underlines, and frequency topX

- Replace directional text-shadow with 4-corner outline shadow for sharper readability
- Increase JLPT level border-bottom from 2px to 3px; add drop-shadow filter
- Add margin-left 0.18em to character image token
- Raise frequencyDictionary topX default from 1000 to 10000

* docs: add subtitle style changelog fragment

* docs: update generated config examples

* style: wrap long textShadow strings and add blank line in CSS
2026-05-28 00:18:39 -07:00
sudacode 8d0535f3ca feat: add Anki deck dropdown with Yomitan auto-fill in settings (#95) 2026-05-27 23:13:43 -07:00
sudacode 75f9b8a803 chore: prepare 0.15.0-beta.11 prerelease 2026-05-27 16:42:04 -07:00
sudacode c30cce0a14 chore: consolidate changelog fragments for release
- Merge per-fix fragments into grouped topic files (jellyfin, overlay, anki, character-dictionary, etc.)
- Improve auto-update fragment wording for clarity
2026-05-27 14:56:49 -07:00
sudacode 3e6591e390 fix: include audio padding in animated AVIF source range (#94) 2026-05-27 14:06:48 -07:00
sudacode f033f87329 fix: ensure macOS mpv window helper is built and bundled correctly (#93) 2026-05-27 13:34:51 -07:00
sudacode 29cb8c7fb4 chore: release 0.15.0-beta.10 2026-05-27 01:54:52 -07:00
sudacode 1dcfed86ab fix: Kiku field grouping, frequency particles, sidebar media, Yomitan popup visibility (#91) 2026-05-27 01:40:48 -07:00
sudacode efe50ed1e4 docs: add startup flow diagram and document new config options (#90) 2026-05-26 23:51:36 -07:00
sudacode 5b44981688 docs: remove legacy option refs and modernize config docs (#89) 2026-05-26 18:15:23 -07:00
sudacode 2add95d541 fix(anilist): dedupe failures during retry cooldown and block dead-lette
- Ignore markFailure calls while an item is still within its retry backoff window
- Prevent enqueue from re-adding keys already in the dead-letter queue
2026-05-26 01:57:25 -07:00
sudacode f62fff2585 chore(release): 0.15.0-beta.9 2026-05-26 00:55:34 -07:00
sudacode 11c196821d Fix Windows mpv logging and add log export (#88) 2026-05-26 00:31:38 -07:00
sudacode 43ebc7d371 chore: prepare 0.15.0-beta.8 prerelease 2026-05-25 20:34:56 -07:00
sudacode 639e331f24 fix(character-dictionary): add surname honorifics for Japanese localized aliases (#87) 2026-05-25 20:12:27 -07:00
sudacode 78be72e32f chore: prepare 0.15.0-beta.7 prerelease 2026-05-25 18:37:03 -07:00
sudacode 3932e53ced feat(character-dictionary): add manager modal and scope name matching to current media (#86) 2026-05-25 18:29:20 -07:00
sudacode 097b619d71 fix: settings window z-order on Hyprland and Linux app detach (#85) 2026-05-25 13:21:38 -07:00
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
782 changed files with 50803 additions and 12110 deletions
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: [ksyasuda]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: sudacode
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+105
View File
@@ -0,0 +1,105 @@
name: Bug Report
description: Report something that is broken or behaving incorrectly
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to file a bug report! Please search [existing issues](https://github.com/ksyasuda/SubMiner/issues?q=is%3Aissue) first to avoid duplicates.
- type: textarea
id: what-happened
attributes:
label: What happened?
description: A clear description of the bug, including what you expected to happen instead.
placeholder: When I open the Yomitan popup, the overlay freezes...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Minimal, ordered steps that reliably trigger the bug.
placeholder: |
1. Launch `subminer`
2. Play a video in MPV
3. Hover a word and press ...
4. See error
validations:
required: true
- type: dropdown
id: area
attributes:
label: Affected area
description: Which part of SubMiner is affected?
options:
- Overlay / Yomitan popup
- Anki mining
- Subtitle annotations
- Subtitle sidebar
- Immersion tracking / stats
- Launcher / CLI
- MPV plugin
- Jellyfin integration
- Jimaku integration
- AniList integration
- YouTube integration
- Character dictionary
- WebSocket / texthooker API
- Configuration
- Documentation
- Other / not sure
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
options:
- Linux
- macOS
- Windows
validations:
required: true
- type: input
id: version
attributes:
label: SubMiner version
description: Output of `subminer --version`, or the release tag / commit you are running.
placeholder: v0.15.0
validations:
required: true
- type: input
id: compositor
attributes:
label: Compositor (Linux only)
description: SubMiner's overlay supports Hyprland and sway. Name yours (and version if known). Leave blank on macOS / Windows.
placeholder: Hyprland 0.55
validations:
required: false
- type: input
id: mpv-version
attributes:
label: MPV version
description: Output of `mpv --version` (first line).
placeholder: mpv 0.38.0
validations:
required: false
- type: textarea
id: logs
attributes:
label: Logs / console output
description: |
Relevant logs. For verbose output, run `electron . --dev --log-level debug`.
This will be rendered as code automatically — no backticks needed.
render: shell
validations:
required: false
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Documentation
url: https://docs.subminer.moe
about: Setup, configuration, and feature docs — check here before filing an issue.
- name: Troubleshooting guide
url: https://docs.subminer.moe/troubleshooting
about: Common problems and fixes (Hyprland rules, MPV detection, Anki connection, etc.).
@@ -0,0 +1,59 @@
name: Feature Request
description: Suggest a new feature or an improvement to an existing one
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for the idea! Please search [existing issues](https://github.com/ksyasuda/SubMiner/issues?q=is%3Aissue) first to avoid duplicates.
- type: textarea
id: problem
attributes:
label: Problem / motivation
description: What problem are you trying to solve? What is missing or frustrating today?
placeholder: When mining a card I have to manually switch to Anki because...
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: Describe the feature or change you'd like to see.
validations:
required: true
- type: dropdown
id: area
attributes:
label: Related area
description: Which part of SubMiner does this relate to?
options:
- Overlay / Yomitan popup
- Anki mining
- Subtitle annotations
- Subtitle sidebar
- Immersion tracking / stats
- Launcher / CLI
- MPV plugin
- Jellyfin integration
- Jimaku integration
- AniList integration
- YouTube integration
- Character dictionary
- WebSocket / texthooker API
- Configuration
- Documentation
- Other / new area
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Any workarounds you currently use or other approaches you've thought about.
validations:
required: false
+34 -1
View File
@@ -1,3 +1,36 @@
<!--
Thanks for contributing to SubMiner! Fill out the sections below.
Keep it short — a couple of sentences per section is fine.
-->
## Summary
<!-- What does this PR do and why? -->
## Type of change
<!-- Check all that apply. -->
- [ ] Bug fix
- [ ] New feature
- [ ] Refactor / internal
- [ ] Documentation
- [ ] Other
## Related issues
<!-- e.g. "Closes #123". Delete if none. -->
## How was this tested?
<!--
Describe verification. The default handoff gate is:
bun run typecheck && bun run test:fast && bun run test:env && bun run build && bun run test:smoke:dist
If docs-site/ changed, also: bun run docs:test && bun run docs:build
-->
## Checklist
- [ ] Added a changelog fragment in `changes/`, or this PR is labeled `skip-changelog`
- [ ] Reconciled current-outcome changelog fragment(s), or this PR is labeled `skip-changelog` (see [`changes/README.md`](../changes/README.md))
- [ ] Docs updated in the same PR if behavior, defaults, flags, shortcuts, ports, or APIs changed
- [ ] Relevant checks pass locally (typecheck, tests, build)
+5 -5
View File
@@ -148,7 +148,7 @@ jobs:
name: appimage
path: |
release/*.AppImage
release/*.yml
release/latest*.yml
release/*.blockmap
if-no-files-found: error
@@ -226,7 +226,7 @@ jobs:
path: |
release/*.dmg
release/*.zip
release/*.yml
release/latest*.yml
release/*.blockmap
if-no-files-found: error
@@ -279,7 +279,7 @@ jobs:
path: |
release/*.exe
release/*.zip
release/*.yml
release/latest*.yml
release/*.blockmap
if-no-files-found: error
@@ -353,7 +353,7 @@ jobs:
- name: Generate checksums
run: |
shopt -s nullglob
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/*.yml release/*.blockmap dist/launcher/subminer)
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer)
if [ "${#files[@]}" -eq 0 ]; then
echo "No release artifacts found for checksum generation."
exit 1
@@ -389,7 +389,7 @@ jobs:
release/*.exe
release/*.zip
release/*.tar.gz
release/*.yml
release/latest*.yml
release/*.blockmap
release/SHA256SUMS.txt
dist/launcher/subminer
+5 -5
View File
@@ -139,7 +139,7 @@ jobs:
name: appimage
path: |
release/*.AppImage
release/*.yml
release/latest*.yml
release/*.blockmap
build-macos:
@@ -216,7 +216,7 @@ jobs:
path: |
release/*.dmg
release/*.zip
release/*.yml
release/latest*.yml
release/*.blockmap
build-windows:
@@ -268,7 +268,7 @@ jobs:
path: |
release/*.exe
release/*.zip
release/*.yml
release/latest*.yml
release/*.blockmap
if-no-files-found: error
@@ -342,7 +342,7 @@ jobs:
- name: Generate checksums
run: |
shopt -s nullglob
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/*.yml release/*.blockmap dist/launcher/subminer)
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer)
if [ "${#files[@]}" -eq 0 ]; then
echo "No release artifacts found for checksum generation."
exit 1
@@ -396,7 +396,7 @@ jobs:
release/*.exe
release/*.zip
release/*.tar.gz
release/*.yml
release/latest*.yml
release/*.blockmap
release/SHA256SUMS.txt
dist/launcher/subminer
+19 -2
View File
@@ -13,6 +13,8 @@ Start here, then leave this file.
`docs-site/` is user-facing. Do not treat it as the canonical internal source of truth.
`CLAUDE.md` is a symlink to this file — there is one project instruction file, not two.
## Quick Start
- Init workspace: `git submodule update --init --recursive`
@@ -42,6 +44,20 @@ Start here, then leave this file.
- Runtime-compat / dist-sensitive: `bun run test:runtime:compat`
- Docs-only: `bun run docs:test`, then `bun run docs:build`
## Docs Upkeep
- Docs ship with the change, not after. If a change alters behavior, defaults, flags, shortcuts, ports, or APIs, update the matching docs in the same PR. Touching code without reconciling its docs is an incomplete change.
- Source of truth for config defaults is the generated `config.example.jsonc`. Never write a default value into prose you didn't read from it — and don't restate the same default across multiple docs; cite/link to one place so there's a single thing to update.
- Trigger map (touch left → update right):
- `src/config/definitions/**` (schema/defaults/template) → `bun run generate:config-example`, then reconcile `docs-site/configuration.md` + any feature doc that cites that default
- shortcuts/keybindings (`shortcuts.*`, `keybindings`, `stats.*Key`, `subtitleSidebar.toggleKey`, controller bindings) → `docs-site/shortcuts.md`
- CLI flags/subcommands (`src/cli/args.ts`, `launcher/**`) → `docs-site/usage.md` + relevant integration doc
- feature behavior (anki / jellyfin / jimaku / anilist / youtube / immersion / stats / websocket / sidebar / character-dictionary / annotations) → matching `docs-site/<feature>.md`
- architecture / IPC / workflow / internal process → internal `docs/` (system of record)
- feature set / requirements / install flow → `README.md`
- Removing or renaming a config key: grep `docs-site/` and `docs/` for the old key and any value it documented; legacy/hidden keys (`LEGACY_HIDDEN_CONFIG_PATHS`) should not appear in user docs as current settings.
- Verify after doc edits: `bun run verify:config-example` (if config touched), `bun run docs:test`, `bun run docs:build`.
## Sensitive Files
- Launcher source of truth: `launcher/*.ts`
@@ -52,7 +68,8 @@ Start here, then leave this file.
## Release / PR Notes
- User-visible PRs need one fragment in `changes/*.md`
- User-visible PRs need reconciled current-outcome fragment(s) in `changes/*.md` — format and rules in [`changes/README.md`](./changes/README.md) (`type` + `area` keys required; inspect existing same-PR fragments, then update/remove stale bullets or add only genuinely separate outcomes; apply the `skip-changelog` label to opt out)
- User-visible docs changes get a `type: docs` fragment
- CI enforces `bun run changelog:lint` and `bun run changelog:pr-check`
- PR review helpers:
- `gh pr view --json number,title,url --jq '"PR #\\(.number): \\(.title)\\n\\(.url)"'`
@@ -63,4 +80,4 @@ Start here, then leave this file.
- Use Codex background for long jobs; tmux only when persistence/interaction is required
- CI red: `gh run list/view`, rerun, fix, repeat until green
- TypeScript: keep files small; follow existing patterns
- Swift: use workspace helper/daemon; validate `swift build` + tests
- Only Swift is the `scripts/get-mpv-window-macos.swift` helper (macOS mpv window detection); validate via `bun test scripts/get-mpv-window-macos.test.ts`
+183 -4
View File
@@ -1,10 +1,189 @@
# Changelog
## v0.15.2 (2026-06-02)
### Changed
- Yomitan: Updated the bundled Yomitan build to the latest vendored revision.
### Fixed
- Anki - Animated AVIF: Clip timing no longer starts or ends early; word-audio lead-in and clip duration are now aligned to frame boundaries.
- Overlay (Hyprland): Fixed fullscreen overlay alignment - modal, stats, and sidebar content no longer shift below the mpv window.
- Overlay (macOS): Subtitle bars are now interactive immediately after autoplay starts with "wait for overlay to be ready" enabled, without requiring a manual click.
- Overlay (macOS): Fixed overlay, subtitles, and subtitle sidebar staying hidden after a modal closes until the user clicked the mpv window; focus is now restored to mpv when the last modal closes, so playback shortcuts and the overlay reappear correctly - including in native fullscreen.
## v0.15.1 (2026-05-31)
### Fixed
- **Linux Overlay Stacking**: Fixed the overlay intermittently dropping behind mpv on KDE Plasma and other non-Hyprland/Sway Wayland sessions; restored subtitle hover, pause-on-hover, and Yomitan lookups on X11/XWayland; the overlay now correctly layers above/below mpv based on fullscreen state, yields to foreground windows (Settings, Yomitan, AniList, etc.), and avoids startup flashes and fullscreen transition glitches.
- **Linux Overlay (Hyprland Lua)**: Fixed overlay placement on Hyprland 0.55+ when using a Lua-based config.
- **Manual Overlay Startup**: Fixed manual visible-overlay startup from mpv - now correctly attaches to playback, keeps the window bounds synced with mpv, and primes current subtitles before showing.
- **Playlist Transitions**: Reused the warm overlay when mpv advances to the next playlist item, avoiding a redundant tokenization pause and preserving visible subtitles across tracks.
- **macOS Overlay**: Fixed the visible subtitle overlay staying click-through after pause-until-ready releases playback; restored mpv focus after closing modal windows so subtitles and keybinds resume without clicking the player.
- **Mouse Keybindings**: Fixed keybinding capture and runtime handling for mouse buttons, including side buttons like `MBTN_BACK` and `MBTN_FORWARD`.
- **Windows mpv Shortcut**: Fixed the Windows `SubMiner mpv` shortcut so videos attach to an already-running background app instead of spawning a second process.
### Docs
- **Troubleshooting**: Updated Hyprland overlay docs with current Lua (`hl.window_rule`) and legacy config syntax; added troubleshooting for KDE/Wayland and other non-Hyprland/Sway Wayland sessions; added a Character Dictionary troubleshooting section; added a "See Also" index linking each feature's troubleshooting page.
## v0.15.0 (2026-05-29)
### Breaking Changes
- **Subsync:**
- The `subsync.defaultMode` config option has been removed
- Subsync now always opens the manual subtitle picker regardless of any previously set default mode
- **N+1 Highlighting:**
- N+1 highlighting now has its own dedicated `ankiConnect.nPlusOne.enabled` option, separate from known-word highlighting
- It is no longer enabled automatically when known-word highlighting is on - enable it explicitly to keep N+1 annotations
### Added
- **Auto-Updater:**
- Tray and `subminer -u` update checks with app update prompts
- Launcher and Linux rofi theme auto-updates
- Checksum verification and configurable notifications
- Opt-in prerelease channel via `updates.channel: "prerelease"`
- **Settings Window:**
- New dedicated Settings window via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections
- Click-to-learn keybinding controls
- AnkiConnect-backed deck, field, and note-type pickers that auto-fill from the configured Anki deck
- Cross-category search
- Live save for most options including subtitle CSS, stats keys, logging level, Jimaku, Subsync, and Anki mappings
- AI and translation settings remain config-file only
- **Inline Character Portraits:**
- Optional AniList character portraits appear inline for name-matched subtitle text
- Manual AniList overrides scoped per parent media directory so separate season folders maintain separate character dictionary selections
- **Character Dictionary Manager:** New `Ctrl/Cmd+D` manager modal to remove, reorder, or override loaded entries.
- **Log Export:** Sanitized log ZIP export from the tray menu and via `subminer logs -e`, with home-directory usernames redacted from exported contents.
- **Launcher CLI:**
- `subminer --version` / `subminer -v` prints the installed app version
- `mpv.profile` config and Settings support passes a named mpv profile to managed launches
- Bundled mpv plugin startup options are now configurable from SubMiner config
- **First-Run Setup:**
- Optional installer for Bun and the `subminer` CLI on Linux, macOS, and Windows
- Windows `subminer.cmd` PATH shim so `subminer` works without manually adding `SubMiner.exe` to PATH
- Setup recognizes existing Homebrew or user PATH installs and avoids writing into Homebrew-owned paths
- Includes an Open SubMiner Settings button
- Standalone setup app quits after completing, returning terminal control
- **Primary Subtitle Visibility on Yomitan Popup:** New `subtitleStyle.primaryVisibleOnYomitanPopup` option keeps hover-mode primary subtitles visible while a Yomitan popup is open.
### Changed
- **Subtitle Appearance Config:**
- Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css`, `subtitleStyle.secondary.css`, and `subtitleSidebar.css`
- Known-word and N+1 annotation colors moved to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`
- Subtitle font defaults updated to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`
- Existing configs migrate automatically; legacy Anki color keys still accepted with deprecation warnings
- **Subtitle Style Defaults:**
- Stronger outline-style text shadow
- Thicker JLPT underlines
- Frequency `topX` default raised to `10000`
- **Character Dictionary:**
- Entries scoped to the current AniList media for name matching and inline portraits
- Generates Japanese-only name aliases so raw romanized/English aliases no longer surface as separate results
- In-app AniList selector waits for an explicit search with the box prefilled from the current filename
- `subtitleStyle.nameMatchEnabled` is now the sole switch for dictionary sync and builds
- **Electron Runtime:** Updated from 39.8.6 to 42.2.0, returning SubMiner to a supported Electron release line.
- **Jellyfin Setup:**
- Removed the server presets dropdown
- Setup now shows a single editable server URL field
- **Jellyfin Cast Identity:**
- Device identity now derived from the OS hostname and always reported as SubMiner
- Previously configurable identity fields are ignored, preventing multiple installs from sharing a remote-session identity
- **Startup Defaults:** Jellyfin remote-session startup warmup and character-name subtitle highlighting now default to off.
- **Setup Appearance:** Removed the bundled mpv runtime plugin readiness card from the setup flow.
### Fixed
- **AniList Progress:**
- Progress updates fire correctly when playback reaches or skips past the watched threshold, using fresh mpv timing events
- Season-specific results preferred for multi-season files, with a clear message when the matched season is not in Planning or Watching
- Repeated missing-token checks no longer exhaust retry attempts or duplicate dead-letter entries
- **Anki Mining:**
- Sentence-audio padding is opt-in by default
- Animated AVIF freeze-frame duration aligned to word audio length without double-counting
- Multi-line sentence alignment fixed for repeated subtitle text
- Kiku duplicate-card detection, auto-merge, modal acknowledgment race, and field/tag ordering corrected
- YouTube playback cards use mpv's resolved stream URLs
- Sentence cards refresh the secondary subtitle before saving
- **Jellyfin Discovery:**
- Startup, subtitle track selection, and duplicate ready-signal handling all fixed
- Paused mpv no longer misreported as playing
- Resume corrected when a remote play command sends `StartPositionTicks: 0` despite saved progress
- **Jellyfin Remote:**
- Tray checkbox stays in sync on Linux after tray, CLI, or startup changes
- Remote controller visibility and progress sync fixed for seeks, stops, startup path changes, and Linux websocket reconnect windows
- Play and Resume now behave correctly (Play from beginning, Resume from saved position)
- Final progress reports reuse SubMiner's last known position when mpv resets on stop
- Windows setup login flow fixed with an IPC bridge, immediate feedback, and a timeout with inline error for unreachable servers
- **Overlay (macOS):**
- Overlay hides when mpv loses focus, is minimized, or is no longer the foreground app
- Stays stable through transient window geometry disappearances from macOS APIs and when clicking from the overlay back into mpv
- Stats overlay opened inactive so it appears over fullscreen mpv without switching Spaces
- Passthrough fixed so mpv controls stay clickable before hovering a subtitle bar
- **Yomitan Sidebar:**
- Playback stays paused for sidebar-opened Yomitan popups when auto-pause is enabled
- Popups now open when startup races the Yomitan extension load
- Sidebar mining cards use audio and images from the clicked sidebar line instead of the current primary subtitle
- **Launcher:**
- `subminer app` on Linux returns terminal control immediately
- `subminer app --setup` opens the setup flow when SubMiner is already running in the background
- **YouTube Playback:**
- Selected subtitles downloaded to local temp files so the primary bar and sidebar read the same source, with cleanup on reload and quit
- False load-failure notifications suppressed
- Tray icon created on launcher-managed playback that attaches to an already-running process
- **Shortcuts:**
- Native mpv menu shortcuts disabled during managed macOS playback so configured SubMiner shortcuts work while mpv has focus
- Custom session shortcuts including `stats.markWatchedKey` wired through mpv
- Multi-line copy/mine overlay correctly focused so number keys choose the line count on macOS and Windows
- **Controller Bindings:**
- Controller config and debug shortcuts stay closed while controller support is disabled
- Binding learn mode starts from the edit pencil
- Remaps saved per controller profile
- Binding badges also start learn mode
- Row reset buttons restore individual bindings to defaults
- **Logging:**
- `logging.level` forwarded to launcher-started and Windows shortcut-started mpv sessions, covering mpv log verbosity, plugin logging, and plugin-launched app logging
- `logging.rotation` (default 7 days) and per-component `logging.files` toggles added, with mpv logs disabled by default
- Repeated IPC socket warning spam suppressed while waiting for mpv to recreate the socket
- Windows mpv IPC, subtitle track, and Yomitan diagnostics added
- **In-Player Stats:**
- Layering fixed so delete confirmations, overlay modals, and update-check dialogs appear above the stats window
- Jellyfin playback stats grouped by item metadata so watched episodes merge with matching local library titles and keep clean display names
- **WebSocket Annotations:**
- Annotation spans and token metadata stay on the annotation WebSocket
- The regular subtitle WebSocket is plain-text only
- **Subtitle Annotation Prefetching:** Cached colored annotations and character images ready sooner for live subtitle changes without delaying raw subtitle display.
- **Windows Startup Errors:** Fatal startup failures now show a native error dialog and write details to the app log instead of exiting silently.
### Docs
- **Documentation Site:**
- Published stable docs at the site root with current development docs under `/main/`
- Fixed versioned docs navigation, archived page link handling, and local dev version routing
- Documented all previously undocumented config options including `subtitleStyle.primaryDefaultMode`, `stats.markWatchedKey`, `immersionTracking.lifetimeSummaries.*`, and all seven `mpv.*` launcher options
- Added Playback Startup Flow and Runtime Sockets diagrams to the architecture docs with cross-reference pointers in the MPV Plugin and Troubleshooting pages
<details>
<summary>Internal changes</summary>
### Internal
- **Release Tooling:**
- Release-note polishing treats pending fragments and reviewed prerelease notes as a cumulative final outcome, collapsing prerelease-only fixes into the final user-facing change
- Prerelease generation reuses existing reviewed notes and merges only new fragment material
- `make clean` preserves `release/prerelease-notes.md`
- **Tests:** Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
</details>
## v0.14.0 (2026-05-12)
### Added
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches - open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
- **Primary Subtitle Bar:** Added a `V` shortcut and mpv plugin binding to toggle the primary subtitle bar without affecting mpv's native subtitle visibility.
- **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
@@ -58,7 +237,7 @@
### Internal
- Replaced the changelog renderer with an AI polish pass that merges related fragments and writes user-facing release notes. `CHANGELOG.md` keeps internal items in a collapsed `<details>` block; GitHub release notes omit them entirely.
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain - run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
</details>
@@ -77,8 +256,8 @@
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity.
- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility.
- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard.
- Stats: Session timeline no longer plots seek-forward/seek-backward markers they were too noisy on sessions with lots of rewinds.
- Stats: Replaced the "Library Per Day" section on the Stats → Trends page with a "Library Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
- Stats: Session timeline no longer plots seek-forward/seek-backward markers - they were too noisy on sessions with lots of rewinds.
- Stats: Replaced the "Library - Per Day" section on the Stats → Trends page with a "Library - Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
### Fixed
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+7 -4
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 docs-test docs-build docs-build-versioned docs-dev
.PHONY: help submodules 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
@@ -72,7 +72,8 @@ help:
" generate-config Generate ~/.config/SubMiner/config.jsonc from centralized defaults" \
"" \
"Other targets:" \
" deps Install JS dependencies (root + stats + texthooker-ui)" \
" submodules Initialize/update git submodules" \
" deps Initialize submodules and install JS dependencies (root + stats + texthooker-ui)" \
" uninstall-linux Remove Linux install artifacts" \
" uninstall-macos Remove macOS install artifacts" \
" uninstall-windows Remove Windows mpv plugin artifacts" \
@@ -105,8 +106,10 @@ print-dirs:
"MACOS_APP_SRC=$(MACOS_APP_SRC)" \
"MACOS_ZIP_SRC=$(MACOS_ZIP_SRC)"
deps:
@$(MAKE) --no-print-directory ensure-bun
submodules:
@git submodule update --init --recursive
deps: submodules ensure-bun
@bun install
@cd stats && bun install --frozen-lockfile
@cd vendor/texthooker-ui && bun install --frozen-lockfile
+19 -17
View File
@@ -4,7 +4,7 @@
# SubMiner
Integrates Yomitan with mpv - look up words, mine to Anki, and track your immersion without leaving the player.
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 @@ Integrates Yomitan with mpv - look up words, mine to Anki, and track your immers
### 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,16 +110,18 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
## Requirements
Only **mpv** is required. Everything else is optional but enhances the experience.
Only **mpv** and Anki+AnkiConnect are required. Everything else is optional but enhances the experience.
| Dependency | Status | What it does |
| -------------------- | ----------- | ------------------------------------------------- |
| -------------------- | ----------- | ---------------------------------------- |
| mpv | Required | The video player SubMiner overlays on |
| Anki + AnkiConnect | Required | Card creation from the Yomitan popup |
| ffmpeg | Recommended | Audio clips & screenshots for Anki cards |
| MeCab + mecab-ipadic | Recommended | More precise N+1, JLPT, and frequency annotations |
| 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 |
| guessit | Optional | Better anime title and episode detection |
<details>
<summary><b>Platform-specific install commands</b></summary>
@@ -196,25 +198,24 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
Run SubMiner and the first-run setup wizard will guide you through importing Yomitan dictionaries and optionally installing the `subminer` command-line launcher.
```bash
# Linux (AUR)
# Linux
subminer app --setup
# macOS — open SubMiner.app, or:
subminer app --setup
```
On **Windows**, just run `SubMiner.exe` setup opens automatically on first launch.
On **Windows**, just run `SubMiner.exe` and the setup will open automatically on first launch.
### 3. Play
### 3. Mine
```bash
subminer video.mkv # play video with overlay
subminer stats # open immersion dashboard
subminer config # open configuration window
subminer --config # open configuration window via flag
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 setup — double-click it or drag a video file onto it.
On **Windows**, use the **SubMiner mpv** shortcut created during setup. Double-click it or drag a video file onto it.
## Documentation
@@ -228,6 +229,7 @@ SubMiner builds on the work of these open-source projects:
| Project | Role |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| [ani-skip](https://github.com/synacktraa/ani-skip) | AniSkip API client for anime intro/outro skip timestamps |
| [Anacreon-Script](https://github.com/friedrich-de/Anacreon-Script) | Inspiration for the mining workflow |
| [asbplayer](https://github.com/killergerbah/asbplayer) | Inspiration for subtitle sidebar and logic for YouTube subtitle parsing |
| [Bee's Character Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) | Character name recognition in subtitles |
-193
View File
@@ -1,193 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveMacAppBundlePath = resolveMacAppBundlePath;
exports.isMacApplicationsFolderBundle = isMacApplicationsFolderBundle;
exports.isKnownLinuxPackageManagedAppImage = isKnownLinuxPackageManagedAppImage;
exports.isNativeUpdaterSupported = isNativeUpdaterSupported;
exports.configureAutoUpdater = configureAutoUpdater;
exports.createElectronAppUpdater = createElectronAppUpdater;
const node_fs_1 = require("node:fs");
const node_child_process_1 = require("node:child_process");
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const node_util_1 = require("node:util");
const electron_updater_1 = require("electron-updater");
const release_assets_1 = require("./release-assets");
const updaterErrorListeners = new WeakMap();
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
function resolveMacAppBundlePath(execPath) {
const marker = '.app/Contents/MacOS/';
const markerIndex = execPath.indexOf(marker);
if (markerIndex < 0)
return null;
return execPath.slice(0, markerIndex + '.app'.length);
}
async function readMacCodeSignature(appBundlePath) {
try {
const result = await execFileAsync('/usr/bin/codesign', ['-dv', '--verbose=4', appBundlePath], {
encoding: 'utf8',
});
return `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
}
catch {
return null;
}
}
function realpathOrOriginal(filePath) {
try {
return (0, node_fs_1.realpathSync)(filePath);
}
catch {
return filePath;
}
}
function isSameOrInsideDirectory(parentPath, candidatePath) {
const relative = node_path_1.default.relative(parentPath, candidatePath);
return (relative === '' ||
(relative.length > 0 && !relative.startsWith('..') && !node_path_1.default.isAbsolute(relative)));
}
function isMacApplicationsFolderBundle(appBundlePath, homeDir = node_os_1.default.homedir()) {
const resolvedBundlePath = node_path_1.default.resolve(appBundlePath);
return (isSameOrInsideDirectory('/Applications', resolvedBundlePath) ||
isSameOrInsideDirectory(node_path_1.default.join(homeDir, 'Applications'), resolvedBundlePath));
}
function isKnownLinuxPackageManagedAppImage(appImagePath) {
return realpathOrOriginal(appImagePath) === '/opt/SubMiner/SubMiner.AppImage';
}
async function isNativeUpdaterSupported(options) {
if (!options.isPackaged) {
options.log?.('Skipping native updater because this build is not packaged.');
return false;
}
if (options.platform === 'linux') {
options.log?.('Skipping native Linux updater because Linux tray checks use GitHub release assets.');
return false;
}
if (options.platform !== 'darwin') {
options.log?.('Skipping native updater because this platform uses GitHub metadata checks.');
return false;
}
const appBundlePath = resolveMacAppBundlePath(options.execPath);
if (!appBundlePath) {
options.log?.('Skipping native macOS updater because the app bundle path could not be resolved.');
return false;
}
if (!isMacApplicationsFolderBundle(appBundlePath, options.homeDir)) {
options.log?.('Skipping native macOS updater because the app is not installed in an Applications folder.');
return false;
}
const signature = await (options.readCodeSignature ?? readMacCodeSignature)(appBundlePath);
if (!signature) {
options.log?.('Skipping native macOS updater because the app code signature could not be read.');
return false;
}
if (/Signature=adhoc\b/.test(signature) || /TeamIdentifier=not set\b/.test(signature)) {
options.log?.('Skipping native macOS updater because this build is ad-hoc signed.');
return false;
}
return true;
}
function configureAutoUpdater(updater, log = () => { }, channel = 'stable') {
updater.autoDownload = false;
// On macOS this avoids invoking Squirrel until the explicit restart/install step.
updater.autoInstallOnAppQuit = false;
updater.allowPrerelease = channel === 'prerelease';
updater.allowDowngrade = false;
updater.logger = {
info: () => { },
debug: () => { },
warn: (message) => log(message),
error: (message) => log(message),
};
const previousErrorListener = updaterErrorListeners.get(updater);
if (previousErrorListener) {
if (updater.off) {
updater.off('error', previousErrorListener);
}
else {
updater.removeListener?.('error', previousErrorListener);
}
}
if (updater.on) {
const errorListener = (error) => {
const message = error instanceof Error ? error.message : String(error);
log(`Updater error event: ${message}`);
};
updater.on('error', errorListener);
updaterErrorListeners.set(updater, errorListener);
}
return updater;
}
function createElectronAppUpdater(options) {
const getChannel = options.getChannel ?? (() => 'stable');
const updater = configureAutoUpdater(options.updater ?? electron_updater_1.autoUpdater, options.log, getChannel());
if (options.configureHttpExecutor) {
// electron-updater has no public executor hook; keep the macOS cURL override localized.
updater.httpExecutor = options.configureHttpExecutor();
}
if (options.disableDifferentialDownload !== undefined) {
updater.disableDifferentialDownload = options.disableDifferentialDownload;
}
let nativeUpdaterSupported = null;
async function getNativeUpdaterSupported() {
if (!options.isNativeUpdaterSupported)
return true;
if (nativeUpdaterSupported === null) {
nativeUpdaterSupported = Promise.resolve(options.isNativeUpdaterSupported());
}
return nativeUpdaterSupported;
}
return {
async checkForUpdates(channel) {
if (!options.isPackaged) {
return {
available: false,
version: options.currentVersion,
canUpdate: false,
};
}
if (!(await getNativeUpdaterSupported())) {
options.log('Skipping native app update check because native updater is unsupported.');
return {
available: false,
version: options.currentVersion,
canUpdate: false,
};
}
configureAutoUpdater(updater, options.log, channel ?? getChannel());
const result = await updater.checkForUpdates();
const version = result?.updateInfo?.version ?? options.currentVersion;
return {
available: (0, release_assets_1.compareSemverLike)(version, options.currentVersion) > 0,
version,
canUpdate: true,
};
},
async downloadUpdate() {
if (!options.isPackaged) {
options.log('Skipping app update download because this build is not packaged.');
return;
}
if (!(await getNativeUpdaterSupported())) {
options.log('Skipping app update download because native updater is unsupported.');
return;
}
await updater.downloadUpdate();
},
async quitAndInstall() {
if (!options.isPackaged) {
options.log('Skipping app update install because this build is not packaged.');
return;
}
if (!(await getNativeUpdaterSupported())) {
options.log('Skipping app update install because native updater is unsupported.');
return;
}
updater.quitAndInstall(false, true);
},
};
}
//# sourceMappingURL=app-updater.js.map
+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=="],
@@ -0,0 +1,4 @@
type: added
area: release
- Release notes now credit contributors with a `What's Changed` list (`by @author in #pr`) and a `New Contributors` section for first-time authors, resolved from changelog fragments via git and the GitHub API.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: jellyfin
- Fixed the Jellyfin setup popup login path on Windows by using an IPC bridge, showing immediate login progress, and timing out unreachable server login attempts with an inline error.
+10
View File
@@ -31,14 +31,24 @@ Rules:
- `README.md` is ignored by the generator
- if a PR should not produce release notes, apply the `skip-changelog` label instead of adding a fragment
PR branch workflow:
- Before adding a fragment or bullet, inspect the `changes/*.md` files already changed in the PR
- If the new work fixes, modifies, renames, or supersedes behavior introduced or referenced by that fragment, edit or remove the stale bullet instead of adding follow-up churn
- Add a new bullet only when it describes a truly separate user-visible outcome
- Multiple fragment files are allowed when one PR has genuinely separate release-note outcomes, but keep them minimized and current
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.
- GitHub release notes and prerelease notes use short top-level items with nested bullets for the change, user benefit, and any useful action note. The stable `CHANGELOG.md` can stay in compact single-line bullets.
- `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
@@ -0,0 +1,4 @@
type: fixed
area: anilist
- Marked AniList entries completed when a post-watch update reaches the final known episode of the season.
+6
View File
@@ -0,0 +1,6 @@
type: changed
area: playback
- AniSkip intro detection now runs in the SubMiner app instead of the mpv plugin: lookups cover every local file loaded during an mpv session (including playlist advances), and the plugin no longer performs any network calls.
- `mpv.aniskipEnabled` and `mpv.aniskipButtonKey` now hot-reload without restarting playback.
- AniSkip now requires the SubMiner app to be connected to mpv; plugin-only mpv sessions without the app no longer fetch skip windows.
-4
View File
@@ -1,4 +0,0 @@
type: changed
area: config
- Settings: Changed the AniSkip button key setting to use click-to-learn key capture instead of raw text entry.
+5
View File
@@ -0,0 +1,5 @@
type: fixed
area: playback
- Fixed AniSkip intro markers disappearing after same-media mpv reloads.
- Fixed AniSkip metadata detection for intros that start at `0` seconds and common release-group filenames without `guessit`.
-4
View File
@@ -1,4 +0,0 @@
type: added
area: updater
- Added tray and `subminer -u` update checks for SubMiner releases, including app update prompts, launcher updates, Linux rofi theme updates, checksum verification, configurable update notifications, and an opt-in prerelease update channel for beta/RC testing.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: launcher
- Reused an already-running background SubMiner app for launcher-opened videos, preserving warmups and keeping the tray app alive after playback closes.
@@ -0,0 +1,4 @@
type: changed
area: release
- Changed PR changelog guidance to preserve multiple fragments for genuinely separate outcomes while directing contributors to update, remove, or merge same-PR fragment notes before adding follow-up churn.
@@ -1,4 +0,0 @@
type: fixed
area: character-dictionary
- Reused cached character-dictionary media matches so loading a title with an existing snapshot no longer sends another AniList search request.
-4
View File
@@ -1,4 +0,0 @@
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.
-4
View File
@@ -1,4 +0,0 @@
type: changed
area: config
- Reorganized the Configuration window into clearer Appearance, Behavior, Anki, input, and integration sections with learned keybinding controls and AnkiConnect-backed deck, field, and note-type pickers.
-4
View File
@@ -1,4 +0,0 @@
type: changed
area: settings
- Simplified configuration option rows by hiding raw config paths and placing the live/restart status beside each option title.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: config
- Fixed Configuration window search so it searches across all categories, narrows on multi-word terms, hides settings owned by richer editors, and no longer shows the Open File button.
-8
View File
@@ -1,8 +0,0 @@
type: added
area: config
- Added a dedicated Configuration window with launcher entry points via `subminer --config` and `subminer config`.
- Fixed the Configuration window preload so launcher-opened windows can initialize even when Electron sandboxing is active.
- Kept config-window startup lightweight by skipping AniList token refresh and automatic update polling.
- Marked safe live config options in the Configuration window and expanded hot reload for stats keys, logging level, Jimaku, Subsync, YouTube language defaults, Anki media/sentence/misc field mappings, sentence card model, and selected Anki annotation/runtime options.
- Hid AI and translation fields from the Configuration window while keeping them supported in config files.
@@ -1,6 +0,0 @@
type: fixed
area: overlay
- Controller config and debug shortcuts now stay closed while controller support is disabled and show a notice to enable `controller.enabled` manually.
- Controller binding rows now start learn mode from the edit pencil, so clicking edit and pressing a controller button saves the remap.
- Controller remaps are now saved per controller profile, binding badges also start learn mode, and row reset buttons restore individual bindings to their defaults.
-4
View File
@@ -1,4 +0,0 @@
type: docs
area: docs
- Published stable docs at the site root with current development docs under `/main/`.
@@ -1,5 +0,0 @@
type: added
area: setup
- Added optional first-run setup controls to install Bun and the `subminer` command-line launcher on Linux, macOS, and Windows.
- Added a Windows `subminer.cmd` user PATH shim so users can type `subminer` without adding `SubMiner.exe` to PATH.
-6
View File
@@ -1,6 +0,0 @@
type: fixed
area: anilist
- Used fresh mpv time-position, duration, and subtitle timing events for AniList post-watch threshold checks so progress updates still fire when playback reaches or skips past the watched threshold.
- Prefer season-specific AniList search results for multi-season files before falling back to the base title.
- Show a clear AniList message when the matched season is not in Planning or Watching instead of silently queueing an impossible progress update.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Primed the first startup subtitle before autoplay resumes so the overlay can render text before video playback begins.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: subtitles
- Kept frequency highlighting for determiner-led noun compounds like `その場` while still filtering standalone determiners.
@@ -1,4 +0,0 @@
type: fixed
area: launcher
- Suppressed Electron macOS menu diagnostics from `subminer config` launcher output.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: shortcuts
- Disabled native mpv menu shortcuts during managed macOS playback so configured SubMiner shortcuts also work while mpv has focus.
-10
View File
@@ -1,10 +0,0 @@
type: fixed
area: overlay
- Hid the macOS visible overlay when mpv is no longer the foreground target so other apps and Spaces are not covered by SubMiner subtitles.
- Kept the macOS overlay layered above active mpv while stats mouse passthrough is enabled, and treated the frontmost mpv app as the focus signal.
- Opened the stats overlay inactive on macOS so it appears over fullscreen mpv instead of switching back to SubMiner's original desktop.
- Preserved the active mpv focus state through transient macOS helper misses so subtitles do not flicker while mpv remains foreground.
- Kept fullscreen macOS overlays stable when mpv remains frontmost but window geometry temporarily disappears from the macOS window APIs.
- Released the macOS overlay when the helper reports mpv is no longer foreground so other apps are no longer covered.
- Reduced macOS window-tracker background work by preferring the compiled helper and slowing polls while mpv is stably focused.
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Fixed macOS overlay tracking so transient mpv window misses no longer hide the overlay; minimizing mpv still hides it.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Fixed macOS overlay passthrough so mpv controls remain clickable before hovering subtitle bars.
@@ -1,4 +0,0 @@
type: fixed
area: playback
- Fixed managed mpv startup so launcher-owned videos quit SubMiner when playback ends, background/tray sessions stay alive, and pause-until-ready waits for the overlay and tokenization readiness before playback resumes.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Fixed subtitle sync modal opens so macOS no longer flashes and hides the first modal attempt or leaves stale modal state after syncing.
-4
View File
@@ -1,4 +0,0 @@
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,5 @@
type: fixed
area: jellyfin
- Restarted the Jellyfin remote session after successful setup login so websocket reconnects use the freshly saved credentials.
- Stopped the Jellyfin remote session on setup logout.
-4
View File
@@ -1,4 +0,0 @@
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
@@ -1,4 +0,0 @@
type: added
area: launcher
- Added `subminer --version` and `subminer -v` to print the installed SubMiner app version.
-5
View File
@@ -1,5 +0,0 @@
type: fixed
area: updater
- Made Linux `subminer -u` perform release updates from the launcher, independent of any running tray app instance, while reporting `up to date` without downloading assets when the latest release is not newer.
- Limited support asset updates to the Linux rofi theme.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: launcher
- Fixed Linux first-run launcher installs by building the packaged launcher with a valid Bun shebang.
-4
View File
@@ -1,4 +0,0 @@
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
@@ -1,4 +0,0 @@
type: fixed
area: updater
- Stopped Linux tray update checks from invoking the native Electron updater, using GitHub release metadata/assets instead so checks do not crash the tray app.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: setup
- First-run setup now recognizes installed macOS launchers in Homebrew or user PATH dirs, while manual setup installs avoid Homebrew-owned directories.
-4
View File
@@ -1,4 +0,0 @@
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.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: updater
- Bring macOS update dialogs to the front when `subminer --update` is run from the launcher.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: build
- Fixed one-shot `make clean build install` flows so install picks up the AppImage built earlier in the same make invocation.
-4
View File
@@ -1,4 +0,0 @@
type: added
area: launcher
- Managed bundled mpv plugin startup options from SubMiner config.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Wired configured session shortcuts, including `stats.markWatchedKey`, through mpv so custom add/remove changes work while mpv has focus.
-6
View File
@@ -1,6 +0,0 @@
type: fixed
area: updates
- 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
@@ -1,4 +0,0 @@
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
@@ -1,4 +0,0 @@
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.
+25
View File
@@ -0,0 +1,25 @@
type: changed
area: notifications
breaking: true
- Added overlay notifications with a Catppuccin Macchiato stack, a 3-second transient timeout, and persistent long-running job notifications for character dictionary sync.
- Added `notifications.overlayPosition` to place overlay notifications at the top left, top center, or top right; top right remains the default.
- Added a notification history panel (default `Ctrl/Cmd+N`, configurable via `shortcuts.toggleNotificationHistory`) that logs every notification shown during the session; the toggle works whether the overlay or mpv has focus, the panel slides in from the same edge as notifications (right when centered), and entries can be removed individually or cleared.
- Made the overlay error/recovery toast follow the configured `notifications.overlayPosition` instead of always pinning to the top-right corner, and kept the notification stack and history panel side synced from that position before first open so left-side history panels slide in from the left.
- Routed startup tokenization, subtitle annotation, and character dictionary status through queued overlay notifications for `overlay`/`both` instead of falling back to mpv OSD while the overlay loads; queued loading cards are shown before their ready update when both happen before the overlay is ready, and the bundled mpv plugin now only emits startup OSD messages for `osd` and `osd-system`.
- Preserved character dictionary checking/building/importing/ready phases in overlay notification history and sent those phases to system notifications when `notificationType` is `both`.
- Initialized the tray and visible overlay shell before deferred tokenization warmups finish on visible-overlay startup, while keeping playback paused until SubMiner reports autoplay readiness.
- Kept playback feedback such as subtitle visibility, subtitle track, subtitle delay, and AniSkip prompt/skip text on overlay/OSD surfaces only; desktop/system notifications are reserved for real notifications like mined cards, errors, and updates.
- Routed mpv-plugin restart feedback through the configured overlay/OSD feedback surface so `overlay` and `both` notification modes show restart progress and completion in the overlay, while keeping the loading OSD spinner visible until the overlay reports ready.
- Reused the active primary/secondary subtitle mode overlay notification while cycling modes so rapid toggles update one card instead of stacking duplicate feedback.
- Updated repeated progress notifications such as subsync syncing in place so their spinner stays live instead of flickering on every tick.
- Stabilized overlay startup notifications so queued progress updates do not replay the card entrance animation or trigger macOS pass-through hover flicker after the loading OSD hands off to overlay notifications.
- Fixed mined-card overlay notifications so `overlay` and `both` modes show generated card thumbnails in both live cards and the notification history panel.
- Added Open in Anki buttons to mined-card overlay notifications and their history entries, with a direct AnkiConnect fallback when the live integration is unavailable.
- Fixed those Open in Anki buttons so their fallback honors runtime AnkiConnect URL overrides and the default AnkiConnect endpoint.
- Added an Update button to overlay update-available notifications so users can start the app update flow from the notification.
- Fixed sentence-card mining so the Ctrl+S flow shows only the Anki update progress notification instead of also stacking a generic SubMiner toast.
- Fixed overlay notification layering so notification close/actions stay clickable above subtitle bars on Linux overlays.
- Fixed character dictionary sync so duplicate MPV media-path events do not repeat check/ready notifications for the same opened video.
- Changed `both` notification routing to mean overlay + system; users who used `both` for mpv OSD + system notifications should set `notificationType` to `osd-system` in `config.jsonc`.
- Kept `osd` and `osd-system` as config-file-only legacy notification values; Settings normally offers only overlay, system, both, and none, while still showing an already configured legacy value as selected.
-4
View File
@@ -1,4 +0,0 @@
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
@@ -1,4 +0,0 @@
type: changed
area: release
- Prerelease note generation now reuses existing reviewed prerelease notes and asks Claude to merge only new fragment material, while `make clean` preserves `release/prerelease-notes.md`.
@@ -1,4 +0,0 @@
type: changed
area: jellyfin
- Removed the Jellyfin setup server presets dropdown; setup now shows a single editable server URL field.
@@ -1,4 +0,0 @@
type: internal
area: tests
- Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
+6
View File
@@ -0,0 +1,6 @@
type: changed
area: stats
- Split local and Jellyfin library entries by detected season, using season folders first and filename parsing as fallback.
- Repaired older combined-series stats rows by moving parsed episodes into season-specific library entries, rebuilding summaries, and deleting now-empty legacy rows.
- Refresh anime detail and library cover art immediately after manually changing an AniList entry.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: launcher
- Fixed `subminer app --setup` so it opens the setup flow when SubMiner is already running in the background.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: setup
- Quit standalone setup app launches after first-run setup finishes, returning the terminal instead of leaving the app process open.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: build
- Updated `make deps` so a fresh source checkout initializes submodules before installing root, stats, and texthooker-ui dependencies.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed startup pause-until-ready so SubMiner releases playback after tokenization and overlay content are ready even when playback starts before the first subtitle line.
+9
View File
@@ -0,0 +1,9 @@
type: changed
area: stats
- Added the Stats Search tab for realtime subtitle sentence search with media context, headword matching, and mining actions for source-backed sentence cards or exact-match word/audio cards.
- Improved Stats mining from Search and vocabulary examples: empty `ankiConnect.deck` can use Yomitan's mining deck, sentence cards are created before slow media generation finishes, stored/requested secondary subtitles are preserved before falling back to sidecar files or temporary alass-retimed English sidecars for sentence Selection Text, invalid stored timings are blocked before FFmpeg runs, future out-of-order subtitle timing pairs are skipped until valid timings arrive, and partial media failures are shown.
- Fixed Stats mining field/audio behavior so sentence clips update `SentenceAudio`, word audio uses the configured Yomitan sources, English subtitle text is not written onto word cards, and secondary subtitle auto-selection prefers regular English tracks over Signs/Songs tracks.
- Improved vocabulary review with remembered Hide Known/Hide Kana filters, cross-title Hide Kana filtering, duplicate-collapsed exclusions across token variants, and Related Seen Words matching based on shared readings or kanji.
- Reorganized the Stats Trends tab into clearer Activity, Cumulative Totals, Efficiency, Patterns, and Library sections, disambiguated per-period vs cumulative charts, and added Words/Min and Cards/Hour efficiency charts.
- Improved Stats browsing reliability by remembering library card size, retrying stored cover art without extra AniList lookups, preserving PNG/WebP cover MIME types, honoring custom AnkiConnect URLs for Browse, showing progress during session deletes, and making session deletes refresh faster.
@@ -1,4 +0,0 @@
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
@@ -1,4 +0,0 @@
type: fixed
area: config
- Migrated legacy subtitle hover token colors into `subtitleStyle.css` instead of leaving `hoverTokenColor` or `hoverTokenBackgroundColor` behind.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: config
- Migrated legacy primary and secondary subtitle appearance options into `subtitleStyle.css` automatically when loading config files.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: config
- Fixed live Configuration window saves so primary and secondary subtitle CSS declarations apply immediately to open video overlays.
+9
View File
@@ -0,0 +1,9 @@
type: fixed
area: overlay
- Fixed visible overlay startup/resume so subtitle bars can be hovered and clicked as soon as the first subtitle line appears, without waiting for the next subtitle update.
- Released playback after the first overlay measurement instead of waiting for cold subtitle annotation warmup, so overlay notifications and subtitle controls do not freeze during visible-overlay startup.
- Primed Linux overlay input from the first measured subtitle/notification surface before playback resumes, so first-line subtitles and startup notifications are clickable immediately.
- Restored visible-overlay loading feedback as an mpv OSD spinner that stops once the overlay is content-ready and visible.
- Starts that OSD spinner when mpv connects, opens media, or the visible overlay is requested, so cold startup shows feedback before the overlay is almost ready.
- Shows an immediate plugin-side mpv OSD on `start-file` for visible overlay startup, even when normal plugin status OSD messages are disabled or the launcher owns the overlay start, and keeps it spinning until Electron reports the visible overlay is content-ready.
-4
View File
@@ -1,4 +0,0 @@
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`.
-10
View File
@@ -1,10 +0,0 @@
type: fixed
area: tray
- Kept the tray app running when closing tray-launched Yomitan settings.
- Kept tray-launched Yomitan settings loading from blocking other tray actions.
- Replaced the default native Yomitan settings menu with a close-only menu so closing settings does not quit the tray app.
- Added an in-page close button for Yomitan settings on Hyprland, where native window controls are not available.
- Disabled Yomitan's embedded popup preview in the tray-launched settings window to avoid renderer hangs during normal sidebar navigation.
- Serialized copied Yomitan extension refreshes so startup cannot race itself and leave extension loading in an error state.
- Fixed tray-launched session help focus handling so the modal can close without mpv running.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: windows
- Windows startup failures now show a native error dialog and write fatal details to the SubMiner app log instead of exiting silently.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: updates
- Windows automatic updates now keep the native `electron-updater`/NSIS install path enabled while routing updater HTTP through main-process fetch, avoiding the delayed app exit seen shortly after launch without requiring `curl.exe`.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Fixed Yomitan popups not opening when playback/overlay startup races the Yomitan extension load.
+47 -29
View File
@@ -46,10 +46,16 @@
// Logging
// Controls logging verbosity.
// Set to debug for full runtime diagnostics.
// Hot-reload: logging.level applies live while SubMiner is running.
// Hot-reload: logging.level and logging.files apply live while SubMiner is running.
// ==========================================
"logging": {
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
"level": "warn", // Minimum log level for runtime logging. Values: debug | info | warn | error
"rotation": 7, // Number of days of app, launcher, and mpv logs to retain.
"files": {
"app": true, // Write SubMiner app runtime logs. Values: true | false
"launcher": true, // Write launcher command logs. Values: true | false
"mpv": false // Write mpv player logs. Enable temporarily when debugging mpv/plugin startup. Values: true | false
} // Files setting.
}, // Controls logging verbosity.
// ==========================================
@@ -83,7 +89,7 @@
"rightStickPress": 10, // Raw button index used for controller R3 input.
"leftTrigger": 6, // Raw button index used for controller L2 input.
"rightTrigger": 7 // Raw button index used for controller R2 input.
}, // Semantic button-name reference mapping used for legacy configs and debug output. Updating it does not rewrite existing raw binding descriptors.
}, // Semantic button-name reference mapping used for debug output. Updating it does not rewrite existing raw binding descriptors.
"bindings": {
"toggleLookup": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
@@ -155,7 +161,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.
// ==========================================
@@ -166,10 +172,19 @@
"updates": {
"enabled": true, // Run automatic update checks in the background. Values: true | false
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
"notificationType": "system", // How SubMiner announces available updates. Values: system | osd | both | none
"notificationType": "system", // How SubMiner announces available updates. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system
"channel": "stable" // Release channel used for update checks. Values: stable | prerelease
}, // Automatic update check behavior.
// ==========================================
// Notifications
// Overlay notification display behavior.
// Hot-reload: position changes apply to the next overlay notification.
// ==========================================
"notifications": {
"overlayPosition": "top-right" // Position for in-overlay notification cards. Values: top-left | top | top-right
}, // Overlay notification display behavior.
// ==========================================
// Keyboard Shortcuts
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
@@ -187,13 +202,14 @@
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
"toggleSecondarySub": "CommandOrControl+Shift+V", // Accelerator that toggles the secondary subtitle bar visibility.
"markAudioCard": "CommandOrControl+Shift+A", // Accelerator that marks the last mined card as an audio card.
"openCharacterDictionary": "CommandOrControl+Alt+A", // Accelerator that opens the character dictionary modal.
"openCharacterDictionaryManager": "CommandOrControl+D", // Accelerator that opens the character dictionary manager modal.
"openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
"openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
"openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet.
"openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal.
"openControllerDebug": "Alt+Shift+C", // Accelerator that opens the controller debug modal with live axis/button readouts.
"toggleSubtitleSidebar": "Backslash" // Accelerator that toggles the subtitle sidebar visibility.
"toggleSubtitleSidebar": "Backslash", // Accelerator that toggles the subtitle sidebar visibility.
"toggleNotificationHistory": "CommandOrControl+N" // Accelerator that toggles the overlay notification history panel.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// ==========================================
@@ -336,12 +352,11 @@
}, // 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": "", // 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.
@@ -375,7 +390,7 @@
"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.
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // 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.
@@ -384,7 +399,9 @@
"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
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
"primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
"nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
"nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
"nPlusOneColor": "#c6a0f6", // Color used for the single N+1 target token subtitle highlight.
"knownWordColor": "#a6da95", // Color used for known-word subtitle highlights.
@@ -398,7 +415,7 @@
"frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
"topX": 10000, // Only color tokens with frequency rank <= topX (default: 10000).
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
@@ -423,7 +440,7 @@
"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.
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
} // Secondary setting.
@@ -439,7 +456,7 @@
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
"pauseVideoOnHover": 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
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
@@ -489,6 +506,7 @@
"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 use the Yomitan mining deck when available.
"fields": {
"word": "Expression", // Card field for the mined word or expression text.
"audio": "ExpressionAudio", // Card field that receives generated sentence audio.
@@ -508,11 +526,14 @@
"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, // Seconds of padding appended to both ends of generated sentence audio.
"audioPadding": 0, // Seconds of padding appended to both ends of generated sentence audio and animated AVIF clips.
"fallbackDuration": 3, // Fallback clip duration in seconds when subtitle timing data is unavailable.
"maxMediaDuration": 30 // Maximum allowed media clip duration in seconds.
}, // Media setting.
@@ -520,15 +541,15 @@
"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"] }.
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Cache matches always receive known-word highlighting even when POS filters suppress other annotation types. Values: headword | surface
"decks": {} // Decks and expression/word fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word"] }.
}, // Known words setting.
"behavior": {
"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
"notificationType": "overlay", // Notification surface used to announce mining and update outcomes. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
}, // Behavior setting.
"nPlusOne": {
@@ -556,6 +577,8 @@
// ==========================================
"jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Base URL of the Jimaku subtitle search API.
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
"maxEntryResults": 10 // Maximum Jimaku search results returned.
}, // Jimaku API configuration and defaults.
@@ -582,11 +605,8 @@
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
"characterDictionary": {
"enabled": false, // Enable automatic Yomitan character dictionary sync for currently watched AniList media. Values: true | false
"refreshTtlHours": 168, // Legacy setting; merged character dictionary retention is now usage-based and this value is ignored.
"maxLoaded": 3, // Maximum number of most-recently-used anime snapshots included in the merged Yomitan character dictionary.
"evictionPolicy": "delete", // Legacy setting; merged character dictionary eviction is usage-based and this value is ignored. Values: disable | delete
"profileScope": "all", // Yomitan profile scope for dictionary enable/disable updates. Values: all | active
"profileScope": "all", // Yomitan profile scope for character dictionary settings updates. Values: all | active
"collapsibleSections": {
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
@@ -612,18 +632,20 @@
// 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
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
"profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
"socketPath": "\\\\.\\pipe\\subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
"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.
"aniskipEnabled": true, // Enable AniSkip intro detection, chapter markers, and the skip-intro key. Values: true | false
"aniskipButtonKey": "TAB" // mpv key used to skip the detected intro while the skip prompt is visible.
}, // SubMiner-managed mpv launch and bundled plugin options.
// ==========================================
@@ -637,14 +659,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", // Stable device identifier sent on the Jellyfin authentication handshake; primarily internal.
"clientName": "SubMiner", // Client name sent on the Jellyfin authentication handshake; primarily internal.
"clientVersion": "0.1.0", // Client version sent on the Jellyfin authentication handshake; primarily internal.
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
"remoteControlDeviceName": "SubMiner", // Device name reported for Jellyfin remote control sessions.
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
+27 -13
View File
@@ -19,18 +19,26 @@ type VersionManifest = {
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';
function optionalEnv(value: string | undefined): string | undefined {
return value && value !== 'undefined' ? value : undefined;
}
const base = normalizeBase(optionalEnv(process.env.SUBMINER_DOCS_BASE) ?? '/');
const outDir = optionalEnv(process.env.SUBMINER_DOCS_OUT_DIR);
const docsSourceDir = optionalEnv(process.env.SUBMINER_DOCS_SOURCE_DIR) ?? process.cwd();
const channel = normalizeChannel(optionalEnv(process.env.SUBMINER_DOCS_CHANNEL));
const docsVersion = optionalEnv(process.env.SUBMINER_DOCS_VERSION);
const latestStable = optionalEnv(process.env.SUBMINER_DOCS_LATEST_STABLE) ?? 'v0.14.0';
const versionManifest = parseVersionManifest(process.env.SUBMINER_DOCS_VERSION_MANIFEST);
const versionLinkOrigin = process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN ?? 'production';
const versionLinkOrigin =
optionalEnv(process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN) ?? 'production';
function getLocalArchiveDir(): string {
return resolve(
optionalEnv(process.env.SUBMINER_DOCS_LOCAL_ARCHIVE_DIR) ??
join(docsSourceDir, '..', '.tmp/docs-versioned-site'),
);
}
function normalizeBase(value: string): string {
if (!value || value === '/') return '/';
@@ -43,7 +51,7 @@ function normalizeChannel(value: string | undefined): DocsChannel {
}
function parseVersionManifest(value: string | undefined): VersionManifest {
if (!value) {
if (!value || value === 'undefined') {
return {
latestStable,
channels: [
@@ -218,6 +226,7 @@ function isFile(path: string): boolean {
function archiveFileForPathname(pathname: string): string | null {
if (!shouldHandleLocalVersionRoute(pathname)) return null;
const localArchiveDir = getLocalArchiveDir();
const routePath = decodeURIComponent(pathname).replace(/^\/+/, '');
const filePath = resolve(localArchiveDir, routePath);
if (filePath !== localArchiveDir && !filePath.startsWith(`${localArchiveDir}${sep}`)) {
@@ -234,7 +243,11 @@ function archiveFileForPathname(pathname: string): string | null {
}
function serveLocalArchiveRoute(pathname: string, response: DevServerResponse): boolean {
if (versionLinkOrigin !== 'local') return false;
if (
(optionalEnv(process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN) ?? versionLinkOrigin) !== 'local'
) {
return false;
}
const filePath = archiveFileForPathname(pathname);
if (!filePath) return false;
@@ -315,6 +328,7 @@ const sidebar: DefaultTheme.SidebarItem[] = [
{ text: 'YouTube', link: '/youtube-integration' },
{ text: 'Jimaku', link: '/jimaku-integration' },
{ text: 'AniList', link: '/anilist-integration' },
{ text: 'AniSkip', link: '/aniskip-integration' },
{ text: 'Character Dictionary', link: '/character-dictionary' },
],
},
+4 -3
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:
@@ -36,7 +38,7 @@ SubMiner monitors playback and triggers an AniList progress update when an episo
The update flow:
1. **Title detection** -- SubMiner extracts the anime title, season, and episode number from the media filename. It tries [`guessit`](https://github.com/guessit-io/guessit) first for accurate parsing, then falls back to an internal filename parser if guessit is unavailable.
1. **Title detection** -- SubMiner extracts the anime title, season, and episode number from the media filename and path. Season folders such as `Season 2` are treated as a strong season signal. SubMiner tries [`guessit`](https://github.com/guessit-io/guessit) first for accurate parsing, then falls back to an internal filename parser if guessit is unavailable.
2. **AniList search** -- The detected title is searched against the AniList GraphQL API. For season 2 and later files, SubMiner searches the season-specific title first, then falls back to the base title. SubMiner picks the best match by comparing titles (romaji, English, native) and filtering by episode count.
3. **Progress check** -- SubMiner fetches your current list entry for the matched media. The media must already be in Planning or Watching; otherwise SubMiner shows an MPV message explaining that the update is not possible. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`.
@@ -96,12 +98,11 @@ All AniList API calls go through a shared rate limiter that enforces a sliding w
| ------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------ |
| `enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
| `accessToken` | string | Explicit AniList access token override; when blank, SubMiner uses the stored encrypted token (default: `""`) |
| `characterDictionary.enabled` | `true`, `false` | Enable auto-sync of the merged character dictionary from AniList (default: `false`) |
| `characterDictionary.maxLoaded` | number | Number of recent media snapshots kept in the merged dictionary (default: `3`) |
| `characterDictionary.profileScope` | `"all"`, `"active"` | Apply dictionary to all Yomitan profiles or only the active one |
| `characterDictionary.collapsibleSections.*` | `true`, `false` | Control which dictionary entry sections start expanded |
See the [Character Dictionary](/character-dictionary) page for full details on the character dictionary feature, including name generation, matching, auto-sync lifecycle, and dictionary entry format.
See the [Character Dictionary](/character-dictionary) page for full details on the character dictionary feature, including name generation, matching, auto-sync lifecycle, and dictionary entry format. Character dictionary sync follows `subtitleStyle.nameMatchEnabled`.
## CLI Commands
+51
View File
@@ -0,0 +1,51 @@
# AniSkip Integration
SubMiner integrates with [AniSkip](https://aniskip.com) to automatically detect anime intro intervals and let you skip them with a single key press.
Intro detection runs in the SubMiner app over the mpv IPC socket. It is available whenever the overlay is connected to mpv - not just at launch - and covers every local file loaded during an mpv session, including playlist advances.
## Setup
AniSkip is opt-in. Enable it in your config:
```jsonc
{
"mpv": {
"aniskipEnabled": true,
"aniskipButtonKey": "TAB",
},
}
```
Both settings hot-reload: changing them in your config takes effect immediately without restarting playback or mpv.
For best title and episode detection, install [`guessit`](https://github.com/guessit-io/guessit):
```bash
python3 -m pip install --user guessit
```
Without `guessit`, SubMiner falls back to an internal filename parser which handles most common naming conventions but may miss unusual formats.
## How It Works
On each local file load:
1. SubMiner infers the anime title, season, and episode number from the filename and path (using `guessit` if available, otherwise the built-in parser). Remote URLs are skipped entirely.
2. The title is matched against MyAnimeList to resolve a MAL id.
3. SubMiner queries the AniSkip API for an OP skip interval for that MAL id and episode.
4. If an interval is found, SubMiner adds `AniSkip Intro Start` and `AniSkip Intro End` chapter markers to the current file and binds the skip key (`mpv.aniskipButtonKey`, default `TAB`).
5. At the start of the intro, an OSD prompt appears for 3 seconds: `You can skip by pressing TAB` (reflects your configured key). Pressing the key at any point during the intro seeks to the intro end.
Results are cached per file for the app session. Reload detection is also handled: if mpv reloads the same file, SubMiner re-applies the chapter markers without a new API lookup.
## Triggering from mpv
You can trigger AniSkip actions from mpv script-messages:
| Command | Effect |
| ------- | ------ |
| `script-message subminer-skip-intro` | Skip to the intro end immediately (same as pressing the key) |
| `script-message subminer-aniskip-refresh` | Force a fresh lookup for the current file, discarding any cached result |
These are handled by the SubMiner app over the IPC socket.
+22 -9
View File
@@ -3,6 +3,14 @@
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 +23,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.
@@ -29,8 +37,8 @@ In both modes, the enrichment workflow is the same:
4. Fills the translation field from the secondary subtitle or AI.
5. Writes metadata to the miscInfo field.
Polling mode uses the query `"deck:<ankiConnect.deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks.
Known-word sync scope is controlled by `ankiConnect.knownWords.decks` (object map), with `ankiConnect.deck` used as legacy fallback.
Polling mode uses the query `"deck:<ankiConnect.deck>" added:1` to find recently added cards. If no deck is configured, it uses Yomitan's current mining deck when available; otherwise it searches all decks. In Settings, the AnkiConnect deck dropdown auto-fills and persists Yomitan's current mining deck when available, then falls back to the decks reported by AnkiConnect.
Known-word sync scope is controlled by `ankiConnect.knownWords.decks`.
### Proxy Mode Setup (Yomitan / Texthooker)
@@ -147,13 +155,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
}
}
@@ -208,11 +216,15 @@ Animated AVIF requires an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`)
"overwriteImage": true, // replace existing image, or append
"mediaInsertMode": "append", // "append" or "prepend" to field content
"autoUpdateNewCards": true, // auto-update when new card detected
"notificationType": "osd" // "osd", "system", "both", or "none"
"notificationType": "overlay" // "overlay", "system", "both", or "none"
}
}
```
`both` now means overlay + system notification. `osd` and `osd-system` are legacy config-file-only values; set `notificationType` to `"osd-system"` in `config.jsonc` if you previously used `both` and want to keep mpv OSD + system notifications. The Settings window shows `osd` or `osd-system` when already configured, but only offers `overlay`, `system`, `both`, and `none` as normal choices.
When media is available, mined-card overlay and system notifications include the same current-frame thumbnail.
`overwriteAudio` applies to automatic card updates and duplicate-card enrichment. Manual clipboard subtitle updates (`Ctrl/Cmd+C`, then `Ctrl/Cmd+V`) always replace generated sentence audio, while leaving the word audio field unchanged.
## AI Translation
@@ -322,6 +334,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 +347,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": {
@@ -342,7 +355,7 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
"overwriteImage": true,
"mediaInsertMode": "append",
"autoUpdateNewCards": true,
"notificationType": "osd",
"notificationType": "overlay",
},
"ai": {
"enabled": false,
+52 -15
View File
@@ -30,11 +30,11 @@ launcher/ # Standalone CLI launcher wrapper and mpv helpers
plugin/
subminer/ # Modular mpv plugin (init · main · bootstrap · lifecycle · process
# state · messages · hover · ui · options · environment · log
# binary · aniskip · aniskip_match)
# binary)
src/
ai/ # AI translation provider utilities (client, config)
main-entry.ts # Background-mode bootstrap wrapper before loading main.js
main.ts # Entry point delegates to runtime composers/domain modules
main.ts # Entry point - delegates to runtime composers/domain modules
preload.ts # Electron preload bridge
types.ts # Shared type definitions
main/ # Main-process composition/runtime adapters
@@ -130,7 +130,7 @@ src/renderer/
### Launcher + Plugin Runtimes
- `launcher/main.ts` dispatches commands through `launcher/commands/*` and shared config readers in `launcher/config/*`. It handles mpv startup, app passthrough, Jellyfin helper commands, and playback handoff.
- `plugin/subminer/init.lua` runs inside mpv and loads modular Lua files: `main.lua` (orchestration), `bootstrap.lua` (startup), `lifecycle.lua` (connect/disconnect), `process.lua` (process management), `state.lua` (shared state), `messages.lua` (IPC), `hover.lua` (hover-token highlight rendering), `ui.lua` (OSD rendering), `options.lua` (config), `environment.lua` (detection), `log.lua` (logging), `binary.lua` (path resolution), `aniskip.lua` + `aniskip_match.lua` (intro-skip UX).
- `plugin/subminer/init.lua` runs inside mpv and loads modular Lua files: `main.lua` (orchestration), `bootstrap.lua` (startup), `lifecycle.lua` (connect/disconnect), `process.lua` (process management), `state.lua` (shared state), `messages.lua` (IPC), `hover.lua` (hover-token highlight rendering), `ui.lua` (OSD rendering), `options.lua` (config), `environment.lua` (detection), `log.lua` (logging), `binary.lua` (path resolution). AniSkip intro detection lives in the SubMiner app (`src/main/runtime/aniskip-runtime.ts`), which drives mpv chapters and the skip key over the IPC socket.
## Flow Diagram
@@ -226,17 +226,17 @@ Most runtime code follows a dependency-injection pattern:
The composition root (`src/main.ts`) delegates to focused modules in `src/main/` and `src/main/runtime/composers/`:
- `startup.ts` argv/env processing and bootstrap flow
- `app-lifecycle.ts` Electron lifecycle event registration
- `startup-lifecycle.ts` app-ready initialization sequence
- `state.ts` centralized application runtime state container
- `ipc-runtime.ts` IPC channel registration and handler wiring
- `cli-runtime.ts` CLI command parsing and dispatch
- `overlay-runtime.ts` overlay window selection and modal state management
- `subsync-runtime.ts` subsync command orchestration
- `runtime/composers/anilist-tracking-composer.ts` AniList media tracking/probe/retry wiring
- `runtime/composers/jellyfin-runtime-composer.ts` Jellyfin config/client/playback/command/setup composition wiring
- `runtime/composers/mpv-runtime-composer.ts` MPV event/factory/tokenizer/warmup wiring
- `startup.ts` - argv/env processing and bootstrap flow
- `app-lifecycle.ts` - Electron lifecycle event registration
- `startup-lifecycle.ts` - app-ready initialization sequence
- `state.ts` - centralized application runtime state container
- `ipc-runtime.ts` - IPC channel registration and handler wiring
- `cli-runtime.ts` - CLI command parsing and dispatch
- `overlay-runtime.ts` - overlay window selection and modal state management
- `subsync-runtime.ts` - subsync command orchestration
- `runtime/composers/anilist-tracking-composer.ts` - AniList media tracking/probe/retry wiring
- `runtime/composers/jellyfin-runtime-composer.ts` - Jellyfin config/client/playback/command/setup composition wiring
- `runtime/composers/mpv-runtime-composer.ts` - MPV event/factory/tokenizer/warmup wiring
Composer modules share contract conventions via `src/main/runtime/composers/contracts.ts`:
@@ -269,11 +269,48 @@ For domains migrated to reducer-style transitions (for example AniList token/que
- Reducer boundary: when a domain has transition helpers in `src/main/state.ts`, new callsites should route updates through those helpers instead of ad-hoc object mutation in `main.ts` or composers.
- Tests for migrated domains should assert both the intended field changes and non-targeted field invariants.
## Playback Startup Flow
Before the app boots, something has to launch mpv, inject the plugin, and bring the overlay up. SubMiner-managed launches own this step - the `subminer` launcher, the app's own playback, and the packaged Windows shortcut all follow the same path. The launcher reads `config.jsonc`, spawns mpv with the IPC socket and the bundled plugin, and passes runtime settings as `--script-opts`. The plugin never reads a config file: the shipped `subminer.conf` is intentionally empty so command-line opts always win.
Once mpv is up, exactly one of two triggers brings up the overlay. On a first launch the plugin's `file-loaded` hook self-starts the app once the socket is ready (because the launcher injected `auto_start=yes`). When the app is already running - or for explicit `--start-overlay` and YouTube flows - the launcher instead attaches over the control socket and suppresses the plugin's auto-start, so the two never fire together. Both converge on the same app bring-up, which then runs the Program Lifecycle below.
```mermaid
flowchart TB
classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef proc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef app fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef overlay fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
Entry["Managed launch<br/>subminer CLI · app · Windows shortcut"]:::entry
Entry --> Cfg["Launcher reads config.jsonc<br/>→ plugin runtime config"]:::extrt
Cfg --> Spawn["Spawn mpv<br/>--input-ipc-server=/tmp/subminer-socket<br/>--script=plugin/subminer/main.lua<br/>--script-opts=subminer-… (auto_start, backend, …)"]:::proc
Spawn --> Boot["Plugin boot · read_options('subminer')<br/>empty subminer.conf; CLI opts win"]:::extrt
Boot --> Sock["mpv IPC socket ready"]:::proc
Sock --> Who{"Overlay trigger"}:::decision
Who -->|"app already running, or<br/>--start-overlay / YouTube"| Attach["Launcher startOverlay()<br/>attach via control socket<br/>plugin auto-start suppressed"]:::proc
Who -->|"first launch, auto_start=yes"| Self["Plugin file-loaded hook<br/>polls socket → process.start_overlay()"]:::extrt
Attach --> AppUp
Self --> AppUp
AppUp["Spawn / attach SubMiner app<br/>--start --managed-playback --socket … --backend …"]:::app
AppUp --> Ctrl["App control server up<br/>/tmp/subminer-control-* dedupes a 2nd launch"]:::app
Ctrl --> Life["app.whenReady → Program Lifecycle (below)"]:::app
Life --> Conn["MpvIpcClient connects to /tmp/subminer-socket"]:::overlay
Conn --> Show["Transparent overlay over mpv<br/>Yomitan lookup · mine"]:::overlay
```
The runtime sockets in this flow are detailed in [IPC + Runtime Contracts](./ipc-contracts#runtime-sockets).
## Program Lifecycle
- **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.
+189 -7
View File
@@ -1,12 +1,194 @@
# Changelog
## v0.14.0 (2026-05-12)
## v0.15.2 (2026-06-02)
SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin is injected at runtime only when SubMiner launches mpv — through the `subminer` launcher, the app's managed launch, or the packaged Windows SubMiner mpv shortcut. When you open mpv on its own, SubMiner is not involved and the plugin is never loaded. If you have a legacy global SubMiner plugin under mpv's `scripts` directory, first-run setup detects it and prompts you to remove it before playback starts.
**Changed**
- Yomitan: Updated the bundled Yomitan build to the latest vendored revision.
**Fixed**
- Anki - Animated AVIF: Clip timing no longer starts or ends early; word-audio lead-in and clip duration are now aligned to frame boundaries.
- Overlay (Hyprland): Fixed fullscreen overlay alignment - modal, stats, and sidebar content no longer shift below the mpv window.
- Overlay (macOS): Subtitle bars are now interactive immediately after autoplay starts with "wait for overlay to be ready" enabled, without requiring a manual click.
- Overlay (macOS): Fixed overlay, subtitles, and subtitle sidebar staying hidden after a modal closes until the user clicked the mpv window; focus is now restored to mpv when the last modal closes, so playback shortcuts and the overlay reappear correctly - including in native fullscreen.
## v0.15.1 (2026-05-31)
**Fixed**
- **Linux Overlay Stacking**: Fixed the overlay intermittently dropping behind mpv on KDE Plasma and other non-Hyprland/Sway Wayland sessions; restored subtitle hover, pause-on-hover, and Yomitan lookups on X11/XWayland; the overlay now correctly layers above/below mpv based on fullscreen state, yields to foreground windows (Settings, Yomitan, AniList, etc.), and avoids startup flashes and fullscreen transition glitches.
- **Linux Overlay (Hyprland Lua)**: Fixed overlay placement on Hyprland 0.55+ when using a Lua-based config.
- **Manual Overlay Startup**: Fixed manual visible-overlay startup from mpv - now correctly attaches to playback, keeps the window bounds synced with mpv, and primes current subtitles before showing.
- **Playlist Transitions**: Reused the warm overlay when mpv advances to the next playlist item, avoiding a redundant tokenization pause and preserving visible subtitles across tracks.
- **macOS Overlay**: Fixed the visible subtitle overlay staying click-through after pause-until-ready releases playback; restored mpv focus after closing modal windows so subtitles and keybinds resume without clicking the player.
- **Mouse Keybindings**: Fixed keybinding capture and runtime handling for mouse buttons, including side buttons like `MBTN_BACK` and `MBTN_FORWARD`.
- **Windows mpv Shortcut**: Fixed the Windows `SubMiner mpv` shortcut so videos attach to an already-running background app instead of spawning a second process.
**Docs**
- **Troubleshooting**: Updated Hyprland overlay docs with current Lua (`hl.window_rule`) and legacy config syntax; added troubleshooting for KDE/Wayland and other non-Hyprland/Sway Wayland sessions; added a Character Dictionary troubleshooting section; added a "See Also" index linking each feature's troubleshooting page.
## v0.15.0 (2026-05-29)
**Breaking Changes**
- **Subsync:**
- The `subsync.defaultMode` config option has been removed
- Subsync now always opens the manual subtitle picker regardless of any previously set default mode
- **N+1 Highlighting:**
- N+1 highlighting now has its own dedicated `ankiConnect.nPlusOne.enabled` option, separate from known-word highlighting
- It is no longer enabled automatically when known-word highlighting is on - enable it explicitly to keep N+1 annotations
**Added**
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches — open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
- **Auto-Updater:**
- Tray and `subminer -u` update checks with app update prompts
- Launcher and Linux rofi theme auto-updates
- Checksum verification and configurable notifications
- Opt-in prerelease channel via `updates.channel: "prerelease"`
- **Settings Window:**
- New dedicated Settings window via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections
- Click-to-learn keybinding controls
- AnkiConnect-backed deck, field, and note-type pickers that auto-fill from the configured Anki deck
- Cross-category search
- Live save for most options including subtitle CSS, stats keys, logging level, Jimaku, Subsync, and Anki mappings
- AI and translation settings remain config-file only
- **Inline Character Portraits:**
- Optional AniList character portraits appear inline for name-matched subtitle text
- Manual AniList overrides scoped per parent media directory so separate season folders maintain separate character dictionary selections
- **Character Dictionary Manager:** New `Ctrl/Cmd+D` manager modal to remove, reorder, or override loaded entries.
- **Log Export:** Sanitized log ZIP export from the tray menu and via `subminer logs -e`, with home-directory usernames redacted from exported contents.
- **Launcher CLI:**
- `subminer --version` / `subminer -v` prints the installed app version
- `mpv.profile` config and Settings support passes a named mpv profile to managed launches
- Bundled mpv plugin startup options are now configurable from SubMiner config
- **First-Run Setup:**
- Optional installer for Bun and the `subminer` CLI on Linux, macOS, and Windows
- Windows `subminer.cmd` PATH shim so `subminer` works without manually adding `SubMiner.exe` to PATH
- Setup recognizes existing Homebrew or user PATH installs and avoids writing into Homebrew-owned paths
- Includes an Open SubMiner Settings button
- Standalone setup app quits after completing, returning terminal control
- **Primary Subtitle Visibility on Yomitan Popup:** New `subtitleStyle.primaryVisibleOnYomitanPopup` option keeps hover-mode primary subtitles visible while a Yomitan popup is open.
**Changed**
- **Subtitle Appearance Config:**
- Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css`, `subtitleStyle.secondary.css`, and `subtitleSidebar.css`
- Known-word and N+1 annotation colors moved to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`
- Subtitle font defaults updated to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`
- Existing configs migrate automatically; legacy Anki color keys still accepted with deprecation warnings
- **Subtitle Style Defaults:**
- Stronger outline-style text shadow
- Thicker JLPT underlines
- Frequency `topX` default raised to `10000`
- **Character Dictionary:**
- Entries scoped to the current AniList media for name matching and inline portraits
- Generates Japanese-only name aliases so raw romanized/English aliases no longer surface as separate results
- In-app AniList selector waits for an explicit search with the box prefilled from the current filename
- `subtitleStyle.nameMatchEnabled` is now the sole switch for dictionary sync and builds
- **Electron Runtime:** Updated from 39.8.6 to 42.2.0, returning SubMiner to a supported Electron release line.
- **Jellyfin Setup:**
- Removed the server presets dropdown
- Setup now shows a single editable server URL field
- **Jellyfin Cast Identity:**
- Device identity now derived from the OS hostname and always reported as SubMiner
- Previously configurable identity fields are ignored, preventing multiple installs from sharing a remote-session identity
- **Startup Defaults:** Jellyfin remote-session startup warmup and character-name subtitle highlighting now default to off.
- **Setup Appearance:** Removed the bundled mpv runtime plugin readiness card from the setup flow.
**Fixed**
- **AniList Progress:**
- Progress updates fire correctly when playback reaches or skips past the watched threshold, using fresh mpv timing events
- Season-specific results preferred for multi-season files, with a clear message when the matched season is not in Planning or Watching
- Repeated missing-token checks no longer exhaust retry attempts or duplicate dead-letter entries
- **Anki Mining:**
- Sentence-audio padding is opt-in by default
- Animated AVIF freeze-frame duration aligned to word audio length without double-counting
- Multi-line sentence alignment fixed for repeated subtitle text
- Kiku duplicate-card detection, auto-merge, modal acknowledgment race, and field/tag ordering corrected
- YouTube playback cards use mpv's resolved stream URLs
- Sentence cards refresh the secondary subtitle before saving
- **Jellyfin Discovery:**
- Startup, subtitle track selection, and duplicate ready-signal handling all fixed
- Paused mpv no longer misreported as playing
- Resume corrected when a remote play command sends `StartPositionTicks: 0` despite saved progress
- **Jellyfin Remote:**
- Tray checkbox stays in sync on Linux after tray, CLI, or startup changes
- Remote controller visibility and progress sync fixed for seeks, stops, startup path changes, and Linux websocket reconnect windows
- Play and Resume now behave correctly (Play from beginning, Resume from saved position)
- Final progress reports reuse SubMiner's last known position when mpv resets on stop
- Windows setup login flow fixed with an IPC bridge, immediate feedback, and a timeout with inline error for unreachable servers
- **Overlay (macOS):**
- Overlay hides when mpv loses focus, is minimized, or is no longer the foreground app
- Stays stable through transient window geometry disappearances from macOS APIs and when clicking from the overlay back into mpv
- Stats overlay opened inactive so it appears over fullscreen mpv without switching Spaces
- Passthrough fixed so mpv controls stay clickable before hovering a subtitle bar
- **Yomitan Sidebar:**
- Playback stays paused for sidebar-opened Yomitan popups when auto-pause is enabled
- Popups now open when startup races the Yomitan extension load
- Sidebar mining cards use audio and images from the clicked sidebar line instead of the current primary subtitle
- **Launcher:**
- `subminer app` on Linux returns terminal control immediately
- `subminer app --setup` opens the setup flow when SubMiner is already running in the background
- **YouTube Playback:**
- Selected subtitles downloaded to local temp files so the primary bar and sidebar read the same source, with cleanup on reload and quit
- False load-failure notifications suppressed
- Tray icon created on launcher-managed playback that attaches to an already-running process
- **Shortcuts:**
- Native mpv menu shortcuts disabled during managed macOS playback so configured SubMiner shortcuts work while mpv has focus
- Custom session shortcuts including `stats.markWatchedKey` wired through mpv
- Multi-line copy/mine overlay correctly focused so number keys choose the line count on macOS and Windows
- **Controller Bindings:**
- Controller config and debug shortcuts stay closed while controller support is disabled
- Binding learn mode starts from the edit pencil
- Remaps saved per controller profile
- Binding badges also start learn mode
- Row reset buttons restore individual bindings to defaults
- **Logging:**
- `logging.level` forwarded to launcher-started and Windows shortcut-started mpv sessions, covering mpv log verbosity, plugin logging, and plugin-launched app logging
- `logging.rotation` (default 7 days) and per-component `logging.files` toggles added, with mpv logs disabled by default
- Repeated IPC socket warning spam suppressed while waiting for mpv to recreate the socket
- Windows mpv IPC, subtitle track, and Yomitan diagnostics added
- **In-Player Stats:**
- Layering fixed so delete confirmations, overlay modals, and update-check dialogs appear above the stats window
- Jellyfin playback stats grouped by item metadata so watched episodes merge with matching local library titles and keep clean display names
- **WebSocket Annotations:**
- Annotation spans and token metadata stay on the annotation WebSocket
- The regular subtitle WebSocket is plain-text only
- **Subtitle Annotation Prefetching:** Cached colored annotations and character images ready sooner for live subtitle changes without delaying raw subtitle display.
- **Windows Startup Errors:** Fatal startup failures now show a native error dialog and write details to the app log instead of exiting silently.
**Docs**
- **Documentation Site:**
- Published stable docs at the site root with current development docs under `/main/`
- Fixed versioned docs navigation, archived page link handling, and local dev version routing
- Documented all previously undocumented config options including `subtitleStyle.primaryDefaultMode`, `stats.markWatchedKey`, `immersionTracking.lifetimeSummaries.*`, and all seven `mpv.*` launcher options
- Added Playback Startup Flow and Runtime Sockets diagrams to the architecture docs with cross-reference pointers in the MPV Plugin and Troubleshooting pages
<details>
<summary>Internal changes</summary>
**Internal**
- **Release Tooling:**
- Release-note polishing treats pending fragments and reviewed prerelease notes as a cumulative final outcome, collapsing prerelease-only fixes into the final user-facing change
- Prerelease generation reuses existing reviewed notes and merges only new fragment material
- `make clean` preserves `release/prerelease-notes.md`
- **Tests:** Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
</details>
## Previous Versions
<details>
<summary>v0.14.x</summary>
<h2>v0.14.0 (2026-05-12)</h2>
**Added**
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches - open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
- **Primary Subtitle Bar:** Added a `V` shortcut and mpv plugin binding to toggle the primary subtitle bar without affecting mpv's native subtitle visibility.
- **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
@@ -60,11 +242,11 @@ SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin
**Internal**
- Replaced the changelog renderer with an AI polish pass that merges related fragments and writes user-facing release notes. `CHANGELOG.md` keeps internal items in a collapsed `<details>` block; GitHub release notes omit them entirely.
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain - run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
</details>
## Previous Versions
</details>
<details>
<summary>v0.12.x</summary>
@@ -84,8 +266,8 @@ SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity.
- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility.
- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard.
- Stats: Session timeline no longer plots seek-forward/seek-backward markers they were too noisy on sessions with lots of rewinds.
- Stats: Replaced the "Library Per Day" section on the Stats → Trends page with a "Library Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
- Stats: Session timeline no longer plots seek-forward/seek-backward markers - they were too noisy on sessions with lots of rewinds.
- Stats: Replaced the "Library - Per Day" section on the Stats → Trends page with a "Library - Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
**Fixed**
+91 -47
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.
@@ -8,28 +10,25 @@ The dictionary is generated per-media, merged across your recently-watched title
The feature has three stages: **snapshot**, **merge**, and **match**.
1. **Snapshot** When you start watching a new title, SubMiner queries the AniList GraphQL API for the media's character list. Each character's names, reading, role, description, birthday, voice actors, and portrait are fetched and saved as a local JSON snapshot in `character-dictionaries/snapshots/anilist-{mediaId}.json`. Images are downloaded and base64-encoded into the snapshot.
1. **Snapshot** - When you start watching a new title, SubMiner queries the AniList GraphQL API for the media's character list. Each character's names, reading, role, description, birthday, voice actors, and portrait are fetched and saved as a local JSON snapshot in `character-dictionaries/snapshots/anilist-{mediaId}.json`. Images are downloaded and base64-encoded into the snapshot.
2. **Merge** SubMiner maintains a most-recently-used list of media IDs (default: 3). Snapshots from those titles are merged into a single Yomitan ZIP `character-dictionaries/merged.zip` which is always named "SubMiner Character Dictionary" so Yomitan treats it as a single stable dictionary across rebuilds.
2. **Merge** - SubMiner maintains a most-recently-used list of media IDs (default: 3). Snapshots from those titles are merged into a single Yomitan ZIP - `character-dictionaries/merged.zip` - which is always named "SubMiner Character Dictionary" so Yomitan treats it as a single stable dictionary across rebuilds.
3. **Match** During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. Tokens that match a character entry are flagged with `isNameMatch` and highlighted in the overlay with a distinct color.
3. **Match** - During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. SubMiner only accepts character entries for the current AniList media when that media ID is known, then flags matching tokens with `isNameMatch` and highlights them in the overlay with a distinct color.
## Enabling the Feature
Character dictionary sync is disabled by default. To turn it on:
1. Authenticate with AniList (see [AniList Integration](/anilist-integration#setup)).
2. Set `anilist.characterDictionary.enabled` to `true` in your config.
3. Start watching — SubMiner will generate a snapshot for the current media and import the merged dictionary into Yomitan automatically.
1. Enable **Name Match** in Settings → Subtitle Style, or set `subtitleStyle.nameMatchEnabled: true` in your config.
2. Start watching - SubMiner queries AniList's public GraphQL API (no authentication required) and imports the merged dictionary into Yomitan automatically.
3. Optionally enable **Name Match Images** (Settings → Subtitle Style) to show inline circular character portraits next to matched names in subtitles.
```jsonc
{
"anilist": {
"enabled": true,
"accessToken": "your-token",
"characterDictionary": {
"enabled": true,
},
"subtitleStyle": {
"nameMatchEnabled": true,
"nameMatchImagesEnabled": true, // optional - inline portraits
},
}
```
@@ -38,6 +37,10 @@ Character dictionary sync is disabled by default. To turn it on:
The first sync for a media title takes a few seconds while character data and portraits are fetched from AniList. Subsequent launches reuse the cached media match and snapshot without a fresh AniList lookup.
:::
::: info
AniList character data is fetched via public GraphQL queries - no account or access token is needed. AniList authentication is only required for the separate [watch-progress sync](/anilist-integration) feature.
:::
::: warning
If `yomitan.externalProfilePath` is set, SubMiner switches to read-only external-profile mode. In that mode SubMiner can reuse another app's installed Yomitan dictionaries/settings, but SubMiner's own character-dictionary features are fully disabled.
:::
@@ -57,7 +60,7 @@ A single character produces many searchable terms so that names are recognized r
- ア・リ・ス → アリス (combined), plus individual segments
**Honorific suffixes** each base name is expanded with 15 common suffixes:
**Honorific suffixes** - each base name is expanded with 15 common suffixes:
| Honorific | Reading |
| --------- | ---------- |
@@ -77,38 +80,63 @@ A single character produces many searchable terms so that names are recognized r
| 社長 | しゃちょう |
| 部長 | ぶちょう |
**Romanized names** names stored in romaji on AniList are converted to kana aliases so they can match against Japanese subtitle text.
**Romanized names** - names stored in romaji on AniList are converted to kana aliases so they can match against Japanese subtitle text.
This means a character like "太郎" generates entries for 太郎, 太郎さん, 太郎先生, 太郎君, 太郎ちゃん, and so on all with correct readings.
This means a character like "太郎" generates entries for 太郎, 太郎さん, 太郎先生, 太郎君, 太郎ちゃん, and so on - all with correct readings.
## Name Matching
Name matching runs inside Yomitan's scanning pipeline during subtitle tokenization.
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`).
2. Entries from "SubMiner Character Dictionary" are checked with exact primary-source matching - the token must match the entry's `originalText` with `isPrimary: true` and `matchType: 'exact'`.
3. When the current AniList media ID is known, entries whose embedded media ID belongs to a different title are ignored for name matching and inline portraits.
4. Matched tokens are flagged `isNameMatch: true` and forwarded to the renderer.
5. If `subtitleStyle.nameMatchEnabled` is enabled, the renderer applies the name-match highlight color (default: `#f5bde6`).
6. If `subtitleStyle.nameMatchImagesEnabled` is enabled, the renderer also injects a small circular AniList portrait from the cached snapshot image data.
Older snapshot schema versions are regenerated automatically. Current-version snapshots are normally reused, but when `subtitleStyle.nameMatchImagesEnabled` is enabled SubMiner also checks whether the cached snapshot contains usable character portrait data. If it does not, the snapshot is refreshed so the merged dictionary can include images.
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.nameMatchEnabled` | `false` | Enable dictionary sync and highlighting |
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits beside names |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
## Inline Character Portraits
When `subtitleStyle.nameMatchImagesEnabled` is enabled, SubMiner injects a small circular portrait image directly into the subtitle line next to each matched character name.
Portraits are sourced from the local snapshot - they are embedded at snapshot-generation time and served from the cached ZIP, so no network request happens during playback. Images are downloaded from AniList CDN once per character and stored in `character-dictionaries/img/`.
If a snapshot was generated before portrait data was available (e.g. during an earlier version or offline sync), SubMiner detects the missing image data on the next media match and automatically refreshes the snapshot so portraits are included in the next merged dictionary build.
**To enable:**
- Settings → Subtitle Style → **Name Match Images**, or
- `subtitleStyle.nameMatchImagesEnabled: true` in config.
The portrait size is controlled by the surrounding subtitle font size and renders as a circle clipped from the character's AniList cover image.
::: tip
Inline portraits help you quickly associate names with faces while building vocabulary - especially useful for shows with large casts where you're still learning who's who.
:::
## Dictionary Entries
Each character entry in the Yomitan dictionary includes structured content:
- **Name** — native (Japanese) and romanized forms
- **Role badge** — color-coded by role: main (score 100), supporting (90), side (80), background (70)
- **Portrait** character image from AniList, embedded in the ZIP
- **Description** — biography text from AniList (collapsible)
- **Character information** — age, birthday, gender, blood type (collapsible)
- **Voiced by** — voice actor name and portrait (collapsible)
- **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)
- **Character information** - age, birthday, gender, blood type (collapsible)
- **Voiced by** - voice actor name and portrait (collapsible)
The three collapsible sections can be configured to start open or closed:
@@ -128,16 +156,18 @@ The three collapsible sections can be configured to start open or closed:
## Auto-Sync Lifecycle
When `characterDictionary.enabled` is `true`, SubMiner runs an auto-sync routine whenever the active media changes.
When `subtitleStyle.nameMatchEnabled` is `true`, SubMiner runs an auto-sync routine whenever the active media changes.
These phases are emitted through the configured notification surface. Some phases are skipped when unnecessary: `generating` only appears on a cache miss, `building` only appears when the merged ZIP must be rebuilt, and `importing` only appears when Yomitan needs a new dictionary import.
**Phases:**
1. **checking** Is there already a cached snapshot for this media ID?
2. **generating** No cache hit: fetch characters from AniList GraphQL, download portraits (250ms throttle between image requests), save snapshot JSON.
3. **syncing** Add the media ID to the most-recently-used list. Evict old entries beyond `maxLoaded`.
4. **building** Merge active snapshots into a single Yomitan ZIP. A SHA-1 revision hash is computed from the media set if it matches the previously imported revision, the import is skipped.
5. **importing** Push the ZIP into Yomitan. Waits for Yomitan mutation readiness (7-second timeout per operation).
6. **ready** Dictionary is live. Character names will match on the next subtitle line.
1. **checking** - Is there already a cached snapshot for this media ID?
2. **generating** - No cache hit: fetch characters from AniList GraphQL, download portraits (250ms throttle between image requests), save snapshot JSON.
3. **syncing** - Add the media ID to the most-recently-used list. Evict old entries beyond `maxLoaded`.
4. **building** - Merge active snapshots into a single Yomitan ZIP. A SHA-1 revision hash is computed from the media set - if it matches the previously imported revision, the import is skipped.
5. **importing** - Push the ZIP into Yomitan. Waits for Yomitan mutation readiness (7-second timeout per operation).
6. **ready** - Dictionary is live. Character names will match on the next subtitle line.
**State tracking** is persisted in `character-dictionaries/auto-sync-state.json`. AniList media matches are cached separately in `character-dictionaries/anilist-resolution-cache.json` so snapshot hits do not need another AniList search.
@@ -167,10 +197,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 manager with `Ctrl/Cmd+D`, use the **Override** tab/button, edit the prefilled title if needed, then search and choose the correct result.
- CLI: `--dictionary-candidates` still lists matches for the current filename guess.
```bash
# List candidate AniList matches for a file
subminer dictionary --candidates "/path/to/episode.mkv"
@@ -183,10 +216,20 @@ SubMiner.AppImage --dictionary-candidates --dictionary-target "/path/to/episode.
SubMiner.AppImage --dictionary-select --dictionary-anilist-id 21355 --dictionary-target "/path/to/episode.mkv"
# Open the in-app selector from the running app
subminer app --open-character-dictionary
subminer app --session-action '{"actionId":"openCharacterDictionaryManager"}'
```
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.
## Managing Loaded Entries
Open the manager with `Ctrl/Cmd+D` (`shortcuts.openCharacterDictionaryManager`). The manager shows the merged dictionary's active MRU entries, marks the current anime, and lets you adjust eviction priority for the other loaded entries.
- **Remove** drops a non-current entry from the active merged dictionary and rebuilds/imports once.
- **Up/Down** changes MRU order for future eviction without rebuilding.
- **Override** opens the AniList selector for that entry's title so you can replace a saved loaded entry.
The current anime cannot be removed while you are watching it; it stays loaded until playback changes.
## File Structure
@@ -205,7 +248,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** (v17): 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:
@@ -222,20 +265,20 @@ merged.zip
| Option | Default | Description |
| ---------------------------------------------------------------------- | --------- | --------------------------------------------------------------- |
| `anilist.characterDictionary.enabled` | `false` | Enable auto-sync of character dictionary from AniList |
| `anilist.characterDictionary.maxLoaded` | `3` | Number of recent media snapshots kept in the merged dictionary |
| `anilist.characterDictionary.profileScope` | `"all"` | Apply dictionary to `"all"` Yomitan profiles or `"active"` only |
| `anilist.characterDictionary.collapsibleSections.description` | `false` | Start Description section expanded |
| `anilist.characterDictionary.collapsibleSections.characterInformation` | `false` | Start Character Information section expanded |
| `anilist.characterDictionary.collapsibleSections.voicedBy` | `false` | Start Voiced By section expanded |
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting in subtitles |
| `subtitleStyle.nameMatchEnabled` | `false` | Enable character-dictionary sync and name highlighting |
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits beside matched names |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for character-name matches |
## Reference Implementation
SubMiner's character dictionary builder is inspired by the [Japanese Character Name Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) project a standalone Rust web service that generates Yomitan character dictionaries from AniList and VNDB data.
SubMiner's character dictionary builder is inspired by the [Japanese Character Name Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) project - a standalone Rust web service that generates Yomitan character dictionaries from AniList and VNDB data.
The reference implementation covers similar ground name variant generation, honorific expansion, structured Yomitan content, portrait embedding and additionally supports VNDB as a data source for visual novel characters. Key differences:
The reference implementation covers similar ground - name variant generation, honorific expansion, structured Yomitan content, portrait embedding - and additionally supports VNDB as a data source for visual novel characters. Key differences:
| | SubMiner | Reference Implementation |
| ---------------------- | -------------------------------------------- | ------------------------------------- |
@@ -250,14 +293,15 @@ 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.
- **Names not highlighting:** Confirm `subtitleStyle.nameMatchEnabled` is `true`. Check that the current media has an AniList entry - SubMiner needs a media ID to fetch characters.
- **Inline portraits missing:** Confirm `subtitleStyle.nameMatchImagesEnabled` is `true`. On the next character dictionary sync, SubMiner refreshes current-version snapshots that do not contain usable cached character portrait data. Portraits still require AniList to return an image and the image download to succeed.
- **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 manager (`Ctrl/Cmd+D`) to remove/reorder loaded titles, then use **Override** to correct the active AniList match. You can also run `--dictionary-candidates`, then save the correct media with `--dictionary-select --dictionary-anilist-id <id>`. SubMiner ignores character entries from other loaded titles for subtitle name matching and inline portraits once the current media ID is known.
- **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.
## Related
- [Subtitle Annotations](/subtitle-annotations) how name matches interact with N+1, frequency, and JLPT layers
- [AniList Integration](/anilist-integration) — authentication, episode tracking, and AniList settings
- [Configuration Reference](/configuration) full config options
- [Subtitle Annotations](/subtitle-annotations) - how name matches interact with N+1, frequency, and JLPT layers
- [AniList Integration](/anilist-integration) - watch-progress sync and AniList authentication (separate from character dictionary)
- [Configuration Reference](/configuration) - full config options
+244 -213
View File
@@ -8,6 +8,8 @@ outline: [2, 3]
import { withBase } from 'vitepress';
</script>
SubMiner is configured through a single file (`config.jsonc`). Most settings are also editable from the in-app **Settings** window - you rarely need to edit the file by hand. This page is the full reference: it explains the Settings window, where the config file lives, and documents every option grouped by topic. New to SubMiner? The Quick Start below plus the [Settings window](#settings) cover everything most users need.
## Quick Start
For most users, start with this minimal configuration:
@@ -19,7 +21,7 @@ For most users, start with this minimal configuration:
"deck": "YourDeckName",
"knownWords": {
"decks": {
"YourDeckName": ["Word", "Word Reading", "Expression"]
"YourDeckName": ["Word"]
}
},
"fields": {
@@ -31,13 +33,13 @@ For most users, start with this minimal configuration:
}
```
`ankiConnect.deck` is still accepted for backward-compatible polling scope and legacy known-word fallback behavior. For known-word cache scope, prefer `ankiConnect.knownWords.decks` with deck-to-fields mapping.
Use the known-word deck map to choose which Anki decks and note fields feed the known-word cache.
Then customize as needed using the sections below.
## Settings
SubMiner includes a dedicated **Settings** window accessible from the tray menu, the app `--config` flag, or launcher commands such as `subminer --config` and `subminer config`. It is the primary way to configure SubMiner all changes are written directly to `config.jsonc`, so manual file editing is not required for most users.
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:
@@ -50,9 +52,9 @@ The Settings window groups options by workflow instead of mirroring the raw conf
- 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.
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. The AnkiConnect deck field also reads Yomitan's current mining deck and persists it into an empty setting when one is found. Stats mining also uses Yomitan's current mining deck when `ankiConnect.deck` is empty. 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.
The Settings window preserves existing JSONC comments, trailing commas, and unrelated keys. Resetting a field removes the explicit config path so the built-in default applies.
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.
@@ -92,35 +94,10 @@ On macOS, these validation warnings also open a native dialog with full details
SubMiner watches the active config file (`config.jsonc` or `config.json`) while running and applies supported updates automatically.
Hot-reloadable fields:
- `subtitleStyle`
- `subtitleSidebar`
- `keybindings`
- `shortcuts`
- `secondarySub.defaultMode`
- `stats.toggleKey`
- `stats.markWatchedKey`
- `logging.level`
- `youtube.primarySubLanguages`
- `jimaku.*`
- `subsync.*`
- `ankiConnect.ai.enabled`
- `ankiConnect.behavior.autoUpdateNewCards`
- `ankiConnect.knownWords.highlightEnabled`
- `ankiConnect.knownWords.refreshMinutes`
- `ankiConnect.knownWords.addMinedWordsImmediately`
- `ankiConnect.knownWords.matchMode`
- `ankiConnect.knownWords.decks`
- `ankiConnect.nPlusOne.enabled`
- `ankiConnect.nPlusOne.minSentenceWords`
- `ankiConnect.fields.word`
- `ankiConnect.fields.audio`
- `ankiConnect.fields.image`
- `ankiConnect.fields.sentence`
- `ankiConnect.fields.miscInfo`
- `ankiConnect.isLapis.sentenceCardModel`
- `ankiConnect.isKiku.fieldGrouping`
Hot-reloadable settings include subtitle appearance, sidebar controls, keybindings,
logging level, selected source-language preferences, Jimaku/Subsync settings, and
the Anki deck, known-word, N+1, field, sentence-card, and Kiku options listed
in the reference tables below.
When these values change, SubMiner applies them live. Invalid config edits are rejected and the previous valid runtime config remains active.
@@ -128,7 +105,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.
- AnkiConnect transport/proxy/media/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
@@ -171,16 +148,17 @@ 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`
- [**Yomitan**](#yomitan) - Reuse an external read-only Yomitan profile
- [**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
- [**Notifications**](#notifications) - Overlay notification placement
## Core Settings
@@ -191,14 +169,24 @@ Control the minimum log level for runtime output:
```json
{
"logging": {
"level": "info"
"level": "warn",
"rotation": 7,
"files": {
"app": true,
"launcher": true,
"mpv": false
}
}
}
```
| Option | Values | Description |
| ------- | ---------------------------------------- | --------------------------------------------------------- |
| `level` | `"debug"`, `"info"`, `"warn"`, `"error"` | Minimum log level for runtime logging (default: `"info"`) |
| ---------------- | ---------------------------------------- | -------------------------------------------------------------------- |
| `level` | `"debug"`, `"info"`, `"warn"`, `"error"` | Minimum log level for runtime logging (default: `"warn"`) |
| `rotation` | positive integer | Number of days of app, launcher, and mpv logs to retain (default: 7) |
| `files.app` | boolean | Write SubMiner app runtime logs (default: `true`) |
| `files.launcher` | boolean | Write launcher command logs (default: `true`) |
| `files.mpv` | boolean | Write mpv player logs. Enable temporarily for mpv/plugin debugging. |
### Updates
@@ -216,34 +204,55 @@ Configure automatic update checks and update notifications:
```
| Option | Values | Description |
| -------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable automatic background update checks. Manual tray and `subminer -u` checks are always allowed. |
| -------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `updates.enabled` | `true`, `false` | Enable automatic background update checks. Manual tray and `subminer -u` checks are always allowed. |
| `checkIntervalHours` | number | Minimum hours between automatic update checks. Default `24`. |
| `notificationType` | `"system"` \| `"osd"` \| `"both"` \| `"none"` | How SubMiner announces available updates. Default `"system"`. |
| `notificationType` | `"overlay"` \| `"system"` \| `"both"` \| `"none"` | How SubMiner announces available updates. Default `"system"`. `"both"` means overlay + system. |
| `channel` | `"stable"` \| `"prerelease"` | Release channel used for update checks. Use `"prerelease"` to test beta/RC releases. |
When `notificationType` is `"overlay"` or `"both"`, update-available overlay notifications include an **Update** button that starts the app update flow.
`osd` and `osd-system` are legacy config-file-only notification values. The Settings window offers `overlay`, `system`, `both`, and `none`; if your config already contains `osd` or `osd-system`, it is shown as the selected value but not offered as a normal choice. If you previously used `both` for mpv OSD + system notifications, set `notificationType` to `"osd-system"` in `config.jsonc` to keep that behavior.
### Notifications
Configure where overlay notification cards appear:
```json
{
"notifications": {
"overlayPosition": "top-right"
}
}
```
| Option | Values | Description |
| ----------------- | ---------------------------------------- | ------------------------------------------------------------------ |
| `overlayPosition` | `"top-left"` \| `"top"` \| `"top-right"` | Position for in-overlay notification cards. Default `"top-right"`. |
#### Notification history panel
Every overlay notification shown during a session is also recorded in a notification history panel. Press `Ctrl/Cmd+N` (configurable via [`shortcuts.toggleNotificationHistory`](#shortcuts-configuration)) to toggle the panel; the binding works whether the overlay or mpv has focus. The panel slides in from the same edge the notifications use — left when `overlayPosition` is `"top-left"`, and right for `"top-right"` or `"top"` (centered). Character dictionary sync uses one live card but records each distinct phase in history. Each entry can be removed individually, or use **Clear** to empty the history. History is session-only and is not persisted across restarts.
Startup tokenization, subtitle annotation, and character dictionary status follow the configured notification surface. When the surface is `"overlay"` or `"both"`, SubMiner queues those startup notifications until the overlay renderer is ready instead of falling back to mpv OSD. If loading and ready states both finish before the overlay can paint, the loading card is delivered first and then updates to ready shortly after. With `"both"`, character dictionary checking/building/importing/ready status also goes to system notifications; building and importing are only emitted when that work is actually needed. The bundled mpv plugin only shows its startup OSD messages when `ankiConnect.behavior.notificationType` is set to `"osd"` or `"osd-system"` in `config.jsonc`; AniSkip prompts and skip result messages are playback feedback and still route to overlay notifications when configured.
The equivalent direct CLI command is `--playback-feedback <text>` (`playbackFeedback` internally). It sends that one non-empty feedback string through the same route controlled by `ankiConnect.behavior.notificationType`; it does not change the saved config.
### Auto-Start Overlay
Control whether the overlay automatically becomes visible when it connects to mpv:
```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`) |
| -------------------- | --------------- | ----------------------------------------------------- |
| `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 visible-overlay startup, SubMiner brings up the tray and visible overlay shell before tokenization and annotation warmups finish, then releases playback only after autoplay readiness.
On Windows, packaged plugin installs also rewrite the plugin socket path to `\\.\pipe\subminer-socket`.
@@ -258,7 +267,7 @@ Control which startup warmups run in the background versus deferring to first re
"mecab": true,
"yomitanExtension": true,
"subtitleDictionaries": true,
"jellyfinRemoteSession": true
"jellyfinRemoteSession": false
}
}
```
@@ -271,11 +280,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).
@@ -293,9 +302,9 @@ See `config.example.jsonc` for detailed configuration options.
```
| Option | Values | Description |
| --------- | ------------------------- | --------------------------------------------------- |
| `enabled` | `true`, `false`, `"auto"` | Built-in subtitle websocket mode (default: `false`) |
| `port` | number | WebSocket server port (default: 6677) |
| ------------------- | ------------------------- | --------------------------------------------------- |
| `websocket.enabled` | `true`, `false`, `"auto"` | Built-in subtitle websocket mode (default: `false`) |
| `websocket.port` | number | WebSocket server port (default: 6677) |
### Annotation WebSocket
@@ -313,9 +322,9 @@ This stream includes subtitle text plus token metadata (N+1, known-word, frequen
```
| Option | Values | Description |
| --------- | --------------- | -------------------------------------------------------------- |
| `enabled` | `true`, `false` | Toggle annotated websocket stream (independent of `websocket`) |
| `port` | number | Annotation websocket port (default: 6678) |
| ----------------------------- | --------------- | -------------------------------------------------------------- |
| `annotationWebsocket.enabled` | `true`, `false` | Toggle annotated websocket stream (independent of `websocket`) |
| `annotationWebsocket.port` | number | Annotation websocket port (default: 6678) |
### Texthooker
@@ -348,10 +357,10 @@ See `config.example.jsonc` for detailed configuration options.
```json
{
"subtitleStyle": {
"fontColor": "#cad3f5",
"backgroundColor": "transparent",
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"color": "#cad3f5",
"background-color": "transparent",
"font-size": "35px",
"font-weight": "600",
"line-height": "1.35",
@@ -359,17 +368,19 @@ See `config.example.jsonc` for detailed configuration options.
"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)",
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)",
"font-style": "normal",
"backdrop-filter": "blur(6px)"
"backdrop-filter": "blur(6px)",
"--subtitle-hover-token-color": "#f4dbd6",
"--subtitle-hover-token-background-color": "transparent"
},
"secondary": {
"fontColor": "#cad3f5",
"backgroundColor": "transparent",
"css": {
"font-family": "Inter, Noto Sans, Helvetica Neue, sans-serif",
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"color": "#cad3f5",
"background-color": "transparent",
"font-size": "24px",
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)"
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)"
}
}
}
@@ -377,58 +388,60 @@ See `config.example.jsonc` for detailed configuration options.
```
| 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"`) |
| ---------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `primaryDefaultMode` | string | Default primary subtitle bar visibility mode: `"hidden"`, `"visible"`, or `"hover"` (default: `"visible"`) |
| `subtitleStyle.css` | object | CSS declaration object applied to primary subtitles after normal style defaults. Use CSS property names such as `font-size`. |
| `secondary.css` | object | CSS declaration object applied to secondary subtitles after normal secondary style defaults. |
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
| `preserveLineBreaks` | boolean | Preserve line breaks in visible overlay subtitle rendering (`false` by default). Enable to mirror mpv line layout. |
| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text, then resume on leave (`true` by default). |
| `autoPauseVideoOnYomitanPopup` | boolean | Pause playback while the Yomitan popup is open, then resume when the popup closes (`true` by default). |
| `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 (`true` by default) |
| `primaryVisibleOnYomitanPopup` | boolean | Keep hover-mode primary subtitles visible while the Yomitan popup is open (`true` by default). |
| `nameMatchEnabled` | boolean | Enable character dictionary sync and subtitle token coloring for character-name matches (`false` by default) |
| `nameMatchImagesEnabled` | boolean | Show small cached AniList character portraits beside matched character-name tokens (`false` by default) |
| `nameMatchColor` | string | Hex color used for subtitle tokens matched from the SubMiner character dictionary (default: `#f5bde6`) |
| `knownWordColor` | string | Hex color used for known-word subtitle highlights (default: `#a6da95`) |
| `nPlusOneColor` | string | Hex color used for the single N+1 target subtitle highlight (default: `#c6a0f6`) |
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
| `frequencyDictionary.sourcePath` | string | Path to a local frequency dictionary root. Leave empty or omit to use installed/default frequency-dictionary search paths. |
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`1000` by default) |
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`10000` 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 |
Subtitle CSS custom properties:
| CSS Property | Default | Description |
| ----------------------------------------- | ------------- | --------------------------------------- |
| `--subtitle-hover-token-color` | `#f4dbd6` | Hovered subtitle token text color |
| `--subtitle-hover-token-background-color` | `transparent` | Hovered subtitle token background color |
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.
the primary subtitle, secondary subtitle, and sidebar CSS objects. The generated example
uses that same CSS declaration shape.
Frequency dictionary highlighting uses the same dictionary file format as JLPT bundle lookups (`term_meta_bank_*.json` under discovered dictionary directories). A token is highlighted when it has a positive integer `frequencyRank` (lower is more common) and the rank is within `topX`.
Lookup behavior:
- Set `frequencyDictionary.sourcePath` to a directory containing `term_meta_bank_*.json` for a fully custom source.
- Point the source path at a directory containing `term_meta_bank_*.json` for a fully custom source.
- If `sourcePath` is missing or empty, SubMiner searches default install/runtime locations for `frequency-dictionary` directories (for example app resources, user data paths, and current working directory).
- In both cases, only terms with a valid `frequencyRank` are used; everything else falls back to no highlighting.
- `frequencyDictionary.matchMode` controls which token text is used for frequency lookups: `headword` (dictionary form) or `surface` (visible subtitle text).
- Match mode controls which token text is used for frequency lookups: `headword` (dictionary form) or `surface` (visible subtitle text).
- Frequency highlighting skips tokens that look like non-lexical SFX/interjection noise (for example kana reduplication or short kana endings like `っ`), even when dictionary ranks exist.
In `single` mode all highlights use `singleColor`; in `banded` mode tokens map to five ascending color bands from most common to least common inside the topX window.
Character-name highlighting is separate from N+1 and frequency highlighting:
- `nameMatchEnabled` controls whether SubMiner includes character-dictionary name matches in subtitle token metadata and renderer styling.
- `nameMatchEnabled` controls whether SubMiner syncs the character dictionary and includes character-dictionary name matches in subtitle token metadata and renderer styling.
- `nameMatchImagesEnabled` adds small circular portraits beside matched names using the AniList images already cached with character dictionary snapshots.
- `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.
- Matches come from the bundled SubMiner character dictionary, including AniList-synced merged dictionaries when name matching is 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 styling lives in the secondary subtitle CSS object. Any CSS property not set there falls back to the secondary subtitle defaults, then the normal renderer defaults.
**See `config.example.jsonc`** for the complete list of subtitle style configuration options.
@@ -443,32 +456,38 @@ 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",
"fontSize": 16
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP",
"font-size": "16px",
"color": "#cad3f5",
"background-color": "rgba(73, 77, 100, 0.9)",
"--subtitle-sidebar-max-width": "420px"
}
}
}
```
| Option | Values | Description |
| --------------------------- | --------- | ------------------------------------------------------------------------------------------------------- |
| `enabled` | boolean | Enable subtitle sidebar support (`true` by default) |
| --------------------------- | ------- | ------------------------------------------------------------------------------------------------------- |
| `subtitleSidebar.enabled` | boolean | Enable subtitle sidebar support (`true` by default) |
| `autoOpen` | boolean | Open sidebar automatically on overlay startup (`false` by default) |
| `layout` | string | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space to mimic browser-like layout |
| `toggleKey` | string | `KeyboardEvent.code` used to open/close the sidebar (default: `"Backslash"`) |
| `pauseVideoOnHover` | boolean | Pause playback while hovering the sidebar cue list |
| `subtitleSidebar.toggleKey` | string | `KeyboardEvent.code` used to open/close the sidebar (default: `"Backslash"`) |
| `pauseVideoOnHover` | boolean | Pause playback while hovering the sidebar cue list (`true` by default) |
| `autoScroll` | boolean | Keep the active cue in view while playback advances |
| `maxWidth` | number | Maximum sidebar width in CSS pixels (default: `420`) |
| `opacity` | number | Sidebar opacity between `0` and `1` (default: `0.95`) |
| `backgroundColor` | string | Sidebar shell background color |
| `textColor` | hex color | Default cue text color |
| `fontFamily` | string | CSS `font-family` value applied to sidebar cue text |
| `fontSize` | number | Base sidebar cue font size in CSS pixels (default: `16`) |
| `timestampColor` | hex color | Cue timestamp color |
| `activeLineColor` | hex color | Active cue text color |
| `activeLineBackgroundColor` | string | Active cue background color |
| `hoverLineBackgroundColor` | string | Hovered cue background color |
| `subtitleSidebar.css` | object | CSS declaration object applied to the sidebar. Use CSS properties plus sidebar custom properties below. |
Sidebar CSS custom properties:
| CSS Property | Default | Description |
| -------------------------------------------- | --------------------------- | ---------------------------- |
| `--subtitle-sidebar-max-width` | `420px` | Maximum sidebar width |
| `--subtitle-sidebar-timestamp-color` | `#a5adcb` | Cue timestamp color |
| `--subtitle-sidebar-active-line-color` | `#f5bde6` | Active cue text color |
| `--subtitle-sidebar-active-background-color` | `rgba(138, 173, 244, 0.22)` | Active cue background color |
| `--subtitle-sidebar-hover-background-color` | `rgba(54, 58, 79, 0.84)` | Hovered cue background color |
The sidebar is only available when the active subtitle source has been parsed into a cue list. Default colors use Catppuccin Macchiato with a semi-transparent shell so the panel stays readable without feeling like an opaque settings dialog.
@@ -483,7 +502,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:**
@@ -527,18 +546,18 @@ See `config.example.jsonc` for detailed configuration options.
```
| Option | Values | Description |
| ----------------------- | ---------------------------------- | ------------------------------------------------------ |
| `secondarySubLanguages` | string[] | Language codes to auto-load (e.g., `["eng", "en"]`) |
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `secondarySubLanguages` | string[] | Language codes to auto-load (e.g., `["eng", "en"]`); non-Signs/Songs tracks are preferred when several tracks match |
| `autoLoadSecondarySub` | `true`, `false` | Auto-detect and load matching secondary subtitle track |
| `defaultMode` | `"hidden"`, `"visible"`, `"hover"` | Initial display mode (default: `"hover"`) |
`secondarySub.secondarySubLanguages` also acts as the fallback secondary-language priority for managed startup subtitle selection on local playback and YouTube playback.
The secondary-subtitle language list also acts as the fallback secondary-language priority for managed startup subtitle selection on local playback and YouTube playback.
**Display modes:**
- **hidden** Secondary subtitles not shown
- **visible** Always visible at top of overlay
- **hover** Only visible when hovering over the subtitle area (default)
- **hidden** - Secondary subtitles not shown
- **visible** - Always visible at top of overlay
- **hover** - Only visible when hovering over the subtitle area (default)
**See `config.example.jsonc`** for additional secondary subtitle configuration options.
@@ -581,13 +600,15 @@ See `config.example.jsonc` for detailed configuration options and more examples.
{ "key": "ArrowRight", "command": ["seek", 5] },
{ "key": "ArrowLeft", "command": ["seek", -5] },
{ "key": "Shift+ArrowRight", "command": ["seek", 30] },
{ "key": "MBTN_BACK", "command": ["sub-seek", -1] },
{ "key": "MBTN_FORWARD", "command": ["sub-seek", 1] },
{ "key": "KeyR", "command": ["script-binding", "immersive/auto-replay"] },
{ "key": "KeyA", "command": ["script-message", "ankiconnect-add-note"] }
]
}
```
**Key format:** Use `KeyboardEvent.code` values (`Space`, `ArrowRight`, `KeyR`, etc.) with optional modifiers (`Ctrl+`, `Alt+`, `Shift+`, `Meta+`).
**Key format:** Use `KeyboardEvent.code` values (`Space`, `ArrowRight`, `KeyR`, etc.) with optional modifiers (`Ctrl+`, `Alt+`, `Shift+`, `Meta+`). Mouse buttons use mpv button names: `MBTN_LEFT`, `MBTN_MID`, `MBTN_RIGHT`, `MBTN_BACK`, and `MBTN_FORWARD`.
**Disable a default binding:** Set command to `null`:
@@ -621,38 +642,40 @@ See `config.example.jsonc` for detailed configuration options.
"mineSentence": "CommandOrControl+S",
"mineSentenceMultiple": "CommandOrControl+Shift+S",
"markAudioCard": "CommandOrControl+Shift+A",
"openCharacterDictionary": "CommandOrControl+Alt+A",
"openCharacterDictionaryManager": "CommandOrControl+D",
"openRuntimeOptions": "CommandOrControl+Shift+O",
"openSessionHelp": "CommandOrControl+Slash",
"openControllerSelect": "Alt+C",
"openControllerDebug": "Alt+Shift+C",
"openJimaku": "Ctrl+Shift+J",
"toggleSubtitleSidebar": "Backslash",
"toggleNotificationHistory": "CommandOrControl+N",
"multiCopyTimeoutMs": 3000
}
}
```
| Option | Values | Description |
| ----------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| -------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `toggleVisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling visible subtitle overlay (default: `"Alt+Shift+O"`) |
| `copySubtitle` | string \| `null` | Accelerator for copying current subtitle (default: `"CommandOrControl+C"`) |
| `copySubtitleMultiple` | string \| `null` | Accelerator for multi-copy mode (default: `"CommandOrControl+Shift+C"`) |
| `updateLastCardFromClipboard` | string \| `null` | Accelerator for updating card from clipboard (default: `"CommandOrControl+V"`) |
| `triggerFieldGrouping` | string \| `null` | Accelerator for Kiku field grouping on last card (default: `"CommandOrControl+G"`; only active when `behavior.autoUpdateNewCards` is `false`) |
| `triggerFieldGrouping` | string \| `null` | Accelerator for Kiku field grouping on last card (default: `"CommandOrControl+G"`; only active when automatic card updates are disabled) |
| `triggerSubsync` | string \| `null` | Accelerator for running Subsync (default: `"Ctrl+Alt+S"`) |
| `mineSentence` | string \| `null` | Accelerator for creating sentence card from current subtitle (default: `"CommandOrControl+S"`) |
| `mineSentenceMultiple` | string \| `null` | Accelerator for multi-mine sentence card mode (default: `"CommandOrControl+Shift+S"`) |
| `multiCopyTimeoutMs` | number | Timeout in ms for multi-copy/mine digit input (default: `3000`) |
| `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) |
| `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) |
| `openCharacterDictionary` | string \| `null` | Opens the character dictionary AniList selector (default: `"CommandOrControl+Alt+A"`) |
| `openCharacterDictionaryManager` | string \| `null` | Opens the loaded character dictionary manager (default: `"CommandOrControl+D"`) |
| `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) |
| `openSessionHelp` | string \| `null` | Opens the in-overlay session help modal (default: `"CommandOrControl+Slash"`) |
| `openControllerSelect` | string \| `null` | Opens the controller config/remap modal (default: `"Alt+C"`) |
| `openControllerDebug` | string \| `null` | Opens the controller debug modal (default: `"Alt+Shift+C"`) |
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
| `toggleSubtitleSidebar` | string \| `null` | Dispatches the subtitle sidebar toggle action (default: `"Backslash"`). `subtitleSidebar.toggleKey` remains the primary bare-key setting. |
| `toggleNotificationHistory` | string \| `null` | Toggles the overlay notification history panel (default: `"CommandOrControl+N"`). The panel slides in from the same edge as notifications (right when notifications are centered). |
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
@@ -677,7 +700,7 @@ Important behavior:
- Learned bindings are saved under `controller.profiles` for the selected controller id. Global `controller.bindings` remains the fallback for controllers without a profile.
- `Alt+Shift+C` opens the debug modal by default, and you can remap that shortcut through `shortcuts.openControllerDebug`.
- The debug modal shows raw axes/button values plus a ready-to-copy `buttonIndices` config block.
- `controller.buttonIndices` is a semantic reference/legacy mapping. Changing it does not rewrite the raw numeric descriptor values already stored under `controller.bindings`.
- The button-index map is a semantic reference mapping. Changing it does not rewrite the raw numeric descriptor values already stored under controller bindings.
- Turning keyboard-only mode off clears the keyboard-only token highlight state.
- Closing the Yomitan popup clears the temporary native text-selection fill, but keeps controller token selection active.
@@ -766,11 +789,11 @@ If you bind a discrete action to an axis manually, include `direction`:
}
```
Treat `controller.buttonIndices` as reference-only unless you are still using legacy semantic bindings or copying values from the debug modal. Updating `controller.buttonIndices` alone does not rewrite the hardcoded raw numeric values already present in `controller.bindings` or `controller.profiles.*.bindings`. If you need a real remap, prefer the `Alt+C` learn flow so both the source and the descriptor shape stay correct.
Treat the button-index map as reference-only unless you are copying values from the debug modal. Updating it alone does not rewrite the hardcoded raw numeric values already present in controller bindings or controller profiles. If you need a real remap, prefer the `Alt+C` learn flow so both the source and the descriptor shape stay correct.
If you choose to bind `L2` or `R2` manually, set `triggerInputMode` to `analog` and tune `triggerDeadzone` when your controller reports triggers as analog values instead of digital pressed/not-pressed buttons. `auto` accepts either style and remains the default.
If one controller reports non-standard raw button numbers, override `controller.profiles["<controller id>"].buttonIndices` using values from the `Alt+Shift+C` debug modal. Use global `controller.buttonIndices` only when the mapping should apply to every controller without a profile.
If one controller reports non-standard raw button numbers, override that controller profile's button-index map using values from the `Alt+Shift+C` debug modal. Use the global button-index map only when the mapping should apply to every controller without a profile.
If you update this controller documentation or the generated controller examples, run `bun run docs:test` and `bun run docs:build` before merging.
@@ -778,19 +801,19 @@ Tune `scrollPixelsPerSecond`, `horizontalJumpPixels`, deadzones, repeat timing,
### Manual Card Update Shortcuts
When `behavior.autoUpdateNewCards` is set to `false`, new cards are detected but not automatically updated. Use these keyboard shortcuts for manual control:
When automatic card updates are disabled, new cards are detected but not automatically updated. Use these keyboard shortcuts for manual control:
| Shortcut | Action |
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
| -------------- | ------------------------------------------------------------------------------------------------------------- |
| `Ctrl+C` | Copy the current subtitle line to clipboard (preserves line breaks) |
| `Ctrl+Shift+C` | Enter multi-copy mode. Press `1-9` to copy that many recent lines, or `Esc` to cancel. Timeout: 3 seconds |
| `Ctrl+V` | Update the last added Anki card using subtitles from clipboard |
| `Ctrl+G` | Trigger Kiku duplicate field grouping for the last added card (only when `behavior.autoUpdateNewCards` is `false`) |
| `Ctrl+G` | Trigger Kiku duplicate field grouping for the last added card (only when automatic card updates are disabled) |
| `Ctrl+S` | Create a sentence card from the current subtitle line |
| `Ctrl+Shift+S` | Enter multi-mine mode. Press `1-9` to create a sentence card from that many recent lines, or `Esc` to cancel |
| `Ctrl+Shift+V` | Cycle secondary subtitle display mode (hidden → visible → hover) |
| `Ctrl+Shift+A` | Mark the last added Anki card as an audio card (sets IsAudioCard, SentenceAudio, Sentence, Picture) |
| `Ctrl+Alt+A` | Open character dictionary AniList selector |
| `Ctrl+D` | Open loaded character dictionary manager |
| `Ctrl+Shift+O` | Open runtime options palette (session-only live toggles) |
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist (fixed, not currently configurable) |
@@ -831,16 +854,11 @@ When config hot-reload updates shortcut/keybinding/style values, close and reope
Use the runtime options palette to toggle settings live while SubMiner is running. These changes are session-only and reset on restart.
Current runtime options:
Current runtime options cover automatic card updates, known-word highlighting,
JLPT underlines, frequency highlighting, known-word match mode, and Kiku field
grouping mode.
- `ankiConnect.behavior.autoUpdateNewCards` (`On` / `Off`)
- `ankiConnect.knownWords.highlightEnabled` (`On` / `Off`)
- `subtitleStyle.enableJlpt` (`On` / `Off`)
- `subtitleStyle.frequencyDictionary.enabled` (`On` / `Off`)
- `ankiConnect.knownWords.matchMode` (`headword` / `surface`)
- `ankiConnect.isKiku.fieldGrouping` (`auto` / `manual` / `disabled`)
Annotation toggles (`nPlusOne`, `enableJlpt`, `frequencyDictionary.enabled`) only apply to new subtitle lines after the toggle. The currently displayed line is not re-tokenized in place.
Annotation toggles only apply to new subtitle lines after the toggle. The currently displayed line is not re-tokenized in place.
Default shortcut: `Ctrl+Shift+O`
@@ -855,8 +873,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
{
@@ -864,6 +881,7 @@ 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
}
@@ -871,18 +889,18 @@ Anki reads this provider directly. Legacy subtitle fallback keeps the same provi
```
| Option | Values | Description |
| ------------------ | -------------------- | ------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable shared AI provider features |
| ------------------ | -------------------- | ------------------------------------------------------------------------------------ |
| `ai.enabled` | `true`, `false` | Enable shared AI provider features (default: `false`) |
| `apiKey` | string | Static API key for the shared provider |
| `apiKeyCommand` | string | Shell command used to resolve the API key |
| `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 |
| `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:
- Anki translation/enrichment when `ankiConnect.ai.enabled` is `true`
- Anki translation/enrichment when Anki AI is enabled
### AnkiConnect
@@ -895,7 +913,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"
@@ -912,8 +930,8 @@ Enable automatic Anki card creation and updates with media generation:
},
"ai": {
"enabled": false,
"model": "openai/gpt-4o-mini",
"systemPrompt": "Translate mined sentence text only."
"model": "",
"systemPrompt": ""
},
"media": {
"generateAudio": true,
@@ -921,13 +939,13 @@ Enable automatic Anki card creation and updates with media generation:
"imageType": "static",
"imageFormat": "jpg",
"imageQuality": 92,
"imageMaxWidth": 1280,
"imageMaxHeight": 720,
"imageMaxWidth": 0,
"imageMaxHeight": 0,
"animatedFps": 10,
"animatedMaxWidth": 640,
"animatedMaxHeight": 360,
"animatedMaxHeight": 0,
"animatedCrf": 35,
"audioPadding": 0.5,
"audioPadding": 0,
"fallbackDuration": 3,
"maxMediaDuration": 30
},
@@ -940,8 +958,8 @@ Enable automatic Anki card creation and updates with media generation:
"pattern": "[SubMiner] %f (%t)"
},
"isLapis": {
"enabled": true,
"sentenceCardModel": "Japanese sentences"
"enabled": false,
"sentenceCardModel": "Lapis"
},
"isKiku": {
"enabled": false,
@@ -957,8 +975,8 @@ This example is intentionally compact. The option table below documents availabl
**Requirements:** [AnkiConnect](https://github.com/FooSoft/anki-connect) plugin must be installed and running in Anki. ffmpeg must be installed for media generation.
| Option | Values | Description |
| ------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `true`) |
| ------------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ankiConnect.enabled` | `true`, `false` | Enable AnkiConnect integration (default: `true`) |
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
| `pollingRate` | number (ms) | How often to check for new cards in polling mode (default: `3000`; ignored for direct proxy `addNote`/`addNotes` updates) |
| `proxy.enabled` | `true`, `false` | Enable local AnkiConnect-compatible proxy for push-based auto-enrichment (default: `true`) |
@@ -966,8 +984,7 @@ This example is intentionally compact. The option table below documents availabl
| `proxy.port` | number | Bind port for local AnkiConnect proxy (default: `8766`) |
| `proxy.upstreamUrl` | string (URL) | Upstream AnkiConnect URL that proxy forwards to (default: `http://127.0.0.1:8765`) |
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
| `ankiConnect.deck` | string | Legacy Anki polling/compatibility scope. Newer known-word cache scoping should use `ankiConnect.knownWords.decks`. |
| `ankiConnect.knownWords.decks` | object | Deck→fields mapping for known-word cache queries (for example `{ "Kaishi 1.5k": ["Word", "Word Reading"] }`). |
| `ankiConnect.deck` | string | Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to use the Yomitan mining deck when available. In Settings, this dropdown auto-fills and persists Yomitan's current mining deck when available. |
| `fields.word` | string | Card field for mined word / expression text (default: `Expression`) |
| `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) |
| `fields.image` | string | Card field for images (default: `Picture`) |
@@ -989,21 +1006,21 @@ 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 generated sentence media timing (default: `0`). Animated AVIF clips include the same padded source range as sentence audio. |
| `media.fallbackDuration` | number (seconds) | Default duration if timing unavailable (default: `3.0`) |
| `media.maxMediaDuration` | number (seconds) | Max duration for generated media from multi-line copy (default: `30`, `0` to disable) |
| `behavior.overwriteAudio` | `true`, `false` | Replace existing audio on updates; when `false`, new audio is appended/prepended per `behavior.mediaInsertMode`; manual clipboard updates always replace generated sentence audio (default: `true`) |
| `behavior.overwriteImage` | `true`, `false` | Replace existing images on updates; when `false`, new images are appended/prepended per `behavior.mediaInsertMode` (default: `true`) |
| `behavior.overwriteAudio` | `true`, `false` | Replace existing audio on updates; when `false`, new audio is appended/prepended using the configured media insert mode; manual clipboard updates always replace generated sentence audio (default: `true`) |
| `behavior.overwriteImage` | `true`, `false` | Replace existing images on updates; when `false`, new images are appended/prepended using the configured media insert mode (default: `true`) |
| `behavior.mediaInsertMode` | `"append"`, `"prepend"` | Where to insert new media when overwrite is off (default: `"append"`) |
| `behavior.highlightWord` | `true`, `false` | Highlight the word in sentence context (default: `true`) |
| `ankiConnect.knownWords.highlightEnabled` | `true`, `false` | Enable fast local highlighting for words already known in Anki (default: `false`) |
| `ankiConnect.knownWords.addMinedWordsImmediately` | `true`, `false` | Add words from successful mines into the local known-word cache immediately (default: `true`) |
| `ankiConnect.knownWords.matchMode` | `"headword"`, `"surface"` | Matching strategy for known-word highlighting (default: `"headword"`). `headword` uses token headwords; `surface` uses visible subtitle text. |
| `ankiConnect.knownWords.refreshMinutes` | number | Minutes between known-word cache refreshes (default: `1440`) |
| `ankiConnect.knownWords.decks` | object | Deck→fields mapping used for known-word cache query scope (e.g. `{ "Kaishi 1.5k": ["Word", "Word Reading"] }`). |
| `ankiConnect.knownWords.decks` | object | Deck→fields mapping used for known-word cache query scope (e.g. `{ "Kaishi 1.5k": ["Word"] }`). |
| `ankiConnect.nPlusOne.enabled` | `true`, `false` | Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Independent from `knownWords.highlightEnabled`. Requires known-word cache data (default: `false`). |
| `ankiConnect.nPlusOne.minSentenceWords` | number | Minimum number of words required in a sentence before single unknown-word N+1 highlighting can trigger (default: `3`). |
| `behavior.notificationType` | `"osd"`, `"system"`, `"both"`, `"none"` | Notification type on card update (default: `"osd"`) |
| `behavior.notificationType` | `"overlay"`, `"system"`, `"both"`, `"none"` | Notification type on card update (default: `"overlay"`). `"both"` means overlay + system. `osd` and `osd-system` are legacy config-file-only values; use `"osd-system"` to keep the old OSD + system behavior. |
| `behavior.autoUpdateNewCards` | `true`, `false` | Automatically update cards on creation (default: `true`) |
| `metadata.pattern` | string | Format pattern for metadata: `%f`=filename, `%F`=filename+ext, `%t`=time |
| `isLapis` | object | Lapis/shared sentence-card config: `{ enabled, sentenceCardModel }`. Sentence/audio field names are fixed to `Sentence` and `SentenceAudio`. |
@@ -1037,21 +1054,21 @@ SubMiner is intentionally built for [Kiku](https://kiku.youyoumu.my.id/) and [La
### N+1 Word Highlighting
When `ankiConnect.knownWords.highlightEnabled` is enabled, SubMiner builds a local cache of known words from Anki to highlight already learned tokens in subtitle rendering.
When known-word highlighting is enabled, SubMiner builds a local cache of known words from Anki to highlight already learned tokens in subtitle rendering.
Known-word cache policy:
- Initial sync runs when the integration starts if the cache is missing or stale.
- `ankiConnect.knownWords.refreshMinutes` controls the minimum time between refreshes; between refreshes, cached words are reused without querying Anki.
- The refresh interval controls the minimum time between syncs; between refreshes, cached words are reused without querying Anki.
- `subtitleStyle.nPlusOneColor` sets the color for the single target token when exactly one eligible unknown word exists.
- `ankiConnect.nPlusOne.minSentenceWords` sets the minimum token count required in a sentence for N+1 highlighting (default: `3`).
- The N+1 minimum sentence-word setting controls the token count required before N+1 highlighting can trigger.
- `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.
- The known-word deck map accepts an object keyed by deck name.
- Prefer expression/word fields such as `Expression` or `Word`. Avoid reading-only fields unless you intentionally want homophone readings to count as known words.
- 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.
- 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.
- Cache lookups are in-memory. By default, token headwords are matched against cached `Expression` / `Word` values; set known-word matching to `"surface"` for raw subtitle text matching.
- A known-word cache match always receives known-word highlighting, even when part-of-speech filters suppress N+1, frequency, or JLPT annotations for that token.
- If AnkiConnect is unreachable, the cache remains in its previous state and an on-screen/system status message is shown.
- Known-word sync activity is logged at `INFO`/`DEBUG` level with the `anki` logger scope and includes scope, notes returned, and word counts.
@@ -1111,17 +1128,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)
- [`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
```json
{
"subsync": {
"defaultMode": "auto",
"alass_path": "",
"ffsubsync_path": "",
"ffmpeg_path": "",
@@ -1131,13 +1147,14 @@ 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>`. |
Stats dashboard sentence mining also uses `alass_path` when available to align a local English sidecar against the local Japanese sidecar before filling the card translation field. This stats-only retime writes a temporary cached copy and never edits the original subtitle files.
Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
Customize it there, or set it to `null` to disable.
@@ -1151,10 +1168,7 @@ AniList integration is opt-in and disabled by default. Enable it to allow SubMin
"enabled": true,
"accessToken": "",
"characterDictionary": {
"enabled": false,
"refreshTtlHours": 168,
"maxLoaded": 3,
"evictionPolicy": "delete",
"profileScope": "all",
"collapsibleSections": {
"description": false,
@@ -1167,13 +1181,10 @@ AniList integration is opt-in and disabled by default. Enable it to allow SubMin
```
| Option | Values | Description |
| -------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
| -------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |
| `anilist.enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
| `accessToken` | string | Optional explicit AniList access token override (default: empty string) |
| `characterDictionary.enabled` | `true`, `false` | Enable automatic import/update of the merged SubMiner character dictionary for recent AniList media |
| `characterDictionary.refreshTtlHours` | number | Legacy compatibility setting. Parsed and preserved, but merged dictionary retention is now usage-based |
| `characterDictionary.maxLoaded` | number | Maximum number of most-recently-used AniList media snapshots included in the merged dictionary (default: `3`) |
| `characterDictionary.evictionPolicy` | `"delete"`, `"disable"` | Legacy compatibility setting. Parsed and preserved, but merged dictionary eviction is now usage-based |
| `characterDictionary.collapsibleSections.description` | `true`, `false` | Open the Description section by default in generated dictionary entries |
| `characterDictionary.collapsibleSections.characterInformation` | `true`, `false` | Open the Character Information section by default in generated dictionary entries |
| `characterDictionary.collapsibleSections.voicedBy` | `true`, `false` | Open the Voiced by section by default in generated dictionary entries |
@@ -1200,7 +1211,7 @@ Current post-watch behavior:
Setup flow details:
1. Set `anilist.enabled` to `true`.
2. Leave `anilist.accessToken` empty and restart SubMiner (or run `--anilist-setup`) to trigger setup.
2. Leave the AniList access-token field empty and restart SubMiner (or run `--anilist-setup`) to trigger setup.
3. Approve access in AniList.
4. Callback flow returns to SubMiner via `subminer://anilist-setup?...`, and SubMiner stores the token automatically.
- Encryption backend: Linux defaults to `gnome-libsecret`.
@@ -1208,7 +1219,7 @@ Setup flow details:
Token + detection notes:
- `anilist.accessToken` can be set directly in config; when blank, SubMiner uses the locally stored encrypted token from setup.
- The AniList access token can be set directly in config; when blank, SubMiner uses the locally stored encrypted token from setup.
- Detection quality is best when `guessit` is installed and available on `PATH`.
- When `guessit` cannot parse or is missing, SubMiner falls back automatically to internal filename parsing.
@@ -1244,7 +1255,7 @@ External-profile mode behavior:
- SubMiner does not open its own Yomitan settings window in this mode.
- SubMiner does not import, delete, or update dictionaries/settings in the external profile.
- SubMiner character-dictionary features are fully disabled in this mode, including auto-sync, manual generation, and subtitle-side character-dictionary annotations.
- First-run setup does not require any internal dictionaries while this mode is configured. If you later launch without `yomitan.externalProfilePath`, setup will require at least one internal Yomitan dictionary unless SubMiner already finds one.
- First-run setup does not require any internal dictionaries while this mode is configured. If you later launch without an external Yomitan profile, setup will require at least one internal Yomitan dictionary unless SubMiner already finds one.
### Jellyfin
@@ -1260,7 +1271,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"],
@@ -1270,26 +1280,22 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
```
| Option | Values | Description |
| -------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------ |
| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
| -------------------------- | --------------- | ------------------------------------------------------------------------------------------------------ |
| `jellyfin.enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
| `serverUrl` | string (URL) | Jellyfin server base URL |
| `recentServers` | string[] | Recent Jellyfin server URLs shown in setup; entries are trimmed, deduped, and capped at 5 |
| `username` | string | Default username used by `--jellyfin-login` |
| `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`) |
| `remoteControlAutoConnect` | `true`, `false` | Auto-connect Jellyfin remote session on app startup (requires Jellyfin integration and remote control) |
| `autoAnnounce` | `true`, `false` | Auto-run cast-target visibility announce check on connect (default: `false`) |
| `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. The legacy `jellyfin.accessToken` and `jellyfin.userId` config keys are not resolver-backed settings in the current runtime. The Settings window also hides low-level client identity and default library fields (`deviceId`, `clientName`, `clientVersion`, and `defaultLibraryId`) so normal setup stays focused on server, auth, playback, and remote-control behavior.
Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted storage after login/setup. SubMiner reports the Jellyfin client as `SubMiner`, derives the Jellyfin device id and visible device name from the OS hostname, and owns the client version internally. The Settings window also hides low-level default library fields (`defaultLibraryId`) so normal setup stays focused on server, auth, playback, and remote-control behavior.
- On Linux, token storage defaults to `gnome-libsecret` for `safeStorage`. Override with `--password-store=<backend>` on launcher/app invocations when needed.
@@ -1304,7 +1310,9 @@ Launcher subcommands:
See [Jellyfin Integration](/jellyfin-integration) for the full setup and cast-to-device guide.
Jellyfin remote auto-connect runs only when all three are `true`: `jellyfin.enabled`, `jellyfin.remoteControlEnabled`, and `jellyfin.remoteControlAutoConnect`.
Jellyfin remote auto-connect runs only when Jellyfin integration, remote control, and remote auto-connect are all enabled.
Jellyfin playback auto-launched through SubMiner loads the mpv plugin the same way regular playback does, and shows the visible subtitle overlay automatically so `subtitleStyle` applies to subtitles selected from Jellyfin.
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.
@@ -1324,8 +1332,8 @@ Discord Rich Presence is enabled by default. SubMiner publishes a polished activ
```
| Option | Values | Description |
| ------------------ | ------------------------------------------------ | ---------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable Discord Rich Presence updates (default: `true`) |
| ------------------------- | ------------------------------------------------ | ---------------------------------------------------------- |
| `discordPresence.enabled` | `true`, `false` | Enable Discord Rich Presence updates (default: `true`) |
| `presenceStyle` | `"default"`, `"meme"`, `"japanese"`, `"minimal"` | Card text preset (default: `"default"`) |
| `updateIntervalMs` | number | Minimum interval between activity updates in milliseconds |
| `debounceMs` | number | Debounce window for bursty playback events in milliseconds |
@@ -1390,7 +1398,7 @@ Enable or disable local immersion analytics stored in SQLite for mined subtitles
| Option | Values | Description |
| ------------------------------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `enabled` | `true`, `false` | Enable immersion tracking. Defaults to `true`. |
| `immersionTracking.enabled` | `true`, `false` | Enable immersion tracking. Defaults to `true`. |
| `dbPath` | string | Optional SQLite database path. Leave empty to use default app-data path at `<config dir>/immersion.sqlite`. |
| `batchSize` | integer (`1`-`10000`) | Buffered writes per transaction. Default `25`. |
| `flushIntervalMs` | integer (`50`-`60000`) | Maximum queue delay before flush. Default `500ms`. |
@@ -1405,6 +1413,9 @@ Enable or disable local immersion analytics stored in SQLite for mined subtitles
| `retention.dailyRollupsDays` | integer (`0`-`36500`) | Daily rollup retention window. Default `0` (keep all). |
| `retention.monthlyRollupsDays` | integer (`0`-`36500`) | Monthly rollup retention window. Default `0` (keep all). |
| `retention.vacuumIntervalDays` | integer (`0`-`3650`) | Minimum spacing between `VACUUM` passes. `0` disables vacuum. Default `0` (disabled). |
| `lifetimeSummaries.global` | `true`, `false` | Maintain global lifetime stats rows (default: `true`). |
| `lifetimeSummaries.anime` | `true`, `false` | Maintain per-anime lifetime stats rows (default: `true`). |
| `lifetimeSummaries.media` | `true`, `false` | Maintain per-media lifetime stats rows (default: `true`). |
You can also disable immersion tracking for a single session using:
@@ -1434,6 +1445,7 @@ Configure the local stats UI served from SubMiner and the in-app stats overlay t
{
"stats": {
"toggleKey": "Backquote",
"markWatchedKey": "KeyW",
"serverPort": 6969,
"autoStartServer": true,
"autoOpenBrowser": false
@@ -1443,7 +1455,8 @@ Configure the local stats UI served from SubMiner and the in-app stats overlay t
| Option | Values | Description |
| ----------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| `toggleKey` | Electron key code | Overlay-local key code used to toggle the stats overlay. Default `Backquote`. |
| `stats.toggleKey` | Electron key code | Overlay-local key code used to toggle the stats overlay. Default `Backquote`. |
| `markWatchedKey` | Electron key code | Key code to mark the current video as watched and advance to the next playlist entry. Default `KeyW`. |
| `serverPort` | integer | Localhost port for the browser stats UI. Default `6969`. |
| `autoStartServer` | `true`, `false` | Start the local stats HTTP server automatically once immersion tracking is active. Default `true`. |
| `autoOpenBrowser` | `true`, `false` | When `subminer stats` starts the server on demand, also open the dashboard in your default browser. Default `false`. |
@@ -1453,31 +1466,49 @@ Usage notes:
- The browser UI is served at `http://127.0.0.1:<serverPort>`.
- The overlay toggle is local to the focused visible overlay window; it is not registered as a global OS shortcut.
- The dashboard reads from the same immersion-tracking database, so keep `immersionTracking.enabled` on if you want data to appear.
- The UI includes Overview, Library, Trends, Vocabulary, and Sessions tabs.
- The UI includes Overview, Library, Trends, Vocabulary, Search, and Sessions tabs.
### 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": "",
"launchMode": "normal"
"launchMode": "normal",
"profile": "",
"socketPath": "\\\\.\\pipe\\subminer-socket",
"backend": "auto",
"autoStartSubMiner": true,
"pauseUntilOverlayReady": true,
"subminerBinaryPath": "",
"aniskipEnabled": true,
"aniskipButtonKey": "TAB"
}
}
```
| 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"`) |
| `socketPath` | string | mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin (default: `\\\\.\\pipe\\subminer-socket`) |
| `backend` | `"auto"` \| `"hyprland"` \| `"sway"` \| `"x11"` \| `"macos"` \| `"windows"` | Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform (default: `"auto"`) |
| `autoStartSubMiner` | `true`, `false` | Start SubMiner in the background when SubMiner-managed mpv loads a file (default: `true`) |
| `pauseUntilOverlayReady` | `true`, `false` | Pause mpv on visible-overlay auto-start until SubMiner signals subtitle tokenization readiness, with a 30-second fallback (default: `true`) |
| `subminerBinaryPath` | string | SubMiner app binary path passed to the bundled mpv plugin. Leave empty to use the launcher-detected app path (default: `""`) |
| `aniskipEnabled` | `true`, `false` | Enable AniSkip intro detection, chapter markers, and the skip-intro key (default: `true`) |
| `aniskipButtonKey` | string | mpv key used to skip the detected intro while the skip prompt is visible (default: `"TAB"`) |
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.
- **`maximized`** mpv starts maximized via `--window-maximized=yes`, keeping taskbar access.
- **`fullscreen`** mpv starts in true fullscreen via `--fullscreen`.
- **`normal`** - mpv opens at its default window size with no extra flags.
- **`maximized`** - mpv starts maximized via `--window-maximized=yes`, keeping taskbar access.
- **`fullscreen`** - mpv starts in true fullscreen via `--fullscreen`.
### YouTube Playback Settings
@@ -1501,14 +1532,14 @@ Current launcher behavior:
- If YouTube/mpv already exposes an authoritative matching subtitle track, SubMiner reuses it; otherwise it downloads and injects only the missing side.
- SubMiner loads the primary subtitle plus a best-effort secondary subtitle.
- Playback waits only for primary subtitle readiness; secondary failures do not block playback.
- English secondary subtitles are selected from `secondarySub.secondarySubLanguages` when primary language matches are unavailable.
- English secondary subtitles are selected from the secondary-subtitle language list when primary language matches are unavailable.
- Native mpv secondary subtitle rendering stays hidden during this flow so the SubMiner overlay remains the visible secondary subtitle surface.
- If primary subtitle loading fails, use `Ctrl+Alt+C` to open the subtitle modal and pick a track.
Language targets are derived from subtitle config:
- primary track: `youtube.primarySubLanguages` (falls back to `["ja","jpn"]`)
- secondary track: `secondarySub.secondarySubLanguages` (falls back to English when empty)
- secondary track: secondary-subtitle language list (falls back to English when empty)
- Local playback uses the same priorities after mpv reports subtitle track metadata, so sidecar/internal mixed sets can override an incorrect initial `sid=auto` pick.
- Tracks are resolved and loaded before mpv starts; the older launcher mode switch has been removed.
+2 -2
View File
@@ -1,6 +1,6 @@
# Feature Demos
Short recordings of SubMiner's key features and integrations from real playback sessions.
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';
@@ -25,7 +25,7 @@ Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner aut
## Subtitle Download & Sync
Search and download subtitles from Jimaku, then 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="withBase(`/assets/demos/subtitle-sync-poster.jpg?v=${v}`)">
<source :src="withBase(`/assets/demos/subtitle-sync.webm?v=${v}`)" type="video/webm" />
+11 -11
View File
@@ -11,15 +11,10 @@ For internal architecture/workflow guidance, use `docs/README.md` at the repo ro
```bash
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
cd SubMiner
# if you cloned without --recurse-submodules:
git submodule update --init --recursive
bun install
(cd stats && bun install --frozen-lockfile)
(cd vendor/texthooker-ui && bun install --frozen-lockfile)
make deps
```
`make deps` is still available as a convenience wrapper around the same dependency install flow.
`make deps` initializes submodules and installs root, `stats/`, and `vendor/texthooker-ui` dependencies. The Yomitan submodule installs its own dependencies on demand during `bun run build`.
## Building
@@ -68,10 +63,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
@@ -211,7 +211,7 @@ Run `make help` for a full list of targets. Key ones:
| `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 deps` | Init submodules and install root/stats/texthooker-ui deps |
| `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 |
+36 -20
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
@@ -12,8 +18,8 @@ Episode completion for local `watched` state uses the shared `DEFAULT_MIN_WATCH_
{
"immersionTracking": {
"enabled": true,
"dbPath": ""
}
"dbPath": "",
},
}
```
@@ -42,13 +48,19 @@ Recent sessions, streak calendar, watch-time history, and a tracking snapshot wi
Cover-art library with search and sorting, per-series progress, episode drill-down, and direct links into mined cards.
Local files and Jellyfin items with detected season numbers are split into season-specific library entries, so `Season 1` and `Season 2` folders do not merge into one show card.
When older stats already grouped multiple seasons under one series entry, SubMiner moves parsed episodes into the season-specific entries on startup and rebuilds the affected summaries.
Jellyfin stream URLs are normalized to stable item links before stats titles are shown, so playback query parameters are not displayed in the dashboard.
When YouTube channel metadata is available, the Library tab groups videos by creator/channel and treats each tracked video as an episode-like entry inside that channel section.
![Stats Library](/screenshots/stats-library.png)
#### Trends
Watch time, sessions, words seen, and per-anime progress/pattern charts with configurable date ranges and grouping.
Grouped into Activity (per-day/month watch time, cards, words, sessions), Cumulative Totals (running totals incl. new words seen and episodes), Efficiency (words/min, cards/hour, lookups per 100 words), Patterns (watch time by day of week and hour), and per-anime Library charts — all with configurable date ranges and grouping.
![Stats Trends](/screenshots/stats-trends.png)
@@ -60,10 +72,14 @@ Expandable session history with new-word activity, cumulative totals, and pause/
#### Vocabulary
Top repeated words (click a bar to open the word), new-word timeline, frequency rank table with full readings, kanji breakdown, word exclusion list, and click-through occurrence drilldown with Mine Word / Mine Sentence / Mine Audio buttons.
Top repeated words (click a bar to open the word), new-word timeline, cross-title and frequency rank tables with Hide Known / Hide Kana filters, kanji breakdown, word exclusion list, and click-through occurrence drilldown with Mine Word / Mine Sentence / Mine Audio buttons.
![Stats Vocabulary](/screenshots/stats-vocabulary.png)
#### Search
Realtime search across tracked primary subtitle lines and media titles. Results show the source media, session, line number, timing, and sentence text. Secondary subtitle text is not shown or searched here because separate subtitle tracks may not line up sentence-for-sentence. Sentence cards can be mined from any result with a valid local source and timing. Word and audio card buttons appear only when the searched word exactly appears in the primary sentence text; matching text is highlighted in the result.
Stats server config lives under `stats`:
```jsonc
@@ -72,8 +88,8 @@ Stats server config lives under `stats`:
"toggleKey": "Backquote",
"serverPort": 6969,
"autoStartServer": true,
"autoOpenBrowser": false
}
"autoOpenBrowser": false,
},
}
```
@@ -90,15 +106,15 @@ Stats server config lives under `stats`:
## Mining Cards from the Stats Page
The Vocabulary tab's word detail panel shows example lines from your viewing history. Each example line with a valid source file offers three mining buttons:
The Search tab and the Vocabulary tab's word detail panel both mine from subtitle lines in your viewing history. Search matches sentence text and media titles, and **Search by headword** is enabled by default so dictionary-form searches such as `知らない` can find tracked subtitle lines with inflected variants. Turn that toggle off for exact text/title matching only. Each line with a valid source file offers sentence-card mining; word/audio mining is available when the selected word or searched word appears in the sentence:
- **Mine Word** performs a full Yomitan dictionary lookup for the word (definition, reading, pitch accent, etc.) via a short-lived hidden helper, then enriches the card with sentence audio, a screenshot or animated AVIF clip, the highlighted sentence, and metadata extracted from the source video file. Requires Anki and Yomitan dictionaries to be loaded.
- **Mine Sentence** creates a sentence card directly with the `IsSentenceCard` flag set (for Lapis/Kiku workflows), along with audio, image, and translation from the secondary subtitle if available.
- **Mine Audio** creates an audio-only card with the `IsAudioCard` flag, attaching only the sentence audio clip.
- **Mine Word** - performs a full Yomitan dictionary lookup for the word (definition, reading, pitch accent, etc.) via a short-lived hidden helper, then enriches the card with sentence audio, a screenshot or animated AVIF clip, the highlighted sentence, and metadata extracted from the source video file. Requires Anki and Yomitan dictionaries to be loaded.
- **Mine Sentence** - creates a sentence card directly with the `IsSentenceCard` flag set (for Lapis/Kiku workflows), along with audio and image from the source video.
- **Mine Audio** - creates an audio-only card with the `IsAudioCard` flag, attaching only the sentence audio clip.
All three modes respect your `ankiConnect` config: deck, model, field mappings, media settings (static vs AVIF, quality, dimensions), audio padding, metadata pattern, and tags. Media generation runs in parallel for faster card creation.
Secondary subtitle text (typically English translations) is stored alongside primary subtitles during playback and used as the translation field when mining from the stats page.
Secondary subtitle text (typically English translations) is stored alongside primary subtitles during playback and can be used as the translation field when mining sentence cards from Search or vocabulary occurrences. The Search tab does not use that text for display or matching.
### Word Exclusion List
@@ -109,7 +125,7 @@ The Vocabulary tab toolbar includes an **Exclusions** button for hiding words fr
By default, SubMiner keeps all retention tables and raw data (`0` means keep all) while continuing daily/monthly rollup maintenance:
| Data type | Retention |
| -------------- | --------- |
| --------------- | ------------ |
| Raw events | 0 (keep all) |
| Telemetry | 0 (keep all) |
| Sessions | 0 (keep all) |
@@ -141,7 +157,7 @@ The tracker is optimized for "keep everything" defaults:
All policy options live under `immersionTracking` in your config:
| Option | Description |
| ------ | ----------- |
| ------------------------------ | ------------------------------------------------------------------ |
| `batchSize` | Writes per flush batch |
| `flushIntervalMs` | Max delay between flushes (default: 500ms) |
| `queueCap` | Max queued writes before oldest are dropped |
@@ -275,11 +291,11 @@ LIMIT ?;
Core tables:
- `imm_videos` video key/title/source metadata
- `imm_sessions` session UUID, video reference, timing/status, final denormalized totals
- `imm_session_telemetry` high-frequency session aggregates over time
- `imm_session_events` event stream with compact numeric event types
- `imm_subtitle_lines` persisted subtitle text and timing per session/video
- `imm_videos` - video key/title/source metadata
- `imm_sessions` - session UUID, video reference, timing/status, final denormalized totals
- `imm_session_telemetry` - high-frequency session aggregates over time
- `imm_session_events` - event stream with compact numeric event types
- `imm_subtitle_lines` - persisted subtitle text and timing per session/video
Lifetime summary tables:
@@ -300,5 +316,5 @@ Vocabulary tables:
Media-art tables:
- `imm_media_art` per-video cover metadata plus shared blob reference
- `imm_cover_art_blobs` deduplicated image bytes keyed by blob hash
- `imm_media_art` - per-video cover metadata plus shared blob reference
- `imm_cover_art_blobs` - deduplicated image bytes keyed by blob hash
+5 -5
View File
@@ -24,7 +24,7 @@ features:
src: /assets/mpv.svg
alt: mpv icon
title: Built for mpv
details: Tracks subtitles via mpv IPC in real time. Launch with the wrapper script or the mpv plugin no external bridge needed.
details: Tracks subtitles via mpv IPC in real time. Launch with the wrapper script or the mpv plugin - no external bridge needed.
link: /usage
linkText: How it works
- icon:
@@ -45,14 +45,14 @@ features:
src: /assets/highlight.svg
alt: Highlight icon
title: Reading Annotations
details: N+1 targeting, character-name matching, frequency highlighting, and JLPT tagging all layered on subtitle text in real time.
details: N+1 targeting, character-name matching, frequency highlighting, and JLPT tagging - all layered on subtitle text in real time.
link: /subtitle-annotations
linkText: Annotation details
- icon:
src: /assets/video.svg
alt: Video playback icon
title: YouTube Playback
details: Play YouTube URLs or ytsearch targets directly SubMiner automatically selects and loads subtitles for the video.
details: Play YouTube URLs or ytsearch targets directly - SubMiner automatically selects and loads subtitles for the video.
link: /usage#youtube-playback
linkText: YouTube playback
- icon:
@@ -66,14 +66,14 @@ features:
src: /assets/subtitle-download.svg
alt: Subtitle download icon
title: Subtitle Download & Sync
details: Search and pull subtitles from Jimaku, then auto-sync timing with alass or ffsubsync all from the overlay.
details: Search and pull subtitles from Jimaku, then retime subtitles with alass or ffsubsync - all from the overlay.
link: /jimaku-integration
linkText: Jimaku integration
- icon:
src: /assets/tokenization.svg
alt: Tracking chart icon
title: Stats Dashboard
details: Browse session history, streak calendars, vocabulary frequency, and per-series progress in a local dashboard then mine cards straight from your viewing history.
details: Browse session history, streak calendars, vocabulary frequency, and per-series progress in a local dashboard - then mine cards straight from your viewing history.
link: /immersion-tracking
linkText: Dashboard & tracking
- icon:
+36 -34
View File
@@ -1,10 +1,12 @@
# Installation
SubMiner is a desktop app that draws an interactive layer - an **overlay** - on top of the [mpv](https://mpv.io) video player. As you watch native Japanese media, you can click or hover any word in the subtitles to look it up, then turn it into an Anki flashcard without pausing to switch apps. Building flashcards from real content you're watching is called **sentence mining**, and it's what SubMiner is built for. It bundles its own copy of **Yomitan** (a pop-up dictionary) and talks to **AnkiConnect** (an add-on that lets other programs add cards to Anki) so cards get filled in automatically.
Three steps to get started:
1. **Install requirements** mpv and a few optional extras
2. **Install SubMiner** from the AUR, or download from GitHub Releases
3. **Launch the app** first-run setup walks you through dictionaries, the launcher, and everything else
1. **Install requirements** - mpv and a few optional extras
2. **Install SubMiner** - from the AUR, or download from GitHub Releases
3. **Launch the app** - first-run setup walks you through dictionaries, the launcher, and everything else
## 1. Install Requirements
@@ -22,19 +24,19 @@ Only **mpv** is strictly required to run SubMiner. Everything else enhances the
| ffmpegthumbnailer | Optional | Video thumbnail generation for the picker. |
| guessit | Optional | Better AniSkip title/season/episode parsing. |
| alass | Optional | Subtitle sync engine (preferred). Disabled without alass or ffsubsync. |
| ffsubsync | Optional | Subtitle sync engine (fallback). Disabled without alass or ffsubsync. |
| ffsubsync | Optional | Audio-based subtitle sync engine. Disabled without alass or ffsubsync. |
| fuse2 | Linux only | Required to run the AppImage. |
### Linux
**Window backend** you need one of these depending on your compositor:
**Window backend** - you need one of these depending on your compositor:
- **Hyprland** native Wayland support (uses `hyprctl`)
- **Sway** native Wayland support (uses `swaymsg`)
- **X11 / Xwayland** for X11 sessions or any other Wayland compositor (uses `xdotool` and `xwininfo`)
- **Hyprland** - native Wayland support (uses `hyprctl`)
- **Sway** - native Wayland support (uses `swaymsg`)
- **X11 / Xwayland** - for X11 sessions or any other Wayland compositor (uses `xdotool` and `xwininfo`)
::: warning Wayland support is compositor-specific
Wayland has no universal API for window positioning each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Only Hyprland and Sway have native Wayland backends. If you run a different Wayland compositor (GNOME, KDE Plasma, river, etc.), both mpv **and** SubMiner must run under X11 or Xwayland. The `subminer` launcher handles this automatically when `--backend x11` is set or the X11 backend is auto-detected.
Wayland has no universal API for window positioning - each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Only Hyprland and Sway have native Wayland backends. If you run a different Wayland compositor (GNOME, KDE Plasma, river, etc.), both mpv **and** SubMiner must run under X11 or Xwayland. The `subminer` launcher handles this automatically when `--backend x11` is set or the X11 backend is auto-detected.
:::
<details>
@@ -67,7 +69,7 @@ sudo apt install yt-dlp fzf rofi chafa ffmpegthumbnailer
sudo apt install xdotool x11-utils
# Optional: subtitle sync
pip install ffsubsync
# alass is not in apt install via cargo: cargo install alass-cli
# alass is not in apt - install via cargo: cargo install alass-cli
```
</details>
@@ -92,7 +94,7 @@ pip install ffsubsync
### macOS
macOS 10.13 or later. Accessibility permission is required for window tracking (see [step 2](#macos-dmg)).
macOS 11 (Big Sur) or later. Accessibility permission - the macOS setting that lets one app observe and position another app's windows - is required so the overlay can follow the mpv window (see [step 2](#macos-dmg)).
```bash
brew install mpv ffmpeg
@@ -109,7 +111,7 @@ pip install ffsubsync
Windows 10 or later. Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on `PATH`. Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary.
No compositor tools or window helpers are needed native window tracking is built in.
No compositor tools or window helpers are needed - native window tracking is built in.
## 2. Install SubMiner
@@ -170,8 +172,8 @@ If you prefer to install it manually, see [manual launcher install](#manual-laun
Download the latest installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
- `SubMiner-<version>.exe` installer (recommended)
- `SubMiner-<version>.zip` portable fallback
- `SubMiner-<version>.exe` - installer (recommended)
- `SubMiner-<version>-win.zip` - portable fallback
Make sure `mpv.exe` is on your `PATH`, or set `mpv.executablePath` in the config during first-run setup.
@@ -183,7 +185,7 @@ Make sure `mpv.exe` is on your `PATH`, or set `mpv.executablePath` in the config
```bash
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
cd SubMiner
bun install
make deps
bun run build
# Optional: build AppImage
@@ -200,7 +202,7 @@ Bundled Yomitan is built during `bun run build`.
```bash
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
cd SubMiner
git submodule update --init --recursive
make deps
make build-macos
```
@@ -214,14 +216,14 @@ The built app will be in the `release` directory (`.dmg` and `.zip`). For unsign
```powershell
git clone https://github.com/ksyasuda/SubMiner.git
cd SubMiner
git submodule update --init --recursive
bun install
# Windows requires building texthooker-ui manually before the main build
Set-Location vendor/texthooker-ui
Set-Location stats
bun install --frozen-lockfile
Set-Location ../vendor/texthooker-ui
bun install --frozen-lockfile
bun run build
Set-Location ../..
bun run build:win
```
@@ -238,18 +240,18 @@ subminer app --setup
# Linux (AppImage directly)
~/.local/bin/SubMiner.AppImage --setup
# macOS launch SubMiner.app from /Applications, or:
# macOS - launch SubMiner.app from /Applications, or:
subminer app --setup
```
On **Windows**, just run `SubMiner.exe` the setup wizard opens automatically on first launch.
On **Windows**, just run `SubMiner.exe` - the setup wizard opens automatically on first launch.
The setup wizard walks you through:
- **Config file** auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
- **Yomitan dictionaries** import at least one dictionary so word lookups work
- **Bun + `subminer` launcher** _(optional)_ installs the command-line launcher into a writable PATH directory
- **Windows shortcut** _(Windows only)_ create a `SubMiner mpv` Start Menu/Desktop shortcut
- **Config file** - auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
- **Yomitan dictionaries** - import at least one dictionary so word lookups work
- **Bun + `subminer` launcher** _(optional)_ - installs the command-line launcher into a writable PATH directory
- **Windows shortcut** _(Windows only)_ - create a `SubMiner mpv` Start Menu/Desktop shortcut
The `Finish setup` button requires a config file and at least one Yomitan dictionary. Bun and the launcher are optional and never block setup completion.
@@ -266,7 +268,7 @@ subminer video.mkv
You should see the overlay appear over mpv. If subtitles are loaded, they will appear as interactive text in the overlay.
On **Windows**, the recommended way to play video is with the **SubMiner mpv** shortcut created during setup double-click it, or drag a video file onto it.
On **Windows**, the recommended way to play video is with the **SubMiner mpv** shortcut created during setup - double-click it, or drag a video file onto it.
### Verify Setup
@@ -283,7 +285,7 @@ This checks for the app binary, mpv, ffmpeg, config file, and socket path. Fix a
If you plan to mine Anki cards:
1. Install [Anki](https://apps.ankiweb.net/)
2. Install [AnkiConnect](https://ankiweb.net/shared/info/2055492159) open Anki → **Tools → Add-ons → Get Add-ons** → enter code `2055492159`
2. Install [AnkiConnect](https://ankiweb.net/shared/info/2055492159) - open Anki → **Tools → Add-ons → Get Add-ons** → enter code `2055492159`
3. Restart Anki and keep it running while using SubMiner
AnkiConnect listens on `http://127.0.0.1:8765` by default. SubMiner connects automatically with no extra config needed.
@@ -300,17 +302,17 @@ subminer --update
SubMiner verifies AppImage, launcher, and rofi theme downloads against `SHA256SUMS.txt`. If the binary is in a protected path, SubMiner shows the exact command to run rather than elevating itself.
On Linux, `subminer -u` performs the AppImage update from the launcher process directly.
The tray "Check for Updates" entry installs the new app automatically on Linux, macOS, and Windows. On Linux it replaces the running `.AppImage` in place via `electron-updater`; AppImages managed by a system package (for example the AUR `/opt/SubMiner/SubMiner.AppImage`) are skipped so the package manager stays in charge.
On macOS, tray update checks can also update the app automatically through Electron's built-in updater.
`subminer -u` also performs the AppImage update directly from the launcher process, which is useful when SubMiner is not currently running.
## How It All Fits Together
SubMiner is an overlay that sits on top of mpv. It connects to mpv through an IPC socket, renders subtitles as interactive text using a bundled Yomitan dictionary engine, and optionally creates Anki flashcards via AnkiConnect.
The `subminer` launcher handles mpv IPC socket setup automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or `\\.\pipe\subminer-socket` on Windows) without it the overlay starts but subtitles won't appear.
The `subminer` launcher handles mpv IPC socket setup automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or `\\.\pipe\subminer-socket` on Windows) - without it the overlay starts but subtitles won't appear.
The bundled mpv plugin is injected at runtime automatically you don't need to install it separately. It provides in-player keybindings (the `y` chord) for controlling the overlay from within mpv. See [MPV Plugin](/mpv-plugin) for the full keybinding and configuration reference.
The bundled mpv plugin is injected at runtime automatically - you don't need to install it separately. It provides in-player keybindings (the `y` chord) for controlling the overlay from within mpv. See [MPV Plugin](/mpv-plugin) for the full keybinding and configuration reference.
## Platform Notes
@@ -328,7 +330,7 @@ Ensure `mecab` is available on your PATH when launching SubMiner.
- The **SubMiner mpv** shortcut is the recommended way to launch playback. It starts `mpv.exe` with the right IPC socket and subtitle defaults.
- First-run setup adds only `%LOCALAPPDATA%\SubMiner\bin` to the HKCU user PATH. It does not add `SubMiner.exe` to PATH.
- IPC socket on Windows is `\\.\pipe\subminer-socket` do not use `/tmp/subminer-socket`.
- IPC socket on Windows is `\\.\pipe\subminer-socket` - do not use `/tmp/subminer-socket`.
- Config is stored at `%APPDATA%\SubMiner\config.jsonc`.
## Manual Launcher Install
@@ -372,4 +374,4 @@ cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
Override with `SUBMINER_ROFI_THEME=/absolute/path/to/theme.rasi`.
Next: [Usage](/usage) learn about the `subminer` wrapper, keybindings, and YouTube playback.
Next: [Usage](/usage) - learn about the `subminer` wrapper, keybindings, and YouTube playback.
+39 -8
View File
@@ -1,8 +1,8 @@
# IPC + Runtime Contracts
SubMiner's Electron app runs two isolated processes main and renderer that can only communicate through IPC channels. This boundary is intentional: the renderer is an untrusted surface (it loads Yomitan, renders user-controlled subtitle text, and runs in a Chromium sandbox), so every message crossing the bridge passes through a validation layer before it can reach domain logic.
SubMiner's Electron app runs two isolated processes - main and renderer - that can only communicate through IPC channels. This boundary is intentional: the renderer is an untrusted surface (it loads Yomitan, renders user-controlled subtitle text, and runs in a Chromium sandbox), so every message crossing the bridge passes through a validation layer before it can reach domain logic.
The contract system enforces this by making channel names, payload shapes, and validators co-located and co-evolved. A change to any IPC surface touches the contract, the validator, the preload bridge, and the handler in the same commit drift between any of those layers is treated as a bug.
The contract system enforces this by making channel names, payload shapes, and validators co-located and co-evolved. A change to any IPC surface touches the contract, the validator, the preload bridge, and the handler in the same commit - drift between any of those layers is treated as a bug.
## Message Flow
@@ -36,13 +36,44 @@ flowchart TB
style E fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
```
## Runtime Sockets
The renderer↔main bridge above lives *inside* the Electron app. A separate set of OS sockets connects the app to the other runtimes - mpv and the launcher/plugin. These carry no renderer payloads and bypass the contract/validator layer; they are command and property channels between processes.
- **mpv IPC socket** (`/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows): the `MpvIpcClient` in the main process connects here to send JSON commands and subscribe to playback/subtitle properties via `observe_property`. Created by mpv's `--input-ipc-server`.
- **App control socket** (`/tmp/subminer-control-<uid>-<hash>.sock`, or a named pipe on Windows): the launcher and the mpv plugin send CLI-style commands (`--start`, `--show-visible-overlay`, `--texthooker`) to a running app here. It also dedupes a second `subminer` invocation into the existing instance instead of launching twice.
```mermaid
flowchart LR
classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef app fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef ext fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
subgraph MpvProc["mpv process"]
direction TB
Mpv["mpv core"]:::ext
Plugin["SubMiner plugin (Lua)"]:::extrt
end
Launcher["Launcher CLI"]:::extrt
App["SubMiner app (Electron main)"]:::app
App <-->|"mpv IPC socket · /tmp/subminer-socket<br/>JSON commands + property observe"| Mpv
Launcher -->|"app control socket · /tmp/subminer-control-*<br/>--start, --show-visible-overlay, …"| App
Plugin -->|"app control socket<br/>spawn / attach"| App
style MpvProc fill:#363a4f,stroke:#494d64,color:#cad3f5
```
How these sockets are established during launch is covered in [Playback Startup Flow](./architecture#playback-startup-flow).
## Core Surfaces
| File | Role |
| --- | --- |
| `src/shared/ipc/contracts.ts` | Canonical channel names and payload type contracts. Single source of truth for both processes. |
| `src/shared/ipc/validators.ts` | Runtime payload parsers and type guards. Every `invoke` payload is validated here before the handler runs. |
| `src/preload.ts` | Renderer-side bridge. Exposes a typed API surface to the renderer only approved channels are accessible. |
| `src/preload.ts` | Renderer-side bridge. Exposes a typed API surface to the renderer - only approved channels are accessible. |
| `src/main/ipc-runtime.ts` | Main-process handler registration and routing. Wires validated channels to domain handlers. |
| `src/core/services/ipc.ts` | Service-level invoke handling. Applies guardrails (validation, error wrapping) before calling domain logic. |
| `src/core/services/anki-jimaku-ipc.ts` | Integration-specific IPC boundary for Anki and Jimaku operations. |
@@ -50,19 +81,19 @@ flowchart TB
## Contract Rules
These rules exist to prevent a class of bugs where the renderer and main process silently disagree about message shapes which surfaces as undefined fields, swallowed errors, or state corruption.
These rules exist to prevent a class of bugs where the renderer and main process silently disagree about message shapes - which surfaces as undefined fields, swallowed errors, or state corruption.
- **Use shared constants.** Channel names come from `contracts.ts`, never ad-hoc literal strings. This makes channels greppable and refactor-safe.
- **Validate before handling.** Every `invoke` payload passes through `validators.ts` before reaching domain logic. This catches shape drift at the boundary instead of deep inside a service.
- **Return structured failures.** Handlers return `{ ok: false, error: string }` on failure rather than throwing. The renderer can always distinguish success from failure without try/catch.
- **Keep payloads narrow.** Send only what the handler needs. Avoid passing entire state objects across the bridge it couples the renderer to internal main-process structure.
- **Keep payloads narrow.** Send only what the handler needs. Avoid passing entire state objects across the bridge - it couples the renderer to internal main-process structure.
- **Co-evolve all layers.** When a payload shape changes, update `contracts.ts`, `validators.ts`, `preload.ts`, and the handler in the same commit. Partial updates are treated as bugs.
## Two Message Patterns
**Invoke (request/response):** The renderer calls a typed bridge method and awaits a result. The main process validates the payload, runs the handler, and returns a structured response. Used for operations where the renderer needs a result lookups, config reads, mining actions.
**Invoke (request/response):** The renderer calls a typed bridge method and awaits a result. The main process validates the payload, runs the handler, and returns a structured response. Used for operations where the renderer needs a result - lookups, config reads, mining actions.
**Fire-and-forget (send):** The renderer sends a message with no response. The main process validates and handles it silently. Malformed payloads are dropped. Used for notifications where the renderer doesn't need confirmation UI state hints, focus events, position updates.
**Fire-and-forget (send):** The renderer sends a message with no response. The main process validates and handles it silently. Malformed payloads are dropped. Used for notifications where the renderer doesn't need confirmation - UI state hints, focus events, position updates.
## Add a New IPC Action
@@ -77,7 +108,7 @@ These rules exist to prevent a class of bugs where the renderer and main process
- Prefer runtime/domain composition via `src/main/runtime/composers/*` and `src/main/runtime/domains/*`. IPC handlers should delegate to composers rather than containing orchestration logic.
- Route shared mutable state updates through transition helpers in `src/main/state.ts` for migrated domains. Direct mutation from IPC handlers bypasses invariant checks.
- Keep IPC handlers thin they validate, delegate, and return. Business logic belongs in services.
- Keep IPC handlers thin - they validate, delegate, and return. Business logic belongs in services.
## Troubleshooting
+94 -161
View File
@@ -1,185 +1,118 @@
# Jellyfin Integration
SubMiner includes an optional Jellyfin CLI integration for:
[Jellyfin](https://jellyfin.org) is a free, self-hosted media server - think of it as your own private streaming service for video you own. If you keep your anime on a Jellyfin server, SubMiner can play episodes through mpv with the full mining overlay.
- authenticating against a server
- listing libraries and media items
- launching item playback in the connected mpv instance
- receiving Jellyfin remote cast-to-device playback events in-app
- opening an in-app setup window for server URL and authentication
- toggling Jellyfin cast discovery from the tray once configured
::: tip Who needs this?
This page is only relevant if you already run (or have access to) a Jellyfin server. If you watch local files or YouTube, you can skip it. The in-app setup window (`subminer jellyfin`) is the easiest starting point.
:::
SubMiner can act as a **cast-to-device target** for Jellyfin (similar to jellyfin-mpv-shim). Sign in once, turn on discovery, and SubMiner shows up in the "Play on…" / cast menu of any Jellyfin app - web, phone, or TV. Pick an episode, cast it to SubMiner, and it plays in SubMiner's mpv window with the full overlay and Yomitan click-to-lookup.
This is the recommended way to use Jellyfin with SubMiner. A terminal-only option is covered in [Launcher playback](#launcher-playback) at the end.
## Requirements
- Jellyfin server URL and user credentials
- For `--jellyfin-play`: connected mpv IPC socket (`--start` or existing mpv plugin workflow)
- On Linux, token encryption defaults to `gnome-libsecret`; pass `--password-store=<backend>` to override.
- A Jellyfin server plus your username and password
- SubMiner installed and running (see [Installation](/installation))
- On Linux, the session token is stored with `gnome-libsecret` by default
## Setup
## Quick start
1. Set base config values (`config.jsonc`):
### 1. Start SubMiner
Launch SubMiner so it's running in the system tray.
### 2. Sign in to your server
Open the tray menu and click **Configure Jellyfin**. In the window that opens, enter your **Server URL** (for example `http://127.0.0.1:8096`), **Username**, and **Password**, then click **Login**.
On success, SubMiner:
- saves an encrypted session token - your password is never stored,
- turns the Jellyfin integration on, and
- remembers the server and username for next time.
Reopen this window any time to switch servers or **Logout**.
### 3. Turn on discovery
Discovery is what makes SubMiner appear as a cast target. Two ways to enable it:
- **For the current session** - open the tray menu and tick **Jellyfin Discovery**. (This item appears once you've signed in.)
- **Automatically on every launch** - already on by default. After your first sign-in, SubMiner auto-connects to Jellyfin at startup, so the cast target is ready without touching the tray. You can change this under [Settings](#settings).
### 4. Cast from any Jellyfin app
In the Jellyfin web UI or mobile app, start playing something, open the **cast / "Play on"** menu, and pick your device - SubMiner appears there named after your computer's hostname. Playback opens in SubMiner.
From then on, pause / resume / seek / stop and audio or subtitle track changes you make in the Jellyfin app are mirrored in SubMiner, and your watch progress syncs back to Jellyfin (now-playing and resume position).
## What happens during playback
- **mpv launches automatically.** If mpv isn't already running when you cast, SubMiner starts it with SubMiner defaults and the bundled mpv plugin, so keybindings work right away.
- **The overlay is managed by SubMiner,** so your configured `subtitleStyle` controls how subtitles look. Use the [overlay-toggle shortcut](/shortcuts) to hide it for a session.
- **Resume works.** If Jellyfin has a saved position for the item, SubMiner seeks there on load.
- **Direct play first.** When the source allows it and the container is in your direct-play allowlist, SubMiner streams the original file; otherwise it requests a transcoded stream from Jellyfin.
- **Japanese subtitles are auto-selected,** preferring Jellyfin's default and embedded tracks over external sidecar files when several match.
- **Subtitle timing is corrected when possible.** SubMiner removes Jellyfin's server-selected subtitle stream from the mpv load URL, suppresses the mpv plugin's one-shot subtitle auto-selection and overlay auto-start for managed Jellyfin loads, stages downloaded subtitle tracks without letting mpv auto-switch between tracks, then selects the Japanese track once after applying any saved or inferred timing delay. When Jellyfin provides both Japanese and English subtitle files, SubMiner compares their cue timelines and applies a global delay if one track is clearly offset. Manual delay shifts you make with SubMiner's adjacent-cue controls are saved per item and subtitle track, then restored the next time you select that track.
## Settings
All Jellyfin options live under **Settings → Integrations → Jellyfin** (open settings from the tray's **Open SubMiner Settings**). The ones that matter for casting:
| Setting | Default | What it does |
| ------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
| **Enabled** | Off | Turns the Jellyfin integration on. Switched on for you when you sign in. |
| **Server Url** | - | Your Jellyfin server. Filled in when you sign in. |
| **Remote Control Enabled** | On | Lets SubMiner act as a cast target. |
| **Remote Control Auto Connect** | On | Connects to Jellyfin at startup so discovery is automatic. Turn off if you'd rather start it from the tray each time. |
| **Auto Announce** | Off | Re-broadcasts visibility on connect. Enable if your device is slow to appear in the cast menu. |
Prefer editing the config file? The same keys live under `jellyfin` in `config.jsonc`:
```jsonc
{
"jellyfin": {
"enabled": true,
"serverUrl": "http://127.0.0.1:8096",
"recentServers": ["http://127.0.0.1:8096"],
"username": "your-user",
"remoteControlEnabled": true,
"remoteControlAutoConnect": true,
"autoAnnounce": false,
"remoteControlDeviceName": "SubMiner",
"defaultLibraryId": "",
"pullPictures": false,
"iconCacheDir": "/tmp/subminer-jellyfin-icons",
"directPlayPreferred": true,
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"],
"transcodeVideoCodec": "h264",
},
}
```
2. Authenticate:
See [Configuration](/configuration) for the full list (transcode codec, direct-play containers, default library, and more).
## Troubleshooting
**SubMiner doesn't appear in the cast menu**
- Make sure SubMiner is running.
- Make sure you're signed in - reopen **Configure Jellyfin** and log in again if your token expired.
- Make sure discovery is on (tray **Jellyfin Discovery**, or **Remote Control Auto Connect** in settings).
- Make sure SubMiner and the Jellyfin client point at the same server.
**Casting starts but nothing plays**
- Confirm the item plays normally in another Jellyfin client.
- If mpv was closed, give it a moment - SubMiner launches it on demand and retries.
**SubMiner keeps disconnecting**
- Check server/network stability and whether the session token has expired.
## Security notes
- The Jellyfin session (access token + user ID) is kept in SubMiner's local encrypted token storage. Your password is used only to log in and is never saved.
- Treat the token storage and your `config.jsonc` as secrets - don't commit them.
- Advanced/headless: the `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID` environment variables can supply a session without the sign-in window.
## Launcher playback
If you'd rather stay in the terminal, the `subminer` launcher can browse and play Jellyfin media directly, without casting from a Jellyfin app:
```bash
subminer jellyfin
subminer jellyfin -l \
--server http://127.0.0.1:8096 \
--username your-user \
--password 'your-password'
subminer jellyfin -p # alias: subminer jf -p
```
`subminer jellyfin` opens the setup window. It pre-fills the server URL from the configured server, a recent successful server, or the local default. Successful login keeps the window open, stores the Jellyfin session token in encrypted storage, updates the configured server/username/client metadata, and refreshes recent servers. Passwords are never stored.
3. List libraries:
```bash
SubMiner.AppImage --jellyfin-libraries
```
Launcher wrapper equivalent for interactive playback flow:
```bash
subminer jellyfin -p
```
Launcher wrapper for Jellyfin cast discovery mode (background app + tray):
```bash
subminer jellyfin -d
```
After Jellyfin is enabled with a server URL and SubMiner is already running, the tray menu shows `Jellyfin Discovery`. Use that checkbox to start or stop discovery for the current runtime session without changing config. If the stored login session is missing or expired, starting discovery shows a warning and setup remains the path to refresh credentials. It does not survive app restart.
Stop discovery session/app:
```bash
subminer app --stop
```
`subminer jf ...` is an alias for `subminer jellyfin ...`.
To clear saved session credentials:
```bash
subminer jellyfin --logout
```
4. List items in a library:
```bash
SubMiner.AppImage --jellyfin-items --jellyfin-library-id LIBRARY_ID --jellyfin-search term
```
Optional listing controls:
- `--jellyfin-recursive=true|false` (default: true)
- `--jellyfin-include-item-types=Series,Season,Folder,CollectionFolder,Movie,...`
These are used by the launcher picker flow to:
- keep root search focused on shows/folders/movies (exclude episode rows)
- browse selected anime/show directories as folder-or-file lists
- recurse for playable files only after selecting a folder
5. Start playback:
```bash
SubMiner.AppImage --start
SubMiner.AppImage --jellyfin-play --jellyfin-item-id ITEM_ID
```
Optional stream overrides:
- `--jellyfin-audio-stream-index N`
- `--jellyfin-subtitle-stream-index N`
## Playback Behavior
- Direct play is attempted first when:
- `jellyfin.directPlayPreferred=true`
- media source supports direct stream
- source container matches `jellyfin.directPlayContainers`
- If direct play is not selected/available, SubMiner requests a Jellyfin transcoded stream (`master.m3u8`) using `jellyfin.transcodeVideoCodec`.
- Resume position (`PlaybackPositionTicks`) is applied via mpv seek.
- Media title is set in mpv as `[Jellyfin/<mode>] <title>`.
## Cast To Device Mode (jellyfin-mpv-shim style)
When SubMiner is running with a valid Jellyfin session, it can appear as a
remote playback target in Jellyfin's cast-to-device menu.
### Requirements
- `jellyfin.enabled=true`
- valid `jellyfin.serverUrl` and Jellyfin auth session (env override or stored login session)
- `jellyfin.remoteControlEnabled=true` (default)
- `jellyfin.remoteControlAutoConnect=true` (default) for startup auto-connect
- `jellyfin.autoAnnounce=false` by default (`true` enables auto announce/visibility check logs on connect)
### Behavior
- SubMiner connects to Jellyfin remote websocket and posts playback capabilities.
- Startup auto-connect still requires `remoteControlAutoConnect=true`; the tray `Jellyfin Discovery` checkbox can start discovery later even when startup auto-connect is disabled.
- `Play` events open media in mpv with the same defaults used by `--jellyfin-play`.
- If mpv IPC is not connected at cast time, SubMiner auto-launches mpv in idle mode with SubMiner defaults and retries playback.
- `Playstate` events map to mpv pause/resume/seek/stop controls.
- Stream selection commands (`SetAudioStreamIndex`, `SetSubtitleStreamIndex`) are mapped to mpv track selection.
- SubMiner reports start/progress/stop timeline updates back to Jellyfin so now-playing and resume state stay synchronized.
- `--jellyfin-remote-announce` forces an immediate capability re-broadcast and logs whether server sessions can see the device.
### Troubleshooting
- Device not visible in Jellyfin cast menu:
- ensure SubMiner is running
- ensure session token is valid (`--jellyfin-login` again if needed)
- ensure `remoteControlEnabled` is true
- use tray `Jellyfin Discovery` or `subminer jellyfin -d` to start discovery
- Cast command received but playback does not start:
- verify mpv IPC can connect (`--start` flow)
- verify item is playable from normal `--jellyfin-play --jellyfin-item-id ...`
- Frequent reconnects:
- check Jellyfin server/network stability and token expiration
## Failure Handling
User-visible errors are shown through CLI logs and mpv OSD for:
- invalid credentials
- expired/invalid token
- server/network errors
- missing library/item identifiers
- no playable source
- mpv not connected for playback
## Security Notes and Limitations
- Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted token storage after login/setup.
- Launcher wrappers support `--password-store=<backend>` and forward it through to the app process.
- Optional environment overrides are supported: `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID`.
- Treat both token storage and config files as secrets and avoid committing them.
- Password is used only for login and is not stored.
- Optional setup UI is available via `--jellyfin`; all actions are also available via CLI flags.
- `subminer` wrapper uses Jellyfin subcommands (`subminer jellyfin ...`, alias `subminer jf ...`). Use `SubMiner.AppImage` for direct `--jellyfin-libraries` and `--jellyfin-items`.
- For direct app CLI usage (`SubMiner.AppImage ...`), `--jellyfin-server` can override server URL for login/play flows without editing config.
This opens an fzf picker (add `-R` for rofi) to browse your libraries and episodes, then plays the selected item in SubMiner's mpv with the same overlay, resume, and subtitle behavior described above. Sign in first (step 2) so the launcher can reach your server. See [Launcher Script](/launcher-script) for the rest of the launcher's features.
+20 -16
View File
@@ -1,19 +1,23 @@
# Jimaku Integration
[Jimaku](https://jimaku.cc) is a community-driven subtitle repository for anime. SubMiner integrates with the Jimaku API so you can search, browse, and download Japanese subtitle files directly from the overlay no alt-tabbing or manual file management required. Downloaded subtitles are loaded into mpv immediately.
[Jimaku](https://jimaku.cc) is a community-driven subtitle repository for anime - a shared online library of subtitle files contributed by other learners. SubMiner integrates with the Jimaku API so you can search, browse, and download Japanese subtitle files directly from the overlay - no alt-tabbing or manual file management required. Downloaded subtitles are loaded into mpv immediately.
::: tip Prerequisite: a free API key
You need a Jimaku account and an API key (a personal access string) before this feature works. Create an account at [jimaku.cc](https://jimaku.cc), copy your key, and add it to your config as shown under [Configuration](#configuration) below. Without a key, the search modal will report "Jimaku API key not set."
:::
## How It Works
The Jimaku integration runs through an in-overlay modal accessible via a keyboard shortcut (`Ctrl+Shift+J` by default).
When you open the modal, SubMiner parses the current video filename to extract a title, season, and episode number. Common naming conventions are supported `S01E03`, `1x03`, `E03`, and dash-separated episode numbers all work. If the filename yields a high-confidence match (title + episode), SubMiner auto-searches immediately.
When you open the modal, SubMiner parses the current video filename to extract a title, season, and episode number. Common naming conventions are supported - `S01E03`, `1x03`, `E03`, and dash-separated episode numbers all work. If the filename yields a high-confidence match (title + episode), SubMiner auto-searches immediately.
From there:
1. **Search** SubMiner queries the Jimaku API with the parsed title. Results appear as a list of anime entries (Japanese and English names).
2. **Browse entries** Select an entry to load its available subtitle files, filtered by episode if one was detected.
3. **Browse files** Files show name, size, and last-modified date. If a language preference is configured, files are sorted accordingly (e.g., Japanese-tagged files first).
4. **Download** Selecting a file downloads it to the same directory as the video (or a temp directory for remote/streamed media) and loads it into mpv as a new subtitle track.
1. **Search** - SubMiner queries the Jimaku API with the parsed title. Results appear as a list of anime entries (Japanese and English names).
2. **Browse entries** - Select an entry to load its available subtitle files, filtered by episode if one was detected.
3. **Browse files** - Files show name, size, and last-modified date. If a language preference is configured, files are sorted accordingly (e.g., Japanese-tagged files first).
4. **Download** - Selecting a file downloads it to the same directory as the video (or a temp directory for remote/streamed media) and loads it into mpv as a new subtitle track.
If no files match the current episode filter, a "Show all files" button lets you broaden the search to all episodes for that entry.
@@ -44,8 +48,8 @@ Add a `jimaku` section to your `config.jsonc`:
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `jimaku.apiKey` | `string` | | Jimaku API key (plaintext). Mutually exclusive with `apiKeyCommand`. |
| `jimaku.apiKeyCommand` | `string` | | Shell command that prints the API key to stdout. Useful for secret managers (e.g., `pass jimaku/api-key`). |
| `jimaku.apiKey` | `string` | - | Jimaku API key (plaintext). Mutually exclusive with `apiKeyCommand`. |
| `jimaku.apiKeyCommand` | `string` | - | Shell command that prints the API key to stdout. Useful for secret managers (e.g., `pass jimaku/api-key`). |
| `jimaku.apiBaseUrl` | `string` | `"https://jimaku.cc"` | Base URL for the Jimaku API. Only change this if using a mirror or local instance. |
| `jimaku.languagePreference` | `"ja"` \| `"en"` \| `"none"` | `"ja"` | Sort subtitle files by language tag. `"ja"` pushes Japanese-tagged files to the top; `"en"` does the same for English. `"none"` preserves the API order. |
| `jimaku.maxEntryResults` | `number` | `10` | Maximum number of anime entries returned per search. |
@@ -64,8 +68,8 @@ The keyboard shortcut is configured separately under `shortcuts`:
An API key is required to use the Jimaku integration. You can get one from [jimaku.cc](https://jimaku.cc). There are two ways to provide it:
- **`apiKey`** set the key directly in config. Simple, but the key is stored in plaintext.
- **`apiKeyCommand`** a shell command that outputs the key. Runs with a 10-second timeout. Preferred if you use a secret manager like `pass`, `gpg`, or a keychain tool.
- **`apiKey`** - set the key directly in config. Simple, but the key is stored in plaintext.
- **`apiKeyCommand`** - a shell command that outputs the key. Runs with a 10-second timeout. Preferred if you use a secret manager like `pass`, `gpg`, or a keychain tool.
If both are set, `apiKey` takes priority.
@@ -75,8 +79,8 @@ SubMiner extracts media info from the current video path to pre-fill the search
- **Season + episode patterns:** `S01E03`, `1x03`
- **Episode-only patterns:** `E03`, `EP03`, or dash-separated numbers like `Title - 03 -`
- **Bracket tags:** `[SubGroup]`, `[1080p]`, `[HEVC]` stripped before title extraction
- **Year tags:** `(2024)` stripped
- **Bracket tags:** `[SubGroup]`, `[1080p]`, `[HEVC]` - stripped before title extraction
- **Year tags:** `(2024)` - stripped
- **Dots and underscores:** treated as spaces
- **Remote/streamed URLs:** SubMiner checks URL query parameters (`title`, `name`, `q`) and path segments to extract a meaningful title
@@ -94,7 +98,7 @@ The Jimaku API has rate limits. If you see 429 errors, wait for the retry durati
**No entries found**
Try simplifying the title remove season/episode qualifiers and search with just the anime name. Jimaku's search matches against its own database of anime titles, so the exact spelling matters.
Try simplifying the title - remove season/episode qualifiers and search with just the anime name. Jimaku's search matches against its own database of anime titles, so the exact spelling matters.
**No files found for this episode**
@@ -106,6 +110,6 @@ Verify mpv is running and connected via IPC. SubMiner loads the subtitle by issu
## Related
- [Configuration Reference](/configuration#jimaku) full config options
- [Mining Workflow](/mining-workflow#jimaku-subtitle-search) how Jimaku fits into the sentence mining loop
- [Troubleshooting](/troubleshooting#jimaku) additional error guidance
- [Configuration Reference](/configuration#jimaku) - full config options
- [Mining Workflow](/mining-workflow#jimaku-subtitle-search) - how Jimaku fits into the sentence mining loop
- [Troubleshooting](/troubleshooting#jimaku) - additional error guidance
+5 -2
View File
@@ -3,7 +3,7 @@
The `subminer` launcher is an all-in-one script that handles video selection, mpv startup, and overlay management. It is the recommended way to use SubMiner on Linux and macOS because it guarantees mpv is launched with the correct IPC socket and SubMiner defaults. It's a Bun script distributed as a release asset alongside the AppImage and DMG.
::: tip Windows users
On Windows, the recommended way to launch playback is the **SubMiner mpv** shortcut created during first-run setup double-click it, drag a file onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See [Windows mpv Shortcut](/usage#windows-mpv-shortcut) for details.
On Windows, the recommended way to launch playback is the **SubMiner mpv** shortcut created during first-run setup - double-click it, drag a file onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See [Windows mpv Shortcut](/usage#windows-mpv-shortcut) for details.
:::
## Video Picker
@@ -64,6 +64,7 @@ subminer video.mkv # play a specific file (default plugin c
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
subminer --backend x11 video.mkv # Force x11 backend for a specific file
subminer -u # check for SubMiner updates
subminer logs -e # export sanitized log ZIP
subminer stats # open immersion dashboard
subminer stats -b # start background stats daemon
```
@@ -77,6 +78,8 @@ subminer stats -b # start background stats daemon
| `subminer stats -b` | Start or reuse background stats daemon (non-blocking) |
| `subminer stats cleanup` | Backfill vocabulary metadata and prune stale rows |
| `subminer doctor` | Dependency + config + socket diagnostics |
| `subminer settings` | Open the SubMiner settings window |
| `subminer logs -e` | Export a sanitized log ZIP and print its path |
| `subminer config path` | Print active config file path |
| `subminer config show` | Print active config contents |
| `subminer mpv status` | Check mpv socket readiness |
@@ -118,4 +121,4 @@ With default plugin settings (`auto_start=yes`, `auto_start_visible_overlay=yes`
- Default log level is `info`
- `--background` mode defaults to `warn` unless `--log-level` is explicitly set
- `--dev` / `--debug` control app behavior, not logging verbosity use `--log-level` for that
- `--dev` / `--debug` control app behavior, not logging verbosity - use `--log-level` for that
+92 -121
View File
@@ -1,77 +1,18 @@
# Mining Workflow
This guide walks through the sentence mining loop from watching a video to creating Anki cards with audio, screenshots, and context.
This guide walks through the sentence mining loop - from watching a video to creating Anki cards with audio, screenshots, and context.
## Overview
SubMiner runs as a transparent overlay on top of mpv. As subtitles play, the overlay displays them as interactive text. You hover a word, trigger Yomitan lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, audio clip, and screenshot.
_Sentence mining_ means turning real sentences you encounter while watching native video into Anki flashcards, so you learn vocabulary in the context where you actually met it. SubMiner automates the tedious parts of that loop.
## Subtitle Delivery Path (Startup + Runtime)
SubMiner runs as a transparent overlay on top of mpv (the video player). As subtitles play, the overlay displays them as interactive text. You hover a word, trigger a Yomitan dictionary lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, an audio clip, and a screenshot to that card - no manual copy-pasting or screen capturing.
SubMiner prioritizes subtitle responsiveness over heavy initialization:
1. The first subtitle render is **plain text first** (no tokenization wait).
2. Tokenized enrichment (word spans, known-word flags, JLPT/frequency metadata) is applied right after parsing completes.
3. Under rapid subtitle churn, SubMiner uses a **latest-only tokenization queue** so stale lines are dropped instead of building lag.
4. MeCab, Yomitan extension load, and dictionary prewarm run as background warmups after overlay initialization (configurable via `startupWarmups`, including low-power mode).
This keeps early playback snappy and avoids mpv-side sluggishness while startup work completes.
## Overlay Model
SubMiner uses one overlay window with modal surfaces.
### Primary Subtitle Layer
The visible overlay renders subtitles as tokenized hoverable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
- Word-level hover targets for Yomitan lookup
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
- Auto pause/resume while the Yomitan popup is open (enabled by default via `subtitleStyle.autoPauseVideoOnYomitanPopup`)
- Right-click to pause/resume
- Right-click + drag to reposition subtitles
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
- **Reading annotations** — known words, N+1 targets, character-name matches, JLPT levels, and frequency hits can all be visually highlighted
Toggle visibility with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
### Secondary Subtitle Bar
The secondary subtitle bar is a compact top-strip region in the same overlay window for translation/context visibility while keeping primary reading flow below. It mirrors your configured secondary subtitle preference and can be independently shown or hidden.
It is controlled by `secondarySub` configuration and shares lifecycle with the main overlay window.
### Modal Surfaces
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
## Looking Up Words
1. Hover over the subtitle area — the overlay activates pointer events.
2. Hover the word you want. SubMiner keeps per-token boundaries so Yomitan can target that token cleanly.
3. Trigger Yomitan lookup with your configured lookup key/modifier (for example `Shift` if that is how your Yomitan profile is set up).
4. Yomitan opens its lookup popup for the hovered token.
5. From the popup, add the word to Anki.
### Controller Workflow
With a gamepad connected and keyboard-only mode enabled, the full mining loop works without a mouse or keyboard:
1. **Navigate** — push the left stick left/right to move the token highlight across subtitle words.
2. **Look up** — press `A` to trigger Yomitan lookup on the highlighted word.
3. **Browse the popup** — push the left stick up/down to smooth-scroll through the Yomitan popup, or use the right stick for larger jumps.
4. **Cycle audio** — press `R1` to move to the next dictionary audio entry, `L1` to play the current one.
5. **Mine** — press `X` to create an Anki card for the current sentence (same as `Ctrl+S`).
6. **Close** — press `B` to dismiss the Yomitan popup and return to subtitle navigation.
7. **Pause/resume** — press `L3` (left stick click) to toggle mpv pause at any time.
After controller support is enabled, the controller and keyboard can be used interchangeably — switching mid-session is seamless. Toggle keyboard-only mode on or off with `Y` on the controller.
See [Usage — Controller Support](/usage#controller-support) for setup details and [Configuration — Controller Support](/configuration#controller-support) for the full mapping and tuning options.
> **Yomitan** is the popup dictionary that shows definitions when you hover or scan a word. **AnkiConnect** is the add-on that lets SubMiner talk to Anki. Both are set up during installation - see [Anki Integration](/anki-integration) if you have not configured them yet.
## Creating Anki Cards
There are three ways to create cards, depending on your workflow.
There are four ways to create or enrich cards, depending on your workflow.
### 1. Auto-Update from Yomitan
@@ -80,11 +21,11 @@ This is the most common flow. Yomitan creates a card in Anki, and SubMiner enric
1. Hover a word, then trigger Yomitan lookup → Yomitan popup appears.
2. Click the Anki icon in Yomitan to add the word.
3. SubMiner receives or detects the new card:
- **Proxy mode** (`ankiConnect.proxy.enabled: true`): immediate enrich after successful `addNote` / `addNotes`.
- **Polling mode** (default): detects via AnkiConnect polling (`ankiConnect.pollingRate`, default 3 seconds).
- **Proxy mode** (default, `ankiConnect.proxy.enabled: true`): immediate enrich after a successful `addNote` / `addNotes` is pushed through the local proxy.
- **Polling mode** (fallback, when the proxy is disabled): detects new cards via AnkiConnect polling (`ankiConnect.pollingRate`, default 3 seconds).
4. SubMiner updates the card with:
- **Sentence**: The current subtitle line.
- **Audio**: Extracted from the video using the subtitle's start/end timing (plus configurable padding).
- **Audio**: Extracted from the video using the subtitle's start/end timing (plus optional configured padding).
- **Image**: A screenshot or animated clip from the current playback position.
- **Translation**: From the secondary subtitle track, or generated via AI if configured.
- **MiscInfo**: Metadata like filename and timestamp.
@@ -98,7 +39,7 @@ If you prefer a hands-on approach (animecards-style), you can copy the current s
1. Add a word via Yomitan as usual.
2. Press `Ctrl/Cmd+C` to copy the current subtitle line to the clipboard.
- For multiple lines: press `Ctrl/Cmd+Shift+C`, then a digit `1``9` to select how many recent subtitle lines to combine. The combined text is copied to the clipboard.
3. Press `Ctrl/Cmd+V` to update the last-added card with the clipboard contents plus audio, image, and translation the same fields auto-update would fill.
3. Press `Ctrl/Cmd+V` to update the last-added card with the clipboard contents plus audio, image, and translation - the same fields auto-update would fill.
Manual clipboard updates always replace generated sentence audio, even when `ankiConnect.behavior.overwriteAudio` is disabled. The word audio field is left unchanged because the word itself does not change in this flow.
@@ -120,7 +61,7 @@ Create a standalone sentence card without going through Yomitan:
The sentence card uses the note type configured in `isLapis.sentenceCardModel` and always maps sentence/audio to `Sentence` and `SentenceAudio`.
::: warning Requires Lapis/Kiku note type
Sentence card creation requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
Sentence card creation requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration - Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
:::
### 4. Mark as Audio Card
@@ -128,58 +69,91 @@ Sentence card creation requires a [Lapis](https://github.com/donkuri/lapis) or [
After adding a word via Yomitan, press the audio card shortcut to overwrite the audio with a longer clip spanning the full subtitle timing.
::: warning Requires Lapis/Kiku note type
Audio card marking requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
Audio card marking requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration - Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
:::
## Secondary Subtitles
SubMiner can display a secondary subtitle track (typically English) alongside the primary Japanese subtitles. This is useful for:
- Quick comprehension checks without leaving the mining flow.
- Auto-populating the translation field on mined cards.
### Display Modes
Cycle through modes with the configured shortcut:
- **Hidden**: Secondary subtitle not shown.
- **Visible**: Always displayed below the primary subtitle.
- **Hover**: Only shown when you hover over the primary subtitle.
When a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
## Field Grouping (Kiku)
### Field Grouping (Kiku)
If you mine the same word from different sentences, SubMiner can merge the cards instead of creating duplicates. This feature is designed for use with [Kiku](https://github.com/youyoumu/kiku) and similar note types that support grouped fields.
### How It Works
1. You add a word via Yomitan.
2. SubMiner detects the new card and checks if a card with the same expression already exists.
3. If a duplicate is found:
- **Auto mode** (`fieldGrouping: "auto"`): Merges automatically. Both sentences, audio clips, and images are combined into the existing card. The duplicate is optionally deleted.
- **Manual mode** (`fieldGrouping: "manual"`): A modal appears showing both cards side by side. You choose which card to keep and preview the merged result before confirming.
3. If a duplicate is found (this requires `ankiConnect.isKiku.fieldGrouping` to be set to `"auto"` or `"manual"`; it defaults to `"disabled"`):
- **Auto mode** (`ankiConnect.isKiku.fieldGrouping: "auto"`): Merges automatically. Both sentences, audio clips, and images are combined into the existing card. The duplicate is optionally deleted.
- **Manual mode** (`ankiConnect.isKiku.fieldGrouping: "manual"`): A modal appears showing both cards side by side. You choose which card to keep and preview the merged result before confirming.
See [Anki Integration Field Grouping](/anki-integration#field-grouping-kiku) for configuration options, merge behavior, and modal keyboard shortcuts.
See [Anki Integration - Field Grouping](/anki-integration#field-grouping-kiku) for configuration options, merge behavior, and modal keyboard shortcuts.
## Jimaku Subtitle Search
## Overlay Model
SubMiner integrates with [Jimaku](https://jimaku.cc) to search and download subtitle files for anime directly from the overlay.
SubMiner uses one overlay window with modal surfaces. It carries two subtitle bars - a primary reading bar and a secondary translation/context bar - plus modal dialogs that open on top.
1. Open the Jimaku modal via the configured shortcut (`Ctrl+Shift+J` by default).
2. SubMiner auto-fills the search from the current video filename (title, season, episode).
3. Browse matching entries and select a subtitle file to download.
4. The subtitle is loaded into mpv as a new track.
Toggle the entire overlay window with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
Requires an internet connection. An API key (`jimaku.apiKey`) is optional but recommended for higher rate limits.
### Primary Subtitle Layer
## Texthooker
The primary bar renders subtitles as tokenized hoverable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) that serves a texthooker UI. This allows external tools — such as a browser-based Yomitan instance — to receive subtitle text in real time.
- Word-level hover targets for Yomitan lookup
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
- Auto pause/resume while the Yomitan popup is open (enabled by default via `subtitleStyle.autoPauseVideoOnYomitanPopup`)
- Right-click to pause/resume
- Right-click + drag to reposition subtitles
- **Reading annotations** - known words, N+1 targets, character-name matches, JLPT levels, and frequency hits can all be visually highlighted
The texthooker page displays the current subtitle and updates as new lines arrive. This is useful if you prefer to do lookups in a browser rather than through the overlay's built-in Yomitan.
### Secondary Subtitle Bar
If you want to build your own browser client, websocket consumer, or automation relay, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
The secondary bar is a compact top-strip region in the same overlay window. It shows a secondary subtitle track (typically English) for translation/context while keeping the primary reading flow below. It is useful for:
- Quick comprehension checks without leaving the mining flow.
- Auto-populating the translation field on mined cards - when a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
It is controlled by `secondarySub` configuration and shares its lifecycle with the main overlay window. Cycle which track feeds it with `Shift+J`.
### Display Modes
Both the primary and secondary subtitle bars share the same three visibility modes, and each can be changed independently at runtime:
- **Hidden** - the bar is not shown.
- **Visible** - the bar is always shown.
- **Hover** - the bar is revealed only while you hover over the overlay.
By default the **primary** bar is `visible` (`subtitleStyle.primaryDefaultMode`) and the **secondary** bar is `hover` (`secondarySub.defaultMode`).
Cycle each bar's mode at runtime with its own shortcut:
| Shortcut | Action | Config key |
| ------------------ | -------------------------------------------------------- | ------------------------------ |
| `V` | Cycle primary subtitle mode (hidden → visible → hover) | overlay-local |
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
### Modal Surfaces
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
## Looking Up Words
1. Hover over the subtitle area - the overlay activates pointer events.
2. Hover the word you want. SubMiner keeps per-token boundaries so Yomitan can target that token cleanly.
3. Trigger Yomitan lookup with your configured lookup key/modifier (for example `Shift` if that is how your Yomitan profile is set up).
4. Yomitan opens its lookup popup for the hovered token.
5. From the popup, add the word to Anki.
### Controller Workflow
With a gamepad connected and keyboard-only mode enabled, the full mining loop works without a mouse or keyboard:
1. **Navigate** - push the left stick left/right to move the token highlight across subtitle words.
2. **Look up** - press `A` to trigger Yomitan lookup on the highlighted word.
3. **Browse the popup** - push the left stick up/down to smooth-scroll through the Yomitan popup, or use the right stick for larger jumps.
4. **Cycle audio** - press `R1` to move to the next dictionary audio entry, `L1` to play the current one.
5. **Mine** - press `X` to create an Anki card for the current sentence (same as `Ctrl+S`).
6. **Close** - press `B` to dismiss the Yomitan popup and return to subtitle navigation.
7. **Pause/resume** - press `L3` (left stick click) to toggle mpv pause at any time.
After controller support is enabled, the controller and keyboard can be used interchangeably - switching mid-session is seamless. Toggle keyboard-only mode on or off with `Y` on the controller.
See [Usage - Controller Support](/usage#controller-support) for setup details and [Configuration - Controller Support](/configuration#controller-support) for the full mapping and tuning options.
## Subtitle Sync (Subsync)
@@ -190,29 +164,26 @@ If your subtitle file is out of sync with the audio, SubMiner can resynchronize
3. For alass, select a reference subtitle track from the video.
4. SubMiner runs the sync and reloads the corrected subtitle.
Install the sync tools separately — see [Troubleshooting](/troubleshooting#subtitle-sync-subsync) if the tools are not found.
For remote streams, including Jellyfin playback, the modal only offers alass. Jellyfin subtitle URLs are cached as temporary subtitle files so alass can read them, but the video stream is not downloaded. ffsubsync needs direct access to the local media file and is unavailable for stream URLs.
## N+1 Word Highlighting
When you mine a sentence card from the stats dashboard, SubMiner can also use `alass` automatically to align a local English sidecar against the matching local Japanese sidecar before filling the card translation field. The source subtitle files are not modified; SubMiner writes a temporary retimed copy and reuses it while the stats server is running.
When enabled, SubMiner cross-references your Anki decks to highlight known words in the overlay, making true N+1 sentences (exactly one unknown word) easy to spot during immersion.
Install the sync tools separately - see [Troubleshooting](/troubleshooting#subtitle-sync-subsync) if the tools are not found.
See [Subtitle Annotations — N+1](/subtitle-annotations#n1-word-highlighting) for configuration options and color settings.
## Texthooker
## Immersion Tracking
SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) that serves a texthooker UI. This allows external tools - such as a browser-based Yomitan instance - to receive subtitle text in real time.
SubMiner can log your watching and mining activity to a local SQLite database and expose it in the built-in stats dashboard — session times, words seen, cards mined, and daily/monthly rollups.
The texthooker page displays the current subtitle and updates as new lines arrive. This is useful if you prefer to do lookups in a browser rather than through the overlay's built-in Yomitan.
Enable it in your config:
If you want to build your own browser client, websocket consumer, or automation relay, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
```jsonc
"immersionTracking": {
"enabled": true,
"dbPath": "" // leave empty to use the default location
}
```
## Related Features
Open the dashboard in the overlay with `stats.toggleKey` (default: `` ` ``), launch it in a browser with `subminer stats`, keep a dedicated background server alive with `subminer stats -b`, stop that background server with `subminer stats -s`, or visit `http://127.0.0.1:6969` directly if the local stats server is already running. The dashboard covers overview totals, anime progress, session detail, and vocabulary drill-down from the same local immersion database.
These features support the mining loop but have their own dedicated pages:
See [Immersion Tracking](/immersion-tracking) for dashboard details, schema, and retention settings.
- **[Jimaku subtitle search](/jimaku-integration)** - search and download anime subtitle files directly from the overlay (`Ctrl+Shift+J` by default), then load them into mpv.
- **[N+1 word highlighting](/subtitle-annotations#n1-word-highlighting)** - cross-reference your Anki decks to highlight known words, making true N+1 sentences (exactly one unknown word) easy to spot during immersion.
- **[Immersion tracking](/immersion-tracking)** - log watching and mining activity to a local database and view session times, words seen, and cards mined in the built-in stats dashboard.
Next: [Anki Integration](/anki-integration) field mapping, media generation, and card enrichment configuration.
Next: [Anki Integration](/anki-integration) - field mapping, media generation, and card enrichment configuration.

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