Compare commits

...
331 Commits
Author SHA1 Message Date
sudacode 9ffa2bce94 chore(release): prepare v0.20.1 2026-10-04 00:01:18 -07:00
sudacode 88b73656a7 chore(release): prepare v0.20.1-beta.2 2026-10-03 21:22:19 -07:00
sudacode e77aa5c3b3 fix(stats): count mined cards once and keep lexical stats exact after deletes (#280) 2026-10-03 21:16:12 -07:00
sudacode 121b688afb fix(deps): bump http-cache-semantics to 4.3.0 for GHSA-ch52-4w7c-c8xp (#283) 2026-10-03 21:13:32 -07:00
sudacode 727be5683a fix(youtube): keep mpv queue when advancing to a queued video on Windows (#281) 2026-10-02 22:13:35 -07:00
sudacode 138bfe9759 fix(subtitles): allow Whisper generation during Windows YouTube stream playback (#282) 2026-10-02 22:13:15 -07:00
sudacode 0aba17d95b chore(release): generate concise release notes per feature or area
- Rewrite polish prompt to default to inline items, nesting only for several distinct changes
- Add length/density example and drop internal details from release bodies
- Tell prerelease mode to shorten long baseline bullets
- Update changes/README.md and prompt assertions in tests
2026-10-02 19:52:16 -07:00
sudacode a5d0b63792 fix(storage): protect Yomitan profiles across Electron runtime changes (#211) 2026-10-01 21:44:53 -07:00
sudacode 15cb9fe918 test(launcher): allow Windows bootstrap integration startup time
Cold and cached launches each have a 15-second command timeout. Give the integration test time for both launches and executable copies instead of the default five-second budget on hosted Windows runners.
2026-10-01 01:42:10 -07:00
sudacode db45a70ec2 chore(release): prepare v0.20.1-beta.1 2026-10-01 01:21:59 -07:00
sudacode 9e79fa93c2 feat(overlay): forward mpv mouse button and wheel bindings (#279) 2026-10-01 00:44:51 -07:00
sudacode 9fadbc8cdd docs: align README with installation guide and fix Arch MeCab install
- Install MeCab from the AUR (mecab-git) on Arch, since it is not in the official repos
- Rewrite the README feature blurbs, requirements table, and quick start so they match docs-site/installation.md
- Add fuse2, X11 window tools, whisper.cpp, and the thumbnail tools to the README requirements
- Drop the playlist browser and texthooker sections and the standalone launcher download steps
- Mention AniList as a guessit consumer in the installation guide
2026-09-30 23:45:53 -07:00
sudacode ffbbe698fc fix(overlay): reload imported mpv keys after mpv connects (#278)
* fix(overlay): reload imported mpv keys after mpv connects

The overlay fetched mpv's input-bindings before SubMiner connected to mpv,
cached an empty list, and never refetched because the connect-time refresh
only broadcast session bindings when their compiled signature changed. Keys
from input.conf and mpv defaults (9/0 volume, m mute) then did nothing while
the overlay had focus, most often in mpv.backend x11 mode.

Main now emits mpv-input-bindings:changed whenever the discovered mpv key
set or client changes, and the renderer refreshes its imported keys on it.

* fix(overlay): subscribe to mpv key changes before initial discovery

The renderer registered the mpv-input-bindings:changed listener only after awaiting setupMpvInputForwarding and several other startup calls. If mpv connected in that window, the event was dropped and the imported mpv keys stayed empty.
2026-09-30 23:24:39 -07:00
sudacode 7b72dd2cc4 build(deps): bump axios to 1.20.0 2026-09-30 20:21:02 -07:00
sudacode 416cd7e9ac fix(overlay): keep Hyprland IPC env when forcing mpv X11 backend
- Stop stripping HYPRLAND_INSTANCE_SIGNATURE in applyX11EnvOverrides so the overlay can use hyprctl to lift Hyprland's fullscreen input block
- Fixes the overlay becoming unclickable over fullscreen mpv with mpv.backend: x11
- Update launcher and backend tests, add changelog fragment
2026-09-30 19:50:24 -07:00
sudacode eb749f8c19 fix(overlay): force packed float audio for macOS timing review preview
- Pass --audio-format=float to the preview mpv on darwin so coreaudio accepts Opus output on Bluetooth devices like AirPods instead of falling back to avfoundation
- Fixes the stuttering preview cursor that jumped to the end partway through the selection
- Thread the platform into buildMediaTimingPreviewArgs and add a test covering darwin vs linux args
2026-09-30 01:01:49 -07:00
sudacode 65ac5a7728 fix(linux): skip AppImage mount keepalive on sandbox squashfs mounts (#277) 2026-09-29 22:59:01 -07:00
sudacode df675cfbfe build(deps): bump brace-expansion override to 5.0.11 2026-09-29 20:30:42 -07:00
sudacode e49b5f6589 fix(overlay): send session help commands before closing the modal
- Closing help first could destroy the dedicated modal window on Linux/Windows, dropping the IPC for the selected command
- dispatchConfiguredMpvCommand now returns a promise so help closes only after subtitle seek + re-pause are sent
- Add test asserting IPC order for a paused subtitle seek
2026-09-29 20:22:01 -07:00
sudacode 0bbd8617f4 feat(overlay): run session help commands with Enter or double-click (#275) 2026-09-29 18:48:56 -07:00
sudacode 4e43c5c6e1 fix(overlay): keep overlay mapped during Hyprland/Sway fullscreen (#276) 2026-09-29 18:39:55 -07:00
sudacode 953abdf42a build(deps): bump electron to 42.10.0 and undici to 7.29.1
- Resolve security advisories blocking CI
- Add changelog fragment for patched dependencies
2026-09-29 18:37:16 -07:00
sudacode fdb0a31fff fix(overlay): drive timing review playhead from mpv playback position
- Preview session observes time-pos, waits for loaded media before seeking, and acks each command
- Forward positions to the modal over media-timing-review:preview-position
- Cursor follows the real position, pauses while buffering, and stays at the clip end when done
- Replace the fixed end-of-clip timer with a 15s stall timeout
- Document preview cursor behavior and ownership in docs
2026-09-29 18:07:07 -07:00
sudacode 04b878dd93 build(deps): bump fast-uri override to 3.1.7 2026-09-28 22:35:10 -07:00
sudacode 07b32626a6 feat(youtube): add YouTube browser window and Whisper subtitle source (#273) 2026-09-28 20:35:47 -07:00
sudacode c68b73c8c1 fix(overlay): thin centered trim handles and label-free subtitle boundaries in timing review (#274) 2026-09-28 19:53:38 -07:00
sudacode b24008237b fix(jellyfin): keep selecting subtitles when a track download fails (#272) 2026-09-24 21:32:56 -07:00
sudacode 63eb228e4f fix(jellyfin): use the current audio track for media generation (#271) 2026-09-24 21:14:34 -07:00
sudacode ba0155e429 fix(jellyfin): select Japanese subtitles before slower tracks finish (#270) 2026-09-24 20:42:14 -07:00
sudacode 23af9c6464 docs: restore docs/architecture pointer in development page 2026-09-24 16:58:31 -07:00
sudacode 8825cca642 docs: rewrite docs site pages to be shorter and easier to scan
- Pages now start with setup and usage, and reference material is in compact tables
- Configuration reference gives each config block a short explanation and a key/default table
- Internal detail removed from user pages, and docs that had drifted from current behavior fixed
- The status line shows today's date, set on the client, instead of the page's last-updated date
- Add changelog fragment
2026-09-24 16:20:42 -07:00
sudacode bc73a02623 ci(docs): serve frozen version archives from R2 (#269) 2026-09-24 15:28:58 -07:00
sudacode 5096ca217b chore(release): remove package size reports
- Drop the afterAllArtifactBuild size reporting hook and the previous-release baseline downloads in CI
- Stop uploading, checksumming, and publishing package-size-*.json
- Keep the afterPack package content audit, now without size tracking
- Update RELEASING.md and the verification docs, and add a changelog fragment
2026-09-23 21:50:19 -07:00
sudacode 285b19ccc3 fix(ci): download AUR assets without release metadata 2026-09-23 01:43:40 -07:00
sudacode 7fbc36dc41 chore(release): prepare v0.20.0 2026-09-23 01:15:20 -07:00
sudacode e93b696e71 chore(release): prepare v0.20.0-beta.2 2026-09-22 22:24:10 -07:00
sudacode 7c161723cd fix(anki): regenerate sentence furigana from the final sentence (#268) 2026-09-22 21:43:19 -07:00
sudacode b0fb39a8f5 feat(overlay): add optional subtitle selection modal (#265) 2026-09-22 18:30:26 -07:00
Abdulrazzaq Alhendi a7a302bdd0 fix(jellyfin): respect Windows mpv configuration when casting (#267) 2026-09-22 18:29:53 -07:00
sudacode 6a1d1211ae fix(release): resolve style.css asar lookup with native separators
The shared-font check still handed asar a forward-slash path, which
fails on Windows the same way the inventory listing did.
2026-09-22 02:29:28 -07:00
sudacode 7585d430ae fix(release): unbreak packaging workflow on all three platforms
- Bump electron-builder to 26.16.1 so macOS signing passes the temp keychain
  password to set-key-partition-list (upstream #10066), which the macOS 26
  runner image now enforces
- Stat asar entries with their native path in the package audit; splitting on
  path.sep made every nested file look missing on Windows
- Run the package smoke with --no-sandbox on Linux runners without a setuid
  chrome-sandbox
- Load the packaged stats dashboard over loopback HTTP in the smoke, matching
  how the app serves it since #263, and ignore Chromium's cache-only font probe
2026-09-22 02:15:49 -07:00
sudacode 122ff9a3c5 chore(release): prepare v0.20.0-beta.1
Trim pending change fragments, drop the breaking flag from the stats
request-safety change, and generate prerelease notes.
2026-09-22 01:29:58 -07:00
sudacode 786bd5af81 chore(yomitan): update vendored fork to upstream 26.9.8 2026-09-21 20:53:26 -07:00
sudacode 42d1384c68 fix(jellyfin): support modern authentication (#264) 2026-09-21 20:40:57 -07:00
sudacode 1508863dbb feat(stats): add TMDB metadata for live-action dramas in the Library (#252) 2026-09-21 00:10:00 -07:00
sudacode 4dd30f44d1 chore(yomitan): update fork to upstream 26.9.8 2026-09-21 00:09:48 -07:00
sudacode ab48a5678e fix(stats): restrict local requests and serve the dashboard over HTTP (#263) 2026-09-20 23:36:55 -07:00
sudacode 2f21582666 fix(overlay): cancel pending window transitions and timing reviews (#262) 2026-09-20 23:35:48 -07:00
sudacode 383aed8bad fix(stats): harden server lifecycle and verify compiled runtime (#261) 2026-09-20 23:19:03 -07:00
sudacode f9c4e892dc fix(stats): reject malformed resource IDs before mutations (#259) 2026-09-20 22:49:27 -07:00
sudacode 7959e1a4e5 fix(anki): honor unlimited duration in stats mining (#258) 2026-09-20 22:49:12 -07:00
sudacode 917d52dd94 fix(config): validate AnkiConnect and field grouping settings (#257) 2026-09-20 22:48:54 -07:00
sudacode 05425cc5db fix(anki): separate word audio mapping for animation sync (#256) 2026-09-20 22:07:30 -07:00
sudacode 8097ed27e5 feat(stats): separate YouTube channels in the Library (#253) 2026-09-20 19:57:32 -07:00
sudacode 76e43d7e6d fix(config): align live save feedback with hot reload policy (#255) 2026-09-20 19:51:45 -07:00
Abdulrazzaq Alhendi 4f0762e840 feat(mining): add a screenshot frame picker to media review (#254) 2026-09-20 19:39:30 -07:00
sudacode 026d495fac feat(jimaku): add live-action subtitle search (#251) 2026-09-18 22:50:13 -07:00
sudacode dd76782d30 fix(subtitles): widen subtitle generation modal
- Reduce text wrapping and scrolling while retaining small-window support
- Document the responsive layout change
2026-09-18 21:05:52 -07:00
sudacode a8c16147ad feat(subtitles): use loaded subtitles to guide generation timing (#249) 2026-09-18 00:24:42 -07:00
sudacode 186d4a0640 fix(jellyfin): fix jellyfin media metadata (#250) 2026-09-18 00:24:25 -07:00
sudacode 2831e05124 feat(subtitles): add local Japanese subtitle generation (#240) 2026-09-15 21:43:11 -07:00
sudacode 6841a37a22 fix(sidebar): preserve Space playback after cue seeking (#247) 2026-09-15 21:35:10 -07:00
sudacode 7c1eac03dc feat(overlay): discover unclaimed mpv key bindings (#246) 2026-09-15 18:26:45 -07:00
sudacode e6dc9dfec5 test: clone VM results across Electron boundaries
- Clone tokenizer VM results into the host realm
- Expect null-prototype profile maps in IPC validation tests
2026-09-11 01:55:32 -07:00
sudacode 6d69a56574 fix(overlay): keep Hyprland recovery dialogs above overlays (#245) 2026-09-11 01:52:01 -07:00
sudacode 0c37c665a2 build(release): reduce package size and report release sizes (#244) 2026-09-11 01:15:36 -07:00
sudacode 8ae5bde6a4 test(stats): increase occurrence merge test timeout 2026-09-11 01:09:59 -07:00
sudacode 016a43b804 feat(launcher): bundle Bun and use it across all launchers (#243) 2026-09-11 00:21:10 -07:00
sudacode fb2d1a512b perf(stats): use compressed incremental snapshot transfers (#241) 2026-09-10 23:57:20 -07:00
sudacode 614a8ca912 fix(startup): create config directory before singleton lock (#242) 2026-09-09 12:04:08 -07:00
sudacode 9eecb7358b docs: refresh user-facing guides and examples
- Clarify current integration, media review, and Anki behavior
- Remove obsolete AI translation and configuration references
- Standardize headings and prose across docs
2026-09-08 00:23:38 -07:00
sudacode 1e5d7747b4 feat(sidebar): add dialogue selection and copying (#238) 2026-09-07 14:03:54 -07:00
sudacode c14c690875 chore(release): prepare v0.19.6 2026-09-04 01:59:39 -07:00
sudacode 84f718043a feat(anki): add media timing review before card creation (#203) 2026-09-04 01:40:56 -07:00
sudacode 99266294b8 fix(tsukihime): filter releases by subtitle language (#235) 2026-09-02 20:29:59 -07:00
sudacode c055359be1 chore(deps): update fast-uri to 3.1.6 2026-09-02 20:16:08 -07:00
sudacode ec5a147095 fix(subtitles): merge wrapped positioned caption rows (#234) 2026-09-01 23:11:38 -07:00
sudacode a0635f4360 fix(subtitles): drop ASS furigana from recorded cues (#233) 2026-09-01 22:49:20 -07:00
sudacode c0a78ef008 feat(anki): support Senren scene-switching field grouping (#230) 2026-09-01 22:18:30 -07:00
sudacode 87b01155df fix(mining): copy multi-line subtitles backward from current line (#231) 2026-09-01 22:17:41 -07:00
sudacode fc5c49e365 fix(subtitles): keep native secondary subtitles hidden (#232)
- Reapply hidden visibility after secondary track changes
- Relinquish suppression only after successful restoration
2026-09-01 00:49:37 -07:00
sudacode a20269e9f5 fix(jellyfin): stop inferring subtitle delays (#227) 2026-08-31 23:31:31 -07:00
sudacode 2ad491e95c chore(release): prepare v0.19.5 2026-08-30 01:02:44 -07:00
sudacode 6fffcc731f fix(youtube): keep auto captions on screen for their full span (#219) 2026-08-29 13:18:55 -07:00
sudacode 5cc21113fd fix(subtitles): drop ASS furigana from published cues (#224) 2026-08-29 13:15:30 -07:00
sudacode 051140f910 fix(anki): respect configured fields for word-card enrichment (#223) 2026-08-29 13:09:31 -07:00
sudacode 6e945f0872 fix(subtitles): keep overlapping lines that join an already active cue (#221) 2026-08-27 23:13:29 -07:00
sudacode c2c25c0da6 fix(anki): keep overlay progress visible through card updates (#218) 2026-08-25 20:27:58 -07:00
sudacode 556de61756 chore(changelog): split release details into nested bullets
- Update changelog generation guidance and tests
- Reformat current release notes and document the new style
2026-08-25 12:39:45 -07:00
sudacode a98fb0fddf chore(release): prepare v0.19.4 2026-08-25 02:43:48 -07:00
sudacode e816b3b371 fix(subtitles): recover positioned ASS word spacing and drop control debris (#217) 2026-08-25 02:22:21 -07:00
sudacodeandsudacode ed7d3f4c3d feat(release): track prerelease deltas and validate committed notes (#216)
Co-authored-by: sudacode <claude@lmaoxd.lol>
2026-08-23 15:24:18 -07:00
sudacode 509dc5bf7f fix(subtitles): improve secondary subtitle extraction and display (#215) 2026-08-23 01:56:49 -07:00
sudacode 0a0aa3ec98 chore(release): prepare v0.19.4-beta.4 2026-08-23 01:23:16 -07:00
sudacode b87cc3cfdd chore(workflow): add release skill 2026-08-23 01:14:03 -07:00
sudacode 8cb3c8c90a fix(macos): pin window helper deployment target to fix older systems
- Build the Swift window-tracking helper with an explicit `-target ...-apple-macos12.0` instead of letting swiftc stamp the build machine's OS, so the overlay attaches to mpv on older macOS (e.g. Ventura) instead of crashing at load
- Fold the standalone build-macos-helper.sh into prepare-build-assets.mjs and update docs/tests to match
2026-08-23 00:58:24 -07:00
sudacode 03ea903927 fix(subtitles): preserve authored spaces in fragmented ASS karaoke
- Only trim indentation before slicing ASS event fields so a trailing authored space at a fragment's end is kept, preventing words from being joined together when fragmented karaoke lines are reconstructed
- Add regression test covering event-boundary word spacing
2026-08-22 23:32:35 -07:00
sudacode 3aea42e6f8 chore(release): prepare v0.19.4-beta.3 2026-08-21 23:25:37 -07:00
sudacode 88bb3edfa4 fix(subtitles): preserve lyric selection and deduplicate secondary text
- Seek sidebar selections past overlapping karaoke exit spans
- Collapse duplicate long ASS lines and omit reconstructed sign grids
2026-08-21 01:28:00 -07:00
sudacode 9445aef004 fix(subtitles): navigate by sanitized cues across ASS animation
- Route subtitle navigation through parsed cue boundaries
- Treat explicit short seeks as subtitle priming events
2026-08-20 00:20:51 -07:00
sudacode c01bcd9d0f fix(subtitles): collapse layered ASS lyrics across startup and seeks
- Reconstruct fragment-only karaoke while preserving canonical animated signs
- Reprocess live subtitles after initial playback and seek updates
- Add a development launcher for playable media files
2026-08-19 22:55:10 -07:00
sudacode 695613b1e7 chore(release): prepare v0.19.4-beta.2 2026-08-19 02:19:40 -07:00
sudacode 20797772f8 fix(character-dictionary): cache completed MeCab refreshes (#212) 2026-08-19 01:49:15 -07:00
sudacode 72c312810a docs: link README demo to GitHub video playback
- Use GitHub-hosted video playback for the README demo
- Document the animated fallback behavior
2026-08-19 01:02:05 -07:00
sudacode b61f050e78 docs: update mining demo and scale video crops
- Refresh mining demo assets and documentation references
- Scale mpv crop coordinates for resized OBS recordings
2026-08-19 00:55:31 -07:00
sudacode 2a77ba9dd4 fix(launcher): restore Matroska thumbnails in Linux rofi picker (#210) 2026-08-18 20:30:58 -07:00
sudacode 4ed878270f fix(stats): accept legacy lexical rollup state schemas 2026-08-18 02:24:49 -07:00
sudacode 1d1b0c7bb6 fix(subtitles): collapse duplicate primary ASS style layers
- Use parsed cues when they fully explain mpv live text
- Preserve unmatched overlapping dialogue and signs
2026-08-18 01:51:19 -07:00
sudacode db61ce358d fix(stats): preserve session rollups across schema upgrades
- Stop deleting imm_daily_rollups/imm_monthly_rollups on unrelated schema version bumps; their source session/telemetry rows may already be pruned, so deleted buckets could not be rebuilt
- Run startup session-rollup maintenance before the lexical rollup backfill takes the write lock, so recovery no longer races playback writes
- Update the vocabulary summary totals changelog fragment to reflect that watch-time, activity, efficiency, and library charts are no longer cleared during rebuilds
2026-08-18 01:12:58 -07:00
sudacode e9778a945a fix(overlay): deduplicate secondary subtitle rendering (#208) 2026-08-18 00:44:02 -07:00
sudacode 7de73e16a1 fix(stats): report complete vocabulary totals and new-word history (#202) 2026-08-18 00:42:46 -07:00
sudacode 273652f781 fix(subtitles): recover canonical lines from ASS animation (#207) 2026-08-18 00:30:48 -07:00
sudacode 14c0086520 chore(stats): reword duplicates popup 2026-08-17 18:42:58 -07:00
sudacode ff0bc0af6d fix(dictionary): prevent freezes and restore AppImage notifications (#205) 2026-08-17 18:41:03 -07:00
sudacode 00b1b79bf4 fix(mpv): recover from stalled IPC connects (#204) 2026-08-16 22:58:28 -07:00
sudacode e11a5fea0d chore(release): prepare v0.19.4-beta.1 2026-08-16 02:00:32 -07:00
sudacode f73fe179d0 fix(docs): keep versioned pages out of search indexes
- Add self-canonical noindex signals and headers for archived docs
- Restore sitemap lastmod dates from the tracked checkout
2026-08-16 01:45:08 -07:00
sudacode 2938e7a32a fix(overlay): prevent Windows mouse lag during click-through tracking (#201) 2026-08-16 01:34:51 -07:00
sudacode 82f6b4705a fix(overlay): recycle Windows modal windows after close
- Refresh the hidden modal renderer between Windows sessions
- Add regression coverage and stabilize launcher completion testing
2026-08-16 01:10:07 -07:00
sudacode a02c33dac4 fix(overlay): keep macOS modal windows on fullscreen Spaces (#200) 2026-08-15 21:43:26 -07:00
sudacode 2174e689a2 fix(overlay): support native Wayland file drag-and-drop (#199) 2026-08-15 00:15:38 -07:00
sudacode 7d729cb60c fix(notifications): replace Linux progress updates in place (#198) 2026-08-14 22:20:33 -07:00
sudacode d963def1a9 fix(stats): subtract lifetime totals incrementally on delete (#196) 2026-08-14 22:13:59 -07:00
sudacode 6fb3d2eb13 feat(stats): add library entry merge and episode move (#190) 2026-08-14 21:54:17 -07:00
sudacode c5a77ac067 fix(anki): snapshot mining media clip timing (#197) 2026-08-14 21:50:32 -07:00
sudacode 8ddc151435 docs: hide unfinished feature demos from sidebar 2026-08-14 21:50:08 -07:00
sudacode b98d4d65c7 fix(stats): stop counting duplicate typeset subtitle lines (#191) 2026-08-14 00:12:56 -07:00
sudacode 046e74ea91 chore(workflow): simplify agent tooling and docs 2026-08-13 23:34:04 -07:00
sudacode 8bf847503d fix(media): tolerate slow MKV audio extraction (#195) 2026-08-13 23:19:22 -07:00
sudacode 47b5903392 chore(release): prepare v0.19.3 2026-08-13 23:02:57 -07:00
sudacode bf85554d1e fix(stats): batch deletes off the main thread (#194) 2026-08-13 22:31:52 -07:00
sudacode d74c7e1235 chore: add Claude instructions symlink and update js-yaml 2026-08-11 22:31:26 -07:00
sudacode 57ddd19953 fix(overlay): handle X11 display scaling across monitors (#193) 2026-08-11 22:24:01 -07:00
sudacode ee25536d90 fix(dictionary): stop large character dictionaries from timing out (#189) 2026-08-11 18:40:18 -07:00
sudacode 7b0fbdf254 fix(subtitles): collapse duplicate ASS events and decode text once (#186) 2026-08-10 22:21:44 -07:00
sudacode 2fefc83e3f fix(playback): stop forcing legacy OpenGL renderer on X11 mpv backend (#188) 2026-08-06 23:52:35 -07:00
sudacode dbdf578c68 perf(tokenizer): single-pass Yomitan scan with cross-line caching and prefetch fixes (#185) 2026-08-06 21:44:09 -07:00
sudacode 441ecf3c04 feat(overlay): add in-app changelog modal (#187) 2026-08-05 22:19:13 -07:00
sudacode a0dde4ee3e chore(release): v0.19.2 2026-08-04 18:51:15 -07:00
sudacode fe4dacc1e7 fix(overlay): show plain subtitle line immediately on tokenization cache miss (#184) 2026-08-04 01:55:52 -07:00
sudacode b08cd0db35 fix(streaming): keep subtitle tokenization prefetch warm for full episodes (#183) 2026-08-03 21:22:18 -07:00
sudacode bffb1c5982 fix(logging): surface subtitle processing debug/warn logs (#182) 2026-08-03 20:44:39 -07:00
sudacode 5b8848518a feat(subsync): add reference and target subtitle track picker (#181) 2026-08-03 01:00:14 -07:00
sudacode 176edd67f1 chore(release): v0.19.1 2026-08-01 23:59:20 -07:00
sudacode 4d65dec340 fix(youtube): prevent playlist URLs from stalling yt-dlp probes (#180) 2026-08-01 22:56:28 -07:00
sudacode 6607c333bc fix(overlay): strip spinner frame from subsync overlay card
- Add overlayBody override to ConfiguredStatusNotificationOptions so overlay/OSD/desktop can diverge
- Extract getSubsyncStatusNotificationOptions() to strip the ASCII spinner frame from the overlay card (OSD keeps it since it renders the raw spinner)
- Add tests for spinner stripping and subsync result notifications
2026-07-31 18:03:26 -07:00
sudacode b2bbf1ae12 chore: regenerate config example artifacts 2026-07-31 17:49:02 -07:00
sudacode b204d4dd6e feat(anki): add configurable word card type for Kiku/Lapis (#175) 2026-07-31 17:17:29 -07:00
sudacode 89ed675935 fix(overlay): keep Yomitan popup interactive on macOS/Windows (#177) 2026-07-30 19:47:08 -07:00
sudacode 1995200e76 chore(release): prepare v0.19.0 2026-07-29 22:26:03 -07:00
sudacode e876e483d6 chore(release): prepare v0.19.0-beta.5 2026-07-29 02:09:01 -07:00
sudacode 6d2a72e13b feat(stats): add library entry deletion and app-wide delete progress (#174) 2026-07-29 02:00:16 -07:00
sudacode 0d7084c8aa fix(anilist): resolve later seasons via sequel relations, not title guessing (#173) 2026-07-28 22:56:26 -07:00
sudacode 95e0abc7b7 test(immersion): apply runtime SQLite pragmas in query tests
The query test suite opened each temp database with SQLite's defaults
(rollback journal, synchronous=FULL) while the runtime opens them via
applyPragmas (WAL, synchronous=NORMAL). Every statement therefore ran as
its own fsync-ing transaction.

That is unnoticeable on a fast local disk but dominates on CI: the file
took 40.6s there versus 0.5s locally, and the 106-insert case
'getVocabularyStats pages past hidden rows' crossed the 5s per-test
timeout and failed the quality gate.

Open test databases through a helper that applies the same pragmas as
production, so the tests exercise the runtime's actual SQLite
configuration instead of a slower one.
2026-07-28 02:23:16 -07:00
sudacode d9155ceacb docs: update requirements table and Windows install instructions
- Add TsukiHime and AniSkip to feature table; note xz dependency
- Clarify Anki+AnkiConnect is required to mine, not to run
- Rework Windows setup with winget/scoop commands and PATH steps
- Note ffmpeg has no path-override setting, unlike mpv
- Fix screenshot alt text and installation.md anchor link
2026-07-28 02:10:50 -07:00
sudacode 9d0585423c chore(release): prepare v0.19.0-beta.4 2026-07-28 01:09:48 -07:00
sudacode 455cfff90a docs: fix inaccuracies and add link/anchor validation tests
- Generate config.example.jsonc with a Linux socket path instead of Windows, so the example stays reproducible across platforms
- Document TsukiHime config (`tsukihime.*`), maturity-based known-word highlighting keys, and other config surfaces missing from the reference
- Correct secondary-subtitle auto-load defaults and Anki field-matching (case-insensitive) claims
- Expand Windows installation guide with winget/Scoop package tables and manual PATH setup
- Add docs-site/links.test.ts to catch broken internal links and heading-anchor mismatches
- Fix stale cross-references in jimaku-integration.md and demos.md
2026-07-28 00:56:38 -07:00
sudacode 987b325edb Anki maturity-based known-word highlighting (#172) 2026-07-27 23:58:02 -07:00
sudacode 08c6807cb1 fix(launcher): normalize rofi prompt spacing
- Add formatRofiPrompt to trim trailing whitespace and append a single space, keeping the prompt from running into the input field
- Apply it across all rofi -p usages in picker.ts and history-command.ts
- Add tests for formatRofiPrompt edge cases (empty/whitespace-only prompts)
2026-07-26 00:13:54 -07:00
sudacode 18c6410f24 feat(launcher): add previous episode option to history entry menu
- Extract buildHistoryEntryActions to build the series action menu, now including a previous-episode option alongside replay/next/browse/quit
- Update docs and changelog entry to describe the new option
- Add tests covering previous/replay/next ordering and omission when last watched file is missing
2026-07-25 23:35:44 -07:00
sudacode e223cf9b71 fix(release): restore compatible js-yaml override 2026-07-22 23:51:21 -07:00
sudacode c718bb8b34 chore(release): prepare 0.19.0-beta.2 2026-07-22 23:34:08 -07:00
sudacode 177f7e5fd6 docs(readme): remove TsukiHime credit
- Drop the TsukiHime row from the open-source credits table
2026-07-22 23:07:07 -07:00
sudacode 20b50ec4e1 feat(launcher): add post-playback history menu with previous episode (#170) 2026-07-22 23:05:53 -07:00
sudacode deae61b211 refactor: split anki-connect and stats-server resolvers into modules (#169) 2026-07-17 23:05:59 -07:00
sudacode 44959ed282 fix(anki,appimage): handle proxy port conflicts and stray GPU child
- AnkiConnect proxy now detects EADDRINUSE, logs a warning, and surfaces an overlay notification instead of crashing video startup
- Background AppImage bootstrap runs Chromium with in-process-gpu so no GPU child survives app.exit() and outlives the FUSE mount, fixing the remaining DrKonqi "Service Crash" case
- Add changelog fragment for the proxy fix; update the AppImage quit fragment with the GPU child root cause
2026-07-17 23:04:56 -07:00
sudacode 2398f5c030 refactor(main): eliminate local pass-through wrappers in main.ts (#168) 2026-07-15 03:22:03 -07:00
sudacode 8711cf1a48 remove redundant isVerbNonIndependent and consolidate test lanes
- drop always-true isVerbNonIndependent export; collapse shouldMerge branch
- remove node-compat exclusion list from bun-src-full lane
- simplify test:fast (drop test:runtime:compat), test:env, test:smoke:dist scripts
- rename test:launcher:env:src -> test:env in docs
2026-07-14 19:57:50 -07:00
sudacode 9f4888293b chore(release): prepare 0.19.0-beta.1 2026-07-13 23:47:50 -07:00
sudacode 29332b103e Consolidate sync changelog fragments into one entry
- Merge the five separate sync change fragments (launcher command, UI window, close/quit fixes, active-session double-count, monthly rollup timezone) into a single changes/sync.md
- Fold kana-pos-content-classification.md into annotation-noise-rule-table.md
- Drop the now-stale Unreleased sections from CHANGELOG.md and docs-site/changelog.md
- Tweak tsukihime-english-subtitles.md wording
2026-07-13 23:40:28 -07:00
sudacode 8b2cee2c58 ci: reuse quality gate across CI and release workflows (#167) 2026-07-13 23:14:19 -07:00
sudacode 2e2ee3f028 refactor(tsukihime): swap Animetosho backend for TsukiHime API (#165) 2026-07-13 22:03:35 -07:00
sudacode 49b926e08c Sync Stats & History window, headless --sync-cli, and Windows remote support (#160) 2026-07-13 18:56:51 -07:00
sudacode 66f8ca4f80 refactor(renderer): extract modal registry for open/dismiss/active queries (#166) 2026-07-13 18:16:39 -07:00
sudacode 6fe1e0fee4 refactor(stats): drop dead IPC handlers, unify stats types over HTTP (#164) 2026-07-13 12:09:56 -07:00
sudacode 0ab840b362 refactor(tokenizer): extract subtitle annotation filter into rule table (#162) 2026-07-12 23:54:13 -07:00
sudacode 21a06c12fb refactor(tokenizer): consolidate kana/POS helpers into token-classification (#161) 2026-07-12 23:27:25 -07:00
sudacode 14070acceb fix(overlay): defer autoplay release for untokenized startup priming
- Autoplay-ready gate now ignores untokenized subtitle payloads while tokenization warmup is pending, so playback doesn't resume before the first tokenized cue
- Wire isTokenizationReady dep through main.ts and update gate/wiring tests accordingly
- Drop stale release/release-notes.md, add changelog entry under changes/
2026-07-12 02:11:10 -07:00
sudacode 7d81342f0f fix(app): keep AppImage mount alive until Chromium children finish shutdown
Background AppImage launches executed directly from a FUSE mount owned by a
short-lived helper's AppImage runtime. On quit, the runtime unmounted the
squashfs while utility children (network service) were still mid-shutdown,
SIGBUSing them and triggering KDE Service Crash notifications on every video
close. Detach a POSIX-sh supervisor instead: mount via --appimage-mount, run
AppRun from the mount, and release the holder only once no process still
executes from the mount. Falls back to the old direct launch on any failure
or with SUBMINER_NO_APPIMAGE_MOUNT_KEEPALIVE=1.
2026-07-12 02:09:42 -07:00
sudacode 4b7f750919 feat(animetosho): add English/Japanese subtitle download integration (#159) 2026-07-12 00:48:12 -07:00
sudacode 6ab3d823a4 feat(shortcuts): make clipboard-video-append shortcut configurable (#158) 2026-07-11 23:36:44 -07:00
sudacode 8797719a09 docs(docs-site): correct and clarify docs against current code
Comprehensive accuracy pass over docs-site verifying every page against
current source. Fixes wrong/stale claims (AniSkip default, YouTube track
selection, Anki sentence-card requirements, immersion schema v18 + SQL
column names, plugin entrypoint, launcher flags, Cloudflare deploy path,
etc.) and fills gaps (watch history, mediaCache.maxHeight, youtubeSubgen,
character-dictionary refresh/eviction, expanded hot-reload lists).
2026-07-11 00:53:34 -07:00
sudacode 8acc78cc1c test(tokenizer): add golden-file regression corpus with 11 fixtures (#157) 2026-07-11 00:50:03 -07:00
sudacode 321461c50f feat(launcher): add subminer sync for cross-machine stats/history merge over SSH (#152) 2026-07-11 00:40:15 -07:00
sudacode beeab564b4 chore(release): prepare v0.18.0 2026-07-10 02:03:10 -07:00
sudacode cab7975a2b chore(docs): remove implemented plan docs
- Drop early-managed-overlay-startup and macos-notification-hover design docs (both implemented, no longer needed)
2026-07-10 01:09:52 -07:00
sudacode 846292809c docs(changes): consolidate and tighten changelog entries
- Merge related fix entries into single files (name-match-span-merge into character-name-split-and-scan-recovery, kanji-non-independent-noun-annotations + lexicalized-expression-frequency into content-adverb-annotation-stop-list, greedy-name-parsing-priority into character-name-split-and-scan-recovery, kiku-field-grouping-cancel-double-toast into kiku-field-grouping-modal-recovery, history-rofi-cover-art into launcher-history-command)
- Rewrite remaining entries for brevity and reclassify stats-trend-title-limits as "changed"
2026-07-10 01:04:01 -07:00
sudacode 8712780d08 fix(tokenizer): keep frequency rank for lexicalized kana expressions (#156) 2026-07-10 00:54:17 -07:00
sudacode 7b1a005a65 feat(anki): mirror mpv software volume into generated sentence audio (#155) 2026-07-10 00:44:23 -07:00
sudacode 84c75f50aa fix(release): skip gh attribution without CI token 2026-07-09 02:26:00 -07:00
sudacode 8b838f2c7d chore(release): prepare 0.18.0-beta.3 2026-07-09 01:20:02 -07:00
sudacode db4139ba0b fix(tokenizer): block kanji reading collisions and name/generic length t
- Add allowReadingOnlyMatch:false to kanji token known-word lookups so 渓谷/けいこく no longer matches a mined 警告/けいこく card via reading-only index
- Greedy name pre-pass yields when a strictly longer generic word starts at the same position (空 no longer splits 空気; ties still go to the name)
2026-07-09 01:03:58 -07:00
sudacode 6c251502b3 fix(tokenizer): exclude unparsed-run tokens from annotations and N+1 (#153) 2026-07-09 00:28:38 -07:00
sudacode ae40934d3a fix(tokenizer): greedy name pre-pass to prevent generic matches swallowing character names (#151) 2026-07-08 23:53:45 -07:00
sudacode cdb1475a54 chore(assets): update icons and favicons 2026-07-08 22:35:47 -07:00
sudacode a2e49b369b fix(tokenizer): merge scanner metadata per token instead of all-or-nothi
- Replace hasSameTokenSpans + full-discard with mergeScannerTokensIntoParseTokens
- Grafts isNameMatch/frequencyRank/etc onto matching parse spans; filler chunks degrade only themselves
- Fixes name annotations dropping for entire subtitle lines containing unmatched interjections
2026-07-08 22:26:34 -07:00
sudacode 7f13aed50a fix(overlay): keep frequency/JLPT highlight for kanji non-independent nouns (#150) 2026-07-08 22:25:32 -07:00
sudacode 8b21a2bca8 fix(overlay): remove content adverbs from annotation stop-word list
- 確かに and やはり no longer excluded from frequency/JLPT highlighting and vocab stats
- Stop-word list now covers only interjections, pronouns, and grammar fragments
2026-07-08 20:15:45 -07:00
sudacode 925413adfe chore(release): prepare 0.18.0-beta.2 2026-07-08 02:31:14 -07:00
sudacode d0644ab2eb fix(stats): parse v3 reading-aware known-word cache in stats server (#149) 2026-07-08 02:15:58 -07:00
sudacode d253710c2e fix(stats): fetch cover art eagerly at session start instead of on series page visit (#148) 2026-07-08 00:59:35 -07:00
sudacode c3df510e4f docs(release): reclassify audio normalization as added, not fixed
- Move card audio normalization entry from Fixed → Added in prerelease notes
- Update changes/audio-normalization.md type: fixed → added
2026-07-08 00:38:38 -07:00
sudacode 187f68e5b6 fix(tokenizer): prevent grammar tokens from borrowing known-word highlight via unrelated readings (#147) 2026-07-07 23:57:47 -07:00
sudacode 0e254cbbef fix(launcher): move fzf previews below menus 2026-07-07 22:36:56 -07:00
sudacode 7b94adafbd chore(release): prepare 0.18.0-beta.1 2026-07-07 02:40:05 -07:00
sudacode 61f39d1e09 fix(overlay): resolve unspaced Japanese name splits and scan recovery (#146) 2026-07-07 02:33:46 -07:00
sudacode e7739de51c fix(tokenizer): keep unparsed Yomitan tokens hoverable (#145) 2026-07-07 02:01:04 -07:00
sudacode ad1d240f20 refactor(tests): centralize lane definitions and add per-file isolation (#143) 2026-07-07 01:51:05 -07:00
sudacode 8b0ef662bc chore: remove shipped changelog entries and demo asset
- Drop changes/kiku-field-grouping-modal-wiring.md (landed in #138)
- Drop changes/known-word-partial-reading-highlight.md (landed in #142)
- Remove assets/minecard.mkv demo video
2026-07-07 00:18:48 -07:00
sudacode 0a58c20ad7 fix(stats): start stats server on background app launch (#144) 2026-07-07 00:16:57 -07:00
sudacode 38ddb29aa0 feat(anki): reading-aware known-word matching (cache v3) (#142) 2026-07-07 00:13:10 -07:00
sudacode 8b9a70c5a6 feat(stats): Trends dashboard overhaul — title visibility, ranking modes, calendar-accurate windows, tooltips (#140) 2026-07-06 23:52:43 -07:00
sudacode 48a084914a feat(branding): replace app icon with contributed pixel-art set (#141) 2026-07-06 23:31:19 -07:00
sudacode a042b04357 fix(overlay): prevent field grouping modal from freezing overlay on Hyprland (#138) 2026-07-06 22:13:14 -07:00
sudacode 35ca2afc6f fix(overlay): collapse karaoke syllable spam in secondary subtitles (#139) 2026-07-06 01:09:06 -07:00
sudacode b14f977e33 fix(tokenizer): stop partial furigana readings from marking words known (#137) 2026-07-05 17:02:44 -07:00
sudacode eef4500599 feat(launcher): add -H/--history command to browse local watch history (#136) 2026-07-05 16:46:05 -07:00
sudacode 73af1451b7 fix(anki): lower audio normalization target to -23 LUFS (EBU R128) 2026-07-05 01:13:54 -07:00
sudacode 36a3704815 fix(youtube): parse mpv EDL stream URLs with byte-length guards (#134) 2026-07-02 01:37:37 -07:00
sudacode 359cb0a301 Normalize generated Anki audio by default (#135) 2026-07-02 00:34:16 -07:00
sudacode 4b10e85053 chore(release): prepare v0.17.2 2026-06-28 23:03:18 -07:00
sudacode c942a2cf2d fFix(youtube): recover source URL for background media cache on direct mpv open (#132) 2026-06-28 22:46:11 -07:00
sudacode f65afa6046 feat(youtube): notify on manual picker open and show success after track load (#133) 2026-06-28 22:43:16 -07:00
sudacode 389d8e06e0 chore: remove stale npm artifacts 2026-06-28 20:18:08 -07:00
sudacode 0008b55b70 chore(release): prepare v0.17.1 2026-06-27 22:59:28 -07:00
sudacode d16ae9c745 chore(release): prepare v0.17.1-beta.1 2026-06-27 22:10:02 -07:00
sudacode 36f94151b8 fix(logs): use local date for log filenames and expand export redaction (#131) 2026-06-27 21:08:11 -07:00
sudacode 5326ad32f5 feat(youtube): add mediaCache mode and safer stream media extraction (#130) 2026-06-27 19:20:18 -07:00
sudacode d199376364 chore(release): prepare v0.17.0 2026-06-15 01:58:42 -07:00
sudacode 3aee88c150 chore(release): prepare v0.17.0-beta.2 2026-06-14 20:58:26 -07:00
sudacode 70da3ee8bd Fix Windows Anki startup and overlay regressions (#128) 2026-06-14 20:51:56 -07:00
sudacode aa8eb753f6 fix: scope prerelease note reuse by version 2026-06-14 18:04:03 -07:00
sudacode 8d73de8731 docs: scope 0.17.0 beta prerelease notes 2026-06-14 17:50:09 -07:00
sudacode 5a98397efe chore: prepare 0.17.0-beta.1 prerelease 2026-06-14 17:34:05 -07:00
sudacode a117c5759c fix(linux): auto-install managed plugin copy; include in asset updates (#127) 2026-06-14 17:25:28 -07:00
sudacode ae7e6f82a8 fix(overlay): restore macOS Yomitan popup focus without breaking click-away (#125) 2026-06-14 16:46:13 -07:00
sudacode 1158be5b39 fix(overlay): preserve visible state across playlist item transitions (#124) 2026-06-12 23:38:54 -07:00
sudacode 33e767458f refactor(main): split main.ts into focused runtime modules (#123) 2026-06-12 17:35:46 -07:00
sudacode 94a65416ae fix(stats): strip Season N suffix from AniList title searches (#121) 2026-06-12 01:07:11 -07:00
sudacode 0a384a22c9 Replace subtitle delay actions with native mpv keybindings (#120) 2026-06-12 00:03:06 -07:00
sudacode b3b45521b6 fix(release): preserve attribution placement; default update notifs to o
- Move What's Changed/New Contributors before Installation in release notes
- Preserve committed attribution when regenerating via writeReleaseNotesForVersion
- Change notificationType default from 'both' to 'overlay' for new installs
2026-06-10 23:53:31 -07:00
sudacode 131b23efa9 chore(release): prepare v0.16.0 2026-06-10 23:05:37 -07:00
sudacode e2afceb492 fix(anki): write sentence card audio only to sentence audio field (#118) 2026-06-10 22:51:19 -07:00
sudacode 7be1843c41 feat(notifications): add overlay notifications with position config (#110) 2026-06-10 22:46:52 -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
sudacode a54f03f0cd Fix Jellyfin Login (#76) 2026-05-20 00:46:11 -07:00
sudacode 799cce6991 fix(docs): correct versioned nav links and local dev version routing (#74) 2026-05-18 01:07:17 -07:00
sudacode 6b2cb002ac [codex] add versioned Pages deployment (#73) 2026-05-17 19:54:59 -07:00
sudacode e84674e3b5 feat(macos): configuration window + curl-backed macOS updater (#71) 2026-05-17 02:23:44 -07:00
sudacode 6ca5cede3e feat(launcher): add --version / -v flag to print app version
- Exits early with `SubMiner <version>` output, no app binary required
- Maps `-v` at root level without conflicting with `stats cleanup -v`
- Adds `version: boolean` to `Args` type and default args
- Documents flag in docs-site and adds changelog fragment
2026-05-16 22:47:57 -07:00
sudacode 4d010e6a18 chore(release): prepare 0.15.0-beta.3 2026-05-16 20:58:12 -07:00
sudacode 5250ca8214 fix(controller): save remaps per profile, gate modals on enabled (#69) 2026-05-16 20:43:27 -07:00
sudacode 49f89e6452 Fix macOS overlay foreground handling and character-dictionary cache reuse (#68) 2026-05-16 20:43:01 -07:00
sudacode 89723e2ccb fix(updater): handle unsupported macOS app updates 2026-05-16 02:05:28 -07:00
sudacode d05e2bd8ec ci(prerelease): use committed release-notes, drop CI claude regen
CI runners don't have Claude Code installed, so the regen step in the
prerelease workflow died on 'claude CLI not found'. Mirror the stable
release model: generate prerelease-notes.md locally (claude -p), commit
it, and have CI just verify and reference the committed file.
2026-05-16 00:54:49 -07:00
sudacode 7484d3c102 ci(prerelease): keep Install Lua before test:fast for workflow assertion 2026-05-16 00:33:19 -07:00
sudacode f78a875ba3 ci(prerelease): install Lua before env suite 2026-05-16 00:23:39 -07:00
sudacode a025652542 chore(release): prepare v0.15.0-beta.1 2026-05-16 00:11:21 -07:00
sudacode 91a01b86a9 feat: add auto update support (#65) 2026-05-16 00:09:14 -07:00
sudacode 105713361e fix(macos): preserve overlay on transient tracker loss; fix subsync modal open (#66) 2026-05-15 20:46:00 -07:00
sudacode 4cb0dbfaad Remove vendor source-inspection tests from yomitan-settings (#67) 2026-05-15 03:06:05 -07:00
sudacode 801cdcafca fix(docs): update Plausible proxy hostname to worker.sudacode.com
- Change `PLAUSIBLE_PROXY_HOSTNAME` from `worker.subminer.moe` to `worker.sudacode.com`
- Update test assertion to match new hostname
2026-05-15 02:13:20 -07:00
sudacode 094bcce0dc feat: add auto update support 2026-05-15 01:47:56 -07:00
sudacode d1ec678d7a fix(docs): proxy Plausible tracking script 2026-05-15 01:26:26 -07:00
sudacode f0324cd93a docs: simplify Windows setup instructions and remove experimental warnin
- Remove "Windows support is experimental" warning across README and docs
- Replace `bun run subminer` references with SubMiner mpv shortcut as recommended Windows launch method
- Remove `subminer doctor` step from README quickstart
- Clarify launcher script is primarily for Linux/macOS
2026-05-13 00:30:55 -07:00
sudacode 1b2ee03678 docs: add v0.14.0 bundled plugin migration note to changelog 2026-05-13 00:11:18 -07:00
1459 changed files with 324005 additions and 27604 deletions

No files matched your search

-20
View File
@@ -1,20 +0,0 @@
{
"name": "subminer-local",
"interface": {
"displayName": "SubMiner Local"
},
"plugins": [
{
"name": "subminer-workflow",
"source": {
"source": "local",
"path": "./plugins/subminer-workflow"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
@@ -1,22 +1,45 @@
--- ---
name: 'subminer-change-verification' name: subminer-change-verification
description: 'Compatibility shim. Canonical SubMiner change verification workflow now lives in the repo-local subminer-workflow plugin.' description: Verify SubMiner changes with repo-native cheap-first test lanes. Use after code, config, launcher, plugin, runtime, stats, documentation, or workflow changes; do not use for read-only questions.
--- ---
# Compatibility Shim # SubMiner Change Verification
Canonical source: Verify the behavior claimed by a change without running unrelated expensive checks by default.
- `plugins/subminer-workflow/skills/subminer-change-verification/SKILL.md` ## Workflow
Canonical helper scripts: 1. Inspect the requested scope and changed paths with `git status --short` and `git diff`.
2. Read `docs/workflow/verification.md` as the source of truth for maintained lanes.
3. Run the cheapest lane or lanes that cover the changed behavior.
4. Escalate to the full handoff gate only for substantial or cross-boundary changes.
5. Report exact commands, results, skipped checks, blockers, and remaining risk.
- `plugins/subminer-workflow/skills/subminer-change-verification/scripts/classify_subminer_diff.sh` Do not use hidden wrapper commands. Verification commands are owned by `package.json` and the workflow documentation.
- `plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh`
When this shim is invoked: ## Lane Selection
1. Read the canonical plugin-owned skill. - Internal docs, `AGENTS.md`, or `.agents/skills/**`: `bun run test:docs:kb`
2. Follow the plugin-owned skill as the source of truth. - User-facing `docs-site/**`: `bun run docs:test`, then `bun run docs:build`
3. Use the wrapper scripts in this shim directory only for compatibility with existing commands and docs. - Config/schema/defaults: `bun run test:config`
4. Do not duplicate workflow changes here; update the plugin-owned skill and scripts instead. - If defaults or templates changed, also run `bun run generate:config-example` and `bun run verify:config-example`.
- General TypeScript source: `bun run typecheck`, then `bun run test:fast`
- Launcher or mpv plugin: `bun run test:launcher` or `bun run test:env`, based on the behavior changed
- Runtime compatibility or dist-sensitive wiring: `bun run test:runtime:compat`
- Stats dashboard: `bun run test:stats`
- Build/release scripts: `bun run test:scripts`
For substantial changes, use the full gate documented in `AGENTS.md` and `docs/workflow/verification.md`.
## Runtime Escalation
Real runtime checks are required when the claim depends on actual Electron, mpv, overlay, focus, window tracking, launch, or socket behavior. Run the relevant application flow when the environment supports it. Otherwise, report the missing runtime dependency and do not present cheaper checks as authoritative runtime validation.
## Pre-Handoff Checks
Before handoff, reconcile both questions:
1. Do behavior, defaults, flags, shortcuts, ports, APIs, architecture, or workflow changes require documentation updates?
2. Does the change require a current-outcome fragment under `changes/` according to `changes/README.md`?
Complete required updates before handoff or report the blocker.
@@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd "$SCRIPT_DIR/../../../.." && pwd)
TARGET="$REPO_ROOT/plugins/subminer-workflow/skills/subminer-change-verification/scripts/classify_subminer_diff.sh"
if [[ ! -x "$TARGET" ]]; then
echo "Missing canonical script: $TARGET" >&2
exit 1
fi
exec "$TARGET" "$@"
@@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd "$SCRIPT_DIR/../../../.." && pwd)
TARGET="$REPO_ROOT/plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh"
if [[ ! -x "$TARGET" ]]; then
echo "Missing canonical script: $TARGET" >&2
exit 1
fi
exec "$TARGET" "$@"
+34
View File
@@ -0,0 +1,34 @@
---
name: subminer-release
description: Prepare, cut, publish, or repair SubMiner stable and prerelease releases. Use for hands-on release work; do not use for general release questions.
---
# SubMiner release
Carry out the requested release phase using the repository's current release process.
## Source of truth
Read `docs/RELEASING.md` completely before changing files or release state. Treat it as canonical. Read `changes/README.md` when the work touches change fragments or generated release notes.
Do not copy release commands or policy into this skill. If this skill disagrees with the release guide, follow the guide and reconcile the skill before handoff.
## Workflow
1. Identify whether the request is for a stable release, prerelease, release preparation, publication, or repair.
2. Inspect the current branch, worktree status, package version, pending change fragments, relevant tags, and latest CI state before making changes.
3. Follow the matching procedure in `docs/RELEASING.md` in order. Review generated changelog and release-note Markdown before it can be committed or published.
4. Run every required gate for the requested release phase. Do not treat a cheaper test lane as a substitute for the documented release gate.
5. Before a stable tag, confirm the package and tag versions match and no pending `changes/*.md` fragments remain. Preserve fragments for prereleases as documented.
6. Report the resulting version, completed checks, local commit and tag state, remote publication state, skipped platform checks, and any remaining manual work.
## Authorization boundaries
- A request to prepare a release stops before commit, tag, push, or remote publication unless the user also authorizes those actions.
- A clear request to cut or publish a release includes the documented commit, tag, and push steps. Ask before the first remote mutation when the wording is ambiguous.
- Do not edit an existing GitHub release, publish to the AUR, change secrets, or alter signing configuration unless the user explicitly requests that operation.
- Do not switch branches without consent.
## Stop conditions
Stop and report the blocker when required CI or a release gate fails, authentication is missing, versions disagree, required artifacts are absent, or the worktree contains unexpected changes that overlap the release. Do not tag or publish a partially verified release.
@@ -1,18 +0,0 @@
---
name: 'subminer-scrum-master'
description: 'Compatibility shim. Canonical SubMiner scrum-master workflow now lives in the repo-local subminer-workflow plugin.'
---
# Compatibility Shim
Canonical source:
- `plugins/subminer-workflow/skills/subminer-scrum-master/SKILL.md`
When this shim is invoked:
1. Read the canonical plugin-owned skill.
2. Follow the plugin-owned skill as the source of truth.
3. Do not duplicate workflow changes here; update the plugin-owned skill instead.
This shim exists so existing repo references and prompts keep resolving during the migration to the repo-local plugin workflow.
+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 ## 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)
+1 -95
View File
@@ -8,98 +8,4 @@ on:
jobs: jobs:
build-test-audit: build-test-audit:
runs-on: ubuntu-latest uses: ./.github/workflows/quality-gate.yml
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Lint changelog fragments
run: bun run changelog:lint
- name: Lint stats (formatting)
run: bun run lint:stats
- name: Enforce pull request changelog fragments (`skip-changelog` label bypass)
if: github.event_name == 'pull_request'
run: bun run changelog:pr-check --base-ref "origin/${{ github.base_ref }}" --head-ref "HEAD" --labels "${{ join(github.event.pull_request.labels.*.name, ',') }}"
- name: Build (TypeScript check)
# Keep explicit typecheck for fast fail before full build/bundle.
run: bun run typecheck
- name: Verify generated config examples
run: bun run verify:config-example
- name: Internal docs knowledge-base checks
run: bun run test:docs:kb
- name: Test suite (source)
run: bun run test:fast
- name: Coverage suite (maintained source lane)
run: bun run test:coverage:src
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-test-src
path: coverage/test-src/lcov.info
if-no-files-found: error
- name: Launcher smoke suite (source)
run: bun run test:launcher:smoke:src
- name: Upload launcher smoke artifacts (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: launcher-smoke
path: .tmp/launcher-smoke/**
if-no-files-found: ignore
- name: Build (bundle)
run: bun run build
- name: Immersion SQLite verification
run: bun run test:immersion:sqlite:dist
- name: Dist smoke suite
run: bun run test:smoke:dist
- name: Security audit
run: bun audit --audit-level high
continue-on-error: true
- name: Build Bun subminer wrapper
run: make build-launcher
- name: Verify Bun subminer wrapper
run: dist/launcher/subminer --help >/dev/null
- name: Enforce generated launcher workflow
run: bash scripts/verify-generated-launcher.sh
+88
View File
@@ -0,0 +1,88 @@
name: Docs Pages
on:
workflow_dispatch:
inputs:
rebuild_archives:
description: 'Stable tags to rebuild in R2 (comma-separated, or "all"); archives are otherwise built once'
required: false
default: ''
push:
branches:
- main
tags:
- 'v*'
paths:
- 'docs-site/**'
- 'scripts/docs-versioning.ts'
- 'scripts/docs-versioned-assets.ts'
- 'scripts/docs-archive-store.ts'
- 'scripts/build-versioned-docs.ts'
- '.github/workflows/docs-pages.yml'
- 'package.json'
- 'bun.lock'
concurrency:
group: docs-pages-production
cancel-in-progress: false
jobs:
deploy:
if: ${{ github.ref_type != 'tag' || !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Guard stable docs tag shape
id: tag_guard
if: github.ref_type == 'tag'
env:
TAG_NAME: ${{ github.ref_name }}
run: |
if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::notice::Skipping non-stable docs tag $TAG_NAME"
echo "stable_tag=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "stable_tag=true" >> "$GITHUB_OUTPUT"
- name: Setup Bun
if: steps.tag_guard.outputs.stable_tag != 'false'
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Install dependencies
if: steps.tag_guard.outputs.stable_tag != 'false'
run: |
bun install --frozen-lockfile
cd docs-site && bun install --frozen-lockfile
- name: Test docs
if: steps.tag_guard.outputs.stable_tag != 'false'
run: bun run docs:test
# Builds only archives missing from R2 (plus any requested rebuilds), then the
# root and /main/ builds that make up the Pages deployment.
- name: Build versioned docs
if: steps.tag_guard.outputs.stable_tag != 'false'
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
DOCS_ARCHIVE_R2_ACCESS_KEY_ID: ${{ secrets.DOCS_ARCHIVE_R2_ACCESS_KEY_ID }}
DOCS_ARCHIVE_R2_SECRET_ACCESS_KEY: ${{ secrets.DOCS_ARCHIVE_R2_SECRET_ACCESS_KEY }}
DOCS_ARCHIVE_R2_BUCKET: ${{ vars.DOCS_ARCHIVE_R2_BUCKET }}
REBUILD_ARCHIVES: ${{ inputs.rebuild_archives }}
run: bun run scripts/build-versioned-docs.ts --require-archives "--rebuild-archives=${REBUILD_ARCHIVES}"
- name: Deploy docs to Cloudflare Pages
if: steps.tag_guard.outputs.stable_tag != 'false'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# Run from docs-site so Wrangler bundles docs-site/functions (the /v/* archive server).
workingDirectory: docs-site
command: pages deploy ../.tmp/docs-versioned-site --project-name "${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME }}" --branch main
+238
View File
@@ -0,0 +1,238 @@
name: Package release
on:
workflow_call:
secrets:
CSC_LINK:
required: true
CSC_KEY_PASSWORD:
required: true
APPLE_ID:
required: true
APPLE_APP_SPECIFIC_PASSWORD:
required: true
APPLE_TEAM_ID:
required: true
# Project TMDB key baked into release artifacts; builds stay valid without it.
SUBMINER_TMDB_API_KEY:
required: false
permissions:
contents: read
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
run: |
cd vendor/texthooker-ui
bun install --frozen-lockfile
bun run build
- name: Build AppImage
run: bun run build:appimage
env:
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
- name: Build unversioned AppImage
run: |
shopt -s nullglob
appimages=(release/SubMiner-*.AppImage)
if [ "${#appimages[@]}" -eq 0 ]; then
echo "No versioned AppImage found to create unversioned artifact."
ls -la release
exit 1
fi
cp "${appimages[0]}" release/SubMiner.AppImage
- name: Smoke packaged runtime assets
shell: bash
run: xvfb-run -a bun run test:package "release/linux-unpacked/resources"
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: appimage
path: |
release/*.AppImage
release/latest*.yml
release/*.blockmap
if-no-files-found: error
build-macos:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Validate macOS signing/notarization secrets
run: |
missing=0
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
if [ -z "${!name}" ]; then
echo "Missing required secret: $name"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
echo "Set all required macOS signing/notarization secrets and rerun."
exit 1
fi
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
run: |
cd vendor/texthooker-ui
bun install --frozen-lockfile
bun run build
- name: Build signed + notarized macOS artifacts
run: bun run build:mac
env:
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Smoke packaged runtime assets
shell: bash
run: bun run test:package "release/mac-arm64/SubMiner.app/Contents/Resources"
- name: Upload macOS artifacts
uses: actions/upload-artifact@v4
with:
name: macos
path: |
release/*.dmg
release/*.zip
release/latest*.yml
release/*.blockmap
if-no-files-found: error
build-windows:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
shell: powershell
run: |
Set-Location vendor/texthooker-ui
bun install --frozen-lockfile
bun run build
- name: Verify managed Windows launcher
run: bun test src/main/runtime/managed-launcher.test.ts
- name: Verify Windows launcher bootstrap
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts
- name: Build unsigned Windows artifacts
run: bun run build:win:unsigned
env:
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
- name: Smoke packaged runtime assets
shell: bash
run: bun run test:package "release/win-unpacked/resources"
- name: Upload Windows artifacts
uses: actions/upload-artifact@v4
with:
name: windows
path: |
release/*.exe
release/*.zip
release/latest*.yml
release/*.blockmap
if-no-files-found: error
+45 -265
View File
@@ -12,265 +12,25 @@ concurrency:
jobs: jobs:
quality-gate: quality-gate:
runs-on: ubuntu-latest permissions:
steps: contents: read
- name: Checkout uses: ./.github/workflows/quality-gate.yml
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun package:
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Lint stats (formatting)
run: bun run lint:stats
- name: Build (TypeScript check)
run: bun run typecheck
- name: Test suite (source)
run: bun run test:fast
- name: Environment suite
run: bun run test:env
- name: Coverage suite (maintained source lane)
run: bun run test:coverage:src
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-test-src
path: coverage/test-src/lcov.info
if-no-files-found: error
- name: Launcher smoke suite (source)
run: bun run test:launcher:smoke:src
- name: Upload launcher smoke artifacts (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: launcher-smoke
path: .tmp/launcher-smoke/**
if-no-files-found: ignore
- name: Build (bundle)
run: bun run build
- name: Immersion SQLite verification
run: bun run test:immersion:sqlite:dist
- name: Dist smoke suite
run: bun run test:smoke:dist
build-linux:
needs: [quality-gate] needs: [quality-gate]
runs-on: ubuntu-latest permissions:
steps: contents: read
- name: Checkout uses: ./.github/workflows/package-release.yml
uses: actions/checkout@v4 secrets:
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
run: |
cd vendor/texthooker-ui
bun install
bun run build
- name: Build AppImage
run: bun run build:appimage
- name: Build unversioned AppImage
run: |
shopt -s nullglob
appimages=(release/SubMiner-*.AppImage)
if [ "${#appimages[@]}" -eq 0 ]; then
echo "No versioned AppImage found to create unversioned artifact."
ls -la release
exit 1
fi
cp "${appimages[0]}" release/SubMiner.AppImage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: appimage
path: release/*.AppImage
if-no-files-found: error
build-macos:
needs: [quality-gate]
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Validate macOS signing/notarization secrets
run: |
missing=0
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
if [ -z "${!name}" ]; then
echo "Missing required secret: $name"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
echo "Set all required macOS signing/notarization secrets and rerun."
exit 1
fi
env:
CSC_LINK: ${{ secrets.CSC_LINK }} CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
run: |
cd vendor/texthooker-ui
bun install
bun run build
- name: Build signed + notarized macOS artifacts
run: bun run build:mac
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Upload macOS artifacts
uses: actions/upload-artifact@v4
with:
name: macos
path: |
release/*.dmg
release/*.zip
if-no-files-found: error
build-windows:
needs: [quality-gate]
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
shell: powershell
run: |
Set-Location vendor/texthooker-ui
bun install
bun run build
- name: Build unsigned Windows artifacts
run: bun run build:win:unsigned
- name: Upload Windows artifacts
uses: actions/upload-artifact@v4
with:
name: windows
path: |
release/*.exe
release/*.zip
if-no-files-found: error
release: release:
needs: [build-linux, build-macos, build-windows] needs: [package]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -316,11 +76,11 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: bun install --frozen-lockfile
- name: Build Bun subminer wrapper - name: Build launcher runtime artifacts
run: make build-launcher run: make build-launcher
- name: Verify Bun subminer wrapper - name: Smoke launcher bundle
run: dist/launcher/subminer --help >/dev/null run: bun dist/launcher/subminer.js --help >/dev/null
- name: Enforce generated launcher workflow - name: Enforce generated launcher workflow
run: bash scripts/verify-generated-launcher.sh run: bash scripts/verify-generated-launcher.sh
@@ -334,12 +94,17 @@ jobs:
config.example.jsonc \ config.example.jsonc \
plugin/subminer \ plugin/subminer \
plugin/subminer.conf \ plugin/subminer.conf \
assets/themes/subminer.rasi assets/themes/subminer.rasi \
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer \
resources/bun/licenses
- name: Package Bun corresponding source
run: bun scripts/package-bun-source.mjs
- name: Generate checksums - name: Generate checksums
run: | run: |
shopt -s nullglob shopt -s nullglob
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz dist/launcher/subminer) files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
if [ "${#files[@]}" -eq 0 ]; then if [ "${#files[@]}" -eq 0 ]; then
echo "No release artifacts found for checksum generation." echo "No release artifacts found for checksum generation."
exit 1 exit 1
@@ -355,12 +120,23 @@ jobs:
id: version id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Generate prerelease notes from pending fragments - name: Verify committed prerelease notes
run: bun run changelog:prerelease-notes --version "${{ steps.version.outputs.VERSION }}" env:
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: |
if [ ! -s release/prerelease-notes.md ]; then
echo "::error::release/prerelease-notes.md is missing or empty. Run 'bun run changelog:prerelease-notes --version <version>' locally and commit the file before tagging."
exit 1
fi
if ! bun run changelog:check-prerelease-notes --version "$RELEASE_VERSION"; then
echo "::error::release/prerelease-notes.md was not generated for $RELEASE_VERSION. Rerun 'bun run changelog:prerelease-notes --version $RELEASE_VERSION' locally, commit, and retag."
exit 1
fi
- name: Publish Prerelease - name: Publish Prerelease
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: | run: |
set -euo pipefail set -euo pipefail
@@ -371,8 +147,12 @@ jobs:
release/*.exe release/*.exe
release/*.zip release/*.zip
release/*.tar.gz release/*.tar.gz
release/*.tar.gz.sha256
release/latest*.yml
release/*.blockmap
release/SHA256SUMS.txt release/SHA256SUMS.txt
dist/launcher/subminer dist/launcher/subminer
dist/launcher/subminer.cmd
) )
if [ "${#artifacts[@]}" -eq 0 ]; then if [ "${#artifacts[@]}" -eq 0 ]; then
@@ -380,27 +160,27 @@ jobs:
exit 1 exit 1
fi fi
if gh release view "${{ steps.version.outputs.VERSION }}" >/dev/null 2>&1; then if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then
gh release edit "${{ steps.version.outputs.VERSION }}" \ gh release edit "$RELEASE_VERSION" \
--draft \ --draft \
--prerelease \ --prerelease \
--title "${{ steps.version.outputs.VERSION }}" \ --title "$RELEASE_VERSION" \
--notes-file release/prerelease-notes.md --notes-file release/prerelease-notes.md
else else
gh release create "${{ steps.version.outputs.VERSION }}" \ gh release create "$RELEASE_VERSION" \
--draft \ --draft \
--latest=false \ --latest=false \
--prerelease \ --prerelease \
--title "${{ steps.version.outputs.VERSION }}" \ --title "$RELEASE_VERSION" \
--notes-file release/prerelease-notes.md --notes-file release/prerelease-notes.md
fi fi
for asset in "${artifacts[@]}"; do for asset in "${artifacts[@]}"; do
gh release upload "${{ steps.version.outputs.VERSION }}" "$asset" --clobber gh release upload "$RELEASE_VERSION" "$asset" --clobber
done done
gh release edit "${{ steps.version.outputs.VERSION }}" \ gh release edit "$RELEASE_VERSION" \
--draft=false \ --draft=false \
--prerelease \ --prerelease \
--title "${{ steps.version.outputs.VERSION }}" \ --title "$RELEASE_VERSION" \
--notes-file release/prerelease-notes.md --notes-file release/prerelease-notes.md
+145
View File
@@ -0,0 +1,145 @@
name: Quality Gate
on:
workflow_call:
permissions:
contents: read
jobs:
launcher-runtime:
strategy:
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Verify native runtime staging
run: bun test src/main/runtime/managed-launcher.test.ts
- name: Verify Windows launcher bootstrap
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts
- name: Verify native mpv process launch
run: bun test src/main/runtime/mpv-process.test.ts
- name: Verify POSIX launcher bootstrap
run: bun test src/main/runtime/posix-launcher-bootstrap.test.ts
quality-gate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Lint changelog fragments
run: bun run changelog:lint
- name: Lint stats (formatting)
run: bun run lint:stats
- name: Enforce pull request changelog fragments (`skip-changelog` label bypass)
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.base_ref }}
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
run: bun run changelog:pr-check --base-ref "origin/$BASE_REF" --head-ref "HEAD" --labels "$PR_LABELS"
- name: Build (TypeScript check)
run: bun run typecheck
- name: Verify generated config examples
run: bun run verify:config-example
- name: Install Lua
run: |
# Lua needs only Ubuntu sources; unrelated runner repositories can be unavailable.
test -f /etc/apt/sources.list.d/ubuntu.sources
apt_sources=(-o Dir::Etc::sourcelist=sources.list.d/ubuntu.sources -o Dir::Etc::sourceparts=-)
sudo apt-get "${apt_sources[@]}" update
sudo apt-get "${apt_sources[@]}" install -y lua5.4
sudo ln -sf /usr/bin/lua5.4 /usr/local/bin/lua
lua -v
- name: Launcher unit and script suites
run: bun run test:launcher:unit:src && bun run test:scripts
- name: Environment suite
run: bun run test:env
- name: Upload launcher smoke artifacts (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: launcher-smoke
path: .tmp/launcher-smoke/**
if-no-files-found: ignore
- name: Coverage suite (maintained source lane)
run: bun run test:coverage:src
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-test-src
path: coverage/test-src/lcov.info
if-no-files-found: error
- name: Stats UI tests
run: bun run test:stats
- name: Build (bundle)
run: bun run build
- name: Immersion SQLite verification
run: bun run test:immersion:sqlite:dist
- name: Dist smoke suite
run: bun run test:smoke:dist
- name: Security audit
run: bun audit --audit-level high
- name: Build launcher runtime artifacts
run: make build-launcher
- name: Smoke launcher bundle
run: bun dist/launcher/subminer.js --help >/dev/null
- name: Enforce generated launcher workflow
run: bash scripts/verify-generated-launcher.sh
+65 -271
View File
@@ -13,260 +13,25 @@ concurrency:
jobs: jobs:
quality-gate: quality-gate:
runs-on: ubuntu-latest permissions:
steps: contents: read
- name: Checkout uses: ./.github/workflows/quality-gate.yml
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun package:
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Lint stats (formatting)
run: bun run lint:stats
- name: Build (TypeScript check)
run: bun run typecheck
- name: Test suite (source)
run: bun run test:fast
- name: Coverage suite (maintained source lane)
run: bun run test:coverage:src
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-test-src
path: coverage/test-src/lcov.info
if-no-files-found: error
- name: Launcher smoke suite (source)
run: bun run test:launcher:smoke:src
- name: Upload launcher smoke artifacts (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: launcher-smoke
path: .tmp/launcher-smoke/**
if-no-files-found: ignore
- name: Build (bundle)
run: bun run build
- name: Immersion SQLite verification
run: bun run test:immersion:sqlite:dist
- name: Dist smoke suite
run: bun run test:smoke:dist
build-linux:
needs: [quality-gate] needs: [quality-gate]
runs-on: ubuntu-latest permissions:
steps: contents: read
- name: Checkout uses: ./.github/workflows/package-release.yml
uses: actions/checkout@v4 secrets:
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
run: |
cd vendor/texthooker-ui
bun install
bun run build
- name: Build AppImage
run: bun run build:appimage
- name: Build unversioned AppImage
run: |
shopt -s nullglob
appimages=(release/SubMiner-*.AppImage)
if [ "${#appimages[@]}" -eq 0 ]; then
echo "No versioned AppImage found to create unversioned artifact."
ls -la release
exit 1
fi
cp "${appimages[0]}" release/SubMiner.AppImage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: appimage
path: release/*.AppImage
build-macos:
needs: [quality-gate]
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Validate macOS signing/notarization secrets
run: |
missing=0
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
if [ -z "${!name}" ]; then
echo "Missing required secret: $name"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
echo "Set all required macOS signing/notarization secrets and rerun."
exit 1
fi
env:
CSC_LINK: ${{ secrets.CSC_LINK }} CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
run: |
cd vendor/texthooker-ui
bun install
bun run build
- name: Build signed + notarized macOS artifacts
run: bun run build:mac
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Upload macOS artifacts
uses: actions/upload-artifact@v4
with:
name: macos
path: |
release/*.dmg
release/*.zip
build-windows:
needs: [quality-gate]
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
shell: powershell
run: |
Set-Location vendor/texthooker-ui
bun install
bun run build
- name: Build unsigned Windows artifacts
run: bun run build:win:unsigned
- name: Upload Windows artifacts
uses: actions/upload-artifact@v4
with:
name: windows
path: |
release/*.exe
release/*.zip
if-no-files-found: error
release: release:
needs: [build-linux, build-macos, build-windows] needs: [package]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -312,11 +77,11 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: bun install --frozen-lockfile
- name: Build Bun subminer wrapper - name: Build launcher runtime artifacts
run: make build-launcher run: make build-launcher
- name: Verify Bun subminer wrapper - name: Smoke launcher bundle
run: dist/launcher/subminer --help >/dev/null run: bun dist/launcher/subminer.js --help >/dev/null
- name: Enforce generated launcher workflow - name: Enforce generated launcher workflow
run: bash scripts/verify-generated-launcher.sh run: bash scripts/verify-generated-launcher.sh
@@ -330,12 +95,17 @@ jobs:
config.example.jsonc \ config.example.jsonc \
plugin/subminer \ plugin/subminer \
plugin/subminer.conf \ plugin/subminer.conf \
assets/themes/subminer.rasi assets/themes/subminer.rasi \
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer \
resources/bun/licenses
- name: Package Bun corresponding source
run: bun scripts/package-bun-source.mjs
- name: Generate checksums - name: Generate checksums
run: | run: |
shopt -s nullglob shopt -s nullglob
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz dist/launcher/subminer) files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
if [ "${#files[@]}" -eq 0 ]; then if [ "${#files[@]}" -eq 0 ]; then
echo "No release artifacts found for checksum generation." echo "No release artifacts found for checksum generation."
exit 1 exit 1
@@ -352,33 +122,40 @@ jobs:
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Guard against pending changelog fragments - name: Guard against pending changelog fragments
env:
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: | run: |
if find changes -maxdepth 1 -name '*.md' -not -name README.md -print -quit | grep -q .; then if find changes -maxdepth 1 -name '*.md' -not -name README.md -print -quit | grep -q .; then
echo "::error::Pending changelog fragments detected. Run 'bun run changelog:build --version ${{ steps.version.outputs.VERSION }}' locally and commit the polished CHANGELOG.md before tagging. CI no longer auto-builds the changelog because the polish step requires the local 'claude' CLI." echo "::error::Pending changelog fragments detected. Run 'bun run changelog:build --version $RELEASE_VERSION' locally and commit the polished CHANGELOG.md before tagging. CI no longer auto-builds the changelog because the polish step requires the local 'claude' CLI."
exit 1 exit 1
fi fi
- name: Verify changelog is ready for tagged release - name: Verify changelog is ready for tagged release
run: bun run changelog:check --version "${{ steps.version.outputs.VERSION }}" env:
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: bun run changelog:check --version "$RELEASE_VERSION"
- name: Generate release notes from changelog - name: Generate release notes from changelog
run: bun run changelog:release-notes --version "${{ steps.version.outputs.VERSION }}" env:
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: bun run changelog:release-notes --version "$RELEASE_VERSION"
- name: Publish Release - name: Publish Release
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: | run: |
set -euo pipefail set -euo pipefail
if gh release view "${{ steps.version.outputs.VERSION }}" >/dev/null 2>&1; then if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then
# Do not pass the prerelease flag here; gh defaults to a normal release. # Do not pass the prerelease flag here; gh defaults to a normal release.
gh release edit "${{ steps.version.outputs.VERSION }}" \ gh release edit "$RELEASE_VERSION" \
--draft=false \ --draft=false \
--title "${{ steps.version.outputs.VERSION }}" \ --title "$RELEASE_VERSION" \
--notes-file release/release-notes.md --notes-file release/release-notes.md
else else
gh release create "${{ steps.version.outputs.VERSION }}" \ gh release create "$RELEASE_VERSION" \
--title "${{ steps.version.outputs.VERSION }}" \ --title "$RELEASE_VERSION" \
--notes-file release/release-notes.md --notes-file release/release-notes.md
fi fi
@@ -389,8 +166,12 @@ jobs:
release/*.exe release/*.exe
release/*.zip release/*.zip
release/*.tar.gz release/*.tar.gz
release/*.tar.gz.sha256
release/latest*.yml
release/*.blockmap
release/SHA256SUMS.txt release/SHA256SUMS.txt
dist/launcher/subminer dist/launcher/subminer
dist/launcher/subminer.cmd
) )
if [ "${#artifacts[@]}" -eq 0 ]; then if [ "${#artifacts[@]}" -eq 0 ]; then
@@ -399,7 +180,7 @@ jobs:
fi fi
for asset in "${artifacts[@]}"; do for asset in "${artifacts[@]}"; do
gh release upload "${{ steps.version.outputs.VERSION }}" "$asset" --clobber gh release upload "$RELEASE_VERSION" "$asset" --clobber
done done
aur-publish: aur-publish:
@@ -472,39 +253,52 @@ jobs:
echo "skip=true" >> "$GITHUB_OUTPUT" echo "skip=true" >> "$GITHUB_OUTPUT"
- name: Download release assets for AUR - name: Download release assets for AUR
id: aur_assets
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: | run: |
set -euo pipefail set -euo pipefail
version="${{ steps.version.outputs.VERSION }}" version="$RELEASE_VERSION"
install -dm755 .tmp/aur-release-assets install -dm755 .tmp/aur-release-assets
gh release download "$version" \ for asset in "SubMiner-${version#v}.AppImage" subminer subminer-assets.tar.gz; do
--dir .tmp/aur-release-assets \ destination=".tmp/aur-release-assets/$asset"
--pattern "SubMiner-${version#v}.AppImage" \ if ! curl --fail --silent --show-error --location \
--pattern "subminer" \ --retry 3 --retry-delay 1 --retry-all-errors \
--pattern "subminer-assets.tar.gz" --connect-timeout 30 --max-time 600 \
--output "$destination.partial" \
"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$version/$asset"; then
echo "::warning::Unable to download $asset after retries; skipping automated AUR publish."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
mv "$destination.partial" "$destination"
done
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Update AUR packaging metadata - name: Update AUR packaging metadata
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true'
env:
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: | run: |
set -euo pipefail set -euo pipefail
version_no_v="${{ steps.version.outputs.VERSION }}" version_no_v="$RELEASE_VERSION"
version_no_v="${version_no_v#v}" version_no_v="${version_no_v#v}"
cp packaging/aur/subminer-bin/PKGBUILD aur-subminer-bin/PKGBUILD cp packaging/aur/subminer-bin/PKGBUILD aur-subminer-bin/PKGBUILD
cp packaging/aur/subminer-bin/.SRCINFO aur-subminer-bin/.SRCINFO cp packaging/aur/subminer-bin/.SRCINFO aur-subminer-bin/.SRCINFO
bash scripts/update-aur-package.sh \ bash scripts/update-aur-package.sh \
--pkg-dir aur-subminer-bin \ --pkg-dir aur-subminer-bin \
--version "${{ steps.version.outputs.VERSION }}" \ --version "$RELEASE_VERSION" \
--appimage ".tmp/aur-release-assets/SubMiner-${version_no_v}.AppImage" \ --appimage ".tmp/aur-release-assets/SubMiner-${version_no_v}.AppImage" \
--wrapper ".tmp/aur-release-assets/subminer" \ --wrapper ".tmp/aur-release-assets/subminer" \
--assets ".tmp/aur-release-assets/subminer-assets.tar.gz" --assets ".tmp/aur-release-assets/subminer-assets.tar.gz"
- name: Commit and push AUR update - name: Commit and push AUR update
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true'
working-directory: aur-subminer-bin working-directory: aur-subminer-bin
env: env:
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: | run: |
set -euo pipefail set -euo pipefail
if git diff --quiet -- PKGBUILD .SRCINFO; then if git diff --quiet -- PKGBUILD .SRCINFO; then
@@ -514,7 +308,7 @@ jobs:
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add PKGBUILD .SRCINFO git add PKGBUILD .SRCINFO
git commit -m "Update to ${{ steps.version.outputs.VERSION }}" git commit -m "Update to $RELEASE_VERSION"
attempts=3 attempts=3
for attempt in $(seq 1 "$attempts"); do for attempt in $(seq 1 "$attempts"); do
+10
View File
@@ -1,5 +1,6 @@
# Dependencies # Dependencies
node_modules/ node_modules/
/package-lock.json
# Superpowers brainstorming # Superpowers brainstorming
.superpowers/ .superpowers/
@@ -10,11 +11,14 @@ dist/
release/* release/*
!release/ !release/
!release/release-notes.md !release/release-notes.md
!release/prerelease-notes.md
build/yomitan/ build/yomitan/
coverage/ coverage/
# Launcher build artifact (produced by make build-launcher) # Launcher build artifact (produced by make build-launcher)
/subminer /subminer
/main-entry.js
/main-entry.js.map
# Logs # Logs
*.log *.log
@@ -45,6 +49,7 @@ tests/*
!.agents/skills/ !.agents/skills/
.agents/skills/* .agents/skills/*
!.agents/skills/subminer-change-verification/ !.agents/skills/subminer-change-verification/
!.agents/skills/subminer-release/
!.agents/skills/subminer-scrum-master/ !.agents/skills/subminer-scrum-master/
.agents/skills/subminer-change-verification/* .agents/skills/subminer-change-verification/*
!.agents/skills/subminer-change-verification/SKILL.md !.agents/skills/subminer-change-verification/SKILL.md
@@ -52,8 +57,13 @@ tests/*
.agents/skills/subminer-change-verification/scripts/* .agents/skills/subminer-change-verification/scripts/*
!.agents/skills/subminer-change-verification/scripts/classify_subminer_diff.sh !.agents/skills/subminer-change-verification/scripts/classify_subminer_diff.sh
!.agents/skills/subminer-change-verification/scripts/verify_subminer_change.sh !.agents/skills/subminer-change-verification/scripts/verify_subminer_change.sh
.agents/skills/subminer-release/*
!.agents/skills/subminer-release/SKILL.md
.agents/skills/subminer-scrum-master/* .agents/skills/subminer-scrum-master/*
!.agents/skills/subminer-scrum-master/SKILL.md !.agents/skills/subminer-scrum-master/SKILL.md
favicon.png favicon.png
.claude/* .claude/*
!stats/public/favicon.png !stats/public/favicon.png
# Browser-automation session artifacts (page snapshots, console logs, downloads)
.playwright-mcp/
+1
View File
@@ -4,3 +4,4 @@ release
coverage coverage
vendor vendor
*.log *.log
src/core/services/tokenizer/__fixtures__/golden/*.json
+30 -13
View File
@@ -1,4 +1,4 @@
# AGENTS.MD # AGENTS.md
## Internal Docs ## Internal Docs
@@ -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. `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 ## Quick Start
- Init workspace: `git submodule update --init --recursive` - Init workspace: `git submodule update --init --recursive`
@@ -23,8 +25,9 @@ Start here, then leave this file.
## Build / Test ## Build / Test
- Runtime/package manager: Bun (`packageManager: bun@1.3.5`) - Runtime/package manager: Bun; use the version pinned by `package.json`.
- Default handoff gate: - Follow [`docs/workflow/verification.md`](./docs/workflow/verification.md) and start with the cheapest sufficient lane.
- Full handoff gate for substantial changes:
`bun run typecheck` `bun run typecheck`
`bun run test:fast` `bun run test:fast`
`bun run test:env` `bun run test:env`
@@ -40,7 +43,26 @@ Start here, then leave this file.
- Config/schema/defaults: `bun run test:config`; if template/defaults changed, `bun run generate:config-example` - Config/schema/defaults: `bun run test:config`; if template/defaults changed, `bun run generate:config-example`
- Launcher/plugin: `bun run test:launcher` or `bun run test:env` - Launcher/plugin: `bun run test:launcher` or `bun run test:env`
- Runtime-compat / dist-sensitive: `bun run test:runtime:compat` - Runtime-compat / dist-sensitive: `bun run test:runtime:compat`
- Docs-only: `bun run docs:test`, then `bun run docs:build` - Stats dashboard UI (`stats/`): `bun run test:stats`
- Build/release scripts (`scripts/**`): `bun run test:scripts`
- Internal docs, `AGENTS.md`, or repo skills: `bun run test:docs:kb`
- User-facing `docs-site/`: `bun run docs:test`, then `bun run docs:build`
- macOS mpv window helper: `bun test scripts/get-mpv-window-macos.test.ts`
- Test lanes are directory-discovered via `scripts/test-lanes.ts`; never hand-list test files in `package.json`
## 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 ## Sensitive Files
@@ -52,15 +74,10 @@ Start here, then leave this file.
## Release / PR Notes ## 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 live 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` - CI enforces `bun run changelog:lint` and `bun run changelog:pr-check`
- PR review helpers: - PR review helpers:
- `gh pr view --json number,title,url --jq '"PR #\\(.number): \\(.title)\\n\\(.url)"'` - `gh pr view --json number,title --jq '"PR #\\(.number): \\(.title)"'`
- `gh api repos/:owner/:repo/pulls/<num>/comments --paginate` - `gh api repos/:owner/:repo/pulls/<num>/comments --paginate`
- For CI debugging, inspect runs with `gh run list/view`; rerun or fix only within the requested scope.
## Runtime Notes
- 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
+657 -4
View File
@@ -1,10 +1,663 @@
# Changelog # Changelog
## v0.20.1 (2026-10-03)
### Added
- **YouTube Browser Window**:
- Browse YouTube with your login kept across restarts. Open it with `subminer youtube`, `subminer yt`, or tray **Browse YouTube**.
- Clicking a video plays it in mpv. Middle-click, Shift/Ctrl-click, or the right-click menu adds it to mpv's playlist.
- Queued videos get the same Japanese subtitle setup, and the rest of the queue is kept on Windows.
- Closing the window quits a SubMiner started by `subminer youtube`.
- **Whisper Subtitles for YouTube**:
- Set `youtube.subtitleSource` to `whisper` to transcribe YouTube videos with Whisper instead of using YouTube's captions.
- The video stays paused while Whisper transcribes, and the subtitle generation modal shows progress.
- The subtitle generation modal (`Ctrl+Shift+G`) now works on YouTube videos, including Windows stream playback.
- **Scroll Wheel and Mouse Bindings**: `keybindings` now accepts scroll wheel keys, and the settings key editor can capture them. Mouse button and wheel bindings from mpv now work while the cursor is over the overlay, including on Hyprland.
- **Run Commands from Session Help**: During playback, press `Enter` or double-click a row in session help to run that command.
### Changed
- **Release Assets**: Releases no longer include package-size reports.
### Fixed
- **Hyprland Overlay**: The overlay stays clickable after mpv goes fullscreen with `mpv.backend: x11`, and it no longer interrupts mpv while mpv enters fullscreen.
- **mpv Key Bindings**: Bindings from mpv such as `9`/`0` for volume now work while the overlay has focus, even if the overlay loaded before mpv connected.
- **Jellyfin**:
- The Japanese subtitle track is selected as soon as it downloads, without waiting for the other tracks.
- Japanese and English tracks are auto-selected on episodes whose embedded subtitles are images.
- Timing review previews and mined card audio use the audio track you are playing.
- **Media Timing Review**: Trim handles are now thin lines on the clip edges, and original subtitle boundaries no longer cover the waveform. The preview cursor no longer stutters on macOS with Bluetooth headphones.
- **YouTube Subtitles**: Japanese subtitles no longer fail with "HTTP 429" on videos where YouTube also lists a machine-translated Japanese track.
- **Stats**:
- Mined cards are counted once in session and lifetime stats.
- Word and kanji dates and the new-words charts stay accurate after deleting sessions. Existing data is repaired in the background.
- Overview totals, charts, and the activity calendar refresh after deleting sessions.
- **Yomitan Data Safety**: Electron is updated to 43.7.7. SubMiner now refuses to load your Yomitan data with an unsupported or downgraded runtime. Automatic character dictionary changes stop if Yomitan suddenly reports no dictionaries.
- **AppImage in Sandboxes**: The AppImage now opens its window when launched in a sandbox such as `firejail --appimage`.
- **Ctrl+C Crash**: SubMiner no longer crashes with "write EPIPE" when the terminal command that started it is stopped with Ctrl+C.
### Docs
- **Docs Site**: The docs site is shorter and easier to scan, with a key and default table for each config block. Outdated pages were corrected, and the MeCab install command for Arch Linux is fixed.
- **Stats Docs**: The docs now cover the Overview refresh after deleting sessions and how discovery dates are kept after old sessions are pruned.
<details>
<summary>Internal changes</summary>
### Internal
- **Release Notes**: Release notes and `CHANGELOG.md` are now generated as short items per feature or area.
- **Dev Profile**: Development launches use a separate `SubMiner-dev` profile unless production-profile access is requested.
- **Dependencies**: undici is updated to 7.29.1 to clear the security advisories that were blocking CI.
- **Package Checks**: CI no longer compares package sizes. Package content checks still run.
</details>
## v0.20.0 (2026-09-23)
### Added
- **Japanese Subtitle Generation**:
- Generate Japanese SRT subtitles locally with whisper.cpp. Start it from a new modal (Ctrl+Shift+G), from the generate button in an empty subtitle sidebar, or with `subminer generate-subs`.
- Generation shows progress, can be cancelled, and loads the finished subtitles into mpv automatically.
- Pick an official multilingual Whisper model, including quantized variants, with size and accuracy guidance. You can download it in the app or point Settings at a model you already have.
- `large-v3-turbo` is recommended when CUDA support is detected, and `small` otherwise.
- whisper-cli, ffmpeg, and ffprobe are found on PATH unless you override them. SubMiner names any missing tools before a download starts.
- An optional "Focus on spoken dialogue" mode uses a separately downloaded Silero VAD model. It keeps audible sections it is unsure about, so dialogue under music is not dropped, but songs may also be transcribed.
- Long passages are split near speech starts or quiet pauses to reduce subtitles that appear too early. When an eligible embedded or external subtitle track is loaded in mpv, it guides the split points.
- Each passage runs in a fresh Whisper process, which prevents repeated-character output.
- **Subtitle Selection Modal**:
- An optional modal for choosing primary and secondary mpv subtitle tracks.
- Turn it on in Settings under Behavior, then press g followed by s to open it. Turning it off restores mpv's own subtitle selection binding.
- Single-key actions take priority over configured key sequence prefixes.
- Conflicting sequences are disabled with a warning, and the existing y commands stay reserved.
- **Subtitle Sidebar Copy**:
- Select dialogue across several sidebar rows and copy it without timestamps using Ctrl/Cmd+C or the Copy button.
- Selecting text does not seek playback and does not require mining a card.
- **Media Timing Review Screenshot Picker**:
- Choose the still screenshot separately from the audio range, with a live preview and its own time slider.
- Step through decoded frames one at a time to get the exact frame you want.
- Works with local video and with seekable remote streams such as Jellyfin.
- **mpv Keybindings in the Overlay**:
- The overlay now picks up keyboard bindings from mpv defaults, `input.conf`, and loaded scripts when they do not conflict with SubMiner.
- SubMiner controls and bindings you explicitly disabled take precedence.
- These bindings apply only to the current session and are not listed in the help menu.
- **Jimaku Live Action Search**: The Jimaku modal has new Anime and Live action tabs, so you can search Jimaku's live action catalogue as well as anime. Use Arrow Left and Arrow Right to switch tabs.
- **TMDB Live-Action Library**:
- Live-action dramas and movies in the stats Library now get posters, synopses, and titles from TMDB.
- Release builds include a project key. Setting `tmdb.apiKey` or `tmdb.apiKeyCommand` overrides it, and one of them is required when running from source.
- Titles that AniList cannot match are looked up on TMDB automatically when the parsed filename exactly matches a Japanese live-action title. For everything else, use the new **Link to TMDB** action.
- Entries linked to the same TMDB title merge into one card, and the Library kind selector has a new Live Action option.
- If a replacement download fails during provider reassignment, the previous link and artwork are kept. Merges and sync keep AniList and TMDB identities separate, and the merge dialog explains mixed selections instead of failing.
- **YouTube Library Kind**:
- YouTube channels are now their own Library media kind. Existing channel entries migrate automatically, and viewing history and manual video assignments are unchanged.
- New All Titles, Anime, and YouTube Library filters.
- Channels are excluded from AniList matching, season repair, and duplicate recommendations.
- Merges and video moves can no longer combine an anime entry with a YouTube channel.
### Changed
- **Launcher Uses Bundled Bun**:
- Every installed and downloadable launcher now runs on the Bun runtime that ships with SubMiner. A system Bun is no longer needed.
- Recognized legacy launchers migrate automatically.
- Windows gets a `subminer.cmd` launcher download.
- First-run setup is reduced to one optional launcher control. Runtime repair guidance appears only when it is needed.
- **Faster Sync Transfers**:
- Sync between compatible macOS and Linux machines now uses compressed, incremental rsync transfers.
- The last snapshot received from each peer is cached, which reduces traffic on later syncs.
- Machines without a compatible rsync, including Windows, fall back to compressed scp.
- Older peers still work without the upload cache.
- Transfers abort after 30 minutes.
- **Stats Server Request Safety**:
- The stats server now accepts loopback hosts only and rejects requests from browser origins other than its own.
- Requests that change data must send an `application/json` body. Scripts that POST to the server need to set a JSON content type.
- The in-app stats overlay now loads from the local server, so it gets the same protection.
- Dashboards served through a reverse proxy or Tailscale Serve are no longer supported.
- **Smaller Downloads**:
- Installers and unpacked apps are smaller. Demo media, source maps, TypeScript sources, test fixtures, and unused Koffi binaries are no longer packaged.
- All windows now share one Japanese UI font.
- Release builds publish package size reports that compare against the previous release.
- **Bundled Yomitan**: Updated with upstream Yomitan 26.9.8 changes, including historical Japanese kana transformations, Ukrainian language support, and improvements to Anki duplicate searches and audio retrieval.
### Fixed
- **Jellyfin 12 Compatibility**:
- Playback, subtitle, artwork, and remote-control requests now authenticate with the `ApiKey` query parameter, so the integration works on Jellyfin 12, where legacy authorization is off by default.
- "Play on SubMiner" stays available. The cast connection now answers keep-alive requests and reconnects when the server stops responding, instead of silently dying after about a minute.
- The Jellyfin "now playing" bar clears when you close or finish a cast video instead of running on to the end of the episode.
- Cards mined during Jellyfin playback get the episode title in the misc info field again instead of "Unknown media".
- **Jellyfin Privacy and Playback**:
- Jellyfin streams no longer leak titles taken from the stream URL, or stream URLs that contain credentials, into metadata lookups, Anki source fields, Discord presence, stats, or AniList retries.
- Previously cached metadata that contained credentials is cleaned up. Watch history and library assignments are not touched.
- Jellyfin playback and casting now use your configured mpv executable, so they work when mpv is installed outside PATH. Portable plugins next to that executable are detected.
- **Anki Mining**:
- New `ankiConnect.fields.wordAudio` setting reads word audio separately from the sentence audio field. This fixes animated images that started moving immediately when `fields.audio` pointed to `SentenceAudio`. Existing animated images need to be regenerated to pick up the fix.
- Sentence furigana on word cards stays in sync with the full stats-search context and with expanded timing review selections. Stale readings are cleared if regeneration fails.
- Closing the overlay while media timing review is still loading now cancels the review, resumes playback if the review paused it, and cleans up the hidden preview player.
- `ankiConnect.media.maxMediaDuration: 0` now means unlimited when mining from the stats dashboard, matching overlay mining.
- Invalid AnkiConnect, Kiku, and Senren settings are now rejected with a warning and fall back to defaults.
- **Stats Server Stability**:
- A port conflict is now reported in a status notification instead of crashing SubMiner.
- Simultaneous startup requests share one server start. Stopping the background server no longer disconnects dashboards open in the foreground.
- Shutdown waits only a limited time for active requests to finish.
- Malformed resource IDs, and ID lists with any invalid entries, are rejected before Library changes or cover backfills run.
- **Subtitle Sidebar**:
- Clicking a cue no longer leaves the row focused, and Space no longer seeks back to a focused cue. Enter still seeks to the focused cue, and Space keeps its configured playback action.
- The sidebar stays near the current playback position during gaps when the subtitle file has a cue that starts at zero.
- **Settings Save Feedback**:
- Settings marked LIVE no longer show false restart warnings, including for notifications and subtitle generation.
- When a save mixes live and restart-only changes, the live changes apply right away and only the changed sections that need a restart are listed.
- **Overlay Windows**:
- On Hyprland, recovery dialogs stay above SubMiner windows so overlay placement updates no longer cover their Wait and Close buttons.
- On Linux, a delayed close callback during teardown can no longer reopen the overlay.
- **First Launch on macOS**: SubMiner no longer exits on first launch when the config directory does not exist yet.
### Docs
- **Launcher**: Documented the launcher install that uses the bundled runtime, migration from legacy launchers, package-managed updates, and the bundled Bun runtime's MIT and LGPL notices. The AUR package installs these notices under `/usr/share/licenses/subminer-bin`, and they are also included in `subminer-assets.tar.gz`.
- **Subtitle Generation**: Documented model choice, VAD behavior, splitting guided by a reference track, fallback behavior, and known limits.
- **Subtitle Selection**: Documented the subtitle selector setting, its shortcut override, and the primary and secondary track controls.
- **Settings**: Clarified save feedback for live settings, warnings for saves that mix live and restart-only changes, and how subtitle generation settings reload.
- **Jellyfin**:
- Clarified that Windows mpv playback and Jellyfin casting can use a configured executable path instead of PATH.
- Documented how Jellyfin media titles and stats identities keep stream credentials out of metadata.
- **Stats Library**:
- Documented TMDB linking, provider reassignment, merge compatibility, and caching of the credential command's output.
- Documented YouTube channel filtering and video statistics in the Library.
- **Mining**:
- Documented choosing the screenshot separately in media timing review.
- Documented the separate word audio field mapping, including that existing animated images need to be regenerated.
- **Sync**: Documented compressed transfers, where the incremental sync cache is stored, and compatibility with older peers.
<details>
<summary>Internal changes</summary>
### Internal
- Removed duplicate source and launcher smoke runs from the reusable CI quality gate. Every distinct test lane and failure artifact is kept.
- Replaced mislabeled dist source reruns with a small Electron-runtime smoke check. It covers compiled stats startup, the HTTP service, native SQLite, port conflicts, and cleanup.
</details>
## v0.19.6 (2026-09-04)
### Added
- **Card Timing Review**:
- Optional pre-generation timing review for word, sentence, and audio cards, with a speech-weighted waveform that flattens background noise so dialogue edges stand out clearly.
- The clip end automatically snaps back to where the line's dialogue actually ends once the waveform loads, with drag and keyboard adjustments available.
- Audio preview includes a sweeping playhead that plays the clip to its true end, even on high-latency outputs like Bluetooth headphones.
- Previous and next subtitle lines can be pulled onto the card with `P`/`N` (or the Prev/Next steppers) and removed with Shift; the sentence preview and waveform markers update automatically.
- Cancelling lets you keep a card without media, and the review can be toggled on or off for the session.
- **Senren Field Grouping**:
- Enable `ankiConnect.isSenren` to merge duplicate mined cards using Senren's scene-switching markup, grouping sentence, furigana, audio, picture, and misc-info fields.
- Supports the same auto/manual/disabled modes as Kiku, including the manual merge modal; only one of Senren or Kiku can be enabled at a time.
### Changed
- **Remote Stream Mining Performance**: Mining a card from a remote stream (Jellyfin and other HTTP sources) now downloads the clip window once and reuses it for the timing review waveform, audio preview, audio extraction, and screenshot, instead of re-fetching the stream at each step; the temporary file is cleaned up after ten minutes of inactivity or on exit.
- **TsukiHime Release Filtering**: The TsukiHime modal's Japanese and secondary-language tabs now filter the release list by the subtitle languages each release actually carries, and report when no release has subtitles for the active tab.
### Fixed
- **Subtitle & Mining Accuracy**:
- Broadcast-style captions that split one sentence across two on-screen rows (e.g. Crunchyroll Japanese subs) now merge into a single line for the sidebar and mined cards, while separate speakers, sound effects, and labeled turns still stay on their own lines.
- Mining from the overlay no longer pulls in a lingering row from the previous caption; the mined sentence and clip timing now match what's actually on screen.
- Multi-line copy and mining now select lines backward in timeline order after seeking, instead of in playback encounter order.
- Copying a subtitle, mining a sentence, or recording immersion stats no longer includes the separate furigana line that broadcast ASS captions place above a word.
- **Card Update Notifications**: Dismissed lingering overlay card-update progress when notification settings switch to OSD before an update finishes.
- **Overlay Stability on Hyprland**: Opening a modal window (timing review, Jimaku, session help, and others) while mpv is fullscreen no longer causes the overlay to flicker while the modal loads; the overlay now stays on screen untouched until the modal is ready.
- **Jellyfin Subtitle Sync**: Jellyfin subtitle files now load with zero mpv delay instead of inferring and saving an offset from Japanese and English cue timelines.
- **Secondary Subtitle Visibility**: Native mpv secondary subtitles stay hidden when switching secondary subtitle tracks during playback.
## v0.19.5 (2026-08-30)
### Fixed
- **Anki Card Update Progress**: The card-update spinner now stays visible until audio and image updates finish, instead of disappearing early.
- **Anki Word-Card Fields**: Word-card enrichment now writes sentence text and audio to the fields configured in AnkiConnect, while the dedicated sentence-card and audio-card actions keep their existing compatible field names.
- **Overlapping Subtitles**:
- Subtitle lines that start while another line is still on screen now appear alongside it, instead of staying hidden until a track switch or seek.
- Subtitles shown at the same time now stack by their authored screen position, with top signs and song lines above bottom dialogue.
- Half-size ASS furigana is no longer shown as if it were a dialogue line.
- **YouTube Auto Captions**:
- Auto-generated captions now follow their intended timing and two-row roll-up layout.
- Long speech is paged instead of covering the video with a wall of text.
- Explicitly timed sound cues like `[音楽]` no longer cover later dialogue.
## v0.19.4 (2026-08-25)
### Added
- **Library Merge & Move**: Duplicate library cards for the same show can now be combined. Select cards in the library grid and use "Merge Selected" to pick which entry to keep and move every episode onto it, preserving sessions, mined cards, and watch time. Episodes can also be reassigned individually via the "→" button, useful when a file lands under a stray title; manual assignments survive later filename parsing, Jellyfin refreshes, and season repair. Exact AniList title matches with compatible seasons now merge automatically, while fuzzy matches surface as dismissible "Possible duplicate" reviews instead of merging silently.
- **Duplicate Line Cleanup Tool**: The Vocabulary tab's new "Duplicates" button scans a chosen time window (7 days through all time) for old karaoke/typeset duplicate-line bursts, shows what it found, and collapses each run to one line once confirmed; `subminer stats cleanup --duplicate-lines` does the same from the terminal, with `--dry-run` and `--lookback-days <n>` options. Watch time and lines-seen totals are left unchanged.
### Changed
- **Prerelease Release Notes**: Prerelease notes now open with a "Changes since" section listing only what changed versus the previous beta/RC of the same version, above the cumulative highlights, and CI rejects prerelease tags whose committed notes were generated for a different beta/RC.
### Fixed
- **Subtitle & Karaoke Duplication**:
- Karaoke and animated signs are reconstructed once from their authored text and shown only while actually sung, with original word spacing preserved, instead of flooding the overlay, subtitle sidebar, immersion history, mining, or stats with glyph fragments, per-frame color phases, and repeated animation events.
- Decorative layers (highlight sweeps, glow/shadow copies, symbol-font decoration, particle swarms, hidden or zero-scaled text) stay out of published text, while ordinary repeated dialogue, positioned signs, wrapped lyric rows, and multi-row CC-style blocks still display correctly.
- Embedded subtitle tracks on network-mounted (SMB/NFS) media are extracted and parsed again instead of falling back to live-text-only, restoring karaoke reconstruction, sidebar cues, and mining for releases that only ship subtitles inside the container.
- Secondary subtitles go through the same deduplication pipeline as primary subtitles and no longer clip display after about four lines.
- Event-heavy karaoke files that previously stalled subtitle loading for several seconds now parse in well under a second.
- **Character Dictionary Reliability**:
- Generation, merged rebuilds, and imports no longer freeze the app on large dictionaries; snapshot I/O, archive building, and image/name lookup caches moved off the UI's critical path.
- Dictionaries are reused instead of regenerated when MeCab finds no name splits.
- Cached portraits restore correctly after the portrait index finishes loading post-tokenization.
- Desktop progress notifications on Linux AppImage installs update in place instead of flickering, fixing a bug where the AppImage's bundled libraries broke the system notification helper.
- **Overlay Startup & Modals**:
- The macOS window-tracking helper targets macOS 12.0+ instead of requiring the build machine's exact macOS version, fixing crashes on older systems like Ventura that left the overlay stuck on "Overlay loading".
- mpv IPC connection attempts time out and retry, showing an actionable error if content still isn't ready after 30 seconds.
- Dedicated overlay modals are prewarmed on macOS and Windows so shortcuts open them promptly.
- On macOS, reused modals and the stats window open above fullscreen mpv on its current Space instead of jumping to another desktop.
- **Wayland File Drop**: Fixed native Wayland drag-and-drop from file managers such as Thunar, so subtitle and video files dropped on the visible overlay are resolved and forwarded to mpv.
- **Windows Mouse Lag**: Fixed system-wide mouse lag on Windows while SubMiner is running, caused by the overlay's global mouse hook for click-through forwarding and by the mpv window tracker blocking the app on repeated PowerShell lookups.
- **Sentence Mining Audio & Clips**: Sentence-audio generation no longer times out on slow network-mounted media with many subtitle/font streams (bounded FFmpeg probing, two-minute extraction budget, clearer error reporting), and mined audio/animated AVIF clips now capture the subtitle line that was actually mined by snapshotting the clip range at lookup time instead of reading live mpv state later.
- **Stats Performance & Reliability**: Immersion stats storage now sets its SQLite busy timeout before WAL setup, avoiding transient lock errors under concurrent writes. Deletes in the stats dashboard no longer freeze the UI, run proportional to what's deleted instead of rebuilding full lifetime summaries, retry safely if the delete worker crashes, and no longer rescan the whole library when deleting very common words; a new index also makes large session deletes drop from minutes to milliseconds. Library merges, video moves, and AniList reassignments got the same lifetime-summary fix.
- **Vocabulary Stats Accuracy**: Vocabulary totals and charts now count all tracked vocabulary instead of only the first page, new-word history uses corrected daily rollups (fixing legacy timestamp and time-zone issues), summary cards refresh automatically after edits to the exclusion list, and rapid exclusion edits no longer race each other.
- **Rofi MKV Thumbnails**: Fixed missing MKV thumbnails in the Linux rofi picker when system thumbnailer registrations only advertise legacy Matroska MIME aliases.
<details>
<summary>Internal changes</summary>
### Internal
- Docs Site Indexing: Excluded the `/main/` and `/v/<version>/` docs trees from search indexing (self-referential canonical, `noindex,follow`, matching `X-Robots-Tag`) so crawlers focus on current docs instead of ~30 archived copies of every page, and restored `<lastmod>` dates in the docs sitemap that were silently dropped by production builds.
</details>
## v0.19.3 (2026-08-13)
### Added
- Changelog Modal: Adds an in-app changelog you can open from the tray ("View Changelog") or the "What's New" button on the update notification, so the notification stays reachable while you read. It shows the newest published release notes (falling back to the bundled changelog if that fetch fails), folds older versions while keeping the current one expanded, and supports keyboard navigation (`J`/`K`/arrows, `Enter`, `R`, `Esc`).
### Changed
- Subtitle Tokenization Performance: Reworks subtitle dictionary lookups to cut per-line work roughly in half, cache repeated lookups across lines, and stop tokenization from competing with on-screen subtitle prefetching. Also fixes several accuracy issues along the way: dropped readings on trailing kana, character names being skipped after a dictionary sync, annotations not refreshing after mining a card, and halfwidth katakana character names losing their reading or being swallowed by other words.
### Fixed
- Character Dictionary Large Imports: Large character dictionaries (e.g. One Piece) no longer fail to install from a fixed timeout budget; the import now scales its time budget to dictionary size and reports detailed progress (page/character counts, image download progress, elapsed time) instead of one static message.
- Stats Delete Responsiveness: Deleting sessions, episodes, or library entries no longer freezes the stats page or an active video player; deletes are now batched into a single transaction.
- Styled Subtitle Cue Parsing: Heavily typeset subtitles (karaoke, signs) no longer flood the subtitle sidebar with garbage; vector drawing commands are no longer shown as text, and duplicate/animation-burst cues now collapse into one.
- X11 mpv Renderer: Fixes an mpv crash on the first fullscreen toggle for X11/XWayland users with `gpu-next` shaders (e.g. ArtCNN), which was caused by X11 mode forcing the legacy OpenGL renderer.
- X11 Overlay Display Scaling: Fixes the overlay appearing oversized and offset from mpv on X11/XWayland under fractional or mixed-monitor display scaling.
<details>
<summary>Internal changes</summary>
### Internal
- Subtitle text is now decoded from ASS exactly once at ingest, so the renderer, timing tracker, and tokenizer all share one decoded value instead of each re-deriving it.
- Added per-stage debug timings (`scanMs`, `mecabMs`, `frequencyMs`, `annotateMs`) to the subtitle tokenization pipeline log.
</details>
## v0.19.2 (2026-08-04)
### Changed
- Subsync: The sync modal now lets you choose both the reference subtitle (correct timing) and the out-of-sync subtitle to retime, for both alass and ffsubsync. alass can also use the loaded video's audio as a reference for local files. Retiming the secondary track now reloads the result into the secondary slot instead of overwriting the primary subtitle.
### Fixed
- Streaming Subtitle Tokenization: Jellyfin streams now seed subtitle tokenization directly from the downloaded subtitle file instead of relying on an mpv event that could be missed, and prefetching now runs to the end of the file and clears between episodes. The tokenization cache was raised from 256 to 2500 lines, and parsed cues are no longer lost when the active subtitle track briefly can't be resolved (e.g. switching to an embedded track). Together these prevent episodes from falling back to slow, line-by-line tokenization during playback.
- Overlay: Subtitle lines now appear immediately at their cue time even on a tokenization cache miss, upgrading in place once tokens and annotations are ready, instead of waiting on a line still being processed. A failed tokenization is no longer cached as plain text, so repeated lines get another chance at annotations.
- Background Logging: Background startup now respects the configured logging level when no explicit log level is passed.
<details>
<summary>Internal changes</summary>
### Internal
- Patched three high-severity dependency advisories (`undici`, `brace-expansion`, `fast-uri`).
</details>
## v0.19.1 (2026-08-01)
### Added
- Word Card Type: Adds a setting (Settings > Mining/Anki > Kiku/Lapis Features > "Word Card Type") to choose which card-type flag SubMiner marks on Kiku/Lapis word cards — `word-and-sentence` (default), `click`, `sentence`, `audio`, or `none`. Click cards (`IsClickCard`) can now be flagged, and setting any card-type flag clears the others so a note can't claim two types at once.
### Fixed
- Yomitan Popup: Fixes the macOS Yomitan popup going inert after mining a card — clicks outside the popup no longer pass through to mpv, and scrolling over the popup scrolls its definitions instead of seeking playback.
- YouTube Playlist Links: Fixes opening a video from a playlist URL (e.g. a Watch Later link with `list=`/`index=`) timing out while probing subtitles, metadata, or the playback URL.
## v0.19.0 (2026-07-29)
### Added
- Anki Maturity Highlighting: Known-word subtitle highlights can now be colored by Anki card maturity (new, learning, young, mature), similar to asbplayer. Tier thresholds and colors are configurable, with a runtime toggle and an updated help legend.
- Post-Playback Menu: After a watch-history episode ends, the fzf/rofi launcher returns to that series with options to play the previous or next episode, rewatch, pick another episode, or quit. The pre-playback series menu now offers the previous episode too.
- Delete Library Entries: The stats Library detail view can now delete an entire title in one step (episodes, sessions, subtitle lines, rollups, cover art, and vocabulary counts). Delete progress is now shown app-wide via a progress bar and status toast instead of disappearing when you switch tabs.
- Cross-Machine Sync: Added SSH-based syncing of stats and watch history between machines, available from the tray ("Sync Stats & History") or `subminer sync`, with saved devices, per-host sync direction, background auto-sync, connection testing, manual snapshots, and support for Windows remotes.
- TsukiHime Subtitle Downloads: Added subtitle downloads for the current video via TsukiHime, loading Japanese as the primary track and your configured secondary language directly into mpv.
### Changed
- Clipboard-Video Shortcut: The "append clipboard video to queue" shortcut is now configurable.
### Fixed
- AniList Season Resolution: Season 2+ files now resolve to the correct AniList entry instead of silently falling back to season 1 (which mismatched character dictionaries and watch progress). Manual overrides now stay scoped per season, fix both the dictionary and progress tracking together, and also correct per-season cover art.
- Subtitle Annotation Accuracy: Fixed several annotation edge cases, including inconsistent POS exclusions on merged quote-particle tokens, dropped annotations on supplementary-plane kanji, katakana punctuation wrongly treated as noise, and certain kanji vocabulary losing N+1 highlighting eligibility.
- AnkiConnect Proxy Port Conflict: Video startup no longer crashes when another process already holds the configured AnkiConnect proxy port; a notification now explains how to resolve it.
- AppImage Quit Crash: Fixed a "Service Crash" desktop notification appearing after closing a video when running the Linux AppImage.
- Autoplay Pause Timing: Fixed playback resuming a few seconds before subtitle tokenization warmup finished, most noticeable when resuming mid-episode or when a cue starts within the first two seconds.
- Stats Known-Word Count: Fixed stats reporting 0 known words for every session after the known-word cache format changed.
- Stats Library Cover Art: Relinking a title to a different AniList entry now updates its cover in the Library grid immediately instead of leaving a stale, mismatched cover cached.
- Rofi Prompt Spacing: Rofi menu prompts now keep a space before the input field instead of running into the placeholder text.
- Stats Settings & Reliability: Hardened stats settings validation (nested/legacy AnkiConnect config now falls back safely instead of breaking) and stats routes against malformed requests and other edge cases.
- Stats Delete Performance: Deleting sessions, episodes, and library entries is now dramatically faster and no longer stalls playback (e.g. a 12-episode title dropped from about a minute to under a second on a large library); the Vocabulary tab also loads much faster. The first launch after upgrading runs a one-time database migration.
<details>
<summary>Internal changes</summary>
### Internal
- Added a golden-file regression test corpus for the tokenizer/annotation pipeline, plus scripts to record new fixtures and diff against stock Yomitan.
- Consolidated renderer modal state handling into a descriptor registry.
- Consolidated CI quality checks (PR, stable, and prerelease) into one reusable workflow with mpv plugin tests and dependency audits.
- Removed the unused stats IPC transport and unified stats dashboard HTTP types with the backend contract.
- Added a script to verify known-word highlight tiers against live Anki data outside of playback.
</details>
## v0.18.0 (2026-07-10)
### Added
- Sentence Audio Normalization: Generated sentence audio is now normalized to -23 LUFS by default, and clips mined from playback mirror mpv's software volume curve with a limiter to prevent clipping. Both behaviors are configurable independently.
- Watch History Command: Added `subminer -H` / `--history` to browse watch history, replay or continue episodes, or pick one via fzf or rofi, with cover art shown in the rofi picker.
### Changed
- Fzf Preview Layout: Moved fzf previews below launcher menus, giving long titles and metadata more room.
- Known-Word Highlighting: Now compares subtitle and Anki-card readings, preventing false matches between homographs and unrelated words that share a reading, while still supporting matching across kana and kanji spellings.
- Annotation Filtering: Standalone suffix tokens (e.g. さん, れる) are now excluded from JLPT/frequency/N+1 highlighting by default, matching how particles and interjections are treated; configurable via the pos2 exclusion setting.
- App Icon: Replaced the app icon with new pixel-art submarine artwork contributed by the community, used across the app icon, tray, notifications, README, docs site, and Stats page.
- Stats Trend Charts: Overhauled with persisted title visibility, per-chart title limits, "top" and "most recent" ranking modes, an option to show or hide empty days, calendar-aligned periods, and value-sorted tooltips.
### Fixed
- Background Stats Server: `subminer app` background launches now auto-start the stats server when enabled, and skip startup if one is already running.
- Character Name Highlighting: Character dictionaries now split unspaced native names more reliably, and portraits, highlights, and hover lookup survive punctuation, unmatched text, and competing dictionary matches without incorrectly splitting longer words.
- Highlighting Coverage: Frequency/JLPT highlighting and vocabulary stats now include content adverbs (e.g. 確かに, やはり) and kanji nouns MeCab tags as non-independent (e.g. 日, 点, 以外), while still suppressing interjections, pronouns, and grammar fragments; lexicalized kana expressions like かといって keep their annotations.
- Cover Art Fetching: Stats now fetches AniList cover art as soon as a new series starts playing, and backfills missing art for existing series on the next Stats visit.
- Kiku Field Grouping: The manual field-grouping dialog now stays above fullscreen mpv, remains usable across repeated attempts, closes abandoned windows after timeouts, and reports a clear error when the original card can no longer be loaded.
- Karaoke Subtitle Collapse: Secondary subtitles no longer stack dozens of one-syllable lines during openings and endings; repeated events collapse into one line, capped to a strip at the top.
- Unparsed Token Hover: Subtitle text Yomitan can't parse (truncated inflections, elongation runs) remains hoverable, while staying excluded from highlighting, N+1 candidate math, and vocabulary stats.
- YouTube Streaming: Fixed direct YouTube stream extraction that could corrupt signed URLs and cause ffmpeg 403 errors.
<details>
<summary>Internal changes</summary>
### Internal
- Test lanes moved to `scripts/test-lanes.ts` with per-directory discovery and isolated per-file timeouts; CI now covers previously orphaned stats, scripts, plugin process-retry, and runtime-compat suites, plus a new stats lane in the change-verification workflow.
</details>
## v0.17.2 (2026-06-28)
### Fixed
- YouTube Background Cache: Fixed Windows YouTube background media cache startup for YouTube URLs opened directly in mpv, including resolved stream URLs when mpv still exposes the original YouTube playlist entry, so queued Anki media updates can append audio and images after the cache finishes.
- YouTube Subtitle Picker: Manual subtitle picker requests now show an immediate configured notification while SubMiner probes tracks and opens the modal. Subtitle download progress is replaced with a transient success notification after tracks load.
## v0.17.1 (2026-06-27)
### Added
- YouTube Media Cache Mode: Adds `youtube.mediaCache.mode` with `direct` and `background` options. Background mode uses a yt-dlp cache download when direct stream extraction is unreliable — creates a text-only card immediately, queues media updates for mined notes, and fills audio/image fields once the download finishes. Progress is announced via overlay/OSD notifications. Downloads are capped at 720p by default (`youtube.mediaCache.maxHeight`). Switching back to direct mode cancels any in-flight background download.
### Fixed
- Log Export: Fixed log filenames to use the local date so exports around UTC midnight include the current day's logs rather than stale prior-day files. Expanded export redaction to mask IPs, emails, auth and cookie headers, yt-dlp cookie arguments, URL credentials, token/key/password fields, and signed YouTube media URL parameters.
- YouTube Card Media: Improved media generation reliability by sending safer ffmpeg options for resolved streams and skipping stale stream maps (including cached YouTube files). Hardened background cache downloads with IPv4 and extractor retry flags; failed downloads now notify the user and clear queued media updates instead of leaving them silently pending. Stale background cache files are cleaned on startup and before each new download.
## v0.17.0 (2026-06-15)
### Changed
- **Subtitle Delay Keybindings**: Updated default overlay subtitle delay and step bindings to match mpv conventions: `z`, `Z`, and `x` adjust `sub-delay`; `Ctrl+Shift+Left/Right` run native `sub-step` with OSD feedback. The previous SubMiner-only adjacent-cue delay action has been removed.
- **Update Notifications**: New installs now default update notifications to overlay-only instead of overlay + system notifications.
### Fixed
- **Anki – Highlight Word**: Fixed bolding of the mined word in Kiku sentence and sentence-furigana fields when the source Yomitan sentence did not already contain bold markup.
- **Anki – Lapis/Kiku Word Cards**: Fixed word-and-sentence marker missing from Lapis/Kiku word cards enriched through SubMiner, which could hide sentence context on the card front.
- **Anki – Windows Media Generation**: Fixed known-word cache refreshes when no deck is configured, and fixed audio/image generation after background launches by recreating missing FFmpeg temp directories before clipping.
- **Character Dictionary – Windows**: Fixed the Windows "SubMiner mpv" shortcut so character dictionary auto-sync can fall back to mpv's current video path when app media state is not yet ready.
- **Notifications**: Restored the SubMiner app icon on system notifications that do not supply a custom notification image.
- **Overlay – macOS Yomitan Popup**: Fixed Yomitan popup focus after card mining or popup reload; fixed popups staying open when clicking transparent overlay space — click-away now closes the popup and returns click passthrough to mpv without a hide/reappear cycle.
- **Overlay – Linux Auto-Pause Startup**: Fixed the visible overlay on Linux auto-paused startup to stay interactive during the initial measurement gap; startup subtitle cache misses now paint raw text before tokenization finishes, and temporarily empty `sub-text` refreshes are resolved before warm readiness resumes playback.
- **Overlay – Playlist Advance**: Fixed the visible overlay being dismissed when mpv advances to the next playlist item, including when the next episode loads after the warm transition delay.
- **Overlay – Windows Subtitle Bar**: Fixed shaky hover and click interaction on the Windows subtitle bar when a video attaches to an already-running background SubMiner instance.
- **Stats – AniList Linking**: Fixed manual AniList linking from the stats anime page so auto-searches strip the generated "Season N" suffix and query only the anime title.
- **Updates – Linux Support Assets**: Fixed Linux updates to correctly install and refresh the launcher runtime plugin copy and rofi theme alongside AppImage and launcher updates; unrelated SubMiner data directories are now left untouched and plugin copies are staged before replacing the live runtime. Fixed first-launch playback on fresh installs by auto-installing missing managed support assets from the bundled app before mpv starts.
### Docs
- **Linux Update Flows**: Documented that Linux update flows manage the launcher runtime plugin copy and rofi theme from `subminer-assets.tar.gz`, and that normal launcher playback auto-installs those managed support assets on a fresh install if either is missing.
<details>
<summary>Internal changes</summary>
### Internal
- **Runtime Modules**: Split main-process runtime wiring into focused modules without changing user-facing behavior; hardened helpers against stale background stats daemon PIDs, stalled subtitle extraction, and dropped async errors.
- **Release CI**: Fixed GitHub release notes to preserve the `What's Changed` and `New Contributors` attribution sections when CI regenerates from the committed changelog; scoped prerelease note reuse to the same base version so a new beta line starts from current fragments.
</details>
## v0.16.0 (2026-06-10)
### Breaking Changes
- **Notification Mode `both`**: `notificationType: "both"` now routes to overlay + system notifications. Users who previously used `"both"` for mpv OSD + system notifications should set `notificationType` to `"osd-system"` in `config.jsonc`. The `osd` and `osd-system` values remain valid as config-file entries but no longer appear in Settings.
### Added
- **Overlay Notifications**: New overlay notification stack (Catppuccin Macchiato theme) with configurable screen position (`notifications.overlayPosition`: top-left, top-center, top-right), 3-second transient dismissals, and persistent cards for long-running jobs like character dictionary sync.
- **Notification History Panel**: `Ctrl/Cmd+N` (configurable via `shortcuts.toggleNotificationHistory`) opens a session log of all notifications. Works whether the overlay or mpv has focus; slides in from the notification edge; entries can be individually removed or cleared.
- **Mined-Card Notification Actions**: Mined-card overlay notifications show generated card thumbnails and include an Open in Anki button in both live cards and history entries.
- **Update Notification Action**: Update-available overlay notifications include an Update button to start the app update flow directly from the notification.
- **Stats Search**: New Search tab in Stats for realtime subtitle sentence search with media context, headword matching, and mining actions for source-backed sentence cards or exact-match word/audio cards.
### Changed
- **AniSkip**: Moved intro detection from the mpv plugin to the SubMiner app. Lookups now cover every file loaded during a session including playlist advances, and `mpv.aniskipEnabled`/`mpv.aniskipButtonKey` hot-reload without restarting playback. The bundled plugin no longer makes network calls. Note: AniSkip now requires the SubMiner app to be connected; plugin-only mpv sessions will not fetch skip windows.
- **Stats Library**: Entries are now split by detected season (season folder first, filename parsing as fallback). Existing combined-series rows are automatically migrated to per-season entries on startup. Cover art and anime details refresh immediately after a manual AniList entry change.
- **Stats Vocabulary**: Remembers Hide Known/Hide Kana filters across sessions, applies Hide Kana filtering cross-title, collapses duplicate token variants in exclusions, and matches Related Seen Words by shared readings or kanji.
- **Stats Trends**: Reorganized into Activity, Cumulative Totals, Efficiency, Patterns, and Library sections; disambiguated per-period vs. cumulative charts; added Words/Min and Cards/Hour efficiency charts.
- **Stats Mining**: Sentence cards are created before slow media generation finishes; stored/requested secondary subtitles are preserved before falling back to sidecar or alass-retimed English subtitles; empty `ankiConnect.deck` falls back to Yomitan's mining deck; partial media failures are surfaced.
- **Stats Browsing**: Remembers library card size; retries stored cover art without extra AniList lookups; preserves PNG/WebP MIME types; honors custom AnkiConnect URLs for Browse; shows progress during session deletes.
- **Startup Notifications**: Tokenization, subtitle annotation, and character dictionary status now route through queued overlay notifications in `overlay`/`both` mode instead of falling back to mpv OSD while the overlay loads.
- **Notification Deduplication**: Cycling subtitle modes updates the active overlay card in place rather than stacking duplicates; repeated progress updates (e.g. subsync) tick in place without flickering.
- **Update Notification Default**: New installs default `notificationType` to `both` so update alerts appear in both overlay and system notifications.
### Fixed
- **AniList Completion**: Entries are now marked completed when a post-watch update reaches the final known episode of the season.
- **AniSkip Markers**: Fixed intro markers disappearing after same-media mpv reloads; fixed metadata detection for intros that start at 0 seconds and common release-group filenames.
- **Jellyfin Session**: Remote session now restarts after setup login so the websocket reconnects with fresh credentials, and stops cleanly on logout.
- **Sentence Card Audio**: Mining a sentence card no longer fills the expression audio field; generated audio goes only to the configured sentence audio field.
- **Stats Mining Fields**: Sentence clips update `SentenceAudio` correctly; word audio uses configured Yomitan sources; English subtitle text is not written to word cards; secondary subtitle auto-selection prefers regular English tracks over Signs/Songs tracks.
- **Overlay Hover Readiness**: Subtitle bars are hoverable and clickable from the first subtitle line on visible overlay startup or resume, without waiting for the next subtitle event.
- **Startup Autoplay**: Playback is released after tokenization and overlay content are ready even when playback begins before the first subtitle line appears.
- **Overlay Startup Feedback**: Restored mpv OSD loading spinner that starts on connect, media open, or overlay request, and clears once the overlay is content-ready and visible.
- **Linux Overlay Input**: Notification close and action buttons remain clickable above subtitle bars on Linux.
<details>
<summary>Internal changes</summary>
### Internal
- **Build**: `make deps` now initializes git submodules before installing dependencies on a fresh source checkout.
- **Release Tooling**: Release notes now credit contributors and first-time authors resolved from changelog fragments via git and the GitHub API.
- **Changelog Guidance**: PR fragment guidance updated to preserve separate-outcome fragments while directing contributors to consolidate same-PR follow-up notes before adding churn.
</details>
## 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) ## v0.14.0 (2026-05-12)
### Added ### 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. - **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. - **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
@@ -58,7 +711,7 @@
### Internal ### 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. - 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> </details>
@@ -77,8 +730,8 @@
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity. - 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: 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: 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: 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: 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 ### Fixed
+46 -19
View File
@@ -1,9 +1,11 @@
.PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-windows uninstall uninstall-linux uninstall-macos uninstall-windows print-dirs pretty lint ensure-bun generate-config generate-example-config dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop .PHONY: help 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 APP_NAME := subminer
THEME_SOURCE := assets/themes/subminer.rasi THEME_SOURCE := assets/themes/subminer.rasi
THUMBNAILER_SOURCE := assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer
LAUNCHER_OUT := dist/launcher/$(APP_NAME) LAUNCHER_OUT := dist/launcher/$(APP_NAME)
THEME_FILE := subminer.rasi THEME_FILE := subminer.rasi
THUMBNAILER_FILE := subminer-ffmpegthumbnailer.thumbnailer
# Default install prefix for the wrapper script. # Default install prefix for the wrapper script.
PREFIX ?= $(HOME)/.local PREFIX ?= $(HOME)/.local
@@ -20,9 +22,10 @@ MACOS_APP_DIR ?= $(HOME)/Applications
MACOS_APP_DEST ?= $(MACOS_APP_DIR)/SubMiner.app MACOS_APP_DEST ?= $(MACOS_APP_DIR)/SubMiner.app
# If building from source, the AppImage will typically land in release/. # If building from source, the AppImage will typically land in release/.
APPIMAGE_SRC := $(firstword $(wildcard release/SubMiner-*.AppImage)) APPIMAGE_SRC = $(firstword $(wildcard release/SubMiner-*.AppImage))
MACOS_APP_SRC := $(firstword $(wildcard release/*.app release/*/*.app)) MACOS_APP_SRC = $(firstword $(wildcard release/*.app release/*/*.app))
MACOS_ZIP_SRC := $(firstword $(wildcard release/SubMiner-*.zip)) MACOS_ZIP_SRC = $(firstword $(wildcard release/SubMiner-*.zip))
PRERELEASE_NOTES := release/prerelease-notes.md
UNAME_S := $(shell uname -s 2>/dev/null || echo Unknown) UNAME_S := $(shell uname -s 2>/dev/null || echo Unknown)
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
@@ -61,13 +64,18 @@ help:
" dev-watch-macos Start watch loop with forced macOS tracker backend" \ " dev-watch-macos Start watch loop with forced macOS tracker backend" \
" dev-toggle Toggle overlay in a running local Electron app" \ " dev-toggle Toggle overlay in a running local Electron app" \
" dev-stop Stop a running local Electron app" \ " dev-stop Stop a running local Electron app" \
" docs-test Run docs tests" \
" docs-build Build the docs site" \
" docs-build-versioned Build production versioned docs site" \
" docs-dev Start the docs dev server" \
" install-linux Install Linux wrapper/theme/app artifacts" \ " install-linux Install Linux wrapper/theme/app artifacts" \
" install-macos Install macOS wrapper/theme/app artifacts" \ " install-macos Install macOS wrapper/theme/app artifacts" \
" install-windows Print Windows packaging/install guidance" \ " install-windows Print Windows packaging/install guidance" \
" generate-config Generate ~/.config/SubMiner/config.jsonc from centralized defaults" \ " generate-config Generate ~/.config/SubMiner/config.jsonc from centralized defaults" \
"" \ "" \
"Other targets:" \ "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-linux Remove Linux install artifacts" \
" uninstall-macos Remove macOS install artifacts" \ " uninstall-macos Remove macOS install artifacts" \
" uninstall-windows Remove Windows mpv plugin artifacts" \ " uninstall-windows Remove Windows mpv plugin artifacts" \
@@ -100,8 +108,10 @@ print-dirs:
"MACOS_APP_SRC=$(MACOS_APP_SRC)" \ "MACOS_APP_SRC=$(MACOS_APP_SRC)" \
"MACOS_ZIP_SRC=$(MACOS_ZIP_SRC)" "MACOS_ZIP_SRC=$(MACOS_ZIP_SRC)"
deps: submodules:
@$(MAKE) --no-print-directory ensure-bun @git submodule update --init --recursive
deps: submodules ensure-bun
@bun install @bun install
@cd stats && bun install --frozen-lockfile @cd stats && bun install --frozen-lockfile
@cd vendor/texthooker-ui && bun install --frozen-lockfile @cd vendor/texthooker-ui && bun install --frozen-lockfile
@@ -150,18 +160,20 @@ build-macos-unsigned: deps
@bun run build:mac:unsigned @bun run build:mac:unsigned
build-launcher: build-launcher:
@printf '%s\n' "[INFO] Bundling launcher script" @printf '%s\n' "[INFO] Building launcher runtime artifacts"
@install -d "$(dir $(LAUNCHER_OUT))" @bun run build:launcher
@bun build ./launcher/main.ts --target=bun --packages=bundle --outfile="$(LAUNCHER_OUT)"
@if ! head -1 "$(LAUNCHER_OUT)" | grep -q '^#!/usr/bin/env bun'; then \
{ printf '#!/usr/bin/env bun\n'; cat "$(LAUNCHER_OUT)"; } > "$(LAUNCHER_OUT).tmp" && mv "$(LAUNCHER_OUT).tmp" "$(LAUNCHER_OUT)"; \
fi
@chmod +x "$(LAUNCHER_OUT)"
@printf '%s\n' "[INFO] Launcher artifact: $(LAUNCHER_OUT)"
clean: clean:
@printf '%s\n' "[INFO] Removing build artifacts" @printf '%s\n' "[INFO] Removing build artifacts"
@rm -rf dist release @if [ -f "$(PRERELEASE_NOTES)" ]; then \
PRERELEASE_NOTES_BACKUP="$$(mktemp -t subminer-prerelease-notes.XXXXXX)" && \
cp "$(PRERELEASE_NOTES)" "$$PRERELEASE_NOTES_BACKUP" && \
rm -rf dist release && \
install -d release && \
mv "$$PRERELEASE_NOTES_BACKUP" "$(PRERELEASE_NOTES)"; \
else \
rm -rf dist release; \
fi
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage" @rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
generate-config: ensure-bun generate-config: ensure-bun
@@ -191,13 +203,27 @@ dev-toggle: ensure-bun
dev-stop: ensure-bun dev-stop: ensure-bun
@bun run electron . --stop @bun run electron . --stop
docs-test: ensure-bun
@bun run docs:test
docs-build: ensure-bun
@bun run docs:build
docs-build-versioned: ensure-bun
@bun run docs:build:versioned
docs-dev: ensure-bun
@bun run docs:dev
install-linux: build-launcher install-linux: build-launcher
@printf '%s\n' "[INFO] Installing Linux wrapper/theme artifacts" @printf '%s\n' "[INFO] Installing Linux wrapper/support artifacts"
@install -d "$(BINDIR)" @install -d "$(BINDIR)"
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)" @install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
@install -d "$(LINUX_DATA_DIR)/themes" @install -d "$(LINUX_DATA_DIR)/themes"
@install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)" @install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
@install -d "$(LINUX_DATA_DIR)/thumbnailers"
@install -m 0644 "./$(THUMBNAILER_SOURCE)" "$(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
@install -d "$(LINUX_DATA_DIR)/plugin/subminer" @install -d "$(LINUX_DATA_DIR)/plugin/subminer"
@cp -R ./plugin/subminer/. "$(LINUX_DATA_DIR)/plugin/subminer/" @cp -R ./plugin/subminer/. "$(LINUX_DATA_DIR)/plugin/subminer/"
@if [ -n "$(APPIMAGE_SRC)" ]; then \ @if [ -n "$(APPIMAGE_SRC)" ]; then \
@@ -206,7 +232,7 @@ install-linux: build-launcher
printf '%s\n' "[WARN] No release/SubMiner-*.AppImage found; skipping AppImage install"; \ printf '%s\n' "[WARN] No release/SubMiner-*.AppImage found; skipping AppImage install"; \
printf '%s\n' " Build one with: make build"; \ printf '%s\n' " Build one with: make build"; \
fi fi
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" @printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
install-macos: build-launcher install-macos: build-launcher
@printf '%s\n' "[INFO] Installing macOS wrapper/theme/app artifacts" @printf '%s\n' "[INFO] Installing macOS wrapper/theme/app artifacts"
@@ -247,8 +273,9 @@ uninstall:
uninstall-linux: uninstall-linux:
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage" @rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)" @rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
@rm -f "$(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
@rm -rf "$(LINUX_DATA_DIR)/plugin/subminer" @rm -rf "$(LINUX_DATA_DIR)/plugin/subminer"
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/plugin/subminer" @printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)" " $(LINUX_DATA_DIR)/plugin/subminer"
uninstall-macos: uninstall-macos:
@rm -f "$(BINDIR)/subminer" @rm -f "$(BINDIR)/subminer"
+87 -117
View File
@@ -4,7 +4,7 @@
# SubMiner # SubMiner
Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv. Look up words with Yomitan, mine them to Anki, and track your immersion without leaving mpv
[Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe) [Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
@@ -15,7 +15,7 @@ Look up words with Yomitan, export to Anki in one key, track your immersion —
[![License](https://img.shields.io/github/license/ksyasuda/SubMiner?style=flat-square&color=1a1a2e)](https://www.gnu.org/licenses/gpl-3.0) [![License](https://img.shields.io/github/license/ksyasuda/SubMiner?style=flat-square&color=1a1a2e)](https://www.gnu.org/licenses/gpl-3.0)
[![TypeScript](https://img.shields.io/badge/TypeScript-1a1a2e?style=flat-square&logo=typescript&logoColor=3178c6)](https://www.typescriptlang.org) [![TypeScript](https://img.shields.io/badge/TypeScript-1a1a2e?style=flat-square&logo=typescript&logoColor=3178c6)](https://www.typescriptlang.org)
[![SubMiner demo](./assets/minecard.webp)](https://github.com/user-attachments/assets/89e61895-e2b7-4b47-8d50-a35afe4132b2) [![SubMiner demo](./assets/minecard.webp)](https://github.com/user-attachments/assets/7abab8a9-4e4e-4f06-9f3c-9783e15a3807)
</div> </div>
@@ -23,7 +23,7 @@ Look up words with Yomitan, export to Anki in one key, track your immersion —
### Dictionary Lookups ### 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 in the subtitles to open the full Yomitan popup with definitions, pitch accent, and frequency data. SubMiner bundles its own Yomitan, separate from any browser install.
<div align="center"> <div align="center">
<img src="docs-site/public/screenshots/yomitan-lookup.png" width="800" alt="Yomitan dictionary popup over annotated subtitles in mpv"> <img src="docs-site/public/screenshots/yomitan-lookup.png" width="800" alt="Yomitan dictionary popup over annotated subtitles in mpv">
@@ -33,7 +33,7 @@ Yomitan runs inside the overlay. Trigger a lookup on any word for full dictionar
### Instant Anki Mining ### Instant Anki Mining
Create an Anki card with the sentence, audio clip, screenshot, and machine translation from the exact playback moment with one key press, click, or controller input. Create an Anki card from the exact playback moment with one key press, click, or controller input. SubMiner fills in the sentence, an audio clip, and a screenshot or animated image.
<div align="center"> <div align="center">
<img src="docs-site/public/screenshots/one-key-mining.png" width="800" alt="Anki card created from SubMiner with sentence, audio, and screenshot"> <img src="docs-site/public/screenshots/one-key-mining.png" width="800" alt="Anki card created from SubMiner with sentence, audio, and screenshot">
@@ -43,7 +43,7 @@ Create an Anki card with the sentence, audio clip, screenshot, and machine trans
### Reading Annotations ### 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. Subtitles are annotated as they play with frequency highlighting, JLPT tags, N+1 targeting, and character names from a generated dictionary. Particles and grammar-only tokens stay plain so the words worth learning stand out.
<div align="center"> <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"> <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 ### Immersion Dashboard
Local stats dashboard — watch time, anime library, vocabulary growth, mining throughput, session history, and trends. All stored locally, no third-party tracking. A stats dashboard tracks watch time, vocabulary growth, mining throughput, session history, and trends. Everything stays on your machine, with no third-party tracking.
<div align="center"> <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"> <img src="docs-site/public/screenshots/stats-overview.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
@@ -61,22 +61,12 @@ Local stats dashboard — watch time, anime library, vocabulary growth, mining t
<br> <br>
### Playlist Browser
Browse sibling episode files and the active mpv queue in one overlay modal. Open it with `Ctrl+Alt+P` to append episodes from the current directory, jump to queued items, remove entries, or reorder the playlist without leaving playback.
<div align="center">
<img src="docs-site/public/screenshots/playlist-browser.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
</div>
<br>
### Integrations ### Integrations
<table> <table>
<tr> <tr>
<td><b>YouTube</b></td> <td><b>YouTube</b></td>
<td>Auto-loaded yt-dlp subtitle tracks at startup with config-driven primary/secondary language priorities and a manual overlay picker on demand (<code>Ctrl+Alt+C</code>)</td> <td>Play YouTube URLs with subtitle tracks picked by your language priorities, or choose tracks yourself in the overlay picker (<code>Ctrl+Alt+C</code>). Requires <code>yt-dlp</code></td>
</tr> </tr>
<tr> <tr>
<td><b>AniList</b></td> <td><b>AniList</b></td>
@@ -84,91 +74,93 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
</tr> </tr>
<tr> <tr>
<td><b>Jellyfin</b></td> <td><b>Jellyfin</b></td>
<td>Browse, launch, and cast media from your Jellyfin server with setup and discovery controls in the app tray</td> <td>Browse your Jellyfin library, or cast to SubMiner from any Jellyfin client. Setup and discovery live in the tray menu</td>
</tr> </tr>
<tr> <tr>
<td><b>Jimaku</b></td> <td><b>Jimaku</b></td>
<td>Search and download Japanese subtitles</td> <td>Search and download Japanese subtitles. Requires a free Jimaku API key</td>
</tr>
<tr>
<td><b>TsukiHime</b></td>
<td>Search and download subtitles extracted from anime releases, with Japanese and secondary-language tabs (<code>Ctrl+Shift+T</code>). Requires <code>xz</code> on your <code>PATH</code></td>
</tr>
<tr>
<td><b>Subtitle generation</b></td>
<td>Transcribe a video's audio into Japanese subtitles locally from the generation modal (<code>Ctrl+Shift+G</code>), the subtitle sidebar, or the launcher. SubMiner can download models for you; optional Silero speech detection helps focus on dialogue. Requires whisper.cpp and FFmpeg. <a href="https://docs.subminer.moe/subtitle-generation">Setup guide</a></td>
</tr>
<tr>
<td><b>AniSkip</b></td>
<td>Automatic intro detection with chapter markers and a one-key skip (<code>TAB</code> by default)</td>
</tr> </tr>
<tr> <tr>
<td><b>alass / ffsubsync</b></td> <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>Retime a subtitle against the audio or another subtitle track (<code>Ctrl+Alt+S</code>). Requires <code>alass</code> or <code>ffsubsync</code>; set <code>subsync.alass_path</code> or <code>subsync.ffsubsync_path</code> if they are not in <code>/usr/bin</code></td>
</tr> </tr>
<tr> <tr>
<td><b>WebSocket</b></td> <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> </tr>
</table> </table>
<div align="center">
<img src="docs-site/public/screenshots/texthooker.png" width="800" alt="Texthooker page receiving annotated subtitle lines via WebSocket">
</div>
<br> <br>
--- ---
## Requirements ## Requirements
| | Required | Recommended | Optional | SubMiner runs on Linux, macOS 11+, and Windows 10+. Only **mpv** is required to run it (plus `fuse2` for the Linux AppImage). Mining cards also needs Anki with the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on. Everything else is optional.
| -------------- | --------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — | — |
| **Processing** | — | `ffmpeg` (audio clips & screenshots) | `mecab` + `mecab-ipadic` (annotation POS filtering), `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
| **Media** | — | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
| **Selection** | — | — | `fzf` / `rofi` |
> [!TIP] | Dependency | Status | What it does |
> `ffmpeg` is not strictly required to run SubMiner, but without it audio clips and screenshots will not be attached to Anki cards. Most users will want it installed. | ------------------------ | ---------------- | -------------------------------------------------------------------- |
| mpv | Required | The video player SubMiner draws over |
> [!NOTE] | fuse2 | Required (Linux) | Running the AppImage |
> [`bun`](https://bun.sh) is required if building from source or using the CLI wrapper: `subminer`. Pre-built releases (AppImage, DMG, installer) do not require it. | Anki + AnkiConnect | Required to mine | Card creation from the Yomitan popup |
| ffmpeg | Recommended | Audio clips and screenshots on cards |
**Platform-specific:** | MeCab + mecab-ipadic | Recommended | More accurate N+1, JLPT, and frequency highlighting |
| xdotool + xwininfo | Required (X11) | Window tracking on desktops other than Hyprland or Sway |
| Linux | macOS | Windows | | yt-dlp | Optional | YouTube playback |
| ------------------------------------------------------------ | ------------------------ | ------------- | | xz | Optional | TsukiHime subtitle downloads (most Linux distros already have it) |
| Hyprland (`hyprctl`) · X11/Xwayland (`xdotool` + `xwininfo`) | Accessibility permission | No extra deps | | alass / ffsubsync | Optional | Subtitle sync |
| whisper.cpp | Optional | [Subtitle generation](https://docs.subminer.moe/subtitle-generation) |
> [!NOTE] | guessit | Optional | Better title, season, and episode detection for AniSkip and AniList |
> **Wayland support is compositor-specific.** Wayland has no universal API for window positioning and each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Hyprland is the only native Wayland backend supported currenlty. All other Linux compositors require both mpv and SubMiner to run under X11 or Xwayland. The launcher detects your compositor and configures this automatically. | fzf / rofi | Optional | Video picker in the `subminer` launcher (rofi is Linux only) |
| chafa, ffmpegthumbnailer | Optional | Thumbnail previews in the launcher pickers |
<details> <details>
<summary><b>Arch Linux</b></summary> <summary><b>Platform-specific install commands</b></summary>
**Arch Linux:**
```bash ```bash
paru -S --needed mpv ffmpeg sudo pacman -S --needed mpv ffmpeg
# Optional paru -S --needed mecab-git mecab-ipadic # MeCab is only in the AUR
paru -S --needed mecab-git mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo
# Optional: subtitle sync (install at least one for subtitle syncing to work)
paru -S --needed alass python-ffsubsync
# X11 / Xwayland (required for non-Hyprland compositors)
paru -S --needed xdotool xorg-xwininfo
``` ```
</details> On desktops other than Hyprland or Sway, also install `xdotool` and `xorg-xwininfo`.
<details> **macOS:**
<summary><b>macOS</b></summary>
```bash ```bash
brew install mpv ffmpeg brew install mpv ffmpeg mecab mecab-ipadic
# Optional
brew install mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
# Optional: subtitle sync (install at least one for subtitle syncing to work)
brew install alass
pip install ffsubsync
``` ```
Grant Accessibility permission to SubMiner in **System Settings > Privacy & Security > Accessibility**. **Windows:**
</details> ```powershell
winget install shinchiro.mpv
winget install Gyan.FFmpeg
```
<details> Then reopen your terminal and check `mpv --version` and `ffmpeg -version`. ffmpeg must be on `PATH`; mpv does not have to be. If `mpv` is not found, either add its folder (usually `%LOCALAPPDATA%\Programs\mpv`) to `PATH` or enter the full path to `mpv.exe` during first-run setup.
<summary><b>Windows</b></summary>
Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on your `PATH`. [Scoop](https://scoop.sh) is the alternative if you want one package manager for everything. It is the only one that also carries `xz`:
Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for additional metadata enrichment. ```powershell
scoop bucket add extras
scoop install extras/mpv main/ffmpeg main/yt-dlp main/xz
```
See the [installation guide](https://docs.subminer.moe/installation#_1-install-requirements) for Ubuntu, Debian, and Fedora commands and the full optional package lists.
</details> </details>
@@ -176,7 +168,7 @@ Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download
## Quick Start ## Quick Start
### 1. Install ### 1. Install SubMiner
<details> <details>
<summary><b>Arch Linux (AUR)</b></summary> <summary><b>Arch Linux (AUR)</b></summary>
@@ -185,11 +177,7 @@ Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download
paru -S subminer-bin paru -S subminer-bin
``` ```
Or manually: Includes the AppImage and the `subminer` command.
```bash
git clone https://aur.archlinux.org/subminer-bin.git && cd subminer-bin && makepkg -si
```
</details> </details>
@@ -200,40 +188,25 @@ git clone https://aur.archlinux.org/subminer-bin.git && cd subminer-bin && makep
mkdir -p ~/.local/bin mkdir -p ~/.local/bin
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage \ wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage \
&& chmod +x ~/.local/bin/SubMiner.AppImage && chmod +x ~/.local/bin/SubMiner.AppImage
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer \
&& chmod +x ~/.local/bin/subminer
``` ```
> [!NOTE] The AppImage is all you need. First-run setup can install the optional `subminer` command, which runs on a copy of Bun bundled with the app, so you do not need Bun installed.
> The `subminer` wrapper uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
</details> </details>
<details> <details>
<summary><b>macOS</b></summary> <summary><b>macOS (DMG)</b></summary>
Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`. Download the latest DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
Also download the `subminer` launcher (recommended): Then enable SubMiner under **System Settings > Privacy & Security > Accessibility**, or the overlay cannot follow the mpv window. If macOS blocks the first launch, right-click the app and choose **Open**.
```bash
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer \
&& sudo chmod +x /usr/local/bin/subminer
```
> [!NOTE]
> The `subminer` launcher uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
</details> </details>
<details> <details>
<summary><b>Windows</b></summary> <summary><b>Windows</b></summary>
Download the latest installer or portable `.zip` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`. Download and run the latest installer (`SubMiner-<version>.exe`) from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). A portable `.zip` is also available.
**Windows support is experimental.** Core features such as mining, annotations, and dictionary lookups work, but some functionality may be missing or unstable. Bug reports welcome.
**Note:** On Windows the `subminer` launcher requires [`bun`](https://bun.sh) and must be invoked with `bun run subminer` instead of running the script directly. The recommended alternative is the **SubMiner mpv** shortcut created during first-run setup — double-click it, drag files onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See the [Windows mpv Shortcut](https://docs.subminer.moe/usage#windows-mpv-shortcut) section for details.
</details> </details>
@@ -244,39 +217,32 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
</details> </details>
### 2. First Launch ### 2. Launch & Set Up
Start SubMiner and the setup window opens on first launch. It creates your config file, imports Yomitan dictionaries (you need at least one for lookups), and can install the `subminer` command. On Windows it also creates a **SubMiner mpv** shortcut.
```bash ```bash
subminer app --setup # launch the first-run setup wizard subminer app --setup # AUR
~/.local/bin/SubMiner.AppImage --setup # AppImage
``` ```
SubMiner creates a default config, starts in the system tray, and opens a setup popup that walks you through installing the mpv plugin and configuring Yomitan dictionaries. Follow the on-screen steps to complete setup. On **macOS**, open `SubMiner.app` from `/Applications`. On **Windows**, run SubMiner from the Start menu. To reopen setup later, run `subminer app --setup`.
Jellyfin setup is available from the tray or `subminer jellyfin`; once Jellyfin is enabled with a server URL, the tray can toggle Jellyfin Discovery for the current app session. For card creation, keep Anki open with AnkiConnect installed. SubMiner connects to it at its default address with no extra setup.
> [!NOTE] ### 3. Mine
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
### 3. Verify Setup
```bash ```bash
subminer doctor # verify mpv, ffmpeg, config, and socket subminer video.mkv # play a video with SubMiner
subminer /path/to/dir # pick a file with fzf
subminer -R /path/to/dir # pick a file with rofi (Linux only)
subminer -H # watch history: replay, next, or previous episode
subminer doctor # check your setup
``` ```
> [!NOTE] On **Windows**, double-click the **SubMiner mpv** shortcut or drag a video file onto it.
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly — first-run setup will guide you through dependency checks.
### 4. Mine Starting mpv some other way? See [Launching mpv yourself](https://docs.subminer.moe/installation#launching-mpv-yourself) for the IPC socket option the overlay needs.
```bash
subminer video.mkv # play video with overlay
subminer --start video.mkv # explicit overlay start
subminer stats # open immersion dashboard
subminer stats -b # stats daemon in background
subminer stats -s # stop background stats daemon
```
On **Windows**, the `subminer` script must be run with `bun run subminer` (e.g. `bun run subminer video.mkv`). The recommended alternative is the **SubMiner mpv** shortcut (created during setup) or `SubMiner.exe --launch-mpv`. Drag a video file onto the shortcut to play it, or double-click it to open mpv with SubMiner's defaults.
## Documentation ## Documentation
@@ -290,9 +256,11 @@ SubMiner builds on the work of these open-source projects:
| Project | Role | | 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 | | [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 | | [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 | | [Bee's Character Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) | Character name recognition in subtitles |
| [Bun](https://github.com/oven-sh/bun) | Bundled runtime for the `subminer` command-line launcher |
| [GameSentenceMiner](https://github.com/bpwhelan/GameSentenceMiner) | Inspiration for Electron overlay with Yomitan integration | | [GameSentenceMiner](https://github.com/bpwhelan/GameSentenceMiner) | Inspiration for Electron overlay with Yomitan integration |
| [jellyfin-mpv-shim](https://github.com/jellyfin/jellyfin-mpv-shim) | Jellyfin integration | | [jellyfin-mpv-shim](https://github.com/jellyfin/jellyfin-mpv-shim) | Jellyfin integration |
| [Jimaku.cc](https://jimaku.cc) | Japanese subtitle search and downloads | | [Jimaku.cc](https://jimaku.cc) | Japanese subtitle search and downloads |
@@ -302,4 +270,6 @@ SubMiner builds on the work of these open-source projects:
## License ## License
[GNU General Public License v3.0](LICENSE) SubMiner is released under the [GNU General Public License v3.0](LICENSE).
Release packages also bundle an unmodified copy of [Bun](https://github.com/oven-sh/bun), which is MIT licensed and statically links JavaScriptCore (LGPL 2.0) and TinyCC (LGPL 2.1). Its license texts and third-party notices ship inside the app under `resources/bun/licenses`, and each release publishes `bun-v1.3.5-source.tar.gz` with the corresponding source. See [Bundled Bun runtime](https://docs.subminer.moe/installation#bundled-bun-runtime).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

@@ -0,0 +1,4 @@
[Thumbnailer Entry]
TryExec=ffmpegthumbnailer
Exec=ffmpegthumbnailer -i %i -o %o -s %s -f
MimeType=video/matroska;video/matroska-3d;video/x-matroska;video/x-matroska-3d;
+31
View File
@@ -0,0 +1,31 @@
{
"schemaVersion": 1,
"version": "1.3.5",
"bunRevision": "1e86cebd74a5723e818b5c0555276b646bcf0e4c",
"releaseTagCommit": "fa5a5bbe556a4bda5bde77b4013aa6c3bb4ec9ab",
"artifacts": {
"darwin-arm64": {
"file": "bun-darwin-aarch64.zip",
"sha256": "db17588a4aea8804856825d4bead3f05e1f37276ca606f37e369b4f72f35d3fb"
},
"darwin-x64": {
"file": "bun-darwin-x64-baseline.zip",
"sha256": "34b9a56b851058dafa1bc9d61233f2c383aa996889bba30b3180f5ccc2cff1b2"
},
"linux-arm64": {
"file": "bun-linux-aarch64.zip",
"sha256": "ed01000f85bd97785228ad2845dc92a1860b8054856826d7317690ac8f8ee74b"
},
"linux-x64": {
"file": "bun-linux-x64-baseline.zip",
"sha256": "6bddacd6a65855698b9816f2d74871eda4dd0b7fa921140c6445248f94a742fd"
},
"win32-x64": {
"file": "bun-windows-x64-baseline.zip",
"sha256": "bf447dcc3b06aba9b9706a9db46fcd65e06a4d47d31439922313825d06eb47ca"
}
},
"licenseInventoryStatus": "source-and-notices-pinned-to-binary-revision",
"sourceManifest": "build/bun-source-manifest.json",
"correspondingSourceAsset": "bun-v1.3.5-source.tar.gz"
}
+148
View File
@@ -0,0 +1,148 @@
{
"schemaVersion": 1,
"version": "1.3.5",
"releaseTag": "bun-v1.3.5",
"releaseTagCommit": "fa5a5bbe556a4bda5bde77b4013aa6c3bb4ec9ab",
"bunRevision": "1e86cebd74a5723e818b5c0555276b646bcf0e4c",
"archiveName": "bun-v1.3.5-source.tar.gz",
"sources": [
{
"name": "bun",
"repository": "oven-sh/bun",
"revision": "1e86cebd74a5723e818b5c0555276b646bcf0e4c",
"destination": "bun",
"sha256": "3a766087902a62a4920e5ac5452dbc7143ff97ea19721f945146629fc9f2d82c",
"licensePaths": ["LICENSE.md"]
},
{
"name": "WebKit",
"repository": "oven-sh/WebKit",
"revision": "6d0f3aac0b817cc01a846b3754b21271adedac12",
"destination": "bun/vendor/WebKit",
"transport": "git-sparse",
"exclude": ["JSTests", "LayoutTests", "ManualTests", "PerformanceTests", "WebDriverTests"],
"licensePaths": ["Source/JavaScriptCore/COPYING.LIB"]
},
{
"name": "boringssl",
"repository": "oven-sh/boringssl",
"revision": "f1ffd9e83d4f5c28a9c70d73f9a4e6fcf310062f",
"destination": "bun/vendor/boringssl",
"sha256": "af8fd325793bb261c70114d09a24f36092fbe252be8ec040f0054ec9455f0ea3",
"licensePaths": ["LICENSE"]
},
{
"name": "brotli",
"repository": "google/brotli",
"revision": "ed738e842d2fbdf2d6459e39267a633c4a9b2f5d",
"upstreamReference": "v1.1.0",
"destination": "bun/vendor/brotli",
"sha256": "aaa739962a45b508b2e783b915e6b2b57ed3b12bd4b0feac73acfb144dffa54f",
"licensePaths": ["LICENSE"]
},
{
"name": "cares",
"repository": "c-ares/c-ares",
"revision": "3ac47ee46edd8ea40370222f91613fc16c434853",
"destination": "bun/vendor/cares",
"sha256": "8c94116cb366ae4a44e487da4d9f7e736287d329efa6f88fdf077cd2d0a2e4b8",
"licensePaths": ["LICENSE.md"]
},
{
"name": "hdrhistogram",
"repository": "HdrHistogram/HdrHistogram_c",
"revision": "be60a9987ee48d0abf0d7b6a175bad8d6c1585d1",
"destination": "bun/vendor/hdrhistogram",
"sha256": "811c5e5ae5303a75ade50688880af6aad5d2f951ec5785f68186bd18635cdfc9",
"licensePaths": ["COPYING.txt", "LICENSE.txt"]
},
{
"name": "highway",
"repository": "google/highway",
"revision": "ac0d5d297b13ab1b89f48484fc7911082d76a93f",
"destination": "bun/vendor/highway",
"sha256": "a7a816f4b62a0414ff0d39c0a8875847468dd6f7c9ad71781a24469a756cdea7",
"licensePaths": ["LICENSE"]
},
{
"name": "libarchive",
"repository": "libarchive/libarchive",
"revision": "9525f90ca4bd14c7b335e2f8c84a4607b0af6bdf",
"destination": "bun/vendor/libarchive",
"sha256": "944db9ab58a3cbdb5d947db4f04a3cc15f83b3147fcf7830ada592ba8d4a102c",
"licensePaths": ["COPYING"]
},
{
"name": "libdeflate",
"repository": "ebiggers/libdeflate",
"revision": "c8c56a20f8f621e6a966b716b31f1dedab6a41e3",
"destination": "bun/vendor/libdeflate",
"sha256": "1e5cc06bdbf3e1245d8b89c9e3588f507e3c8bc53fe8b8229770a9e8661dea81",
"licensePaths": ["COPYING"]
},
{
"name": "libuv",
"repository": "libuv/libuv",
"revision": "f3ce527ea940d926c40878ba5de219640c362811",
"destination": "bun/vendor/libuv",
"sha256": "46040d51e8aa86a7c84e377e224d427c35022ec6e3ed8ed399493b0d8574d0cf",
"licensePaths": ["LICENSE"]
},
{
"name": "lolhtml",
"repository": "cloudflare/lol-html",
"revision": "d64457d9ff0143deef025d5df7e8586092b9afb7",
"destination": "bun/vendor/lolhtml",
"sha256": "893b77b460f4c4f4634c973f72cf35e38ebe30646d14a92db3f50ee7e586c0ad",
"licensePaths": ["LICENSE"]
},
{
"name": "lshpack",
"repository": "litespeedtech/ls-hpack",
"revision": "8905c024b6d052f083a3d11d0a169b3c2735c8a1",
"destination": "bun/vendor/lshpack",
"sha256": "07d8bf901bb1b15543f38eabd23938519e1210eebadb52f3d651d6ef130ef973",
"licensePaths": ["LICENSE"]
},
{
"name": "mimalloc",
"repository": "oven-sh/mimalloc",
"revision": "1beadf9651a7bfdec6b5367c380ecc3fe1c40d1a",
"destination": "bun/vendor/mimalloc",
"sha256": "317ec2a83462ece78c344c4955c6fee103b412a8e6a0d6ddf9ec8963ed9c0881",
"licensePaths": ["LICENSE"]
},
{
"name": "picohttpparser",
"repository": "h2o/picohttpparser",
"revision": "066d2b1e9ab820703db0837a7255d92d30f0c9f5",
"destination": "bun/vendor/picohttpparser",
"sha256": "637ff2ab6f5c7f7e05a5b5dc393d5cf2fea8d4754fcaceaaf935ffff5c1323ee",
"licensePaths": ["picohttpparser.c", "picohttpparser.h"]
},
{
"name": "tinycc",
"repository": "oven-sh/tinycc",
"revision": "29985a3b59898861442fa3b43f663fc1af2591d7",
"destination": "bun/vendor/tinycc",
"sha256": "813cc09aafd6cea9c1ae6b745781e6b27576a0c103f49b63b5c8a732bbe3d290",
"licensePaths": ["COPYING"]
},
{
"name": "zlib",
"repository": "cloudflare/zlib",
"revision": "886098f3f339617b4243b286f5ed364b9989e245",
"destination": "bun/vendor/zlib",
"sha256": "14bf449df8308696af52f87de88f54c9eb9c91ec5e280587f9d37f7a3f6ed9bb",
"licensePaths": ["LICENSE"]
},
{
"name": "zstd",
"repository": "facebook/zstd",
"revision": "f8745da6ff1ad1e7bab384bd1f9d742439278e99",
"destination": "bun/vendor/zstd",
"sha256": "4b0bd1f0cfb25e61b9103c35f27395530ff5b4c0d2513a00fd745849e85ea52c",
"licensePaths": ["COPYING", "LICENSE"]
}
]
}
+223 -215
View File
@@ -7,57 +7,69 @@
"dependencies": { "dependencies": {
"@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist": "^5.2.8",
"@fontsource-variable/geist-mono": "^5.2.7", "@fontsource-variable/geist-mono": "^5.2.7",
"@xhayper/discord-rpc": "^1.3.3", "@xhayper/discord-rpc": "^1.3.4",
"axios": "^1.13.5", "axios": "^1.20.0",
"commander": "^14.0.3", "commander": "^14.0.3",
"hono": "^4.12.7", "electron-updater": "^6.8.9",
"hono": "^4.12.28",
"jsonc-parser": "^3.3.1", "jsonc-parser": "^3.3.1",
"koffi": "^2.15.6", "koffi": "^2.15.6",
"libsql": "^0.5.22", "libsql": "^0.5.22",
"ws": "^8.19.0", "ws": "^8.21.0",
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^25.3.0", "@electron/asar": "3.4.1",
"@types/node": "^24.10.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"electron": "39.8.6", "electron": "43.7.7",
"electron-builder": "26.8.2", "electron-builder": "26.16.1",
"esbuild": "^0.25.12", "esbuild": "^0.25.12",
"eslint": "^10.8.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"undici": "7.29.1",
}, },
}, },
}, },
"patchedDependencies": {
"@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch",
},
"overrides": { "overrides": {
"@xmldom/xmldom": "0.8.12", "@xmldom/xmldom": "0.8.15",
"app-builder-lib": "26.8.2", "app-builder-lib": "26.16.1",
"electron-builder-squirrel-windows": "26.8.2", "brace-expansion": "5.0.11",
"electron-builder-squirrel-windows": "26.16.1",
"fast-uri": "3.1.7",
"form-data": "4.0.6",
"ip-address": "10.2.0",
"js-yaml": "4.3.2",
"lodash": "4.18.0", "lodash": "4.18.0",
"minimatch": "10.2.3", "minimatch": "10.2.5",
"picomatch": "4.0.4", "picomatch": "4.0.4",
"tar": "7.5.11", "tar": "7.5.21",
"tmp": "0.2.7",
"undici": "7.29.1",
}, },
"packages": { "packages": {
"7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="],
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
"@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], "@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"@discordjs/rest": ["@discordjs/rest@2.6.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg=="], "@discordjs/rest": ["@discordjs/rest@2.6.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.27.0" } }, "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg=="],
"@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
"@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.4", "", {}, "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg=="],
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
"@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/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=="], "@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=="],
"@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="],
"@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="], "@electron/rebuild": ["@electron/rebuild@4.2.0", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ=="],
"@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], "@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="],
@@ -115,11 +127,33 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@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.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
"@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.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
"@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="], "@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=="], "@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.2.7", "", {}, "sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA=="],
"@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=="], "@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/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
@@ -147,11 +181,15 @@
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
"@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
"@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="],
"@peculiar/webcrypto": ["@peculiar/webcrypto@1.7.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "tslib": "^2.8.1", "webcrypto-core": "^1.9.2" } }, "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ=="],
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
@@ -165,53 +203,51 @@
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@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/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/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/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/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=="],
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
"@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
"@xhayper/discord-rpc": ["@xhayper/discord-rpc@1.3.3", "", { "dependencies": { "@discordjs/rest": "^2.6.1", "@vladfrangu/async_event_emitter": "^2.4.7", "discord-api-types": "^0.38.42", "ws": "^8.20.0" } }, "sha512-Ih48GHiua7TtZgKO+f0uZPhCeQqb84fY2qUys/oMh8UbUfiUkUJLVCmd/v2AK0/pV33euh0aqSXo7+9LiPSwGw=="], "@xhayper/discord-rpc": ["@xhayper/discord-rpc@1.3.4", "", { "dependencies": { "@discordjs/rest": "^2.6.1", "@vladfrangu/async_event_emitter": "^2.4.7", "discord-api-types": "^0.38.47", "ws": "^8.20.0" } }, "sha512-ff0uEXuibh9wi+l4vOj7xInLUjtlTaQBje/SCyQkeXZ0j2V0y+Zge5PQIQFRHH9TjjGaYJkTofEcQhncM2q7/w=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.8.15", "", {}, "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA=="],
"abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "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@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"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=="], "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=="],
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], "app-builder-lib": ["app-builder-lib@26.16.1", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.4", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^1.8.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.16.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.16.1", "electron-builder-squirrel-windows": "26.16.1" } }, "sha512-FhaO6YOup01ZfQW0Z6gt3AyukJjv1gW4uFK47jTgwcHZKqyN/fSlK2LqPf9tAeZYLP2bRJLDzeOkRImsw2X4Pg=="],
"app-builder-lib": ["app-builder-lib@26.8.2", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.2", "electron-builder-squirrel-windows": "26.8.2" } }, "sha512-z3ptLzJwNl35fyR0wxv4qWOfZuU36VysYHnbs8PDtf8S0QzIl2OWimdDFVmCxYMkIV1k/RT9CeTgcP7oUznFOw=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], "asn1js": ["asn1js@3.0.10", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.5", "tslib": "^2.8.1" } }, "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg=="],
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
@@ -221,29 +257,27 @@
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
"axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
"axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "brace-expansion": ["brace-expansion@5.0.11", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-awigjhi6cLTh90bdw6+QJ9CtmJmyYhEIi70iCbc8Rozn04Fw9FeQIBjv/E22FFGuCGx1bLJyUfB64x/szUSXUg=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="], "builder-util": ["builder-util@26.16.0", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-RLyJhB7Si3YkzKR9ubQslWuXW3Vhs3CGe1i+SeixBZ0qTd1mk3XBmssvY22TlB6CS5blyko8Gu1JzpYk8UkYAg=="],
"builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], "builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
"cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="], "bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="],
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
@@ -259,16 +293,8 @@
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
"cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -283,8 +309,6 @@
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
"crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="],
"cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -293,7 +317,7 @@
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
@@ -309,11 +333,9 @@
"dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="],
"discord-api-types": ["discord-api-types@0.38.43", "", {}, "sha512-sSoBf/nK6m7BGtw65mi+QBuvEWaHE8MMziFLqWL+gT6ME/BLg34dRSVKS3Husx40uU06bvxUc3/X+D9Y6/zAbw=="], "discord-api-types": ["discord-api-types@0.38.49", "", {}, "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg=="],
"dmg-builder": ["dmg-builder@26.8.2", "", { "dependencies": { "app-builder-lib": "26.8.2", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-DaWI+p4DOqiFVZFMovdGYammBOyJAiHHFWUTQ0Z7gNc0twfdIN0LvyJ+vFsgZEDR1fjgbpCj690IVtbYIsZObQ=="], "dmg-builder": ["dmg-builder@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-pnI/3Qb24Uk+rMTgIUrsVUKosVgwmBUdF8Zeb8TexOSbpq8MWc7v6l+n+FrEqVkjNZwzBN+XpDS9ENgZ/rkWAw=="],
"dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
@@ -321,27 +343,27 @@
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], "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@43.7.7", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-y0pg+KhWLAkW2Ts2njkFFNFl3KtB1FmbUp9Te9i+R2Og2v1fcI/I6MADJ7jvnzB5UKdt+U+0EUoCnVL79xMPMA=="],
"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=="], "electron-builder": ["electron-builder@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.16.1", "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-LrLK65QX5PUYYODXqp23FKrV7CILTtVY7mrJckNknO9jLNSMiqFkKbSMiDRw4CjOADMPVDdWLxY4mezOZWswxg=="],
"electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.2", "", { "dependencies": { "app-builder-lib": "26.8.2", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-kXhajX6DzdIQcTlctVTKoG1oO39JhWcTG0lH7ZEJ4FzPaKJy7KFNfNJUd5BoEmLjv5GlrRZpEOYnniD+LcwNJA=="], "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "electron-winstaller": "5.4.0" } }, "sha512-w0y44wSaT1l6R7CAGmeHn4nHPfvzDyCAU1xJyi1w9SbPYJpYn76SmHDzqHf8Y7l91cPWTdPYBpGQtB2T5mJ08A=="],
"electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], "electron-publish": ["electron-publish@26.16.0", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-Vt3KzQIiw9BImvNOYtndg9Mjki+tl4+1sQiC/+G5j8khWaENOJFWodiB+sUl6yyHwtd37avehskdtPw7f8y/+Q=="],
"electron-updater": ["electron-updater@6.8.9", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig=="],
"electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "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=="], "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
@@ -361,32 +383,50 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.8.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.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@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.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ=="],
"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=="], "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=="],
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "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=="], "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -401,6 +441,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": ["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=="], "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=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
@@ -419,25 +461,21 @@
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="], "hono": ["hono@4.12.28", "", {}, "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA=="],
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], "http-cache-semantics": ["http-cache-semantics@4.3.0", "", {}, "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
@@ -445,30 +483,30 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"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-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "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=="], "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=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
@@ -483,11 +521,17 @@
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], "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=="], "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": ["lodash@4.18.0", "", {}, "sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA=="],
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
"lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
@@ -495,8 +539,6 @@
"magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="],
"make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="],
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -507,43 +549,31 @@
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimatch": ["minimatch@10.2.3", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="],
"minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="],
"minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="],
"minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
"minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="], "node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="],
"node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
"node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="],
"node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], "node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="],
"nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
"nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
"normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
@@ -551,39 +581,39 @@
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"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-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-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"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-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pkijs": ["pkijs@3.4.0", "", { "dependencies": { "@noble/hashes": "1.4.0", "asn1js": "^3.0.6", "bytestreamjs": "^2.0.1", "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw=="],
"plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
"postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], "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=="], "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=="], "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
@@ -591,43 +621,45 @@
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
"read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="],
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], "sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="],
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
@@ -641,39 +673,25 @@
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
"slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="],
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tar": ["tar@7.5.11", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ=="], "tar": ["tar@7.5.21", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA=="],
"temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="],
@@ -681,9 +699,11 @@
"tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="],
"tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="],
"tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
@@ -691,39 +711,37 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "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=="], "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=="], "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.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="],
"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=="],
"unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"unzipper": ["unzipper@0.12.5", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "11.3.1", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], "webcrypto-core": ["webcrypto-core@1.9.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q=="],
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"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=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"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=="], "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=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
@@ -735,109 +753,99 @@
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@discordjs/rest/discord-api-types": ["discord-api-types@0.38.43", "", {}, "sha512-sSoBf/nK6m7BGtw65mi+QBuvEWaHE8MMziFLqWL+gT6ME/BLg34dRSVKS3Husx40uU06bvxUc3/X+D9Y6/zAbw=="],
"@discordjs/util/discord-api-types": ["discord-api-types@0.38.43", "", {}, "sha512-sSoBf/nK6m7BGtw65mi+QBuvEWaHE8MMziFLqWL+gT6ME/BLg34dRSVKS3Husx40uU06bvxUc3/X+D9Y6/zAbw=="],
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], "@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/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/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/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=="], "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
"@electron/rebuild/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@electron/universal/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=="], "@electron/universal/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=="],
"@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=="], "@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=="],
"@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=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"@malept/flatpak-bundler/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=="], "@malept/flatpak-bundler/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=="],
"@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=="],
"@npmcli/fs/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@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/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=="],
"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/@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/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "builder-util/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "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=="], "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=="], "es-set-tostringtag/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"global-agent/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "get-intrinsic/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-flush/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=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
"node-abi/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"node-api-version/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"node-gyp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
"simple-update-notifier/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "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=="], "unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="],
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], "@types/cacheable-request/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@types/fs-extra/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@types/keyv/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "@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=="],
"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/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=="], "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"app-builder-lib/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"builder-util/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], "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=="], "node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
+11
View File
@@ -31,14 +31,25 @@ Rules:
- `README.md` is ignored by the generator - `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 - 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: 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. - 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.
- `CHANGELOG.md`, GitHub release notes, and prerelease notes all use one item per feature or area, inline by default: `- **Name**: one or two short sentences.` Items with several separate changes may use a few short nested bullets instead of a run-on sentence. Items say what changed and how to reach it; implementation details, edge cases, and step-by-step usage belong in the docs, not the release body.
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely. - `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. - 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 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` - 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`
- from the second prerelease of a base version onward, the notes also open with a `## Changes since <previous tag>` section generated from the fragment diff against the previous beta/RC tag; keep fragment edits meaningful. Editorial-only rewording is filtered out of that section, while genuinely changed behavior and deleted fragments (reverted changes) are reported
- 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` - 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 - the final stable release is the point where `bun run changelog:build` consumes fragments into the stable changelog and release notes
+282 -131
View File
@@ -7,10 +7,37 @@
{ {
// ========================================== // ==========================================
// Overlay Auto-Start // Subtitle Selection
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles. // Select primary and secondary mpv subtitle tracks from the overlay.
// Hot-reload: enabling or disabling updates the session shortcut immediately.
// ========================================== // ==========================================
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false "subtitleSelection": {
"enabled": false // Use the SubMiner modal to select primary and secondary subtitle tracks. When enabled, its shortcut overrides mpv subtitle selection. Values: true | false
}, // Select primary and secondary mpv subtitle tracks from the overlay.
// ==========================================
// Japanese Subtitle Generation
// Generate timed Japanese subtitles from local audio using whisper.cpp.
// Configure an existing GGML model path or explicitly download a SubMiner-managed model.
// Hot-reload: settings apply to the next generation or model download.
// ==========================================
"subtitleGeneration": {
"whisperPath": "", // Optional path override for whisper.cpp. Leave empty to find whisper-cli on PATH.
"modelPath": "", // Path to an existing multilingual whisper.cpp GGML model. Leave empty to use a SubMiner-managed model. A configured path always takes precedence.
"managedModel": "small", // Multilingual whisper.cpp model to use when modelPath is empty. Download it explicitly from the generation modal or launcher. Values: tiny | tiny-q5_1 | tiny-q8_0 | base | base-q5_1 | base-q8_0 | small | small-q5_1 | small-q8_0 | medium | medium-q5_0 | medium-q8_0 | large-v1 | large-v2 | large-v2-q5_0 | large-v2-q8_0 | large-v3 | large-v3-q5_0 | large-v3-turbo | large-v3-turbo-q5_0 | large-v3-turbo-q8_0
"threads": 4, // Positive integer CPU thread count for whisper.cpp Japanese transcription.
"ffmpegPath": "", // Optional FFmpeg path override for audio extraction. Leave empty to find ffmpeg on PATH.
"ffprobePath": "", // Optional FFprobe path override for audio tracks and timing. Leave empty to find ffprobe on PATH.
"vadModelPath": "", // Path to a whisper.cpp Silero VAD model. Enables dialogue-focused generation while retaining uncertain audible sections, which may include songs. Leave empty to transcribe the full audio.
"vadPath": "" // Optional speech detector executable override. With vadModelPath configured, leave empty to find whisper-vad-speech-segments or vad-speech-segments on PATH.
}, // Generate timed Japanese subtitles from local audio using whisper.cpp.
// ==========================================
// Visible Overlay Auto-Start
// Show the visible subtitle overlay automatically after managed mpv playback starts SubMiner.
// SubMiner can still auto-start in the background when this is false.
// ==========================================
"auto_start_overlay": true, // Show the visible subtitle overlay automatically when the bundled mpv plugin starts SubMiner. Values: true | false
// ========================================== // ==========================================
// Texthooker Server // Texthooker Server
@@ -18,7 +45,7 @@
// ========================================== // ==========================================
"texthooker": { "texthooker": {
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false "launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
"openBrowser": false // Open browser setting. Values: true | false "openBrowser": false // Open the texthooker page in the default browser when the server starts. Values: true | false
}, // Configure texthooker startup launch and browser opening behavior. }, // Configure texthooker startup launch and browser opening behavior.
// ========================================== // ==========================================
@@ -45,9 +72,16 @@
// Logging // Logging
// Controls logging verbosity. // Controls logging verbosity.
// Set to debug for full runtime diagnostics. // Set to debug for full runtime diagnostics.
// Hot-reload: logging.level and logging.files apply live while SubMiner is running.
// ========================================== // ==========================================
"logging": { "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. }, // Controls logging verbosity.
// ========================================== // ==========================================
@@ -81,7 +115,7 @@
"rightStickPress": 10, // Raw button index used for controller R3 input. "rightStickPress": 10, // Raw button index used for controller R3 input.
"leftTrigger": 6, // Raw button index used for controller L2 input. "leftTrigger": 6, // Raw button index used for controller L2 input.
"rightTrigger": 7 // Raw button index used for controller R2 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": { "bindings": {
"toggleLookup": { "toggleLookup": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis "kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
@@ -138,7 +172,8 @@
"axisIndex": 4, // Raw axis index captured for this analog controller action. "axisIndex": 4, // Raw axis index captured for this analog controller action.
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical "dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually. } // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
} // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction. }, // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
"profiles": {} // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
}, // Gamepad support for the visible overlay while keyboard-only mode is active. }, // Gamepad support for the visible overlay while keyboard-only mode is active.
// ========================================== // ==========================================
@@ -152,33 +187,59 @@
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false "mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
"yomitanExtension": true, // Warm up Yomitan extension 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 "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. }, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
// ==========================================
// Updates
// Automatic update check behavior.
// Manual checks from the tray or launcher are always allowed.
// ==========================================
"updates": {
"enabled": true, // Run automatic update checks in the background. Values: true | false
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
"notificationType": "overlay", // 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 // Keyboard Shortcuts
// Overlay keyboard shortcuts. Set a shortcut to null to disable. // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// Hot-reload: shortcut changes apply live and update the session help modal on reopen. // Hot-reload: shortcut changes apply live and update the session help modal on reopen.
// ========================================== // ==========================================
"shortcuts": { "shortcuts": {
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting. "toggleVisibleOverlayGlobal": "Alt+Shift+O", // Global accelerator that toggles overlay visibility from anywhere on the system. Use null to disable.
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting. "copySubtitle": "CommandOrControl+C", // Accelerator that copies the current subtitle line to the clipboard.
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting. "copySubtitleMultiple": "CommandOrControl+Shift+C", // Accelerator that copies consecutive subtitle lines while the multi-copy window stays open.
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting. "updateLastCardFromClipboard": "CommandOrControl+V", // Accelerator that updates the last mined Anki card using the current clipboard contents.
"triggerFieldGrouping": "CommandOrControl+G", // Trigger field grouping setting. "triggerFieldGrouping": "CommandOrControl+G", // Accelerator that triggers Kiku field grouping on duplicate cards.
"triggerSubsync": "Ctrl+Alt+S", // Trigger subsync setting. "triggerSubsync": "Ctrl+Alt+S", // Accelerator that triggers subsync against the active subtitle file.
"mineSentence": "CommandOrControl+S", // Mine sentence setting. "mineSentence": "CommandOrControl+S", // Accelerator that mines the current sentence as a new Anki card.
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Mine sentence multiple setting. "mineSentenceMultiple": "CommandOrControl+Shift+S", // Accelerator that mines consecutive sentences while the multi-mine window stays open.
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes. "multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting. "toggleSecondarySub": "CommandOrControl+Shift+V", // Accelerator that toggles the secondary subtitle bar visibility.
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting. "markAudioCard": "CommandOrControl+Shift+A", // Accelerator that marks the last mined card as an audio card.
"openCharacterDictionary": "CommandOrControl+Alt+A", // Open character dictionary setting. "openCharacterDictionaryManager": "CommandOrControl+D", // Accelerator that opens the character dictionary manager modal.
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting. "openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting. "openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
"openSessionHelp": "CommandOrControl+Slash", // Open session help setting. "openTsukihime": "Ctrl+Shift+T", // Accelerator that opens the TsukiHime subtitle search modal (configured secondary/Japanese primary tabs).
"openControllerSelect": "Alt+C", // Open controller select setting. "openSubtitleSelection": "g-s", // Open subtitle selection when enabled. Use g-s to press g then s. Set null to unbind.
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting. "openSubtitleGeneration": "Ctrl+Shift+G", // Accelerator that opens the standalone Japanese subtitle generation modal.
"toggleSubtitleSidebar": "Backslash" // Toggle subtitle sidebar setting. "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.
"toggleNotificationHistory": "CommandOrControl+N", // Accelerator that toggles the overlay notification history panel.
"appendClipboardVideoToQueue": "CommandOrControl+A" // Accelerator that appends a video path from the clipboard to the mpv playlist.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable. }, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// ========================================== // ==========================================
@@ -259,15 +320,41 @@
] // Command setting. ] // Command setting.
}, },
{ {
"key": "Shift+BracketRight", // Key setting. "key": "Ctrl+Shift+ArrowLeft", // Key setting.
"command": [ "command": [
"__sub-delay-next-line" "sub-step",
-1
] // Command setting. ] // Command setting.
}, },
{ {
"key": "Shift+BracketLeft", // Key setting. "key": "Ctrl+Shift+ArrowRight", // Key setting.
"command": [ "command": [
"__sub-delay-prev-line" "sub-step",
1
] // Command setting.
},
{
"key": "KeyZ", // Key setting.
"command": [
"add",
"sub-delay",
-0.1
] // Command setting.
},
{
"key": "Shift+KeyZ", // Key setting.
"command": [
"add",
"sub-delay",
0.1
] // Command setting.
},
{
"key": "KeyX", // Key setting.
"command": [
"add",
"sub-delay",
0.1
] // Command setting. ] // Command setting.
}, },
{ {
@@ -315,20 +402,20 @@
// Hot-reload: defaultMode updates live while SubMiner is running. // Hot-reload: defaultMode updates live while SubMiner is running.
// ========================================== // ==========================================
"secondarySub": { "secondarySub": {
"secondarySubLanguages": [], // Secondary sub languages setting. "secondarySubLanguages": [], // Language code priority list used to auto-select a secondary subtitle track when available.
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false "autoLoadSecondarySub": false, // Automatically load a matching secondary subtitle when the primary subtitle loads. Values: true | false
"defaultMode": "hover" // Default mode setting. "defaultMode": "hover" // Default visibility mode for the secondary subtitle bar. Values: hidden | visible | hover
}, // Dual subtitle track options. }, // Dual subtitle track options.
// ========================================== // ==========================================
// Auto Subtitle Sync // Subtitle Sync
// Subsync engine and executable paths. // Subsync engine and executable paths.
// Hot-reload: subsync changes apply to the next subtitle sync run.
// ========================================== // ==========================================
"subsync": { "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.
"alass_path": "", // Alass path setting. "ffsubsync_path": "", // Optional absolute path to the ffsubsync binary used by subsync. Leave empty to auto-discover from PATH.
"ffsubsync_path": "", // Ffsubsync path setting. "ffmpeg_path": "", // Optional absolute path to the ffmpeg binary used by subsync. Leave empty to auto-discover from PATH.
"ffmpeg_path": "", // Ffmpeg path setting.
"replace": true // Replace the active subtitle file when sync completes. Values: true | false "replace": true // Replace the active subtitle file when sync completes. Values: true | false
}, // Subsync engine and executable paths. }, // Subsync engine and executable paths.
@@ -337,7 +424,7 @@
// Initial vertical subtitle position from the bottom. // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
"subtitlePosition": { "subtitlePosition": {
"yPercent": 10 // Y percent setting. "yPercent": 10 // Vertical position of the subtitle overlay expressed as a percentage from the bottom of the screen.
}, // Initial vertical subtitle position from the bottom. }, // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
@@ -347,29 +434,39 @@
// ========================================== // ==========================================
"subtitleStyle": { "subtitleStyle": {
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover "primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"color": "#cad3f5", // Color setting.
"background-color": "transparent", // Background color setting.
"font-size": "35px", // Font size setting.
"font-weight": "600", // Font weight setting.
"font-style": "normal", // Font style setting.
"line-height": "1.35", // Line height setting.
"letter-spacing": "-0.01em", // Letter spacing setting.
"word-spacing": "0", // Word spacing setting.
"font-kerning": "normal", // Font kerning setting.
"text-rendering": "geometricPrecision", // Text rendering setting.
"text-shadow": "-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.
}, // CSS declaration object applied to primary subtitles after normal subtitle style defaults.
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false "enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false "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 "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 "autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv. "primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv. "nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false "nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary. "nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
"fontFamily": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting. "nPlusOneColor": "#c6a0f6", // Color used for the single N+1 target token subtitle highlight.
"fontSize": 35, // Font size setting. "knownWordColor": "#a6da95", // Color used for known-word subtitle highlights.
"fontColor": "#cad3f5", // Font color setting. "knownWordMaturityColors": {
"fontWeight": "600", // Font weight setting. "new": "#ee99a0", // Color for known words whose Anki cards are new (never reviewed), when maturity highlighting is enabled.
"lineHeight": 1.35, // Line height setting. "learning": "#b7bdf8", // Color for known words whose Anki cards are in (re)learning, when maturity highlighting is enabled.
"letterSpacing": "-0.01em", // Letter spacing setting. "young": "#91d7e3", // Color for known words whose Anki cards are in review below the mature threshold, when maturity highlighting is enabled.
"wordSpacing": 0, // Word spacing setting. "mature": "#a6da95" // Color for known words whose Anki cards are at or above the mature interval threshold, when maturity highlighting is enabled.
"fontKerning": "normal", // Font kerning setting. }, // Known word maturity colors setting.
"textRendering": "geometricPrecision", // Text rendering setting.
"textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"fontStyle": "normal", // Font style setting.
"backgroundColor": "transparent", // Background color setting.
"backdropFilter": "blur(6px)", // Backdrop filter setting.
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
"knownWordColor": "#a6da95", // Known word color setting.
"jlptColors": { "jlptColors": {
"N1": "#ed8796", // N1 setting. "N1": "#ed8796", // N1 setting.
"N2": "#f5a97f", // N2 setting. "N2": "#f5a97f", // N2 setting.
@@ -380,7 +477,7 @@
"frequencyDictionary": { "frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false "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. "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 "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 "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`. "singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
@@ -393,19 +490,21 @@
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX). ] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
}, // Frequency dictionary setting. }, // Frequency dictionary setting.
"secondary": { "secondary": {
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting. "css": {
"fontSize": 24, // Font size setting. "font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"fontColor": "#cad3f5", // Font color setting. "color": "#cad3f5", // Color setting.
"lineHeight": 1.35, // Line height setting. "background-color": "transparent", // Background color setting.
"letterSpacing": "-0.01em", // Letter spacing setting. "font-size": "24px", // Font size setting.
"wordSpacing": 0, // Word spacing setting. "font-weight": "600", // Font weight setting.
"fontKerning": "normal", // Font kerning setting. "font-style": "normal", // Font style setting.
"textRendering": "geometricPrecision", // Text rendering setting. "line-height": "1.35", // Line height setting.
"textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting. "letter-spacing": "-0.01em", // Letter spacing setting.
"backgroundColor": "transparent", // Background color setting. "word-spacing": "0", // Word spacing setting.
"backdropFilter": "blur(6px)", // Backdrop filter setting. "font-kerning": "normal", // Font kerning setting.
"fontWeight": "600", // Font weight setting. "text-rendering": "geometricPrecision", // Text rendering setting.
"fontStyle": "normal" // Font style 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. } // Secondary setting.
}, // Primary and secondary subtitle styling. }, // Primary and secondary subtitle styling.
@@ -419,18 +518,20 @@
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false "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 "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. "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 "autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
"maxWidth": 420, // Maximum sidebar width in CSS pixels. "css": {
"opacity": 0.95, // Base opacity applied to the sidebar shell. "font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"backgroundColor": "rgba(73, 77, 100, 0.9)", // Background color for the subtitle sidebar shell. "color": "#cad3f5", // Color setting.
"textColor": "#cad3f5", // Default cue text color in the subtitle sidebar. "background-color": "rgba(73, 77, 100, 0.9)", // Background color setting.
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif", // Font family used for subtitle sidebar cue text. "font-size": "16px", // Font size setting.
"fontSize": 16, // Base font size for subtitle sidebar cue text in CSS pixels. "opacity": "0.95", // Opacity setting.
"timestampColor": "#a5adcb", // Timestamp color in the subtitle sidebar. "--subtitle-sidebar-max-width": "420px", // Subtitle sidebar max width setting.
"activeLineColor": "#f5bde6", // Text color for the active subtitle cue. "--subtitle-sidebar-timestamp-color": "#a5adcb", // Subtitle sidebar timestamp color setting.
"activeLineBackgroundColor": "rgba(138, 173, 244, 0.22)", // Background color for the active subtitle cue. "--subtitle-sidebar-active-line-color": "#f5bde6", // Subtitle sidebar active line color setting.
"hoverLineBackgroundColor": "rgba(54, 58, 79, 0.84)" // Background color for hovered subtitle cues. "--subtitle-sidebar-active-background-color": "rgba(138, 173, 244, 0.22)", // Subtitle sidebar active background color setting.
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)" // Subtitle sidebar hover background color setting.
} // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key. }, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
// ========================================== // ==========================================
@@ -441,22 +542,22 @@
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false "enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider. "apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key. "apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
"model": "openai/gpt-4o-mini", // Model setting. "model": "openai/gpt-4o-mini", // Default model identifier requested from the shared AI provider.
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider. "baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting. "systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // Default system prompt sent with shared AI provider requests.
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests. "requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing. }, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
// ========================================== // ==========================================
// AnkiConnect Integration // AnkiConnect Integration
// Automatic Anki updates and media generation options. // Automatic Anki updates and media generation options.
// Hot-reload: ankiConnect.ai.enabled updates live while SubMiner is running. // Hot-reload: ankiConnect.ai.enabled, media.normalizeAudio/mirrorMpvVolume/reviewTiming, knownWords, nPlusOne, fields.word/audio/wordAudio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, isKiku.fieldGrouping, isSenren.fieldGrouping, and lapisKiku.wordCardKind update live while SubMiner is running.
// Shared AI provider transport settings are read from top-level ai and typically require restart. // Shared AI provider transport settings are read from top-level ai and typically require restart.
// Most other AnkiConnect settings still require restart. // Most other AnkiConnect settings still require restart.
// ========================================== // ==========================================
"ankiConnect": { "ankiConnect": {
"enabled": true, // Enable AnkiConnect integration. Values: true | false "enabled": true, // Enable AnkiConnect integration. Values: true | false
"url": "http://127.0.0.1:8765", // Url setting. "url": "http://127.0.0.1:8765", // Base URL of the AnkiConnect HTTP server.
"pollingRate": 3000, // Polling interval in milliseconds. "pollingRate": 3000, // Polling interval in milliseconds.
"proxy": { "proxy": {
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false "enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
@@ -467,13 +568,15 @@
"tags": [ "tags": [
"SubMiner" "SubMiner"
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging. ], // 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": { "fields": {
"word": "Expression", // Card field for the mined word or expression text. "word": "Expression", // Card field for the mined word or expression text.
"audio": "ExpressionAudio", // Audio setting. "audio": "ExpressionAudio", // Card field that receives generated sentence audio.
"image": "Picture", // Image setting. "wordAudio": "ExpressionAudio", // Existing word-audio field read to time the frozen first frame of animated images. This mapping is only used for synchronization.
"sentence": "Sentence", // Sentence setting. "image": "Picture", // Card field that receives the captured screenshot or animated image.
"miscInfo": "MiscInfo", // Misc info setting. "sentence": "Sentence", // Card field that receives the source sentence text.
"translation": "SelectionText" // Translation setting. "miscInfo": "MiscInfo", // Card field that receives the miscellaneous info pattern (see ankiConnect.metadata.pattern).
"translation": "SelectionText" // Card field that receives the current selection or translated text.
}, // Fields setting. }, // Fields setting.
"ai": { "ai": {
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false "enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
@@ -481,72 +584,116 @@
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows. "systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
}, // Ai setting. }, // Ai setting.
"media": { "media": {
"generateAudio": true, // Generate audio setting. Values: true | false "generateAudio": true, // Generate sentence audio for mined cards. Values: true | false
"generateImage": true, // Generate image setting. Values: true | false "generateImage": true, // Generate screenshot or animated image for mined cards. Values: true | false
"imageType": "static", // Image type setting. "imageType": "static", // Image capture type: "static" for a single still frame, "avif" for an animated AVIF. Values: static | avif
"imageFormat": "jpg", // Image format setting. "imageFormat": "jpg", // Encoding format used when imageType is "static". Values: jpg | png | webp
"imageQuality": 92, // Image quality setting. "imageQuality": 92, // Quality (0-100) used for lossy static image encoders.
"animatedFps": 10, // Animated fps setting. "imageMaxWidth": 0, // Maximum width for static images, in pixels. Set to 0 to preserve the source resolution.
"animatedMaxWidth": 640, // Animated max width setting. "imageMaxHeight": 0, // Maximum height for static images, in pixels. Set to 0 to preserve the source resolution.
"animatedCrf": 35, // Animated crf setting. "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 "syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
"audioPadding": 0.5, // Audio padding setting. "normalizeAudio": true, // Normalize generated sentence audio loudness during media extraction. Changes apply live. Values: true | false
"fallbackDuration": 3, // Fallback duration setting. "mirrorMpvVolume": true, // Apply mpv's current software volume curve to generated sentence audio. Changes apply live. Values: true | false
"maxMediaDuration": 30 // Max media duration setting. "reviewTiming": false, // Review and preview subtitle media timing before SubMiner creates or enriches a mined card. Values: true | false
"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. 0 disables the cap.
}, // Media setting. }, // Media setting.
"knownWords": { "knownWords": {
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false "highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
"maturityEnabled": false, // Color known-word highlights by Anki card maturity (new, learning, young, mature) instead of a single color. Requires known-word highlighting. Values: true | false
"matureThresholdDays": 21, // Card interval in days at which a known word counts as mature (Anki convention: 21).
"refreshMinutes": 1440, // Minutes between known-word cache refreshes. "refreshMinutes": 1440, // Minutes between known-word cache refreshes.
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false "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 "matchMode": "headword", // Known-word matching strategy for subtitle annotations. Cache matches always receive known-word highlighting even when POS filters suppress other annotation types. Values: headword | surface
"decks": {}, // Decks and fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word", "Word Reading"] }. "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"] }. Reading fields (Reading, Word Reading, ExpressionReading) are always probed so cached words match only in the reading their note teaches; words from notes without readings match in any reading.
"color": "#a6da95" // Color used for known-word highlights.
}, // Known words setting. }, // Known words setting.
"behavior": { "behavior": {
"overwriteAudio": true, // Overwrite audio setting. Values: true | false "overwriteAudio": true, // When updating an existing card, overwrite the audio field instead of skipping it. Values: true | false
"overwriteImage": true, // Overwrite image setting. Values: true | false "overwriteImage": true, // When updating an existing card, overwrite the image field instead of skipping it. Values: true | false
"mediaInsertMode": "append", // Media insert mode setting. "mediaInsertMode": "append", // Whether new media is appended after or prepended before existing field contents on update. Values: append | prepend
"highlightWord": true, // Highlight word setting. Values: true | false "highlightWord": true, // Bold the mined word inside the sentence field on the saved Anki card. Values: true | false
"notificationType": "osd", // Notification type setting. "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 "autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
}, // Behavior setting. }, // Behavior setting.
"nPlusOne": { "nPlusOne": {
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3). "enabled": false, // Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Requires known-word cache data. Values: true | false
"nPlusOne": "#c6a0f6" // Color used for the single N+1 target token highlight. "minSentenceWords": 3 // Minimum sentence word count required for N+1 targeting (default: 3).
}, // N plus one setting. }, // N plus one setting.
"metadata": { "metadata": {
"pattern": "[SubMiner] %f (%t)" // Pattern setting. "pattern": "[SubMiner] %f (%t)" // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
}, // Metadata setting. }, // Metadata setting.
"isLapis": { "isLapis": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enable Lapis-specific mining behaviors and sentence card model targeting. Values: true | false
"sentenceCardModel": "Japanese sentences" // Sentence card model setting. "sentenceCardModel": "Lapis" // Note type name used by Lapis sentence cards.
}, // Is lapis setting. }, // Is lapis setting.
"isKiku": { "isKiku": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enable Kiku-specific mining behaviors (duplicate handling, field grouping). Values: true | false
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled "fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false "deleteDuplicateInAuto": true // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
} // Is kiku setting. }, // Is kiku setting.
"isSenren": {
"enabled": false, // Enable Senren-specific duplicate handling (scene-switching field grouping, including miscInfo grouping). Mutually exclusive with isKiku.enabled. Values: true | false
"fieldGrouping": "auto", // Senren duplicate-card field grouping mode (scene switching). Values: auto | manual | disabled
"deleteDuplicateInAuto": true // When Senren field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
}, // Is senren setting.
"lapisKiku": {
"wordCardKind": "word-and-sentence" // Card-type flag SubMiner marks on Kiku/Lapis word cards. Only one flag is set at a time; the others are cleared. Requires isKiku.enabled or isLapis.enabled. Values: word-and-sentence | click | sentence | audio | none
} // Lapis kiku setting.
}, // Automatic Anki updates and media generation options. }, // Automatic Anki updates and media generation options.
// ========================================== // ==========================================
// Jimaku // Jimaku
// Jimaku API configuration and defaults. // Jimaku API configuration and defaults.
// Hot-reload: Jimaku changes apply to the next Jimaku request.
// ========================================== // ==========================================
"jimaku": { "jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Api base url setting. "apiBaseUrl": "https://jimaku.cc", // Base URL of the Jimaku subtitle search API.
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none "languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
"maxEntryResults": 10 // Maximum Jimaku search results returned. "maxEntryResults": 10 // Maximum Jimaku search results returned.
}, // Jimaku API configuration and defaults. }, // Jimaku API configuration and defaults.
// ==========================================
// TsukiHime
// TsukiHime subtitle search configuration for Japanese primary and configured secondary subtitles. No API key required.
// Hot-reload: TsukiHime changes apply to the next TsukiHime request.
// ==========================================
"tsukihime": {
"apiBaseUrl": "https://api.tsukihime.org/v1", // Base URL of the TsukiHime API (Animetosho successor). No API key required.
"maxSearchResults": 10 // Maximum TsukiHime search results returned.
}, // TsukiHime subtitle search configuration for Japanese primary and configured secondary subtitles. No API key required.
// ==========================================
// TMDB
// TMDB (The Movie Database) metadata for live-action dramas and movies in the stats Library: posters, synopses, and grouping by show.
// Hot-reload: TMDB changes apply to the next TMDB request.
// ==========================================
"tmdb": {
"apiKey": "", // Your own TMDB API key or read access token for live-action posters and synopses in the stats Library. Release builds bundle a project key, so set this only to use your own quota or when running from source (free under Settings > API on themoviedb.org).
"apiKeyCommand": "" // Shell command that prints the TMDB API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
}, // TMDB (The Movie Database) metadata for live-action dramas and movies in the stats Library: posters, synopses, and grouping by show.
// ========================================== // ==========================================
// YouTube Playback Settings // YouTube Playback Settings
// Defaults for managed subtitle language preferences and YouTube subtitle loading. // Defaults for managed subtitle language preferences and YouTube subtitle loading.
// Hot-reload: primarySubLanguages and subtitleSource apply to the next YouTube subtitle load.
// ========================================== // ==========================================
"youtube": { "youtube": {
"primarySubLanguages": [ "primarySubLanguages": [
"ja", "ja",
"jpn" "jpn"
] // Comma-separated primary subtitle language priority for managed subtitle auto-selection. ], // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
"subtitleSource": "youtube", // Where primary YouTube subtitles come from. Whisper transcribes the audio locally using the subtitleGeneration settings. Values: youtube | whisper
"mediaCache": {
"mode": "direct", // How YouTube card audio/images are extracted. Values: direct | background
"maxHeight": 720 // Maximum video height downloaded for the YouTube background media cache. Set to 0 for unlimited.
} // Media cache setting.
}, // Defaults for managed subtitle language preferences and YouTube subtitle loading. }, // Defaults for managed subtitle language preferences and YouTube subtitle loading.
// ========================================== // ==========================================
@@ -559,11 +706,8 @@
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false "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. "accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
"characterDictionary": { "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. "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 character dictionary settings updates. Values: all | active
"profileScope": "all", // Yomitan profile scope for dictionary enable/disable updates. Values: all | active
"collapsibleSections": { "collapsibleSections": {
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false "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 "characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
@@ -585,14 +729,25 @@
// ========================================== // ==========================================
// MPV Launcher // MPV Launcher
// Optional mpv.exe override for Windows playback entry points. // SubMiner-managed mpv launch and bundled plugin options.
// Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin.
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback. // Set mpv.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. // Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
// ========================================== // ==========================================
"mpv": { "mpv": {
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH. "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 "launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
}, // Optional mpv.exe override for Windows playback entry points. "profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
"backend": "auto", // Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform. Values: auto | hyprland | sway | x11 | macos | windows
"autoStartSubMiner": true, // Start SubMiner in the background when SubMiner-managed mpv loads a file. Values: true | false
"pauseUntilOverlayReady": true, // Pause mpv on visible-overlay auto-start until SubMiner signals subtitle tokenization readiness. Values: true | false
"subminerBinaryPath": "", // Optional SubMiner app binary path passed to the bundled mpv plugin. Leave empty to use the launcher-detected app path.
"aniskipEnabled": true, // Enable AniSkip intro detection, 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.
// ========================================== // ==========================================
// Jellyfin // Jellyfin
@@ -605,14 +760,10 @@
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096). "serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
"recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup. "recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup.
"username": "", // Default Jellyfin username used during CLI login. "username": "", // Default Jellyfin username used during CLI login.
"deviceId": "subminer", // Device id setting.
"clientName": "SubMiner", // Client name setting.
"clientVersion": "0.1.0", // Client version setting.
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing. "defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false "remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. 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 "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 "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. "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 "directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
@@ -635,7 +786,7 @@
// ========================================== // ==========================================
"discordPresence": { "discordPresence": {
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false "enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal". "presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal". Values: default | meme | japanese | minimal
"updateIntervalMs": 3000, // Minimum interval between presence payload updates. "updateIntervalMs": 3000, // Minimum interval between presence payload updates.
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates. "debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
}, // Optional Discord Rich Presence activity card updates for current playback/study session. }, // Optional Discord Rich Presence activity card updates for current playback/study session.
+242 -65
View File
@@ -1,86 +1,192 @@
const DOCS_HOSTNAME = 'https://docs.subminer.moe'; import { spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import type { DefaultTheme, HeadConfig, TransformContext, UserConfig } from 'vitepress';
function pageToCanonicalHref(page: string): string | null { const DOCS_HOSTNAME = 'https://docs.subminer.moe';
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
const PLAUSIBLE_ENDPOINT = `${PLAUSIBLE_PROXY_HOSTNAME}/api/event`;
const PLAUSIBLE_INIT_SCRIPT = [
'window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};',
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
].join('\n');
type DocsChannel = 'stable-root' | 'stable-archive' | 'main';
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();
// The tracked `docs-site/` checkout, which stays a git working tree even when
// `docsSourceDir` points at an untracked release snapshot. Used for git lookups only.
const repoDocsDir = optionalEnv(process.env.SUBMINER_DOCS_REPO_DIR) ?? process.cwd();
const channel = normalizeChannel(optionalEnv(process.env.SUBMINER_DOCS_CHANNEL));
const docsVersion = optionalEnv(process.env.SUBMINER_DOCS_VERSION);
function normalizeBase(value: string): string {
if (!value || value === '/') return '/';
return `/${value.replace(/^\/+|\/+$/g, '')}/`;
}
function normalizeChannel(value: string | undefined): DocsChannel {
if (value === 'main' || value === 'stable-archive') return value;
return 'stable-root';
}
function withDocsBase(path: string): string {
if (/^[a-z]+:\/\//i.test(path)) return path;
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
if (base === '/') return normalizedPath;
return `${base.replace(/\/$/, '')}${normalizedPath}`;
}
function pageToRoute(page: string): string | null {
if (page === '404.md') return null; if (page === '404.md') return null;
const route = page const route = page
.replace(/(^|\/)index\.md$/, '') .replace(/(^|\/)index\.md$/, '')
.replace(/\.md$/, '') .replace(/\.md$/, '')
.replace(/\/$/, ''); .replace(/\/$/, '');
return route ? `${DOCS_HOSTNAME}/${route}` : `${DOCS_HOSTNAME}/`; return route ? `/${route}` : '/';
} }
export default { // Only the root channel is indexable. `main` and every /v/<version>/ archive are
title: 'SubMiner Docs', // near-verbatim copies of it, so they own their URL via a self-referential canonical
description: // and are excluded from the index instead of being consolidated onto root. Uniform
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.', // self-canonical plus noindex avoids mixing noindex with a cross-page canonical,
head: [ // which Google treats as a conflicting signal.
['link', { rel: 'icon', href: '/favicon.ico', sizes: 'any' }], const isIndexableChannel = channel === 'stable-root';
[
'link', function pageToCanonicalHref(page: string): string | null {
{ const route = pageToRoute(page);
rel: 'icon', if (!route) return null;
type: 'image/png',
href: '/favicon-32x32.png', if (!isIndexableChannel) {
sizes: '32x32', return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
}, }
],
[ return route === '/' ? `${DOCS_HOSTNAME}/` : `${DOCS_HOSTNAME}${route}`;
'link', }
{
rel: 'icon', function canonicalRouteWithBase(route: string): string {
type: 'image/png', const routeWithBase = withDocsBase(route);
href: '/favicon-16x16.png', return route === '/' ? routeWithBase : routeWithBase.replace(/\/$/, '');
sizes: '16x16', }
},
], function transformPageHead({ page }: TransformContext): HeadConfig[] {
[ const href = pageToCanonicalHref(page);
'link', const head: HeadConfig[] = href ? [['link', { rel: 'canonical', href }]] : [];
{
rel: 'apple-touch-icon', // Crawlable so links still pass through, but out of the index: ~30 archived copies
href: '/apple-touch-icon.png', // of every page otherwise soak up the crawl budget the current docs need.
sizes: '180x180', if (!isIndexableChannel) {
}, head.push(['meta', { name: 'robots', content: 'noindex,follow' }]);
], }
],
appearance: 'dark', return head;
cleanUrls: true, }
metaChunk: true,
sitemap: { function linkToPagePath(link: string): string | null {
if (!link.startsWith('/') || link.startsWith('/v/') || link.startsWith('/main/')) {
return null;
}
const withoutHash = link.split('#')[0] ?? '/';
const withoutQuery = withoutHash.split('?')[0] ?? '/';
const route = withoutQuery.replace(/^\/+|\/+$/g, '');
return route ? `${route}.md` : 'index.md';
}
function hasPageForLink(link: string): boolean {
const pagePath = linkToPagePath(link);
if (!pagePath) return true;
return existsSync(join(docsSourceDir, pagePath));
}
function filterNav(items: DefaultTheme.NavItem[]): DefaultTheme.NavItem[] {
return items
.map((item) => {
if ('items' in item && item.items) {
return { ...item, items: filterNav(item.items as DefaultTheme.NavItem[]) };
}
if ('link' in item && item.link && !hasPageForLink(item.link)) {
return null;
}
return item;
})
.filter((item): item is DefaultTheme.NavItem => Boolean(item));
}
function filterSidebar(items: DefaultTheme.SidebarItem[]): DefaultTheme.SidebarItem[] {
return items
.map((item) => {
const filteredChildren = item.items ? filterSidebar(item.items) : undefined;
if (item.link && !hasPageForLink(item.link)) return null;
if (item.items && filteredChildren?.length === 0 && !item.link) return null;
return { ...item, items: filteredChildren };
})
.filter((item): item is DefaultTheme.SidebarItem => Boolean(item));
}
// Version navigation targets other builds (root, `/main/`, `/versions`), so it links to
// production by absolute URL: base-relative links would stay inside this build, and
// `target: '_self'` makes the VitePress router do a full page load. The list is
// deliberately static; the full release list lives on the root-only `/versions` page
// so frozen `/v/<version>/` archives never need a rebuild when a new tag ships.
const versionItems: DefaultTheme.NavItemWithLink[] = [
{ text: 'Latest stable', link: `${DOCS_HOSTNAME}/` },
{ text: 'main', link: `${DOCS_HOSTNAME}/main/` },
{ text: 'All versions', link: `${DOCS_HOSTNAME}/versions` },
].map((item) => ({ ...item, target: '_self', noIcon: true }));
function sitemapUrlToPage(url: string): string {
const route = url.replace(/\.html$/, '').replace(/^\/+|\/+$/g, '');
return route ? `${route}.md` : 'index.md';
}
// VitePress derives <lastmod> by running `git log` inside its source dir. Production
// builds point that at an untracked snapshot of the release tag, so the lookup comes
// back empty and the sitemap ships with no dates at all. Resolve it from the tracked
// checkout at the ref being built instead.
function lastModifiedFor(url: string): string | undefined {
const ref = docsVersion && docsVersion !== 'main' ? docsVersion : 'HEAD';
const result = spawnSync('git', ['log', '-1', '--format=%cI', ref, '--', sitemapUrlToPage(url)], {
cwd: repoDocsDir,
encoding: 'utf8',
});
return (result.status === 0 && result.stdout.trim()) || undefined;
}
// Only the root channel publishes a sitemap. Archived and `main` builds would emit
// their own copies listing the same canonical URLs, which just advertises the
// duplicate trees we are trying to keep out of the index.
const sitemap: UserConfig['sitemap'] = isIndexableChannel
? {
hostname: DOCS_HOSTNAME, hostname: DOCS_HOSTNAME,
transformItems(items) { transformItems(items) {
return items.filter( return items
(item) => item.url !== 'README' && item.url !== `${DOCS_HOSTNAME}/README`, .filter((item) => item.url !== 'README' && item.url !== `${DOCS_HOSTNAME}/README`)
); .map((item) => ({ ...item, lastmod: item.lastmod ?? lastModifiedFor(item.url) }));
}, },
}, }
transformHead({ page }) { : undefined;
const href = pageToCanonicalHref(page);
return href ? [['link', { rel: 'canonical', href }]] : []; const nav: DefaultTheme.NavItem[] = [
},
lastUpdated: true,
srcExclude: ['subagents/**'],
markdown: {
theme: {
light: 'catppuccin-latte',
dark: 'catppuccin-macchiato',
},
},
themeConfig: {
logo: {
light: '/assets/SubMiner.png',
dark: '/assets/SubMiner.png',
},
siteTitle: 'SubMiner Docs',
nav: [
{ text: 'Home', link: '/' }, { text: 'Home', link: '/' },
{ text: 'Get Started', link: '/installation' }, { text: 'Get Started', link: '/installation' },
{ text: 'Mining', link: '/mining-workflow' }, { text: 'Mining', link: '/mining-workflow' },
{ text: 'Configuration', link: '/configuration' }, { text: 'Configuration', link: '/configuration' },
{ text: 'Changelog', link: '/changelog' }, { text: 'Changelog', link: '/changelog' },
{ text: 'Troubleshooting', link: '/troubleshooting' }, { text: 'Troubleshooting', link: '/troubleshooting' },
], { text: docsVersion ?? 'main', items: versionItems },
sidebar: [ ];
const sidebar: DefaultTheme.SidebarItem[] = [
{ {
text: 'Getting Started', text: 'Getting Started',
items: [ items: [
@@ -110,7 +216,10 @@ export default {
{ text: 'Jellyfin', link: '/jellyfin-integration' }, { text: 'Jellyfin', link: '/jellyfin-integration' },
{ text: 'YouTube', link: '/youtube-integration' }, { text: 'YouTube', link: '/youtube-integration' },
{ text: 'Jimaku', link: '/jimaku-integration' }, { text: 'Jimaku', link: '/jimaku-integration' },
{ text: 'Subtitle Generation', link: '/subtitle-generation' },
{ text: 'TsukiHime', link: '/tsukihime-integration' },
{ text: 'AniList', link: '/anilist-integration' }, { text: 'AniList', link: '/anilist-integration' },
{ text: 'AniSkip', link: '/aniskip-integration' },
{ text: 'Character Dictionary', link: '/character-dictionary' }, { text: 'Character Dictionary', link: '/character-dictionary' },
], ],
}, },
@@ -124,7 +233,73 @@ export default {
{ text: 'Changelog', link: '/changelog' }, { text: 'Changelog', link: '/changelog' },
], ],
}, },
];
const config: UserConfig = {
title: 'SubMiner Docs',
description:
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
base,
...(outDir ? { outDir } : {}),
head: [
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
[
'script',
{
async: '',
src: `${PLAUSIBLE_PROXY_HOSTNAME}${PLAUSIBLE_SITE_SCRIPT_PATH}`,
},
], ],
['script', {}, PLAUSIBLE_INIT_SCRIPT],
['link', { rel: 'icon', href: withDocsBase('/favicon.ico'), sizes: 'any' }],
[
'link',
{
rel: 'icon',
type: 'image/png',
href: withDocsBase('/favicon-32x32.png'),
sizes: '32x32',
},
],
[
'link',
{
rel: 'icon',
type: 'image/png',
href: withDocsBase('/favicon-16x16.png'),
sizes: '16x16',
},
],
[
'link',
{
rel: 'apple-touch-icon',
href: withDocsBase('/apple-touch-icon.png'),
sizes: '180x180',
},
],
],
appearance: 'dark',
cleanUrls: true,
metaChunk: true,
sitemap,
transformHead: transformPageHead,
lastUpdated: true,
srcExclude: ['subagents/**', 'README.md'],
markdown: {
theme: {
light: 'catppuccin-latte',
dark: 'catppuccin-macchiato',
},
},
themeConfig: {
logo: {
light: '/assets/SubMiner.png',
dark: '/assets/SubMiner.png',
},
siteTitle: 'SubMiner Docs',
nav: filterNav(nav),
sidebar: filterSidebar(sidebar),
search: { search: {
provider: 'local', provider: 'local',
}, },
@@ -143,3 +318,5 @@ export default {
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }], socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
}, },
}; };
export default config;
@@ -1,9 +1,10 @@
<script setup> <script setup>
import { useRoute, useData } from 'vitepress'; import { useRoute, useData } from 'vitepress';
import { computed } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { formatStatusLineDate, formatStatusLineFilePath } from '../status-line';
const route = useRoute(); const route = useRoute();
const { page, frontmatter } = useData(); const { frontmatter } = useData();
const mode = computed(() => { const mode = computed(() => {
const layout = frontmatter.value.layout; const layout = frontmatter.value.layout;
@@ -12,8 +13,7 @@ const mode = computed(() => {
}); });
const filePath = computed(() => { const filePath = computed(() => {
const path = route.path; return formatStatusLineFilePath(route.path);
return path === '/' ? 'index.md' : `${path.replace(/^\//, '')}.md`;
}); });
const section = computed(() => { const section = computed(() => {
@@ -23,10 +23,10 @@ const section = computed(() => {
return parts[0] || 'root'; return parts[0] || 'root';
}); });
const lastUpdated = computed(() => { // Set on the client only, so the prerendered HTML never bakes in the build date.
if (!page.value.lastUpdated) return ''; const today = ref('');
const date = new Date(page.value.lastUpdated); onMounted(() => {
return date.toISOString().slice(0, 10); today.value = formatStatusLineDate(new Date());
}); });
</script> </script>
@@ -40,8 +40,8 @@ const lastUpdated = computed(() => {
<div class="tui-statusline__right"> <div class="tui-statusline__right">
<span class="tui-statusline__section">{{ section }}</span> <span class="tui-statusline__section">{{ section }}</span>
<span class="tui-statusline__sep"></span> <span class="tui-statusline__sep"></span>
<span v-if="lastUpdated" class="tui-statusline__date">{{ lastUpdated }}</span> <span v-if="today" class="tui-statusline__date">{{ today }}</span>
<span v-if="lastUpdated" class="tui-statusline__sep"></span> <span v-if="today" class="tui-statusline__sep"></span>
<span class="tui-statusline__branch">GPL-3.0</span> <span class="tui-statusline__branch">GPL-3.0</span>
</div> </div>
</footer> </footer>
-28
View File
@@ -7,32 +7,7 @@ import './mermaid-modal.css';
import TuiLayout from './TuiLayout.vue'; import TuiLayout from './TuiLayout.vue';
let mermaidLoader: Promise<any> | null = null; let mermaidLoader: Promise<any> | null = null;
let plausibleTrackerInitialized = false;
const MERMAID_MODAL_ID = 'mermaid-diagram-modal'; const MERMAID_MODAL_ID = 'mermaid-diagram-modal';
const PLAUSIBLE_DOMAIN = 'subminer.moe';
const PLAUSIBLE_ENABLED_HOSTNAMES = new Set(['docs.subminer.moe']);
const PLAUSIBLE_ENDPOINT = 'https://worker.subminer.moe/api/capture';
async function initPlausibleTracker() {
if (typeof window === 'undefined' || plausibleTrackerInitialized) {
return;
}
if (!PLAUSIBLE_ENABLED_HOSTNAMES.has(window.location.hostname)) {
return;
}
const { init } = await import('@plausible-analytics/tracker');
init({
domain: PLAUSIBLE_DOMAIN,
endpoint: PLAUSIBLE_ENDPOINT,
outboundLinks: true,
fileDownloads: true,
formSubmissions: true,
captureOnLocalhost: false,
});
plausibleTrackerInitialized = true;
}
function closeMermaidModal() { function closeMermaidModal() {
if (typeof document === 'undefined') { if (typeof document === 'undefined') {
@@ -222,9 +197,6 @@ export default {
}; };
onMounted(() => { onMounted(() => {
initPlausibleTracker().catch((error) => {
console.error('Failed to initialize Plausible tracker:', error);
});
render(); render();
}); });
watch(() => route.path, render); watch(() => route.path, render);
@@ -0,0 +1,18 @@
import { expect, test } from 'bun:test';
import { formatStatusLineDate, formatStatusLineFilePath } from './status-line';
test('status line file path formats root home as index markdown', () => {
expect(formatStatusLineFilePath('/')).toBe('index.md');
});
test('status line file path formats version archive home without trailing slash', () => {
expect(formatStatusLineFilePath('/v/0.12.0/')).toBe('v/0.12.0.md');
});
test('status line file path keeps normal docs routes as markdown files', () => {
expect(formatStatusLineFilePath('/v/0.12.0/configuration')).toBe('v/0.12.0/configuration.md');
});
test('status line date uses the local calendar day, zero padded', () => {
expect(formatStatusLineDate(new Date(2026, 0, 5, 23, 59))).toBe('2026-01-05');
});
+11
View File
@@ -0,0 +1,11 @@
export function formatStatusLineFilePath(routePath: string): string {
if (routePath === '/') return 'index.md';
return `${routePath.replace(/^\/|\/$/g, '')}.md`;
}
// Local calendar date as YYYY-MM-DD (toISOString would give the UTC date).
export function formatStatusLineDate(date: Date): string {
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${date.getFullYear()}-${month}-${day}`;
}
+2 -2
View File
@@ -5,7 +5,7 @@
@font-face { @font-face {
font-family: 'M PLUS 1'; font-family: 'M PLUS 1';
src: url('/assets/fonts/Mplus1-Medium.ttf') format('truetype'); src: url('../../public/assets/fonts/Mplus1-Medium.ttf') format('truetype');
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -13,7 +13,7 @@
@font-face { @font-face {
font-family: 'Manrope Default'; font-family: 'Manrope Default';
src: url('/assets/fonts/manrope-latin-600-normal.ttf') format('truetype'); src: url('../../public/assets/fonts/manrope-latin-600-normal.ttf') format('truetype');
font-weight: 600; font-weight: 600;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
+30 -6
View File
@@ -1,4 +1,4 @@
# SubMiner Docs # SubMiner docs
In-repo VitePress documentation source for SubMiner. In-repo VitePress documentation source for SubMiner.
@@ -30,9 +30,33 @@ bun run docs:dev
## Cloudflare Pages ## Cloudflare Pages
- Git repo: `ksyasuda/SubMiner` - Git repo: `ksyasuda/SubMiner`
- Root directory: `docs-site` - Production branch: `main`
- Build command: `bun run docs:build` - Automatic production and preview deployments: disabled
- Build output directory: `.vitepress/dist` - Custom domain: `docs.subminer.moe` attached to Production
- Build watch paths: `docs-site/*` - Deployment path: GitHub Actions direct upload with Wrangler
Cloudflare Pages watch paths use a single `*` wildcard for monorepo subdirectories. `docs-site/*` matches nested files under the docs site; `docs-site/**` can cause docs-only pushes to be skipped. The public docs root is stable-only:
- `/` serves the latest stable release docs.
- `/main/` serves development docs from `main`.
- `/v/<version>/` serves stable release archives.
- `/versions` (root build only) lists every published version.
- Prerelease tags do not update the docs site.
Only `/` is indexable. `/main/` and every `/v/<version>/` page carries a self-referential canonical plus `noindex,follow`, repeated as an `X-Robots-Tag` header (by the generated `_headers` file for `/main/`, by the archive function for `/v/`). They stay crawlable so their links still resolve, but ~30 archived copies of every page would otherwise consume the crawl budget the current docs need. Only the root build emits `sitemap.xml`, and its `<lastmod>` dates come from `git log` against the tracked checkout at the released tag, because the build renders from an untracked snapshot that VitePress cannot date itself.
Keep Cloudflare Git auto-deploy disabled. The production deploy is `.github/workflows/docs-pages.yml`, which uploads `.tmp/docs-versioned-site` with `--branch main` so tag-triggered runs update Production instead of creating preview deployments.
### Stable archives in R2
`/v/<version>/` archives are not part of the Pages deployment. Each one is built once, uploaded to an R2 bucket under `v/<version>/`, and served by the Pages Function in `functions/v/[[path]].ts`. Each deploy builds only archives the bucket is missing (an archive counts as present once its `_archive.json` marker exists), plus the root and `/main/` builds. Archive nav links to `/versions` instead of listing releases, so a new tag never invalidates old archives.
To re-render archives on purpose (theme change, docs fix), run the `Docs Pages` workflow manually with `rebuild_archives` set to comma-separated tags or `all`.
One-time setup:
- R2 bucket for archives; its name goes in the `DOCS_ARCHIVE_R2_BUCKET` repository variable.
- R2 API token with Object Read & Write on that bucket; its S3 credentials go in the `DOCS_ARCHIVE_R2_ACCESS_KEY_ID` and `DOCS_ARCHIVE_R2_SECRET_ACCESS_KEY` repository secrets.
- Pages project: Settings > Bindings > R2 bucket, variable name `DOCS_ARCHIVES`, pointing at the same bucket.
The first deploy after setup builds and uploads every stable archive; later deploys only add new tags.
+39 -99
View File
@@ -1,128 +1,68 @@
# AniList Integration # AniList integration
SubMiner can sync your watch progress to [AniList](https://anilist.co) automatically. When you finish an episode, SubMiner detects the title and episode number from the filename, finds the matching AniList entry, and updates your progress via the GraphQL API. Failed updates are retried with exponential backoff in the background. SubMiner updates your [AniList](https://anilist.co) watch progress when you finish an episode. The same connection supplies cover art for the stats dashboard and names for the [character dictionary](/character-dictionary).
AniList data also powers two additional features: [cover art](#cover-art) for the stats dashboard and the [Character Dictionary](/character-dictionary) for in-overlay name lookup.
## Setup ## Setup
AniList integration is opt-in. To enable it: 1. Set `anilist.enabled` to `true`:
1. Set `anilist.enabled` to `true` in your config. ```jsonc
2. Leave `anilist.accessToken` empty and restart SubMiner (or run `--anilist-setup`). {
3. Approve access in the AniList authorization page.
4. The callback returns to SubMiner via the `subminer://anilist-setup?...` protocol URL, and SubMiner stores the token automatically.
```jsonc
{
"anilist": { "anilist": {
"enabled": true, "enabled": true,
"accessToken": "" },
} }
} ```
```
The access token is encrypted at rest using Electron's `safeStorage` API. On Linux this defaults to `gnome-libsecret`; override the backend with `--password-store=<backend>` (for example `--password-store=basic_text`). 2. Restart SubMiner. With no token stored, it opens the AniList setup window. You can also open it from the tray (**Configure AniList**) or with `subminer app --anilist-setup`.
3. Approve access on the AniList page. SubMiner receives the token through a `subminer://` link and stores it encrypted.
If the embedded auth UI fails to render, SubMiner opens the authorize URL in your default browser and shows fallback instructions in-app. If the setup window does not render, SubMiner opens the authorization page in your browser instead. To skip the flow entirely, paste a token into `anilist.accessToken`.
::: tip On Linux, the token is stored with `gnome-libsecret` by default. If your keyring is unavailable, start it (gnome-keyring or KWallet) or launch SubMiner with `--password-store=basic_text`.
You can also set `anilist.accessToken` directly in config to skip the setup flow entirely. When blank, SubMiner uses the locally stored encrypted token.
:::
## How Tracking Works ## How updates work
SubMiner monitors playback and triggers an AniList progress update when an episode is considered "watched" -- at least 85% of the episode duration viewed and a minimum of 10 minutes watched. An episode counts as watched after 85% of its length and at least 10 minutes of playback. SubMiner then:
The update flow: 1. Reads the title, season, and episode from the file name and folder. Install [guessit](https://github.com/guessit-io/guessit) for better parsing. A folder named `Season 2` is a strong season hint.
2. Finds the matching AniList entry. For season 2 and later, it follows the show's sequels.
3. Sets your progress to that episode and marks the entry Watching, or Completed on the final episode.
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. The show must already be on your Planning or Watching list. SubMiner does not add new entries, and it never lowers your progress.
2. **AniList search** -- The detected title is searched against the AniList GraphQL API. SubMiner picks the best match by comparing titles (romaji, English, native) and filtering by episode count.
3. **Progress check** -- SubMiner fetches your current list entry for the matched media. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`.
## Update Queue and Retry Failed updates are saved and retried in the background, up to 8 times with growing delays. The queue survives restarts.
Failed AniList updates are persisted to a retry queue on disk and retried with exponential backoff. ## Fixing a wrong match
| Parameter | Value | If a cover or title in the stats Library is wrong, open the title and use **Change AniList Entry**.
| --- | --- |
| Initial backoff | 30 seconds |
| Maximum backoff | 6 hours |
| Maximum attempts | 8 |
| Queue capacity | 500 items |
After 8 failed attempts, the update is moved to a dead-letter queue and no longer retried automatically. The queue is persisted across restarts so no updates are lost if SubMiner exits before a retry succeeds. If SubMiner cannot find a later season, it skips the update rather than writing progress to season 1. Pin the right entry with the character dictionary's AniList override. See [Character dictionary](/character-dictionary).
Use `--anilist-retry-queue` to manually process one ready item from the queue. ## Commands
## Cover Art | Command | What it does |
| ------------------------------------ | --------------------------------------- |
| `subminer app --anilist-setup` | Open the AniList setup window |
| `subminer app --anilist-status` | Show token state and retry queue counts |
| `subminer app --anilist-logout` | Remove the stored token |
| `subminer app --anilist-retry-queue` | Retry one queued update now |
SubMiner fetches cover art from AniList for display in the stats dashboard. When a new video starts playing, the cover art fetcher: ## Options
1. Checks the local database for cached art. | Key | What it does |
2. If missing, parses the media title (guessit then fallback) and searches the AniList API. | --------------------- | ------------------------------------------------------------- |
3. Downloads the cover image from the AniList CDN and caches it locally (both URL and blob). | `anilist.enabled` | Turns on progress updates. |
4. Stores AniList metadata (romaji/English titles, total episodes) alongside the cover for dashboard display. | `anilist.accessToken` | Token override. Leave empty to use the token stored by setup. |
A no-match result is cached for 5 minutes before SubMiner retries, preventing repeated API calls for unrecognized media. Character dictionary settings live under `anilist.characterDictionary` and are covered on the [Character dictionary](/character-dictionary) page. See [Configuration](/configuration#anilist) for defaults.
## Rate Limiting
All AniList API calls go through a shared rate limiter that enforces a sliding window of 20 requests per minute. The limiter also reads AniList's `X-RateLimit-Remaining` and `Retry-After` response headers and pauses requests when the server signals throttling. This applies to both episode tracking and cover art fetching.
## Configuration Reference
```jsonc
{
"anilist": {
"enabled": true,
"accessToken": "",
"characterDictionary": {
"enabled": false,
"maxLoaded": 3,
"profileScope": "all",
"collapsibleSections": {
"description": false,
"characterInformation": false,
"voicedBy": false
}
}
}
}
```
| Option | Values | Description |
| --- | --- | --- |
| `enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
| `accessToken` | string | Explicit AniList access token override; when blank, SubMiner uses the stored encrypted token (default: `""`) |
| `characterDictionary.enabled` | `true`, `false` | Enable auto-sync of the merged character dictionary from AniList (default: `false`) |
| `characterDictionary.maxLoaded` | number | Number of recent media snapshots kept in the merged dictionary (default: `3`) |
| `characterDictionary.profileScope` | `"all"`, `"active"` | Apply dictionary to all Yomitan profiles or only the active one |
| `characterDictionary.collapsibleSections.*` | `true`, `false` | Control which dictionary entry sections start expanded |
See the [Character Dictionary](/character-dictionary) page for full details on the character dictionary feature, including name generation, matching, auto-sync lifecycle, and dictionary entry format.
## CLI Commands
| Command | Description |
| --- | --- |
| `--anilist-setup` | Open AniList setup/auth flow helper window |
| `--anilist-status` | Print current token resolution state and retry queue counters |
| `--anilist-logout` | Clear stored AniList token from local persisted state |
| `--anilist-retry-queue` | Process one ready retry queue item immediately |
## Troubleshooting ## Troubleshooting
- **Updates not triggering:** Confirm `anilist.enabled` is `true`. SubMiner requires at least 85% of the episode watched and a minimum of 10 minutes. Short episodes or partial watches will not trigger an update. **No update after an episode.** Check that `anilist.enabled` is `true` and that you watched at least 85% of the episode.
- **Wrong episode or title matched:** Detection quality is best when `guessit` is installed and on your `PATH`. Without it, SubMiner falls back to internal filename parsing which can be less accurate with unusual naming conventions.
- **Token issues:** Run `--anilist-status` to check token state. If the token is invalid or expired, run `--anilist-setup` or `--anilist-logout` and re-authenticate.
- **Updates failing repeatedly:** Run `--anilist-status` to see retry queue counters. Items that fail 8 times are moved to the dead-letter queue. Check network connectivity and AniList API status.
- **Cover art missing:** Cover art is fetched on a best-effort basis using title matching. If the filename is hard to parse, the search may return no results. The fetcher retries after 5 minutes.
- **Encryption unavailable on Linux:** If you see warnings about safeStorage, try `--password-store=basic_text` as a workaround, or ensure your desktop keyring (gnome-keyring, KWallet) is running.
## Related **"AniList update not possible."** Add the show to your Planning or Watching list, then mark the episode watched again.
- [Character Dictionary](/character-dictionary) -- AniList-powered character name dictionary for Yomitan **Wrong show or episode.** Install guessit and make sure it is on your `PATH`. Unusual file names parse poorly without it.
- [Configuration Reference](/configuration) -- full config options
- [Jellyfin Integration](/jellyfin-integration) -- media server integration **Token errors.** Run `subminer app --anilist-status`. If the token is invalid, run `--anilist-logout`, then `--anilist-setup`.
+39
View File
@@ -0,0 +1,39 @@
# AniSkip integration
SubMiner looks up opening timestamps on [AniSkip](https://aniskip.com) so you can skip an anime's intro with one key.
## Setup
AniSkip is on by default. To turn it off or change the key:
```jsonc
{
"mpv": {
"aniskipEnabled": true,
"aniskipButtonKey": "TAB",
},
}
```
Both settings apply immediately, without restarting mpv. For better title and episode detection, install [guessit](https://github.com/guessit-io/guessit):
```bash
python3 -m pip install --user guessit
```
## Usage
When a local file loads, SubMiner reads the title and episode from the file name, finds the show on MyAnimeList, and asks AniSkip for the intro's timestamps. Streams and URLs are skipped.
If AniSkip has an intro, SubMiner adds `AniSkip Intro Start` and `AniSkip Intro End` chapters. When the intro starts, mpv shows "You can skip by pressing TAB" (with your key) for 3 seconds. Press the key any time during the intro to jump to its end.
With a custom key other than `TAB` or `y-k`, `y-k` also skips.
## Triggering from mpv
| Command | What it does |
| ----------------------------------------- | ------------------------------------------------------- |
| `script-message subminer-skip-intro` | Skip to the end of the intro |
| `script-message subminer-aniskip-refresh` | Look up the current file again, ignoring cached results |
Use `subminer-aniskip-refresh` after a lookup failed or matched the wrong show.
+131 -279
View File
@@ -1,42 +1,34 @@
# Anki Integration # Anki integration
SubMiner uses the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on to create and update Anki cards with sentence context, audio, and screenshots. SubMiner talks to Anki through the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on. It fills new cards with the sentence, an audio clip, and a screenshot, and can create sentence cards and merge duplicate words. It is built for the [Lapis](https://github.com/donkuri/lapis), [Kiku](https://kiku.youyoumu.my.id/), and [Senren](https://github.com/BrenoAqua/Senren) note types, but works with any note type once you map its fields.
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.
For the day-to-day flow, see [Mining workflow](/mining-workflow). Every key on this page, with its default, is listed in the [AnkiConnect config reference](/configuration#ankiconnect).
## Prerequisites ## Prerequisites
1. Install [Anki](https://apps.ankiweb.net/). 1. Install [Anki](https://apps.ankiweb.net/).
2. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on (code: `2055492159`). 2. Install AnkiConnect (add-on code `2055492159`).
3. Keep Anki running while using SubMiner. 3. Install FFmpeg and make sure it is on your `PATH`. SubMiner uses it for audio and images.
4. Keep Anki running while you mine.
AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the port in AnkiConnect's settings, update `ankiConnect.url` in your SubMiner config. If you changed AnkiConnect's port, set `ankiConnect.url` to match.
## Auto-Enrichment Transport ## How cards get filled
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: When Yomitan adds a note, SubMiner fills the sentence, audio, image, and MiscInfo fields. It finds new notes in one of two ways:
**Proxy mode** — SubMiner runs a local AnkiConnect-compatible proxy and intercepts card creation instantly. Recommended when possible. - **Proxy (default).** SubMiner runs a local AnkiConnect-compatible server. Yomitan sends notes through it, and SubMiner fills each one right after Anki accepts it.
- **Polling.** With `ankiConnect.proxy.enabled` set to `false`, SubMiner asks AnkiConnect for recently added notes every `ankiConnect.pollingRate` milliseconds.
**Polling mode** (default) — SubMiner polls AnkiConnect every few seconds for newly added cards. Simpler setup, but with a short delay (~3 seconds). Set `ankiConnect.behavior.autoUpdateNewCards` to `false` to stop automatic filling and update cards by hand with `Ctrl/Cmd+V` instead.
Use proxy mode if you want immediate enrichment. Use polling mode if your Yomitan instance is external (browser-based) or you prefer minimal configuration. `ankiConnect.deck` limits enrichment and duplicate checks to one deck. If it is empty, SubMiner uses Yomitan's mining deck when it can read it, and otherwise searches all decks.
In both modes, the enrichment workflow is the same: ### Proxy mode setup (Yomitan / texthooker) {#proxy-mode-setup-yomitan-texthooker}
1. Checks if a duplicate expression already exists (for field grouping).
2. Updates the sentence field with the current subtitle.
3. Generates and uploads audio and image media.
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.
### Proxy Mode Setup (Yomitan / Texthooker)
```jsonc ```jsonc
"ankiConnect": { "ankiConnect": {
"url": "http://127.0.0.1:8765", // real AnkiConnect "url": "http://127.0.0.1:8765",
"proxy": { "proxy": {
"enabled": true, "enabled": true,
"host": "127.0.0.1", "host": "127.0.0.1",
@@ -46,325 +38,185 @@ Known-word sync scope is controlled by `ankiConnect.knownWords.decks` (object ma
} }
``` ```
Then point Yomitan/clients to `http://127.0.0.1:8766` instead of `8765`. Clients must send notes to the proxy (`http://127.0.0.1:8766` here), not to AnkiConnect directly.
When SubMiner loads the bundled Yomitan extension, it also attempts to update the **default Yomitan profile** (`profiles[0].options.anki.server`) to the active SubMiner endpoint: - **Bundled Yomitan.** SubMiner sets the active Yomitan profile's Anki server for you. With the proxy on, it always points the profile at the proxy. With the proxy off, it sets `ankiConnect.url`, but only if the profile's server is blank or the stock `http://127.0.0.1:8765`.
- **Browser Yomitan or other clients.** Set the Anki server to the proxy URL yourself. To leave your main profile alone, create a separate Yomitan profile for SubMiner, set its Anki server (Settings, Anki) to the proxy URL, and make it active while you mine.
- proxy URL when `ankiConnect.proxy.enabled` is `true` ### Proxy troubleshooting
- direct `ankiConnect.url` when proxy mode is disabled
To avoid clobbering custom setups, this auto-update only changes the default profile when its current server is blank or the stock Yomitan default (`http://127.0.0.1:8765`). If cards are not getting filled:
For browser-based Yomitan or other external clients (for example Texthooker in a normal browser profile), set their Anki server to the same proxy URL separately: `http://127.0.0.1:8766` (or your configured `proxy.host` + `proxy.port`). 1. Check that the proxy is listening while SubMiner runs:
### Browser/Yomitan external setup (separate profile) ```bash
ss -ltnp | grep 8766
```
If you want SubMiner to use proxy mode without touching your main/default Yomitan profile, create or select a separate Yomitan profile just for SubMiner and set its Anki server to the proxy URL. 2. Check that requests pass through to Anki:
That profile isolation gives you both benefits: ```bash
curl -sS http://127.0.0.1:8766 \
- SubMiner can auto-enrich immediately via proxy.
- Your default Yomitan profile keeps its existing Anki server setting.
In Yomitan, go to Settings → Profile and:
1. Create a profile for SubMiner (or choose one dedicated profile).
2. Open Anki settings for that profile.
3. Set server to `http://127.0.0.1:8766` (or your configured proxy URL).
4. Save and make that profile active when using SubMiner.
This is only for non-bundled, external/browser Yomitan or other clients. The bundled profile auto-update logic only targets `profiles[0]` when it is blank or still default.
### Proxy Troubleshooting (quick checks)
If auto-enrichment appears to do nothing:
1. Confirm proxy listener is running while SubMiner is active:
```bash
ss -ltnp | rg 8766
```
2. Confirm requests can pass through the proxy:
```bash
curl -sS http://127.0.0.1:8766 \
-H 'content-type: application/json' \ -H 'content-type: application/json' \
-d '{"action":"version","version":2}' -d '{"action":"version","version":2}'
``` ```
3. Check both log sinks: 3. Read the app log (`app-YYYY-MM-DD.log`) in the logs folder. See [Troubleshooting](/troubleshooting) for where logs live.
- Launcher/mpv-integrated log: `~/.cache/SubMiner/mp.log` ## Field mapping
- App runtime log: `~/.config/SubMiner/logs/SubMiner-YYYY-MM-DD.log`
4. Ensure config JSONC is valid and logging shape is correct: `ankiConnect.fields` maps SubMiner's data to fields on your note type.
```jsonc | Key | Receives |
"logging": { | ------------------ | ------------------------------------------------------------------------- |
"level": "debug" | `fields.word` | The mined word |
} | `fields.audio` | Sentence audio cut from the video |
``` | `fields.wordAudio` | Read only: Yomitan's word audio, used to time animated images (see below) |
| `fields.image` | Screenshot or animated clip |
`"logging": "debug"` is invalid for current schema and can break reload/start behavior. | `fields.sentence` | Subtitle text |
| `fields.miscInfo` | Text from `ankiConnect.metadata.pattern` |
## Field Mapping
SubMiner maps its data to your Anki note fields. Configure these under `ankiConnect.fields`:
```jsonc ```jsonc
"ankiConnect": { "ankiConnect": {
"fields": { "fields": {
"word": "Expression", // mined word / expression text "audio": "SentenceAudio",
"audio": "ExpressionAudio", // audio clip from the video "sentence": "Sentence"
"image": "Picture", // screenshot or animated clip
"sentence": "Sentence", // subtitle text
"miscInfo": "MiscInfo", // metadata (filename, timestamp)
"translation": "SelectionText" // secondary sub or AI translation
} }
} }
``` ```
Field names must match your Anki note type exactly (case-sensitive). If a configured field does not exist on the note type, SubMiner skips it without error. Field names are matched case-insensitively. A mapped field that is missing from the note type is skipped.
### Minimal Config `fields.audio` gets sentence audio, not word audio. Yomitan writes its own dictionary audio into your note, so point `fields.audio` at a separate field such as `SentenceAudio`. The default, `ExpressionAudio`, is the field many note types use for Yomitan's word audio, so leaving it would overwrite that audio.
If you only want sentence and audio on your cards: `ankiConnect.tags` adds tags to every mined or updated card. Set it to `[]` to add none.
```jsonc `ankiConnect.metadata.pattern` builds the MiscInfo text. Tokens: `%f` file name, `%F` file name with extension, `%t` timestamp, `%T` timestamp with milliseconds, `<br>` line break.
"ankiConnect": {
"enabled": true,
"fields": {
"sentence": "Sentence",
"audio": "ExpressionAudio"
}
}
```
## Media Generation ## Media
SubMiner uses FFmpeg to generate audio and image media from the video. FFmpeg must be installed and on `PATH`. | Key | What it does |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `media.generateAudio` | Cut sentence audio (MP3) from the subtitle's start and end time |
| `media.audioPadding` | Seconds added before and after the clip |
| `media.fallbackDuration` | Clip length when the subtitle has no timing |
| `media.maxMediaDuration` | Longest allowed clip, in seconds (`0` removes the cap) |
| `media.normalizeAudio` | Normalize clip loudness |
| `media.mirrorMpvVolume` | Scale the clip by mpv's current volume, so quiet playback gives quiet clips |
| `media.generateImage` | Capture an image |
| `media.imageType` | `static` for one frame, `avif` for an animated clip of the line |
| `media.imageFormat` | Static format: `jpg`, `png`, or `webp` |
| `media.imageQuality` | Static image quality |
| `media.imageMaxWidth` / `Height` | Static size limit (`0` keeps source size) |
| `media.animatedFps` | Animated clip frame rate |
| `media.animatedMaxWidth` / `Height` | Animated size limit (`0` keeps aspect ratio) |
| `media.animatedCrf` | Animated quality, `0` to `63`, lower is better |
| `media.syncAnimatedImageToWordAudio` | Hold the first frame for the length of the word audio in `fields.wordAudio`, so the motion starts with the sentence audio |
| `media.reviewTiming` | Pause and let you adjust the clip before media is made (see below) |
### Audio Animated AVIF needs an FFmpeg build with an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`).
Audio is extracted from the video file using the subtitle's start and end timestamps, with configurable padding added before and after. Media settings apply to the next card without a restart.
```jsonc ### Review media timing
"ankiConnect": {
"media": {
"generateAudio": true,
"audioPadding": 0.5, // seconds before and after subtitle timing
"maxMediaDuration": 30 // cap total duration in seconds
}
}
```
Output format: MP3 at 44100 Hz. If the video has multiple audio streams, SubMiner uses the active stream. With `media.reviewTiming` on, SubMiner pauses before making media for word, sentence, and audio cards and opens a review dialog. You can also toggle it for the current session with **Review Media Timing** in the runtime options palette (`Ctrl/Cmd+Shift+O`). Clipboard updates and stats-dashboard mining skip the review.
The audio is uploaded to Anki's media folder and inserted as `[sound:audio_<timestamp>.mp3]`. The dialog shows the clip over a speech waveform. When the waveform loads, an untouched clip end moves back to just after the last speech in the line. The orange bars still mark the subtitle's original start and end.
### Screenshots (Static) During preview, the yellow cursor follows the audio player's position through the selected range and stays at the end when playback finishes. Buffering pauses the cursor too. If the player stops reporting progress for 15 seconds, the preview stops so you can retry.
A single frame is captured at the current playback position. | Action | How |
| ------------------------ | --------------------------------------------------------------------- |
| Trim | Drag either edge, or click the waveform to move the nearer edge there |
| Nudge an edge | Arrow keys on a focused edge (100 ms, `Shift` for 500 ms) |
| Slide the clip | Drag the middle |
| Show more timeline | Earlier / Later |
| Pick the screenshot | Screenshot slider or Frame buttons (static images only) |
| Add previous / next line | `P` / `N` (`Shift+P` / `Shift+N` removes) |
| Preview | `Space` |
| Confirm | `Enter` |
| Cancel | `Escape` |
```jsonc The confirmed range is used as is, with no extra padding. Added lines go into the sentence field. Reset restores the original timing and removes added lines.
"ankiConnect": {
"media": {
"generateImage": true,
"imageType": "static",
"imageFormat": "jpg", // "jpg", "png", or "webp"
"imageQuality": 92, // 1–100
"imageMaxWidth": null, // optional, preserves aspect ratio
"imageMaxHeight": null
}
}
```
### Animated Clips (AVIF) When you cancel, you can go back to editing, keep the original timing, create the card without media, or discard it. Discard deletes the Yomitan note or audio card, and skips creation for a sentence card.
Instead of a static screenshot, SubMiner can generate an animated AVIF covering the subtitle duration. ### Update behavior
```jsonc | Key | What it does |
"ankiConnect": { | ----------------------------- | ---------------------------------------------------- |
"media": { | `behavior.overwriteAudio` | Replace existing audio instead of adding to it |
"generateImage": true, | `behavior.overwriteImage` | Replace the existing image instead of adding to it |
"imageType": "avif", | `behavior.mediaInsertMode` | `append` or `prepend` new media when not overwriting |
"animatedFps": 10, | `behavior.autoUpdateNewCards` | Fill new Yomitan notes automatically |
"animatedMaxWidth": 640, | `behavior.highlightWord` | Bold the mined word in the sentence field |
"animatedMaxHeight": null, | `behavior.notificationType` | `overlay`, `system`, `both`, or `none` |
"animatedCrf": 35 // 0–63, lower = better quality
}
}
```
Animated AVIF requires an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`) in your FFmpeg build. Generation timeout is 60 seconds. Manual clipboard updates (`Ctrl/Cmd+V`) always replace the sentence audio, whatever `overwriteAudio` says.
### Behavior Options ## Sentence cards (Lapis) {#sentence-cards-lapis}
```jsonc `Ctrl/Cmd+S` creates a standalone sentence card from the current line, and `Ctrl/Cmd+Shift+S` then a digit combines several lines. The card uses the note type named in `ankiConnect.isLapis.sentenceCardModel`, which must exist in Anki. If it is empty, no card is created.
"ankiConnect": {
"behavior": {
"overwriteAudio": true, // replace existing audio, or append
"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"
}
}
```
`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
SubMiner can auto-translate the mined sentence and fill the translation field.
Secondary subtitle text still wins when present. AI translation is only attempted when `ankiConnect.ai.enabled` is `true` and no secondary subtitle exists.
```jsonc
"ai": {
"enabled": true,
"apiKey": "sk-...",
"apiKeyCommand": "",
"baseUrl": "https://openrouter.ai/api",
"requestTimeoutMs": 15000
},
"ankiConnect": {
"ai": {
"enabled": true,
"model": "openai/gpt-4o-mini",
"systemPrompt": "Translate mined sentence text only."
}
}
```
`ankiConnect.ai` controls feature-local enablement plus optional `model` / `systemPrompt` overrides.
Provider credentials and request transport settings live in top-level `ai`.
Translation priority:
1. If a secondary subtitle is available, use it as the translation.
2. If `ankiConnect.ai.enabled` is `true` and top-level `ai.enabled` is `true`, call the shared AI provider.
3. If AI translation fails and no secondary subtitle exists, fall back to the original sentence text.
The built-in translation request asks for English output by default. Customize that behavior through `ankiConnect.ai.systemPrompt`.
## Sentence Cards (Lapis)
SubMiner can create standalone sentence cards (without a word/expression) using a separate note type. This is designed for use with [Lapis](https://github.com/donkuri/Lapis) and similar sentence-focused note types.
::: warning Required config
Sentence card creation and audio card marking both require `ankiConnect.isLapis.enabled: true` and a valid `sentenceCardModel` pointing to your Lapis/Kiku note type. Without this, the `Ctrl/Cmd+S` and `Ctrl/Cmd+Shift+A` shortcuts will not create cards.
:::
```jsonc ```jsonc
"ankiConnect": { "ankiConnect": {
"isLapis": { "isLapis": {
"enabled": true, "enabled": true,
"sentenceCardModel": "Japanese sentences" "sentenceCardModel": "Lapis"
} }
} }
``` ```
Trigger with the mine sentence shortcut (`Ctrl/Cmd+S` by default). The card is created directly via AnkiConnect with the sentence, audio, and image filled in. Sentence cards and audio cards (`Ctrl/Cmd+Shift+A`) always write to the `Sentence` and `SentenceAudio` fields. Normal word cards keep using your `ankiConnect.fields` mapping.
To mine multiple subtitle lines as one sentence card, use `Ctrl/Cmd+Shift+S` followed by a digit (1–9) to select how many recent lines to combine. ## Word card type (Kiku/Lapis)
## Field Grouping (Kiku) When `isKiku` or `isLapis` is enabled, SubMiner sets a card-type flag on word cards it fills. Choose the flag with `ankiConnect.lapisKiku.wordCardKind`:
When you mine the same word multiple times, SubMiner can merge the cards instead of creating duplicates. This is designed for note types like [Kiku](https://github.com/youyoumu/kiku) that support grouped sentence/audio/image fields. | Value | Flag |
| ------------------- | ----------------------- |
| `word-and-sentence` | `IsWordAndSentenceCard` |
| `click` | `IsClickCard` |
| `sentence` | `IsSentenceCard` |
| `audio` | `IsAudioCard` |
| `none` | Leaves flags alone |
The other card-type flags are cleared. Sentence cards and audio cards keep their own flag.
## Field grouping (Kiku/Senren) {#field-grouping-kiku-senren}
When you mine a word that already has a card, SubMiner can merge the new card into the old one. The sentence, audio, image, and MiscInfo from both cards are kept as grouped entries, and the template lets you switch between them. This works with [Kiku](https://github.com/youyoumu/kiku) and [Senren](https://github.com/BrenoAqua/Senren) (which calls it [scene switching](https://github.com/BrenoAqua/Senren/blob/main/docs/scene_switching.md)).
Enable one of them. They write different markup to the same fields, so only one can be on. If both are enabled, Kiku is used and SubMiner logs a config warning.
```jsonc ```jsonc
"ankiConnect": { "ankiConnect": {
"isKiku": { "isKiku": {
"enabled": true, "enabled": true,
"fieldGrouping": "manual", // "auto", "manual", or "disabled" "fieldGrouping": "manual",
"deleteDuplicateInAuto": true // delete new card after auto-merge "deleteDuplicateInAuto": true
} }
} }
``` ```
### Modes For Senren, use the same keys under `isSenren`.
**Disabled** (`"disabled"`): No duplicate detection. Each card is independent. | `fieldGrouping` | Behavior |
| --------------- | ------------------------------------------------------------------------------- |
| `disabled` | No duplicate check |
| `auto` | Merge into the existing card. With `deleteDuplicateInAuto`, delete the new card |
| `manual` | Show both cards, let you choose which to keep and preview the merge |
**Auto** (`"auto"`): When a duplicate expression is found, SubMiner merges the new card into the existing one automatically. Both sentences, audio clips, and images are preserved, and exact duplicate values are collapsed to one entry. If `deleteDuplicateInAuto` is true, the new card is deleted after merging. The manual dialog cancels itself after 90 seconds. Identical entries are not deduplicated. Press `Ctrl/Cmd+G` to run the duplicate check on the last card yourself.
**Manual** (`"manual"`): A modal appears in the overlay showing both cards. You choose which card to keep, preview the merge result, then confirm. The modal has a 90-second timeout, after which it cancels automatically.
### What Gets Merged
| Field | Merge behavior |
| -------- | --------------------------------------------------------------- |
| Sentence | Both sentences preserved (exact duplicate text is deduplicated) |
| Audio | Both `[sound:...]` entries kept (exact duplicates deduplicated) |
| Image | Both images kept (exact duplicates deduplicated) |
### Keyboard Shortcuts in the Modal
| Key | Action | | Key | Action |
| --------- | ---------------------------------- | | ----------- | ------------------------------------- |
| `1` / `2` | Select card 1 or card 2 to keep | | `1` / `2` | Keep card 1 or card 2 |
| `Enter` | Confirm selection | | `Enter` | Confirm |
| `Esc` | Cancel (keep both cards unchanged) | | `Backspace` | Back from the merge preview |
| `Esc` | Cancel and leave both cards unchanged |
## Full Config Example ## Config validation
```jsonc Invalid `ankiConnect` values produce a warning and fall back to the default. Use JSON booleans (`true`, not `"true"`) and a positive number for `pollingRate`.
{
"ankiConnect": {
"enabled": true,
"url": "http://127.0.0.1:8765",
"pollingRate": 3000,
"proxy": {
"enabled": false,
"host": "127.0.0.1",
"port": 8766,
"upstreamUrl": "http://127.0.0.1:8765",
},
"fields": {
"audio": "ExpressionAudio",
"image": "Picture",
"sentence": "Sentence",
"miscInfo": "MiscInfo",
"translation": "SelectionText",
},
"media": {
"generateAudio": true,
"generateImage": true,
"imageType": "static",
"imageFormat": "jpg",
"imageQuality": 92,
"audioPadding": 0.5,
"maxMediaDuration": 30,
},
"behavior": {
"overwriteAudio": true,
"overwriteImage": true,
"mediaInsertMode": "append",
"autoUpdateNewCards": true,
"notificationType": "osd",
},
"ai": {
"enabled": false,
"model": "openai/gpt-4o-mini",
"systemPrompt": "Translate mined sentence text only.",
},
"isKiku": {
"enabled": false,
"fieldGrouping": "disabled",
"deleteDuplicateInAuto": true,
},
"isLapis": {
"enabled": false,
"sentenceCardModel": "Japanese sentences",
},
},
"ai": {
"enabled": false,
"apiKey": "",
"apiKeyCommand": "",
"baseUrl": "https://openrouter.ai/api",
"requestTimeoutMs": 15000,
},
}
```
+109 -192
View File
@@ -1,140 +1,55 @@
# Architecture # Architecture
This page is a contributor-facing architecture summary. Canonical internal architecture guidance lives in `docs/architecture/README.md` at the repo root. A contributor-facing map of how SubMiner is put together. The canonical internal guidance, including domain ownership and layering rules, is [`docs/architecture/README.md`](https://github.com/ksyasuda/SubMiner/blob/main/docs/architecture/README.md) in the repo.
SubMiner is split into three cooperating runtimes: SubMiner runs as three cooperating runtimes:
- Electron desktop app (`src/`) for overlay/UI/runtime orchestration. - the Electron desktop app (`src/`): overlay, UI, and runtime orchestration
- Launcher CLI (`launcher/`) for mpv/app command workflows. - the launcher CLI (`launcher/`): mpv and app command workflows
- mpv Lua plugin (`plugin/subminer/init.lua` + module files) for player-side controls and IPC handoff. - the mpv Lua plugin (`plugin/subminer/`): player-side controls and handoff to the app
Within the desktop app, `src/main.ts` is a composition root that wires small runtime/domain modules plus core services. Inside the app, `src/main.ts` is a composition root. It owns wiring and state, and delegates behavior to small runtime and domain modules that can be tested without Electron or mpv.
## Goals ## Project layout
- Keep behavior stable while reducing coupling.
- Prefer small, single-purpose units that can be tested in isolation.
- Keep `main.ts` focused on wiring and state ownership, not implementation detail.
- Follow Unix-style composability:
- each service does one job
- services compose through explicit inputs/outputs
- orchestration is separate from implementation
## Project Structure
```text ```text
launcher/ # Standalone CLI launcher wrapper and mpv helpers launcher/
commands/ # Command modules (doctor/config/mpv/jellyfin/playback/app passthrough) main.ts # entrypoint and command dispatch
config/ # Launcher config parsers + CLI parser builder commands/ # one module per subcommand (playback, jellyfin, stats, sync, ...)
main.ts # Launcher entrypoint and command dispatch config/ # launcher config readers and CLI parser
plugin/ plugin/subminer/ # mpv plugin; main.lua loads init.lua, which boots the other modules
subminer/ # Modular mpv plugin (init · main · bootstrap · lifecycle · process
# state · messages · hover · ui · options · environment · log
# binary · aniskip · aniskip_match)
src/ src/
ai/ # AI translation provider utilities (client, config) main-entry.ts # bootstrap wrapper that runs before main.js
main-entry.ts # Background-mode bootstrap wrapper before loading main.js main.ts # composition root
main.ts # Entry point — delegates to runtime composers/domain modules preload*.ts # preload bridges (overlay, settings, stats, sync, Jellyfin setup)
preload.ts # Electron preload bridge main/ # main-process runtime modules and IPC/CLI wiring
types.ts # Shared type definitions boot/ # pre-ready boot helpers
main/ # Main-process composition/runtime adapters runtime/composers/ # larger runtime clusters assembled for main.ts
app-lifecycle.ts # App lifecycle + app-ready runtime runner factories runtime/domains/ # domain barrels (startup, overlay, mpv, ipc, shortcuts, anilist, jellyfin, mining)
character-dictionary-runtime.ts # Character-dictionary orchestration/public runtime API core/services/ # focused services: mpv client, overlay, tokenizer, mining, integrations, stats
cli-runtime.ts # CLI command runtime service adapters core/utils/ # pure helpers
config-validation.ts # Startup/hot-reload config error formatting and fail-fast helpers shared/ipc/ # IPC channel constants and payload validators
dependencies.ts # Shared dependency builders for IPC/runtime services renderer/ # overlay renderer: subtitle rendering, input handlers, modals
ipc-runtime.ts # IPC runtime registration wrappers config/ # definitions/ (defaults + option registries) and resolve/ (resolution pipeline)
overlay-runtime.ts # Overlay modal routing + active-window selection cli/ # app CLI parsing and help output
overlay-shortcuts-runtime.ts # Overlay keyboard shortcut handling settings/, syncui/ # settings and sync windows
overlay-visibility-runtime.ts # Overlay visibility + tracker-driven bounds service window-trackers/ # Hyprland, Sway, X11, macOS, and Windows trackers
frequency-dictionary-runtime.ts # Frequency dictionary runtime adapter anki-integration/ # AnkiConnect proxy and note-update workflow
jlpt-runtime.ts # JLPT dictionary runtime adapter jimaku/, subsync/, tsukihime/ # integration helpers
media-runtime.ts # Media path/title/subtitle-position runtime service types/ # shared domain types
startup.ts # Startup bootstrap dependency builder stats/ # stats dashboard UI (Vite)
startup-lifecycle.ts # Lifecycle runtime runner adapter vendor/ # Yomitan fork, texthooker-ui, JLPT vocab
state.ts # Application runtime state container + reducer transitions
subsync-runtime.ts # Subsync command runtime adapter
character-dictionary-runtime/ # Character-dictionary fetch/build/cache modules + focused tests
runtime/
composers/ # High-level composition clusters used by main.ts
domains/ # Domain barrel exports (startup/overlay/mpv/jellyfin/...)
registry.ts # Domain registry consumed by main.ts
core/
services/ # Focused runtime services (Electron adapters + pure logic)
anilist/ # AniList token store/update queue/update helpers
immersion-tracker/ # Immersion persistence/session/metadata modules
tokenizer/ # Tokenizer stage modules (selection/enrichment/annotation)
utils/ # Pure helpers and coercion/config utilities
cli/ # CLI parsing and help output
config/ # Config defaults/definitions, loading, parse, resolution pipeline
definitions/ # Domain-specific defaults + option registries
resolve/ # Domain-specific config resolution pipeline stages
shared/ipc/ # Cross-process IPC channel constants + payload validators
renderer/ # Overlay renderer (modularized UI/runtime)
handlers/ # Keyboard/mouse interaction modules
modals/ # Jimaku/Kiku/subsync/runtime-options/session-help modals
positioning/ # Subtitle position controller (drag-to-reposition)
window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS, Windows)
jimaku/ # Jimaku API integration helpers
subsync/ # Subtitle sync (alass/ffsubsync) helpers
subtitle/ # Subtitle processing utilities
tokenizers/ # Tokenizer implementations
anki-integration/ # AnkiConnect proxy server + note-update enrichment workflow
token-mergers/ # Token merge strategies
translators/ # AI translation providers
``` ```
### Service Layer (`src/core/services/`) A few ownership notes that are hard to guess from file names:
- **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts` - mpv access is split into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), and property modules under `src/core/services/`.
- **Shortcuts/input:** `shortcut.ts`, `overlay-shortcut.ts`, `overlay-shortcut-handler.ts`, `shortcut-fallback.ts`, `numeric-shortcut.ts` - The renderer keeps `renderer.ts` to orchestration. Keyboard, mouse, and gamepad input live in `renderer/handlers/`, and each modal flow has its own file in `renderer/modals/`.
- **MPV runtime:** `mpv.ts`, `mpv-transport.ts`, `mpv-protocol.ts`, `mpv-properties.ts`, `mpv-render-metrics.ts` - AniSkip intro detection runs in the app (`src/main/runtime/aniskip-runtime.ts`), which drives mpv chapters and the skip key over the mpv IPC socket. The plugin does not handle it.
- **Mining + Anki/Jimaku runtime:** `mining.ts`, `field-grouping.ts`, `field-grouping-overlay.ts`, `anki-jimaku.ts`, `anki-jimaku-ipc.ts`
- **Subtitle/token pipeline:** `subtitle-processing-controller.ts`, `subtitle-position.ts`, `subtitle-ws.ts`, `tokenizer.ts` + `tokenizer/*` stage modules (including `parser-enrichment-worker-runtime.ts` for async MeCab enrichment and `yomitan-parser-runtime.ts`)
- **Integrations:** `jimaku.ts`, `subsync.ts`, `subsync-runner.ts`, `texthooker.ts`, `jellyfin.ts`, `jellyfin-remote.ts`, `discord-presence.ts`, `yomitan-extension-loader.ts`, `yomitan-settings.ts`
- **Anki integration:** `anki-integration.ts`, `anki-integration/anki-connect-proxy.ts` (local proxy for push-based auto-enrichment), `anki-integration/note-update-workflow.ts`
- **Config/runtime controls:** `config-hot-reload.ts`, `runtime-options-ipc.ts`, `cli-command.ts`, `startup.ts`
- **Domain submodules:** `anilist/*` (token/update queue/updater), `immersion-tracker/*` (storage/session/metadata/query/reducer)
### Renderer Layer (`src/renderer/`) ## Component diagram
The renderer keeps `renderer.ts` focused on orchestration. UI behavior is delegated to per-concern modules. The main process drives one primary overlay window plus modal surfaces. Primary and secondary subtitle layers render in the same overlay renderer, connected to the main process through `preload.ts`. The launcher and mpv plugin run as separate processes and talk to the app through sockets or CLI passthrough.
```text
src/renderer/
renderer.ts # Entrypoint/orchestration only
context.ts # Shared runtime context contract
state.ts # Centralized renderer mutable state (visible overlay only)
error-recovery.ts # Global renderer error boundary + recovery actions
overlay-content-measurement.ts # Reports rendered bounds to main process
subtitle-render.ts # Primary/secondary subtitle rendering + style application
positioning.ts # Facade export for positioning controller
yomitan-popup.ts # Yomitan popup iframe detection utilities
positioning/
controller.ts # Subtitle drag-position controller
position-state.ts # Position state helpers (yPercent)
handlers/
keyboard.ts # Keybindings, chord handling, modal key routing
mouse.ts # Hover/drag behavior, selection + observer wiring
modals/
jimaku.ts # Jimaku modal flow
kiku.ts # Kiku field-grouping modal flow
runtime-options.ts # Runtime options modal flow
session-help.ts # Keyboard shortcuts/help modal flow
subsync.ts # Manual subsync modal flow
utils/
dom.ts # Required DOM lookups + typed handles
platform.ts # Layer/platform capability detection
```
### 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).
## Flow Diagram
The main process orchestrates a single primary overlay window plus modal surfaces: `main.ts` delegates to composition modules that wire together domain services. Subtitle layers (primary + secondary bar) are rendered in the same overlay renderer process, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough.
```mermaid ```mermaid
flowchart TB flowchart TB
@@ -215,69 +130,82 @@ flowchart TB
style ExtRt fill:#363a4f,stroke:#494d64,color:#cad3f5 style ExtRt fill:#363a4f,stroke:#494d64,color:#cad3f5
``` ```
## Composition Pattern ## Composition pattern
Most runtime code follows a dependency-injection pattern: Runtime code uses dependency injection:
1. Define a service interface in `src/core/services/*`. 1. Put the logic in a service under `src/core/services/`, as pure or side-effect-bounded functions.
2. Keep core logic in pure or side-effect-bounded functions. 2. Build its runtime dependencies in a `src/main/` module. Pass simple dependencies inline; extract an adapter only when it adds behavior or gets reused.
3. Build runtime deps in `src/main/` composition modules; extract an adapter/helper only when it adds meaningful behavior or reuse. 3. Call the service from lifecycle or command wiring.
4. Call the service from lifecycle/command wiring points.
The composition root (`src/main.ts`) delegates to focused modules in `src/main/` and `src/main/runtime/composers/`: `main.ts` gets domain handlers through `createMainRuntimeRegistry()` (`src/main/runtime/registry.ts`), which exposes the barrels in `src/main/runtime/domains/`. Larger clusters, such as app-ready startup, mpv, Jellyfin, AniList tracking, shortcuts, and IPC, are assembled by composers in `src/main/runtime/composers/`. Many handlers take a `*MainDeps` object built by a `createBuild*MainDepsHandler` builder, which keeps side effects out of the unit under test.
- `startup.ts` — argv/env processing and bootstrap flow Composers declare their inputs with `ComposerInputs<T>` and results with `ComposerOutputs<T>` from `src/main/runtime/composers/contracts.ts`. A missing dependency then fails at compile time.
- `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`: ### IPC boundary
- composer input surfaces are declared with `ComposerInputs<T>` so required dependencies cannot be omitted at compile time Channel names live in `src/shared/ipc/contracts.ts` and payload validators in `src/shared/ipc/validators.ts`. Renderer payloads are validated at the IPC entry points (`src/core/services/ipc.ts`, `src/core/services/anki-jimaku-ipc.ts`) before any domain handler runs. See [IPC + runtime contracts](/ipc-contracts) for the full rules.
- composer outputs are declared with `ComposerOutputs<T>` to keep result contracts explicit and stable
- builder return payload extraction should use shared type helpers instead of inline ad-hoc inference
This keeps side effects explicit and makes behavior easy to unit-test with fakes. ### Runtime state ownership
Additional conventions in the current code: Some domains, such as AniList token, queue, and media-guess state, use reducer-style transitions:
- `main.ts` uses `createMainRuntimeRegistry()` (`src/main/runtime/registry.ts`) to access domain handlers (`startup`, `overlay`, `mpv`, `ipc`, `shortcuts`, `anilist`, `jellyfin`, `mining`) without importing every runtime module directly. - Composition modules own the mutable state and expose narrow `get*`/`set*` accessors.
- Domain barrels in `src/main/runtime/domains/*` re-export runtime handlers + main-deps builders, while composers in `src/main/runtime/composers/*` assemble larger runtime clusters. - Handlers change another domain's state only through its transition helpers in `src/main/state.ts`, never by mutating the object directly.
- Many runtime handlers accept `*MainDeps` objects generated by `createBuild*MainDepsHandler` builders to isolate side effects and keep units testable. - A transition may update derived counters or snapshots, but must leave metadata it does not own untouched.
- Tests for these domains check both the fields that should change and the ones that should not.
### IPC Contract + Validation Boundary ## Playback startup flow
- Central channel constants live in `src/shared/ipc/contracts.ts` and are consumed by both main (`ipcMain`) and renderer preload (`ipcRenderer`) wiring. A SubMiner-managed launch (the `subminer` launcher, the app's own playback, or the packaged Windows shortcut) starts mpv, injects the plugin, and brings up the overlay. 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` has no settings, so command-line options always win.
- Runtime payload parsers/type guards live in `src/shared/ipc/validators.ts`.
- Rule: renderer-supplied payloads must be validated at IPC entry points (`src/core/services/ipc.ts`, `src/core/services/anki-jimaku-ipc.ts`) before calling domain handlers.
- Malformed invoke payloads return explicit structured errors (for example `{ ok: false, error: ... }`) and malformed fire-and-forget payloads are ignored safely.
### Runtime State Ownership (Migrated Domains) Once mpv is up, exactly one of two triggers starts the overlay:
For domains migrated to reducer-style transitions (for example AniList token/queue/media-guess runtime state), follow these rules: - On a first launch, the launcher sets `auto_start=yes` and the plugin's `file-loaded` hook starts the app once the socket is ready.
- When the app is already running, or for `--start-overlay` and YouTube flows, the launcher attaches over the app control socket and suppresses the plugin's auto-start.
- Composition/runtime modules own mutable state cells and expose narrow `get*`/`set*` accessors. Both paths end in the same app bring-up, which then runs the program lifecycle below.
- Domain handlers do not mutate foreign state directly; they call explicit transition helpers that encode invariants.
- Transition helpers may sync derived counters/snapshots, but must preserve non-owned metadata unless the transition explicitly owns that metadata.
- 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.
## Program Lifecycle ```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
- **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. Entry["Managed launch<br/>subminer CLI · app · Windows shortcut"]:::entry
- **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. Entry --> Cfg["Launcher reads config.jsonc<br/>→ plugin runtime config"]:::extrt
- **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`. 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
- **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. Spawn --> Boot["Plugin boot · read_options('subminer')<br/>empty subminer.conf; CLI opts win"]:::extrt
- **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). Boot --> Sock["mpv IPC socket ready"]:::proc
- **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. Sock --> Who{"Overlay trigger"}:::decision
- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, stops the AnkiConnect proxy server, and cleans Anki/AniList state.
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 sockets in this flow are described in [IPC + runtime contracts](./ipc-contracts#runtime-sockets).
## Program lifecycle
1. **Module init.** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs services, and wires dependencies. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
2. **Startup.** `--generate-config` writes the template and exits. Otherwise `app-lifecycle.ts` takes the single-instance lock and registers Electron lifecycle hooks.
3. **App ready.** `composeAppReadyRuntime()` reloads config strictly, resolves keybindings, creates the `MpvIpcClient` (which connects and observes subtitle and playback properties), and starts the runtime options manager, subtitle timing tracker, and immersion tracker.
4. **Overlay.** `initializeOverlayRuntime()` creates the overlay window, registers global shortcuts, and tracks mpv's window bounds through the active window tracker. `src/main/runtime/overlay-mpv-sub-visibility.ts` hides mpv's own subtitles while the overlay shows them.
5. **Background warmups.** MeCab, Yomitan, JLPT and frequency dictionaries, the optional Jellyfin remote session, Discord presence, AniList token refresh, and the optional AnkiConnect proxy start asynchronously. `startupWarmups` controls which run; its low-power mode defers everything except Yomitan.
6. **Runtime.** Event-driven. mpv property changes, IPC messages, CLI commands, shortcuts, and config hot-reloads route through handlers and composers. Subtitle text goes through `SubtitleProcessingController` (normalize, tokenize, merge) and out to the overlay renderer and modals.
7. **Shutdown.** `onWillQuitCleanup` tears down the tray, config watcher, shortcuts, WebSocket and texthooker servers, mpv socket, window tracker, and Yomitan parser window. It flushes the immersion tracker to SQLite and stops Jellyfin, Discord, and the AnkiConnect proxy.
```mermaid ```mermaid
flowchart TB flowchart TB
@@ -341,11 +269,11 @@ flowchart TB
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5 style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
``` ```
## Subtitle Prefetch Pipeline ## Subtitle prefetch
SubMiner can pre-tokenize upcoming subtitle lines before they appear on screen. When an external subtitle file (SRT, VTT, or ASS) is detected on the active track, the `SubtitlePrefetchService` parses all cues via the `SubtitleCueParser`, identifies a priority window of upcoming lines based on the current playback position, and tokenizes them in the background through the same pipeline used for live subtitles. Results are stored directly into the `SubtitleProcessingController` cache, so when a subtitle actually appears during playback, it hits a warm cache and renders in ~30-50ms instead of ~200-320ms. SubMiner tokenizes upcoming subtitle lines before they appear, so they render from a warm cache. `SubtitlePrefetchService` (`src/core/services/subtitle-prefetch.ts`) gets the cue list from the active track: an external subtitle file, or for local media an embedded text track extracted with ffmpeg. It parses the cues with `subtitle-cue-parser.ts`, picks a window of upcoming lines from the playback position, and tokenizes them through the live pipeline, storing results in the `SubtitleProcessingController` cache.
The prefetcher yields to live subtitle processing (which always takes priority over background work) and re-computes its priority window on seek. Cache invalidation events (e.g. marking a word as known) trigger re-prefetching of the current window to keep results fresh. Live subtitle processing always takes priority; the prefetcher pauses while the on-screen line is being processed. It recomputes its window on seek and re-prefetches when the cache is invalidated, for example after a word is marked known.
```mermaid ```mermaid
flowchart TB flowchart TB
@@ -354,7 +282,7 @@ flowchart TB
classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
SubFile["External Sub File"]:::init SubFile["Subtitle Track"]:::init
Parse["Cue Parser"]:::phase Parse["Cue Parser"]:::phase
Window["Upcoming Lines"]:::phase Window["Upcoming Lines"]:::phase
Tokenize["Pre-tokenize"]:::warmup Tokenize["Pre-tokenize"]:::warmup
@@ -371,22 +299,11 @@ flowchart TB
style Render stroke-width:2px style Render stroke-width:2px
``` ```
## Why This Design ## Extension rules
- **Smaller blast radius:** changing one feature usually touches one service. - Add behavior to a service in `src/core/services/` or a focused module under `src/main/runtime/`. Keep new logic out of `main.ts`.
- **Better testability:** most behavior can be tested without Electron windows/mpv. - For changes that span startup, overlay, mpv, or integration wiring, compose through `src/main/runtime/domains/` and `src/main/runtime/composers/` instead of wiring directly in `main.ts`.
- **Better reviewability:** PRs can be scoped to one subsystem. - Add a cross-process channel in `src/shared/ipc/contracts.ts` first, validate it in `src/shared/ipc/validators.ts`, then wire the handler. See [IPC + runtime contracts](/ipc-contracts#add-a-new-ipc-action).
- **Backward compatibility:** CLI flags and IPC channels can remain stable while internals evolve. - Config is split by domain under `src/config/definitions/` and `src/config/resolve/`. Keep config changes in the matching domain file.
- **Runtime registry + domain barrels:** `src/main/runtime/registry.ts` and `src/main/runtime/domains/*` reduce direct fan-in inside `main.ts` while keeping domain ownership explicit. - Keep CLI flags and IPC channels stable unless a change is meant to break them.
- **Extracted composition root:** `main.ts` delegates to focused modules under `src/main/` and `src/main/runtime/composers/` for lifecycle, IPC, overlay, mpv, shortcut, and integration wiring. - Add or update focused tests when a runtime boundary or contract changes, including malformed-payload tests for IPC.
- **Split MPV service layers:** MPV internals are separated into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), and properties/render metrics modules for maintainability.
- **Config by domain:** defaults, option registries, and resolution are split by domain under `src/config/definitions/*` and `src/config/resolve/*`, keeping config evolution localized.
## Extension Rules
- Add behavior to an existing service in `src/core/services/*` or create a focused runtime module under `src/main/runtime/*`; avoid ad-hoc logic in `main.ts`.
- Add new cross-process channels in `src/shared/ipc/contracts.ts` first, validate payloads in `src/shared/ipc/validators.ts`, then wire handlers in IPC runtime modules.
- See also the contributor IPC onboarding page: [IPC + Runtime Contracts](/ipc-contracts).
- If change spans startup/overlay/mpv/integration wiring, prefer composing through `src/main/runtime/domains/*` + `src/main/runtime/composers/*` rather than direct wiring in `main.ts`.
- Keep service APIs explicit and narrowly scoped, and preserve existing CLI flag / IPC channel behavior unless the change is intentionally breaking.
- Add or update focused tests (including malformed-payload IPC tests) when runtime boundaries or contracts change.
+116
View File
@@ -0,0 +1,116 @@
import { expect, test } from 'bun:test';
import { onRequest, resolveArchiveRoute, type ArchiveBucket } from './functions/v/[[path]]';
// In-memory stand-in for the R2 binding, including the range and precondition behavior
// the function relies on.
function fakeBucket(objects: Record<string, string>): ArchiveBucket {
return {
async get(key, options) {
const content = objects[key];
if (content === undefined) return null;
const bytes = new TextEncoder().encode(content);
const httpEtag = `"${key}"`;
if (options?.onlyIf?.get('if-none-match') === httpEtag) {
return { size: bytes.length, httpEtag };
}
const rangeHeader = options?.range?.get('range');
const match = rangeHeader ? /^bytes=(\d+)-(\d*)$/.exec(rangeHeader) : null;
if (match) {
const offset = Number(match[1]);
const end = match[2] ? Number(match[2]) : bytes.length - 1;
const slice = bytes.slice(offset, end + 1);
return {
size: bytes.length,
httpEtag,
range: { offset, length: slice.length },
body: new Blob([slice]).stream(),
};
}
return { size: bytes.length, httpEtag, body: new Blob([bytes]).stream() };
},
};
}
const bucket = fakeBucket({
'v/0.19.6/index.html': '<h1>home</h1>',
'v/0.19.6/usage.html': '<h1>usage</h1>',
'v/0.19.6/404.html': '<h1>missing</h1>',
'v/0.19.6/assets/app.abc123.js': 'console.log(1)',
});
function request(path: string, init?: RequestInit) {
return onRequest({
request: new Request(`https://docs.subminer.moe${path}`, init),
env: { DOCS_ARCHIVES: bucket },
});
}
test('archive routes follow the clean-URL layout of the built archives', () => {
expect(resolveArchiveRoute('/v/0.19.6/usage')).toEqual({
kind: 'lookup',
version: '0.19.6',
keys: ['v/0.19.6/usage.html', 'v/0.19.6/usage/index.html'],
});
expect(resolveArchiveRoute('/v/0.19.6/')).toEqual({
kind: 'lookup',
version: '0.19.6',
keys: ['v/0.19.6/index.html'],
});
expect(resolveArchiveRoute('/v/0.19.6', '?q=1')).toEqual({
kind: 'redirect',
location: '/v/0.19.6/?q=1',
status: 301,
});
expect(resolveArchiveRoute('/v/')).toEqual({
kind: 'redirect',
location: '/versions',
status: 302,
});
expect(resolveArchiveRoute('/v/0.19.6/%2e%2e/secret')).toEqual({ kind: 'not-found' });
expect(resolveArchiveRoute('/v/latest/')).toEqual({ kind: 'not-found' });
});
test('serves archive pages and hashed assets with their cache policy', async () => {
const page = await request('/v/0.19.6/usage');
expect(page.status).toBe(200);
expect(page.headers.get('content-type')).toBe('text/html; charset=utf-8');
expect(page.headers.get('cache-control')).toBe('public, max-age=3600');
expect(page.headers.get('x-robots-tag')).toBe('noindex, follow');
expect(await page.text()).toBe('<h1>usage</h1>');
const asset = await request('/v/0.19.6/assets/app.abc123.js');
expect(asset.headers.get('content-type')).toBe('text/javascript; charset=utf-8');
expect(asset.headers.get('cache-control')).toContain('immutable');
});
test('missing archive pages fall back to the archive 404 page', async () => {
const response = await request('/v/0.19.6/nope');
expect(response.status).toBe(404);
expect(await response.text()).toBe('<h1>missing</h1>');
const unknownVersion = await request('/v/0.1.0/usage');
expect(unknownVersion.status).toBe(404);
});
test('supports byte ranges, conditional requests, and HEAD', async () => {
const partial = await request('/v/0.19.6/usage', { headers: { Range: 'bytes=4-8' } });
expect(partial.status).toBe(206);
expect(partial.headers.get('content-range')).toBe('bytes 4-8/14');
expect(await partial.text()).toBe('usage');
const notModified = await request('/v/0.19.6/usage', {
headers: { 'If-None-Match': '"v/0.19.6/usage.html"' },
});
expect(notModified.status).toBe(304);
const head = await request('/v/0.19.6/usage', { method: 'HEAD' });
expect(head.status).toBe(200);
expect(head.headers.get('content-length')).toBe('14');
expect(await head.text()).toBe('');
const post = await request('/v/0.19.6/usage', { method: 'POST' });
expect(post.status).toBe(405);
});
-3
View File
@@ -8,7 +8,6 @@
"@catppuccin/vitepress": "^0.1.2", "@catppuccin/vitepress": "^0.1.2",
"@fontsource/jetbrains-mono": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/manrope": "^5.2.8", "@fontsource/manrope": "^5.2.8",
"@plausible-analytics/tracker": "^0.4.4",
"mermaid": "^11.12.3", "mermaid": "^11.12.3",
}, },
"devDependencies": { "devDependencies": {
@@ -143,8 +142,6 @@
"@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="], "@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="],
"@plausible-analytics/tracker": ["@plausible-analytics/tracker@0.4.4", "", {}, "sha512-fz0NOYUEYXtg1TBaPEEvtcBq3FfmLFuTe1VZw4M8sTWX129br5dguu3M15+plOQnc181ShYe67RfwhKgK89VnA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
+689 -6
View File
@@ -1,10 +1,693 @@
# Changelog # Changelog
## v0.14.0 (2026-05-12) ## v0.20.1 (2026-10-03)
**Added**
- **YouTube Browser Window**:
- Browse YouTube with your login kept across restarts. Open it with `subminer youtube`, `subminer yt`, or tray **Browse YouTube**.
- Clicking a video plays it in mpv. Middle-click, Shift/Ctrl-click, or the right-click menu adds it to mpv's playlist.
- Queued videos get the same Japanese subtitle setup, and the rest of the queue is kept on Windows.
- Closing the window quits a SubMiner started by `subminer youtube`.
- **Whisper Subtitles for YouTube**:
- Set `youtube.subtitleSource` to `whisper` to transcribe YouTube videos with Whisper instead of using YouTube's captions.
- The video stays paused while Whisper transcribes, and the subtitle generation modal shows progress.
- The subtitle generation modal (`Ctrl+Shift+G`) now works on YouTube videos, including Windows stream playback.
- **Scroll Wheel and Mouse Bindings**: `keybindings` now accepts scroll wheel keys, and the settings key editor can capture them. Mouse button and wheel bindings from mpv now work while the cursor is over the overlay, including on Hyprland.
- **Run Commands from Session Help**: During playback, press `Enter` or double-click a row in session help to run that command.
**Changed**
- **Release Assets**: Releases no longer include package-size reports.
**Fixed**
- **Hyprland Overlay**: The overlay stays clickable after mpv goes fullscreen with `mpv.backend: x11`, and it no longer interrupts mpv while mpv enters fullscreen.
- **mpv Key Bindings**: Bindings from mpv such as `9`/`0` for volume now work while the overlay has focus, even if the overlay loaded before mpv connected.
- **Jellyfin**:
- The Japanese subtitle track is selected as soon as it downloads, without waiting for the other tracks.
- Japanese and English tracks are auto-selected on episodes whose embedded subtitles are images.
- Timing review previews and mined card audio use the audio track you are playing.
- **Media Timing Review**: Trim handles are now thin lines on the clip edges, and original subtitle boundaries no longer cover the waveform. The preview cursor no longer stutters on macOS with Bluetooth headphones.
- **YouTube Subtitles**: Japanese subtitles no longer fail with "HTTP 429" on videos where YouTube also lists a machine-translated Japanese track.
- **Stats**:
- Mined cards are counted once in session and lifetime stats.
- Word and kanji dates and the new-words charts stay accurate after deleting sessions. Existing data is repaired in the background.
- Overview totals, charts, and the activity calendar refresh after deleting sessions.
- **Yomitan Data Safety**: Electron is updated to 43.7.7. SubMiner now refuses to load your Yomitan data with an unsupported or downgraded runtime. Automatic character dictionary changes stop if Yomitan suddenly reports no dictionaries.
- **AppImage in Sandboxes**: The AppImage now opens its window when launched in a sandbox such as `firejail --appimage`.
- **Ctrl+C Crash**: SubMiner no longer crashes with "write EPIPE" when the terminal command that started it is stopped with Ctrl+C.
**Docs**
- **Docs Site**: The docs site is shorter and easier to scan, with a key and default table for each config block. Outdated pages were corrected, and the MeCab install command for Arch Linux is fixed.
- **Stats Docs**: The docs now cover the Overview refresh after deleting sessions and how discovery dates are kept after old sessions are pruned.
<details>
<summary>Internal changes</summary>
**Internal**
- **Release Notes**: Release notes and `CHANGELOG.md` are now generated as short items per feature or area.
- **Dev Profile**: Development launches use a separate `SubMiner-dev` profile unless production-profile access is requested.
- **Dependencies**: undici is updated to 7.29.1 to clear the security advisories that were blocking CI.
- **Package Checks**: CI no longer compares package sizes. Package content checks still run.
</details>
## v0.20.0 (2026-09-23)
**Added**
- **Japanese Subtitle Generation**:
- Generate Japanese SRT subtitles locally with whisper.cpp. Start it from a new modal (Ctrl+Shift+G), from the generate button in an empty subtitle sidebar, or with `subminer generate-subs`.
- Generation shows progress, can be cancelled, and loads the finished subtitles into mpv automatically.
- Pick an official multilingual Whisper model, including quantized variants, with size and accuracy guidance. You can download it in the app or point Settings at a model you already have.
- `large-v3-turbo` is recommended when CUDA support is detected, and `small` otherwise.
- whisper-cli, ffmpeg, and ffprobe are found on PATH unless you override them. SubMiner names any missing tools before a download starts.
- An optional "Focus on spoken dialogue" mode uses a separately downloaded Silero VAD model. It keeps audible sections it is unsure about, so dialogue under music is not dropped, but songs may also be transcribed.
- Long passages are split near speech starts or quiet pauses to reduce subtitles that appear too early. When an eligible embedded or external subtitle track is loaded in mpv, it guides the split points.
- Each passage runs in a fresh Whisper process, which prevents repeated-character output.
- **Subtitle Selection Modal**:
- An optional modal for choosing primary and secondary mpv subtitle tracks.
- Turn it on in Settings under Behavior, then press g followed by s to open it. Turning it off restores mpv's own subtitle selection binding.
- Single-key actions take priority over configured key sequence prefixes.
- Conflicting sequences are disabled with a warning, and the existing y commands stay reserved.
- **Subtitle Sidebar Copy**:
- Select dialogue across several sidebar rows and copy it without timestamps using Ctrl/Cmd+C or the Copy button.
- Selecting text does not seek playback and does not require mining a card.
- **Media Timing Review Screenshot Picker**:
- Choose the still screenshot separately from the audio range, with a live preview and its own time slider.
- Step through decoded frames one at a time to get the exact frame you want.
- Works with local video and with seekable remote streams such as Jellyfin.
- **mpv Keybindings in the Overlay**:
- The overlay now picks up keyboard bindings from mpv defaults, `input.conf`, and loaded scripts when they do not conflict with SubMiner.
- SubMiner controls and bindings you explicitly disabled take precedence.
- These bindings apply only to the current session and are not listed in the help menu.
- **Jimaku Live Action Search**: The Jimaku modal has new Anime and Live action tabs, so you can search Jimaku's live action catalogue as well as anime. Use Arrow Left and Arrow Right to switch tabs.
- **TMDB Live-Action Library**:
- Live-action dramas and movies in the stats Library now get posters, synopses, and titles from TMDB.
- Release builds include a project key. Setting `tmdb.apiKey` or `tmdb.apiKeyCommand` overrides it, and one of them is required when running from source.
- Titles that AniList cannot match are looked up on TMDB automatically when the parsed filename exactly matches a Japanese live-action title. For everything else, use the new **Link to TMDB** action.
- Entries linked to the same TMDB title merge into one card, and the Library kind selector has a new Live Action option.
- If a replacement download fails during provider reassignment, the previous link and artwork are kept. Merges and sync keep AniList and TMDB identities separate, and the merge dialog explains mixed selections instead of failing.
- **YouTube Library Kind**:
- YouTube channels are now their own Library media kind. Existing channel entries migrate automatically, and viewing history and manual video assignments are unchanged.
- New All Titles, Anime, and YouTube Library filters.
- Channels are excluded from AniList matching, season repair, and duplicate recommendations.
- Merges and video moves can no longer combine an anime entry with a YouTube channel.
**Changed**
- **Launcher Uses Bundled Bun**:
- Every installed and downloadable launcher now runs on the Bun runtime that ships with SubMiner. A system Bun is no longer needed.
- Recognized legacy launchers migrate automatically.
- Windows gets a `subminer.cmd` launcher download.
- First-run setup is reduced to one optional launcher control. Runtime repair guidance appears only when it is needed.
- **Faster Sync Transfers**:
- Sync between compatible macOS and Linux machines now uses compressed, incremental rsync transfers.
- The last snapshot received from each peer is cached, which reduces traffic on later syncs.
- Machines without a compatible rsync, including Windows, fall back to compressed scp.
- Older peers still work without the upload cache.
- Transfers abort after 30 minutes.
- **Stats Server Request Safety**:
- The stats server now accepts loopback hosts only and rejects requests from browser origins other than its own.
- Requests that change data must send an `application/json` body. Scripts that POST to the server need to set a JSON content type.
- The in-app stats overlay now loads from the local server, so it gets the same protection.
- Dashboards served through a reverse proxy or Tailscale Serve are no longer supported.
- **Smaller Downloads**:
- Installers and unpacked apps are smaller. Demo media, source maps, TypeScript sources, test fixtures, and unused Koffi binaries are no longer packaged.
- All windows now share one Japanese UI font.
- Release builds publish package size reports that compare against the previous release.
- **Bundled Yomitan**: Updated with upstream Yomitan 26.9.8 changes, including historical Japanese kana transformations, Ukrainian language support, and improvements to Anki duplicate searches and audio retrieval.
**Fixed**
- **Jellyfin 12 Compatibility**:
- Playback, subtitle, artwork, and remote-control requests now authenticate with the `ApiKey` query parameter, so the integration works on Jellyfin 12, where legacy authorization is off by default.
- "Play on SubMiner" stays available. The cast connection now answers keep-alive requests and reconnects when the server stops responding, instead of silently dying after about a minute.
- The Jellyfin "now playing" bar clears when you close or finish a cast video instead of running on to the end of the episode.
- Cards mined during Jellyfin playback get the episode title in the misc info field again instead of "Unknown media".
- **Jellyfin Privacy and Playback**:
- Jellyfin streams no longer leak titles taken from the stream URL, or stream URLs that contain credentials, into metadata lookups, Anki source fields, Discord presence, stats, or AniList retries.
- Previously cached metadata that contained credentials is cleaned up. Watch history and library assignments are not touched.
- Jellyfin playback and casting now use your configured mpv executable, so they work when mpv is installed outside PATH. Portable plugins next to that executable are detected.
- **Anki Mining**:
- New `ankiConnect.fields.wordAudio` setting reads word audio separately from the sentence audio field. This fixes animated images that started moving immediately when `fields.audio` pointed to `SentenceAudio`. Existing animated images need to be regenerated to pick up the fix.
- Sentence furigana on word cards stays in sync with the full stats-search context and with expanded timing review selections. Stale readings are cleared if regeneration fails.
- Closing the overlay while media timing review is still loading now cancels the review, resumes playback if the review paused it, and cleans up the hidden preview player.
- `ankiConnect.media.maxMediaDuration: 0` now means unlimited when mining from the stats dashboard, matching overlay mining.
- Invalid AnkiConnect, Kiku, and Senren settings are now rejected with a warning and fall back to defaults.
- **Stats Server Stability**:
- A port conflict is now reported in a status notification instead of crashing SubMiner.
- Simultaneous startup requests share one server start. Stopping the background server no longer disconnects dashboards open in the foreground.
- Shutdown waits only a limited time for active requests to finish.
- Malformed resource IDs, and ID lists with any invalid entries, are rejected before Library changes or cover backfills run.
- **Subtitle Sidebar**:
- Clicking a cue no longer leaves the row focused, and Space no longer seeks back to a focused cue. Enter still seeks to the focused cue, and Space keeps its configured playback action.
- The sidebar stays near the current playback position during gaps when the subtitle file has a cue that starts at zero.
- **Settings Save Feedback**:
- Settings marked LIVE no longer show false restart warnings, including for notifications and subtitle generation.
- When a save mixes live and restart-only changes, the live changes apply right away and only the changed sections that need a restart are listed.
- **Overlay Windows**:
- On Hyprland, recovery dialogs stay above SubMiner windows so overlay placement updates no longer cover their Wait and Close buttons.
- On Linux, a delayed close callback during teardown can no longer reopen the overlay.
- **First Launch on macOS**: SubMiner no longer exits on first launch when the config directory does not exist yet.
**Docs**
- **Launcher**: Documented the launcher install that uses the bundled runtime, migration from legacy launchers, package-managed updates, and the bundled Bun runtime's MIT and LGPL notices. The AUR package installs these notices under `/usr/share/licenses/subminer-bin`, and they are also included in `subminer-assets.tar.gz`.
- **Subtitle Generation**: Documented model choice, VAD behavior, splitting guided by a reference track, fallback behavior, and known limits.
- **Subtitle Selection**: Documented the subtitle selector setting, its shortcut override, and the primary and secondary track controls.
- **Settings**: Clarified save feedback for live settings, warnings for saves that mix live and restart-only changes, and how subtitle generation settings reload.
- **Jellyfin**:
- Clarified that Windows mpv playback and Jellyfin casting can use a configured executable path instead of PATH.
- Documented how Jellyfin media titles and stats identities keep stream credentials out of metadata.
- **Stats Library**:
- Documented TMDB linking, provider reassignment, merge compatibility, and caching of the credential command's output.
- Documented YouTube channel filtering and video statistics in the Library.
- **Mining**:
- Documented choosing the screenshot separately in media timing review.
- Documented the separate word audio field mapping, including that existing animated images need to be regenerated.
- **Sync**: Documented compressed transfers, where the incremental sync cache is stored, and compatibility with older peers.
<details>
<summary>Internal changes</summary>
**Internal**
- Removed duplicate source and launcher smoke runs from the reusable CI quality gate. Every distinct test lane and failure artifact is kept.
- Replaced mislabeled dist source reruns with a small Electron-runtime smoke check. It covers compiled stats startup, the HTTP service, native SQLite, port conflicts, and cleanup.
</details>
## Previous Versions
<details>
<summary>v0.19.x</summary>
<h2>v0.19.6 (2026-09-04)</h2>
**Added** **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. - **Card Timing Review**:
- Optional pre-generation timing review for word, sentence, and audio cards, with a speech-weighted waveform that flattens background noise so dialogue edges stand out clearly.
- The clip end automatically snaps back to where the line's dialogue actually ends once the waveform loads, with drag and keyboard adjustments available.
- Audio preview includes a sweeping playhead that plays the clip to its true end, even on high-latency outputs like Bluetooth headphones.
- Previous and next subtitle lines can be pulled onto the card with `P`/`N` (or the Prev/Next steppers) and removed with Shift; the sentence preview and waveform markers update automatically.
- Cancelling lets you keep a card without media, and the review can be toggled on or off for the session.
- **Senren Field Grouping**:
- Enable `ankiConnect.isSenren` to merge duplicate mined cards using Senren's scene-switching markup, grouping sentence, furigana, audio, picture, and misc-info fields.
- Supports the same auto/manual/disabled modes as Kiku, including the manual merge modal; only one of Senren or Kiku can be enabled at a time.
**Changed**
- **Remote Stream Mining Performance**: Mining a card from a remote stream (Jellyfin and other HTTP sources) now downloads the clip window once and reuses it for the timing review waveform, audio preview, audio extraction, and screenshot, instead of re-fetching the stream at each step; the temporary file is cleaned up after ten minutes of inactivity or on exit.
- **TsukiHime Release Filtering**: The TsukiHime modal's Japanese and secondary-language tabs now filter the release list by the subtitle languages each release actually carries, and report when no release has subtitles for the active tab.
**Fixed**
- **Subtitle & Mining Accuracy**:
- Broadcast-style captions that split one sentence across two on-screen rows (e.g. Crunchyroll Japanese subs) now merge into a single line for the sidebar and mined cards, while separate speakers, sound effects, and labeled turns still stay on their own lines.
- Mining from the overlay no longer pulls in a lingering row from the previous caption; the mined sentence and clip timing now match what's actually on screen.
- Multi-line copy and mining now select lines backward in timeline order after seeking, instead of in playback encounter order.
- Copying a subtitle, mining a sentence, or recording immersion stats no longer includes the separate furigana line that broadcast ASS captions place above a word.
- **Card Update Notifications**: Dismissed lingering overlay card-update progress when notification settings switch to OSD before an update finishes.
- **Overlay Stability on Hyprland**: Opening a modal window (timing review, Jimaku, session help, and others) while mpv is fullscreen no longer causes the overlay to flicker while the modal loads; the overlay now stays on screen untouched until the modal is ready.
- **Jellyfin Subtitle Sync**: Jellyfin subtitle files now load with zero mpv delay instead of inferring and saving an offset from Japanese and English cue timelines.
- **Secondary Subtitle Visibility**: Native mpv secondary subtitles stay hidden when switching secondary subtitle tracks during playback.
<h2>v0.19.5 (2026-08-30)</h2>
**Fixed**
- **Anki Card Update Progress**: The card-update spinner now stays visible until audio and image updates finish, instead of disappearing early.
- **Anki Word-Card Fields**: Word-card enrichment now writes sentence text and audio to the fields configured in AnkiConnect, while the dedicated sentence-card and audio-card actions keep their existing compatible field names.
- **Overlapping Subtitles**:
- Subtitle lines that start while another line is still on screen now appear alongside it, instead of staying hidden until a track switch or seek.
- Subtitles shown at the same time now stack by their authored screen position, with top signs and song lines above bottom dialogue.
- Half-size ASS furigana is no longer shown as if it were a dialogue line.
- **YouTube Auto Captions**:
- Auto-generated captions now follow their intended timing and two-row roll-up layout.
- Long speech is paged instead of covering the video with a wall of text.
- Explicitly timed sound cues like `[音楽]` no longer cover later dialogue.
<h2>v0.19.4 (2026-08-25)</h2>
**Added**
- **Library Merge & Move**: Duplicate library cards for the same show can now be combined. Select cards in the library grid and use "Merge Selected" to pick which entry to keep and move every episode onto it, preserving sessions, mined cards, and watch time. Episodes can also be reassigned individually via the "→" button, useful when a file lands under a stray title; manual assignments survive later filename parsing, Jellyfin refreshes, and season repair. Exact AniList title matches with compatible seasons now merge automatically, while fuzzy matches surface as dismissible "Possible duplicate" reviews instead of merging silently.
- **Duplicate Line Cleanup Tool**: The Vocabulary tab's new "Duplicates" button scans a chosen time window (7 days through all time) for old karaoke/typeset duplicate-line bursts, shows what it found, and collapses each run to one line once confirmed; `subminer stats cleanup --duplicate-lines` does the same from the terminal, with `--dry-run` and `--lookback-days <n>` options. Watch time and lines-seen totals are left unchanged.
**Changed**
- **Prerelease Release Notes**: Prerelease notes now open with a "Changes since" section listing only what changed versus the previous beta/RC of the same version, above the cumulative highlights, and CI rejects prerelease tags whose committed notes were generated for a different beta/RC.
**Fixed**
- **Subtitle & Karaoke Duplication**:
- Karaoke and animated signs are reconstructed once from their authored text and shown only while actually sung, with original word spacing preserved, instead of flooding the overlay, subtitle sidebar, immersion history, mining, or stats with glyph fragments, per-frame color phases, and repeated animation events.
- Decorative layers (highlight sweeps, glow/shadow copies, symbol-font decoration, particle swarms, hidden or zero-scaled text) stay out of published text, while ordinary repeated dialogue, positioned signs, wrapped lyric rows, and multi-row CC-style blocks still display correctly.
- Embedded subtitle tracks on network-mounted (SMB/NFS) media are extracted and parsed again instead of falling back to live-text-only, restoring karaoke reconstruction, sidebar cues, and mining for releases that only ship subtitles inside the container.
- Secondary subtitles go through the same deduplication pipeline as primary subtitles and no longer clip display after about four lines.
- Event-heavy karaoke files that previously stalled subtitle loading for several seconds now parse in well under a second.
- **Character Dictionary Reliability**:
- Generation, merged rebuilds, and imports no longer freeze the app on large dictionaries; snapshot I/O, archive building, and image/name lookup caches moved off the UI's critical path.
- Dictionaries are reused instead of regenerated when MeCab finds no name splits.
- Cached portraits restore correctly after the portrait index finishes loading post-tokenization.
- Desktop progress notifications on Linux AppImage installs update in place instead of flickering, fixing a bug where the AppImage's bundled libraries broke the system notification helper.
- **Overlay Startup & Modals**:
- The macOS window-tracking helper targets macOS 12.0+ instead of requiring the build machine's exact macOS version, fixing crashes on older systems like Ventura that left the overlay stuck on "Overlay loading".
- mpv IPC connection attempts time out and retry, showing an actionable error if content still isn't ready after 30 seconds.
- Dedicated overlay modals are prewarmed on macOS and Windows so shortcuts open them promptly.
- On macOS, reused modals and the stats window open above fullscreen mpv on its current Space instead of jumping to another desktop.
- **Wayland File Drop**: Fixed native Wayland drag-and-drop from file managers such as Thunar, so subtitle and video files dropped on the visible overlay are resolved and forwarded to mpv.
- **Windows Mouse Lag**: Fixed system-wide mouse lag on Windows while SubMiner is running, caused by the overlay's global mouse hook for click-through forwarding and by the mpv window tracker blocking the app on repeated PowerShell lookups.
- **Sentence Mining Audio & Clips**: Sentence-audio generation no longer times out on slow network-mounted media with many subtitle/font streams (bounded FFmpeg probing, two-minute extraction budget, clearer error reporting), and mined audio/animated AVIF clips now capture the subtitle line that was actually mined by snapshotting the clip range at lookup time instead of reading live mpv state later.
- **Stats Performance & Reliability**: Immersion stats storage now sets its SQLite busy timeout before WAL setup, avoiding transient lock errors under concurrent writes. Deletes in the stats dashboard no longer freeze the UI, run proportional to what's deleted instead of rebuilding full lifetime summaries, retry safely if the delete worker crashes, and no longer rescan the whole library when deleting very common words; a new index also makes large session deletes drop from minutes to milliseconds. Library merges, video moves, and AniList reassignments got the same lifetime-summary fix.
- **Vocabulary Stats Accuracy**: Vocabulary totals and charts now count all tracked vocabulary instead of only the first page, new-word history uses corrected daily rollups (fixing legacy timestamp and time-zone issues), summary cards refresh automatically after edits to the exclusion list, and rapid exclusion edits no longer race each other.
- **Rofi MKV Thumbnails**: Fixed missing MKV thumbnails in the Linux rofi picker when system thumbnailer registrations only advertise legacy Matroska MIME aliases.
<details>
<summary>Internal changes</summary>
**Internal**
- Docs Site Indexing: Excluded the `/main/` and `/v/<version>/` docs trees from search indexing (self-referential canonical, `noindex,follow`, matching `X-Robots-Tag`) so crawlers focus on current docs instead of ~30 archived copies of every page, and restored `<lastmod>` dates in the docs sitemap that were silently dropped by production builds.
</details>
<h2>v0.19.3 (2026-08-13)</h2>
**Added**
- Changelog Modal: Adds an in-app changelog you can open from the tray ("View Changelog") or the "What's New" button on the update notification, so the notification stays reachable while you read. It shows the newest published release notes (falling back to the bundled changelog if that fetch fails), folds older versions while keeping the current one expanded, and supports keyboard navigation (`J`/`K`/arrows, `Enter`, `R`, `Esc`).
**Changed**
- Subtitle Tokenization Performance: Reworks subtitle dictionary lookups to cut per-line work roughly in half, cache repeated lookups across lines, and stop tokenization from competing with on-screen subtitle prefetching. Also fixes several accuracy issues along the way: dropped readings on trailing kana, character names being skipped after a dictionary sync, annotations not refreshing after mining a card, and halfwidth katakana character names losing their reading or being swallowed by other words.
**Fixed**
- Character Dictionary Large Imports: Large character dictionaries (e.g. One Piece) no longer fail to install from a fixed timeout budget; the import now scales its time budget to dictionary size and reports detailed progress (page/character counts, image download progress, elapsed time) instead of one static message.
- Stats Delete Responsiveness: Deleting sessions, episodes, or library entries no longer freezes the stats page or an active video player; deletes are now batched into a single transaction.
- Styled Subtitle Cue Parsing: Heavily typeset subtitles (karaoke, signs) no longer flood the subtitle sidebar with garbage; vector drawing commands are no longer shown as text, and duplicate/animation-burst cues now collapse into one.
- X11 mpv Renderer: Fixes an mpv crash on the first fullscreen toggle for X11/XWayland users with `gpu-next` shaders (e.g. ArtCNN), which was caused by X11 mode forcing the legacy OpenGL renderer.
- X11 Overlay Display Scaling: Fixes the overlay appearing oversized and offset from mpv on X11/XWayland under fractional or mixed-monitor display scaling.
<details>
<summary>Internal changes</summary>
**Internal**
- Subtitle text is now decoded from ASS exactly once at ingest, so the renderer, timing tracker, and tokenizer all share one decoded value instead of each re-deriving it.
- Added per-stage debug timings (`scanMs`, `mecabMs`, `frequencyMs`, `annotateMs`) to the subtitle tokenization pipeline log.
</details>
<h2>v0.19.2 (2026-08-04)</h2>
**Changed**
- Subsync: The sync modal now lets you choose both the reference subtitle (correct timing) and the out-of-sync subtitle to retime, for both alass and ffsubsync. alass can also use the loaded video's audio as a reference for local files. Retiming the secondary track now reloads the result into the secondary slot instead of overwriting the primary subtitle.
**Fixed**
- Streaming Subtitle Tokenization: Jellyfin streams now seed subtitle tokenization directly from the downloaded subtitle file instead of relying on an mpv event that could be missed, and prefetching now runs to the end of the file and clears between episodes. The tokenization cache was raised from 256 to 2500 lines, and parsed cues are no longer lost when the active subtitle track briefly can't be resolved (e.g. switching to an embedded track). Together these prevent episodes from falling back to slow, line-by-line tokenization during playback.
- Overlay: Subtitle lines now appear immediately at their cue time even on a tokenization cache miss, upgrading in place once tokens and annotations are ready, instead of waiting on a line still being processed. A failed tokenization is no longer cached as plain text, so repeated lines get another chance at annotations.
- Background Logging: Background startup now respects the configured logging level when no explicit log level is passed.
<details>
<summary>Internal changes</summary>
**Internal**
- Patched three high-severity dependency advisories (`undici`, `brace-expansion`, `fast-uri`).
</details>
<h2>v0.19.1 (2026-08-01)</h2>
**Added**
- Word Card Type: Adds a setting (Settings > Mining/Anki > Kiku/Lapis Features > "Word Card Type") to choose which card-type flag SubMiner marks on Kiku/Lapis word cards — `word-and-sentence` (default), `click`, `sentence`, `audio`, or `none`. Click cards (`IsClickCard`) can now be flagged, and setting any card-type flag clears the others so a note can't claim two types at once.
**Fixed**
- Yomitan Popup: Fixes the macOS Yomitan popup going inert after mining a card — clicks outside the popup no longer pass through to mpv, and scrolling over the popup scrolls its definitions instead of seeking playback.
- YouTube Playlist Links: Fixes opening a video from a playlist URL (e.g. a Watch Later link with `list=`/`index=`) timing out while probing subtitles, metadata, or the playback URL.
<h2>v0.19.0 (2026-07-29)</h2>
**Added**
- Anki Maturity Highlighting: Known-word subtitle highlights can now be colored by Anki card maturity (new, learning, young, mature), similar to asbplayer. Tier thresholds and colors are configurable, with a runtime toggle and an updated help legend.
- Post-Playback Menu: After a watch-history episode ends, the fzf/rofi launcher returns to that series with options to play the previous or next episode, rewatch, pick another episode, or quit. The pre-playback series menu now offers the previous episode too.
- Delete Library Entries: The stats Library detail view can now delete an entire title in one step (episodes, sessions, subtitle lines, rollups, cover art, and vocabulary counts). Delete progress is now shown app-wide via a progress bar and status toast instead of disappearing when you switch tabs.
- Cross-Machine Sync: Added SSH-based syncing of stats and watch history between machines, available from the tray ("Sync Stats & History") or `subminer sync`, with saved devices, per-host sync direction, background auto-sync, connection testing, manual snapshots, and support for Windows remotes.
- TsukiHime Subtitle Downloads: Added subtitle downloads for the current video via TsukiHime, loading Japanese as the primary track and your configured secondary language directly into mpv.
**Changed**
- Clipboard-Video Shortcut: The "append clipboard video to queue" shortcut is now configurable.
**Fixed**
- AniList Season Resolution: Season 2+ files now resolve to the correct AniList entry instead of silently falling back to season 1 (which mismatched character dictionaries and watch progress). Manual overrides now stay scoped per season, fix both the dictionary and progress tracking together, and also correct per-season cover art.
- Subtitle Annotation Accuracy: Fixed several annotation edge cases, including inconsistent POS exclusions on merged quote-particle tokens, dropped annotations on supplementary-plane kanji, katakana punctuation wrongly treated as noise, and certain kanji vocabulary losing N+1 highlighting eligibility.
- AnkiConnect Proxy Port Conflict: Video startup no longer crashes when another process already holds the configured AnkiConnect proxy port; a notification now explains how to resolve it.
- AppImage Quit Crash: Fixed a "Service Crash" desktop notification appearing after closing a video when running the Linux AppImage.
- Autoplay Pause Timing: Fixed playback resuming a few seconds before subtitle tokenization warmup finished, most noticeable when resuming mid-episode or when a cue starts within the first two seconds.
- Stats Known-Word Count: Fixed stats reporting 0 known words for every session after the known-word cache format changed.
- Stats Library Cover Art: Relinking a title to a different AniList entry now updates its cover in the Library grid immediately instead of leaving a stale, mismatched cover cached.
- Rofi Prompt Spacing: Rofi menu prompts now keep a space before the input field instead of running into the placeholder text.
- Stats Settings & Reliability: Hardened stats settings validation (nested/legacy AnkiConnect config now falls back safely instead of breaking) and stats routes against malformed requests and other edge cases.
- Stats Delete Performance: Deleting sessions, episodes, and library entries is now dramatically faster and no longer stalls playback (e.g. a 12-episode title dropped from about a minute to under a second on a large library); the Vocabulary tab also loads much faster. The first launch after upgrading runs a one-time database migration.
<details>
<summary>Internal changes</summary>
**Internal**
- Added a golden-file regression test corpus for the tokenizer/annotation pipeline, plus scripts to record new fixtures and diff against stock Yomitan.
- Consolidated renderer modal state handling into a descriptor registry.
- Consolidated CI quality checks (PR, stable, and prerelease) into one reusable workflow with mpv plugin tests and dependency audits.
- Removed the unused stats IPC transport and unified stats dashboard HTTP types with the backend contract.
- Added a script to verify known-word highlight tiers against live Anki data outside of playback.
</details>
</details>
<details>
<summary>v0.18.x</summary>
<h2>v0.18.0 (2026-07-10)</h2>
**Added**
- Sentence Audio Normalization: Generated sentence audio is now normalized to -23 LUFS by default, and clips mined from playback mirror mpv's software volume curve with a limiter to prevent clipping. Both behaviors are configurable independently.
- Watch History Command: Added `subminer -H` / `--history` to browse watch history, replay or continue episodes, or pick one via fzf or rofi, with cover art shown in the rofi picker.
**Changed**
- Fzf Preview Layout: Moved fzf previews below launcher menus, giving long titles and metadata more room.
- Known-Word Highlighting: Now compares subtitle and Anki-card readings, preventing false matches between homographs and unrelated words that share a reading, while still supporting matching across kana and kanji spellings.
- Annotation Filtering: Standalone suffix tokens (e.g. さん, れる) are now excluded from JLPT/frequency/N+1 highlighting by default, matching how particles and interjections are treated; configurable via the pos2 exclusion setting.
- App Icon: Replaced the app icon with new pixel-art submarine artwork contributed by the community, used across the app icon, tray, notifications, README, docs site, and Stats page.
- Stats Trend Charts: Overhauled with persisted title visibility, per-chart title limits, "top" and "most recent" ranking modes, an option to show or hide empty days, calendar-aligned periods, and value-sorted tooltips.
**Fixed**
- Background Stats Server: `subminer app` background launches now auto-start the stats server when enabled, and skip startup if one is already running.
- Character Name Highlighting: Character dictionaries now split unspaced native names more reliably, and portraits, highlights, and hover lookup survive punctuation, unmatched text, and competing dictionary matches without incorrectly splitting longer words.
- Highlighting Coverage: Frequency/JLPT highlighting and vocabulary stats now include content adverbs (e.g. 確かに, やはり) and kanji nouns MeCab tags as non-independent (e.g. 日, 点, 以外), while still suppressing interjections, pronouns, and grammar fragments; lexicalized kana expressions like かといって keep their annotations.
- Cover Art Fetching: Stats now fetches AniList cover art as soon as a new series starts playing, and backfills missing art for existing series on the next Stats visit.
- Kiku Field Grouping: The manual field-grouping dialog now stays above fullscreen mpv, remains usable across repeated attempts, closes abandoned windows after timeouts, and reports a clear error when the original card can no longer be loaded.
- Karaoke Subtitle Collapse: Secondary subtitles no longer stack dozens of one-syllable lines during openings and endings; repeated events collapse into one line, capped to a strip at the top.
- Unparsed Token Hover: Subtitle text Yomitan can't parse (truncated inflections, elongation runs) remains hoverable, while staying excluded from highlighting, N+1 candidate math, and vocabulary stats.
- YouTube Streaming: Fixed direct YouTube stream extraction that could corrupt signed URLs and cause ffmpeg 403 errors.
<details>
<summary>Internal changes</summary>
**Internal**
- Test lanes moved to `scripts/test-lanes.ts` with per-directory discovery and isolated per-file timeouts; CI now covers previously orphaned stats, scripts, plugin process-retry, and runtime-compat suites, plus a new stats lane in the change-verification workflow.
</details>
</details>
<details>
<summary>v0.17.x</summary>
<h2>v0.17.2 (2026-06-28)</h2>
**Fixed**
- YouTube Background Cache: Fixed Windows YouTube background media cache startup for YouTube URLs opened directly in mpv, including resolved stream URLs when mpv still exposes the original YouTube playlist entry, so queued Anki media updates can append audio and images after the cache finishes.
- YouTube Subtitle Picker: Manual subtitle picker requests now show an immediate configured notification while SubMiner probes tracks and opens the modal. Subtitle download progress is replaced with a transient success notification after tracks load.
<h2>v0.17.1 (2026-06-27)</h2>
**Added**
- YouTube Media Cache Mode: Adds `youtube.mediaCache.mode` with `direct` and `background` options. Background mode uses a yt-dlp cache download when direct stream extraction is unreliable — creates a text-only card immediately, queues media updates for mined notes, and fills audio/image fields once the download finishes. Progress is announced via overlay/OSD notifications. Downloads are capped at 720p by default (`youtube.mediaCache.maxHeight`). Switching back to direct mode cancels any in-flight background download.
**Fixed**
- Log Export: Fixed log filenames to use the local date so exports around UTC midnight include the current day's logs rather than stale prior-day files. Expanded export redaction to mask IPs, emails, auth and cookie headers, yt-dlp cookie arguments, URL credentials, token/key/password fields, and signed YouTube media URL parameters.
- YouTube Card Media: Improved media generation reliability by sending safer ffmpeg options for resolved streams and skipping stale stream maps (including cached YouTube files). Hardened background cache downloads with IPv4 and extractor retry flags; failed downloads now notify the user and clear queued media updates instead of leaving them silently pending. Stale background cache files are cleaned on startup and before each new download.
<h2>v0.17.0 (2026-06-15)</h2>
**Changed**
- **Subtitle Delay Keybindings**: Updated default overlay subtitle delay and step bindings to match mpv conventions: `z`, `Z`, and `x` adjust `sub-delay`; `Ctrl+Shift+Left/Right` run native `sub-step` with OSD feedback. The previous SubMiner-only adjacent-cue delay action has been removed.
- **Update Notifications**: New installs now default update notifications to overlay-only instead of overlay + system notifications.
**Fixed**
- **Anki – Highlight Word**: Fixed bolding of the mined word in Kiku sentence and sentence-furigana fields when the source Yomitan sentence did not already contain bold markup.
- **Anki – Lapis/Kiku Word Cards**: Fixed word-and-sentence marker missing from Lapis/Kiku word cards enriched through SubMiner, which could hide sentence context on the card front.
- **Anki – Windows Media Generation**: Fixed known-word cache refreshes when no deck is configured, and fixed audio/image generation after background launches by recreating missing FFmpeg temp directories before clipping.
- **Character Dictionary – Windows**: Fixed the Windows "SubMiner mpv" shortcut so character dictionary auto-sync can fall back to mpv's current video path when app media state is not yet ready.
- **Notifications**: Restored the SubMiner app icon on system notifications that do not supply a custom notification image.
- **Overlay – macOS Yomitan Popup**: Fixed Yomitan popup focus after card mining or popup reload; fixed popups staying open when clicking transparent overlay space — click-away now closes the popup and returns click passthrough to mpv without a hide/reappear cycle.
- **Overlay – Linux Auto-Pause Startup**: Fixed the visible overlay on Linux auto-paused startup to stay interactive during the initial measurement gap; startup subtitle cache misses now paint raw text before tokenization finishes, and temporarily empty `sub-text` refreshes are resolved before warm readiness resumes playback.
- **Overlay – Playlist Advance**: Fixed the visible overlay being dismissed when mpv advances to the next playlist item, including when the next episode loads after the warm transition delay.
- **Overlay – Windows Subtitle Bar**: Fixed shaky hover and click interaction on the Windows subtitle bar when a video attaches to an already-running background SubMiner instance.
- **Stats – AniList Linking**: Fixed manual AniList linking from the stats anime page so auto-searches strip the generated "Season N" suffix and query only the anime title.
- **Updates – Linux Support Assets**: Fixed Linux updates to correctly install and refresh the launcher runtime plugin copy and rofi theme alongside AppImage and launcher updates; unrelated SubMiner data directories are now left untouched and plugin copies are staged before replacing the live runtime. Fixed first-launch playback on fresh installs by auto-installing missing managed support assets from the bundled app before mpv starts.
**Docs**
- **Linux Update Flows**: Documented that Linux update flows manage the launcher runtime plugin copy and rofi theme from `subminer-assets.tar.gz`, and that normal launcher playback auto-installs those managed support assets on a fresh install if either is missing.
<details>
<summary>Internal changes</summary>
**Internal**
- **Runtime Modules**: Split main-process runtime wiring into focused modules without changing user-facing behavior; hardened helpers against stale background stats daemon PIDs, stalled subtitle extraction, and dropped async errors.
- **Release CI**: Fixed GitHub release notes to preserve the `What's Changed` and `New Contributors` attribution sections when CI regenerates from the committed changelog; scoped prerelease note reuse to the same base version so a new beta line starts from current fragments.
</details>
</details>
<details>
<summary>v0.16.x</summary>
<h2>v0.16.0 (2026-06-10)</h2>
**Breaking Changes**
- **Notification Mode `both`**: `notificationType: "both"` now routes to overlay + system notifications. Users who previously used `"both"` for mpv OSD + system notifications should set `notificationType` to `"osd-system"` in `config.jsonc`. The `osd` and `osd-system` values remain valid as config-file entries but no longer appear in Settings.
**Added**
- **Overlay Notifications**: New overlay notification stack (Catppuccin Macchiato theme) with configurable screen position (`notifications.overlayPosition`: top-left, top-center, top-right), 3-second transient dismissals, and persistent cards for long-running jobs like character dictionary sync.
- **Notification History Panel**: `Ctrl/Cmd+N` (configurable via `shortcuts.toggleNotificationHistory`) opens a session log of all notifications. Works whether the overlay or mpv has focus; slides in from the notification edge; entries can be individually removed or cleared.
- **Mined-Card Notification Actions**: Mined-card overlay notifications show generated card thumbnails and include an Open in Anki button in both live cards and history entries.
- **Update Notification Action**: Update-available overlay notifications include an Update button to start the app update flow directly from the notification.
- **Stats Search**: New Search tab in Stats for realtime subtitle sentence search with media context, headword matching, and mining actions for source-backed sentence cards or exact-match word/audio cards.
**Changed**
- **AniSkip**: Moved intro detection from the mpv plugin to the SubMiner app. Lookups now cover every file loaded during a session including playlist advances, and `mpv.aniskipEnabled`/`mpv.aniskipButtonKey` hot-reload without restarting playback. The bundled plugin no longer makes network calls. Note: AniSkip now requires the SubMiner app to be connected; plugin-only mpv sessions will not fetch skip windows.
- **Stats Library**: Entries are now split by detected season (season folder first, filename parsing as fallback). Existing combined-series rows are automatically migrated to per-season entries on startup. Cover art and anime details refresh immediately after a manual AniList entry change.
- **Stats Vocabulary**: Remembers Hide Known/Hide Kana filters across sessions, applies Hide Kana filtering cross-title, collapses duplicate token variants in exclusions, and matches Related Seen Words by shared readings or kanji.
- **Stats Trends**: Reorganized into Activity, Cumulative Totals, Efficiency, Patterns, and Library sections; disambiguated per-period vs. cumulative charts; added Words/Min and Cards/Hour efficiency charts.
- **Stats Mining**: Sentence cards are created before slow media generation finishes; stored/requested secondary subtitles are preserved before falling back to sidecar or alass-retimed English subtitles; empty `ankiConnect.deck` falls back to Yomitan's mining deck; partial media failures are surfaced.
- **Stats Browsing**: Remembers library card size; retries stored cover art without extra AniList lookups; preserves PNG/WebP MIME types; honors custom AnkiConnect URLs for Browse; shows progress during session deletes.
- **Startup Notifications**: Tokenization, subtitle annotation, and character dictionary status now route through queued overlay notifications in `overlay`/`both` mode instead of falling back to mpv OSD while the overlay loads.
- **Notification Deduplication**: Cycling subtitle modes updates the active overlay card in place rather than stacking duplicates; repeated progress updates (e.g. subsync) tick in place without flickering.
- **Update Notification Default**: New installs default `notificationType` to `both` so update alerts appear in both overlay and system notifications.
**Fixed**
- **AniList Completion**: Entries are now marked completed when a post-watch update reaches the final known episode of the season.
- **AniSkip Markers**: Fixed intro markers disappearing after same-media mpv reloads; fixed metadata detection for intros that start at 0 seconds and common release-group filenames.
- **Jellyfin Session**: Remote session now restarts after setup login so the websocket reconnects with fresh credentials, and stops cleanly on logout.
- **Sentence Card Audio**: Mining a sentence card no longer fills the expression audio field; generated audio goes only to the configured sentence audio field.
- **Stats Mining Fields**: Sentence clips update `SentenceAudio` correctly; word audio uses configured Yomitan sources; English subtitle text is not written to word cards; secondary subtitle auto-selection prefers regular English tracks over Signs/Songs tracks.
- **Overlay Hover Readiness**: Subtitle bars are hoverable and clickable from the first subtitle line on visible overlay startup or resume, without waiting for the next subtitle event.
- **Startup Autoplay**: Playback is released after tokenization and overlay content are ready even when playback begins before the first subtitle line appears.
- **Overlay Startup Feedback**: Restored mpv OSD loading spinner that starts on connect, media open, or overlay request, and clears once the overlay is content-ready and visible.
- **Linux Overlay Input**: Notification close and action buttons remain clickable above subtitle bars on Linux.
<details>
<summary>Internal changes</summary>
**Internal**
- **Build**: `make deps` now initializes git submodules before installing dependencies on a fresh source checkout.
- **Release Tooling**: Release notes now credit contributors and first-time authors resolved from changelog fragments via git and the GitHub API.
- **Changelog Guidance**: PR fragment guidance updated to preserve separate-outcome fragments while directing contributors to consolidate same-PR follow-up notes before adding churn.
</details>
</details>
<details>
<summary>v0.15.x</summary>
<h2>v0.15.2 (2026-06-02)</h2>
**Changed**
- Yomitan: Updated the bundled Yomitan build to the latest vendored revision.
**Fixed**
- Anki - Animated AVIF: Clip timing no longer starts or ends early; word-audio lead-in and clip duration are now aligned to frame boundaries.
- Overlay (Hyprland): Fixed fullscreen overlay alignment - modal, stats, and sidebar content no longer shift below the mpv window.
- Overlay (macOS): Subtitle bars are now interactive immediately after autoplay starts with "wait for overlay to be ready" enabled, without requiring a manual click.
- 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.
<h2>v0.15.1 (2026-05-31)</h2>
**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.
<h2>v0.15.0 (2026-05-29)</h2>
**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>
</details>
<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. - **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. - **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
@@ -58,11 +741,11 @@
**Internal** **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. - 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> </details>
## Previous Versions </details>
<details> <details>
<summary>v0.12.x</summary> <summary>v0.12.x</summary>
@@ -82,8 +765,8 @@
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity. - 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: 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: 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: 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: 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** **Fixed**
+60 -219
View File
@@ -1,263 +1,104 @@
# Character Dictionary # 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 builds a Yomitan dictionary of the characters in the show you are watching, using data from [AniList](https://anilist.co). Character names in subtitles get their own color, and hovering one shows the character's portrait, role, voice actor, and description.
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. Ordinary dictionaries rarely contain character names, so without this every name counts as an unknown word and throws off [N+1 highlighting](/subtitle-annotations#n-1-word-highlighting).
## How It Works ## Turning it on
The feature has three stages: **snapshot**, **merge**, and **match**. 1. Set `subtitleStyle.nameMatchEnabled` to `true`, or turn it on in the Settings window under Annotation Display, Character Names.
2. Optionally set `subtitleStyle.nameMatchImagesEnabled` to `true` to show a small portrait next to each name in the subtitle line.
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. 3. Play an episode.
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.
## 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.
```jsonc ```jsonc
{ {
"anilist": { "subtitleStyle": {
"enabled": true, "nameMatchEnabled": true,
"accessToken": "your-token", "nameMatchImagesEnabled": true,
"characterDictionary": {
"enabled": true,
},
}, },
} }
``` ```
::: tip No AniList account is needed. Logging in to AniList is only for [watch progress sync](/anilist-integration).
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 snapshot.
:::
::: warning The character dictionary does not work when `yomitan.externalProfilePath` is set, because SubMiner then uses another app's Yomitan profile read-only.
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.
:::
## Name Generation ## What happens when you play something
A single character produces many searchable terms so that names are recognized regardless of how they appear in dialogue. SubMiner generates variants for: When a new show starts, SubMiner:
**Spacing and combination:** 1. Guesses the title from the filename and finds it on AniList.
2. Downloads the cast list and portraits.
3. Builds the dictionary and imports it into SubMiner's Yomitan.
- Full name with space: 須々木 心一 A notification shows each step. Once it says the dictionary is ready, names match from the next subtitle line.
- Combined form: 須々木心一
- Family name alone: 須々木
- Given name alone: 心一
**Middle-dot removal** (common in katakana foreign names): Each character gets entries for the full name, family name, given name, and common honorifics (`さん`, `君`, `ちゃん`, `先生`, and others), so `太郎さん` matches as well as `太郎`.
- ア・リ・ス → アリス (combined), plus individual segments SubMiner keeps your most recent shows loaded in one merged dictionary. `anilist.characterDictionary.maxLoaded` sets how many. Starting another show drops the oldest one. Only the current show's characters are highlighted.
**Honorific suffixes** — each base name is expanded with 15 common suffixes: ### How long it takes
| Honorific | Reading | The first time you watch a show, most of the time goes into downloading portraits, one at a time. A typical cast takes seconds to a minute. A very large cast takes much longer: One Piece has over a thousand characters and takes around 10 minutes. After that the show is cached, and later episodes load it from disk.
| --------- | ---------- |
| さん | さん |
| 様 | さま |
| 先生 | せんせい |
| 先輩 | せんぱい |
| 後輩 | こうはい |
| 氏 | し |
| 君 | くん |
| くん | くん |
| ちゃん | ちゃん |
| たん | たん |
| 坊 | ぼう |
| 殿 | どの |
| 博士 | はかせ |
| 社長 | しゃちょう |
| 部長 | ぶちょう |
**Romanized names** — names stored in romaji on AniList are converted to kana aliases so they can match against Japanese subtitle text. ## Correcting AniList matches
This means a character like "太郎" generates entries for 太郎, 太郎さん, 太郎先生, 太郎君, 太郎ちゃん, and so on — all with correct readings. SubMiner can match the wrong show when a filename is ambiguous, for example `Re - ZERO, Starting Life in Another World (2016)` matching a different `Re...` series. To fix it:
## Name Matching 1. Press `Ctrl/Cmd+D` to open the character dictionary manager.
2. Click **Override**, edit the title if needed, search, and pick the right result.
Name matching runs inside Yomitan's scanning pipeline during subtitle tokenization. From the command line:
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`).
Name matches are visually distinct from [N+1 targeting, frequency highlighting, and JLPT tags](/subtitle-annotations) so you can tell at a glance whether a highlighted word is a character name or a vocabulary target.
**Key settings:**
| Option | Default | Description |
| -------------------------------- | --------- | ---------------------------------- |
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
## 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)
The three collapsible sections can be configured to start open or closed:
```jsonc
{
"anilist": {
"characterDictionary": {
"collapsibleSections": {
"description": false,
"characterInformation": false,
"voicedBy": false,
},
},
},
}
```
## Auto-Sync Lifecycle
When `characterDictionary.enabled` is `true`, SubMiner runs an auto-sync routine whenever the active media changes.
**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.
**State tracking** is persisted in `character-dictionaries/auto-sync-state.json`:
```jsonc
{
"activeMediaIds": [170942, 163134, 154587],
"mergedRevision": "a1b2c3d4e5f6",
"mergedDictionaryTitle": "SubMiner Character Dictionary",
}
```
The `maxLoaded` setting (default: 3) controls how many media snapshots stay in the active set. When you start a 4th title, the oldest is evicted and the merged dictionary is rebuilt without it.
## Manual Generation
You can generate a character dictionary from the command line without auto-sync:
```bash ```bash
# Generate for a file or directory # List AniList matches for a file
subminer dictionary /path/to/media
# Generate for current anime (AppImage)
SubMiner.AppImage --dictionary
```
This creates a standalone dictionary ZIP for the target media and saves it alongside the snapshots.
## 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.
Use the in-app selector or CLI to pin the correct AniList media for the whole series:
```bash
# List candidate AniList matches for a file
subminer dictionary --candidates "/path/to/episode.mkv" subminer dictionary --candidates "/path/to/episode.mkv"
# Save the correct AniList media ID for that series # Save the correct AniList ID for that series
subminer dictionary --select 21355 "/path/to/episode.mkv" subminer dictionary --select 21355 "/path/to/episode.mkv"
# Equivalent direct app flags
SubMiner.AppImage --dictionary-candidates --dictionary-target "/path/to/episode.mkv"
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
``` ```
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. The override applies to every episode of that season in the same folder. Other seasons are not affected, even when they share the folder. The override also sets which entry [AniList watch progress](/anilist-integration) updates, so one fix covers both.
## File Structure ## Managing loaded shows
All character dictionary data lives under `{userData}/character-dictionaries/`: The manager (`Ctrl/Cmd+D`) lists the shows in the merged dictionary and marks the current one.
```text - **Remove** drops a show from the dictionary. You cannot remove the show you are watching.
character-dictionaries/ - **Up/Down** changes which show gets dropped first when a new one is added.
snapshots/ - **Override** replaces a show's AniList match.
anilist-170942.json # Per-media character snapshot
anilist-163134.json ## Generating from the command line
merged.zip # Active merged dictionary (imported into Yomitan)
auto-sync-state.json # Tracks active media IDs and revision ```bash
anilist-overrides.json # Manual series-to-AniList overrides subminer dictionary /path/to/media
img/
m170942-c12345.jpg # Character portrait
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. This builds a standalone dictionary file for that file or folder without playing it. With the AppImage directly, use `SubMiner.AppImage --dictionary`.
**ZIP structure** follows the Yomitan dictionary format: ## Configuration
```text Defaults are in the [configuration reference](/configuration).
merged.zip
index.json # { title, revision, format: 3, author: "SubMiner" }
tag_bank_1.json # Tag definitions
term_bank_1.json # Up to 10,000 terms per bank
term_bank_2.json
img/ # Embedded character and VA portraits
```
## Configuration Reference | Key | What it does |
| ---------------------------------------------------------------------- | ------------------------------------------------ |
| Option | Default | Description | | `subtitleStyle.nameMatchEnabled` | Build the dictionary and color character names |
| ---------------------------------------------------------------------- | --------- | --------------------------------------------------------------- | | `subtitleStyle.nameMatchImagesEnabled` | Show a portrait next to matched names |
| `anilist.characterDictionary.enabled` | `false` | Enable auto-sync of character dictionary from AniList | | `subtitleStyle.nameMatchColor` | Color for character names |
| `anilist.characterDictionary.maxLoaded` | `3` | Number of recent media snapshots kept in the merged dictionary | | `anilist.characterDictionary.maxLoaded` | Number of recent shows kept in the dictionary |
| `anilist.characterDictionary.profileScope` | `"all"` | Apply dictionary to `"all"` Yomitan profiles or `"active"` only | | `anilist.characterDictionary.collapsibleSections.description` | Show the description expanded in the popup |
| `anilist.characterDictionary.collapsibleSections.description` | `false` | Start Description section expanded | | `anilist.characterDictionary.collapsibleSections.characterInformation` | Show age, birthday, and similar details expanded |
| `anilist.characterDictionary.collapsibleSections.characterInformation` | `false` | Start Character Information section expanded | | `anilist.characterDictionary.collapsibleSections.voicedBy` | Show the voice actor section expanded |
| `anilist.characterDictionary.collapsibleSections.voicedBy` | `false` | Start Voiced By section expanded | | `shortcuts.openCharacterDictionaryManager` | Shortcut for the manager |
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting in subtitles |
| `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.
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 |
| ---------------------- | -------------------------------------------- | ------------------------------------- |
| **Runtime** | TypeScript, runs inside Electron | Rust, standalone web service |
| **Data sources** | AniList only | AniList + VNDB |
| **Delivery** | Auto-synced into bundled Yomitan | ZIP download via web UI |
| **Honorific strategy** | Eager generation at build time | Lazy generation during ZIP export |
| **Caching** | File-based snapshots | Multi-tier (memory + disk + SQLite) |
| **Updates** | Revision-hashed; skips reimport if unchanged | URL-encoded settings for auto-refresh |
If you work with visual novels or want a standalone dictionary generator independent of SubMiner, the reference implementation is worth checking out.
## Troubleshooting ## 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. **It seems stuck.** Check the notification. While generating it shows counts (`image 120/400`), an estimate of time left, and an elapsed clock. If the clock moves, it is still working, and large casts are slow (see [how long it takes](#how-long-it-takes)). If you missed the notification, open the notification history with `Ctrl/Cmd+N`. For errors, check the app log ([log locations](/troubleshooting)).
- **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`.
- **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 **Import failed or timed out.** Yomitan may have been busy importing another dictionary. Play the next episode or restart SubMiner. The import may have finished anyway, so check for the character popup before retrying.
- [Subtitle Annotations](/subtitle-annotations) — how name matches interact with N+1, frequency, and JLPT layers **Names are not highlighted.** Check that `subtitleStyle.nameMatchEnabled` is `true`, that `yomitan.externalProfilePath` is empty, and that the show was found on AniList. The wrong show's cast means a wrong match. See [correcting AniList matches](#correcting-anilist-matches).
- [AniList Integration](/anilist-integration) — authentication, episode tracking, and AniList settings
- [Configuration Reference](/configuration) — full config options **Portraits are missing.** Portraits need AniList to have an image and the download to succeed. If you were offline during the first sync, delete that show's file from `character-dictionaries/snapshots/` in the SubMiner config directory and replay it.
SubMiner's generator is based on the [Japanese Character Name Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) project, which also supports VNDB and works without SubMiner.
+421 -1234
View File
File diff suppressed because it is too large. Load diff
+28 -27
View File
@@ -1,45 +1,46 @@
# Feature Demos # Feature demos
Short recordings of SubMiner's key features and integrations from real playback sessions. Short recordings from real playback sessions.
_Yomitan_ is the pop-up dictionary. _Jimaku_ is a community subtitle database. _alass_ and _ffsubsync_ retime subtitles against the audio. _Jellyfin_ is a self-hosted media server. A _texthooker_ is a web page that mirrors the current subtitle as selectable text.
<script setup> <script setup>
const v = '20260301-1'; import { withBase } from 'vitepress';
const v = '20260819-1';
</script> </script>
## Anki Card Mining & Enrichment ## Anki card mining and enrichment
Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner automatically attaches the sentence, a timing-accurate audio clip, a screenshot, and a translation. Mine a card from Yomitan or straight from a subtitle line. SubMiner attaches the sentence, an audio clip cut to the line timing, and a screenshot.
<video controls playsinline preload="metadata" :poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`"> <video controls playsinline preload="metadata" :poster="withBase(`/assets/minecard-poster.jpg?v=${v}`)">
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" /> <source :src="withBase(`/assets/minecard.webm?v=${v}`)" type="video/webm" />
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" /> <source :src="withBase(`/assets/minecard.mp4?v=${v}`)" type="video/mp4" />
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer"> <a :href="withBase(`/assets/minecard.webm?v=${v}`)" target="_blank" rel="noreferrer">
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" /> <img :src="withBase(`/assets/minecard.webp?v=${v}`)" alt="Animated demo of mining a card" style="width: 100%; height: auto;" />
</a> </a>
</video> </video>
::: info VIDEO COMING SOON ## Subtitle download and sync
:::
## Subtitle Download & Sync Search Jimaku, download a track, then retime it with alass or ffsubsync without leaving SubMiner.
Search and download subtitles from Jimaku, then automatically synchronize them with alass or ffsubsync — all from within SubMiner. <!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/subtitle-sync-poster.jpg?v=${v}`)">
<source :src="withBase(`/assets/demos/subtitle-sync.webm?v=${v}`)" type="video/webm" />
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/subtitle-sync-poster.jpg?v=${v}`"> <source :src="withBase(`/assets/demos/subtitle-sync.mp4?v=${v}`)" type="video/mp4" />
<source :src="`/assets/demos/subtitle-sync.webm?v=${v}`" type="video/webm" />
<source :src="`/assets/demos/subtitle-sync.mp4?v=${v}`" type="video/mp4" />
</video> --> </video> -->
::: info VIDEO COMING SOON ::: info VIDEO COMING SOON
::: :::
## Jellyfin Integration ## Jellyfin integration
Browse your Jellyfin library, cast to devices, and launch playback directly from SubMiner. Watch progress syncs back to your Jellyfin server. Browse your Jellyfin library and play from SubMiner, or cast to SubMiner from another Jellyfin client. Watch progress syncs back to the server.
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/jellyfin-poster.jpg?v=${v}`"> <!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/jellyfin-poster.jpg?v=${v}`)">
<source :src="`/assets/demos/jellyfin.webm?v=${v}`" type="video/webm" /> <source :src="withBase(`/assets/demos/jellyfin.webm?v=${v}`)" type="video/webm" />
<source :src="`/assets/demos/jellyfin.mp4?v=${v}`" type="video/mp4" /> <source :src="withBase(`/assets/demos/jellyfin.mp4?v=${v}`)" type="video/mp4" />
</video> --> </video> -->
::: info VIDEO COMING SOON ::: info VIDEO COMING SOON
@@ -47,11 +48,11 @@ Browse your Jellyfin library, cast to devices, and launch playback directly from
## Texthooker ## Texthooker
Open subtitles in an external texthooker page for use with browser-based tools and extensions alongside the overlay. Mirror subtitles to an external texthooker page so browser extensions can read them while the overlay runs.
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/texthooker-poster.jpg?v=${v}`"> <!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/texthooker-poster.jpg?v=${v}`)">
<source :src="`/assets/demos/texthooker.webm?v=${v}`" type="video/webm" /> <source :src="withBase(`/assets/demos/texthooker.webm?v=${v}`)" type="video/webm" />
<source :src="`/assets/demos/texthooker.mp4?v=${v}`" type="video/mp4" /> <source :src="withBase(`/assets/demos/texthooker.mp4?v=${v}`)" type="video/mp4" />
</video> --> </video> -->
::: info VIDEO COMING SOON ::: info VIDEO COMING SOON
+108 -163
View File
@@ -1,54 +1,35 @@
# Building & Testing # Building and testing
For internal architecture/workflow guidance, use `docs/README.md` at the repo root. This page stays focused on contributor-facing build and test commands. Build, run, and test SubMiner from source. Architecture and workflow rules live in the repo's internal docs, starting at [`docs/README.md`](https://github.com/ksyasuda/SubMiner/blob/main/docs/README.md). Module boundaries and layering rules are in [`docs/architecture/README.md`](https://github.com/ksyasuda/SubMiner/blob/main/docs/architecture/README.md). The lane-by-lane test guide is [`docs/workflow/verification.md`](https://github.com/ksyasuda/SubMiner/blob/main/docs/workflow/verification.md).
## Prerequisites ## Prerequisites
- [Bun](https://bun.sh) - [Bun](https://bun.sh), at the version pinned in `package.json`
- A system `lua` interpreter for the mpv plugin tests (`bun run test:launcher`, `bun run test:env`)
- macOS only: `bun run build` compiles a Swift window helper. Set `SUBMINER_SKIP_MACOS_HELPER_BUILD=1` to skip it.
## Setup ## Setup
```bash ```bash
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
cd SubMiner cd SubMiner
# if you cloned without --recurse-submodules: make deps
git submodule update --init --recursive
bun install
(cd stats && bun install --frozen-lockfile)
(cd vendor/texthooker-ui && bun install --frozen-lockfile)
``` ```
`make deps` is still available as a convenience wrapper around the same dependency install flow. `make deps` initializes submodules and installs dependencies for the root, `stats/`, and `vendor/texthooker-ui`. The Yomitan submodule installs its own dependencies during `bun run build`.
## Building ## Build
```bash ```bash
# Main app build bun run build # app build, including bundled Yomitan from vendor/subminer-yomitan
bun run build
# Platform packages
bun run build:appimage # Linux AppImage bun run build:appimage # Linux AppImage
bun run build:mac # macOS DMG + ZIP (signed) bun run build:mac # macOS DMG + ZIP (signed)
bun run build:mac:unsigned # macOS DMG + ZIP (unsigned) bun run build:mac:unsigned # macOS DMG + ZIP (unsigned)
bun run build:win # Windows NSIS installer + ZIP bun run build:win # Windows NSIS installer + ZIP
make build-launcher # launcher only, output: dist/launcher/subminer
# Optional launcher artifact only
make build-launcher
# output: dist/launcher/subminer
``` ```
`bun run build` includes the Yomitan build step. It builds the bundled Chrome extension directly from the `vendor/subminer-yomitan` submodule into `build/yomitan` using Bun. The launcher source is `launcher/*.ts`. `dist/launcher/subminer` is generated, so never edit it by hand. The repo-root `./subminer` is a stale path and verification rejects it. `make install-linux` and `make install-macos` copy from `dist/launcher/subminer`. To check the launcher build:
## Launcher Artifact Workflow
- Source of truth: `launcher/*.ts`
- Generated output: `dist/launcher/subminer`
- Do not hand-edit generated launcher output.
- Repo-root `./subminer` is a stale artifact path and is rejected by verification checks.
- Install targets (`make install-linux`, `make install-macos`) copy from `dist/launcher/subminer`.
Verify the workflow:
```bash ```bash
make build-launcher make build-launcher
@@ -56,47 +37,33 @@ dist/launcher/subminer --help >/dev/null
bash scripts/verify-generated-launcher.sh bash scripts/verify-generated-launcher.sh
``` ```
## Running Locally ## Run locally
```bash ```bash
bun run dev # builds + launches with --start --dev bun run dev # build, then launch with --start --dev
electron . --start --dev --log-level debug # equivalent Electron launch with verbose logging make dev-watch # watch TS + renderer and relaunch Electron
electron . --background # tray/background mode, minimal default logging make dev-watch-macos # same, forcing --backend macos
make dev-start # build + launch via Makefile electron . --start --dev --log-level debug # verbose launch of an existing build
make dev-watch # watch TS + renderer and launch Electron (faster edit loop) electron . --background # tray/background mode
make dev-watch-macos # same as dev-watch, forcing --backend macos
``` ```
For mpv-plugin-driven testing without exporting `SUBMINER_BINARY_PATH` each run, set a one-time Development and debug launches use a separate `SubMiner-dev` profile so runtime experiments cannot modify the installed app's configuration or Yomitan dictionaries. To intentionally use the production profile for a development launch, set `SUBMINER_USE_PRODUCTION_PROFILE=1`. Use that override only after backing up the profile.
dev binary path in `~/.config/mpv/script-opts/subminer.conf`:
```ini Launch source builds through `bun run dev` or `bun run electron`. SubMiner refuses to load its profile when the running Electron major differs from the version pinned by the repository.
binary_path=/absolute/path/to/SubMiner/scripts/subminer-dev.sh
To test through the mpv plugin without exporting `SUBMINER_BINARY_PATH` each time, point `mpv.subminerBinaryPath` in your config at the dev script. The launcher passes it to the plugin at runtime:
```json
{
"mpv": {
"subminerBinaryPath": "/absolute/path/to/SubMiner/scripts/subminer-dev.sh"
}
}
``` ```
## Testing ## Test
Default lanes: Run the handoff gate before submitting substantial changes:
```bash
bun run test # alias for test:fast
bun run test:fast # default fast lane
bun run test:full # maintained source + launcher-unit + runtime compat surface
bun run test:runtime:compat # compiled/runtime compatibility slice only
bun run test:env # launcher/plugin + env-sensitive verification
bun run test:immersion:sqlite # SQLite persistence lane
bun run test:subtitle # maintained alass/ffsubsync subtitle surface
```
- `bun run test` and `bun run test:fast` cover config/core suites plus representative entry/runtime, Anki integration, release-workflow coverage, typecheck, and runtime-registry checks.
- `bun run test:full` is the maintained full surface: Bun-compatible `src/**` discovery, Bun-compatible launcher unit discovery, and the compiled/runtime compatibility lane for suites routed through `dist/**`.
- `bun run test:runtime:compat` covers the compiled/runtime slice directly: `ipc`, `anki-jimaku-ipc`, `overlay-manager`, `config-validation`, `startup-config`, and `registry`.
- `bun run test:env` covers environment-sensitive checks: launcher smoke/plugin verification plus the Bun source SQLite lane.
- `bun run test:immersion:sqlite` is the reproducible persistence lane when you need real DB-backed SQLite coverage under Bun.
The Bun-managed discovery lanes intentionally exclude a small compiled/runtime-focused set: `src/core/services/ipc.test.ts`, `src/core/services/anki-jimaku-ipc.test.ts`, `src/core/services/overlay-manager.test.ts`, `src/main/config-validation.test.ts`, `src/main/runtime/startup-config.test.ts`, and `src/main/runtime/registry.test.ts`. `bun run test:runtime:compat` keeps them in the standard workflow via `dist/**`.
Suggested local gate before handoff:
```bash ```bash
bun run typecheck bun run typecheck
@@ -106,133 +73,111 @@ bun run build
bun run test:smoke:dist bun run test:smoke:dist
``` ```
If you changed docs in `docs-site/`, also run: For smaller changes, start with the cheapest lane that covers what you touched:
| Command | Covers |
| ------------------------------- | ------------------------------------------------------------------------ |
| `bun run test` / `test:fast` | All `src/**` tests, launcher unit tests, and `scripts/**` tests |
| `bun run test:config` | Config schema, defaults, and `config.example.jsonc` generation |
| `bun run test:launcher` | Launcher tests plus the Lua plugin tests |
| `bun run test:env` | Launcher e2e smoke, Lua plugin tests, SQLite immersion tests from source |
| `bun run test:scripts` | Build and release scripts under `scripts/**` |
| `bun run test:stats` | Stats dashboard UI under `stats/src/**` |
| `bun run test:runtime:compat` | Compiled-runtime smoke against `dist/` (run `bun run build` first) |
| `bun run test:immersion:sqlite` | Compiles, then runs the SQLite-backed immersion tracker tests |
| `bun run test:subtitle` | alass/ffsubsync subtitle sync |
| `bun run test:docs:kb` | Internal docs, `AGENTS.md`, and repo skills |
Lane membership is defined in `scripts/test-lanes.ts` and discovered by directory, so a new test file joins its lane automatically. Do not hand-list test files in `package.json`. `scripts/run-test-lane.mjs` runs each file in its own `bun test` process, so a hanging test cannot take down the rest of the lane. Pass `--jobs N` to parallelize or `--single-process` to share one process while debugging.
Launcher smoke artifacts go to `.tmp/launcher-smoke`. CI uploads them when the smoke step fails.
## Format
```bash ```bash
bun run docs:test make pretty # format the maintained source and stats files
bun run docs:build bun run format:check:src # check the same set without writing
``` ```
Focused commands: `bun run format` runs Prettier over the whole repo. Use it only when you mean to.
## Config generation
```bash ```bash
bun run test:config # Source-level config schema/validation tests bun run electron . --generate-config # write a default config to ~/.config/SubMiner/config.jsonc (%APPDATA%\SubMiner\config.jsonc on Windows)
bun run test:launcher # Launcher regression tests (config discovery + command routing) bun run generate:config-example # regenerate config.example.jsonc from the defaults
bun run test:core # Source-level core regression tests (default lane)
bun run test:launcher:smoke:src # Launcher e2e smoke: launcher -> mpv IPC -> overlay start/stop wiring
bun run test:launcher:env:src # Launcher smoke + Lua plugin gate
bun run test:src # Bun-managed maintained src/** discovery lane
bun run test:launcher:unit:src # Bun-managed maintained launcher unit lane
bun run test:immersion:sqlite:src # Bun source lane
``` ```
Dist-level tests are now an explicit smoke lane used to validate compiled/runtime assumptions. `make generate-config` and `make generate-example-config` wrap the same commands.
Launcher smoke artifacts are written to `.tmp/launcher-smoke` locally and uploaded by CI/release workflows when the smoke step fails. Config definitions are split by domain under `src/config/definitions/`:
Smoke and optional deep dist commands: - defaults: `defaults-*.ts`
- option metadata: `options-*.ts`
- generated template sections and comments: `template-sections.ts`
```bash `src/config/definitions.ts` composes them into the public API (`DEFAULT_CONFIG`, registries, template export). A new key also needs a resolver entry under `src/config/resolve/`, or the resolved config keeps the default.
bun run build # compile dist artifacts
bun run test:immersion:sqlite # compile + run SQLite-backed immersion tests under Bun
bun run test:smoke:dist # explicit smoke scope for compiled runtime
bun run test:config:dist # optional full dist config suite
bun run test:core:dist # optional full dist core suite
```
Use `bun run test:immersion:sqlite` when you need real DB-backed coverage for the immersion tracker. ## Documentation site
## Formatting The user docs live in `docs-site/` (VitePress).
Use the scoped formatter for normal app-repo work:
```bash
make pretty
bun run format:check:src
```
- `make pretty` runs the maintained Prettier allowlist only (`format:src`).
- `bun run format:check:src` checks the same scoped set without writing changes.
- `bun run format` remains the broad repo-wide Prettier command; use it intentionally.
## Config Generation
```bash
# Generate default config to ~/.config/SubMiner/config.jsonc (or %APPDATA%\SubMiner\config.jsonc on Windows)
bun run electron . --generate-config
# Regenerate the repo's config.example.jsonc from centralized defaults
bun run generate:config-example
```
Convenience wrappers still exist:
- `make generate-config`
- `make generate-example-config`
## Documentation Site
The docs site now lives in `docs-site/` inside the main repo.
From the SubMiner app repo:
```bash ```bash
bun --cwd docs-site install bun --cwd docs-site install
bun run docs:dev # Dev server at http://localhost:5173 bun run docs:dev # dev server at http://localhost:5173
bun run docs:build # Production build into docs-site/.vitepress/dist bun run docs:test # docs regression tests (links, pinned strings)
bun run docs:preview # Preview built site at http://localhost:4173 bun run docs:build # production build into docs-site/.vitepress/dist
bun run docs:test # Docs regression tests bun run docs:preview # preview the build at http://localhost:4173
``` ```
Cloudflare Pages deploy settings: Run `bun run docs:test` and `bun run docs:build` whenever you change `docs-site/`.
- Git repo: `ksyasuda/SubMiner` Production uses the versioned build:
- Root directory: `docs-site`
- Build command: `bun run docs:build`
- Build output directory: `.vitepress/dist`
- Build watch paths: `docs-site/*`
Use Cloudflare's single `*` wildcard syntax for watch paths. `docs-site/*` covers nested docs-site changes in the repo; `docs-site/**` is not the correct Pages pattern and may skip docs-only pushes. ```bash
bun run docs:build:versioned
```
## Makefile Reference It writes `.tmp/docs-versioned-site`: the latest stable docs at `/` with a generated `/versions` page, and development docs at `/main/`. Prerelease tags are skipped. Stable archives under `/v/<version>/` are built once and stored in R2. Without R2 credentials the build skips archive sync, so a local run only produces `/` and `/main/`.
Run `make help` for a full list of targets. Key ones: The `docs-pages` GitHub Actions workflow uploads that output to Cloudflare Pages with Wrangler. Cloudflare's Git-integration builds are disabled on purpose, so do not re-enable them in the dashboard. `docs-site/README.md` has the full deployment setup.
## Makefile targets
Run `make help` for the full list.
| Target | Description | | Target | Description |
| ---------------------- | ---------------------------------------------------------------- | | --------------------------- | ------------------------------------------------------------ |
| `make build` | Build platform package for detected OS | | `make deps` | Init submodules and install root, stats, and texthooker deps |
| `make build-launcher` | Generate Bun launcher wrapper at `dist/launcher/subminer` | | `make build` | Build the platform package for the current OS |
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) | | `make build-linux` | Build the Linux package |
| `make deps` | Install JS dependencies (root + stats + texthooker-ui) | | `make build-macos` | Build the signed macOS package |
| `make pretty` | Run scoped Prettier formatting for maintained source/config files | | `make build-macos-unsigned` | Build the unsigned macOS package |
| `make generate-config` | Generate default config from centralized registry | | `make build-launcher` | Generate the launcher in `dist/launcher/` |
| `make build-linux` | Convenience wrapper for Linux packaging | | `make install` | Install platform artifacts (wrapper, theme, AppImage or app) |
| `make build-macos` | Convenience wrapper for signed macOS packaging | | `make pretty` | Run scoped Prettier formatting |
| `make build-macos-unsigned` | Convenience wrapper for unsigned macOS packaging | | `make generate-config` | Generate a default config |
## Contributor Notes ## Contributor notes
- To add/change a config default, edit the matching domain file in `src/config/definitions/defaults-*.ts`. - See [Architecture](/architecture) for module boundaries and [IPC + runtime contracts](/ipc-contracts) before adding IPC channels.
- To add/change config option metadata, edit the matching domain file in `src/config/definitions/options-*.ts`. - `src/core/services/overlay-manager.ts` owns overlay window and visibility state.
- To add/change generated config template blocks/comments, update `src/config/definitions/template-sections.ts`. - In `src/main/` modules, pass simple dependencies as inline objects. Add a helper or adapter only when it adapts, validates, or gets reused.
- Keep `src/config/definitions.ts` as the composed public API (`DEFAULT_CONFIG`, registries, template export) that wires domain modules together. - Packaged Linux desktop launches pass `--background` through `build.linux.executableArgs` in `package.json`.
- Overlay window/visibility state is owned by `src/core/services/overlay-manager.ts`.
- Runtime architecture/module-boundary conventions are summarized in [Architecture](/architecture), with canonical internal guidance in `docs/architecture/README.md` at the repo root.
- Linux packaged desktop launches pass `--background` using electron-builder `build.linux.executableArgs` in `package.json`.
- Prefer direct inline deps objects in `src/main/` modules for simple pass-through wiring.
- Add a helper/adapter service only when it performs meaningful adaptation, validation, or reuse (not identity mapping).
## Environment Variables ## Environment variables
| Variable | Description | | Variable | Description |
| ---------------------------------- | ------------------------------------------------------------------------------ | | ---------------------------------- | ---------------------------------------------------------------- |
| `SUBMINER_APPIMAGE_PATH` | Override SubMiner app binary path for launcher playback commands | | `SUBMINER_APPIMAGE_PATH` | SubMiner app binary the launcher uses for playback |
| `SUBMINER_BINARY_PATH` | Alias for `SUBMINER_APPIMAGE_PATH` | | `SUBMINER_BINARY_PATH` | Alias for `SUBMINER_APPIMAGE_PATH` |
| `SUBMINER_ROFI_THEME` | Override rofi theme path for launcher picker | | `SUBMINER_ROFI_THEME` | rofi theme for the launcher picker |
| `SUBMINER_LOG_LEVEL` | Override app logger level (`debug`, `info`, `warn`, `error`) | | `SUBMINER_MPV_PLUGIN_PATH` | mpv plugin directory the launcher injects |
| `SUBMINER_MPV_LOG` | Override mpv/app shared log file path | | `SUBMINER_LOG_LEVEL` | App log level (`debug`, `info`, `warn`, `error`) |
| `SUBMINER_JIMAKU_API_KEY` | Override Jimaku API key for launcher subtitle downloads | | `SUBMINER_MPV_LOG` | Shared mpv/app log file path |
| `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command used to resolve Jimaku API key at runtime | | `SUBMINER_JIMAKU_API_KEY` | Jimaku API key for launcher subtitle downloads |
| `SUBMINER_JIMAKU_API_BASE_URL` | Override Jimaku API base URL | | `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command that prints the Jimaku API key |
| `SUBMINER_JELLYFIN_ACCESS_TOKEN` | Override Jellyfin access token (used before stored encrypted session fallback) | | `SUBMINER_JIMAKU_API_BASE_URL` | Jimaku API base URL |
| `SUBMINER_JELLYFIN_USER_ID` | Optional Jellyfin user ID override | | `SUBMINER_JELLYFIN_ACCESS_TOKEN` | Jellyfin access token, used before the stored encrypted session |
| `SUBMINER_SKIP_MACOS_HELPER_BUILD` | Set to `1` to skip building the macOS helper binary during `bun run build` | | `SUBMINER_JELLYFIN_USER_ID` | Jellyfin user ID |
| `SUBMINER_SKIP_MACOS_HELPER_BUILD` | Set to `1` to skip the macOS helper build during `bun run build` |
+48 -5
View File
@@ -13,6 +13,10 @@ const ankiIntegrationContents = readFileSync(
'utf8', 'utf8',
); );
const configurationContents = readFileSync(new URL('./configuration.md', import.meta.url), 'utf8'); const configurationContents = readFileSync(new URL('./configuration.md', import.meta.url), 'utf8');
const troubleshootingContents = readFileSync(
new URL('./troubleshooting.md', import.meta.url),
'utf8',
);
function extractReleaseHeadings(content: string, count: number): string[] { function extractReleaseHeadings(content: string, count: number): string[] {
return Array.from(content.matchAll(/^## v[^\n]+$/gm)) return Array.from(content.matchAll(/^## v[^\n]+$/gm))
@@ -37,13 +41,13 @@ test('docs reflect current launcher and release surfaces', () => {
expect(mpvPluginContents).toContain('\\\\.\\pipe\\subminer-socket'); expect(mpvPluginContents).toContain('\\\\.\\pipe\\subminer-socket');
expect(readmeContents).toContain('Root directory: `docs-site`'); expect(readmeContents).toContain('Automatic production and preview deployments: disabled');
expect(readmeContents).toContain('Build output directory: `.vitepress/dist`'); expect(readmeContents).toContain('/main/');
expect(readmeContents).toContain('Build watch paths: `docs-site/*`'); expect(readmeContents).toContain('GitHub Actions direct upload with Wrangler');
expect(developmentContents).not.toContain('../subminer-docs'); expect(developmentContents).not.toContain('../subminer-docs');
expect(developmentContents).toContain('bun run docs:build'); expect(developmentContents).toContain('bun run docs:build');
expect(developmentContents).toContain('bun run docs:test'); expect(developmentContents).toContain('bun run docs:test');
expect(developmentContents).toContain('Build watch paths: `docs-site/*`'); expect(developmentContents).toContain('bun run docs:build:versioned');
expect(developmentContents).not.toContain('test:subtitle:dist'); expect(developmentContents).not.toContain('test:subtitle:dist');
expect(developmentContents).toContain('bun run build:win'); expect(developmentContents).toContain('bun run build:win');
@@ -52,11 +56,50 @@ test('docs reflect current launcher and release surfaces', () => {
expect(configurationContents).not.toContain('youtubeSubgen": {\n "mode"'); expect(configurationContents).not.toContain('youtubeSubgen": {\n "mode"');
expect(configurationContents).not.toContain('youtubeSubgen.primarySubLanguages'); expect(configurationContents).not.toContain('youtubeSubgen.primarySubLanguages');
expect(configurationContents).toContain('youtube.primarySubLanguages'); expect(configurationContents).toContain('youtube.primarySubLanguages');
expect(configurationContents).toContain('### Shared AI Provider'); // The AI provider still exists in src/ai and ankiConnect.ai, but it is not
// exposed in the Settings window and is not documented for users. Keep the
// user-facing docs free of it so nobody configures a hidden surface.
expect(configurationContents).not.toContain('Shared AI Provider');
expect(configurationContents).not.toContain('ankiConnect.ai');
expect(ankiIntegrationContents).not.toContain('AI Translation');
// ankiConnect.fields.translation is a LEGACY_HIDDEN_CONFIG_PATHS key, so it
// must not be documented as a current setting.
expect(configurationContents).not.toContain('fields.translation');
expect(ankiIntegrationContents).not.toContain('SelectionText');
// fields.audio holds SubMiner's generated sentence audio; examples should not
// point it at the field Yomitan uses for word audio.
expect(ankiIntegrationContents).not.toContain('"audio": "ExpressionAudio"');
expect(changelogContents).toContain('v0.5.1 (2026-03-09)'); expect(changelogContents).toContain('v0.5.1 (2026-03-09)');
}); });
test('docs document config surfaces that are easy to miss when they ship', () => {
// Anki maturity-based known-word highlighting (#172) landed in
// subtitle-annotations.md but was missing from the config reference.
expect(configurationContents).toContain('ankiConnect.knownWords.maturityEnabled');
expect(configurationContents).toContain('ankiConnect.knownWords.matureThresholdDays');
// Every top-level config block should be reachable from the config reference.
expect(configurationContents).toContain('### TsukiHime');
expect(configurationContents).toContain('tsukihime.maxSearchResults');
// xz is a hard runtime dependency of the TsukiHime download path.
expect(installationContents).toContain('xz');
});
test('docs state the real secondary-subtitle and Anki field-matching behavior', () => {
// secondarySub auto-load is off by default; the config example previously
// implied otherwise while youtube-integration.md documented it correctly.
expect(configurationContents).toContain('Secondary subtitles do **not** auto-load by default');
expect(configurationContents).toContain('default: `false`');
// Anki field names are matched case-insensitively (src/anki-integration.ts
// resolveFieldName: exact match first, then a lowercase comparison).
expect(usageContents).not.toContain('exactly (case-sensitive)');
expect(troubleshootingContents).not.toContain('exactly (case-sensitive)');
expect(ankiIntegrationContents).toContain('case-insensitively');
});
test('docs changelog keeps the current minor release headings aligned with the root changelog', () => { test('docs changelog keeps the current minor release headings aligned with the root changelog', () => {
const docsHeadings = extractCurrentMinorHeadings(changelogContents); const docsHeadings = extractCurrentMinorHeadings(changelogContents);
expect(docsHeadings.length).toBeGreaterThan(0); expect(docsHeadings.length).toBeGreaterThan(0);
+212
View File
@@ -0,0 +1,212 @@
// Cloudflare Pages Function serving frozen `/v/<version>/` doc archives from R2.
// Archives are uploaded by scripts/build-versioned-docs.ts and never ship in the Pages
// deployment itself, so they do not count toward the Pages file limit. Requires an R2
// binding named DOCS_ARCHIVES on the Pages project (see docs-site/README.md).
// Minimal slice of the Workers R2 API used here; avoids a workers-types dependency.
type R2Range = { offset: number; length?: number } | { suffix: number };
type R2ObjectMeta = {
size: number;
httpEtag: string;
range?: R2Range;
};
type R2ObjectBody = R2ObjectMeta & { body: ReadableStream };
type R2GetOptions = { range?: Headers; onlyIf?: Headers };
export type ArchiveBucket = {
get(key: string, options?: R2GetOptions): Promise<R2ObjectMeta | R2ObjectBody | null>;
};
type ArchiveContext = {
request: Request;
env: { DOCS_ARCHIVES: ArchiveBucket };
};
export type ArchiveRoute =
| { kind: 'redirect'; location: string; status: 301 | 302 }
| { kind: 'lookup'; version: string; keys: string[] }
| { kind: 'not-found' };
const CONTENT_TYPES: Record<string, string> = {
css: 'text/css; charset=utf-8',
gif: 'image/gif',
html: 'text/html; charset=utf-8',
ico: 'image/x-icon',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
js: 'text/javascript; charset=utf-8',
json: 'application/json; charset=utf-8',
jsonc: 'application/json; charset=utf-8',
mjs: 'text/javascript; charset=utf-8',
mkv: 'video/x-matroska',
mp4: 'video/mp4',
png: 'image/png',
svg: 'image/svg+xml',
ttf: 'font/ttf',
txt: 'text/plain; charset=utf-8',
webm: 'video/webm',
webp: 'image/webp',
woff: 'font/woff',
woff2: 'font/woff2',
xml: 'application/xml; charset=utf-8',
};
function extensionOf(path: string): string | null {
const name = path.slice(path.lastIndexOf('/') + 1);
const dot = name.lastIndexOf('.');
return dot > 0 ? name.slice(dot + 1).toLowerCase() : null;
}
function contentTypeFor(key: string): string {
return CONTENT_TYPES[extensionOf(key) ?? ''] ?? 'application/octet-stream';
}
// Maps a request path onto candidate R2 keys, mirroring the Pages clean-URL rules the
// archives were built for (`cleanUrls: true`).
export function resolveArchiveRoute(pathname: string, search = ''): ArchiveRoute {
if (pathname === '/v' || pathname === '/v/') {
return { kind: 'redirect', location: '/versions', status: 302 };
}
const match = /^\/v\/(\d+\.\d+\.\d+)(\/.*)?$/.exec(pathname);
if (!match) return { kind: 'not-found' };
const version = match[1]!;
const rest = match[2];
if (!rest) {
return { kind: 'redirect', location: `/v/${version}/${search}`, status: 301 };
}
let decoded: string;
try {
decoded = decodeURIComponent(rest);
} catch {
return { kind: 'not-found' };
}
if (decoded.split('/').some((segment) => segment === '..' || segment === '.')) {
return { kind: 'not-found' };
}
const prefix = `v/${version}`;
const path = `${prefix}${decoded}`;
const keys = decoded.endsWith('/')
? [`${path}index.html`]
: extensionOf(decoded)
? [path]
: [`${path}.html`, `${path}/index.html`];
return { kind: 'lookup', version, keys };
}
function cacheControlFor(key: string): string {
// VitePress content-hashes everything it emits under assets/.
if (/\/assets\//.test(key) && extensionOf(key) !== 'html') {
return 'public, max-age=31536000, immutable';
}
return 'public, max-age=3600';
}
function hasBody(object: R2ObjectMeta | R2ObjectBody): object is R2ObjectBody {
return 'body' in object && object.body !== undefined;
}
function contentRange(range: R2Range, size: number): { start: number; end: number } {
if ('suffix' in range) {
const length = Math.min(range.suffix, size);
return { start: size - length, end: size - 1 };
}
const length = range.length ?? size - range.offset;
return { start: range.offset, end: range.offset + length - 1 };
}
async function respondWithObject(options: {
request: Request;
bucket: ArchiveBucket;
key: string;
status: number;
}): Promise<Response | null> {
const { request, bucket, key } = options;
// Ranges and conditional requests only make sense for the page that was asked for,
// not the 404 fallback.
const isRequestedObject = options.status === 200;
const wantsRange = isRequestedObject && request.headers.has('range');
let object: R2ObjectMeta | R2ObjectBody | null;
try {
object = await bucket.get(key, {
range: wantsRange ? request.headers : undefined,
onlyIf: isRequestedObject ? request.headers : undefined,
});
} catch {
// R2 rejects unsatisfiable ranges.
return new Response(null, { status: 416 });
}
if (!object) return null;
const headers = new Headers({
'Content-Type': contentTypeFor(key),
'Cache-Control': cacheControlFor(key),
ETag: object.httpEtag,
'Accept-Ranges': 'bytes',
'X-Robots-Tag': 'noindex, follow',
});
// R2 returns the object without a body when an If-None-Match/If-Modified-Since
// precondition matched.
if (!hasBody(object)) {
return new Response(null, { status: 304, headers });
}
let status = options.status;
if (wantsRange && object.range) {
const { start, end } = contentRange(object.range, object.size);
status = 206;
headers.set('Content-Range', `bytes ${start}-${end}/${object.size}`);
headers.set('Content-Length', String(end - start + 1));
} else {
headers.set('Content-Length', String(object.size));
}
return new Response(request.method === 'HEAD' ? null : object.body, { status, headers });
}
export async function onRequest({ request, env }: ArchiveContext): Promise<Response> {
if (request.method !== 'GET' && request.method !== 'HEAD') {
return new Response('Method Not Allowed', { status: 405, headers: { Allow: 'GET, HEAD' } });
}
const url = new URL(request.url);
const route = resolveArchiveRoute(url.pathname, url.search);
if (route.kind === 'redirect') {
return Response.redirect(new URL(route.location, url).toString(), route.status);
}
if (route.kind === 'lookup') {
for (const key of route.keys) {
const response = await respondWithObject({
request,
bucket: env.DOCS_ARCHIVES,
key,
status: 200,
});
if (response) return response;
}
const notFoundPage = await respondWithObject({
request,
bucket: env.DOCS_ARCHIVES,
key: `v/${route.version}/404.html`,
status: 404,
});
if (notFoundPage) return notFoundPage;
}
return new Response('Not Found', {
status: 404,
headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Robots-Tag': 'noindex, follow' },
});
}
+81 -256
View File
@@ -1,304 +1,129 @@
# Immersion Tracking # Immersion tracking
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. SubMiner records what you watch and mine in a local SQLite database and shows it in a stats dashboard. Tracking is on by default. Nothing leaves your machine.
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. ## What gets tracked
Episode completion for local `watched` state uses the shared `DEFAULT_MIN_WATCH_RATIO` (`85%`) value from `src/shared/watch-threshold.ts`. - Watch sessions: time watched, subtitle lines seen, words seen, cards mined, pauses and seeks.
- Every primary subtitle line you see, with its timing, so you can search and mine from it later.
- Vocabulary and kanji you encounter, with how often and where.
- Library entries per show and episode, with cover art from AniList (or TMDB for live action) and YouTube channel metadata.
## Enabling An episode counts as watched once you reach 85% of it.
## Setup
Tracking needs no setup. To turn it off or move the database:
```jsonc ```jsonc
{ {
"immersionTracking": { "immersionTracking": {
"enabled": true, "enabled": true,
"dbPath": "" "dbPath": "",
} },
} }
``` ```
- Leave `dbPath` empty to use the default location (`immersion.sqlite` in SubMiner's app-data directory). An empty `dbPath` stores `immersion.sqlite` in SubMiner's config directory (`~/.config/SubMiner/` on Linux). Set a path to keep it elsewhere.
- Set an explicit path to move the database (useful for backups, cloud syncing, or external tools).
## Stats Dashboard To share stats and watch history between machines, use [`subminer sync <host>`](/launcher-script#sync-between-machines). It merges both databases. Copying the file with a cloud sync tool makes one side overwrite the other.
The same immersion data powers the stats dashboard. ## Open the dashboard
- In-app overlay: focus the visible overlay, then press the key from `stats.toggleKey` (default: `` ` `` / `Backquote`). - In the overlay: focus it and press the `stats.toggleKey` key (Backquote by default).
- Launcher command: run `subminer stats` to start the local stats server on demand and open the dashboard in your browser. - In a browser: run `subminer stats`, then open `http://127.0.0.1:6969` (or your `stats.serverPort`). Set `stats.autoOpenBrowser` to open it automatically.
- Background server: run `subminer stats -b` to start or reuse a dedicated background stats daemon without keeping the launcher attached, and `subminer stats -s` to stop that daemon. - Background server: `subminer stats -b` starts a stats server that keeps running without the launcher attached. `subminer stats -s` stops it. You can still start SubMiner for playback while it runs.
- Maintenance command: run `subminer stats cleanup` or `subminer stats cleanup -v` to backfill/repair vocabulary metadata (`headword`, `reading`, POS) and purge stale or excluded rows from `imm_words` on demand.
- Browser page: open `http://127.0.0.1:6969` directly if the local stats server is already running.
### Dashboard Tabs `subminer stats` fails if `immersionTracking.enabled` is `false`. The server only answers on localhost, so reverse proxies and Tailscale Serve URLs do not work.
#### Overview ## Stats dashboard
Recent sessions, streak calendar, watch-time history, and a tracking snapshot with completed episodes/anime totals. ### Overview
Recent sessions, a streak calendar, watch-time history, and totals for completed episodes and shows.
Deleting a session or session group refreshes the Overview totals, charts, calendar, and vocabulary summary automatically. If a refresh fails, Overview keeps the previous totals visible and offers **Retry**.
![Stats Overview](/screenshots/stats-overview.png) ![Stats Overview](/screenshots/stats-overview.png)
#### Library ### Library
Cover-art library with search and sorting, per-series progress, episode drill-down, and direct links into mined cards. Your shows as cover-art cards with search, sorting, per-series progress, and an episode list linking to mined cards. The **All Titles** / **Anime** / **Live Action** / **YouTube** selector filters the grid. YouTube videos are grouped by channel.
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. Seasons get separate cards when a season number is detected. Live-action titles that AniList cannot match are looked up on [TMDB](/configuration#tmdb). If a title gets no match, open it and use **Link to TMDB** to pick one by hand.
The same show can end up on several cards when release names disagree. To fix that:
- Merge: click **Select**, tick the duplicate cards, choose **Merge Selected**, and pick the entry to keep. Sessions, cards, and watch time move over, and future episodes with those names join the kept entry.
- Move one episode: hover its row in the episode list and click **→** to assign it to another entry. SubMiner remembers the correction.
- Suggested merges appear as **Possible duplicate** above the grid. Choose **Review merge** or **Not duplicates**.
**Delete Entry** in a title's header removes the show with all its episodes, sessions, and lines. You cannot delete the title that is currently playing.
![Stats Library](/screenshots/stats-library.png) ![Stats Library](/screenshots/stats-library.png)
#### Trends ### Trends
Watch time, sessions, words seen, and per-anime progress/pattern charts with configurable date ranges and grouping. Charts for watch time, cards, words, and sessions per day or month, running totals, efficiency (words per minute, cards per hour), and viewing patterns by weekday and hour. Each chart has its own date range and grouping.
![Stats Trends](/screenshots/stats-trends.png) ![Stats Trends](/screenshots/stats-trends.png)
#### Sessions ### Sessions
Expandable session history with new-word activity, cumulative totals, and pause/seek/card markers. Each session row exposes a hover-revealed ↗ button that navigates to the anime media-detail view for that session; pressing the back button there returns to the Sessions tab. Session history with new-word activity and pause, seek, and card markers. The **↗** button on a row opens that show's detail view.
![Stats Sessions](/screenshots/stats-sessions.png) ![Stats Sessions](/screenshots/stats-sessions.png)
#### Vocabulary ### 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. Unique words and kanji you have seen, new words per day, frequency rank tables with Hide Known and Hide Kana filters, and a kanji breakdown. Click a word to see every line it appeared in.
- **Exclusions** hides words from every vocabulary view. You can restore them from the same dialog.
- **Duplicates** cleans up lines repeated by karaoke openings and animated signs (see [Repeated lines](#repeated-lines)).
![Stats Vocabulary](/screenshots/stats-vocabulary.png) ![Stats Vocabulary](/screenshots/stats-vocabulary.png)
Stats server config lives under `stats`: ### Search
```jsonc Searches the primary subtitle lines and titles in your history. **Search by headword** is on by default, so `知らない` also finds inflected forms. Turn it off for exact text matching. Secondary subtitles are not searched.
{
"stats": { ## Mining from the dashboard
"toggleKey": "Backquote",
"serverPort": 6969, Search results and the Vocabulary word panel can create cards from past lines, as long as the source video file is still available:
"autoStartServer": true,
"autoOpenBrowser": false - **Mine Word**: full Yomitan lookup for the word, plus sentence, audio, and image.
} - **Mine Sentence**: a sentence card with `IsSentenceCard` set, for Lapis and Kiku note types.
} - **Mine Audio**: an audio card with `IsAudioCard` set.
Word and audio mining appear only when the word occurs in the sentence. All three use your `ankiConnect` deck, note type, fields, and media settings. Anki must be running, and Mine Word needs Yomitan dictionaries.
## Repeated lines
Karaoke openings and animated signs repeat the same text once per frame. SubMiner collapses these as it records, so one lyric is stored once. Stats recorded before that can hold hundreds of copies and skew Top Repeated Words.
To clean them, use **Duplicates** in the Vocabulary tab: pick a time window, **Scan** to preview, then **Clean Up**. Or from the terminal:
```bash
subminer stats cleanup --duplicate-lines --dry-run --lookback-days 30
subminer stats cleanup --duplicate-lines --lookback-days 30
``` ```
- `toggleKey` is overlay-local, not a system-wide shortcut. Leave out `--lookback-days` to scan all history. Word and kanji counts are corrected. Watch time and session totals are not changed.
- `serverPort` controls the localhost dashboard URL.
- `autoStartServer` starts the local stats HTTP server on launch once immersion tracking is active, or reuses the dedicated background stats server when one is already running.
- `autoOpenBrowser` controls whether `subminer stats` launches the dashboard URL in your browser after ensuring the server is running.
- `subminer stats` forces the dashboard server to start even when `autoStartServer` is `false`.
- `subminer stats -b` starts or reuses the dedicated background stats daemon and exits after startup acknowledgement.
- The background stats daemon is separate from the normal SubMiner overlay app, so you can leave it running and still launch SubMiner later to watch or mine from video.
- `subminer stats -s` stops the dedicated background stats daemon without closing any browser tabs.
- `subminer stats` fails with an error when `immersionTracking.enabled` is `false`.
- `subminer stats cleanup` defaults to vocabulary cleanup, repairs stale `headword`, `reading`, and `part_of_speech` values, attempts best-effort MeCab backfill for legacy rows, and removes rows that still fail vocab filtering.
## Mining Cards from the Stats Page ## Maintenance commands
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: | Command | What it does |
| ------------------------------------------ | ------------------------------------------------------------------------- |
| `subminer stats cleanup` | Repair word readings and part of speech, drop words that fail the filters |
| `subminer stats cleanup -l` | Recompute lifetime totals from episode history, keeping old totals |
| `subminer stats cleanup --duplicate-lines` | Collapse repeated karaoke and sign lines (see above) |
- **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. `subminer stats rebuild` and `subminer stats backfill` run the same lifetime repair as `cleanup -l`.
- **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.
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. ## Retention
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. By default SubMiner keeps everything. To limit history, set `immersionTracking.retentionPreset` to `minimal`, `balanced`, or `deep-history`, or set the `immersionTracking.retention.*Days` values yourself (`0` keeps all). Lifetime totals and vocabulary counts are stored separately and stay exact when old sessions are pruned.
### Word Exclusion List Deleting retained sessions preserves earlier word and kanji discovery dates from pruned history. New occurrences record observation time so deletion can update their dates exactly. Older history used write time, so an earlier discovery date is preserved when its origin cannot be determined.
The Vocabulary tab toolbar includes an **Exclusions** button for hiding words from all vocabulary views. Excluded words are stored in the immersion database, with older browser localStorage exclusions imported on first load after upgrade. They can be managed (restored or cleared) from the exclusion modal. Exclusions affect stat cards, charts, the frequency rank table, and the word list. See [Immersion tracking](/configuration#immersion-tracking) and [Stats dashboard](/configuration#stats-dashboard) in the config reference for every option and default.
## Retention Defaults
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) |
| Daily rollups | 0 (keep all) |
| Monthly rollups | 0 (keep all) |
Maintenance runs on startup and every 24 hours. Vacuum runs only when `retention.vacuumIntervalDays` is non-zero.
In practice:
- Overview totals read from lifetime summary tables, so all-time watch time/cards/words stay exact even if raw query paths evolve.
- Anime and episode pages keep lifetime totals from summary tables while session drill-down still reads retained sessions directly. With the current defaults, both are kept forever.
- Trends can read the full available history because daily/monthly rollups are also kept forever by default.
- Vocabulary and kanji totals are cumulative and not bounded by the raw session retention knobs.
## Storage / Performance Model
The tracker is optimized for "keep everything" defaults:
- Exact all-time totals live in dedicated lifetime summary tables (`imm_lifetime_global`, `imm_lifetime_anime`, `imm_lifetime_media`).
- Ended-session totals are persisted onto `imm_sessions`, so most dashboard reads do not need to rescan raw telemetry.
- Daily and monthly rollups remain available for chart queries and coarse trend views.
- Subtitle text is stored once in `imm_subtitle_lines`; subtitle-line event payloads keep compact metadata only.
- Cover-art binaries are deduplicated through a shared blob store so episodes in the same series do not each carry duplicate image bytes.
- Hot tables have dedicated indexes for session time ranges, telemetry sample windows, frequency-ranked vocabulary, and cover-art lookup keys.
## Configurable Knobs
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 |
| `payloadCapBytes` | Max payload size per write |
| `maintenanceIntervalMs` | How often maintenance runs |
| `retention.eventsDays` | Raw event retention |
| `retention.telemetryDays` | Telemetry retention |
| `retention.sessionsDays` | Session retention |
| `retention.dailyRollupsDays` | Daily rollup retention |
| `retention.monthlyRollupsDays` | Monthly rollup retention |
| `retention.vacuumIntervalDays` | Minimum spacing between vacuums |
| `retentionMode` | `preset` or `advanced` |
| `retentionPreset` | `minimal`, `balanced`, or `deep-history` (used by `retentionMode`) |
| `lifetimeSummaries.global` | Maintain global lifetime totals |
| `lifetimeSummaries.anime` | Maintain per-anime lifetime totals |
| `lifetimeSummaries.media` | Maintain per-media lifetime totals |
## Query Templates
### Session timeline
```sql
SELECT
sample_ms,
total_watched_ms,
active_watched_ms,
lines_seen,
words_seen,
tokens_seen,
cards_mined
FROM imm_session_telemetry
WHERE session_id = ?
ORDER BY sample_ms DESC, telemetry_id DESC
LIMIT ?;
```
### Session throughput summary
```sql
SELECT
s.session_id,
s.video_id,
s.started_at_ms,
s.ended_at_ms,
COALESCE(s.active_watched_ms, 0) AS active_watched_ms,
COALESCE(s.words_seen, 0) AS words_seen,
COALESCE(s.cards_mined, 0) AS cards_mined,
CASE
WHEN COALESCE(s.active_watched_ms, 0) > 0
THEN COALESCE(s.words_seen, 0) / (COALESCE(s.active_watched_ms, 0) / 60000.0)
ELSE NULL
END AS words_per_min,
CASE
WHEN COALESCE(s.active_watched_ms, 0) > 0
THEN (COALESCE(s.cards_mined, 0) * 60.0) / (COALESCE(s.active_watched_ms, 0) / 60000.0)
ELSE NULL
END AS cards_per_hour
FROM imm_sessions s
ORDER BY s.started_at_ms DESC
LIMIT ?;
```
### Lifetime anime totals
```sql
SELECT
a.anime_id,
a.canonical_title,
la.total_sessions,
la.total_active_ms,
la.total_cards,
la.total_words_seen,
la.total_lines_seen,
la.first_watched_ms,
la.last_watched_ms
FROM imm_lifetime_anime la
JOIN imm_anime a ON a.anime_id = la.anime_id
ORDER BY la.last_watched_ms DESC
LIMIT ?;
```
### Daily rollups
```sql
SELECT
rollup_day,
video_id,
total_sessions,
total_active_min,
total_lines_seen,
total_words_seen,
total_tokens_seen,
total_cards,
cards_per_hour,
words_per_min,
lookup_hit_rate
FROM imm_daily_rollups
ORDER BY rollup_day DESC, video_id DESC
LIMIT ?;
```
### Monthly rollups
```sql
SELECT
rollup_month,
video_id,
total_sessions,
total_active_min,
total_lines_seen,
total_words_seen,
total_tokens_seen,
total_cards
FROM imm_monthly_rollups
ORDER BY rollup_month DESC, video_id DESC
LIMIT ?;
```
## Technical Details
- Write path is asynchronous and queue-backed. Hot paths (subtitle parsing, render, token flows) enqueue telemetry and never await SQLite writes.
- Queue overflow policy: drop oldest queued writes, keep newest.
- SQLite tunings: `journal_mode=WAL`, `synchronous=NORMAL`, `foreign_keys=ON`, `busy_timeout=2500`, bounded WAL growth via `journal_size_limit`.
- Maintenance executes `PRAGMA optimize` after periodic cleanup.
- Rollups run incrementally from the last processed telemetry sample; startup performs a one-time bootstrap pass.
- Cover-art blobs are deduplicated into `imm_cover_art_blobs` and referenced from `imm_media_art`.
- Large-table reads are index-backed for `sample_ms`, session time windows, frequency-ranked words/kanji, and cover-art identity lookups.
- Workload-dependent tuning knobs remain at defaults unless you change them: `cache_size`, `mmap_size`, `temp_store`, `auto_vacuum`.
### Schema (v4)
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
Lifetime summary tables:
- `imm_lifetime_global`
- `imm_lifetime_anime`
- `imm_lifetime_media`
- `imm_lifetime_applied_sessions`
Rollup tables:
- `imm_daily_rollups`
- `imm_monthly_rollups`
Vocabulary tables:
- `imm_words(id, headword, word, reading, first_seen, last_seen, frequency)`
- `imm_kanji(id, kanji, first_seen, last_seen, frequency)`
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
+5 -5
View File
@@ -7,18 +7,18 @@ const docsIndexContents = readFileSync(docsIndexPath, 'utf8');
test('docs demo media uses shared cache-busting asset version token', () => { test('docs demo media uses shared cache-busting asset version token', () => {
expect(docsIndexContents).toMatch(/const demoAssetVersion = ['"][^'"]+['"]/); expect(docsIndexContents).toMatch(/const demoAssetVersion = ['"][^'"]+['"]/);
expect(docsIndexContents).toContain( expect(docsIndexContents).toContain(
':poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`"', ':poster="withBase(`/assets/minecard-poster.jpg?v=${demoAssetVersion}`)"',
); );
expect(docsIndexContents).toContain( expect(docsIndexContents).toContain(
'<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />', '<source :src="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" type="video/webm" />',
); );
expect(docsIndexContents).toContain( expect(docsIndexContents).toContain(
'<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />', '<source :src="withBase(`/assets/minecard.mp4?v=${demoAssetVersion}`)" type="video/mp4" />',
); );
expect(docsIndexContents).toContain( expect(docsIndexContents).toContain(
'<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">', '<a :href="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" target="_blank" rel="noreferrer">',
); );
expect(docsIndexContents).toContain( expect(docsIndexContents).toContain(
'<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />', '<img :src="withBase(`/assets/minecard.webp?v=${demoAssetVersion}`)" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />',
); );
}); });
+31 -29
View File
@@ -7,7 +7,7 @@ titleTemplate: Immersion Mining Workflow for MPV
hero: hero:
name: SubMiner name: SubMiner
text: Immersion Mining for MPV text: Immersion Mining for MPV
tagline: Watch media, mine vocabulary, and craft anki cards without leaving the scene. tagline: Watch, look up a word, and get an Anki card with audio and a screenshot. Without pausing your show.
image: image:
src: /assets/SubMiner.png src: /assets/SubMiner.png
alt: SubMiner logo alt: SubMiner logo
@@ -24,69 +24,71 @@ features:
src: /assets/mpv.svg src: /assets/mpv.svg
alt: mpv icon alt: mpv icon
title: Built for mpv 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: Reads subtitle state over mpv's IPC socket. Launch with the wrapper script or the mpv plugin. There is no separate bridge process to run.
link: /usage link: /usage
linkText: How it works linkText: How it works
- icon: - icon:
src: /assets/yomitan-icon.svg src: /assets/yomitan-icon.svg
alt: Yomitan logo alt: Yomitan logo
title: Bundled Yomitan title: Bundled Yomitan
details: Ships with a built-in Yomitan instance for instant word lookups and context-aware card creation directly from subtitle text. details: A Yomitan instance is bundled and preconfigured. Hover a word in the subtitle overlay to look it up and mine it.
link: /mining-workflow link: /mining-workflow
linkText: Mining workflow linkText: Mining workflow
- icon: - icon:
src: /assets/anki-card.svg src: /assets/anki-card.svg
alt: Anki card icon alt: Anki card icon
title: Anki Card Enrichment title: Anki card enrichment
details: Auto-fills card fields with sentence, audio clip, screenshot, and translation so you can focus on learning. details: New cards get the subtitle line, an audio clip cut to the line timing, and a screenshot from that moment.
link: /anki-integration link: /anki-integration
linkText: Anki integration linkText: Anki integration
- icon: - icon:
src: /assets/highlight.svg src: /assets/highlight.svg
alt: Highlight icon alt: Highlight icon
title: Reading Annotations 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, drawn onto the subtitle line as it plays.
link: /subtitle-annotations link: /subtitle-annotations
linkText: Annotation details linkText: Annotation details
- icon: - icon:
src: /assets/video.svg src: /assets/video.svg
alt: Video playback icon alt: Video playback icon
title: YouTube Playback title: YouTube playback
details: Play YouTube URLs or ytsearch targets directly — SubMiner automatically selects and loads subtitles for the video. details: Pass a YouTube URL or a ytsearch target. SubMiner picks a subtitle track for the video and loads it.
link: /usage#youtube-playback link: /usage#youtube-playback
linkText: YouTube playback linkText: YouTube playback
- icon: - icon:
src: /assets/jellyfin.svg src: /assets/jellyfin.svg
alt: Jellyfin icon alt: Jellyfin icon
title: Jellyfin Integration title: Jellyfin integration
details: Browse your Jellyfin library, pick media interactively, and play through mpv with full subtitle and mining support. details: Browse your Jellyfin library from the overlay and play a title through mpv. Subtitles and mining work the same as with local files.
link: /jellyfin-integration link: /jellyfin-integration
linkText: Jellyfin setup linkText: Jellyfin setup
- icon: - icon:
src: /assets/subtitle-download.svg src: /assets/subtitle-download.svg
alt: Subtitle download icon alt: Subtitle download icon
title: Subtitle Download & Sync title: Subtitle download and sync
details: Search and pull subtitles from Jimaku, then auto-sync timing with alass or ffsubsync — all from the overlay. details: Search Jimaku or TsukiHime and download a track, then retime it with alass or ffsubsync. Both run from the overlay.
link: /jimaku-integration link: /jimaku-integration
linkText: Jimaku integration linkText: Jimaku integration
- icon: - icon:
src: /assets/tokenization.svg src: /assets/tokenization.svg
alt: Tracking chart icon alt: Tracking chart icon
title: Stats Dashboard 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: A local dashboard with session history, streak calendars, word frequency, and per-series progress. You can mine cards from lines you already watched.
link: /immersion-tracking link: /immersion-tracking
linkText: Dashboard & tracking linkText: Dashboard & tracking
- icon: - icon:
src: /assets/cross-platform.svg src: /assets/cross-platform.svg
alt: Cross-platform icon alt: Cross-platform icon
title: Cross-Platform title: Cross-platform
details: Runs on Linux (Hyprland, Sway, X11), macOS, and Windows with compositor-aware window positioning and platform-native integration. details: Runs on Linux (Hyprland, Sway, X11), macOS, and Windows. Overlay positioning is handled per compositor rather than assuming one window manager.
link: /installation link: /installation
linkText: Platform setup linkText: Platform setup
--- ---
<script setup> <script setup>
const demoAssetVersion = '20260223-2'; import { withBase } from 'vitepress';
const demoAssetVersion = '20260819-1';
</script> </script>
<div class="landing-shell"> <div class="landing-shell">
@@ -96,38 +98,38 @@ const demoAssetVersion = '20260223-2';
<div class="workflow-step" style="animation-delay: 0ms"> <div class="workflow-step" style="animation-delay: 0ms">
<div class="step-number">01</div> <div class="step-number">01</div>
<div class="step-title">Start</div> <div class="step-title">Start</div>
<div class="step-desc">Launch with the wrapper or existing mpv setup and keep subtitles in sync.</div> <div class="step-desc">Launch through the wrapper, or from an mpv setup you already have.</div>
</div> </div>
<div class="workflow-connector" aria-hidden="true"></div> <div class="workflow-connector" aria-hidden="true"></div>
<div class="workflow-step" style="animation-delay: 60ms"> <div class="workflow-step" style="animation-delay: 60ms">
<div class="step-number">02</div> <div class="step-number">02</div>
<div class="step-title">Lookup</div> <div class="step-title">Lookup</div>
<div class="step-desc">Hover a token in the interactive overlay, then trigger Yomitan lookup to open context.</div> <div class="step-desc">Hover a token in the overlay to open the Yomitan popup for that word.</div>
</div> </div>
<div class="workflow-connector" aria-hidden="true"></div> <div class="workflow-connector" aria-hidden="true"></div>
<div class="workflow-step" style="animation-delay: 120ms"> <div class="workflow-step" style="animation-delay: 120ms">
<div class="step-number">03</div> <div class="step-number">03</div>
<div class="step-title">Mine</div> <div class="step-title">Mine</div>
<div class="step-desc">Create cards from Yomitan or mine sentence cards directly from subtitle lines.</div> <div class="step-desc">Add the word from Yomitan, or mine the whole line as a sentence card.</div>
</div> </div>
<div class="workflow-connector" aria-hidden="true"></div> <div class="workflow-connector" aria-hidden="true"></div>
<div class="workflow-step" style="animation-delay: 180ms"> <div class="workflow-step" style="animation-delay: 180ms">
<div class="step-number">04</div> <div class="step-number">04</div>
<div class="step-title">Enrich</div> <div class="step-title">Enrich</div>
<div class="step-desc">Automatically attach timing-accurate audio, sentence text, and visual evidence.</div> <div class="step-desc">SubMiner fills in the audio clip, the sentence, and a screenshot from that moment.</div>
</div> </div>
<div class="workflow-connector" aria-hidden="true"></div> <div class="workflow-connector" aria-hidden="true"></div>
<div class="workflow-step" style="animation-delay: 240ms"> <div class="workflow-step" style="animation-delay: 240ms">
<div class="step-number">05</div> <div class="step-number">05</div>
<div class="step-title">Track</div> <div class="step-title">Track</div>
<div class="step-desc">Open the stats dashboard to review sessions, vocabulary trends, and mine cards from past viewing history.</div> <div class="step-desc">Review past sessions and word trends, and mine anything you missed the first time.</div>
</div> </div>
</div> </div>
</section> </section>
<section class="demo-section"> <section class="demo-section">
<h2>See it in action</h2> <h2>See it in action</h2>
<p>Subtitles, lookup flow, and card enrichment from a real playback session.</p> <p>Recorded from an actual playback session: subtitle hover, lookup, and the card that comes out the other end.</p>
<div class="demo-window"> <div class="demo-window">
<div class="demo-window__bar"> <div class="demo-window__bar">
<span class="demo-window__dot"></span> <span class="demo-window__dot"></span>
@@ -135,11 +137,11 @@ const demoAssetVersion = '20260223-2';
<span class="demo-window__dot"></span> <span class="demo-window__dot"></span>
<span class="demo-window__title">subminer -- playback</span> <span class="demo-window__title">subminer -- playback</span>
</div> </div>
<video controls playsinline preload="metadata" :poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`"> <video controls playsinline preload="metadata" :poster="withBase(`/assets/minecard-poster.jpg?v=${demoAssetVersion}`)">
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" /> <source :src="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" type="video/webm" />
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" /> <source :src="withBase(`/assets/minecard.mp4?v=${demoAssetVersion}`)" type="video/mp4" />
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer"> <a :href="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" target="_blank" rel="noreferrer">
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" /> <img :src="withBase(`/assets/minecard.webp?v=${demoAssetVersion}`)" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
</a> </a>
</video> </video>
</div> </div>
+237 -359
View File
@@ -1,53 +1,54 @@
# Installation # Installation
## How the Pieces Fit Together SubMiner draws an interactive overlay on top of the [mpv](https://mpv.io) video player. While you watch Japanese media, you hover a word in the subtitles to look it up, then turn it into an Anki card without leaving the video.
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. Building cards from what you watch is called **sentence mining**. SubMiner bundles its own copy of **Yomitan** (a pop-up dictionary) and talks to **AnkiConnect** (an Anki add-on that lets other programs create cards), so it can fill in the sentence, audio, and screenshot for you.
To get a working setup you need: Getting started takes three steps:
1. **mpv** launched with an IPC socket so SubMiner can read subtitle data 1. Install mpv and the optional extras you want.
2. **SubMiner** (the Electron overlay app) 2. Install SubMiner.
3. **Dictionaries** imported into the bundled Yomitan instance (lookups won't work without at least one) 3. Launch it and follow the first-run setup.
4. **Anki + AnkiConnect** _(optional but recommended)_ for card creation and enrichment
The `subminer` launcher script handles step 1 automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or the equivalent named pipe on Windows) — without it the overlay will start but subtitles will never appear. ## 1. Install requirements
## Requirements Only mpv is required. Install ffmpeg too unless you are fine with cards that have no audio or screenshot.
### System Dependencies | Dependency | Needed for | Platforms |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------ |
| mpv | Required. The player SubMiner draws over. | All |
| fuse2 | Required to run the AppImage. | Linux |
| ffmpeg | Recommended. Audio clips and screenshots on cards. Without it those fields stay empty. | All |
| MeCab + mecab-ipadic | Recommended. More accurate N+1, JLPT, and frequency highlighting. | All |
| yt-dlp | YouTube playback. | All |
| xz | [TsukiHime](/tsukihime-integration) subtitle downloads. Most Linux distros already have it. | All |
| guessit | Better title, season, and episode detection for [AniSkip](/aniskip-integration) and [AniList](/anilist-integration). | All |
| alass or ffsubsync | Subtitle syncing. You need at least one to use it. | All |
| fzf, rofi | The file pickers in the `subminer` command (rofi is Linux only). | Linux, macOS |
| chafa, ffmpegthumbnailer | Thumbnail previews in the pickers. | Linux, macOS |
| Dependency | Required | Notes | To generate Japanese subtitles from audio, you also need whisper.cpp. See [Subtitle generation](/subtitle-generation).
| -------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| mpv | Yes | Must support IPC sockets (`--input-ipc-server`) |
| Bun | For wrapper | Required for `subminer` CLI wrapper and source builds. Pre-built releases (AppImage, DMG, installer) work without it — only the `subminer` wrapper script needs Bun on `PATH`. |
| ffmpeg | Recommended | Audio extraction and screenshot generation. Without it SubMiner still runs, but audio and image fields on Anki cards will be empty. |
| MeCab + mecab-ipadic | No | Adds part-of-speech data used to filter particles out of N+1, JLPT, and frequency annotations. Without it annotations still render, but POS-based filtering is less precise. |
| fuse2 | Linux only | Required for AppImage |
| yt-dlp | No | Recommended for YouTube playback and subtitle extraction |
### Platform-Specific ### Linux
**Linux** — one of the following window backends: SubMiner needs to track the mpv window, and how it does that depends on your desktop:
- **Hyprland** — native Wayland support (uses `hyprctl`) - **Hyprland**: supported natively through `hyprctl`.
- **Sway** — native Wayland support (uses `swaymsg`) - **Sway**: supported natively through `swaymsg`.
- **X11 / Xwayland** — for X11 sessions or any other Wayland compositor (uses `xdotool` and `xwininfo`) - **Anything else** (X11, GNOME, KDE Plasma, other Wayland compositors): mpv and SubMiner must run under X11 or Xwayland. Install `xdotool` and `xwininfo`. The `subminer` command picks the X11 backend automatically, or you can force it with `--backend x11`.
::: 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.
:::
<details> <details>
<summary><b>Arch Linux</b></summary> <summary><b>Arch Linux</b></summary>
```bash ```bash
sudo pacman -S --needed mpv ffmpeg sudo pacman -S --needed mpv ffmpeg
# Recommended (MeCab is only in the AUR)
paru -S --needed mecab-git mecab-ipadic
# Optional # Optional
sudo pacman -S --needed mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer sudo pacman -S --needed yt-dlp fzf rofi chafa ffmpegthumbnailer
# Optional: subtitle sync (at least one needed for subtitle syncing) # Optional: subtitle sync (install at least one)
paru -S --needed alass python-ffsubsync paru -S --needed alass python-ffsubsync
# X11 / Xwayland (required for non-Hyprland/Sway compositors) # Only for desktops other than Hyprland or Sway
sudo pacman -S --needed xdotool xorg-xwininfo sudo pacman -S --needed xdotool xorg-xwininfo
``` ```
@@ -58,13 +59,15 @@ sudo pacman -S --needed xdotool xorg-xwininfo
```bash ```bash
sudo apt install mpv ffmpeg sudo apt install mpv ffmpeg
# Recommended
sudo apt install mecab libmecab-dev mecab-ipadic-utf8
# Optional # Optional
sudo apt install mecab libmecab-dev mecab-ipadic-utf8 fzf rofi chafa ffmpegthumbnailer yt-dlp sudo apt install yt-dlp fzf rofi chafa ffmpegthumbnailer
# X11 / Xwayland (required for non-Hyprland/Sway compositors) # Only for desktops other than Hyprland or Sway
sudo apt install xdotool x11-utils sudo apt install xdotool x11-utils
# Optional: subtitle sync # Optional: subtitle sync
pip install ffsubsync pip install ffsubsync
# alass is not in apt — install via cargo: cargo install alass-cli cargo install alass-cli
``` ```
</details> </details>
@@ -74,394 +77,269 @@ pip install ffsubsync
```bash ```bash
sudo dnf install mpv ffmpeg sudo dnf install mpv ffmpeg
# Recommended
sudo dnf install mecab mecab-ipadic
# Optional # Optional
sudo dnf install mecab mecab-ipadic fzf rofi chafa ffmpegthumbnailer yt-dlp sudo dnf install yt-dlp fzf rofi chafa ffmpegthumbnailer
# X11 / Xwayland (required for non-Hyprland/Sway compositors) # Only for desktops other than Hyprland or Sway
sudo dnf install xdotool xorg-x11-utils sudo dnf install xdotool xorg-x11-utils
# Optional: subtitle sync # Optional: subtitle sync
pip install ffsubsync pip install ffsubsync
# alass: cargo install alass-cli cargo install alass-cli
``` ```
</details> </details>
**macOS** — macOS 10.13 or later. Accessibility permission required for window tracking. ### macOS
You need macOS 11 (Big Sur) or later.
```bash ```bash
brew install mpv ffmpeg brew install mpv ffmpeg
# Optional but recommended for annotations # Recommended
brew install mecab mecab-ipadic brew install mecab mecab-ipadic
# Optional # Optional
brew install yt-dlp fzf rofi chafa ffmpegthumbnailer brew install yt-dlp fzf chafa ffmpegthumbnailer
# Optional: subtitle sync # Optional: subtitle sync
brew install alass brew install alass
pip install ffsubsync pip install ffsubsync
``` ```
**Windows** — Windows 10 or later. Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on `PATH`. Keep `mpv.exe` on `PATH` for auto-discovery or set `mpv.executablePath` in config if it lives elsewhere. SubMiner's packaged build handles window tracking directly. Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary. `mecab` must be on your `PATH` when SubMiner starts. Homebrew puts it in `/opt/homebrew/bin` on Apple Silicon and `/usr/local/bin` on Intel.
### Optional Tools ### Windows
| Tool | Purpose | You need Windows 10 or later. Install mpv and ffmpeg with [winget](https://learn.microsoft.com/windows/package-manager/winget/), which ships with Windows 11 and current Windows 10. In PowerShell or Command Prompt:
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| fzf | Terminal-based video picker (default) |
| rofi | GUI-based video picker |
| chafa | Thumbnail previews in fzf |
| ffmpegthumbnailer | Generate video thumbnails for picker |
| guessit | Better AniSkip title/season/episode parsing for file playback |
| alass | Subtitle sync engine (preferred) — must be on `PATH` or set `subsync.alass_path` in config; subtitle syncing is disabled without it or ffsubsync |
| ffsubsync | Subtitle sync engine (fallback) — must be on `PATH` or set `subsync.ffsubsync_path` in config; subtitle syncing is disabled without it or alass |
## Linux ```powershell
winget install shinchiro.mpv
winget install Gyan.FFmpeg
winget install yt-dlp.yt-dlp # optional, for YouTube
```
### Arch Linux (AUR) Close and reopen the terminal, then check both commands work:
Install [`subminer-bin`](https://aur.archlinux.org/packages/subminer-bin) from the AUR if you want the packaged Linux release managed by pacman. The package installs the official SubMiner AppImage plus the `subminer` wrapper. ```powershell
mpv --version
ffmpeg -version
```
ffmpeg must be on `PATH`, because SubMiner runs it by name to make card audio and screenshots. mpv does not have to be. If `mpv --version` says `not recognized`, find `mpv.exe` (usually in `%LOCALAPPDATA%\Programs\mpv`) and either add that folder to `PATH` or enter the full path to `mpv.exe` during first-run setup (`mpv.executablePath`).
<details>
<summary><b>Alternative: Scoop (no admin rights, includes xz)</b></summary>
[Scoop](https://scoop.sh) installs into your user profile and always adds commands to `PATH`. It is the only Windows package manager that also packages `xz`, which [TsukiHime](/tsukihime-integration) downloads need.
```powershell
# One-time Scoop setup
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
scoop bucket add extras
scoop install extras/mpv main/ffmpeg
# Optional
scoop install main/yt-dlp main/xz
```
</details>
<details>
<summary><b>Alternative: manual download</b></summary>
1. Download mpv from [mpv.io/installation](https://mpv.io/installation/) and ffmpeg from [ffmpeg.org/download.html](https://ffmpeg.org/download.html).
2. Unzip each into a permanent folder, for example `C:\Tools\mpv` and `C:\Tools\ffmpeg`. Find the folders that contain `mpv.exe` and `ffmpeg.exe` (for ffmpeg this is usually `bin`).
3. Press `Win`, search for **Edit the system environment variables**, and open it. Click **Environment Variables**, select **Path** under **User variables**, click **Edit**, and add both folders with **New**.
4. Open a new terminal and run `mpv --version` and `ffmpeg -version`. If either says `not recognized`, the folder you added does not contain the `.exe`.
For `xz` without Scoop, download [XZ Utils](https://tukaani.org/xz/) and add its folder to `PATH` the same way.
</details>
For more accurate highlighting, install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary. The fzf and rofi pickers do not apply on Windows.
## 2. Install SubMiner
### Arch Linux (AUR) {#arch-aur}
Install [`subminer-bin`](https://aur.archlinux.org/packages/subminer-bin). It includes the AppImage and the `subminer` command.
```bash ```bash
paru -S subminer-bin paru -S subminer-bin
``` ```
Or manually: ### Linux (AppImage) {#linux-appimage}
```bash
git clone https://aur.archlinux.org/subminer-bin.git
cd subminer-bin
makepkg -si
```
### AppImage (Recommended)
Download the latest AppImage and the `subminer` launcher from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest).
**Step 1 — Install Bun** (required for the launcher):
```bash
curl -fsSL https://bun.sh/install | bash
```
The `subminer` launcher uses a Bun shebang. The AppImage itself does **not** need Bun — only the launcher does. If you skip the launcher and run the AppImage directly (for example `SubMiner.AppImage --start`), you can skip this step, but you will need to configure `mpv.conf` with `input-ipc-server=/tmp/subminer-socket` manually.
**Step 2 — Download and install:**
```bash ```bash
mkdir -p ~/.local/bin mkdir -p ~/.local/bin
# Download and install AppImage
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage
chmod +x ~/.local/bin/SubMiner.AppImage chmod +x ~/.local/bin/SubMiner.AppImage
# Download and install the subminer launcher (recommended)
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer
chmod +x ~/.local/bin/subminer
# Download launcher support assets used for bundled runtime plugin injection
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
mkdir -p ~/.local/share/SubMiner/plugin/subminer
cp -R /tmp/plugin/subminer/. ~/.local/share/SubMiner/plugin/subminer/
``` ```
The `subminer` launcher is the recommended way to use SubMiner on Linux. It ensures mpv is launched with the correct IPC socket, SubMiner defaults, and the bundled runtime plugin so you don't need to configure `mpv.conf` or install a global mpv plugin. First-run setup can install the `subminer` command for you.
### From Source ### macOS (DMG) {#macos-dmg}
1. Download the DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest), open it, and drag `SubMiner.app` into `/Applications`.
2. If macOS blocks the app on first launch, right-click it and choose **Open**, or run:
```bash
xattr -d com.apple.quarantine /Applications/SubMiner.app
```
3. Open **System Settings > Privacy & Security > Accessibility** and enable SubMiner (add it if it is missing). The overlay cannot follow the mpv window without this.
First-run setup can install the `subminer` command for you.
### Windows (installer) {#windows-installer}
Download from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
- `SubMiner-<version>.exe`: the installer. Use this one.
- `SubMiner-<version>-win.zip`: portable version.
- `subminer.cmd`: optional terminal command (setup can install it for you).
### From source
<details>
<summary><b>Linux</b></summary>
```bash ```bash
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
cd SubMiner cd SubMiner
# if you cloned without --recurse-submodules: make deps
git submodule update --init --recursive
bun install
bun run build bun run build
bun run build:appimage # optional: package an AppImage
# Optional packaged Linux artifact
bun run build:appimage
``` ```
Bundled Yomitan is built during `bun run build`. Building from source needs [Bun](https://bun.sh) installed.
If you prefer Make wrappers for local install flows, `make build-launcher` still generates `dist/launcher/subminer` and `make install` still installs the wrapper/theme/AppImage when those artifacts exist.
`make build` also builds the bundled Yomitan Chrome extension from the `vendor/subminer-yomitan` submodule into `build/yomitan` using Bun. </details>
## macOS <details>
<summary><b>macOS</b></summary>
### DMG (Recommended)
Download the **DMG** artifact from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Open it and drag `SubMiner.app` into `/Applications`.
A **ZIP** artifact is also available as a fallback — unzip and drag `SubMiner.app` into `/Applications`.
Install dependencies using Homebrew:
```bash ```bash
brew install mpv ffmpeg git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
# Optional but recommended if you use N+1, JLPT, or frequency annotations cd SubMiner
brew install mecab mecab-ipadic make deps
make build-macos
``` ```
#### Install the `subminer` launcher (recommended) The `.dmg` and `.zip` land in `release/`. For an unsigned local build, run `bun run build:mac:unsigned`.
The `subminer` launcher is the recommended way to use SubMiner on macOS. It launches mpv with the correct IPC socket and SubMiner defaults so you don't need to set up an `mpv.conf` profile manually. </details>
Download it from the same [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) page: <details>
<summary><b>Windows</b></summary>
```powershell
git clone https://github.com/ksyasuda/SubMiner.git
cd SubMiner
git submodule update --init --recursive
bun install
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
```
</details>
## 3. Launch and first-run setup
Start SubMiner. The setup window opens on first launch.
- **Linux (AUR)**: `subminer app --setup`
- **Linux (AppImage)**: `~/.local/bin/SubMiner.AppImage --setup`
- **macOS**: open `SubMiner.app` from `/Applications`
- **Windows**: run SubMiner from the Start menu
Setup walks you through:
1. **Config file.** Created at `~/.config/SubMiner/config.jsonc` (Linux and macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows).
2. **Yomitan dictionaries.** Import at least one dictionary, or lookups will not work. SubMiner's Yomitan is separate from any Yomitan in your browser.
3. **The `subminer` command** (optional). Setup installs it into a folder already on your `PATH`. If there is none on Linux or macOS, it uses `~/.local/bin` and shows the `export PATH=...` line to add to your shell config. On Windows it adds `%LOCALAPPDATA%\SubMiner\bin` to your user `PATH`.
4. **SubMiner mpv shortcut** (Windows only). A Start menu or desktop shortcut that opens mpv with SubMiner attached.
**Finish setup** unlocks once the config exists and at least one dictionary is imported. To reopen setup later, run `subminer app --setup`.
### Play a video
```bash ```bash
sudo wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O /usr/local/bin/subminer subminer video.mkv
sudo chmod +x /usr/local/bin/subminer
``` ```
Or with curl: On Windows, double-click the **SubMiner mpv** shortcut or drag a video onto it.
The overlay appears over mpv, and the subtitle text becomes hoverable. See [Usage](/usage) for everyday use.
### Check your setup
```bash
subminer doctor
```
This checks for the SubMiner app, mpv, ffmpeg, yt-dlp, fzf, rofi, your config file, and the mpv socket path. Only a missing app or mpv counts as a failure. The rest are reported as optional.
## Anki setup
To create cards:
1. Install [Anki](https://apps.ankiweb.net/).
2. In Anki, open **Tools > Add-ons > Get Add-ons** and enter `2055492159` to install [AnkiConnect](https://ankiweb.net/shared/info/2055492159).
3. Restart Anki. Keep it open while you use SubMiner.
SubMiner connects to AnkiConnect at its default address with no extra setup. To choose your deck and card fields, see [Anki integration](/anki-integration).
## Updates
```bash
subminer -u
```
The tray menu's **Check for Updates** also installs updates on Linux, macOS, and Windows. If the AppImage sits in a folder you cannot write to, SubMiner prints the command to run instead of asking for admin rights.
If you installed from the AUR, update through your package manager instead.
## Launching mpv yourself
The `subminer` command and the Windows shortcut start mpv with the IPC socket SubMiner needs. If you start mpv another way, add this option or the overlay starts without subtitles:
```bash
--input-ipc-server=/tmp/subminer-socket # Linux and macOS
--input-ipc-server=\\.\pipe\subminer-socket # Windows
```
SubMiner loads its mpv plugin automatically, so there is nothing else to install. See [mpv plugin](/mpv-plugin) for the in-player keybindings.
## Manual launcher install
Use these if you skipped the launcher during setup. The launcher finds SubMiner in the usual install locations. For a custom location, set `SUBMINER_BINARY_PATH` to the app executable.
### Linux {#manual-launcher-install-linux}
```bash
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer
chmod +x ~/.local/bin/subminer
```
### macOS {#manual-launcher-install-macos}
```bash ```bash
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer
sudo chmod +x /usr/local/bin/subminer sudo chmod +x /usr/local/bin/subminer
``` ```
::: warning Bun required for the launcher ### Windows {#manual-launcher-install-windows}
The `subminer` launcher uses a Bun shebang (`#!/usr/bin/env bun`), so [Bun](https://bun.sh) must be installed and available on `PATH`. Install Bun if you haven't already: `curl -fsSL https://bun.sh/install | bash`.
:::
### From Source (macOS) Download `subminer.cmd` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and put it in a folder on your user `PATH`.
```bash ## Bundled Bun runtime {#bundled-bun-runtime}
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
cd SubMiner
git submodule update --init --recursive
make build-macos
```
The built app will be available in the `release` directory (`.dmg` and `.zip`). The `subminer` command runs on a copy of [Bun](https://bun.sh) 1.3.5 that ships inside the app, so you do not need to install Bun. Bun is MIT licensed and statically links JavaScriptCore (LGPL 2.0) and TinyCC (LGPL 2.1). License texts and a `SOURCE.md` ship in the app under `resources/bun/licenses`, and each GitHub release includes `bun-v1.3.5-source.tar.gz` with the matching sources.
For unsigned local builds: Next: [Usage](/usage).
```bash
bun run build:mac:unsigned
```
Build and install the launcher alongside the app:
```bash
make install-macos
```
This builds the `subminer` launcher into `dist/launcher/subminer` and installs it to `~/.local/bin/subminer` along with the app bundle and rofi theme. To install to `/usr/local/bin` instead (already on the default macOS `PATH`):
```bash
sudo make install-macos PREFIX=/usr/local
```
### Gatekeeper
If macOS blocks SubMiner on first launch, right-click the app and select **Open** to bypass the warning. Alternatively, remove the quarantine attribute:
```bash
xattr -d com.apple.quarantine /Applications/SubMiner.app
```
### Accessibility Permission
After launching SubMiner for the first time, grant accessibility permission:
1. Open **System Settings** → **Privacy & Security** → **Accessibility**
2. Enable SubMiner in the list (add it if it does not appear)
Without this permission, window tracking will not work and the overlay won't follow the mpv window.
### macOS Usage Notes
**Launching with the `subminer` launcher (recommended):**
```bash
subminer video.mkv
```
The launcher handles the IPC socket and SubMiner defaults automatically. If you prefer to launch mpv manually:
```bash
mpv --input-ipc-server=/tmp/subminer-socket video.mkv
```
**Config location:** `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`).
**MeCab paths (Homebrew):**
- Apple Silicon (M1/M2): `/opt/homebrew/bin/mecab`
- Intel: `/usr/local/bin/mecab`
Ensure `mecab` is available on your PATH when launching SubMiner.
**Fullscreen:** The overlay should appear correctly in fullscreen. If you encounter issues, check that accessibility permissions are granted.
**mpv plugin binary path:**
```ini
binary_path=/Applications/SubMiner.app/Contents/MacOS/subminer
```
## Windows
> [!WARNING]
> **Windows support is experimental.** Core features — mining, annotations, and dictionary lookups — work, but some functionality may be missing or unstable. Bug reports welcome.
### Prerequisites
1. Install [`mpv`](https://mpv.io/installation/) and ensure `mpv.exe` is on `PATH`. If mpv is installed elsewhere, you can set `mpv.executablePath` in `config.jsonc` or use the first-run setup field to point at the executable.
2. Install [`ffmpeg`](https://ffmpeg.org/download.html) and add it to `PATH` — recommended for audio/screenshot extraction (without it, media fields on Anki cards will be empty).
3. _(Optional)_ Install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for annotation POS filtering.
No compositor tools or window helpers are needed — native window tracking is built in on Windows.
### Installer (Recommended)
Download the latest Windows installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
- `SubMiner-<version>.exe` installs the app, Start menu shortcut, and default files under `Program Files`
- `SubMiner-<version>.zip` is available as a portable fallback
### Getting Started on Windows
1. **Run `SubMiner.exe` once** — first-run setup creates `%APPDATA%\SubMiner\config.jsonc` and opens Yomitan settings for dictionary import. The global mpv plugin install is optional for compatibility; the SubMiner mpv shortcut injects the bundled runtime plugin.
2. **Create the SubMiner mpv shortcut** _(recommended)_ — the setup popup offers to create a `SubMiner mpv` Start Menu and/or Desktop shortcut. This is the recommended way to launch playback on Windows.
3. **Play a video** — double-click the shortcut, drag a video file onto it, or run from a terminal:
```powershell
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
```
The shortcut and `--launch-mpv` pass SubMiner's default IPC socket, subtitle args, and bundled runtime plugin directly — no `mpv.conf` profile or global mpv plugin install is needed.
### Windows-Specific Notes
- The `subminer` launcher script requires [Bun](https://bun.sh) and must be invoked with `bun run subminer` on Windows since the shebang is not supported. The **SubMiner mpv** shortcut or `SubMiner.exe --launch-mpv` is the simpler alternative.
- First-run plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is in a non-standard location.
- Plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket` — do not keep `/tmp/subminer-socket` on Windows.
- Config is stored at `%APPDATA%\SubMiner\config.jsonc`.
### From Source (Windows)
```powershell
git clone https://github.com/ksyasuda/SubMiner.git
cd SubMiner
bun install
# Windows requires building the texthooker-ui submodule manually before
# the main build (Linux/macOS handle this automatically during `bun run build`).
Set-Location vendor/texthooker-ui
bun install --frozen-lockfile
bun run build
Set-Location ../..
bun run build:win
```
Windows installer builds already get the required NSIS `WinShell` helper through electron-builder's cached `nsis-resources` bundle.
No extra repo-local WinShell plugin install step is required.
## MPV Plugin
SubMiner-managed playback loads the bundled mpv plugin at runtime. No separate global mpv plugin install is required when launching from the app, the launcher, or the packaged Windows SubMiner mpv shortcut.
::: warning Important
If first-run setup detects an older global SubMiner mpv plugin under mpv's `scripts` directory, use **Remove legacy mpv plugin** so regular mpv playback stops loading SubMiner.
:::
See [MPV Plugin](/mpv-plugin) for the keybindings, script messages, and runtime configuration reference.
## Anki Setup (Recommended)
If you plan to mine Anki cards (the primary use case for most users):
1. Install [Anki](https://apps.ankiweb.net/).
2. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on — open Anki, go to **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 will connect to it automatically with no extra config needed for basic card creation.
For enrichment configuration (sentence, audio, screenshot fields), see [Anki Integration](/anki-integration).
## First-Run Setup
Run the setup wizard to create a default config and finish initial configuration. You do **not** need to create the config manually — SubMiner handles it.
```bash
subminer app --setup
```
> [!NOTE]
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
The setup popup walks you through:
- **Config file**: auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
- **mpv plugin**: install the bundled Lua plugin for in-player keybindings
- **Yomitan dictionaries**: import at least one dictionary so lookups work
- **Windows shortcut** _(Windows only)_: optionally create a `SubMiner mpv` Start Menu/Desktop shortcut
The `Finish setup` button stays disabled until the plugin is installed and at least one dictionary is imported. Once you finish, SubMiner will not show the popup again.
> [!TIP]
> You can re-open the setup popup at any time with `subminer app --setup` or `SubMiner.AppImage --setup`.
Once setup is complete, play a video to verify everything works:
```bash
subminer video.mkv
```
You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay.
<details>
<summary><b>More launch examples</b></summary>
```bash
# Optional explicit overlay start for setups with plugin auto_start=no
subminer --start video.mkv
# Useful launch modes for troubleshooting
subminer --log-level debug video.mkv
SubMiner.AppImage --start --log-level debug
# Or with direct AppImage control
SubMiner.AppImage --background # Background tray service mode
SubMiner.AppImage --start
SubMiner.AppImage --start --dev
SubMiner.AppImage --help # Show all CLI options
```
</details>
## Verify Setup
After completing first-run setup, run the built-in diagnostic to confirm everything is in place:
```bash
subminer doctor
```
This checks for the app binary, mpv, ffmpeg, config file, and socket path. Fix any failures before continuing.
> [!NOTE]
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly. Replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below.
## Optional Extras
### Rofi Theme (Linux Only)
SubMiner ships a custom rofi theme bundled in the release assets tarball.
Install path (default auto-detected by `subminer`):
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
```bash
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
mkdir -p ~/.local/share/SubMiner/themes
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.
+67 -46
View File
@@ -1,12 +1,12 @@
# IPC + Runtime Contracts # 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. The Electron main and renderer processes talk only through IPC channels. The renderer is an untrusted surface: it loads Yomitan and renders subtitle text SubMiner did not write. Every payload that crosses the bridge goes through a validator before domain code sees it.
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. Channel names, payload validators, the preload bridge, and the handler change together. When you touch an IPC surface, update all four in the same commit.
## Message Flow ## Message flow
Renderer-initiated calls (`invoke`) pass through four boundaries before reaching a service. Fire-and-forget messages (`send`) follow the same path but skip the response leg. Malformed payloads are caught at the validator and never reach domain code. Renderer calls pass through the preload bridge, the main-process handler, and a validator before they reach a service. Malformed payloads stop at the validator.
```mermaid ```mermaid
flowchart TB flowchart TB
@@ -36,58 +36,79 @@ flowchart TB
style E fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px style E fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
``` ```
## Core Surfaces `IPC_CHANNELS` in `src/shared/ipc/contracts.ts` groups channels by pattern:
- `request`: invoke channels. The renderer awaits a result, for example lookups, config reads, and mining actions. Invalid payloads return a structured failure such as `{ ok: false, ... }` instead of throwing.
- `command`: fire-and-forget sends, for example focus events, UI state hints, and position updates. Invalid payloads are dropped.
- `event`: messages pushed from main to the renderer.
## Runtime sockets
The bridge above lives inside the Electron app. Separate OS sockets connect the app to mpv and to the launcher and plugin. They carry no renderer payloads and do not go through the contract and validator layer.
- **mpv IPC socket**: `/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows. mpv creates it with `--input-ipc-server`. The app's `MpvIpcClient` sends JSON commands here and observes playback and subtitle properties.
- **App control socket**: `subminer-control-<uid>-<hash>.sock` in the temp directory, or `\\.\pipe\subminer-control-<hash>` on Windows. The launcher and plugin send CLI-style commands (`--start`, `--show-visible-overlay`, `--texthooker`) to a running app here. It also routes a second `subminer` invocation into the existing instance.
```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
```
[Playback startup flow](./architecture#playback-startup-flow) shows when each socket comes up during a launch.
## Core files
| File | Role | | File | Role |
| --- | --- | | -------------------------------------- | ----------------------------------------------------------------------------------- |
| `src/shared/ipc/contracts.ts` | Canonical channel names and payload type contracts. Single source of truth for both processes. | | `src/shared/ipc/contracts.ts` | Channel names and payload types, shared by both processes |
| `src/shared/ipc/validators.ts` | Runtime payload parsers and type guards. Every `invoke` payload is validated here before the handler runs. | | `src/shared/ipc/validators.ts` | Runtime payload parsers and type guards |
| `src/preload.ts` | Renderer-side bridge. Exposes a typed API surface to the renderer — only approved channels are accessible. | | `src/preload.ts` | Typed renderer API; only approved channels are exposed |
| `src/main/ipc-runtime.ts` | Main-process handler registration and routing. Wires validated channels to domain handlers. | | `src/core/services/ipc.ts` | Registers overlay handlers and validates payloads before calling domain logic |
| `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` | Same boundary for Anki and Jimaku operations |
| `src/core/services/anki-jimaku-ipc.ts` | Integration-specific IPC boundary for Anki and Jimaku operations. | | `src/main/ipc-runtime.ts` | Builds handler dependencies (via `src/main/dependencies.ts`) and registers handlers |
| `src/main/cli-runtime.ts` | CLI/runtime command boundary. Handles commands that originate from the launcher or mpv plugin rather than the renderer. | | `src/main/cli-runtime.ts` | Handles commands from the launcher or mpv plugin, not the renderer |
## Contract Rules ## 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. - **Use the shared constants.** Take channel names from `contracts.ts`, never string literals.
- **Validate before handling.** Every renderer payload goes through `validators.ts` before domain logic.
- **Return structured failures.** Invoke handlers return `{ ok: false, ... }` on failure instead of throwing, so the renderer can tell success from failure without try/catch.
- **Keep payloads narrow.** Send only what the handler needs, not whole state objects.
- **Keep handlers thin.** Validate, delegate to a service or composer, return. Route shared state changes through the transition helpers in `src/main/state.ts`.
- **Use shared constants.** Channel names come from `contracts.ts`, never ad-hoc literal strings. This makes channels greppable and refactor-safe. ## Add a new IPC action
- **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.
- **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.
**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
1. Add the channel constant in `src/shared/ipc/contracts.ts`. 1. Add the channel constant in `src/shared/ipc/contracts.ts`.
2. Add or extend the payload validator in `src/shared/ipc/validators.ts`. 2. Add or extend the validator in `src/shared/ipc/validators.ts`.
3. Expose a typed bridge method in `src/preload.ts`. 3. Expose a typed bridge method in `src/preload.ts`.
4. Register the handler in `src/main/ipc-runtime.ts` (or the relevant domain runtime module). 4. Register the handler in `src/core/services/ipc.ts` (or `anki-jimaku-ipc.ts`), and supply any new dependency through `src/main/ipc-runtime.ts`.
5. Add tests for both valid and malformed payload cases in `src/core/services/*`. 5. Test valid and malformed payloads in `src/core/services/*`.
6. Update renderer tests when behavior or state transitions change. 6. Update renderer tests if behavior or state transitions change.
## Runtime State Notes
- 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.
## Troubleshooting ## Troubleshooting
- **Unknown payload in handler:** The validator is not being applied before the handler runs. Check that the channel is routed through `ipc-runtime.ts` with validation, not registered directly. - **Handler receives an unexpected payload:** the validator is not applied. Check that the channel is registered through the IPC service with validation, not directly.
- **Renderer invoke fails:** Verify the preload bridge method exists and matches the channel constant. Check that the handler is registered and returning (not throwing). - **Renderer invoke fails:** check that the preload method exists, uses the right channel constant, and that the handler is registered and returns instead of throwing.
- **Contract drift:** When invoke calls return unexpected shapes, compare the shared contract, validator, preload bridge, and main handler signatures side by side. One of them was updated without the others. - **Invoke returns an unexpected shape:** compare the contract, validator, preload method, and handler side by side. One of them changed without the others.
## Related Docs ## Related docs
- [Architecture](/architecture) - [Architecture](/architecture)
- [Development](/development) - [Building and testing](/development)
- [Configuration](/configuration)
- [Troubleshooting](/troubleshooting)
+42 -167
View File
@@ -1,185 +1,60 @@
# Jellyfin Integration # Jellyfin integration
SubMiner includes an optional Jellyfin CLI integration for: If your anime lives on a [Jellyfin](https://jellyfin.org) server, SubMiner can appear as a cast target in any Jellyfin client. Cast an episode and it plays in SubMiner's mpv with the overlay and Yomitan lookup attached.
- authenticating against a server
- listing libraries and media items
- launching item playback in the connected mpv instance
- receiving Jellyfin remote cast-to-device playback events in-app
- opening an in-app setup window for server selection and authentication
- toggling Jellyfin cast discovery from the tray once configured
## Requirements
- 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.
## Setup ## Setup
1. Set base config values (`config.jsonc`): You need a Jellyfin server (Jellyfin 12 is supported) and your username and password.
```jsonc 1. Start SubMiner and leave it in the system tray.
{ 2. Open the tray menu and click **Configure Jellyfin**. You can also run `subminer jellyfin`.
"jellyfin": { 3. Enter the **Server URL** (for example `http://127.0.0.1:8096`), **Username**, and **Password**, then click **Login**.
"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: SubMiner stores an encrypted session token, not your password, and turns the integration on. Reopen the same window to switch servers or log out.
## Casting from Jellyfin
After you sign in, SubMiner connects to Jellyfin at startup and shows up in the cast ("Play on") menu under your computer's hostname. To connect for the current session only, tick **Jellyfin Discovery** in the tray menu.
1. In the Jellyfin web or mobile app, start playing an episode.
2. Open the cast menu and pick your computer.
SubMiner starts mpv if it is not already running. Pause, seek, stop, and track changes in the Jellyfin app are mirrored in mpv, and watch progress syncs back to Jellyfin. Playback resumes from Jellyfin's saved position.
SubMiner selects a Japanese subtitle track automatically and resets mpv's subtitle delay to zero. It direct-plays files when it can and asks Jellyfin to transcode the rest.
On Windows, casting finds mpv through `mpv.executablePath`, then `SUBMINER_MPV_PATH`, then `PATH`. An invalid `mpv.executablePath` stops mpv from starting.
## Playing from the terminal
The launcher can browse your libraries and play an item without a Jellyfin client:
```bash ```bash
subminer jellyfin subminer jellyfin -p # fzf picker; `jf` is an alias for `jellyfin`
subminer jellyfin -l \ subminer -R jellyfin -p # rofi picker
--server http://127.0.0.1:8096 \
--username your-user \
--password 'your-password'
``` ```
`subminer jellyfin` opens the setup window. It offers the configured server, recent servers, and a manual server URL field. Successful login keeps the window open, stores the Jellyfin session token in encrypted storage, updates the configured server/username/client metadata, and refreshes recent servers. Passwords are never stored. Sign in first. See [Launcher script](/launcher-script) for the other `jellyfin` subcommands.
3. List libraries: ## Options
```bash All options are under **Settings > Integrations > Jellyfin**, or `jellyfin` in `config.jsonc`. See [Configuration](/configuration#jellyfin) for the full list and defaults.
SubMiner.AppImage --jellyfin-libraries
```
Launcher wrapper equivalent for interactive playback flow: | Key | What it does |
| -------------------------- | -------------------------------------------------------------------- |
| `enabled` | Turns the integration on. Set for you when you sign in. |
| `serverUrl` | Your Jellyfin server. Filled in when you sign in. |
| `remoteControlEnabled` | Lets SubMiner act as a cast target. |
| `remoteControlAutoConnect` | Connects at startup. Turn off to start discovery from the tray. |
| `autoAnnounce` | Re-announces the device on connect. Try it if SubMiner appears late. |
| `transcodeVideoCodec` | Video codec requested when Jellyfin transcodes. |
```bash For headless setups, `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID` supply a session without the sign-in window. Treat the token store and `config.jsonc` as secrets.
subminer jellyfin -p
```
Launcher wrapper for Jellyfin cast discovery mode (background app + tray): ## Troubleshooting
```bash **SubMiner is missing from the cast menu.** Check that SubMiner is running, that you are signed in (log in again if the token expired), and that discovery is on. The Jellyfin client and SubMiner must use the same server.
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. **Casting starts but nothing plays.** Confirm the item plays in another Jellyfin client. If mpv was closed, give SubMiner a few seconds to start it.
Stop discovery session/app: **Linux token storage fails.** SubMiner stores the token with `gnome-libsecret` by default. Start your keyring, or pass `--password-store=basic_text`.
```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.
+36 -85
View File
@@ -1,111 +1,62 @@
# Jimaku Integration # 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 archive of Japanese subtitles for anime and live action. SubMiner searches it from the overlay, downloads the file you pick, and loads it into mpv.
## How It Works ## Setup
The Jimaku integration runs through an in-overlay modal accessible via a keyboard shortcut (`Ctrl+Shift+J` by default). 1. Create a free account at [jimaku.cc](https://jimaku.cc) and copy your API key.
2. Add the key to `config.jsonc`, either directly or through a command that prints it:
When you open the modal, SubMiner parses the current video filename to extract a title, season, and episode number. Common naming conventions are supported — `S01E03`, `1x03`, `E03`, and dash-separated episode numbers all work. If the filename yields a high-confidence match (title + episode), SubMiner auto-searches immediately.
From there:
1. **Search** — SubMiner queries the Jimaku API with the parsed title. Results appear as a list of anime entries (Japanese and English names).
2. **Browse entries** — Select an entry to load its available subtitle files, filtered by episode if one was detected.
3. **Browse files** — Files show name, size, and last-modified date. If a language preference is configured, files are sorted accordingly (e.g., Japanese-tagged files first).
4. **Download** — Selecting a file downloads it to the same directory as the video (or a temp directory for remote/streamed media) and loads it into mpv as a new subtitle track.
If no files match the current episode filter, a "Show all files" button lets you broaden the search to all episodes for that entry.
### Modal Keyboard Shortcuts
| Key | Action |
| --- | --- |
| `Enter` (in text field) | Search |
| `Enter` (in list) | Select entry / download file |
| `Arrow Up` / `Arrow Down` | Navigate entries or files |
| `Escape` | Close modal |
## Configuration
Add a `jimaku` section to your `config.jsonc`:
```jsonc ```jsonc
{ {
"jimaku": { "jimaku": {
"apiKey": "YOUR_API_KEY", "apiKey": "YOUR_API_KEY",
"apiKeyCommand": "cat ~/.jimaku_key", // or, to keep it out of the config file:
"apiBaseUrl": "https://jimaku.cc", // "apiKeyCommand": "pass jimaku/api-key",
"languagePreference": "ja", },
"maxEntryResults": 10
}
} }
``` ```
| Option | Type | Default | Description | If both are set, `apiKey` wins. `apiKeyCommand` must print the key within 10 seconds. Without a key, the modal shows "Jimaku API key not set."
| --- | --- | --- | --- |
| `jimaku.apiKey` | `string` | — | Jimaku API key (plaintext). Mutually exclusive with `apiKeyCommand`. |
| `jimaku.apiKeyCommand` | `string` | — | Shell command that prints the API key to stdout. Useful for secret managers (e.g., `pass jimaku/api-key`). |
| `jimaku.apiBaseUrl` | `string` | `"https://jimaku.cc"` | Base URL for the Jimaku API. Only change this if using a mirror or local instance. |
| `jimaku.languagePreference` | `"ja"` \| `"en"` \| `"none"` | `"ja"` | Sort subtitle files by language tag. `"ja"` pushes Japanese-tagged files to the top; `"en"` does the same for English. `"none"` preserves the API order. |
| `jimaku.maxEntryResults` | `number` | `10` | Maximum number of anime entries returned per search. |
The keyboard shortcut is configured separately under `shortcuts`: ## Usage
```jsonc 1. Press `Ctrl+Shift+J` during playback.
{ 2. SubMiner fills in the title, season, and episode from the file name. If it finds both a title and an episode, it searches right away. Otherwise, fix the fields and press `Enter`.
"shortcuts": { 3. Pick the **Anime** or **Live action** tab. Switching tabs repeats the search.
"openJimaku": "Ctrl+Shift+J" 4. Select an entry, then select a file. Files are filtered to the current episode. Click **Broaden search (all files)** to see every file in the entry.
}
}
```
### API Key The file is saved next to the video (or to a temp directory for streams) and loaded into mpv as a new subtitle track.
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: | Key | Action |
| ---------------- | -------------------------------------- |
| `Enter` | Search, or select the highlighted item |
| `Up` / `Down` | Move through entries or files |
| `Left` / `Right` | Switch tabs |
| `Escape` | Close |
- **`apiKey`** — set the key directly in config. Simple, but the key is stored in plaintext. You can also open the modal with `subminer app --open-jimaku`, or change the shortcut with `shortcuts.openJimaku`.
- **`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. The file name parser understands `S01E03`, `1x03`, `E03`, `EP03`, and `Title - 03 -` patterns, and reads the season from a parent folder such as `Season 2`. It ignores bracket tags like `[SubGroup]` and year tags like `(2024)`.
## Filename Parsing ## Options
SubMiner extracts media info from the current video path to pre-fill the search fields. The parser handles: | Key | What it does |
| --------------------------- | ------------------------------------------------------------------- |
| `jimaku.apiKey` | API key in plain text. |
| `jimaku.apiKeyCommand` | Shell command that prints the API key. |
| `jimaku.languagePreference` | Sorts files tagged with this language first: `ja`, `en`, or `none`. |
| `jimaku.maxEntryResults` | Maximum entries per search. |
| `jimaku.apiBaseUrl` | API address. Change only for a mirror. |
- **Season + episode patterns:** `S01E03`, `1x03` See [Configuration](/configuration#jimaku) for defaults.
- **Episode-only patterns:** `E03`, `EP03`, or dash-separated numbers like `Title - 03 -`
- **Bracket tags:** `[SubGroup]`, `[1080p]`, `[HEVC]` — stripped before title extraction
- **Year tags:** `(2024)` — stripped
- **Dots and underscores:** treated as spaces
- **Remote/streamed URLs:** SubMiner checks URL query parameters (`title`, `name`, `q`) and path segments to extract a meaningful title
If the parser produces a high-confidence result (title + episode both detected), the search runs automatically when the modal opens. Otherwise, you can adjust the fields manually before searching.
## Troubleshooting ## Troubleshooting
**"Jimaku API key not set"** **"Jimaku API key not set."** Set `jimaku.apiKey` or `jimaku.apiKeyCommand`. Run the command in your shell to confirm it prints only the key.
Configure `jimaku.apiKey` or `jimaku.apiKeyCommand` in your config. If using `apiKeyCommand`, verify the command works in your shell: it should print the key and exit cleanly. **HTTP 429.** You hit Jimaku's rate limit. Wait for the time shown in the message and retry.
**"Jimaku request failed" or HTTP 429** **No entries found.** Search with just the show's name, without season or episode words. Jimaku matches against its own titles.
The Jimaku API has rate limits. If you see 429 errors, wait for the retry duration shown in the OSD message and try again. An API key provides higher rate limits. **The subtitle downloads but does not load.** SubMiner loads it over the mpv socket. Make sure mpv is still running and connected.
**No entries found**
Try simplifying the title — remove season/episode qualifiers and search with just the anime name. Jimaku's search matches against its own database of anime titles, so the exact spelling matters.
**No files found for this episode**
The entry may not have per-episode files, or files may be named differently. Click "Show all files" to see everything available for the entry.
**Downloaded subtitle not loading**
Verify mpv is running and connected via IPC. SubMiner loads the subtitle by issuing a `sub-add` command over the mpv socket. If mpv is not connected, the download succeeds but the subtitle cannot be loaded.
## Related
- [Configuration Reference](/configuration#jimaku) — full config options
- [Mining Workflow](/mining-workflow#jimaku-subtitle-search) — how Jimaku fits into the sentence mining loop
- [Troubleshooting](/troubleshooting#jimaku) — additional error guidance
+181 -101
View File
@@ -1,116 +1,196 @@
# Launcher Script # Launcher script
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. `subminer` is the command-line entry point for SubMiner. It starts mpv with the socket and options SubMiner needs, opens file pickers, and runs helper commands. This page is the reference for its subcommands and flags. For everyday use, start with [Usage](/usage).
::: tip Windows users You do not need Bun or anything else installed to run it. It uses the runtime bundled with the app. On Windows, the **SubMiner mpv** shortcut is the simpler way to play files (see [Windows mpv shortcut](/usage#windows-mpv-shortcut)).
On Windows the `subminer` script cannot run directly via shebang — use `bun run subminer` instead (e.g. `bun run subminer video.mkv`). The recommended alternative is the **SubMiner mpv** shortcut created during first-run setup, or `SubMiner.exe --launch-mpv`. See [Windows mpv Shortcut](/usage#windows-mpv-shortcut) for details.
:::
## Video Picker
When you run `subminer` without specifying a file, it opens an interactive video picker. By default it uses **fzf** in the terminal; pass `-R` to use **rofi** instead.
### fzf (default)
```bash ```bash
subminer # pick from current directory subminer [options] [file | directory | URL]
subminer -d ~/Videos # pick from a specific directory subminer <subcommand> [options]
subminer -r -d ~/Anime # recursive search
``` ```
fzf shows video files in a fuzzy-searchable list. If `chafa` is installed, you get thumbnail previews in the right pane. Thumbnails are sourced from the freedesktop thumbnail cache first, then generated on the fly with `ffmpegthumbnailer` or `ffmpeg` as fallback. Run `subminer -h` or `subminer <subcommand> -h` for built-in help.
| Optional tool | Purpose |
| ------------------- | --------------------------------- |
| `chafa` | Render thumbnails in the terminal |
| `ffmpegthumbnailer` | Generate thumbnails on the fly |
### rofi
```bash
subminer -R # rofi picker, current directory
subminer -R -d ~/Videos # rofi picker, specific directory
subminer -R -r -d ~/Anime # rofi picker, recursive
subminer -R /directory # rofi picker, directory shortcut
```
rofi shows a GUI menu with icon thumbnails when available. SubMiner ships a custom rofi theme bundled in the release assets tarball:
```bash
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
mkdir -p ~/.local/share/SubMiner/themes
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
```
The theme is auto-detected from these paths (first match wins):
- `$SUBMINER_ROFI_THEME` environment variable (absolute path)
- `$XDG_DATA_HOME/SubMiner/themes/subminer.rasi` (default: `~/.local/share/SubMiner/themes/subminer.rasi`)
- `/usr/local/share/SubMiner/themes/subminer.rasi`
- `/usr/share/SubMiner/themes/subminer.rasi`
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
Override with the `SUBMINER_ROFI_THEME` environment variable:
```bash
SUBMINER_ROFI_THEME=/path/to/custom-theme.rasi subminer -R
```
## Common Commands
```bash
subminer video.mkv # play a specific file (default plugin config auto-starts visible overlay)
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
subminer --backend x11 video.mkv # Force x11 backend for a specific file
subminer stats # open immersion dashboard
subminer stats -b # start background stats daemon
```
## Subcommands
| Subcommand | Purpose |
| ------------------------------------------ | ------------------------------------------------------------------ |
| `subminer jellyfin` / `jf` | Jellyfin workflows (`-d` discovery, `-p` play, `-l` login) |
| `subminer stats` | Start stats server and open immersion dashboard in browser |
| `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 config path` | Print active config file path |
| `subminer config show` | Print active config contents |
| `subminer mpv status` | Check mpv socket readiness |
| `subminer mpv socket` | Print active socket path |
| `subminer mpv idle` | Launch detached idle mpv instance |
| `subminer dictionary <path>` | Generate character dictionary ZIP from file/dir target |
| `subminer dictionary --candidates <path>` | List AniList candidate matches for character dictionary correction |
| `subminer dictionary --select <id> <path>` | Pin an AniList media ID for that target series |
| `subminer texthooker` | Launch texthooker-only mode |
| `subminer texthooker -o` | Launch texthooker and open it in the default browser |
| `subminer app` | Pass arguments directly to SubMiner binary |
Use `subminer <subcommand> -h` for command-specific help.
## Options ## Options
| Flag | Description | | Flag | Description |
| --------------------- | -------------------------------------------------------------------- | | --------------------- | ----------------------------------------------------------------------------------- |
| `-d, --directory` | Video search directory (default: cwd) | | `-d, --directory` | Directory to browse (default: current directory) |
| `-r, --recursive` | Search directories recursively | | `-r, --recursive` | Search subdirectories |
| `-R, --rofi` | Use rofi instead of fzf | | `-R, --rofi` | Use rofi instead of fzf |
| `--setup` | Open first-run setup popup manually | | `-H, --history` | Browse [watch history](#watch-history) |
| `--start` | Explicitly start overlay after mpv launches | | `-b, --backend` | Window backend: `auto`, `hyprland`, `sway`, `x11`, `macos`, `windows` |
| `-S, --start-overlay` | Explicitly start overlay after mpv launches | | `-p, --profile` | mpv profile to load |
| `-T, --no-texthooker` | Disable texthooker server | | `-a, --args` | Extra mpv options as one quoted string, e.g. `--args "--volume=80"` |
| `-p, --profile` | mpv profile name (no default; omitted unless set) | | `--start` | Start the overlay after mpv launches. Only needed if `mpv.autoStartSubMiner` is off |
| `-a, --args` | Pass additional mpv arguments as a quoted string | | `-S, --start-overlay` | Show the overlay on start |
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`, `macos`, `windows`) | | `-T, --no-texthooker` | Do not start the texthooker server |
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) | | `--settings` | Open the settings window |
| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) | | `--log-level` | `debug`, `info`, `warn`, or `error` |
| `-u, --update` | Check for and install updates |
| `-v, --version` | Print the launcher's version |
With default plugin settings (`auto_start=yes`, `auto_start_visible_overlay=yes`, `auto_start_pause_until_ready=yes`), explicit start flags are usually unnecessary. The target can be a video file, a directory (opens the picker there), a URL, or `ytsearch:"query"` for the first YouTube search result.
App flags such as `--setup` and `--dev` are not launcher flags. Pass them through with `subminer app`, for example `subminer app --setup`.
## Subcommands
| Command | What it does |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `subminer stats` | Start the stats dashboard server. Opens your browser if `stats.autoOpenBrowser` is on |
| `subminer stats -b` / `-s` | Start (or reuse) the stats server in the background / stop it |
| `subminer stats cleanup` | Backfill vocabulary metadata and prune stale rows (same as `-v`) |
| `subminer stats cleanup -l` | Rebuild lifetime totals from the retained sessions |
| `subminer stats cleanup -d` | Collapse repeated lines from typeset subtitles. Add `--dry-run` to preview, `--lookback-days <n>` to limit the range |
| `subminer stats rebuild` / `backfill` | Same as `stats cleanup -l` |
| `subminer sync <host>` | Sync stats and watch history with another machine. See [below](#sync-between-machines) |
| `subminer doctor` | Check the app, mpv, ffmpeg, yt-dlp, pickers, config, and mpv socket |
| `subminer doctor --refresh-known-words` | Refresh the known-word cache from Anki |
| `subminer settings` | Open the settings window |
| `subminer youtube` / `yt` | Open the [YouTube browser](/youtube-integration#browse-youtube-in-subminer). Videos you pick play in mpv |
| `subminer generate-subs [video]` | Generate [Japanese subtitles](/subtitle-generation) with whisper.cpp |
| `subminer jellyfin` / `jf` | [Jellyfin](/jellyfin-integration) actions: `setup`, `login`, `logout`, `play`, `discovery` |
| `subminer dictionary <path>` / `dict` | Build a [character dictionary](/character-dictionary) for a file or directory |
| `subminer dictionary --candidates <path>` | List AniList matches for that target |
| `subminer dictionary --select <id> <path>` | Pin an AniList ID for that target |
| `subminer texthooker` | Run only the texthooker server. `-o` opens it in your browser |
| `subminer logs -e` | Export a sanitized log ZIP and print its path |
| `subminer config path` / `show` | Print the config file path or its contents |
| `subminer mpv status` | Exit 0 if the mpv socket is ready, 1 if not |
| `subminer mpv socket` | Print the mpv socket path |
| `subminer mpv idle` | Start an idle mpv in the background with SubMiner's options |
| `subminer app` / `bin` | Pass arguments to the SubMiner app, e.g. `subminer app --stop` |
`stats cleanup` runs one mode at a time. `--lookback-days` must be at least 1. Without it, cleanup scans all history.
`generate-subs` options: `--download-model`, `--model <name>`, `--model-path <file>`, `--output <file>`, and `--audio-stream <index>` (an ffprobe stream index). `--model-path` cannot be combined with `--model` or `--download-model`.
A texthooker is a web page that shows the current subtitle as plain text, so browser extensions and other tools can read along.
## Video picker
With no file argument, `subminer` opens a picker for the current directory, or for `-d <dir>`. Add `-r` to include subdirectories.
- **fzf** (default) runs in the terminal. With `chafa` installed, it shows thumbnail previews.
- **rofi** (`-R`, Linux) opens a graphical menu with thumbnails.
Thumbnails come from your system thumbnail cache, or are generated with `ffmpegthumbnailer` or `ffmpeg`.
The launcher installs its rofi theme automatically. To use your own, set `SUBMINER_ROFI_THEME`:
```bash
SUBMINER_ROFI_THEME=/path/to/theme.rasi subminer -R
```
## Watch history
`subminer -H` lists the shows you have watched, most recent first. Add `-R` to use rofi. Pick a show, then choose:
- **Previous episode** or **Next episode**, moving into the neighboring season folder when needed
- **Replay last watched**
- **Browse episodes**, with a season menu first if the show has several season folders
- **Quit SubMiner**
When an episode ends, the menu comes back for the same show. Press `Escape` to leave.
History comes from the immersion stats database, which SubMiner fills during playback. Shows whose folders are not reachable, such as an unmounted network drive, are hidden.
## mpv options and profiles
The launcher starts mpv with these options:
```
--input-ipc-server=/tmp/subminer-socket
--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
--sub-auto=fuzzy
--sub-file-paths=.;subs;subtitles
--sid=auto
--secondary-sid=auto
--sub-visibility=no
--secondary-sub-visibility=no
```
mpv's own subtitles are hidden because the overlay draws them. Add more options with `-a`, or load an mpv profile with `-p <name>` or `mpv.profile` in the config. No profile is loaded by default.
To launch mpv yourself with the same setup, put the options in a profile in `~/.config/mpv/mpv.conf`:
```ini
[subminer]
input-ipc-server=/tmp/subminer-socket
alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
sub-auto=fuzzy
sub-file-paths=.;subs;subtitles
sid=auto
secondary-sid=auto
secondary-sub-visibility=no
```
Launches through `subminer` start the overlay automatically unless `mpv.autoStartSubMiner` is off. mpv started outside SubMiner does not start the overlay on its own.
## Sync between machines
`subminer sync <host>` merges immersion stats and watch history between two computers over SSH. Both end up with the combined sessions, totals, vocabulary, charts, and `-H` history. `<host>` is anything `ssh` accepts, such as `user@hostname` or an alias from your SSH config.
Both machines need the same SubMiner version. The remote only needs the app. The `subminer` command is optional there.
```bash
subminer sync macbook # two-way sync
subminer sync macbook --push # send local data to macbook only
subminer sync macbook --pull # bring macbook data here only
subminer sync macbook --check # test SSH and the remote install, change nothing
subminer sync macbook --remote-cmd ~/Apps/SubMiner.AppImage # SubMiner in a custom place on the remote
subminer sync --ui # open the sync window
```
Syncing only adds data. It never overwrites or double-counts, so you can run it as often as you like. `--push` and `--pull` do not delete anything on the receiving side.
Before a command-line sync, close SubMiner on both machines and stop the stats server with `subminer stats -s`, or pass `--force`. The sync window does not need this. It syncs while SubMiner and playback are running, and skips the session in progress until it finishes.
Transfers are compressed. When both machines have `rsync` (macOS and Linux), later syncs send only what changed. Windows machines use `scp`.
Known-word status from Anki does not sync. Each machine reads it from its own Anki collection.
<details>
<summary><b>More sync options</b></summary>
| Option | Description |
| ------------------- | ----------------------------------------------------------- |
| `-f, --force` | Skip the check that SubMiner is closed |
| `--db <file>` | Use a different local stats database |
| `--json` | Print progress as NDJSON |
| `--snapshot <file>` | Write a snapshot of the local database, e.g. to copy by USB |
| `--merge <file>` | Merge a snapshot file into the local database |
A Windows remote needs the built-in **OpenSSH Server** enabled. SubMiner finds itself in the default install location there.
If the remote cannot find SubMiner, point `--remote-cmd` at the app or launcher, or link it as `SubMiner` somewhere on the remote `PATH`.
Received snapshots are cached in `sync-transfer-cache/` in the config directory to speed up later syncs. Deleting it is safe.
</details>
### Sync window
`subminer sync --ui`, or **Sync Stats & History** in the tray, opens a window where you can:
- Save devices, each with a direction (two-way, push, or pull), and run **Sync now** or **Test**.
- Turn on **Auto-sync** for a device. It syncs in the background every 60 minutes by default, including during playback.
- Watch progress and see what was merged on each machine.
- Create, merge, or delete database snapshots.
Saved devices live in `sync-hosts.json` in the config directory.
## Environment variables
| Variable | Use |
| ------------------------ | --------------------------------------------------------------------- |
| `SUBMINER_BINARY_PATH` | Path to the SubMiner app, if it is not in a standard install location |
| `SUBMINER_APPIMAGE_PATH` | Same, for an AppImage (Linux) |
| `SUBMINER_ROFI_THEME` | Path to a custom rofi theme |
## Logging ## Logging
- Default log level is `info` The default log level is `warn`. Change it for one run with `--log-level`, or permanently with `logging.level` in the config. The app's `--dev` and `--debug` flags turn on developer mode. They do not change the log level.
- `--background` mode defaults to `warn` unless `--log-level` is explicitly set
- `--dev` / `--debug` control app behavior, not logging verbosity — use `--log-level` for that
+135
View File
@@ -0,0 +1,135 @@
import { expect, test } from 'bun:test';
import { readdirSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const docsSiteDir = fileURLToPath(new URL('.', import.meta.url));
// Mirrors VitePress' heading slugifier (vitepress/dist/node, `rControl` + `rSpecial`).
// Note that a *run* of special characters collapses to a single `-`, so
// "KDE Plasma & other" becomes "kde-plasma-other", not "kde-plasma--other".
const rControl = new RegExp('[\\u0000-\\u001f]', 'g');
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g;
function slugify(heading: string): string {
return heading
.replace(rControl, '')
.replace(rSpecial, '-')
.replace(/-{2,}/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/^(\d)/, '_$1')
.toLowerCase();
}
const EXCLUDED_PAGES = new Set(['README.md']);
const UNLISTED_ROUTES = new Set(['/demos']);
const PUBLIC_PREFIXES = ['/assets/', '/screenshots/', '/config.example.jsonc', '/favicon'];
function loadPages(): Map<string, string> {
const pages = new Map<string, string>();
for (const file of readdirSync(docsSiteDir)) {
if (!file.endsWith('.md') || EXCLUDED_PAGES.has(file)) continue;
const route = `/${file.replace(/\.md$/, '')}`;
pages.set(route, readFileSync(`${docsSiteDir}${file}`, 'utf8'));
}
return pages;
}
function anchorsFor(contents: string): Set<string> {
const anchors = new Set<string>();
for (const match of contents.matchAll(/^#{1,6}\s+(.+?)\s*$/gm)) {
let heading = match[1]!;
const explicitId = heading.match(/\{#([^}]+)\}\s*$/);
if (explicitId) {
anchors.add(explicitId[1]!);
heading = heading.replace(/\{#[^}]+\}\s*$/, '');
}
anchors.add(slugify(heading.replace(/`/g, '')));
}
return anchors;
}
function resolveRoute(target: string, fromRoute: string): string {
if (target === '') return fromRoute;
if (target.startsWith('./')) return `/${target.slice(2).replace(/\.md$/, '')}`;
const normalized = target.replace(/\.md$/, '').replace(/\/$/, '');
return normalized === '' ? '/index' : normalized;
}
const pages = loadPages();
const anchors = new Map([...pages].map(([route, body]) => [route, anchorsFor(body)]));
test('every internal docs link resolves to an existing page', () => {
const broken: string[] = [];
for (const [route, body] of pages) {
for (const match of body.matchAll(/\]\((\/[^)\s]*|\.\/[^)\s]*)\)/g)) {
const link = match[1]!;
const target = link.split('#')[0]!;
if (PUBLIC_PREFIXES.some((prefix) => target.startsWith(prefix))) continue;
const resolved = resolveRoute(target, route);
if (resolved !== '/index' && !pages.has(resolved)) {
broken.push(`${route.slice(1)}.md -> ${link}`);
}
}
}
expect(broken).toEqual([]);
});
test('every internal docs anchor matches a real heading slug', () => {
const broken: string[] = [];
for (const [route, body] of pages) {
for (const match of body.matchAll(/\]\((\/[^)\s]*|\.\/[^)\s]*|#[^)\s]*)\)/g)) {
const link = match[1]!;
const hashIndex = link.indexOf('#');
if (hashIndex < 0) continue;
const target = link.slice(0, hashIndex);
const anchor = link.slice(hashIndex + 1);
if (PUBLIC_PREFIXES.some((prefix) => target.startsWith(prefix))) continue;
const resolved = resolveRoute(target, route);
const pageAnchors = anchors.get(resolved);
if (!pageAnchors || pageAnchors.has(anchor)) continue;
broken.push(`${route.slice(1)}.md -> ${link}`);
}
}
expect(broken).toEqual([]);
});
test('slugify matches the VitePress cases these docs actually rely on', () => {
// Regression guards for the anchors that were previously wrong.
expect(slugify('N+1 Word Highlighting')).toBe('n-1-word-highlighting');
expect(slugify('KDE Plasma & other Wayland compositors')).toBe(
'kde-plasma-other-wayland-compositors',
);
expect(slugify('Proxy Mode Setup (Yomitan / Texthooker)')).toBe(
'proxy-mode-setup-yomitan-texthooker',
);
expect(slugify('Kiku/Lapis Integration')).toBe('kiku-lapis-integration');
expect(slugify('Secondary Subtitles')).toBe('secondary-subtitles');
expect(slugify('2. Install SubMiner')).toBe('_2-install-subminer');
});
test('every docs page is reachable from the sidebar unless explicitly unlisted', async () => {
const { default: config } = await import('./.vitepress/config');
const sidebar = config.themeConfig?.sidebar as Array<{
items?: Array<{ text: string; link?: string }>;
}>;
const linked = new Set<string>();
for (const group of sidebar) {
for (const item of group.items ?? []) {
if (item.link) linked.add(item.link === '/' ? '/index' : item.link);
}
}
const orphans = [...pages.keys()].filter(
(route) => !linked.has(route) && !UNLISTED_ROUTES.has(route),
);
expect(orphans).toEqual([]);
});
+53 -184
View File
@@ -1,218 +1,87 @@
# Mining Workflow # Mining workflow
This guide walks through the sentence mining loop — from watching a video to creating Anki cards with audio, screenshots, and context. SubMiner turns lines from the video you are watching into Anki cards. You look up a word on the overlay, add it with Yomitan, and SubMiner fills in the sentence, an audio clip, and a screenshot. For Anki setup, field mapping, and media options, see [Anki integration](/anki-integration).
## Overview ## Look up a word
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. 1. Hover the subtitle line on the overlay. Each word is its own hover target.
2. Press your Yomitan scan key or modifier (whatever your Yomitan profile uses, for example `Shift`).
3. The Yomitan popup opens for that word.
## Subtitle Delivery Path (Startup + Runtime) Playback pauses while you hover the subtitle and while the Yomitan popup is open. Turn this off with `subtitleStyle.autoPauseVideoOnHover` and `subtitleStyle.autoPauseVideoOnYomitanPopup`.
SubMiner prioritizes subtitle responsiveness over heavy initialization: ## Add a word card
1. The first subtitle render is **plain text first** (no tokenization wait). Click the add button in the Yomitan popup. SubMiner sees the new note and fills it in:
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. | Field | Content |
| -------- | ------------------------------------------------------ |
| Sentence | The current subtitle line, with the mined word in bold |
| Audio | A clip cut from the video using the subtitle's timing |
| Image | A screenshot, or an animated AVIF clip of the line |
| MiscInfo | Source file name and timestamp |
## Overlay Model Which note fields receive each item is set in [`ankiConnect.fields`](/anki-integration#field-mapping). With the default proxy mode the card is filled as soon as Yomitan adds it. If you disable the proxy, SubMiner polls Anki and fills the card a few seconds later.
SubMiner uses one overlay window with modal surfaces. ## Update the last card by hand
### Primary Subtitle Layer Use this when auto-update is off, or when the line you want is not the one on screen.
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: 1. Add the word with Yomitan.
2. Press `Ctrl/Cmd+C` to copy the current line. To combine lines, press `Ctrl/Cmd+Shift+C`, then a digit `1` to `9` for how many recent lines to include.
3. Press `Ctrl/Cmd+V`. SubMiner writes the clipboard text into the last-added card's sentence field and adds fresh audio and an image.
- Word-level hover targets for Yomitan lookup A manual update always replaces the sentence audio, even when `ankiConnect.behavior.overwriteAudio` is `false`.
- 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). ## Mine a sentence card
### Secondary Subtitle Bar Press `Ctrl/Cmd+S` to create a sentence card from the current line without a Yomitan lookup. Press `Ctrl/Cmd+Shift+S`, then a digit `1` to `9`, to combine several recent lines into one card. The digit prompt closes after `shortcuts.multiCopyTimeoutMs`.
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. Sentence cards use the note type named in `ankiConnect.isLapis.sentenceCardModel` and write to its `Sentence` and `SentenceAudio` fields. That note type must exist in Anki. See [sentence cards](/anki-integration#sentence-cards-lapis).
It is controlled by `secondarySub` configuration and shares lifecycle with the main overlay window. ## Mark an audio card
### Modal Surfaces After adding a word, press `Ctrl/Cmd+Shift+A`. SubMiner sets the Lapis/Kiku `IsAudioCard` flag on the last-added card and fills `Sentence`, `SentenceAudio`, the image, and MiscInfo.
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window. ## Merge repeated words
## Looking Up Words If you mine a word you already have a card for, SubMiner can merge the new sentence, audio, and image into the existing card instead of keeping a duplicate. This needs the Kiku or Senren note type with field grouping turned on. In manual mode a dialog shows both cards and lets you pick which one to keep. See [field grouping](/anki-integration#field-grouping-kiku-senren).
1. Hover over the subtitle area — the overlay activates pointer events. ## Subtitle display
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 The overlay has a primary subtitle bar (the Japanese line you mine from) and a secondary bar for a translation track. Each bar is hidden, visible, or shown only on hover.
With a gamepad connected and keyboard-only mode enabled, the full mining loop works without a mouse or keyboard: | Shortcut | Action |
| ------------------ | ---------------------------------- |
| `V` | Cycle primary bar mode |
| `Ctrl/Cmd+Shift+V` | Cycle secondary bar mode |
| `Shift+J` | Cycle the secondary subtitle track |
| Right-click | Pause or resume |
| Right-click + drag | Move the subtitles |
1. **Navigate** — push the left stick left/right to move the token highlight across subtitle words. Set the starting modes with `subtitleStyle.primaryDefaultMode` and `secondarySub.defaultMode`. The full list of keys is on [Keyboard shortcuts](/shortcuts).
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.
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. ## 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. With a gamepad and keyboard-only mode on, you can mine without a mouse: move across words with the left stick, look up with `A`, mine with `X`, and close the popup with `B`. The keyboard keeps working alongside it. See [controller support](/usage#controller-support).
## Creating Anki Cards ## Fix subtitle timing (subsync)
There are three ways to create cards, depending on your workflow. If the subtitles are out of sync, press `Ctrl+Alt+S` to open the subsync dialog. It uses [alass](https://github.com/kaegi/alass) or [ffsubsync](https://github.com/smacke/ffsubsync), which you install separately.
### 1. Auto-Update from Yomitan 1. Pick the engine.
2. For alass, pick a reference: a subtitle track with correct timing (the secondary track by default) or the video file.
3. Pick the track to retime (the active primary track by default).
4. Run it. SubMiner loads the retimed subtitle back into the same slot.
This is the most common flow. Yomitan creates a card in Anki, and SubMiner enriches it automatically. For remote streams such as Jellyfin, only alass with a subtitle reference is available, because ffsubsync and the video reference need the local file. If the tools are not found, see [Troubleshooting](/troubleshooting#subtitle-sync-subsync).
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).
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).
- **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.
Configure which fields to fill in `ankiConnect.fields`. See [Anki Integration](/anki-integration) for details.
### 2. Manual Update from Clipboard
If you prefer a hands-on approach (animecards-style), you can copy the current subtitle to the clipboard and then paste it onto the last-added Anki card:
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.
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.
This is useful when auto-update is disabled or when you want explicit control over which subtitle line gets attached to the card.
| Shortcut | Action | Config key |
| -------------------------- | ------------------------------- | --------------------------------------- |
| `Ctrl/Cmd+C` | Copy current subtitle | `shortcuts.copySubtitle` |
| `Ctrl/Cmd+Shift+C` + digit | Copy multiple recent lines | `shortcuts.copySubtitleMultiple` |
| `Ctrl/Cmd+V` | Update last card from clipboard | `shortcuts.updateLastCardFromClipboard` |
### 3. Mine Sentence (Hotkey)
Create a standalone sentence card without going through Yomitan:
- **Mine current sentence**: `Ctrl/Cmd+S` (configurable via `shortcuts.mineSentence`)
- **Mine multiple lines**: `Ctrl/Cmd+Shift+S` followed by a digit 1–9 to select how many recent subtitle lines to combine.
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.
:::
### 4. Mark as Audio Card
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.
:::
## 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)
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.
See [Anki Integration — Field Grouping](/anki-integration#field-grouping-kiku) for configuration options, merge behavior, and modal keyboard shortcuts.
## Jimaku Subtitle Search
SubMiner integrates with [Jimaku](https://jimaku.cc) to search and download subtitle files for anime directly from the overlay.
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.
Requires an internet connection. An API key (`jimaku.apiKey`) is optional but recommended for higher rate limits.
## Texthooker ## Texthooker
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 serve a texthooker page at `http://127.0.0.1:5174` that shows each subtitle line as it arrives, so you can do lookups in a browser instead of on the overlay. Start it with `texthooker.launchAtStartup`, the `--texthooker` flag, or the mpv plugin's `texthooker_enabled` option. Change the port with the plugin's `texthooker_port` option. To build your own client, see the [WebSocket / texthooker API](/websocket-texthooker-api).
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. ## Related features
If you want to build your own browser client, websocket consumer, or automation relay, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api). - [Jimaku](/jimaku-integration): search and download subtitle files from the overlay (`Ctrl+Shift+J`).
- [Subtitle annotations](/subtitle-annotations): highlight known words, N+1 targets, JLPT levels, and frequency.
## Subtitle Sync (Subsync) - [Immersion tracking](/immersion-tracking): log watch time and cards mined, and view them in the stats dashboard.
If your subtitle file is out of sync with the audio, SubMiner can resynchronize it using [alass](https://github.com/kaegi/alass) or [ffsubsync](https://github.com/smacke/ffsubsync).
1. Open the subsync modal from the overlay.
2. Select the sync engine (alass or ffsubsync).
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.
## N+1 Word Highlighting
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.
See [Subtitle Annotations — N+1](/subtitle-annotations#n1-word-highlighting) for configuration options and color settings.
## Immersion Tracking
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.
Enable it in your config:
```jsonc
"immersionTracking": {
"enabled": true,
"dbPath": "" // leave empty to use the default location
}
```
Open the dashboard in the overlay with `stats.toggleKey` (default: `` ` ``), launch it in a browser with `subminer stats`, keep a dedicated background server alive with `subminer stats -b`, stop that background server with `subminer stats -s`, or visit `http://127.0.0.1:6969` directly if the local stats server is already running. The dashboard covers overview totals, anime progress, session detail, and vocabulary drill-down from the same local immersion database.
See [Immersion Tracking](/immersion-tracking) for dashboard details, schema, and retention settings.
Next: [Anki Integration](/anki-integration) — field mapping, media generation, and card enrichment configuration.
+94 -202
View File
@@ -1,190 +1,112 @@
# MPV Plugin # MPV plugin
The SubMiner mpv plugin (`subminer/main.lua`) provides in-player keybindings to control the overlay without leaving mpv. SubMiner-managed launches inject the bundled runtime plugin, so users do not need to install it into mpv's global `scripts` directory. The SubMiner mpv plugin is a Lua script that runs inside mpv. It adds in-player keys to start, stop, and toggle the overlay, and it runs your SubMiner shortcuts from inside mpv.
## Runtime Loading ## Setup
Launch mpv through the SubMiner app, the `subminer` launcher, or the packaged Windows SubMiner mpv shortcut. These paths pass mpv a bundled plugin path for that playback session only, leaving regular mpv playback untouched. You usually do not install anything. Every SubMiner-managed launch (the app, the `subminer` launcher, and the Windows SubMiner mpv shortcut) loads the bundled plugin for that session only. Regular mpv playback is not affected.
If setup detects an older global SubMiner plugin in mpv's `scripts` directory, use **Remove legacy mpv plugin** in first-run setup. The global plugin is not needed once runtime loading is available. On Linux, the launcher's copy lives in `$XDG_DATA_HOME/SubMiner/plugin/subminer` (default `~/.local/share/SubMiner/plugin/subminer`), or under `/usr/local/share/SubMiner` or `/usr/share/SubMiner` for system installs. `subminer -u` and the tray updater keep it current.
mpv must have IPC enabled for SubMiner to connect: To use the plugin when mpv is started by another program, load its `main.lua` and enable IPC:
```bash
mpv --script="$HOME/.local/share/SubMiner/plugin/subminer/main.lua" \
--input-ipc-server=/tmp/subminer-socket video.mkv
```
To enable IPC for every mpv session, add it to `mpv.conf`:
```ini ```ini
# ~/.config/mpv/mpv.conf
input-ipc-server=/tmp/subminer-socket input-ipc-server=/tmp/subminer-socket
``` ```
On Windows, use a named pipe instead: On Windows, use a named pipe:
```ini ```ini
input-ipc-server=\\.\pipe\subminer-socket input-ipc-server=\\.\pipe\subminer-socket
``` ```
If first-run setup finds an old SubMiner plugin in mpv's global `scripts` directory, click **Remove legacy mpv plugin**. It is no longer needed.
## Keybindings ## Keybindings
All keybindings use a `y` chord prefix — press `y`, then the second key: Most plugin keys are chords: press `y`, then the second key.
| Chord | Action | | Key | Action |
| ----- | ---------------------- | | ----- | -------------------------------------- |
| `y-y` | Open menu | | `y-y` | Open the SubMiner menu |
| `y-s` | Start overlay | | `y-s` | Start the overlay |
| `y-S` | Stop overlay | | `y-S` | Stop the overlay |
| `y-t` | Toggle visible overlay | | `y-t` | Toggle the visible overlay |
| `v` | Toggle primary subtitle bar visibility | | `y-o` | Open the settings window |
| `y-o` | Open settings window | | `y-r` | Restart the overlay |
| `y-r` | Restart overlay |
| `y-c` | Check status | | `y-c` | Check status |
| `y-k` | Skip intro (AniSkip) | | `y-h` | Open the session help modal |
| `v` | Toggle SubMiner's primary subtitle bar |
The bare `v` binding is a forced mpv binding. It overrides mpv's default primary subtitle visibility toggle and routes the action to SubMiner's primary subtitle bar instead. `v` replaces mpv's own subtitle visibility toggle.
## Menu The skip-intro key (`TAB` by default) comes from the SubMiner app, not the plugin. See [AniSkip integration](/aniskip-integration).
Press `y-y` to open an interactive menu in mpv's OSD: The `y-y` menu lists Start overlay, Stop overlay, Toggle overlay, Open options, Restart overlay, Check status, and Stats. Press an item's number to run it. Stats only reminds you to press `` ` `` in the overlay.
## Your shortcuts in mpv
Everything you set under [`shortcuts`](/shortcuts), your custom `keybindings`, and the stats keys also work while mpv has focus. SubMiner writes them to `session-bindings.json` in its config directory, and the plugin registers them as mpv keys. When you change a shortcut, mpv picks it up immediately.
`CommandOrControl` becomes `Cmd` on macOS and `Ctrl` elsewhere. Multi-line copy and mine shortcuts wait for a digit key `1` to `9`, and `Esc` cancels. If two shortcuts map to the same key, or a key has no mpv equivalent, SubMiner logs a warning and skips it.
## Script options
The plugin reads `script-opts` with the `subminer-` prefix, for example `--script-opts=subminer-backend=hyprland`. Managed launches set these from your SubMiner config, so edit the config instead. The shipped `plugin/subminer.conf` is empty on purpose, so it never overrides those values.
| Option | Default | SubMiner config key | What it does |
| ---------------------------------------------- | ---------------- | ---------------------------- | --------------------------------------------------------------------- |
| `binary_path` | `""` | `mpv.subminerBinaryPath` | SubMiner binary. Empty uses [auto-detection](#binary-auto-detection). |
| `socket_path` | platform default | `mpv.socketPath` | mpv IPC socket |
| `backend` | `auto` | `mpv.backend` | Window backend: `auto`, `hyprland`, `sway`, `x11`, `macos` |
| `auto_start` | `no` | `mpv.autoStartSubMiner` | Start SubMiner when a file loads |
| `auto_start_visible_overlay` | `no` | `auto_start_overlay` | Show the overlay when auto-starting |
| `auto_start_pause_until_ready` | `yes` | `mpv.pauseUntilOverlayReady` | Keep mpv paused until subtitles are ready |
| `auto_start_pause_until_ready_timeout_seconds` | `30` | | Resume anyway after this many seconds |
| `overlay_loading_osd` | `no` | | Show a loading message while the overlay starts |
| `texthooker_enabled` | `no` | | Start the texthooker with the overlay |
| `texthooker_port` | `5174` | | Texthooker port |
| `osd_messages` | `yes` | | Show plugin status messages in mpv |
| `log_level` | `info` | | Plugin log level |
Without script options, `socket_path` is `/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows. On Windows, the plugin also rewrites `/tmp/subminer-socket` to the named pipe.
The table's defaults are the plugin's own. Managed launches override them from your config; see [Configuration](/configuration#mpv-launcher).
## Binary auto-detection
With `binary_path` empty, the plugin looks in these places:
| Platform | Locations |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Linux | `~/.local/bin/SubMiner.AppImage`, `/opt/SubMiner/SubMiner.AppImage`, `/usr/local/bin/SubMiner` or `subminer`, `/usr/bin/SubMiner` or `subminer` |
| macOS | `/Applications/SubMiner.app`, `~/Applications/SubMiner.app` |
| Windows | A running SubMiner process, the App Paths registry entry, `SubMiner.exe` on `PATH`, then `%LOCALAPPDATA%\Programs\SubMiner`, `C:\Program Files\SubMiner`, `C:\Program Files (x86)\SubMiner`, `C:\SubMiner` |
## Backend detection
With `backend=auto`, the plugin picks the first match:
1. macOS
2. Hyprland (`HYPRLAND_INSTANCE_SIGNATURE` is set)
3. Sway (`SWAYSOCK` is set)
4. X11 (`XDG_SESSION_TYPE=x11` or `DISPLAY` is set)
5. Otherwise X11, with a warning
Native Wayland support covers only Hyprland and Sway. On other Wayland compositors, run both mpv and SubMiner under Xwayland and install `xdotool` and `xwininfo`.
## Script messages
Other mpv scripts, `input.conf`, or the mpv console can control the plugin:
```text ```text
SubMiner:
1. Start overlay
2. Stop overlay
3. Toggle overlay
4. Open options
5. Restart overlay
6. Check status
```
Select an item by pressing its number.
## Configuration
For advanced/manual runtime use, create or edit `~/.config/mpv/script-opts/subminer.conf`:
```ini
# Path to SubMiner binary. Leave empty for auto-detection.
binary_path=
# MPV IPC socket path. Must match input-ipc-server in mpv.conf.
socket_path=/tmp/subminer-socket
# Enable the texthooker WebSocket server.
texthooker_enabled=yes
# Port for the texthooker server.
texthooker_port=5174
# Window manager backend: auto, hyprland, sway, x11, macos, windows.
backend=auto
# Start the overlay automatically when a file is loaded.
# Runs only when mpv input-ipc-server matches socket_path.
auto_start=yes
# Show the visible overlay on auto-start.
# Runs only when mpv input-ipc-server matches socket_path.
auto_start_visible_overlay=yes
# Pause mpv on visible auto-start until SubMiner signals overlay/tokenization readiness.
# Requires auto_start=yes and auto_start_visible_overlay=yes.
auto_start_pause_until_ready=yes
# Show OSD messages for overlay status changes.
osd_messages=yes
# Logging level: debug, info, warn, error.
log_level=info
# Enable AniSkip intro detection/markers.
aniskip_enabled=yes
# Optional title override (launcher fills from guessit when available).
aniskip_title=
# Optional season override (launcher fills from guessit when available).
aniskip_season=
# Optional MAL ID override. Leave blank to resolve from media title.
aniskip_mal_id=
# Optional episode override. Leave blank to detect from filename/title.
aniskip_episode=
# Show OSD skip button while inside intro range.
aniskip_show_button=yes
# OSD label + keybinding for intro skip action.
aniskip_button_text=You can skip by pressing %s
aniskip_button_key=y-k
aniskip_button_duration=3
```
### Option Reference
| Option | Default | Values | Description |
| ------------------------------ | ----------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------- |
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
| `texthooker_port` | `5174` | 1–65535 | Texthooker server port |
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
| `auto_start` | `yes` | `yes` / `no` | Auto-start overlay on file load when mpv socket matches `socket_path` |
| `auto_start_visible_overlay` | `yes` | `yes` / `no` | Show visible layer on auto-start when mpv socket matches `socket_path` |
| `auto_start_pause_until_ready` | `yes` | `yes` / `no` | Pause mpv on visible auto-start; resume when SubMiner signals tokenization-ready |
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection |
| `aniskip_title` | `""` | string | Override title used for lookup |
| `aniskip_season` | `""` | numeric season | Optional season hint |
| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id |
| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed |
| `aniskip_payload` | `""` | JSON / base64-encoded JSON | Optional pre-fetched AniSkip payload for this media. When set, plugin skips network lookup |
| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt |
| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) |
| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) |
| `aniskip_button_duration` | `3` | float seconds | OSD hint duration |
## Binary Auto-Detection
When `binary_path` is empty, the plugin searches platform-specific locations:
**Linux:**
1. `~/.local/bin/SubMiner.AppImage`
2. `/opt/SubMiner/SubMiner.AppImage`
3. `/usr/local/bin/SubMiner`
4. `/usr/bin/SubMiner`
**macOS:**
1. `/Applications/SubMiner.app/Contents/MacOS/SubMiner`
2. `~/Applications/SubMiner.app/Contents/MacOS/SubMiner`
**Windows:**
1. `C:\Program Files\SubMiner\SubMiner.exe`
2. `C:\Program Files (x86)\SubMiner\SubMiner.exe`
3. `C:\SubMiner\SubMiner.exe`
Packaged Windows plugin installs also rewrite `socket_path` to `\\.\pipe\subminer-socket` automatically.
## Backend Detection
When `backend=auto`, the plugin detects the window manager:
1. **macOS** — detected via platform or `OSTYPE`.
2. **Hyprland** — detected via `HYPRLAND_INSTANCE_SIGNATURE`.
3. **Sway** — detected via `SWAYSOCK`.
4. **X11** — detected via `XDG_SESSION_TYPE=x11` or `DISPLAY`.
5. **Fallback** — defaults to X11 with a warning.
::: tip Wayland is compositor-specific
Native Wayland support is only available for Hyprland and Sway. If you use a different Wayland compositor, auto-detection will fall back to X11 — both mpv and SubMiner must be running under Xwayland, and `xdotool` and `xwininfo` must be installed.
:::
## Script Messages
The plugin can be controlled from other mpv scripts or the mpv command line using script messages:
```
script-message subminer-start script-message subminer-start
script-message subminer-stop script-message subminer-stop
script-message subminer-toggle script-message subminer-toggle
@@ -192,51 +114,21 @@ script-message subminer-menu
script-message subminer-options script-message subminer-options
script-message subminer-restart script-message subminer-restart
script-message subminer-status script-message subminer-status
script-message subminer-autoplay-ready
script-message subminer-aniskip-refresh
script-message subminer-skip-intro
``` ```
The `subminer-start` message accepts overrides: `subminer-start` accepts overrides:
``` ```text
script-message subminer-start backend=hyprland socket=/custom/path texthooker=no log-level=debug script-message subminer-start backend=hyprland socket=/custom/path texthooker=no log-level=debug
``` ```
`log-level` here controls only logging verbosity passed to SubMiner. `log-level` sets SubMiner's log verbosity. Do not use `--debug` for this; it turns on the app's dev mode.
`--debug` is a separate app/dev-mode flag in the main CLI and should not be used here for logging.
## AniSkip Intro Skip The plugin also handles messages the SubMiner app sends it (`subminer-autoplay-ready`, `subminer-autoplay-hold`, `subminer-visible-overlay-shown`, `subminer-visible-overlay-hidden`, `subminer-managed-subtitles-loading`, `subminer-overlay-loading-ready`, `subminer-reload-session-bindings`). You do not need to send these yourself. The AniSkip messages are listed on the [AniSkip page](/aniskip-integration#triggering-from-mpv).
- AniSkip lookups are gated. The plugin only runs lookup when: ## Auto-start behavior
- SubMiner launcher metadata is present, or
- SubMiner app process is already running, or
- You explicitly call `script-message subminer-aniskip-refresh`.
- Lookups are asynchronous (no blocking `ps`/`curl` on `file-loaded`).
- MAL/title resolution is cached for the current mpv session.
- When launched via `subminer`, launcher can pass `aniskip_payload` (pre-fetched AniSkip `skip-times` payload) and the plugin applies it directly without making API calls.
- If the payload is absent or invalid, lookup falls back to title/MAL-based async fetch.
- Install `guessit` for best detection quality (`python3 -m pip install --user guessit`).
- If OP interval exists, plugin adds `AniSkip Intro Start` and `AniSkip Intro End` chapters.
- At intro start, plugin shows an OSD hint for the first 3 seconds (`You can skip by pressing y-k` by default).
- Use `script-message subminer-aniskip-refresh` after changing media metadata/options to retry lookup.
## Lifecycle - With `auto_start=yes`, the plugin starts SubMiner on each file load. Repeated loads while SubMiner is running do not start it again.
- With `auto_start_visible_overlay=yes` and `auto_start_pause_until_ready=yes`, mpv stays paused until SubMiner reports that subtitles are ready, or until the timeout passes.
- **File loaded**: If `auto_start=yes`, the plugin starts the overlay, then defers AniSkip lookup until after startup delay. - With `texthooker_enabled=yes`, the texthooker starts with the overlay.
- **Auto-start pause gate**: If `auto_start_visible_overlay=yes` and `auto_start_pause_until_ready=yes`, launcher starts mpv paused and the plugin resumes playback after SubMiner reports tokenization-ready (with timeout fallback). - When mpv quits, SubMiner sees the closed socket and shuts down its overlay.
- **Duplicate auto-start events**: Repeated `file-loaded` hooks while overlay is already running are ignored for auto-start triggers (prevents duplicate start attempts).
- **MPV shutdown**: The plugin sends a stop command to gracefully shut down both the overlay and the texthooker server.
- **Texthooker**: Starts as a separate subprocess before the overlay to ensure the app lock is acquired first.
## Using with the `subminer` Wrapper
The `subminer` wrapper script handles mpv launch, socket setup, and overlay lifecycle automatically. You do not need the plugin if you always use the wrapper.
The plugin is useful when you:
- Launch mpv from other tools (file managers, media centers).
- Want on-demand overlay control without the wrapper.
- Use mpv's built-in file browser or playlist features.
You can install both — the plugin provides chord keybindings for convenience, while the wrapper handles the full lifecycle.
+1 -2
View File
@@ -8,13 +8,12 @@
"docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev --host 0.0.0.0 --port 5173 --strictPort", "docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev --host 0.0.0.0 --port 5173 --strictPort",
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build", "docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build",
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview --host 0.0.0.0 --port 4173 --strictPort", "docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview --host 0.0.0.0 --port 4173 --strictPort",
"test": "bun test plausible.test.ts index.assets.test.ts docs-sync.test.ts seo.test.ts" "test": "bun test plausible.test.ts index.assets.test.ts archive-function.test.ts docs-sync.test.ts links.test.ts seo.test.ts .vitepress/theme/status-line.test.ts ../scripts/docs-versioning.test.ts"
}, },
"dependencies": { "dependencies": {
"@catppuccin/vitepress": "^0.1.2", "@catppuccin/vitepress": "^0.1.2",
"@fontsource/jetbrains-mono": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/manrope": "^5.2.8", "@fontsource/manrope": "^5.2.8",
"@plausible-analytics/tracker": "^0.4.4",
"mermaid": "^11.12.3" "mermaid": "^11.12.3"
}, },
"devDependencies": { "devDependencies": {
+41 -16
View File
@@ -3,25 +3,50 @@ import { readFileSync } from 'node:fs';
const docsConfigPath = new URL('./.vitepress/config.ts', import.meta.url); const docsConfigPath = new URL('./.vitepress/config.ts', import.meta.url);
const docsThemePath = new URL('./.vitepress/theme/index.ts', import.meta.url); const docsThemePath = new URL('./.vitepress/theme/index.ts', import.meta.url);
const docsPackagePath = new URL('./package.json', import.meta.url);
const versionedBuildPath = new URL('../scripts/build-versioned-docs.ts', import.meta.url);
const docsConfigContents = readFileSync(docsConfigPath, 'utf8'); const docsConfigContents = readFileSync(docsConfigPath, 'utf8');
const docsThemeContents = readFileSync(docsThemePath, 'utf8'); const docsThemeContents = readFileSync(docsThemePath, 'utf8');
const docsPackageContents = readFileSync(docsPackagePath, 'utf8');
const versionedBuildContents = readFileSync(versionedBuildPath, 'utf8');
test('docs site keeps docs hostname while sending plausible events to subminer.moe via worker.subminer.moe capture endpoint', () => { test('docs site loads the docs.subminer.moe Plausible script through the analytics proxy', () => {
expect(docsConfigContents).toContain("const DOCS_HOSTNAME = 'https://docs.subminer.moe'"); expect(docsConfigContents).toContain("const DOCS_HOSTNAME = 'https://docs.subminer.moe'");
expect(docsConfigContents).toContain('hostname: DOCS_HOSTNAME'); expect(docsConfigContents).toContain(
expect(docsThemeContents).toContain("const PLAUSIBLE_DOMAIN = 'subminer.moe'"); "const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com'",
expect(docsThemeContents).toContain('const PLAUSIBLE_ENABLED_HOSTNAMES = new Set([');
expect(docsThemeContents).toContain("'docs.subminer.moe'");
expect(docsThemeContents).toContain(
"const PLAUSIBLE_ENDPOINT = 'https://worker.subminer.moe/api/capture'",
); );
expect(docsThemeContents).toContain('@plausible-analytics/tracker'); expect(docsConfigContents).toContain(
expect(docsThemeContents).toContain('const { init } = await import'); "const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js'",
expect(docsThemeContents).toContain('!PLAUSIBLE_ENABLED_HOSTNAMES.has(window.location.hostname)'); );
expect(docsThemeContents).toContain('domain: PLAUSIBLE_DOMAIN'); expect(docsConfigContents).toContain(
expect(docsThemeContents).toContain('endpoint: PLAUSIBLE_ENDPOINT'); 'const PLAUSIBLE_ENDPOINT = `${PLAUSIBLE_PROXY_HOSTNAME}/api/event`',
expect(docsThemeContents).toContain('outboundLinks: true'); );
expect(docsThemeContents).toContain('fileDownloads: true'); expect(docsConfigContents).toContain('hostname: DOCS_HOSTNAME');
expect(docsThemeContents).toContain('formSubmissions: true'); expect(docsConfigContents).toContain("rel: 'preconnect'");
expect(docsThemeContents).toContain('captureOnLocalhost: false'); expect(docsConfigContents).toContain('href: PLAUSIBLE_PROXY_HOSTNAME');
expect(docsConfigContents).toContain("async: ''");
expect(docsConfigContents).toContain(
'src: `${PLAUSIBLE_PROXY_HOSTNAME}${PLAUSIBLE_SITE_SCRIPT_PATH}`',
);
expect(docsConfigContents).toContain('plausible.init({ endpoint:');
expect(docsConfigContents).toContain('PLAUSIBLE_ENDPOINT');
expect(docsConfigContents).not.toContain("'data-domain'");
expect(docsConfigContents).not.toContain("'data-api'");
expect(docsThemeContents).not.toContain('@plausible-analytics/tracker');
expect(docsThemeContents).not.toContain('initPlausibleTracker');
expect(docsPackageContents).not.toContain('@plausible-analytics/tracker');
});
test('versioned docs reuse current VitePress internals for old page snapshots', () => {
expect(versionedBuildContents).toContain("cpSync(join(currentDocsSite, '.vitepress')");
expect(versionedBuildContents).toContain('overlayCurrentVitePress(snapshotDocsSite)');
});
test('versioned docs build deduplicates main public assets and removes build workspaces', () => {
expect(versionedBuildContents).toContain('dedupeVersionedPublicAssets({');
expect(versionedBuildContents).toContain('rmSync(buildRoot, { recursive: true, force: true });');
});
test('docs builds exclude the internal README from VitePress page entries', () => {
expect(docsConfigContents).toContain("srcExclude: ['subagents/**', 'README.md']");
}); });
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

+282 -131
View File
@@ -7,10 +7,37 @@
{ {
// ========================================== // ==========================================
// Overlay Auto-Start // Subtitle Selection
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles. // Select primary and secondary mpv subtitle tracks from the overlay.
// Hot-reload: enabling or disabling updates the session shortcut immediately.
// ========================================== // ==========================================
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false "subtitleSelection": {
"enabled": false // Use the SubMiner modal to select primary and secondary subtitle tracks. When enabled, its shortcut overrides mpv subtitle selection. Values: true | false
}, // Select primary and secondary mpv subtitle tracks from the overlay.
// ==========================================
// Japanese Subtitle Generation
// Generate timed Japanese subtitles from local audio using whisper.cpp.
// Configure an existing GGML model path or explicitly download a SubMiner-managed model.
// Hot-reload: settings apply to the next generation or model download.
// ==========================================
"subtitleGeneration": {
"whisperPath": "", // Optional path override for whisper.cpp. Leave empty to find whisper-cli on PATH.
"modelPath": "", // Path to an existing multilingual whisper.cpp GGML model. Leave empty to use a SubMiner-managed model. A configured path always takes precedence.
"managedModel": "small", // Multilingual whisper.cpp model to use when modelPath is empty. Download it explicitly from the generation modal or launcher. Values: tiny | tiny-q5_1 | tiny-q8_0 | base | base-q5_1 | base-q8_0 | small | small-q5_1 | small-q8_0 | medium | medium-q5_0 | medium-q8_0 | large-v1 | large-v2 | large-v2-q5_0 | large-v2-q8_0 | large-v3 | large-v3-q5_0 | large-v3-turbo | large-v3-turbo-q5_0 | large-v3-turbo-q8_0
"threads": 4, // Positive integer CPU thread count for whisper.cpp Japanese transcription.
"ffmpegPath": "", // Optional FFmpeg path override for audio extraction. Leave empty to find ffmpeg on PATH.
"ffprobePath": "", // Optional FFprobe path override for audio tracks and timing. Leave empty to find ffprobe on PATH.
"vadModelPath": "", // Path to a whisper.cpp Silero VAD model. Enables dialogue-focused generation while retaining uncertain audible sections, which may include songs. Leave empty to transcribe the full audio.
"vadPath": "" // Optional speech detector executable override. With vadModelPath configured, leave empty to find whisper-vad-speech-segments or vad-speech-segments on PATH.
}, // Generate timed Japanese subtitles from local audio using whisper.cpp.
// ==========================================
// Visible Overlay Auto-Start
// Show the visible subtitle overlay automatically after managed mpv playback starts SubMiner.
// SubMiner can still auto-start in the background when this is false.
// ==========================================
"auto_start_overlay": true, // Show the visible subtitle overlay automatically when the bundled mpv plugin starts SubMiner. Values: true | false
// ========================================== // ==========================================
// Texthooker Server // Texthooker Server
@@ -18,7 +45,7 @@
// ========================================== // ==========================================
"texthooker": { "texthooker": {
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false "launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
"openBrowser": false // Open browser setting. Values: true | false "openBrowser": false // Open the texthooker page in the default browser when the server starts. Values: true | false
}, // Configure texthooker startup launch and browser opening behavior. }, // Configure texthooker startup launch and browser opening behavior.
// ========================================== // ==========================================
@@ -45,9 +72,16 @@
// Logging // Logging
// Controls logging verbosity. // Controls logging verbosity.
// Set to debug for full runtime diagnostics. // Set to debug for full runtime diagnostics.
// Hot-reload: logging.level and logging.files apply live while SubMiner is running.
// ========================================== // ==========================================
"logging": { "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. }, // Controls logging verbosity.
// ========================================== // ==========================================
@@ -81,7 +115,7 @@
"rightStickPress": 10, // Raw button index used for controller R3 input. "rightStickPress": 10, // Raw button index used for controller R3 input.
"leftTrigger": 6, // Raw button index used for controller L2 input. "leftTrigger": 6, // Raw button index used for controller L2 input.
"rightTrigger": 7 // Raw button index used for controller R2 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": { "bindings": {
"toggleLookup": { "toggleLookup": {
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis "kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
@@ -138,7 +172,8 @@
"axisIndex": 4, // Raw axis index captured for this analog controller action. "axisIndex": 4, // Raw axis index captured for this analog controller action.
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical "dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually. } // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
} // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction. }, // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
"profiles": {} // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
}, // Gamepad support for the visible overlay while keyboard-only mode is active. }, // Gamepad support for the visible overlay while keyboard-only mode is active.
// ========================================== // ==========================================
@@ -152,33 +187,59 @@
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false "mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
"yomitanExtension": true, // Warm up Yomitan extension 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 "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. }, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
// ==========================================
// Updates
// Automatic update check behavior.
// Manual checks from the tray or launcher are always allowed.
// ==========================================
"updates": {
"enabled": true, // Run automatic update checks in the background. Values: true | false
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
"notificationType": "overlay", // 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 // Keyboard Shortcuts
// Overlay keyboard shortcuts. Set a shortcut to null to disable. // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// Hot-reload: shortcut changes apply live and update the session help modal on reopen. // Hot-reload: shortcut changes apply live and update the session help modal on reopen.
// ========================================== // ==========================================
"shortcuts": { "shortcuts": {
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting. "toggleVisibleOverlayGlobal": "Alt+Shift+O", // Global accelerator that toggles overlay visibility from anywhere on the system. Use null to disable.
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting. "copySubtitle": "CommandOrControl+C", // Accelerator that copies the current subtitle line to the clipboard.
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting. "copySubtitleMultiple": "CommandOrControl+Shift+C", // Accelerator that copies consecutive subtitle lines while the multi-copy window stays open.
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting. "updateLastCardFromClipboard": "CommandOrControl+V", // Accelerator that updates the last mined Anki card using the current clipboard contents.
"triggerFieldGrouping": "CommandOrControl+G", // Trigger field grouping setting. "triggerFieldGrouping": "CommandOrControl+G", // Accelerator that triggers Kiku field grouping on duplicate cards.
"triggerSubsync": "Ctrl+Alt+S", // Trigger subsync setting. "triggerSubsync": "Ctrl+Alt+S", // Accelerator that triggers subsync against the active subtitle file.
"mineSentence": "CommandOrControl+S", // Mine sentence setting. "mineSentence": "CommandOrControl+S", // Accelerator that mines the current sentence as a new Anki card.
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Mine sentence multiple setting. "mineSentenceMultiple": "CommandOrControl+Shift+S", // Accelerator that mines consecutive sentences while the multi-mine window stays open.
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes. "multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting. "toggleSecondarySub": "CommandOrControl+Shift+V", // Accelerator that toggles the secondary subtitle bar visibility.
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting. "markAudioCard": "CommandOrControl+Shift+A", // Accelerator that marks the last mined card as an audio card.
"openCharacterDictionary": "CommandOrControl+Alt+A", // Open character dictionary setting. "openCharacterDictionaryManager": "CommandOrControl+D", // Accelerator that opens the character dictionary manager modal.
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting. "openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting. "openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
"openSessionHelp": "CommandOrControl+Slash", // Open session help setting. "openTsukihime": "Ctrl+Shift+T", // Accelerator that opens the TsukiHime subtitle search modal (configured secondary/Japanese primary tabs).
"openControllerSelect": "Alt+C", // Open controller select setting. "openSubtitleSelection": "g-s", // Open subtitle selection when enabled. Use g-s to press g then s. Set null to unbind.
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting. "openSubtitleGeneration": "Ctrl+Shift+G", // Accelerator that opens the standalone Japanese subtitle generation modal.
"toggleSubtitleSidebar": "Backslash" // Toggle subtitle sidebar setting. "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.
"toggleNotificationHistory": "CommandOrControl+N", // Accelerator that toggles the overlay notification history panel.
"appendClipboardVideoToQueue": "CommandOrControl+A" // Accelerator that appends a video path from the clipboard to the mpv playlist.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable. }, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// ========================================== // ==========================================
@@ -259,15 +320,41 @@
] // Command setting. ] // Command setting.
}, },
{ {
"key": "Shift+BracketRight", // Key setting. "key": "Ctrl+Shift+ArrowLeft", // Key setting.
"command": [ "command": [
"__sub-delay-next-line" "sub-step",
-1
] // Command setting. ] // Command setting.
}, },
{ {
"key": "Shift+BracketLeft", // Key setting. "key": "Ctrl+Shift+ArrowRight", // Key setting.
"command": [ "command": [
"__sub-delay-prev-line" "sub-step",
1
] // Command setting.
},
{
"key": "KeyZ", // Key setting.
"command": [
"add",
"sub-delay",
-0.1
] // Command setting.
},
{
"key": "Shift+KeyZ", // Key setting.
"command": [
"add",
"sub-delay",
0.1
] // Command setting.
},
{
"key": "KeyX", // Key setting.
"command": [
"add",
"sub-delay",
0.1
] // Command setting. ] // Command setting.
}, },
{ {
@@ -315,20 +402,20 @@
// Hot-reload: defaultMode updates live while SubMiner is running. // Hot-reload: defaultMode updates live while SubMiner is running.
// ========================================== // ==========================================
"secondarySub": { "secondarySub": {
"secondarySubLanguages": [], // Secondary sub languages setting. "secondarySubLanguages": [], // Language code priority list used to auto-select a secondary subtitle track when available.
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false "autoLoadSecondarySub": false, // Automatically load a matching secondary subtitle when the primary subtitle loads. Values: true | false
"defaultMode": "hover" // Default mode setting. "defaultMode": "hover" // Default visibility mode for the secondary subtitle bar. Values: hidden | visible | hover
}, // Dual subtitle track options. }, // Dual subtitle track options.
// ========================================== // ==========================================
// Auto Subtitle Sync // Subtitle Sync
// Subsync engine and executable paths. // Subsync engine and executable paths.
// Hot-reload: subsync changes apply to the next subtitle sync run.
// ========================================== // ==========================================
"subsync": { "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.
"alass_path": "", // Alass path setting. "ffsubsync_path": "", // Optional absolute path to the ffsubsync binary used by subsync. Leave empty to auto-discover from PATH.
"ffsubsync_path": "", // Ffsubsync path setting. "ffmpeg_path": "", // Optional absolute path to the ffmpeg binary used by subsync. Leave empty to auto-discover from PATH.
"ffmpeg_path": "", // Ffmpeg path setting.
"replace": true // Replace the active subtitle file when sync completes. Values: true | false "replace": true // Replace the active subtitle file when sync completes. Values: true | false
}, // Subsync engine and executable paths. }, // Subsync engine and executable paths.
@@ -337,7 +424,7 @@
// Initial vertical subtitle position from the bottom. // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
"subtitlePosition": { "subtitlePosition": {
"yPercent": 10 // Y percent setting. "yPercent": 10 // Vertical position of the subtitle overlay expressed as a percentage from the bottom of the screen.
}, // Initial vertical subtitle position from the bottom. }, // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
@@ -347,29 +434,39 @@
// ========================================== // ==========================================
"subtitleStyle": { "subtitleStyle": {
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover "primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
"css": {
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"color": "#cad3f5", // Color setting.
"background-color": "transparent", // Background color setting.
"font-size": "35px", // Font size setting.
"font-weight": "600", // Font weight setting.
"font-style": "normal", // Font style setting.
"line-height": "1.35", // Line height setting.
"letter-spacing": "-0.01em", // Letter spacing setting.
"word-spacing": "0", // Word spacing setting.
"font-kerning": "normal", // Font kerning setting.
"text-rendering": "geometricPrecision", // Text rendering setting.
"text-shadow": "-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.
}, // CSS declaration object applied to primary subtitles after normal subtitle style defaults.
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false "enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false "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 "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 "autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv. "primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv. "nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false "nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary. "nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
"fontFamily": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting. "nPlusOneColor": "#c6a0f6", // Color used for the single N+1 target token subtitle highlight.
"fontSize": 35, // Font size setting. "knownWordColor": "#a6da95", // Color used for known-word subtitle highlights.
"fontColor": "#cad3f5", // Font color setting. "knownWordMaturityColors": {
"fontWeight": "600", // Font weight setting. "new": "#ee99a0", // Color for known words whose Anki cards are new (never reviewed), when maturity highlighting is enabled.
"lineHeight": 1.35, // Line height setting. "learning": "#b7bdf8", // Color for known words whose Anki cards are in (re)learning, when maturity highlighting is enabled.
"letterSpacing": "-0.01em", // Letter spacing setting. "young": "#91d7e3", // Color for known words whose Anki cards are in review below the mature threshold, when maturity highlighting is enabled.
"wordSpacing": 0, // Word spacing setting. "mature": "#a6da95" // Color for known words whose Anki cards are at or above the mature interval threshold, when maturity highlighting is enabled.
"fontKerning": "normal", // Font kerning setting. }, // Known word maturity colors setting.
"textRendering": "geometricPrecision", // Text rendering setting.
"textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"fontStyle": "normal", // Font style setting.
"backgroundColor": "transparent", // Background color setting.
"backdropFilter": "blur(6px)", // Backdrop filter setting.
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
"knownWordColor": "#a6da95", // Known word color setting.
"jlptColors": { "jlptColors": {
"N1": "#ed8796", // N1 setting. "N1": "#ed8796", // N1 setting.
"N2": "#f5a97f", // N2 setting. "N2": "#f5a97f", // N2 setting.
@@ -380,7 +477,7 @@
"frequencyDictionary": { "frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false "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. "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 "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 "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`. "singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
@@ -393,19 +490,21 @@
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX). ] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
}, // Frequency dictionary setting. }, // Frequency dictionary setting.
"secondary": { "secondary": {
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting. "css": {
"fontSize": 24, // Font size setting. "font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"fontColor": "#cad3f5", // Font color setting. "color": "#cad3f5", // Color setting.
"lineHeight": 1.35, // Line height setting. "background-color": "transparent", // Background color setting.
"letterSpacing": "-0.01em", // Letter spacing setting. "font-size": "24px", // Font size setting.
"wordSpacing": 0, // Word spacing setting. "font-weight": "600", // Font weight setting.
"fontKerning": "normal", // Font kerning setting. "font-style": "normal", // Font style setting.
"textRendering": "geometricPrecision", // Text rendering setting. "line-height": "1.35", // Line height setting.
"textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting. "letter-spacing": "-0.01em", // Letter spacing setting.
"backgroundColor": "transparent", // Background color setting. "word-spacing": "0", // Word spacing setting.
"backdropFilter": "blur(6px)", // Backdrop filter setting. "font-kerning": "normal", // Font kerning setting.
"fontWeight": "600", // Font weight setting. "text-rendering": "geometricPrecision", // Text rendering setting.
"fontStyle": "normal" // Font style 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. } // Secondary setting.
}, // Primary and secondary subtitle styling. }, // Primary and secondary subtitle styling.
@@ -419,18 +518,20 @@
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false "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 "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. "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 "autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
"maxWidth": 420, // Maximum sidebar width in CSS pixels. "css": {
"opacity": 0.95, // Base opacity applied to the sidebar shell. "font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
"backgroundColor": "rgba(73, 77, 100, 0.9)", // Background color for the subtitle sidebar shell. "color": "#cad3f5", // Color setting.
"textColor": "#cad3f5", // Default cue text color in the subtitle sidebar. "background-color": "rgba(73, 77, 100, 0.9)", // Background color setting.
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif", // Font family used for subtitle sidebar cue text. "font-size": "16px", // Font size setting.
"fontSize": 16, // Base font size for subtitle sidebar cue text in CSS pixels. "opacity": "0.95", // Opacity setting.
"timestampColor": "#a5adcb", // Timestamp color in the subtitle sidebar. "--subtitle-sidebar-max-width": "420px", // Subtitle sidebar max width setting.
"activeLineColor": "#f5bde6", // Text color for the active subtitle cue. "--subtitle-sidebar-timestamp-color": "#a5adcb", // Subtitle sidebar timestamp color setting.
"activeLineBackgroundColor": "rgba(138, 173, 244, 0.22)", // Background color for the active subtitle cue. "--subtitle-sidebar-active-line-color": "#f5bde6", // Subtitle sidebar active line color setting.
"hoverLineBackgroundColor": "rgba(54, 58, 79, 0.84)" // Background color for hovered subtitle cues. "--subtitle-sidebar-active-background-color": "rgba(138, 173, 244, 0.22)", // Subtitle sidebar active background color setting.
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)" // Subtitle sidebar hover background color setting.
} // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key. }, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
// ========================================== // ==========================================
@@ -441,22 +542,22 @@
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false "enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider. "apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key. "apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
"model": "openai/gpt-4o-mini", // Model setting. "model": "openai/gpt-4o-mini", // Default model identifier requested from the shared AI provider.
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider. "baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting. "systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // Default system prompt sent with shared AI provider requests.
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests. "requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing. }, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
// ========================================== // ==========================================
// AnkiConnect Integration // AnkiConnect Integration
// Automatic Anki updates and media generation options. // Automatic Anki updates and media generation options.
// Hot-reload: ankiConnect.ai.enabled updates live while SubMiner is running. // Hot-reload: ankiConnect.ai.enabled, media.normalizeAudio/mirrorMpvVolume/reviewTiming, knownWords, nPlusOne, fields.word/audio/wordAudio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, isKiku.fieldGrouping, isSenren.fieldGrouping, and lapisKiku.wordCardKind update live while SubMiner is running.
// Shared AI provider transport settings are read from top-level ai and typically require restart. // Shared AI provider transport settings are read from top-level ai and typically require restart.
// Most other AnkiConnect settings still require restart. // Most other AnkiConnect settings still require restart.
// ========================================== // ==========================================
"ankiConnect": { "ankiConnect": {
"enabled": true, // Enable AnkiConnect integration. Values: true | false "enabled": true, // Enable AnkiConnect integration. Values: true | false
"url": "http://127.0.0.1:8765", // Url setting. "url": "http://127.0.0.1:8765", // Base URL of the AnkiConnect HTTP server.
"pollingRate": 3000, // Polling interval in milliseconds. "pollingRate": 3000, // Polling interval in milliseconds.
"proxy": { "proxy": {
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false "enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
@@ -467,13 +568,15 @@
"tags": [ "tags": [
"SubMiner" "SubMiner"
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging. ], // 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": { "fields": {
"word": "Expression", // Card field for the mined word or expression text. "word": "Expression", // Card field for the mined word or expression text.
"audio": "ExpressionAudio", // Audio setting. "audio": "ExpressionAudio", // Card field that receives generated sentence audio.
"image": "Picture", // Image setting. "wordAudio": "ExpressionAudio", // Existing word-audio field read to time the frozen first frame of animated images. This mapping is only used for synchronization.
"sentence": "Sentence", // Sentence setting. "image": "Picture", // Card field that receives the captured screenshot or animated image.
"miscInfo": "MiscInfo", // Misc info setting. "sentence": "Sentence", // Card field that receives the source sentence text.
"translation": "SelectionText" // Translation setting. "miscInfo": "MiscInfo", // Card field that receives the miscellaneous info pattern (see ankiConnect.metadata.pattern).
"translation": "SelectionText" // Card field that receives the current selection or translated text.
}, // Fields setting. }, // Fields setting.
"ai": { "ai": {
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false "enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
@@ -481,72 +584,116 @@
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows. "systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
}, // Ai setting. }, // Ai setting.
"media": { "media": {
"generateAudio": true, // Generate audio setting. Values: true | false "generateAudio": true, // Generate sentence audio for mined cards. Values: true | false
"generateImage": true, // Generate image setting. Values: true | false "generateImage": true, // Generate screenshot or animated image for mined cards. Values: true | false
"imageType": "static", // Image type setting. "imageType": "static", // Image capture type: "static" for a single still frame, "avif" for an animated AVIF. Values: static | avif
"imageFormat": "jpg", // Image format setting. "imageFormat": "jpg", // Encoding format used when imageType is "static". Values: jpg | png | webp
"imageQuality": 92, // Image quality setting. "imageQuality": 92, // Quality (0-100) used for lossy static image encoders.
"animatedFps": 10, // Animated fps setting. "imageMaxWidth": 0, // Maximum width for static images, in pixels. Set to 0 to preserve the source resolution.
"animatedMaxWidth": 640, // Animated max width setting. "imageMaxHeight": 0, // Maximum height for static images, in pixels. Set to 0 to preserve the source resolution.
"animatedCrf": 35, // Animated crf setting. "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 "syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
"audioPadding": 0.5, // Audio padding setting. "normalizeAudio": true, // Normalize generated sentence audio loudness during media extraction. Changes apply live. Values: true | false
"fallbackDuration": 3, // Fallback duration setting. "mirrorMpvVolume": true, // Apply mpv's current software volume curve to generated sentence audio. Changes apply live. Values: true | false
"maxMediaDuration": 30 // Max media duration setting. "reviewTiming": false, // Review and preview subtitle media timing before SubMiner creates or enriches a mined card. Values: true | false
"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. 0 disables the cap.
}, // Media setting. }, // Media setting.
"knownWords": { "knownWords": {
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false "highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
"maturityEnabled": false, // Color known-word highlights by Anki card maturity (new, learning, young, mature) instead of a single color. Requires known-word highlighting. Values: true | false
"matureThresholdDays": 21, // Card interval in days at which a known word counts as mature (Anki convention: 21).
"refreshMinutes": 1440, // Minutes between known-word cache refreshes. "refreshMinutes": 1440, // Minutes between known-word cache refreshes.
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false "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 "matchMode": "headword", // Known-word matching strategy for subtitle annotations. Cache matches always receive known-word highlighting even when POS filters suppress other annotation types. Values: headword | surface
"decks": {}, // Decks and fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word", "Word Reading"] }. "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"] }. Reading fields (Reading, Word Reading, ExpressionReading) are always probed so cached words match only in the reading their note teaches; words from notes without readings match in any reading.
"color": "#a6da95" // Color used for known-word highlights.
}, // Known words setting. }, // Known words setting.
"behavior": { "behavior": {
"overwriteAudio": true, // Overwrite audio setting. Values: true | false "overwriteAudio": true, // When updating an existing card, overwrite the audio field instead of skipping it. Values: true | false
"overwriteImage": true, // Overwrite image setting. Values: true | false "overwriteImage": true, // When updating an existing card, overwrite the image field instead of skipping it. Values: true | false
"mediaInsertMode": "append", // Media insert mode setting. "mediaInsertMode": "append", // Whether new media is appended after or prepended before existing field contents on update. Values: append | prepend
"highlightWord": true, // Highlight word setting. Values: true | false "highlightWord": true, // Bold the mined word inside the sentence field on the saved Anki card. Values: true | false
"notificationType": "osd", // Notification type setting. "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 "autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
}, // Behavior setting. }, // Behavior setting.
"nPlusOne": { "nPlusOne": {
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3). "enabled": false, // Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Requires known-word cache data. Values: true | false
"nPlusOne": "#c6a0f6" // Color used for the single N+1 target token highlight. "minSentenceWords": 3 // Minimum sentence word count required for N+1 targeting (default: 3).
}, // N plus one setting. }, // N plus one setting.
"metadata": { "metadata": {
"pattern": "[SubMiner] %f (%t)" // Pattern setting. "pattern": "[SubMiner] %f (%t)" // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
}, // Metadata setting. }, // Metadata setting.
"isLapis": { "isLapis": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enable Lapis-specific mining behaviors and sentence card model targeting. Values: true | false
"sentenceCardModel": "Japanese sentences" // Sentence card model setting. "sentenceCardModel": "Lapis" // Note type name used by Lapis sentence cards.
}, // Is lapis setting. }, // Is lapis setting.
"isKiku": { "isKiku": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enable Kiku-specific mining behaviors (duplicate handling, field grouping). Values: true | false
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled "fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false "deleteDuplicateInAuto": true // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
} // Is kiku setting. }, // Is kiku setting.
"isSenren": {
"enabled": false, // Enable Senren-specific duplicate handling (scene-switching field grouping, including miscInfo grouping). Mutually exclusive with isKiku.enabled. Values: true | false
"fieldGrouping": "auto", // Senren duplicate-card field grouping mode (scene switching). Values: auto | manual | disabled
"deleteDuplicateInAuto": true // When Senren field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
}, // Is senren setting.
"lapisKiku": {
"wordCardKind": "word-and-sentence" // Card-type flag SubMiner marks on Kiku/Lapis word cards. Only one flag is set at a time; the others are cleared. Requires isKiku.enabled or isLapis.enabled. Values: word-and-sentence | click | sentence | audio | none
} // Lapis kiku setting.
}, // Automatic Anki updates and media generation options. }, // Automatic Anki updates and media generation options.
// ========================================== // ==========================================
// Jimaku // Jimaku
// Jimaku API configuration and defaults. // Jimaku API configuration and defaults.
// Hot-reload: Jimaku changes apply to the next Jimaku request.
// ========================================== // ==========================================
"jimaku": { "jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Api base url setting. "apiBaseUrl": "https://jimaku.cc", // Base URL of the Jimaku subtitle search API.
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none "languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
"maxEntryResults": 10 // Maximum Jimaku search results returned. "maxEntryResults": 10 // Maximum Jimaku search results returned.
}, // Jimaku API configuration and defaults. }, // Jimaku API configuration and defaults.
// ==========================================
// TsukiHime
// TsukiHime subtitle search configuration for Japanese primary and configured secondary subtitles. No API key required.
// Hot-reload: TsukiHime changes apply to the next TsukiHime request.
// ==========================================
"tsukihime": {
"apiBaseUrl": "https://api.tsukihime.org/v1", // Base URL of the TsukiHime API (Animetosho successor). No API key required.
"maxSearchResults": 10 // Maximum TsukiHime search results returned.
}, // TsukiHime subtitle search configuration for Japanese primary and configured secondary subtitles. No API key required.
// ==========================================
// TMDB
// TMDB (The Movie Database) metadata for live-action dramas and movies in the stats Library: posters, synopses, and grouping by show.
// Hot-reload: TMDB changes apply to the next TMDB request.
// ==========================================
"tmdb": {
"apiKey": "", // Your own TMDB API key or read access token for live-action posters and synopses in the stats Library. Release builds bundle a project key, so set this only to use your own quota or when running from source (free under Settings > API on themoviedb.org).
"apiKeyCommand": "" // Shell command that prints the TMDB API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
}, // TMDB (The Movie Database) metadata for live-action dramas and movies in the stats Library: posters, synopses, and grouping by show.
// ========================================== // ==========================================
// YouTube Playback Settings // YouTube Playback Settings
// Defaults for managed subtitle language preferences and YouTube subtitle loading. // Defaults for managed subtitle language preferences and YouTube subtitle loading.
// Hot-reload: primarySubLanguages and subtitleSource apply to the next YouTube subtitle load.
// ========================================== // ==========================================
"youtube": { "youtube": {
"primarySubLanguages": [ "primarySubLanguages": [
"ja", "ja",
"jpn" "jpn"
] // Comma-separated primary subtitle language priority for managed subtitle auto-selection. ], // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
"subtitleSource": "youtube", // Where primary YouTube subtitles come from. Whisper transcribes the audio locally using the subtitleGeneration settings. Values: youtube | whisper
"mediaCache": {
"mode": "direct", // How YouTube card audio/images are extracted. Values: direct | background
"maxHeight": 720 // Maximum video height downloaded for the YouTube background media cache. Set to 0 for unlimited.
} // Media cache setting.
}, // Defaults for managed subtitle language preferences and YouTube subtitle loading. }, // Defaults for managed subtitle language preferences and YouTube subtitle loading.
// ========================================== // ==========================================
@@ -559,11 +706,8 @@
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false "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. "accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
"characterDictionary": { "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. "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 character dictionary settings updates. Values: all | active
"profileScope": "all", // Yomitan profile scope for dictionary enable/disable updates. Values: all | active
"collapsibleSections": { "collapsibleSections": {
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false "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 "characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
@@ -585,14 +729,25 @@
// ========================================== // ==========================================
// MPV Launcher // MPV Launcher
// Optional mpv.exe override for Windows playback entry points. // SubMiner-managed mpv launch and bundled plugin options.
// Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin.
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback. // Set mpv.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. // Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
// ========================================== // ==========================================
"mpv": { "mpv": {
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH. "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 "launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
}, // Optional mpv.exe override for Windows playback entry points. "profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
"backend": "auto", // Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform. Values: auto | hyprland | sway | x11 | macos | windows
"autoStartSubMiner": true, // Start SubMiner in the background when SubMiner-managed mpv loads a file. Values: true | false
"pauseUntilOverlayReady": true, // Pause mpv on visible-overlay auto-start until SubMiner signals subtitle tokenization readiness. Values: true | false
"subminerBinaryPath": "", // Optional SubMiner app binary path passed to the bundled mpv plugin. Leave empty to use the launcher-detected app path.
"aniskipEnabled": true, // Enable AniSkip intro detection, 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.
// ========================================== // ==========================================
// Jellyfin // Jellyfin
@@ -605,14 +760,10 @@
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096). "serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
"recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup. "recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup.
"username": "", // Default Jellyfin username used during CLI login. "username": "", // Default Jellyfin username used during CLI login.
"deviceId": "subminer", // Device id setting.
"clientName": "SubMiner", // Client name setting.
"clientVersion": "0.1.0", // Client version setting.
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing. "defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false "remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. 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 "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 "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. "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 "directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
@@ -635,7 +786,7 @@
// ========================================== // ==========================================
"discordPresence": { "discordPresence": {
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false "enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal". "presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal". Values: default | meme | japanese | minimal
"updateIntervalMs": 3000, // Minimum interval between presence payload updates. "updateIntervalMs": 3000, // Minimum interval between presence payload updates.
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates. "debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
}, // Optional Discord Rich Presence activity card updates for current playback/study session. }, // Optional Discord Rich Presence activity card updates for current playback/study session.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+127
View File
@@ -1,7 +1,10 @@
import { expect, test } from 'bun:test'; import { expect, test } from 'bun:test';
import { fileURLToPath } from 'node:url';
import type { TransformContext } from 'vitepress'; import type { TransformContext } from 'vitepress';
import docsConfig from './.vitepress/config'; import docsConfig from './.vitepress/config';
const docsSiteDir = fileURLToPath(new URL('.', import.meta.url));
function makeTransformContext(page: string): TransformContext { function makeTransformContext(page: string): TransformContext {
return { return {
page, page,
@@ -31,6 +34,111 @@ test('docs pages emit stable self-referential canonical URLs', async () => {
expect(JSON.stringify(rootHead).toLowerCase()).not.toContain('noindex'); expect(JSON.stringify(rootHead).toLowerCase()).not.toContain('noindex');
}); });
test('main docs canonical uses /main/ and emits noindex', async () => {
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
const previousBase = process.env.SUBMINER_DOCS_BASE;
process.env.SUBMINER_DOCS_CHANNEL = 'main';
process.env.SUBMINER_DOCS_BASE = '/main/';
const { default: mainDocsConfig } = await import('./.vitepress/config?main-docs');
const head = await mainDocsConfig.transformHead?.(makeTransformContext('usage.md'));
const rootHead = await mainDocsConfig.transformHead?.(makeTransformContext('index.md'));
expect(head).toContainEqual([
'link',
{ rel: 'canonical', href: 'https://docs.subminer.moe/main/usage' },
]);
expect(rootHead).toContainEqual([
'link',
{ rel: 'canonical', href: 'https://docs.subminer.moe/main/' },
]);
expect(head).toContainEqual(['meta', { name: 'robots', content: 'noindex,follow' }]);
expect(mainDocsConfig.sitemap).toBeUndefined();
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
process.env.SUBMINER_DOCS_BASE = previousBase;
});
test.each([
['v0.14.0', '/v/0.14.0/', 'https://docs.subminer.moe/v/0.14.0/usage'],
['v0.12.0', '/v/0.12.0/', 'https://docs.subminer.moe/v/0.12.0/usage'],
])(
'%s archive keeps a self-referential canonical and stays out of the index',
async (version, base, expectedCanonical) => {
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
const previousBase = process.env.SUBMINER_DOCS_BASE;
const previousVersion = process.env.SUBMINER_DOCS_VERSION;
process.env.SUBMINER_DOCS_CHANNEL = 'stable-archive';
process.env.SUBMINER_DOCS_BASE = base;
process.env.SUBMINER_DOCS_VERSION = version;
try {
const { default: archiveConfig } = await import(`./.vitepress/config?archive-${version}`);
const head = await archiveConfig.transformHead?.(makeTransformContext('usage.md'));
expect(head).toContainEqual(['link', { rel: 'canonical', href: expectedCanonical }]);
expect(head).toContainEqual(['meta', { name: 'robots', content: 'noindex,follow' }]);
// A sitemap here would advertise the archive tree we just excluded.
expect(archiveConfig.sitemap).toBeUndefined();
} finally {
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
process.env.SUBMINER_DOCS_BASE = previousBase;
process.env.SUBMINER_DOCS_VERSION = previousVersion;
}
},
);
test('archive nav keeps page links in-version and version links release-independent', async () => {
const previousCwd = process.cwd();
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
const previousBase = process.env.SUBMINER_DOCS_BASE;
const previousVersion = process.env.SUBMINER_DOCS_VERSION;
process.chdir(docsSiteDir);
process.env.SUBMINER_DOCS_CHANNEL = 'stable-archive';
process.env.SUBMINER_DOCS_BASE = '/v/0.12.0/';
process.env.SUBMINER_DOCS_VERSION = 'v0.12.0';
try {
const { default: archiveConfig } = await import('./.vitepress/config?stable-archive-links');
const nav = archiveConfig.themeConfig?.nav as Array<{
text: string;
link?: string;
items?: Array<{ text: string; link: string }>;
}>;
const sidebar = archiveConfig.themeConfig?.sidebar as Array<{
text: string;
items?: Array<{ text: string; link: string }>;
}>;
const configurationSidebar = sidebar
.find((item) => item.text === 'Reference')
?.items?.find((item) => item.text === 'Configuration');
expect(nav.find((item) => item.text === 'Configuration')?.link).toBe('/configuration');
expect(configurationSidebar?.link).toBe('/configuration');
// Frozen archives must not embed the release list, or every new tag would
// invalidate them. They point at the root-only /versions page instead.
expect(nav.find((item) => item.text === 'v0.12.0')?.items).toEqual([
{ text: 'Latest stable', link: 'https://docs.subminer.moe/', target: '_self', noIcon: true },
{ text: 'main', link: 'https://docs.subminer.moe/main/', target: '_self', noIcon: true },
{
text: 'All versions',
link: 'https://docs.subminer.moe/versions',
target: '_self',
noIcon: true,
},
]);
expect(archiveConfig.themeConfig?.logo).toEqual({
light: '/assets/SubMiner.png',
dark: '/assets/SubMiner.png',
});
} finally {
process.chdir(previousCwd);
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
process.env.SUBMINER_DOCS_BASE = previousBase;
process.env.SUBMINER_DOCS_VERSION = previousVersion;
}
});
test('docs sitemap excludes duplicate README page from indexable URLs', async () => { test('docs sitemap excludes duplicate README page from indexable URLs', async () => {
const items = [{ url: '' }, { url: 'README' }, { url: 'usage' }]; const items = [{ url: '' }, { url: 'README' }, { url: 'usage' }];
@@ -38,3 +146,22 @@ test('docs sitemap excludes duplicate README page from indexable URLs', async ()
expect(transformedItems?.map((item) => item.url)).toEqual(['', 'usage']); expect(transformedItems?.map((item) => item.url)).toEqual(['', 'usage']);
}); });
test('docs sitemap dates every URL from the tracked checkout', async () => {
const previousRepoDir = process.env.SUBMINER_DOCS_REPO_DIR;
// Production builds render from an untracked snapshot, so the date has to come from
// the real checkout rather than VitePress's own srcDir git lookup.
process.env.SUBMINER_DOCS_REPO_DIR = docsSiteDir;
try {
const { default: sitemapConfig } = await import('./.vitepress/config?sitemap-lastmod');
const items = await sitemapConfig.sitemap?.transformItems?.([{ url: '' }, { url: 'usage' }]);
expect(items).toHaveLength(2);
for (const item of items ?? []) {
expect(item.lastmod).toMatch(/^\d{4}-\d{2}-\d{2}T/);
}
} finally {
process.env.SUBMINER_DOCS_REPO_DIR = previousRepoDir;
}
});
+86 -88
View File
@@ -1,149 +1,147 @@
# Keyboard Shortcuts # Keyboard shortcuts
All shortcuts are configurable in `config.jsonc` under `shortcuts` and `keybindings`. Set any shortcut to `null` to disable it. Every key SubMiner responds to, with its default binding. `Ctrl/Cmd` means `Ctrl` on Windows and Linux and `Cmd` on macOS (`CommandOrControl` in config).
## Global Shortcuts Shortcuts work when the overlay has focus. With the [mpv plugin](/mpv-plugin), `shortcuts.*` and `keybindings` entries also work while mpv has focus. If a key does nothing, click the video once. Set any shortcut to `null` to disable it. Changes to `shortcuts`, `keybindings`, and `subtitleSidebar` apply without a restart.
These work system-wide regardless of which window has focus. ## Global
| Shortcut | Action | Configurable | | Shortcut | Action | Config key |
| ------------- | ---------------------- | -------------------------------------- | | ------------- | ---------------------- | -------------------------------------- |
| `Alt+Shift+O` | Toggle visible overlay | `shortcuts.toggleVisibleOverlayGlobal` | | `Alt+Shift+O` | Toggle visible overlay | `shortcuts.toggleVisibleOverlayGlobal` |
| `Alt+Shift+Y` | Open Yomitan settings | Fixed (not configurable) | | `Alt+Shift+Y` | Open Yomitan settings | Fixed |
::: tip `Alt+Shift+Y` is registered with the OS and works from any app. If another app already uses it, SubMiner cannot take it and you cannot rebind it.
Global shortcuts are registered with the OS. If they conflict with another application, update them in `shortcuts` config and restart SubMiner.
:::
## Mining Shortcuts ## Mining
These work when the overlay window has focus.
| Shortcut | Action | Config key | | Shortcut | Action | Config key |
| ------------------ | ----------------------------------------------- | --------------------------------------- | | ------------------ | --------------------------------------------------- | --------------------------------------- |
| `Ctrl/Cmd+S` | Mine current subtitle as sentence card | `shortcuts.mineSentence` | | `Ctrl/Cmd+S` | Mine current line as a sentence card | `shortcuts.mineSentence` |
| `Ctrl/Cmd+Shift+S` | Mine multiple lines (press 1–9 to select count) | `shortcuts.mineSentenceMultiple` | | `Ctrl/Cmd+Shift+S` | Mine several lines as one sentence card | `shortcuts.mineSentenceMultiple` |
| `Ctrl/Cmd+C` | Copy current subtitle text | `shortcuts.copySubtitle` | | `Ctrl/Cmd+C` | Copy current line | `shortcuts.copySubtitle` |
| `Ctrl/Cmd+Shift+C` | Copy multiple lines (press 1–9 to select count) | `shortcuts.copySubtitleMultiple` | | `Ctrl/Cmd+Shift+C` | Copy several lines | `shortcuts.copySubtitleMultiple` |
| `Ctrl/Cmd+V` | Update last Anki card from clipboard text | `shortcuts.updateLastCardFromClipboard` | | `Ctrl/Cmd+V` | Update last-added card from the clipboard | `shortcuts.updateLastCardFromClipboard` |
| `Ctrl/Cmd+G` | Trigger field grouping (Kiku merge check) | `shortcuts.triggerFieldGrouping` | | `Ctrl/Cmd+G` | Run the field grouping check on the last-added card | `shortcuts.triggerFieldGrouping` |
| `Ctrl/Cmd+Shift+A` | Mark last card as audio card | `shortcuts.markAudioCard` | | `Ctrl/Cmd+Shift+A` | Mark last-added card as an audio card | `shortcuts.markAudioCard` |
The multi-line shortcuts open a digit selector with a 3-second timeout (`shortcuts.multiCopyTimeoutMs`). Press `1`–`9` to select how many recent subtitle lines to combine. After a multi-line shortcut, press `1` to `9` for how many lines to combine, counting back from and including the current line. The prompt closes after `shortcuts.multiCopyTimeoutMs`.
## Overlay Controls When text is selected in the [subtitle sidebar](/subtitle-sidebar), `Ctrl/Cmd+C` copies that selection instead.
These control playback and subtitle display. They require overlay window focus. ## Playback
These are the default `keybindings` entries. Remap or disable them in the `keybindings` array.
| Shortcut | Action | | Shortcut | Action |
| -------------------- | --------------------------------------------------- | | ------------------ | ---------------------------------------- |
| `Space` | Toggle mpv pause | | `Space` | Pause or resume |
| `F` | Toggle fullscreen | | `F` | Toggle fullscreen |
| `V` | Toggle primary subtitle bar visibility |
| `J` | Cycle primary subtitle track | | `J` | Cycle primary subtitle track |
| `Shift+J` | Cycle secondary subtitle track | | `Shift+J` | Cycle secondary subtitle track |
| `Ctrl+Alt+P` | Open playlist browser for current directory + queue |
| `ArrowRight` | Seek forward 5 seconds | | `ArrowRight` | Seek forward 5 seconds |
| `ArrowLeft` | Seek backward 5 seconds | | `ArrowLeft` | Seek back 5 seconds |
| `ArrowUp` | Seek forward 60 seconds | | `ArrowUp` | Seek forward 60 seconds |
| `ArrowDown` | Seek backward 60 seconds | | `ArrowDown` | Seek back 60 seconds |
| `Shift+H` | Jump to previous subtitle | | `Shift+H` | Jump to previous subtitle |
| `Shift+L` | Jump to next subtitle | | `Shift+L` | Jump to next subtitle |
| `Shift+[` | Shift subtitle delay to previous subtitle cue | | `Ctrl+Shift+Left` | Shift subtitle delay to the previous cue |
| `Shift+]` | Shift subtitle delay to next subtitle cue | | `Ctrl+Shift+Right` | Shift subtitle delay to the next cue |
| `Ctrl+Shift+H` | Replay current subtitle (play to end, then pause) | | `Z` | Subtitle delay -100 ms |
| `Ctrl+Shift+L` | Play next subtitle (jump, play to end, then pause) | | `Shift+Z` | Subtitle delay +100 ms |
| `X` | Subtitle delay +100 ms |
| `Ctrl+Shift+H` | Replay current subtitle, then pause |
| `Ctrl+Shift+L` | Play next subtitle, then pause |
| `Ctrl+Alt+P` | Open playlist browser |
| `Ctrl+Alt+C` | Open YouTube subtitle picker |
| `Q` | Quit mpv | | `Q` | Quit mpv |
| `Ctrl+W` | Quit mpv | | `Ctrl+W` | Quit mpv |
| `Right-click` | Toggle pause (outside subtitle area) |
| `Right-click + drag` | Reposition subtitles (on subtitle area) |
| `Ctrl/Cmd+A` | Append clipboard video path to mpv playlist |
These keybindings can be overridden or disabled via the `keybindings` config array. The playlist browser opens a split overlay modal with sibling video files on the left and the live mpv playlist on the right. Built into the overlay, not configurable:
Mouse-hover playback behavior is configured separately from shortcuts: `subtitleStyle.autoPauseVideoOnHover` defaults to `true` (pause on subtitle hover, resume on leave). | Input | Action |
| ------------------------- | -------------------------------------------------- |
| `V` | Cycle primary subtitle bar: hidden, visible, hover |
| Right-click | Pause or resume (outside the subtitle area) |
| Right-click + drag | Move the subtitles |
| Drop files on the overlay | Replace the mpv playlist |
| `Shift` + drop files | Append to the mpv playlist |
## Subtitle & Feature Shortcuts ## Overlay features
| Shortcut | Action | Config key | | Shortcut | Action | Config key |
| ------------------ | -------------------------------------------------------- | ----------------------------------- | | ------------------ | ------------------------------------------------------ | ------------------------------------------ |
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` | | `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle bar: hidden, visible, hover | `shortcuts.toggleSecondarySub` |
| `Ctrl/Cmd+Alt+A` | Open character dictionary AniList selector | `shortcuts.openCharacterDictionary` | | `Ctrl/Cmd+Shift+O` | Open runtime options | `shortcuts.openRuntimeOptions` |
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` | | `Ctrl/Cmd+/` | Open session help | `shortcuts.openSessionHelp` |
| `Ctrl/Cmd+/` | Open session help modal | `shortcuts.openSessionHelp` | | `Ctrl/Cmd+D` | Open character dictionary manager | `shortcuts.openCharacterDictionaryManager` |
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` | | `Ctrl/Cmd+N` | Toggle notification history | `shortcuts.toggleNotificationHistory` |
| `Ctrl+Alt+C` | Open the manual YouTube subtitle picker | `keybindings` | | `Ctrl/Cmd+A` | Append the video path on the clipboard to the playlist | `shortcuts.appendClipboardVideoToQueue` |
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` | | `Ctrl+Shift+J` | Open Jimaku subtitle search | `shortcuts.openJimaku` |
| `Ctrl+Shift+T` | Open TsukiHime subtitle search | `shortcuts.openTsukihime` |
| `Ctrl+Shift+G` | Open Japanese subtitle generation | `shortcuts.openSubtitleGeneration` |
| `Ctrl+Alt+S` | Open subtitle sync (subsync) | `shortcuts.triggerSubsync` |
| `g` then `s` | Pick primary and secondary subtitles (when enabled) | `shortcuts.openSubtitleSelection` |
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` | | `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` |
| `` ` `` | Toggle stats overlay | `stats.toggleKey` | | `` ` `` | Toggle stats overlay | `stats.toggleKey` |
| `W` | Mark video watched and play the next one in the queue | `stats.markWatchedKey` |
| `Alt+C` | Open controller setup and remapping | `shortcuts.openControllerSelect` |
| `Alt+Shift+C` | Open controller debug view | `shortcuts.openControllerDebug` |
The stats toggle is handled inside the focused visible overlay window. It is configurable through the top-level `stats.toggleKey` setting and defaults to `Backquote`. While a video is playing, session help can also run commands: highlight a row with the arrow keys or the mouse, then press `Enter` or double-click it. Fixed overlay controls, global shortcuts, `y` chords, the multi-line copy and mine actions, Open session help, and the color legend are reference only. Help opened from the tray with no video loaded is read-only.
The subtitle sidebar toggle is overlay-local and only opens when SubMiner has a parsed cue list for the active subtitle source. The sidebar key has a separate mpv-side binding, `shortcuts.toggleSubtitleSidebar`. The sidebar only opens when SubMiner has parsed the active subtitle file. In the sidebar, `Enter` seeks to the focused line.
## Controller Shortcuts The subtitle picker (`g` then `s`) is off until you turn it on in **Settings, Behavior, Subtitle Selection**. Press the second key within one second. If `g` already has an action in SubMiner or mpv, the sequence is disabled and a warning is shown. See [subtitle selection](/configuration#subtitle-selection).
These overlay-local shortcuts open controller utilities for the Chrome Gamepad API integration. ## mpv plugin keys
| Shortcut | Action | Configurable | Press `y`, then the second key.
| ------------- | ------------------------------------ | -------------------------------- |
| `Alt+C` | Open controller config + remap modal | `shortcuts.openControllerSelect` |
| `Alt+Shift+C` | Open controller debug modal | `shortcuts.openControllerDebug` |
Controller input only drives the overlay while keyboard-only mode is enabled. The controller mapping and tuning live under the top-level `controller` config block; keyboard-only mode still works normally without a controller. | Keys | Action |
| ----- | -------------------------- |
## MPV Plugin Chords | `y-y` | Open the SubMiner menu |
| `y-s` | Start the overlay |
When the mpv plugin is installed, all commands use a `y` chord prefix — press `y`, then the second key within 1 second. | `y-S` | Stop the overlay |
| `y-t` | Toggle the visible overlay |
| Chord | Action |
| ----- | ------------------------ |
| `y-y` | Open SubMiner menu (OSD) |
| `y-s` | Start overlay |
| `y-S` | Stop overlay |
| `y-t` | Toggle visible overlay |
| `v` | Toggle primary subtitle bar visibility |
| `y-o` | Open Yomitan settings | | `y-o` | Open Yomitan settings |
| `y-r` | Restart overlay | | `y-r` | Restart the overlay |
| `y-c` | Check overlay status | | `y-c` | Show overlay status |
| `y-h` | Open session help | | `y-h` | Open session help |
| `v` | Cycle primary subtitle bar |
The bare `v` plugin binding intentionally overrides mpv's native primary subtitle visibility toggle so the SubMiner primary subtitle bar is hidden or restored instead. The plugin's `v` replaces mpv's own subtitle visibility toggle. When the overlay has focus, `y` then `d` toggles DevTools.
When the overlay has focus, press `y` then `d` to toggle DevTools (debugging helper). ## Customizing
## Drag-and-Drop `shortcuts.*` values are [Electron accelerator strings](https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts).
| Gesture | Action |
| ------------------------- | ------------------------------------------------ |
| Drop file(s) onto overlay | Replace current mpv playlist with dropped files |
| `Shift` + drop file(s) | Append all dropped files to current mpv playlist |
## Customizing Shortcuts
All `shortcuts.*` keys accept [Electron accelerator strings](https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts), for example `"CommandOrControl+Alt+A"`. Use `null` to disable a shortcut.
```jsonc ```jsonc
{ {
"shortcuts": { "shortcuts": {
"mineSentence": "CommandOrControl+S", "mineSentence": "CommandOrControl+S",
"copySubtitle": "CommandOrControl+C",
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
"openJimaku": null, // disabled "openJimaku": null, // disabled
}, },
} }
``` ```
The `keybindings` array overrides or extends the overlay's built-in key handling for mpv commands: `keybindings` entries map a key to an mpv command. They are merged with the defaults above. Set `command` to `null` to disable a default.
```jsonc ```jsonc
{ {
"keybindings": [ "keybindings": [
{ "key": "f", "command": ["cycle", "fullscreen"] },
{ "key": "m", "command": ["cycle", "mute"] }, { "key": "m", "command": ["cycle", "mute"] },
{ "key": "Space", "command": null }, // disable default Space → pause { "key": "MBTN_BACK", "command": ["sub-seek", -1] },
{ "key": "Ctrl+WHEEL_UP", "command": ["add", "sub-scale", 0.1] },
{ "key": "Space", "command": null },
], ],
} }
``` ```
Both `shortcuts`, `keybindings`, and `subtitleSidebar` are [hot-reloadable](/configuration#hot-reload-behavior) — changes take effect without restarting SubMiner. Mouse button names are `MBTN_LEFT`, `MBTN_MID`, `MBTN_RIGHT`, `MBTN_BACK`, and `MBTN_FORWARD`. Scroll wheel names are `WHEEL_UP`, `WHEEL_DOWN`, `WHEEL_LEFT`, and `WHEEL_RIGHT`. See [keybindings](/configuration#keybindings) and [shortcuts configuration](/configuration#shortcuts-configuration) in the config reference.
## Automatic mpv bindings
The overlay reads single-key, mouse button, and scroll wheel bindings from the running mpv (`input.conf`, mpv defaults, and scripts). If SubMiner does not handle the input, it passes it to mpv, so mpv's defaults like double-click for fullscreen and the wheel for volume work over the overlay. SubMiner shortcuts and `keybindings` entries win, including ones set to `null`, and right-click always pauses. Keys and clicks are not forwarded while you type in a text field, use an overlay menu, or have a Yomitan popup open, and clicks on subtitles or overlay controls stay with SubMiner. Scrolling over an overlay menu, the subtitle sidebar, or notification history scrolls that panel instead.
Keypad and media keys, key sequences, and mouse movement are not imported. Bindings imported this way do not appear in session help. If you add an mpv binding while SubMiner runs, refocus the overlay to pick it up.
+82 -98
View File
@@ -1,135 +1,119 @@
# Subtitle Annotations # Subtitle annotations
SubMiner annotates subtitle tokens in real time as they appear in the overlay. Four annotation layers work together to surface useful context while you watch: **N+1 highlighting**, **character-name highlighting**, **frequency highlighting**, and **JLPT tagging**. SubMiner can color and underline words in the subtitle overlay: words you already know, the one new word in an N+1 line, common words, JLPT levels, and character names. Each layer is off by default and works on its own, so turn on only the ones you want.
All four are opt-in and configured under `subtitleStyle`, `ankiConnect.knownWords`, and `ankiConnect.nPlusOne` in your config. They apply independently — you can enable any combination. Yomitan splits the subtitle into words, so your installed Yomitan dictionaries and their order decide where word boundaries fall. Grammar words such as particles (`は`), auxiliaries (`です`), and endings like `んです` stay hoverable but never get annotation colors.
Before any of those layers render, SubMiner strips annotation metadata from tokens that are usually just subtitle glue or annotation noise. Standalone particles, auxiliaries, adnominals, common explanatory endings like `んです` / `のだ`, merged trailing quote-particle forms like `...って`, auxiliary-stem grammar tails like `そうだ` (MeCab POS3 `助動詞語幹`), repeated kana interjections, and similar non-lexical helper tokens remain hoverable in the subtitle text, but they render as plain tokens without known-word, N+1, frequency, JLPT, or name-match annotation styling. Defaults for every key below are in the [configuration reference](/configuration).
## N+1 Word Highlighting ## Known words {#known-words}
N+1 highlighting identifies sentences where you know every word except one, making them ideal mining targets. When enabled, SubMiner builds a local cache of your known vocabulary from Anki and highlights tokens accordingly. Colors every word that already appears in your Anki decks, so you can see how much of a line you know.
**How it works:** Needs: Anki running with AnkiConnect, and at least one deck in `ankiConnect.knownWords.decks`.
1. SubMiner queries your Anki decks for existing `Expression` / `Word` field values. ```jsonc
2. The results are cached locally (`known-words-cache.json`) and refreshed on a configurable interval. {
3. When a subtitle line appears, each token is checked against the cache. "ankiConnect": {
4. If exactly one unknown word remains in the sentence, it is highlighted with `nPlusOneColor` (default: `#c6a0f6`). "knownWords": {
5. Already-known tokens can optionally display in `knownWordColor` (default: `#a6da95`). "highlightEnabled": true,
"decks": { "Kaishi 1.5k": ["Word"] },
},
},
}
```
**Key settings:** Map each deck to its expression or word field. SubMiner also reads the note's reading field when it has one, so a known word only matches in the reading its card teaches.
| Option | Default | Description | | Key | What it does |
| --- | --- | --- | | ------------------------------------------------- | ------------------------------------------------------------------- |
| `ankiConnect.knownWords.highlightEnabled` | `false` | Enable known-word cache lookups used by N+1 highlighting | | `ankiConnect.knownWords.highlightEnabled` | Turn known-word coloring on |
| `ankiConnect.knownWords.refreshMinutes` | `1440` | Minutes between Anki cache refreshes | | `ankiConnect.knownWords.decks` | Deck name to list of fields to read |
| `ankiConnect.knownWords.decks` | `{}` | Deck→fields map for known-word cache queries (legacy fallback: `ankiConnect.deck`) | | `ankiConnect.knownWords.matchMode` | `headword` matches the dictionary form, `surface` the text as shown |
| `ankiConnect.knownWords.matchMode` | `"headword"` | `"headword"` (dictionary form) or `"surface"` (raw text) | | `ankiConnect.knownWords.refreshMinutes` | How often the known-word list is re-read from Anki |
| `ankiConnect.nPlusOne.minSentenceWords` | `3` | Minimum tokens in a sentence for N+1 to trigger | | `ankiConnect.knownWords.addMinedWordsImmediately` | Count a word as known as soon as you mine it |
| `ankiConnect.nPlusOne.nPlusOne` | `#c6a0f6` | Color for the single unknown target word | | `subtitleStyle.knownWordColor` | Color for known words |
| `ankiConnect.knownWords.color` | `#a6da95` | Color for already-known tokens |
::: tip ### Known-word maturity highlighting
Set `refreshMinutes` to `1440` (24 hours) for daily sync if your Anki collection is large.
:::
## Character-Name Highlighting Colors known words by how well you know them instead of using one color. Each word gets the tier of its most mature card:
Character-name matches are built from the active merged SubMiner character dictionary, which auto-syncs character data from AniList for your recently-watched titles. Matching names are highlighted in subtitles and become available for hover-driven Yomitan character profiles — portraits, roles, voice actors, and biographical detail. - `new`: never studied
- `learning`: in the learning or relearning queue
- `young`: in review, interval below the threshold
- `mature`: in review, interval at or above `ankiConnect.knownWords.matureThresholdDays`
**How it works:** Turn it on with `ankiConnect.knownWords.maturityEnabled` (known-word highlighting must also be on). Set the colors under `subtitleStyle.knownWordMaturityColors` (`new`, `learning`, `young`, `mature`).
1. Subtitles are tokenized, then candidate name tokens are matched against the character dictionary via Yomitan's scanning pipeline. Tiers update when the known-word list refreshes, so a card that turns mature today keeps its old color until the next refresh. If your deck has no relearning steps, lapsed cards go straight back to review and show as `young`.
2. Matching tokens receive a dedicated style distinct from N+1 and frequency layers.
3. This layer can be independently toggled with `subtitleStyle.nameMatchEnabled`.
**Key settings:** ## N+1 word highlighting
| Option | Default | Description | An N+1 line has exactly one word you don't know. It is the easiest kind of sentence to mine, because the rest of the line gives you context. SubMiner colors that one unknown word.
| --- | --- | --- |
| `subtitleStyle.nameMatchEnabled` | `true` | Enable character-name token highlighting |
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Color used for character-name matches |
For full details on dictionary generation, name variant expansion, auto-sync lifecycle, and configuration, see the dedicated [Character Dictionary](/character-dictionary) page. Needs: the same Anki setup as [known words](#known-words) (`ankiConnect.knownWords.decks`). Known-word coloring itself can stay off.
## Frequency Highlighting | Key | What it does |
| --------------------------------------- | --------------------------------------- |
| `ankiConnect.nPlusOne.enabled` | Turn N+1 highlighting on |
| `ankiConnect.nPlusOne.minSentenceWords` | Skip lines shorter than this many words |
| `subtitleStyle.nPlusOneColor` | Color for the unknown word |
Frequency highlighting colors tokens based on how common they are, using dictionary frequency rank data. This helps you spot high-value vocabulary at a glance. ## Frequency highlighting
**Modes:** Colors words by how common they are, so a rare word in an easy line stands out.
- **Single** — all highlighted tokens share one color (`singleColor`). Needs: at least one frequency dictionary installed in Yomitan. When several are installed, SubMiner uses them in your Yomitan priority order. Occurrence-count dictionaries are skipped. You can also point `sourcePath` at a folder of Yomitan-format frequency files as a fallback.
- **Banded** — tokens are assigned to five color bands from most common to least common within the `topX` window.
SubMiner looks up each token's `frequencyRank` from `term_meta_bank_*.json` files. Only tokens with a positive rank at or below `topX` are highlighted. | Key | What it does |
| ------------------------------------------------ | ---------------------------------------------------------------------- |
| `subtitleStyle.frequencyDictionary.enabled` | Turn frequency highlighting on |
| `subtitleStyle.frequencyDictionary.topX` | Only color words whose rank is this number or lower (1 is most common) |
| `subtitleStyle.frequencyDictionary.mode` | `single` uses one color, `banded` splits the range into five colors |
| `subtitleStyle.frequencyDictionary.singleColor` | Color for `single` mode |
| `subtitleStyle.frequencyDictionary.bandedColors` | Five colors for `banded` mode, most common first |
| `subtitleStyle.frequencyDictionary.matchMode` | `headword` or `surface`, as for known words |
| `subtitleStyle.frequencyDictionary.sourcePath` | Optional folder of frequency files |
**Key settings:** ## JLPT tagging
| Option | Default | Description | Underlines each word in a color for its JLPT level, N1 to N5. The JLPT word lists ship with SubMiner, so there is nothing to install.
| --- | --- | --- |
| `subtitleStyle.frequencyDictionary.enabled` | `false` | Enable frequency highlighting |
| `subtitleStyle.frequencyDictionary.topX` | `1000` | Max frequency rank to highlight |
| `subtitleStyle.frequencyDictionary.mode` | `"single"` | `"single"` or `"banded"` |
| `subtitleStyle.frequencyDictionary.matchMode` | `"headword"` | `"headword"` or `"surface"` |
| `subtitleStyle.frequencyDictionary.singleColor` | — | Color for single mode |
| `subtitleStyle.frequencyDictionary.bandedColors` | — | Array of five hex colors for banded mode |
| `subtitleStyle.frequencyDictionary.sourcePath` | — | Custom path to frequency dictionary root |
When `sourcePath` is omitted, SubMiner searches default install/runtime locations for `frequency-dictionary` directories automatically. | Key | What it does |
| ------------------------------------- | ------------------------------ |
| `subtitleStyle.enableJlpt` | Turn JLPT underlines on |
| `subtitleStyle.jlptColors.N1` to `N5` | Underline color for each level |
::: info ## Character names
Frequency highlighting skips tokens that look like non-lexical noise (kana reduplication, short kana endings like `っ`), even when dictionary ranks exist.
:::
::: info Colors character names from the current show and lets you hover them for a portrait, role, and voice actor.
Frequency, JLPT, and N+1 metadata are only shown for tokens that survive the subtitle-annotation noise filter. Standalone grammar tokens like `は`, `です`, and `この` are intentionally left unannotated even if a dictionary can assign them metadata.
:::
## JLPT Tagging Needs: the [character dictionary](/character-dictionary), which SubMiner builds from AniList when you turn this on.
JLPT tagging adds colored underlines to tokens based on their JLPT level (N1–N5), giving you an at-a-glance sense of difficulty distribution in each subtitle line. | Key | What it does |
| -------------------------------------- | ----------------------------------------------- |
| `subtitleStyle.nameMatchEnabled` | Build the character dictionary and color names |
| `subtitleStyle.nameMatchImagesEnabled` | Show a small portrait next to each matched name |
| `subtitleStyle.nameMatchColor` | Color for character names |
**How it works:** ## Toggling during playback
SubMiner loads offline `term_meta_bank_*.json` files from `vendor/yomitan-jlpt-vocab` and matches each token's headword against the bank entries. Tokens with a recognized JLPT level receive a colored underline. Open the runtime options palette (`Ctrl/Cmd+Shift+O`) to switch these without restarting:
**Default colors:** - known-word highlighting, maturity colors, and known-word match mode
- N+1 highlighting
- JLPT tagging
- frequency highlighting
| Level | Color | Preview | Character names are toggled in the config file or the Settings window. Changes apply from the next subtitle line.
| --- | --- | --- |
| N1 | `#ed8796` | Red |
| N2 | `#f5a97f` | Peach |
| N3 | `#f9e2af` | Yellow |
| N4 | `#a6e3a1` | Green |
| N5 | `#8aadf4` | Blue |
All colors are customizable via the `subtitleStyle.jlptColors` object. ## When layers overlap
**Key settings:** If one word matches several layers, the first match in this list sets its color:
| Option | Default | Description | 1. Character name (also removes N+1, frequency, and JLPT marks)
| --- | --- | --- | 2. N+1 target
| `subtitleStyle.enableJlpt` | `false` | Enable JLPT underline styling | 3. Known word
| `subtitleStyle.jlptColors.N1`–`N5` | see above | Per-level underline colors | 4. Frequency
## Runtime Toggles JLPT is an underline, so it shows alongside any of these except a character name.
All annotation layers can be toggled at runtime via the mpv command menu without restarting:
- `ankiConnect.knownWords.highlightEnabled` (`On` / `Off`)
- `subtitleStyle.nameMatchEnabled` (`On` / `Off`)
- `subtitleStyle.enableJlpt` (`On` / `Off`)
- `subtitleStyle.frequencyDictionary.enabled` (`On` / `Off`)
Toggles only apply to new subtitle lines after the change — the currently displayed line is not re-tokenized in place.
## Rendering Priority
When multiple annotations apply to the same token, the visual priority is:
1. **N+1 target** (highest) — the single unknown word in an N+1 sentence
2. **Character-name match** — dictionary-driven character-name token styling
3. **Known-word color** — already-learned token tint
4. **Frequency highlight** — common-word coloring (not applied when N+1/character-name/known-word already matched)
5. **JLPT underline** — level-based underline (stacks with the above since it uses underline rather than text color)
+89
View File
@@ -0,0 +1,89 @@
# Japanese subtitle generation
When a video has no Japanese subtitles, SubMiner can transcribe its audio into a Japanese SRT with [whisper.cpp](https://github.com/ggml-org/whisper.cpp). Everything runs on your computer. You only need internet access to download a model.
## Setup
1. Install whisper.cpp's `whisper-cli` and FFmpeg (including `ffprobe`). SubMiner downloads models but not these programs.
2. Make sure they are on your `PATH`, or set their paths under **Settings > Integrations > Japanese Subtitle Generation** (`whisperPath`, `ffmpegPath`, `ffprobePath`).
3. Pick a model. Either choose one in the generation modal and click **Download model**, or set `subtitleGeneration.modelPath` to a multilingual whisper.cpp GGML `.bin` file you already have. English-only models and Python Whisper checkpoints do not work.
Downloaded models go to `models/whisper/` next to your SubMiner config file. A configured `modelPath` always wins over the modal's choice.
YouTube videos can use the same setup automatically in place of YouTube's captions. See [Generate subtitles with Whisper](/youtube-integration#generate-subtitles-with-whisper).
The modal's **Local tools** section lists anything missing. After you install a tool or change a path, click **Check again**.
## Generating from the overlay
1. Open a local video in mpv and select its Japanese audio track, or play a YouTube video.
2. Press `Ctrl+Shift+G`. If the subtitle sidebar is empty, its **Generate Japanese subtitles** button opens the same modal.
3. Pick a model and download it if needed.
4. Optionally check **Focus on spoken dialogue** (see below).
5. Click **Generate subtitles**.
The modal shows progress. **Cancel** stops the job. Closing the modal lets the job keep running, and reopening it shows the progress.
SubMiner saves `<video>.ja.generated.srt` next to the video and adds a number if that name is taken. If the same file is still playing, it loads the subtitles and resets the subtitle delay.
For a YouTube video, SubMiner downloads its audio first and pauses the video until the subtitles load. The subtitles are temporary, and the audio is deleted when generation ends or you switch videos. See [YouTube integration](/youtube-integration#generate-subtitles-with-whisper).
Change the shortcut with `shortcuts.openSubtitleGeneration`.
## Generating from the launcher
```bash
subminer generate-subs # current mpv file and audio track
subminer generate-subs episode.mkv --download-model
subminer generate-subs episode.mkv --model-path /path/to/ggml-small.bin
```
| Flag | What it does |
| ------------------------ | ------------------------------------------------------------- |
| `--model <name>` | Use this managed model, such as `small` or `large-v3-turbo` |
| `--download-model` | Download the managed model if it is missing |
| `--model-path <path>` | Use an existing model file |
| `--audio-stream <index>` | Pick an audio stream by its absolute FFmpeg index |
| `--output <path>` | Write to this SRT path. Existing files are never overwritten. |
With a file argument, SubMiner uses the audio stream tagged Japanese, or the first stream. `Ctrl+C` cancels.
## Choosing a model
The modal recommends **large-v3-turbo** if it finds an NVIDIA GPU (`nvidia-smi`) and your `whisper-cli` can use CUDA. Otherwise it recommends **small**. AMD, Vulkan, and Apple GPUs do not trigger the turbo recommendation. The recommendation does not change your settings.
| Model | Tradeoff |
| ---------------------- | ----------------------------------------------- |
| tiny, base | Fast and small, more recognition errors |
| small | Balanced quality and CPU time |
| medium, large-v1/v2/v3 | More accurate, needs more memory and time |
| large-v3-turbo | Faster than large-v3 with a small accuracy loss |
Quantized variants (`-q5_0`, `-q5_1`, `-q8_0`) use less disk and memory, with some accuracy loss. Your pick in the modal lasts for the session. Set `subtitleGeneration.managedModel` to change the default.
## Prioritizing spoken dialogue
**Focus on spoken dialogue** uses a speech detection (VAD) model to drop silent stretches before transcription. Long passages are split near detected speech, which reduces subtitles that appear before the line is spoken.
It needs two extra pieces:
- The Silero VAD model. Click **Download speech detection model** in the modal, or set `vadModelPath` to your own [Silero GGML model](https://huggingface.co/ggml-org/whisper-vad/tree/main).
- whisper.cpp's [speech segment detector](https://github.com/ggml-org/whisper.cpp/tree/master/examples/vad-speech-segments), found as `whisper-vad-speech-segments` or `vad-speech-segments` on `PATH`. Set `vadPath` for any other location.
The checkbox lasts for the session. Setting `vadModelPath` turns it on by default.
Dialogue mode keeps music and background sound that might contain speech, so songs can still produce subtitles. It can also take longer than a plain run, because each passage is transcribed separately.
## Using loaded subtitles as timing references
If the video playing in mpv already has a dialogue subtitle track loaded, SubMiner uses its cue times to decide where to split long audio. This works with or without dialogue mode. Whisper still writes the Japanese text and final timestamps. The launcher uses a reference only when its input is the file open in mpv.
SubMiner prefers English tracks and tracks labeled full or dialogue. It skips forced, image-based, generated, and signs or songs tracks, based on their titles and file names. An unlabeled signs-only file can slip through.
The reference must be timed correctly for the video. SubMiner does not fix a mistimed reference.
## Limitations
- Only local files and their internal audio tracks are supported. Not URLs, and not a separate audio file loaded in mpv. Pass a separate local audio file to the launcher instead.
- Whisper can miss, repeat, or invent lines, and its timing is approximate, especially over music or overlapping speech. Check the text and audio when you mine.
- SubMiner does not translate subtitles.
+50 -49
View File
@@ -1,69 +1,70 @@
# Subtitle Sidebar # Subtitle sidebar
The subtitle sidebar displays the full parsed cue list for the active subtitle file as a scrollable panel alongside mpv. It lets you review past and upcoming lines, click any cue to seek directly to that moment, and follow along without depending on the transient overlay subtitles. The subtitle sidebar lists every line of the current subtitle file in a scrollable panel next to mpv. Use it to reread lines you missed, look ahead, jump to any line, or copy a stretch of dialogue.
The sidebar is enabled by default. Set `subtitleSidebar.enabled` to `false` if you want to turn it off. ## Using the sidebar
## How It Works Press `\` to open or close it. The sidebar is on by default. Set `subtitleSidebar.enabled` to `false` to turn it off, or `subtitleSidebar.autoOpen` to `true` to open it at startup.
When SubMiner parses the active subtitle source into a cue list, the sidebar becomes available. Toggle it with the `\` key (configurable via `subtitleSidebar.toggleKey`). While open: - Click a line to seek to it. With a line focused from the keyboard, `Enter` seeks to it.
- The current line is highlighted and kept in view as playback moves (`autoScroll`).
- Hovering the list pauses playback (`pauseVideoOnHover`).
- Switching media or subtitle track updates the list.
- The active cue is highlighted and kept in view as playback advances (when `autoScroll` is `true`). The sidebar needs a subtitle file SubMiner can parse. Tracks that mpv renders itself, such as embedded ASS tracks, leave it empty. With no lines loaded, the sidebar shows a **Generate Japanese subtitles** button that opens [subtitle generation](/subtitle-generation). You can also open generation any time with `Ctrl+Shift+G`.
- Clicking any cue seeks mpv to that timestamp.
- The sidebar stays synchronized with the overlay — media transitions and subtitle source changes update both simultaneously.
The sidebar only appears when a parsed cue list is available. External subtitle sources that SubMiner cannot parse (for example, embedded ASS tracks rendered directly by mpv) will not populate the sidebar. For karaoke and animated ASS subtitles, SubMiner merges the per-frame effect lines into one clean line per cue.
## Layout Modes ## Copying dialogue {#selecting-and-copying-dialogue}
Two layout modes are available via `subtitleSidebar.layout`: 1. Drag across the text to select it. The selection can span several lines, and you can scroll to extend it.
2. Press `Ctrl/Cmd+C` or click **Copy**.
**`overlay`** (default) — The sidebar floats over mpv as a panel. It does not affect the player window size or position. SubMiner copies the text in subtitle order, without timestamps, with a blank line between cues. Dragging does not seek, and auto-scroll pauses while you have a selection. Press `Escape` to clear it. Changing media or subtitle track, or closing the sidebar, also clears it.
**`embedded`** — Reserves space on the right side of the player and shifts the video area to mimic a split-pane layout. Useful if you want the cue list visible without it covering the video. If you see unexpected positioning in your environment, switch back to `overlay` to isolate the issue. ## Layout
`subtitleSidebar.layout` has two modes:
- `overlay`: the sidebar floats over mpv and does not change the player window.
- `embedded`: reserves space on the right of the player and moves the video over, so the list doesn't cover it. Placement depends on your compositor. If the geometry comes out wrong, switch back to `overlay`.
## Configuration ## Configuration
Enable and configure the sidebar under `subtitleSidebar` in your config file: All keys live under `subtitleSidebar`. Defaults are in the [configuration reference](/configuration).
```json | Key | What it does |
| ------------------- | --------------------------------------------------------------- |
| `enabled` | Turn the sidebar on or off |
| `autoOpen` | Open the sidebar when the overlay starts |
| `layout` | `overlay` or `embedded` |
| `toggleKey` | Toggle key, as a `KeyboardEvent.code` value such as `Backslash` |
| `pauseVideoOnHover` | Pause playback while the pointer is over the list |
| `autoScroll` | Keep the current line in view |
| `css` | Styling, see below |
`css` takes CSS properties (`font-family`, `font-size`, `color`, `background-color`, `opacity`) and these custom properties:
| Property | Styles |
| -------------------------------------------- | ------------------------------ |
| `--subtitle-sidebar-max-width` | Maximum sidebar width |
| `--subtitle-sidebar-timestamp-color` | Timestamp text |
| `--subtitle-sidebar-active-line-color` | Current line text |
| `--subtitle-sidebar-active-background-color` | Current line background |
| `--subtitle-sidebar-hover-background-color` | Background of the hovered line |
```jsonc
{ {
"subtitleSidebar": { "subtitleSidebar": {
"enabled": true, "layout": "embedded",
"autoOpen": false, "css": {
"layout": "overlay", "font-size": "18px",
"toggleKey": "Backslash", "--subtitle-sidebar-max-width": "480px",
"pauseVideoOnHover": false, },
"autoScroll": true, },
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif",
"fontSize": 16
}
} }
``` ```
| Option | Type | Default | Description | Your `css` object replaces the default one as a whole. To keep a default value, copy it from the configuration reference into your object.
| --------------------------- | ------- | ------------ | -------------------------------------------------------------------------------------------------- |
| `enabled` | boolean | `true` | Enable subtitle sidebar support |
| `autoOpen` | boolean | `false` | Open the sidebar automatically on overlay startup |
| `layout` | string | `"overlay"` | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space |
| `toggleKey` | string | `"Backslash"` | `KeyboardEvent.code` for the toggle shortcut |
| `pauseVideoOnHover` | boolean | `false` | Pause playback while hovering the cue list |
| `autoScroll` | boolean | `true` | Keep the active cue in view during playback |
| `maxWidth` | number | `420` | Maximum sidebar width in CSS pixels |
| `opacity` | number | `0.95` | Sidebar opacity between `0` and `1` |
| `backgroundColor` | string | — | Sidebar shell background color |
| `textColor` | string | — | Default cue text color |
| `fontFamily` | string | — | CSS `font-family` applied to cue text |
| `fontSize` | number | `16` | Base cue font size in CSS pixels |
| `timestampColor` | string | — | Cue timestamp color |
| `activeLineColor` | string | — | Active cue text color |
| `activeLineBackgroundColor` | string | — | Active cue background color |
| `hoverLineBackgroundColor` | string | — | Hovered cue background color |
## Keyboard Shortcut See [keyboard shortcuts](/shortcuts) for all overlay keys.
| Key | Action | Config key |
| --- | ----------------------- | ------------------------------ |
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` |
The toggle is overlay-local and only opens when SubMiner has a parsed cue list for the active subtitle source. See [Keyboard Shortcuts](/shortcuts) for the full shortcut reference.
+163 -240
View File
@@ -1,305 +1,222 @@
# Troubleshooting # Troubleshooting
Common issues and how to resolve them. Find your symptom below. If you saw an error message, search this page for its text.
## MPV Connection ## Diagnose first
**Overlay starts but shows no subtitles** Run the launcher's dependency check:
SubMiner connects to mpv via a Unix socket (or named pipe on Windows). If the socket does not exist or the path does not match, the overlay will appear but subtitles will never arrive. ```bash
subminer doctor
- Ensure mpv is running with `--input-ipc-server=/tmp/subminer-socket`.
- If you use a custom socket path, set it in both your mpv config and SubMiner config (`mpvSocketPath`).
- The `subminer` wrapper script sets the socket automatically when it launches mpv. If you launch mpv yourself, the `--input-ipc-server` flag is required.
SubMiner retries the connection automatically with increasing delays (200 ms, 500 ms, 1 s, 2 s on first connect; 1 s, 2 s, 5 s, 10 s on reconnect). If mpv exits and restarts, the overlay reconnects without needing a restart.
## Logging and App Mode
- Default log output is `info`.
- Use `--log-level` for more/less output.
- Use `--dev`/`--debug` only to force app/dev mode (for example to get dev behavior from the overlay/app); they do not change log verbosity.
- You can combine both, for example `SubMiner.AppImage --start --dev --log-level debug`, when you need maximum diagnostics.
## Performance and Resource Impact
### At a glance
- Baseline: `SubMiner --start` is usually lightweight for normal playback.
- Common spikes come from:
- first subtitle parse/tokenization bursts
- media generation (`ffmpeg` audio/image and AVIF paths)
- media sync and subtitle tooling (`alass`, `ffsubsync`)
- `ankiConnect` enrichment (plus polling overhead when proxy mode is disabled)
### If playback feels sluggish
1. Reduce overlay workload:
- set secondary subtitles hidden:
- `secondarySub.defaultMode: "hidden"`
- disable optional enrichment:
- `subtitleStyle.enableJlpt: false`
- `subtitleStyle.frequencyDictionary.enabled: false`
2. Reduce rendering pressure:
- lower `subtitleStyle.fontSize`
- keep overlay complexity minimal during heavy CPU periods
3. Reduce media overhead:
- keep `ankiConnect.media.imageType` set to `static` (avoid animated AVIF unless needed)
- lower `ankiConnect.media.imageQuality`
- reduce `ankiConnect.media.maxMediaDuration`
4. Lower integration cost:
- disable AI translation when not needed (`ankiConnect.ai.enabled: false`)
- if needed, run immersion telemetry with lower duration expectations (`immersionTracking.enabled: false` for constrained sessions)
- favor the default lightweight YouTube subtitle startup settings on low-resource systems
### Practical low-impact profile
```json
{
"subtitleStyle": {
"fontSize": 30,
"enableJlpt": false,
"frequencyDictionary": {
"enabled": false
}
},
"secondarySub": {
"defaultMode": "hidden"
},
"ankiConnect": {
"media": {
"imageType": "static",
"imageQuality": 80,
"maxMediaDuration": 12
},
"ai": {
"enabled": false
}
},
"immersionTracking": {
"enabled": false
}
}
``` ```
### If usage is still high It reports the app binary, `mpv`, `yt-dlp`, `ffmpeg`, `fzf`, `rofi`, your config file, and the mpv socket path. It exits non-zero when the app binary or `mpv` is missing.
- Confirm only one SubMiner instance is running. Logs are written to daily files:
- Check whether bottlenecks are `ffmpeg`, `yt-dlp`, or sync tooling in system monitor.
- Use `info` logs by default; keep `debug` for targeted diagnosis.
- Reproduce once with `SubMiner.AppImage --start --log-level debug` and open DevTools (`y` then `d`) if freezes recur.
**"Failed to parse MPV message"** | Platform | Log directory |
| ------------- | -------------------------- |
| Linux / macOS | `~/.config/SubMiner/logs/` |
| Windows | `%APPDATA%\SubMiner\logs\` |
Logged when a malformed JSON line arrives from the mpv socket. Usually harmless — SubMiner skips the bad line and continues. If it happens constantly, check that nothing else is writing to the same socket path. Files are named `app-<date>.log`, `launcher-<date>.log`, and `mpv-<date>.log`. The mpv log is off by default. Turn log files on or off and set retention under [`logging`](/configuration#logging).
## AnkiConnect For more detail, raise the log level for one run:
**"AnkiConnect: unable to connect"** ```bash
subminer --log-level debug video.mkv
SubMiner.AppImage --start --log-level debug
```
First confirm you've completed the [Anki Integration prerequisites](/anki-integration#prerequisites) — Anki must be running with the AnkiConnect add-on installed. The default level is `warn`. `--dev` and `--debug` switch the app into dev mode but do not change log verbosity. To inspect the overlay itself, focus it and press `y` then `d` to open DevTools.
SubMiner connects to the active Anki endpoint: ## Overlay starts but shows no subtitles
- `ankiConnect.url` (direct mode, default `http://127.0.0.1:8765`) SubMiner reads subtitles from mpv over an IPC socket (a named pipe on Windows). If the paths do not match, the overlay appears but stays empty.
- `http://<ankiConnect.proxy.host>:<ankiConnect.proxy.port>` (proxy mode)
This error means the active endpoint is unavailable, or (in proxy mode) the proxy cannot reach `ankiConnect.proxy.upstreamUrl`. - The `subminer` launcher sets the socket for you. If you start mpv yourself, pass `--input-ipc-server=/tmp/subminer-socket`.
- If you changed `mpv.socketPath`, use the same path in your mpv config.
- If you changed the AnkiConnect port, update `ankiConnect.url` (or `ankiConnect.proxy.upstreamUrl` if using proxy mode). SubMiner reconnects on its own if mpv restarts.
- If using external Yomitan/browser clients, confirm they point to your SubMiner proxy URL.
SubMiner retries with exponential backoff (up to 5 s) and suppresses repeated error logs after 5 consecutive failures. When Anki comes back, you will see "AnkiConnect connection restored". ## Overlay does not appear
**Cards are created but fields are empty** - Confirm SubMiner is running (`SubMiner.AppImage --start`, or check for the process).
- Linux: Hyprland and Sway work natively. Any other compositor needs mpv and SubMiner under X11 or Xwayland, with `xdotool`, `xprop`, and `xwininfo` installed. See [KDE Plasma and other Wayland compositors](#kde-plasma-and-other-wayland-compositors).
- macOS: grant Accessibility permission in System Settings > Privacy & Security > Accessibility.
Field names in your config must match your Anki note type exactly (case-sensitive). Check `ankiConnect.fields` — for example, if your note type uses `SentenceAudio` but your config says `Audio`, the field will not be populated. ## Overlay is on the wrong monitor or position
See [Anki Integration](/anki-integration) for the full field mapping reference. SubMiner follows the mpv window. Tracking needs `hyprctl` (Hyprland), `swaymsg` (Sway), or `xdotool` and `xwininfo` (X11) on `PATH`.
**"Update failed" OSD message** If the position is only slightly off, right-click and drag the subtitle text to adjust the offset.
Shown when SubMiner tries to update a card that no longer exists, or when AnkiConnect rejects the update. Common causes: ## Clicks pass through the overlay
- The card was deleted in Anki between creation and enrichment update. - The overlay only takes input while the cursor is over subtitle text. Hover the text directly.
- The note type changed and a mapped field no longer exists. - Toggle the overlay off and on with `Alt+Shift+O`.
- Linux: if clicks keep failing, toggle the overlay off, click the mpv window, then toggle it back on.
## Overlay ## Hovering a word shows no popup
**Overlay does not appear** If you have not set up dictionaries yet, start with [Yomitan setup](/usage#yomitan-setup).
- Confirm SubMiner is running: `SubMiner.AppImage --start` or check for the process. - Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --yomitan`) and confirm at least one dictionary is imported and enabled.
- On Linux, the overlay requires a supported window backend. Hyprland and Sway have native Wayland support; all other compositors require both mpv and SubMiner to run under X11 or Xwayland (`xdotool` and `xwininfo` must be installed). - If `yomitan.externalProfilePath` is set, manage dictionaries in that external profile. SubMiner opens it read-only and has no settings window of its own in that mode.
- On macOS, grant Accessibility permission to SubMiner in System Settings > Privacy & Security > Accessibility. - Check the log for "Loaded Yomitan extension".
**Overlay appears but clicks pass through / cannot interact** Word boundaries come from Yomitan's parser. Some splits will be wrong, since Japanese has no spaces.
- Make sure you are hovering over subtitle text — the overlay only becomes interactive when the cursor is over a subtitle. ## "Electron downgrade blocked" or "Unsupported Electron runtime"
- On macOS/Windows: toggle the overlay off and back on (`Alt+Shift+O`) to re-enable pointer events.
- On Linux: mouse event handling is unreliable in some Electron/compositor combinations. If clicks consistently fail, toggle the overlay off, click the underlying mpv window, then toggle it back on.
**Overlay briefly freezes after a modal/runtime error** SubMiner refuses to load Yomitan storage when the current Electron major does not match the app build, or when the profile was previously opened by a newer Electron major or minor version. Patch downgrades within the same major and minor version are allowed, such as `43.7.3` to `43.7.2`. Downgrades from `43.7.x` to `43.6.x`, or from Electron 43 to 42, are blocked. Launch the packaged app or use the repository's `bun run dev` command. Do not delete the runtime safety record to force an older major or minor version to open the profile.
- Renderer errors now trigger an automatic recovery path. You should see a short toast ("Renderer error recovered. Overlay is still running."). Older SubMiner builds without this guard cannot enforce the safety record. When testing Electron 43 before returning to an older Electron 42 build, use the separate `SubMiner-dev` profile described in [development](/development#run-locally) so the older build never opens the upgraded profile.
- Recovery closes any open modal and restores click-through/shortcuts automatically without interrupting mpv playback.
- If errors keep recurring, toggle the overlay's DevTools using overlay chord `y` then `d` (or global `F12`) and inspect the `renderer overlay recovery` error payload for stack trace + modal/subtitle context.
**Overlay is on the wrong monitor or position** ## "Yomitan reported zero dictionaries after previously reporting ..."
SubMiner positions the overlay by tracking the mpv window. If tracking fails: SubMiner detected that a previously non-empty Yomitan profile suddenly appears empty. Automatic character-dictionary changes are blocked so they cannot overwrite the suspicious state. Close SubMiner, preserve the profile directory, and restore a known-good backup before importing or deleting dictionaries.
- Hyprland: Ensure `hyprctl` is available. ## "Yomitan extension not found in any search path"
- Sway: Ensure `swaymsg` is available.
- X11: Ensure `xdotool` and `xwininfo` are installed.
If the overlay position is slightly off, right-click and drag on subtitle text to fine-tune the overlay subtitle offset. The bundled Yomitan is missing. Re-download the AppImage, or place an unpacked Yomitan extension in `~/.config/SubMiner/yomitan`. Source builds must run `bun run build` first to produce `build/yomitan`.
## Yomitan ## "MeCab not found on system"
If you haven't set up dictionaries yet, see [Yomitan setup](/usage#yomitan-setup) first. This is informational. Tokenization uses Yomitan, not MeCab. Install MeCab only if you want to silence the message:
**"Yomitan extension not found in any search path"** - Arch: `sudo pacman -S mecab mecab-ipadic`
- Ubuntu/Debian: `sudo apt install mecab libmecab-dev mecab-ipadic-utf8`
- macOS: `brew install mecab mecab-ipadic`
SubMiner bundles Yomitan and searches for it in these locations (in order): ## "AnkiConnect: unable to connect"
1. `build/yomitan` (local/source build output) Anki must be running with the AnkiConnect add-on. See [Anki integration prerequisites](/anki-integration#prerequisites).
2. `<resources>/yomitan` (Electron resources path)
3. `/usr/share/SubMiner/yomitan`
4. `~/.config/SubMiner/yomitan` (user-data fallback on Linux)
SubMiner does not load the source tree directly from `vendor/subminer-yomitan`; source builds must produce `build/yomitan` first. - Direct mode: check that `ankiConnect.url` matches the AnkiConnect port.
- Proxy mode: check `ankiConnect.proxy.upstreamUrl`, and point external Yomitan or browser clients at the SubMiner proxy.
If you installed from the AppImage and see this error, the package may be incomplete. Re-download the AppImage or place the unpacked Yomitan extension manually in `~/.config/SubMiner/yomitan`. SubMiner keeps retrying and logs "AnkiConnect connection restored" once Anki is back.
**Yomitan lookup popup does not appear when hovering words or triggering lookup** ## Cards are created but fields are empty
- Verify Yomitan loaded successfully — check the terminal output for "Loaded Yomitan extension". Each name in `ankiConnect.fields` must match a field on your note type. Matching is case-insensitive, but otherwise the spelling must match. Unknown fields are skipped without an error. For example, config `Audio` does not fill a note field named `SentenceAudio`. See [Anki integration](/anki-integration).
- Yomitan requires dictionaries to be installed. Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --settings`) and confirm at least one dictionary is imported.
- If `yomitan.externalProfilePath` is set, import/check dictionaries in the external app/profile instead. SubMiner treats that profile as read-only and does not open its own Yomitan settings window.
- If the overlay shows subtitles but hover lookup never resolves on tokens, the tokenizer may have failed. See the MeCab section below.
## MeCab / Tokenization ## "Update failed" when mining
**"MeCab not found on system"** The card was deleted in Anki before SubMiner finished enriching it, or the note type changed and a mapped field no longer exists.
This is informational, not an error. SubMiner tokenization is driven by Yomitan's internal parser. MeCab availability checks may still run for auxiliary token metadata, but MeCab is not used as a tokenization fallback path. ## "Subtitle timing not found; copy again while playing"
To install MeCab: SubMiner has no timing for the current line yet. This happens when paused before any subtitle arrived, after switching subtitle tracks, or while an external subtitle file is still loading. Resume playback, wait for the next line, and mine again.
- **Arch Linux**: `sudo pacman -S mecab mecab-ipadic` ## "FFmpeg not found"
- **Ubuntu/Debian**: `sudo apt install mecab libmecab-dev mecab-ipadic-utf8`
- **macOS**: `brew install mecab mecab-ipadic`
**Words are not segmented correctly** Audio clips and screenshots need FFmpeg. Without it, cards are still created with empty media fields.
Japanese word boundaries depend on Yomitan parser output. If segmentation seems wrong: - Arch: `sudo pacman -S ffmpeg`
- Ubuntu/Debian: `sudo apt install ffmpeg`
- macOS: `brew install ffmpeg`
- Verify Yomitan dictionaries are installed and active. ## Audio or screenshot generation is slow or times out
- Note that CJK characters without spaces are segmented using parser heuristics, which is not always perfect.
## Media Generation - Use a local copy if the video is on a slow network mount.
- Set `ankiConnect.media.imageType` to `"static"`. Animated AVIF is the slowest path.
- Lower `ankiConnect.media.imageQuality` or `ankiConnect.media.maxMediaDuration`.
**"FFmpeg not found"** ## Subtitle sync (subsync)
SubMiner uses FFmpeg to extract audio clips and generate screenshots. Install it: Subtitle sync needs at least one of alass or ffsubsync. Neither ships with SubMiner.
- **Arch Linux**: `sudo pacman -S ffmpeg` **"Configured alass executable not found"**: install it (`paru -S alass` or `cargo install alass-cli`), or set `subsync.alass_path`.
- **Ubuntu/Debian**: `sudo apt install ffmpeg`
- **macOS**: `brew install ffmpeg`
Without FFmpeg, card creation still works but audio and image fields will be empty. **"Configured ffsubsync executable not found"**: install it (`paru -S python-ffsubsync` or `pip install ffsubsync`), or set `subsync.ffsubsync_path`.
**Audio or screenshot generation hangs** **"alass synchronization failed" / "ffsubsync synchronization failed"**:
Media generation has a 30-second timeout (60 seconds for animated AVIF). If your video file is on a slow network mount or the codec requires software decoding, generation may time out. Try: - alass needs a reference: a second subtitle track or the local video file. It cannot use the track being retimed.
- `ffmpeg` must be installed to extract internal subtitle tracks.
- ffsubsync only works on local files, not streams.
- Run the tool by hand to see its full error output.
- Using a local copy of the video file. ## "xz binary not found"
- Reducing `media.imageQuality` or switching from `avif` to `static` image type.
- Checking that `media.maxMediaDuration` is not set too high.
## Shortcuts TsukiHime subtitles are xz-compressed. Install `xz`:
**"Failed to register global shortcut"** - Arch: `sudo pacman -S xz`
- Ubuntu/Debian: `sudo apt install xz-utils`
- Fedora: `sudo dnf install xz`
- macOS: `brew install xz`
- Windows: `scoop install main/xz`, or download XZ Utils from [tukaani.org/xz](https://tukaani.org/xz/) and add the folder with `xz.exe` to `PATH`. Restart SubMiner afterwards.
Global shortcuts (`Alt+Shift+O`, `Alt+Shift+Y`) may conflict with other applications or desktop environment keybindings. Other TsukiHime errors are covered in [TsukiHime integration](/tsukihime-integration#troubleshooting).
- Check your DE/WM keybinding settings for conflicts.
- Change the shortcut in your config under `shortcuts.toggleVisibleOverlayGlobal`.
- On Wayland, global shortcut registration has limitations depending on the compositor. Only Hyprland and Sway are supported natively — see the [Hyprland](#hyprland) section below for shortcut passthrough rules. Other Wayland compositors require X11/Xwayland.
**Overlay keybindings not working**
Overlay-local shortcuts (Space, arrow keys, etc.) only work when the overlay window has focus. Click on the overlay or use the global shortcut to toggle it to give it focus.
## Subtitle Timing
**"Subtitle timing not found; copy again while playing"**
This OSD message appears when you try to mine a sentence but SubMiner has no timing data for the current subtitle. Causes:
- The video is paused and no subtitle has been received yet.
- The subtitle track changed and timing data was cleared.
- You are using an external subtitle file that mpv has not fully loaded.
Resume playback and wait for the next subtitle to appear, then try mining again.
## Subtitle Sync (Subsync)
Both **alass** and **ffsubsync** are optional external dependencies. Subtitle syncing requires at least one of them to be installed.
**"Configured alass executable not found"**
Install alass or configure the path:
- **Arch Linux (AUR)**: `paru -S alass`
- **Cargo**: `cargo install alass-cli`
- Set the path: `subsync.alass_path` in your config.
**"Configured ffsubsync executable not found"**
Install ffsubsync or configure the path:
- **Arch Linux (AUR)**: `paru -S python-ffsubsync`
- **pip**: `pip install ffsubsync`
- Must be on `PATH` or configured via `subsync.ffsubsync_path` in your config.
**"Subtitle synchronization failed"**
SubMiner tries alass first, then falls back to ffsubsync. If both fail:
- Ensure the reference subtitle track exists in the video (alass requires a source track).
- Check that `ffmpeg` is available (used to extract the internal subtitle track).
- Try running the sync tool manually to see detailed error output.
- ffsubsync requires local files and cannot handle remote media streams (e.g., streaming URLs).
## Jimaku ## Jimaku
**"Jimaku request failed" or HTTP 429** **"Jimaku request failed" or HTTP 429**: you hit the Jimaku rate limit. Wait for the time shown in the message. Setting `jimaku.apiKey` or `jimaku.apiKeyCommand` gives you a higher limit.
The Jimaku API has rate limits. If you see 429 errors, wait for the retry duration shown in the OSD message and try again. If you have a Jimaku API key, set it in `jimaku.apiKey` or `jimaku.apiKeyCommand` to get higher rate limits. ## Character names are not highlighted
## Platform-Specific See [Character dictionary](/character-dictionary) for the full list. The common causes:
### Linux - `subtitleStyle.nameMatchEnabled` is off, or the media did not resolve to an AniList entry.
- Portraits need `subtitleStyle.nameMatchImagesEnabled`.
- Wrong characters: open the manager (`Ctrl/Cmd+D`) and use **Override** to pick the right AniList entry.
- The feature is disabled when `yomitan.externalProfilePath` is set.
- **Wayland (Hyprland/Sway only)**: Native Wayland support is limited to Hyprland and Sway. Window tracking uses compositor-specific commands (`hyprctl` / `swaymsg`). If these are not on `PATH`, tracking will fail silently. Other Wayland compositors are not supported — both mpv and SubMiner must run under X11 or Xwayland instead. ## "Failed to register global shortcut"
- **X11 / Xwayland**: Requires `xdotool` and `xwininfo`. If missing, the overlay cannot track the mpv window position. This is the required backend for any Wayland compositor other than Hyprland or Sway — both mpv and SubMiner must be running under X11/Xwayland for window tracking to work.
- **Mouse passthrough**: On Linux, Electron's mouse passthrough is unreliable. SubMiner keeps pointer events enabled, meaning you may need to toggle the overlay off to interact with mpv controls underneath. Another app or your desktop already uses `Alt+Shift+Y` (Yomitan settings). Free it in your desktop or window manager settings. On Hyprland, add a `pass` rule (see [Hyprland](#hyprland)).
## Overlay shortcuts do nothing
Overlay shortcuts only work while the overlay has focus. Click the overlay, or press `Alt+Shift+O` with mpv or the overlay focused.
## Update checks
**"Update check failed"**: GitHub could not be reached. Check your connection and retry from the tray menu or with `subminer -u`.
**No prerelease offered**: set `updates.channel` to `"prerelease"` to include beta and RC builds.
**Launcher update shows a sudo command**: the launcher lives in a protected path such as `/usr/local/bin`. Run the command shown to replace it.
## Playback feels sluggish
Idle playback is cheap. Load comes from the first tokenization burst, media generation, subtitle sync, and Anki enrichment. To cut it:
- `ankiConnect.media.imageType: "static"`, plus lower `imageQuality` and `maxMediaDuration`.
- `subtitleStyle.enableJlpt: false` and `subtitleStyle.frequencyDictionary.enabled: false`.
- `secondarySub.defaultMode: "hidden"`.
- `immersionTracking.enabled: false` to stop stats logging.
Also check that only one SubMiner instance is running, and whether `ffmpeg`, `yt-dlp`, or a sync tool is the process using CPU.
## Linux
### Tray icon missing
Linux trays need a StatusNotifier/AppIndicator host. Hyprland has none by default. Enable a tray in Waybar, Hyprpanel, or another panel.
### Hyprland ### Hyprland
SubMiner's overlay is a transparent, frameless, always-on-top Electron window. Hyprland needs window rules to keep it transparent and borderless, and `pass` bindings to forward global shortcuts to SubMiner. SubMiner applies float, no-border, and no-blur properties to its window itself. If the overlay still has a border or an opaque background, a global opacity or blur rule is usually overriding it. Add a rule for the `SubMiner` class. Lua config:
**Overlay is not transparent or has a visible border** ```lua
hl.window_rule({
match = { class = "^SubMiner$" },
float = true,
border_size = 0,
xray = false,
no_shadow = true,
no_blur = true,
no_dim = true,
opaque = true,
dim_around = false,
opacity = "1.0 override 1.0 override",
})
```
Add these window rules to your `hyprland.conf`: Older `hyprland.conf` configs:
```ini ```ini
windowrule = float on, match:class SubMiner windowrule = float on, match:class SubMiner
@@ -309,28 +226,34 @@ windowrule = no_shadow on, match:class SubMiner
windowrule = no_blur on, match:class SubMiner windowrule = no_blur on, match:class SubMiner
``` ```
Without `xray off override`, the compositor may render the transparent overlay incorrectly — you might see a solid background or visual artifacts instead of the mpv video underneath. Hyprland swallows global shortcuts unless you pass them through. Add a `pass` bind for each one, and update it if you remap the key:
**Global shortcuts not working**
On Hyprland, Electron cannot register global shortcuts on its own. You must explicitly pass keybindings to SubMiner using `pass` rules:
```ini ```ini
bind = ALT SHIFT, O, pass, class:^(SubMiner)$ bind = ALT SHIFT, O, pass, class:^(SubMiner)$
bind = ALT SHIFT, Y, pass, class:^(SubMiner)$ bind = ALT SHIFT, Y, pass, class:^(SubMiner)$
``` ```
Add a `pass` rule for each global shortcut you configure. The defaults are `Alt+Shift+O` (toggle overlay) and `Alt+Shift+Y` (Yomitan settings). If you remap `shortcuts.toggleVisibleOverlayGlobal` to a different key, update the `pass` rule to match. If the overlay stays behind fullscreen mpv, check that the mpv socket is connected and that `hyprctl -j clients` works from the environment that launched SubMiner.
Without these rules, Hyprland intercepts the keypresses before they reach SubMiner, and the shortcuts silently do nothing. SubMiner restacks the existing overlay during fullscreen transitions on Hyprland. It keeps the overlay mapped so repeated hide/show calls do not steal focus and cancel mpv's fullscreen entry. Sway retains the hide/show refresh because SubMiner has no Sway-specific restacking implementation.
**Overlay stays behind mpv after fullscreen** See the Hyprland wiki on [global keybinds](https://wiki.hypr.land/Configuring/Binds/#global-keybinds) and [window rules](https://wiki.hypr.land/Configuring/Window-Rules/).
SubMiner watches mpv's `fullscreen` property and refreshes the overlay geometry when it changes. If the overlay still does not move or rise above fullscreen mpv, confirm that the mpv IPC socket is connected and that `hyprctl -j clients` and `hyprctl -j monitors` work from the same environment that launched SubMiner. ### KDE Plasma and other Wayland compositors
For more details, see the Hyprland docs on [global keybinds](https://wiki.hypr.land/Configuring/Binds/#global-keybinds) and [window rules](https://wiki.hypr.land/Configuring/Window-Rules/). Outside Hyprland and Sway, Wayland does not let the overlay stay on top of mpv, so both must run under Xwayland. SubMiner does this automatically for itself and for every mpv it launches (launcher, tray, Jellyfin, YouTube). Install `xdotool`, `xprop`, and `xwininfo`.
### macOS **Overlay sits behind mpv, and hover or Yomitan stops working**: mpv started as a native Wayland window. This happens when you launch mpv yourself. Launch through SubMiner, or force Xwayland in your own command:
- **Accessibility permission**: Required for window tracking. Grant it in System Settings > Privacy & Security > Accessibility. ```bash
- **Gatekeeper**: If macOS blocks SubMiner, right-click the app and select "Open" to bypass the warning, or remove the quarantine attribute: `xattr -d com.apple.quarantine /path/to/SubMiner.app` mpv --gpu-context=x11vk,x11egl,x11 video.mkv
```
`WAYLAND_DISPLAY= mpv video.mkv` also works, as does `gpu-context=x11vk` (or `x11egl`) in `mpv.conf`. To check, `xdotool search --class mpv` should print a window id.
**Overlay stays above an unrelated app**: SubMiner can only see X11/Xwayland windows in this mode. Run that app under Xwayland too.
## macOS
- Accessibility permission is required for window tracking: System Settings > Privacy & Security > Accessibility.
- If Gatekeeper blocks the app, right-click it and choose Open, or run `xattr -d com.apple.quarantine /path/to/SubMiner.app`.
+48
View File
@@ -0,0 +1,48 @@
# TsukiHime integration
[TsukiHime](https://tsukihime.org) extracts the subtitle tracks from anime torrent releases and hosts them for download. SubMiner searches it from the overlay, so you can grab Japanese or secondary-language subtitles for the current episode without a torrent client. Most releases carry only English subtitles, so [Jimaku](/jimaku-integration) is usually the better source for Japanese.
## Setup
TsukiHime needs no account or API key. SubMiner needs the `xz` binary on your `PATH` to unpack downloads. Most Linux distributions ship it (package `xz` or `xz-utils`).
## Usage
1. Press `Ctrl+Shift+T` during playback.
2. SubMiner fills in the title and episode from the file name and searches right away when it finds both. Otherwise, fix the fields and press `Enter`.
3. Pick a tab. The first tab shows your secondary language (`secondarySub.secondarySubLanguages`, or English if unset). The second tab shows Japanese. Each tab lists only releases that carry that language.
4. Select a release, then a subtitle track.
The track is saved next to the video with a language suffix, such as `<video>.ja.ass` or `<video>.en.ass` (a temp directory is used for streams). A Japanese track becomes mpv's primary subtitle. A track from the secondary tab loads as the secondary subtitle and leaves the primary alone.
Pick the release that matches your video file, same group and same version, and the timing will line up. With any other release, fix the offset with subtitle sync (`Ctrl+Alt+S`).
| Key | Action |
| ---------------- | -------------------------------------- |
| `Enter` | Search, or select the highlighted item |
| `Up` / `Down` | Move through releases or tracks |
| `Left` / `Right` | Switch tabs |
| `Escape` | Close |
You can also open the modal with `subminer app --open-tsukihime`, bind a key to `["__tsukihime-open"]` in `keybindings`, or change the shortcut with `shortcuts.openTsukihime`.
## Options
| Key | What it does |
| ---------------------------- | ------------------------------------------------------ |
| `tsukihime.maxSearchResults` | Maximum releases per search. The API caps this at 100. |
| `tsukihime.apiBaseUrl` | API address. Change only for a mirror. |
See [Configuration](/configuration#tsukihime) for defaults.
## Troubleshooting
**"xz binary not found."** Install `xz` or `xz-utils` with your package manager.
**"No releases with Japanese subtitles."** None of the results carry a Japanese track. Try another search, or use Jimaku.
**"Batch releases are not supported."** TsukiHime only has extracted tracks for single-file torrents. Pick the single-episode release.
**"No text subtitle tracks in this release."** The release has only image-based subtitles (PGS or VobSub) or none. Try another release.
**Timing is off.** The subtitle came from a different release than your video. Use subtitle sync (`Ctrl+Alt+S`) or pick the matching release.
+121 -312
View File
@@ -1,347 +1,156 @@
# Usage # Usage
> [!IMPORTANT] This page covers everyday use: starting playback, working with the overlay, and the commands you will reach for most. For every `subminer` subcommand and flag, see [Launcher script](/launcher-script).
> SubMiner requires the bundled Yomitan instance to have at least one dictionary imported for lookups to work.
> See [Yomitan setup](#yomitan-setup) for details.
::: tip Just finished first-run setup? ## Play a video
If you want Anki card enrichment (sentence, audio, screenshot), the only config you need is `ankiConnect` with your deck name and field names. Here is a minimal working example:
```jsonc
{
"ankiConnect": {
"enabled": true,
"deck": "Mining",
"fields": {
"sentence": "Sentence",
"audio": "SentenceAudio",
"image": "Picture",
},
},
}
```
Field names must match your Anki note type exactly (case-sensitive). See [Anki Integration](/anki-integration) for the full reference.
:::
## How It Works
1. SubMiner starts the overlay app in the background
2. MPV runs with an IPC socket at `/tmp/subminer-socket`
3. The overlay connects and subscribes to subtitle changes
4. Subtitles are tokenized with Yomitan's internal parser
5. Words are displayed as interactive spans in the overlay
6. Hover a word, then trigger Yomitan lookup with your configured lookup key/modifier to open the Yomitan popup
7. Optional [subtitle annotations](/subtitle-annotations) (N+1, character-name, frequency, JLPT) highlight useful cues in real time
There are several ways to use SubMiner:
> [!TIP]
> **New users: start with the `subminer` wrapper script** (or the **SubMiner mpv** shortcut on Windows). It handles mpv launch, IPC socket setup, and overlay lifecycle automatically so you don't need to configure anything in `mpv.conf`.
| Approach | Use when | How |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| **`subminer` script** | You want SubMiner to handle everything — launch mpv, set up the socket, start the overlay. **The simplest path and recommended starting point.** | `subminer video.mkv` |
| **SubMiner mpv shortcut** (Windows) | The recommended Windows entry point. Created during first-run setup, launches mpv with SubMiner's defaults directly. | Double-click, drag a file onto it, or run `SubMiner.exe --launch-mpv` |
| **MPV plugin** (all platforms) | You launch mpv yourself or from another tool (file manager, Jellyfin, etc.). Requires `--input-ipc-server=/tmp/subminer-socket` in your mpv config. | Use `y` chord keybindings inside mpv |
You can use both — the plugin provides in-player controls, while the `subminer` script (or the Windows shortcut) is convenient for direct playback. The `subminer` script runs directly via shebang on Linux and macOS (no `bun run` needed); on Windows it must be invoked with `bun run subminer` since the shebang is not supported.
## Live Config Reload
While SubMiner is running, it watches your active config file and applies safe updates automatically.
Live-updated settings:
- `subtitleStyle`
- `keybindings`
- `shortcuts`
- `secondarySub.defaultMode`
- `ankiConnect.ai`
Invalid config edits are rejected; SubMiner keeps the previous valid runtime config and shows an error notification.
For restart-required sections, SubMiner shows a restart-needed notification.
## Commands
On Windows, replace `SubMiner.AppImage` with `SubMiner.exe` in the direct packaged-app examples below.
```bash ```bash
# Browse and play videos subminer video.mkv
subminer # Current directory (uses fzf)
subminer -R # Use rofi instead of fzf
subminer -d ~/Videos # Specific directory
subminer -r -d ~/Anime # Recursive search
subminer video.mkv # Play specific file (default plugin config auto-starts visible overlay)
subminer --start video.mkv # Optional explicit overlay start (use when plugin auto_start=no)
subminer -S video.mkv # Same as above via --start-overlay
subminer https://youtu.be/... # Play a YouTube URL
subminer ytsearch:"jp news" # Play first YouTube search result
subminer --setup # Open first-run setup popup
subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging
subminer --log-level warn video.mkv # Set logging level explicitly
subminer --args '--fs=opengl-hq --ytdl-format=bestvideo*+bestaudio/best' video.mkv # Pass extra mpv args
# Options
subminer -T video.mkv # Disable texthooker server
subminer -b x11 video.mkv # Force X11 backend
subminer video.mkv # No mpv profile passed by default
subminer -p gpu-hq video.mkv # Use a specific mpv profile
subminer jellyfin # Open Jellyfin setup window (subcommand form)
subminer jellyfin -l --server http://127.0.0.1:8096 --username me --password 'secret'
subminer jellyfin --logout # Clear stored Jellyfin token/session data
subminer jellyfin -p # Interactive Jellyfin library/item picker + playback
subminer jellyfin -d # Jellyfin cast-discovery mode (background tray app)
subminer app --stop # Stop background app (including Jellyfin cast broadcast)
subminer doctor # Dependency + config + socket diagnostics
subminer config path # Print active config path
subminer config show # Print active config contents
subminer mpv socket # Print active mpv socket path
subminer mpv status # Exit 0 if socket is ready, else exit 1
subminer mpv idle # Launch detached idle mpv with SubMiner defaults
subminer dictionary /path/to/file-or-directory # Generate character dictionary ZIP from target (manual Yomitan import)
subminer dictionary --candidates /path/to/file.mkv
subminer dictionary --select 21355 /path/to/file.mkv
subminer texthooker # Launch texthooker-only mode
subminer texthooker -o # Launch texthooker and open it in your browser
subminer app --anilist # Pass args directly to SubMiner binary (example: AniList login flow)
# Direct packaged app control
SubMiner.AppImage --background # Start in background (tray + IPC wait, minimal logs)
SubMiner.AppImage --start --texthooker # Start overlay with texthooker
SubMiner.AppImage --texthooker # Launch texthooker only (no overlay window)
SubMiner.AppImage --texthooker --open-browser # Launch texthooker and open browser
SubMiner.AppImage --setup # Open first-run setup popup
SubMiner.AppImage --stop # Stop overlay
SubMiner.AppImage --start --toggle # Start MPV IPC + toggle visibility
SubMiner.AppImage --show-visible-overlay # Force show visible overlay
SubMiner.AppImage --hide-visible-overlay # Force hide visible overlay
SubMiner.AppImage --toggle-primary-subtitle-bar # Toggle primary subtitle bar visibility
SubMiner.AppImage --start --dev # Enable app/dev mode only
SubMiner.AppImage --start --debug # Alias for --dev
SubMiner.AppImage --start --log-level debug # Force verbose logging without app/dev mode
SubMiner.AppImage --settings # Open Yomitan settings
SubMiner.AppImage --jellyfin # Open Jellyfin setup window
SubMiner.AppImage --jellyfin-login --jellyfin-server http://127.0.0.1:8096 --jellyfin-username me --jellyfin-password 'secret'
SubMiner.AppImage --jellyfin-logout # Clear stored Jellyfin token/session data
SubMiner.AppImage --jellyfin-libraries
SubMiner.AppImage --jellyfin-items --jellyfin-library-id LIBRARY_ID --jellyfin-search anime --jellyfin-limit 20
SubMiner.AppImage --jellyfin-play --jellyfin-item-id ITEM_ID --jellyfin-audio-stream-index 1 --jellyfin-subtitle-stream-index 2 # Requires connected mpv IPC (--start or plugin workflow)
SubMiner.AppImage --jellyfin-remote-announce # Force cast-target capability announce + visibility check
SubMiner.AppImage --dictionary # Generate character dictionary ZIP for current anime
SubMiner.AppImage --dictionary-candidates # List AniList candidates for current character dictionary series
SubMiner.AppImage --dictionary-select --dictionary-anilist-id 21355 # Pin correct AniList media for series
SubMiner.AppImage --open-character-dictionary # Open in-app AniList selector
SubMiner.AppImage --help # Show all options
``` ```
Once Jellyfin is configured, the tray menu includes `Jellyfin Discovery` for starting or stopping cast discovery in the current app session without changing config. On Windows, double-click the **SubMiner mpv** shortcut or drag a video onto it.
### Logging and App Mode SubMiner starts mpv, connects to it, and opens the overlay. Subtitle lines appear as hoverable words. Hover a word to look it up, then mine it into Anki. [Mining workflow](/mining-workflow) covers lookup and card creation in detail.
- `--log-level` controls logger verbosity. Run `subminer` with no file to pick one from the current directory instead. See [Picking files](#picking-files).
- `--dev` and `--debug` are app/dev-mode switches; they are not log-level aliases.
- `--background` defaults to quieter logging (`warn`) unless `--log-level` is set.
- `--background` launched from a terminal detaches and returns the prompt; stop it with tray Quit or `SubMiner.AppImage --stop` (`SubMiner.exe --stop` on Windows).
- Linux desktop launcher starts SubMiner with `--background` by default (via electron-builder `linux.executableArgs`).
- On Linux, the app now defaults `safeStorage` to `gnome-libsecret` for encrypted token persistence.
Launcher pass-through commands also support `--password-store=<backend>` and forward it to the app when present.
Override with e.g. `--password-store=basic_text`.
- Use both when needed, for example `SubMiner.AppImage --start --dev --log-level debug` (or `SubMiner.exe --start --dev --log-level debug` on Windows).
### Windows mpv Shortcut
First-run setup creates the config file, then requires Yomitan dictionaries before it can finish. The global mpv plugin install is optional because SubMiner-managed mpv launches inject the bundled runtime plugin.
If you enabled the optional Windows shortcut during install, SubMiner creates a `SubMiner mpv` shortcut in the Start menu and/or on the desktop. On Windows, that shortcut is the recommended way to launch local files with SubMiner because it starts `mpv.exe` with the right defaults directly.
After setup completes, the shortcut is the normal Windows playback entry point.
You can use it three ways:
- Double-click `SubMiner mpv` to open `mpv` with SubMiner's default socket/subtitle args.
- Drag a video file onto `SubMiner mpv` to launch that file with the same defaults.
- Run it directly from Command Prompt or PowerShell with `--launch-mpv`.
```powershell
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
```
This flow requires `mpv.exe` to be discoverable. Leave `mpv.executablePath` blank to auto-discover from `PATH`, or set it to the full `mpv.exe` path if mpv is installed elsewhere. `SUBMINER_MPV_PATH` is still honored as a fallback.
### Launcher Subcommands
- `subminer jellyfin` / `subminer jf`: Jellyfin-focused workflow aliases.
- `subminer doctor`: health checks for core dependencies and runtime paths.
- `subminer config`: config helpers (`path`, `show`).
- `subminer mpv`: mpv helpers (`status`, `socket`, `idle`).
- `subminer dictionary <path>`: generates a Yomitan-importable character dictionary ZIP from a file/directory target.
- Use `subminer dictionary --candidates <path>` and `subminer dictionary --select <id> <path>` to correct AniList character-dictionary matches for a whole series.
- `subminer texthooker`: texthooker-only shortcut (same behavior as `--texthooker`).
- `subminer app` / `subminer bin`: direct passthrough to the SubMiner binary/AppImage.
- Subcommand help pages are available (for example `subminer jellyfin -h`).
### First-Run Setup
Setup popup appears on first launch, or when setup has not been completed.
You can also open it manually:
```bash
subminer --setup
SubMiner.AppImage --setup
```
Setup flow:
- config file: create the default config directory and prefer `config.jsonc`
- plugin compatibility: optionally install the legacy global mpv plugin; managed launches use the bundled runtime plugin without it
- legacy plugin cleanup: remove detected global SubMiner mpv plugin files from mpv script directories via the OS trash when you do not want regular mpv to load SubMiner
- Yomitan shortcut: open bundled Yomitan settings directly from the setup window
- dictionary check: ensure at least one bundled Yomitan dictionary is available, unless an external Yomitan profile is configured
- Windows: optionally create or remove `SubMiner mpv` Start Menu/Desktop shortcuts (`SubMiner.exe --launch-mpv`)
- Windows: optionally set `mpv.executablePath` if `mpv.exe` is not on `PATH`
- refresh: re-check plugin + dictionary state without restarting
- `Finish setup` stays disabled until the config and dictionary gates are satisfied
- finish action writes setup completion state and suppresses future auto-open prompts
AniList character dictionary auto-sync (optional):
- Enable with `anilist.characterDictionary.enabled=true` in config.
- SubMiner syncs the currently watched AniList media into a per-media snapshot, then rebuilds one merged `SubMiner Character Dictionary` from the most recently used snapshots.
- Rotation limit defaults to 3 recent media snapshots in that merged dictionary (`maxLoaded`).
Use subcommands for Jellyfin workflows (`subminer jellyfin ...`).
Top-level launcher flags like `--jellyfin-*` are intentionally rejected.
### MPV Profile Example (mpv.conf)
`subminer` passes the following MPV options directly on launch by default:
- `--input-ipc-server=/tmp/subminer-socket` (or your configured socket path)
- `--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us`
- `--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us`
- `--sub-auto=fuzzy`
- `--sub-file-paths=.;subs;subtitles`
- `--sid=auto`
- `--secondary-sid=auto`
- `--secondary-sub-visibility=no`
You can append additional MPV arguments with launcher `-a/--args`, for example `--args "--ao=alsa --volume=80"`.
You can define a matching profile in `~/.config/mpv/mpv.conf` for consistency when launching `mpv` manually or from other tools. The Windows `SubMiner.exe --launch-mpv` shortcut path uses equivalent args directly, but skips the extra current-directory subtitle scan to avoid duplicate sidecar detection when you drag a video onto the shortcut; the optional profile remains useful for manual mpv launches and the `subminer` wrapper defaults to `--profile=subminer` (or override with `subminer -p <profile> ...`):
```ini
[subminer]
# IPC socket (must match SubMiner config)
input-ipc-server=/tmp/subminer-socket
# Prefer JP/EN audio + subtitle language variants
alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
# Auto-load external subtitles
sub-auto=fuzzy
sub-file-paths=.;subs;subtitles
# Select primary + secondary subtitle tracks automatically
sid=auto
secondary-sid=auto
secondary-sub-visibility=no
```
### Yomitan setup ### Yomitan setup
SubMiner includes a bundled Yomitan extension for overlay word lookup. This bundled extension is separate from any Yomitan browser extension you may have installed. Lookups need at least one dictionary in SubMiner's bundled Yomitan. First-run setup asks you to import one. To add more later, open Yomitan settings with `Alt+Shift+Y` or `subminer app --yomitan`.
For SubMiner overlay lookups to work, open Yomitan settings (`subminer app --settings` or `SubMiner.AppImage --settings`) and import at least one dictionary in the bundled Yomitan instance. The bundled Yomitan is separate from any Yomitan in your browser. It has its own dictionaries and settings.
If you also use Yomitan in a browser, configure that browser profile separately; it does not inherit dictionaries or settings from the bundled instance. ## Picking files
### YouTube Playback ```bash
subminer # fzf picker for the current directory
subminer -d ~/Anime -r # pick from a directory, searching subfolders
subminer -R # rofi picker instead of fzf (Linux)
subminer -H # watch history: replay, next, or previous episode
```
`subminer` accepts direct URLs (for example, YouTube links) and `ytsearch:` targets. See [Launcher script](/launcher-script#video-picker) for picker and history details.
For YouTube playback, SubMiner resolves subtitle selection during startup while mpv is paused: it auto-selects the default primary subtitle track plus a best-effort secondary track, then resumes when primary subtitles are ready.
Notes: ## Overlay basics
- Install `yt-dlp` so mpv can resolve YouTube streams and subtitle tracks reliably. | Key | Action |
- For YouTube URLs, startup no longer requires opening the picker first; SubMiner loads subtitles and keeps the overlay available for retries. | ------------- | --------------------------------------------------------------------- |
- Press `Ctrl+Alt+C` during active YouTube playback to open the manual YouTube subtitle picker and retry track selection. | `Alt+Shift+O` | Show or hide the overlay (works while the overlay or mpv has focus) |
- For YouTube URLs, `subminer` probes available YouTube subtitle tracks, reuses existing authoritative tracks when available, and downloads only missing sides. | `Alt+Shift+Y` | Open Yomitan settings (works from any window, not configurable) |
- Native mpv secondary subtitle rendering stays hidden so the overlay remains the visible secondary subtitle surface. | `V` | Cycle the subtitle bar through hidden, visible, and hover-only |
- Primary subtitle target languages come from `youtube.primarySubLanguages` (defaults to `["ja","jpn"]`). | `Ctrl+Alt+P` | Open the playlist browser to queue, reorder, or jump between episodes |
- Secondary target languages come from `secondarySub.secondarySubLanguages` (defaults to English if unset). | `Ctrl/Cmd+/` | Show every overlay and mpv keybinding for this session |
- Configure defaults in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`) under `youtube` and `secondarySub`.
For local video files, SubMiner uses the same config-driven language priorities to auto-select the primary and secondary subtitle tracks from internal and external subtitle sources. Hovering subtitle text pauses mpv, and moving away resumes it. An open Yomitan popup also keeps playback paused. Turn these off with `subtitleStyle.autoPauseVideoOnHover` and `subtitleStyle.autoPauseVideoOnYomitanPopup`.
## Controller Support You can drop files onto the overlay:
SubMiner supports gamepad/controller input for couch-friendly usage via the Chrome Gamepad API. Controller input drives the overlay while keyboard-only mode is enabled. - A video replaces what is playing. Hold `Shift` to add it to the playlist instead.
- A subtitle file loads as a new subtitle track.
### Getting Started The full list is in [Keyboard shortcuts](/shortcuts). The in-player `y` key chords are in [mpv plugin](/mpv-plugin).
1. Connect a controller before or after launching SubMiner. ## YouTube playback
2. Enable keyboard-only mode — press `Y` on the controller (default binding) or use the overlay keybinding.
3. Press `Alt+C` in the overlay by default to pick the controller you want to save and remap any action inline.
4. Click `Learn` on the overlay action you want, then press the matching button, trigger, or stick direction on the controller.
5. Use the left stick to navigate subtitle tokens and scroll the popup; use the right stick vertically for popup page jumps.
6. Press `A` to look up the selected word, `X` to mine a card, `B` to close the popup.
By default SubMiner uses the first connected controller. `Alt+C` opens the controller config modal, where you can save the preferred controller and remap bindings inline, and `Alt+Shift+C` opens the live debug modal with raw axes/button values for non-standard pads. Both shortcuts can be changed through `shortcuts.openControllerSelect` and `shortcuts.openControllerDebug`. Pass a URL or a search. Install `yt-dlp` first.
### Default Button Mapping ```bash
subminer https://youtu.be/...
subminer ytsearch:"jp news" # play the first search result
```
SubMiner picks subtitles during startup while mpv is paused. It selects a Japanese primary track and an English secondary track, downloads whatever is missing, and resumes once the primary subtitles are ready. If the choice is wrong, press `Ctrl+Alt+C` to open the YouTube subtitle picker and choose again.
Language preferences live under `youtube` and `secondarySub` in the config. See [YouTube integration](/youtube-integration).
## Common commands
```bash
subminer stats # start the immersion stats dashboard
subminer settings # open the settings window
subminer yt # browse YouTube; picked videos play in mpv
subminer doctor # check dependencies, config, and the mpv socket
subminer generate-subs video.mkv # make Japanese subtitles from the audio
subminer logs -e # export a log ZIP for bug reports
subminer app --setup # reopen first-run setup
subminer -u # update SubMiner
```
Two flags help early on:
- `-a/--args` passes options to mpv, for example `subminer --args "--volume=80" video.mkv`.
- `--log-level debug` turns on verbose logs when something is wrong.
[Launcher script](/launcher-script) lists every command. Jellyfin, sync, and character dictionary commands are covered in [Jellyfin](/jellyfin-integration), [Sync between machines](/launcher-script#sync-between-machines), and [Character dictionary](/character-dictionary).
### Generate Japanese subtitles locally
`subminer generate-subs` transcribes audio with whisper.cpp and writes a Japanese SRT file. If that file is playing in mpv, it loads the new subtitles right away. Leave out the path to use the file mpv is playing.
```bash
subminer generate-subs video.mkv --download-model # download a model on first use
subminer generate-subs video.mkv --model-path ~/models/ggml-medium.bin
```
You need `whisper-cli`, `ffmpeg`, and `ffprobe`. Check the output before mining, since speech recognition makes mistakes over music and overlapping voices. See [Subtitle generation](/subtitle-generation) for models, timing references, and settings.
## Windows mpv shortcut
First-run setup can create a **SubMiner mpv** shortcut in the Start menu and on the desktop. It is the easiest way to play local files on Windows:
- Double-click it to open mpv with SubMiner attached.
- Drag a video onto it to play that file.
- Run it from a terminal:
```powershell
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
```
mpv must be on `PATH`, or `mpv.executablePath` must point to `mpv.exe`. The `subminer` terminal command also works on Windows if you installed it during setup.
## Tray menu
The tray icon gives you:
- **Export Logs**: saves a log ZIP and shows its path. Usernames, IP addresses, emails, tokens, passwords, and cookies are masked in the exported copy. Your log files on disk stay unchanged.
- **View Changelog**: release notes, including versions newer than yours. Use `J`/`K` to move between versions, `Enter` to expand one, and `Esc` to close.
- **Sync Stats & History**: opens the [sync window](/launcher-script#sync-between-machines).
- **Jellyfin Discovery**: turns cast discovery on or off for this session, once [Jellyfin](/jellyfin-integration) is set up.
On Wayland, the tray icon only appears if your panel provides a StatusNotifier (AppIndicator) tray.
## Controller support {#controller-support}
You can drive the overlay with a gamepad.
1. Set `controller.enabled` to `true` in your config.
2. Connect a controller. SubMiner uses the first one it sees.
3. Press `Y` on the controller to turn on keyboard-only mode. The controller only works in this mode.
4. Move between words with the left stick, press `A` to look one up, and `X` to mine it.
Press `Alt+C` to choose a controller and remap buttons. Click an action's **Learn** button, then press the button you want. `Alt+Shift+C` shows raw input values for unusual pads.
| Button | Action | | Button | Action |
| ----------------------- | --------------------------------------- | | --------------------- | ------------------------------------ |
| `A` (South) | Toggle lookup | | `A` (South) | Look up the selected word |
| `B` (East) | Close lookup | | `B` (East) | Close the lookup |
| `X` (West) | Mine a card |
| `Y` (North) | Toggle keyboard-only mode | | `Y` (North) | Toggle keyboard-only mode |
| `X` (West) | Mine card | | `L1` | Play the current Yomitan audio |
| `L1` | Play current Yomitan audio | | `R1` | Next Yomitan audio source |
| `R1` | Next Yomitan audio track | | `L3` | Pause or resume mpv |
| `L3` (left stick press) | Toggle mpv pause |
| `Select` / `Minus` | Quit mpv | | `Select` / `Minus` | Quit mpv |
| `L2` / `R2` | Unbound (available for custom bindings) | | Left stick | Move between words, scroll the popup |
| Right stick (up/down) | Jump through the popup |
### Analog Controls On controllers that report the W3C standard layout, the default quit button lands on `L2` instead of `Select`. Remap it with `Alt+C`. All options are in [Configuration](/configuration#controller-support).
| Input | Action | ## Development profiles
| --------------------- | --------------------------------------------- |
| Left stick horizontal | Move token selection left/right |
| Left stick vertical | Scroll Yomitan popup |
| Right stick vertical | Jump through Yomitan popup |
| D-pad | Fallback for stick navigation when configured |
Learn mode ignores already-held inputs and waits for the next fresh button press or axis direction, which avoids accidental captures when you open the modal mid-input. `--dev` and `--debug` use a separate `SubMiner-dev` profile. They do not read or modify dictionaries and configuration from the installed app unless `SUBMINER_USE_PRODUCTION_PROFILE=1` is explicitly set. See [Development](/development#run-locally).
All button and axis mappings are configurable under the `controller` config block. See [Configuration — Controller Support](/configuration#controller-support) for the full options. ## Changing settings while you watch
## Keybindings SubMiner watches your config file and applies most changes without a restart, including subtitle style, keybindings, and most Anki settings. If a change needs a restart, SubMiner tells you. If the file has an error, it keeps the last working config and shows a notification. See [Configuration](/configuration).
See [Keyboard Shortcuts](/shortcuts) for the full reference, including mining shortcuts, overlay controls, and customization. Next: [Mining workflow](/mining-workflow).
**Global shortcuts** (work system-wide):
| Keybind | Action |
| ------------- | ---------------------- |
| `Alt+Shift+O` | Toggle visible overlay |
| `Alt+Shift+Y` | Open Yomitan settings |
`Alt+Shift+Y` is fixed and not configurable. All other shortcuts can be changed under `shortcuts` in your config.
Useful overlay-local default keybinding: `Ctrl+Alt+P` opens the playlist browser for the current video's parent directory and the live mpv queue so you can append, reorder, remove, or jump between episodes without leaving playback.
Press `V` to hide or restore the primary SubMiner subtitle bar. The mpv plugin also binds bare `v` to the same action, overriding mpv's native primary subtitle visibility toggle.
`Ctrl/Cmd+/` opens the session help modal with the current overlay and mpv keybindings. If you use the mpv plugin, the same help view is also available through the `y-h` chord.
Hovering over subtitle text pauses mpv by default; leaving resumes it. Yomitan popups also pause playback by default. Set `subtitleStyle.autoPauseVideoOnHover: false` or `subtitleStyle.autoPauseVideoOnYomitanPopup: false` to disable either behavior.
### Drag-and-Drop
- Drop video files onto the overlay to replace current playback.
- Hold `Shift` while dropping to append to the playlist instead.
Next: [Mining Workflow](/mining-workflow) — word lookup, card creation, and the full mining loop.
+107 -226
View File
@@ -1,68 +1,71 @@
# WebSocket / Texthooker API & Integration # WebSocket and texthooker API
SubMiner exposes a small set of local integration surfaces for browser tools, automation helpers, and mpv-driven workflows: SubMiner streams the current subtitle over local WebSockets and serves a texthooker page, so browser tools and your own scripts can follow along. This page is the reference for building a client. If you only want subtitles in a browser tab for Yomitan, see [Texthooker page](#texthooker-integration-guide).
- **Subtitle WebSocket** at `ws://127.0.0.1:6677` by default for plain subtitle pushes.
- **Annotation WebSocket** at `ws://127.0.0.1:6678` by default for token-aware clients.
- **Texthooker HTTP UI** at `http://127.0.0.1:5174` by default for browser-based subtitle consumption.
- **mpv plugin script messages** for in-player automation and extension.
This page documents those integration points and shows how to build custom consumers around them.
## Quick Reference
| Surface | Default | Purpose | | Surface | Default | Purpose |
| --- | --- | --- | | --------------------- | --------------------------- | ----------------------------------------------------- |
| `websocket` | `ws://127.0.0.1:6677` | Basic subtitle broadcast stream | | `websocket` | `ws://127.0.0.1:6677` | Plain subtitle text |
| `annotationWebsocket` | `ws://127.0.0.1:6678` | Structured stream with token metadata | | `annotationWebsocket` | `ws://127.0.0.1:6678` | Subtitle text plus token metadata and rendered HTML |
| `texthooker` | `http://127.0.0.1:5174` | Local texthooker UI with injected websocket config | | `texthooker` | `http://127.0.0.1:5174` | Bundled texthooker page, preconfigured for your setup |
| mpv plugin | `script-message subminer-*` | Start/stop/toggle/status automation inside mpv | | mpv plugin | `script-message subminer-*` | Start, stop, toggle, and status automation inside mpv |
## Enable and Configure the Services All servers bind to `127.0.0.1` only. There is no authentication.
SubMiner's integration ports are configured in `config.jsonc`. ## Enable the services
All three services are off by default. Turn on the ones you need in `config.jsonc`:
```jsonc ```jsonc
{ {
"websocket": { "websocket": {
"enabled": "auto", "enabled": "auto",
"port": 6677 "port": 6677,
}, },
"annotationWebsocket": { "annotationWebsocket": {
"enabled": true, "enabled": true,
"port": 6678 "port": 6678,
}, },
"texthooker": { "texthooker": {
"launchAtStartup": true, "launchAtStartup": true,
"openBrowser": false "openBrowser": false,
} },
} }
``` ```
### How startup behaves - `websocket.enabled`: `true` always starts the plain stream. `"auto"` starts it unless the external `mpv_websocket` plugin is installed at `~/.config/mpv/mpv_websocket`.
- `annotationWebsocket.enabled`: starts the annotated stream. It is independent of `websocket`.
- `texthooker.launchAtStartup`: starts the texthooker page with the app.
- `texthooker.openBrowser`: opens the page in your browser when it starts.
- `websocket.enabled: "auto"` starts the basic subtitle websocket unless SubMiner detects the external `mpv_websocket` plugin. See [Configuration](/configuration) for all related options.
- `annotationWebsocket` is independent from `websocket` and stays enabled unless you explicitly disable it.
- `texthooker.launchAtStartup` starts the local HTTP UI automatically.
- `texthooker.openBrowser` controls whether SubMiner opens the texthooker page in your browser when it starts.
If you use the [mpv plugin](/mpv-plugin), it can also start a texthooker-only helper process and override the texthooker port in `subminer.conf`. ## Subtitle WebSocket
## Developer API Documentation `ws://127.0.0.1:6677`. Use it when you only need the current line as text.
### 1. Subtitle WebSocket The server pushes only; it ignores client messages. On connect it sends the latest subtitle if there is one, then a new message each time the subtitle changes. Reconnecting is up to the client.
Use the basic subtitle websocket when you only need the current subtitle line and a ready-to-render HTML sentence string. ```json
{
"version": 1,
"text": "無事",
"sentence": "無事",
"tokens": []
}
```
- **Default URL:** `ws://127.0.0.1:6677` | Field | Type | Notes |
- **Transport:** local WebSocket server bound to `127.0.0.1` | ---------- | ------ | ------------------------------------------------------------------ |
- **Direction:** server push only | `version` | number | Payload version, currently `1` |
- **Client auth:** none | `text` | string | Raw subtitle text |
- **Reconnects:** client-managed | `sentence` | string | HTML-escaped text with line breaks as `<br>`, no annotation markup |
| `tokens` | array | Always empty on this stream |
When a client connects, SubMiner immediately sends the latest subtitle payload if one is available. After that, it pushes a new message each time the current subtitle changes. ## Annotation WebSocket
#### Message shape `ws://127.0.0.1:6678`. The same token data the bundled texthooker uses. Prefer this stream for new clients. It keeps running when the plain stream is auto-disabled by `mpv_websocket`.
When a line is not yet tokenized, the stream first sends it with an empty `tokens` array, then sends the annotated version when tokenization finishes. Treat each message as the complete current state and replace the previous one. The plain stream does not repeat the line for this upgrade.
```json ```json
{ {
@@ -91,98 +94,48 @@ When a client connects, SubMiner immediately sends the latest subtitle payload i
} }
``` ```
#### Field reference
| Field | Type | Notes |
| --- | --- | --- |
| `version` | number | Current websocket payload version. Today this is `1`. |
| `text` | string | Raw subtitle text. |
| `sentence` | string | HTML string with `<span>` wrappers and `data-*` attributes for client rendering. |
| `tokens` | array | Token metadata; empty when the subtitle is not tokenized yet. |
Each token may include:
| Token field | Type | Notes | | Token field | Type | Notes |
| --- | --- | --- | | --------------------- | ---------------- | ------------------------------------------------------------------------------ |
| `surface` | string | Display text for the token | | `surface` | string | Display text |
| `reading` | string | Kana reading when available | | `reading` | string | Kana reading when available |
| `headword` | string | Dictionary headword when available | | `headword` | string | Dictionary headword when available |
| `startPos` / `endPos` | number | Character offsets in the subtitle text | | `startPos` / `endPos` | number | Character offsets in `text` |
| `partOfSpeech` | string | SubMiner token POS label | | `partOfSpeech` | string | SubMiner part-of-speech label |
| `isMerged` | boolean | Whether this token represents merged content | | `isMerged` | boolean | Token was merged from several parser tokens |
| `isKnown` | boolean | Marked known by SubMiner's known-word logic | | `isKnown` | boolean | Word is known |
| `isNPlusOneTarget` | boolean | True when the token is the sentence's N+1 target | | `isNPlusOneTarget` | boolean | Token is the line's N+1 target |
| `isNameMatch` | boolean | True for prioritized character-name matches | | `isNameMatch` | boolean | Token matched a character name |
| `frequencyRank` | number | Frequency rank when available | | `frequencyRank` | number | Frequency rank; omitted when unavailable or a name match |
| `jlptLevel` | string | JLPT level when available | | `jlptLevel` | string | JLPT level; omitted when unavailable or a name match |
| `className` | string | CSS-ready class list derived from token state | | `className` | string | CSS class list for the token |
| `frequencyRankLabel` | string or `null` | Preformatted rank label for UIs | | `frequencyRankLabel` | string or `null` | Rank label, set only when the rank is within your frequency highlight settings |
| `jlptLevelLabel` | string or `null` | Preformatted JLPT label for UIs | | `jlptLevelLabel` | string or `null` | JLPT label for display |
### 2. Annotation WebSocket ### HTML markup
Use the annotation websocket for custom clients that want the same structured token payload the bundled texthooker UI consumes. `sentence` is HTML rendered by SubMiner. Each token is a `<span>` with these classes as they apply:
- **Default URL:** `ws://127.0.0.1:6678` - `word` on every token
- **Payload shape:** same JSON contract as the basic subtitle websocket - one of `word-name-match`, `word-n-plus-one`, or `word-known`
- **Primary difference:** this stream is intended to stay on even when the basic websocket auto-disables because `mpv_websocket` is installed
In practice, if you are building a new client, prefer `annotationWebsocket` unless you specifically need compatibility with an existing `websocket` consumer.
### 3. HTML markup conventions
The `sentence` field is pre-rendered HTML generated by SubMiner. Depending on token state, it can include classes such as:
- `word`
- `word-known`
- `word-n-plus-one`
- `word-name-match`
- `word-jlpt-n1` through `word-jlpt-n5` - `word-jlpt-n1` through `word-jlpt-n5`
- `word-frequency-single` - `word-frequency-single`, or `word-frequency-band-1` through `word-frequency-band-5`, on words that are not known, N+1, or names
- `word-frequency-band-1` through `word-frequency-band-5`
SubMiner also adds tooltip-friendly data attributes when available: Spans also carry `data-reading`, `data-headword`, `data-frequency-rank`, and `data-jlpt-level` when available. For a fully custom UI, ignore `sentence` and render from `tokens`.
- `data-reading` ## Texthooker page {#texthooker-integration-guide}
- `data-headword`
- `data-frequency-rank`
- `data-jlpt-level`
If you need a fully custom UI, ignore `sentence` and render from `tokens` instead. The bundled texthooker is a browser tab that updates live with the current subtitle, works with browser Yomitan, and uses SubMiner's colors. Start it with the app (`texthooker.launchAtStartup`) or from the launcher:
## Texthooker Integration Guide
### When to use the bundled texthooker page
Use texthooker when you want a browser tab that:
- updates live from current subtitles
- works well with browser-based Yomitan setups
- inherits SubMiner's coloring preferences and websocket URL automatically
Start it with either:
```bash ```bash
subminer texthooker subminer texthooker # start the texthooker
# or open the page immediately subminer texthooker -o # start it and open the browser
subminer texthooker -o
``` ```
or by leaving `texthooker.launchAtStartup` enabled. SubMiner injects the page's settings into `window.localStorage` when it serves it: the WebSocket URL (`bannou-texthooker-websocketUrl`), the known, N+1, name, frequency, and JLPT coloring toggles, and CSS custom properties for the token colors. The page connects to the annotation stream if it is enabled, otherwise to the plain stream. With neither running, it has nothing to connect to.
### What SubMiner injects into the page ## Build a client
When SubMiner serves the local texthooker UI, it injects bootstrap values into `window.localStorage`, including: A minimal browser client for the annotation stream:
- `bannou-texthooker-websocketUrl`
- coloring toggles for known/N+1/name/frequency/JLPT styling
- CSS custom properties for SubMiner's token colors
That means the bundled page already knows which websocket to connect to and which color palette to use.
### Build a custom websocket client
Here is a minimal browser client for the annotation stream:
```html ```html
<!doctype html> <!doctype html>
@@ -204,7 +157,7 @@ Here is a minimal browser client for the annotation stream:
</script> </script>
``` ```
### Build a custom Node client A Node client:
```js ```js
import WebSocket from 'ws'; import WebSocket from 'ws';
@@ -221,107 +174,15 @@ ws.on('message', (raw) => {
}); });
``` ```
### Integration tips Tips:
- Bind only to `127.0.0.1`; these services are local-only by design. - Handle empty `tokens` arrays. Text can arrive before tokenization finishes.
- Handle empty `tokens` arrays gracefully because subtitle text can arrive before tokenization completes. - Reconnect on disconnect yourself.
- Reconnect on disconnect; SubMiner does not manage client reconnects for you. - Use `text` for logging and automation, and `sentence` or `tokens` for display.
- Prefer `payload.text` for logging/automation and `payload.sentence` or `payload.tokens` for UI rendering.
## Plugin Development ### Forward lines to a webhook
SubMiner does **not** currently expose a general-purpose third-party plugin SDK inside the app itself. Today, the supported extension surfaces are: SubMiner does not send webhooks itself. Relay the stream to your own endpoint instead. This example forwards only lines that contain an N+1 target:
1. the local websocket streams
2. the local texthooker UI
3. the mpv Lua plugin's script-message API
4. the launcher CLI
### mpv script messages
The mpv plugin accepts these script messages:
```text
script-message subminer-start
script-message subminer-stop
script-message subminer-toggle
script-message subminer-menu
script-message subminer-options
script-message subminer-restart
script-message subminer-status
script-message subminer-autoplay-ready
script-message subminer-aniskip-refresh
script-message subminer-skip-intro
```
The start command also accepts inline overrides:
```text
script-message subminer-start backend=hyprland socket=/custom/path texthooker=no log-level=debug
```
### Practical extension patterns
#### Add another mpv script that coordinates with SubMiner
Examples:
- send `subminer-start` after your own media-selection script chooses a file
- send `subminer-status` before running follow-up automation
- send `subminer-aniskip-refresh` after you update title/episode metadata
#### Build a launcher wrapper
Examples:
- open a media picker, then call `subminer /path/to/file.mkv`
- launch browser-only subtitle tooling with `subminer texthooker -o`
- disable the helper UI for a session with `subminer --no-texthooker video.mkv`
#### Build an overlay-adjacent client
Examples:
- browser widget showing current subtitle + token breakdown
- local vocabulary capture helper that writes interesting lines to a file
- bridge service that forwards websocket events into your own workflow engine
## Webhook Examples
SubMiner does **not** currently send outbound webhooks by itself. The supported pattern is to consume the websocket locally and relay events into another system.
That still makes webhook-style automation straightforward.
### Example: forward subtitle lines to a local webhook receiver
```js
import WebSocket from 'ws';
const ws = new WebSocket('ws://127.0.0.1:6678');
ws.on('message', async (raw) => {
const payload = JSON.parse(String(raw));
await fetch('http://127.0.0.1:5678/subminer/subtitle', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
text: payload.text,
tokens: payload.tokens,
receivedAt: new Date().toISOString(),
}),
});
});
```
### Automation ideas
- **n8n / Make / Zapier relay:** send each subtitle line into an automation workflow for logging, translation, or summarization.
- **Discord / Slack notifier:** post only lines that contain unknown words or N+1 targets.
- **Obsidian / Markdown capture:** append subtitle lines plus token metadata to a daily immersion note.
- **Local LLM pipeline:** trigger a glossary, translation, or sentence-mining workflow whenever a new line arrives.
### Filtering example: only forward N+1 lines
```js ```js
import WebSocket from 'ws'; import WebSocket from 'ws';
@@ -331,7 +192,6 @@ const ws = new WebSocket('ws://127.0.0.1:6678');
ws.on('message', async (raw) => { ws.on('message', async (raw) => {
const payload = JSON.parse(String(raw)); const payload = JSON.parse(String(raw));
const hasNPlusOne = payload.tokens.some((token) => token.isNPlusOneTarget); const hasNPlusOne = payload.tokens.some((token) => token.isNPlusOneTarget);
if (!hasNPlusOne) return; if (!hasNPlusOne) return;
await fetch('http://127.0.0.1:5678/subminer/n-plus-one', { await fetch('http://127.0.0.1:5678/subminer/n-plus-one', {
@@ -342,18 +202,39 @@ ws.on('message', async (raw) => {
}); });
``` ```
## Recommended Integration Combinations The same pattern works for n8n or Zapier workflows, Discord notifiers, or appending lines to a notes file.
- **Browser Yomitan client:** `texthooker` + `annotationWebsocket` ## mpv script messages
- **Custom dashboard:** `annotationWebsocket` only
- **Lightweight subtitle mirror:** `websocket` only
- **mpv-side automation:** mpv plugin script messages + optional websocket relay
- **Webhook-style workflows:** `annotationWebsocket` + your own local relay service
## Related Pages SubMiner has no in-app plugin SDK. Besides the streams above, you can drive it from other mpv scripts and from the [launcher CLI](/launcher-script).
- [Configuration](/configuration#websocket-server) The mpv plugin accepts these script messages:
- [Mining Workflow — Texthooker](/mining-workflow#texthooker)
- [MPV Plugin](/mpv-plugin) | Message | Action |
- [Launcher Script](/launcher-script) | ----------------------- | ------------------------------------------ |
- [Anki Integration](/anki-integration#proxy-mode-setup-yomitan--texthooker) | `subminer-start` | Start the overlay |
| `subminer-stop` | Stop the overlay |
| `subminer-toggle` | Toggle the visible overlay |
| `subminer-menu` | Open the plugin menu |
| `subminer-options` | Open the SubMiner settings window |
| `subminer-restart` | Restart the overlay |
| `subminer-status` | Show overlay status on the mpv OSD |
| `subminer-stats-toggle` | Show an OSD hint for the overlay stats key |
`subminer-start` accepts overrides for `backend` (`auto`, `hyprland`, `sway`, `x11`, `macos`), `socket`, `texthooker`, and `log-level`:
```text
script-message subminer-start backend=hyprland socket=/custom/path texthooker=no log-level=debug
```
The plugin also registers `subminer-autoplay-ready`, `subminer-autoplay-hold`, `subminer-visible-overlay-shown`, `subminer-visible-overlay-hidden`, `subminer-managed-subtitles-loading`, `subminer-overlay-loading-ready`, and `subminer-reload-session-bindings`. The SubMiner app sends these to keep the plugin in sync, so do not send them from your own scripts.
While the app is connected to mpv, it also handles two AniSkip messages over the mpv IPC socket: `subminer-skip-intro` skips the intro, and `subminer-aniskip-refresh` reloads intro data, for example after your script changes title or episode metadata.
## Related pages
- [Configuration](/configuration)
- [Mining workflow](/mining-workflow)
- [mpv plugin](/mpv-plugin)
- [Launcher script](/launcher-script)
- [Anki integration](/anki-integration)
+65 -110
View File
@@ -1,145 +1,100 @@
# YouTube Integration # YouTube integration
SubMiner auto-loads Japanese subtitles when you play a YouTube URL, giving you the same sentence-mining overlay experience as local video files. It probes available subtitle tracks via `yt-dlp`, selects the best primary and secondary tracks, downloads them, and loads them into mpv before playback resumes. Play a YouTube URL and SubMiner downloads its Japanese subtitles and loads them into mpv, so you can mine from it like a local file.
## Requirements ## Setup
- **yt-dlp** must be installed and on `PATH` (or set `SUBMINER_YTDLP_BIN` to its path) Install [yt-dlp](https://github.com/yt-dlp/yt-dlp) and make sure it is on your `PATH`. If it is somewhere else, set `SUBMINER_YTDLP_BIN` to the full path of the binary.
- mpv with `--input-ipc-server` configured (handled automatically by the `subminer` launcher)
## How It Works ## Usage
When SubMiner detects a YouTube URL (or `ytsearch:` target), it pauses mpv at startup and runs a subtitle pipeline before resuming playback: ```bash
subminer https://www.youtube.com/watch?v=VIDEO_ID
1. **Probe** --- `yt-dlp --dump-single-json` extracts all available subtitle tracks (manual uploads and auto-generated captions) along with video metadata. subminer ytsearch:"keyword" # plays the first search result
2. **Discover** --- Each track is normalized into a `YoutubeTrackOption` with language code, kind (`manual` or `auto`), display label, and direct download URL.
3. **Select** --- SubMiner picks the best primary track (Japanese, preferring manual over auto) and secondary track (English, preferring manual over auto).
4. **Download** --- Selected tracks are fetched via direct URL when available, falling back to `yt-dlp --write-subs` / `--write-auto-subs`. YouTube TimedText XML formats (`srv1`/`srv2`/`srv3`) are converted to VTT on the fly. Auto-generated VTT captions are normalized to remove rolling-caption duplication.
5. **Load** --- Subtitle files are injected into mpv via `sub-add`. Playback resumes once the primary track is ready; secondary failures do not block.
## Pipeline Diagram
```mermaid
flowchart TD
classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a
classDef action fill:#8aadf4,stroke:#494d64,color:#24273a
classDef result fill:#a6da95,stroke:#494d64,color:#24273a
classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a
classDef ext fill:#eed49f,stroke:#494d64,color:#24273a
A[YouTube URL detected]:::step
B[yt-dlp probe]:::ext
C[Track discovery]:::action
D{Auto or manual selection?}:::step
E[Auto-select best tracks]:::action
F[Manual picker — Ctrl+Alt+C]:::action
G[Download subtitle files]:::action
H[Convert TimedText to VTT]:::enrich
I[Normalize auto-caption duplicates]:::enrich
K[sub-add into mpv]:::action
L[Overlay renders subtitles]:::result
A --> B
B --> C
C --> D
D -- startup --> E
D -- user request --> F
E --> G
F --> G
G --> H
H --> I
I --> K
K --> L
``` ```
## Auto-Load Flow mpv starts paused while SubMiner fetches the subtitle list. It picks a primary and a secondary track, loads them, and resumes playback once the primary track is ready. A playlist link plays only the linked video.
On startup with a YouTube URL: SubMiner picks tracks in this order. Manual (uploaded) tracks win over auto-generated captions.
1. mpv launches paused. | Track | Choice |
2. SubMiner calls `yt-dlp --dump-single-json` to probe all subtitle tracks. | --------- | -------------------------------------------------------------------------------- |
3. Tracks are split into **manual** (human-uploaded) and **auto** (machine-generated) categories. | Primary | Japanese manual, then Japanese auto, then any manual track, then the first track |
4. The selection algorithm picks: | Secondary | English manual, then English auto. Skipped if none exists. |
- **Primary**: first Japanese manual track, then Japanese auto track, then any manual track, then first available track.
- **Secondary**: first English manual track, then English auto track (excluding the primary).
5. If mpv already exposes an authoritative matching track, SubMiner reuses it instead of downloading again.
6. Missing tracks are downloaded to a temp directory and loaded via `sub-add`.
7. Playback unpauses once the primary subtitle is ready.
## Manual Subtitle Picker Press `Ctrl+Alt+C` during playback to open the subtitle picker. It lists every track with its language and kind, and lets you choose different primary and secondary tracks or retry a failed load.
Press **Ctrl+Alt+C** during YouTube playback to open the subtitle picker overlay. This lets you: ## Generate subtitles with Whisper
- Browse all discovered tracks (manual and auto-generated) SubMiner can transcribe a YouTube video's audio on your computer. For one video, press `Ctrl+Shift+G` and click **Generate subtitles**. To always use Whisper instead of YouTube's captions, set `youtube.subtitleSource` to `whisper`, or choose **Generate with Whisper** under **Settings > Behavior > YouTube Playback Settings**. The change applies to the next video.
- Select different primary and secondary tracks
- Retry track loading if the auto-load failed or picked the wrong track
The picker displays each track with its language, kind (manual/auto), and title when available. ```jsonc
{
"youtube": { "subtitleSource": "whisper" },
}
```
## Subtitle Format Handling Whisper uses the model and tools from [Japanese subtitle generation](/subtitle-generation), so set those up first.
SubMiner handles several YouTube subtitle formats transparently: - The video stays paused while the subtitles are generated, and the subtitle generation modal shows the progress. Close the modal to keep watching while generation continues. **Cancel** stops it and resumes the video without subtitles.
- The subtitles load and playback resumes as soon as they are ready.
- SubMiner downloads the smallest audio stream of at least 48 kbps, in the video's original language, never an auto-dub.
- The audio is deleted as soon as generation finishes or fails, and when you switch to another video, close mpv, or quit SubMiner. Switching videos also stops the generation.
- No secondary subtitles load. The picker (`Ctrl+Alt+C`) still loads YouTube tracks by hand.
| Format | Handling | ## Browse YouTube in SubMiner
| ------ | -------- |
| `srt`, `vtt` | Used directly (preferred for manual tracks) |
| `srv1`, `srv2`, `srv3` | YouTube TimedText XML --- converted to VTT automatically |
| Auto-generated VTT | Normalized to remove rolling-caption text duplication |
For auto-generated tracks, SubMiner prefers `srv3` > `srv2` > `srv1` > `vtt` (TimedText XML produces cleaner output). For manual tracks, `srt` > `vtt` is preferred. Open a YouTube window with `subminer youtube` (or `subminer yt`), or from the tray (**Browse YouTube**). Sign in once and the login is kept across restarts. Videos you pick play in mpv with the same subtitle setup as above, and SubMiner starts mpv if it is not running.
## Configuration Reference When SubMiner was started by `subminer youtube`, closing the window quits it. If a video is still playing, SubMiner quits when you close mpv instead.
### Primary Subtitle Languages | Action | Result |
| ------------------------------------------------------------ | ----------------------------------- |
| Click a video | Play it now |
| Middle-click, or `Shift`/`Ctrl`+click | Add it to the end of mpv's playlist |
| Right-click a video | **Play in mpv** or **Queue in mpv** |
| `Alt+Left` / `Alt+Right`, mouse back/forward, or right-click | Go back or forward |
Queued videos get their subtitles loaded when mpv reaches them. Open the queue with the playlist browser (`Ctrl+Alt+P`) to reorder or skip entries.
If Google refuses the sign-in, try again once. The window uses a standard Chrome user agent, but Google can still block embedded browsers.
## Secondary subtitle languages
YouTube secondary selection is fixed to English. `secondarySub.secondarySubLanguages` and `secondarySub.autoLoadSecondarySub` apply only to local files and Jellyfin. `secondarySub.defaultMode` still controls how the secondary bar is shown. Use the picker to load a different secondary language.
Likewise, `youtube.primarySubLanguages` does not change which YouTube track is picked. It sets which languages count as a primary subtitle for local and playlist subtitle selection and for the "primary subtitle missing" notification.
## Card media
By default, card audio and screenshots are cut from mpv's live YouTube stream. If card media fails with `403` errors, switch to the background cache:
```jsonc ```jsonc
{ {
"youtube": { "youtube": {
"primarySubLanguages": ["ja", "jpn"] "mediaCache": { "mode": "background" },
} },
} }
``` ```
| Option | Type | Description | In background mode, SubMiner downloads the video with yt-dlp after playback starts. Cards you mine get their text fields right away, and audio and images are added once the download finishes. `youtube.mediaCache.maxHeight` caps the download resolution (`0` for no limit). If the download fails, SubMiner tells you and drops the pending media updates.
| ------ | ---- | ----------- |
| `primarySubLanguages` | `string[]` | Language priority for YouTube primary subtitle auto-loading (default `["ja", "jpn"]`) |
### Secondary Subtitle Languages See [Configuration](/configuration#youtube-playback-settings) for all `youtube` options and defaults.
Secondary track selection uses the shared `secondarySub` config: ## Troubleshooting
```jsonc **No Japanese subtitles.** The video may not have any. Open the picker with `Ctrl+Alt+C` to see what is available.
{
"secondarySub": {
"secondarySubLanguages": ["eng", "en"],
"autoLoadSecondarySub": true,
"defaultMode": "hover"
}
}
```
| Option | Type | Description | **yt-dlp not found.** Install it and put it on `PATH`, or set `SUBMINER_YTDLP_BIN`.
| ------ | ---- | ----------- |
| `secondarySubLanguages` | `string[]` | Language codes for secondary subtitle auto-loading (default: English) |
| `autoLoadSecondarySub` | `boolean` | Auto-detect and load matching secondary track |
| `defaultMode` | `"hidden"` / `"visible"` / `"hover"` | Initial display mode for secondary subtitles (default: `"hover"`) |
Precedence: CLI flag > environment variable > `config.jsonc` > built-in default. **Timeouts.** Each yt-dlp call times out after 15 seconds. Slow or rate-limited connections can hit this. Retry, or update yt-dlp.
## Limitations and Troubleshooting **Poor subtitle quality.** Auto-generated captions are often inaccurate. SubMiner uses a manual track when one exists.
- **No subtitles found**: The video may not have Japanese subtitles. Open the picker with `Ctrl+Alt+C` to see all available tracks. **Subtitles fail with HTTP 429.** YouTube is refusing caption requests from your network. It can last hours or days, and waiting or signing in does not always help. [Generate them with Whisper](#generate-subtitles-with-whisper) instead.
- **yt-dlp not found**: Install `yt-dlp` and ensure it is on `PATH`, or set `SUBMINER_YTDLP_BIN` to the binary path.
- **Probe timeout**: `yt-dlp` has a 15-second timeout per operation. Slow connections or rate-limited IPs may hit this. Retry or update `yt-dlp`.
- **Auto-caption quality**: YouTube auto-generated captions vary in quality. Manual subtitles (when available) are always preferred.
- **`ytsearch:` targets**: `subminer ytsearch:"keyword"` plays the first search result. Subtitle availability depends on the matched video.
- **Secondary subtitle fails**: Secondary track failures never block playback. The primary subtitle loads independently.
- **Native mpv secondary rendering**: Stays hidden during YouTube flows so the SubMiner overlay remains the visible secondary subtitle surface.
## Related Pages A missing or failed secondary track never blocks playback.
- [Usage --- YouTube Playback](/usage#youtube-playback) ## Stats
- [Configuration --- YouTube Playback Settings](/configuration#youtube-playback-settings)
- [Configuration --- Secondary Subtitle](/configuration#secondary-subtitle) The stats Library groups YouTube videos by channel. Choose **YouTube** in the Library filter to see them. See [Immersion tracking](/immersion-tracking).
- [Keyboard Shortcuts](/shortcuts)
- [Jellyfin Integration](/jellyfin-integration)
+1 -2
View File
@@ -3,7 +3,7 @@
# SubMiner Internal Docs # SubMiner Internal Docs
Status: active Status: active
Last verified: 2026-03-13 Last verified: 2026-05-23
Owner: Kyle Yasuda Owner: Kyle Yasuda
Read when: you need internal architecture, workflow, verification, or release guidance Read when: you need internal architecture, workflow, verification, or release guidance
@@ -15,7 +15,6 @@ Read when: you need internal architecture, workflow, verification, or release gu
- [Workflow](./workflow/README.md) - planning, execution, verification expectations - [Workflow](./workflow/README.md) - planning, execution, verification expectations
- [Knowledge Base](./knowledge-base/README.md) - how docs are structured, maintained, and audited - [Knowledge Base](./knowledge-base/README.md) - how docs are structured, maintained, and audited
- [Release Guide](./RELEASING.md) - tagged release checklist - [Release Guide](./RELEASING.md) - tagged release checklist
- [Plans](./plans/) - active design and implementation artifacts
## Fast Paths ## Fast Paths
+87 -5
View File
@@ -6,11 +6,46 @@
- `claude` (Claude Code CLI) installed, on `PATH`, and authenticated. - `claude` (Claude Code CLI) installed, on `PATH`, and authenticated.
`changelog:build` and `changelog:prerelease-notes` invoke `changelog:build` and `changelog:prerelease-notes` invoke
`claude -p --model sonnet` to merge and rewrite `changes/*.md` fragments into `claude -p --model opus --effort medium` to merge and rewrite `changes/*.md` fragments into
a polished, user-facing release body. Either OAuth login (`claude /login`) or a polished, user-facing release body. Either OAuth login (`claude /login`) or
`ANTHROPIC_API_KEY` works. Install from <https://claude.com/claude-code> if `ANTHROPIC_API_KEY` works. Install from <https://claude.com/claude-code> if
you don't already have it. you don't already have it.
## Package contents checks
Stable and prerelease workflows share `.github/workflows/package-release.yml`.
Both callers explicitly pass the five required macOS signing/notarization
secrets plus the optional `SUBMINER_TMDB_API_KEY` (the project TMDB key that
`scripts/prepare-build-assets.mjs` stages into `dist/bundled-integration-keys.json`;
artifacts built without it simply require users to set `tmdb.apiKey`).
`GITHUB_TOKEN` remains automatically available to the reusable workflow.
Each platform verifies its ASAR and external resources before signing. Missing
runtime assets, foreign SQLite/Koffi binaries, duplicate UI fonts, demo media,
source maps, TypeScript files, and nested test or fixture directories fail the build.
Current targets are Linux x64, macOS arm64, and Windows x64.
The runtime allowlist includes `dist/`, `stats/dist/`, and
`vendor/texthooker-ui/docs/` plus metadata, config example, and license. The
texthooker `docs/` directory is its built UI. Keep the positive `package.json`
pattern in platform `files` lists: electron-builder otherwise treats an
exclusion-only platform list as a separate include-all matcher. Windows keeps
only its target Koffi binary; other platforms omit Koffi. Desktop UIs share the
original M PLUS 1 TTF in `dist/fonts/`.
The shared workflow runs `bun run test:package <resources-directory>` with the
pinned Electron runtime and temporary user data. On headless Linux, prefix it
with `xvfb-run -a`. This checks packaged SQLite, Windows FFI loading/polling,
texthooker serving, Yomitan loading, UI assets, and Japanese font loading.
Standalone pages lack app IPC handlers and can log related errors; this check
does not replace an installed app session.
Before shipping packaging changes, check each platform's installed app:
startup and mpv tracking, dictionary lookup and stroke orders, settings/sync UI,
stats persistence, sentence mining with AnkiConnect, and updating from the prior
release. Preserve Electron locales, graphics fallbacks, codecs, dictionaries,
license notices, updater metadata, blockmaps, and the macOS updater ZIP. Trim
files before signing and generating updater hashes, never from a signed app.
## Stable Release ## Stable Release
1. Confirm `main` is green: `gh run list --workflow CI --limit 5`. 1. Confirm `main` is green: `gh run list --workflow CI --limit 5`.
@@ -31,9 +66,21 @@
`bun run test:fast` `bun run test:fast`
`bun run test:env` `bun run test:env`
`bun run build` `bun run build`
Confirm `dist/launcher` contains only `subminer`, `subminer.cmd`,
`subminer.js`, `prepare.cjs`, and `version`. Release CI smoke-tests
`subminer.js` with Bun, then publishes both wrapper files and their checksums.
Tagged CI runs `bun scripts/package-bun-source.mjs` and must publish
`bun-v<version>-source.tar.gz` plus its `.sha256` file. The script
fails if Bun's CMake dependency pins, WebKit pin, source checksums, patch
inputs, or collected license files differ from
`build/bun-source-manifest.json`.
When validating auto-update metadata, also run the relevant platform package
build and confirm `release/` contains the generated updater metadata
(`latest*.yml`) and blockmaps (`*.blockmap`).
8. If `docs-site/` changed, also run: 8. If `docs-site/` changed, also run:
`bun run docs:test` `bun run docs:test`
`bun run docs:build` `bun run docs:build`
`bun run docs:build:versioned`
9. Commit release prep. 9. Commit release prep.
10. Tag the commit: `git tag v<version>`. 10. Tag the commit: `git tag v<version>`.
11. Push commit + tag. 11. Push commit + tag.
@@ -50,11 +97,30 @@
`bun run test:fast` `bun run test:fast`
`bun run test:env` `bun run test:env`
`bun run build` `bun run build`
5. Commit the prerelease prep. Do not run `bun run changelog:build`. Confirm both launcher wrappers and their checksums are included in the
prerelease assets.
Prerelease CI also assembles and publishes the pinned Bun corresponding
source archive. A missing source repository or license file fails the
release instead of publishing only the executable.
When validating packaged updater output, confirm the platform build writes
`latest*.yml` and `*.blockmap` files under `release/`.
5. Commit the prerelease prep (package.json version bump + the generated
`release/prerelease-notes.md`). CI does not regenerate notes — it uses the
committed file — so review it before committing. Rerun
`bun run changelog:prerelease-notes --version <version>` for every later
beta/RC, even if no fragments changed: the notes carry a hidden
`prerelease-version` marker and CI rejects the tag when the marker does not
match it (verify locally with
`bun run changelog:check-prerelease-notes --version <version>`). The
generator reuses the existing notes as the cumulative baseline when their
marker (or legacy `prerelease-base-version` marker) matches the current base
version, and asks Claude to merge only the new fragment material. Do not run
`bun run changelog:build`.
6. Tag the commit: `git tag v<version>`. 6. Tag the commit: `git tag v<version>`.
7. Push commit + tag. 7. Push commit + tag.
Prerelease tags publish a GitHub prerelease only. They do not update `CHANGELOG.md`, `docs-site/changelog.md`, or the AUR package, and they do not consume `changes/*.md` fragments. The final stable release is still the point where `bun run changelog:build` consumes fragments into `CHANGELOG.md` and regenerates stable release notes. Prerelease tags publish a GitHub prerelease only. They do not update `CHANGELOG.md`, `docs-site/changelog.md`, or the AUR package, and they do not consume `changes/*.md` fragments. The final stable release is still the point where `bun run changelog:build` consumes fragments into `CHANGELOG.md` and regenerates stable release notes.
Prerelease tags also do not update `https://docs.subminer.moe/`.
Notes: Notes:
@@ -62,13 +128,29 @@ Notes:
- Supported prerelease channels are `beta` and `rc`, with versions like `0.11.3-beta.1` and `0.11.3-rc.1`. - Supported prerelease channels are `beta` and `rc`, with versions like `0.11.3-beta.1` and `0.11.3-rc.1`.
- Pass `--date` explicitly when you want the release stamped with the local cut date; otherwise the generator uses the current ISO date, which can roll over to the next UTC day late at night. - Pass `--date` explicitly when you want the release stamped with the local cut date; otherwise the generator uses the current ISO date, which can roll over to the next UTC day late at night.
- `changelog:check` now rejects tag/package version mismatches. - `changelog:check` now rejects tag/package version mismatches.
- `changelog:prerelease-notes` also rejects tag/package version mismatches and writes `release/prerelease-notes.md` without mutating tracked changelog files. - `changelog:prerelease-notes` also rejects tag/package version mismatches and writes `release/prerelease-notes.md` without mutating tracked changelog files. When that file already exists, the generator includes it in the Claude prompt so later beta/RC notes reuse the reviewed text instead of starting over.
- From the second prerelease of a base version onward, the notes open with a `## Changes since <previous tag>` section above the cumulative `## Highlights`. The generator locates the newest preceding beta/RC tag for the same base version (semver order: all betas before all RCs), diffs `changes/*.md` between that tag and the working tree, and asks Claude to describe only the behavioral beta-to-beta differences — added fragments as new changes, modified fragments by their before/after difference (editorial-only edits are dropped), deleted fragments as removed/reverted changes. If no fragments changed (for example a packaging-only rebuild), the section states that explicitly without a Claude call. The delta section carries no separate contributor attribution; `## What's Changed` stays cumulative like `## Highlights`.
- `changelog:check-prerelease-notes --version <version>` verifies the committed notes' `prerelease-version` marker matches the version being tagged; the prerelease workflow runs it and fails the release on stale notes.
- `changelog:build` generates `CHANGELOG.md` + `release/release-notes.md` (both polished by `claude -p`) and removes the released `changes/*.md` fragments. The CHANGELOG keeps internal notes inside a `<details><summary>Internal changes</summary>` collapse; the release notes drop them entirely. - `changelog:build` generates `CHANGELOG.md` + `release/release-notes.md` (both polished by `claude -p`) and removes the released `changes/*.md` fragments. The CHANGELOG keeps internal notes inside a `<details><summary>Internal changes</summary>` collapse; the release notes drop them entirely.
- `release/release-notes.md` (and `release/prerelease-notes.md`) include GitHub-style attribution after `## Highlights`: a `## What's Changed` list crediting each released fragment as `by @<author> in #<pr>`, plus a `## New Contributors` section for first-time authors. Attribution is resolved per fragment via `git log` (the commit that added the fragment) + `gh api .../commits/<sha>/pulls`, with one `gh` search per author for the first-contribution check. It needs `gh` installed and authenticated; if `gh` is unavailable or a lookup fails, the generator warns and emits notes without the attribution sections rather than failing. The CHANGELOG itself stays attribution-free.
- The release workflow no longer auto-runs `changelog:build`. If pending `changes/*.md` fragments are present on a tag-based run, CI exits with a clear `::error::` pointing at the local fix. Run `bun run changelog:build --version <version>` locally, commit the polished output, then tag. - The release workflow no longer auto-runs `changelog:build`. If pending `changes/*.md` fragments are present on a tag-based run, CI exits with a clear `::error::` pointing at the local fix. Run `bun run changelog:build --version <version>` locally, commit the polished output, then tag.
- Do not tag while `changes/*.md` fragments still exist. - Do not tag while `changes/*.md` fragments still exist.
- Prerelease tags intentionally keep `changes/*.md` fragments in place so multiple prereleases can reuse the same cumulative pending notes until the final stable cut. - Prerelease tags intentionally keep `changes/*.md` fragments in place so multiple prereleases can reuse the same cumulative pending notes until the final stable cut. `make clean` preserves `release/prerelease-notes.md` while deleting generated build artifacts.
- If you need to repair a published release body (for example, a prior version’s section was omitted), regenerate notes from `CHANGELOG.md` and re-edit the release with `gh release edit --notes-file`. - If you need to repair a published release body (for example, a prior version’s section was omitted), regenerate notes from `CHANGELOG.md` and re-edit the release with `gh release edit --notes-file`.
- Prerelease tags are handled by `.github/workflows/prerelease.yml`, which always publishes a GitHub prerelease with all current release platforms and never runs the AUR sync job. - Prerelease tags are handled by `.github/workflows/prerelease.yml`, which always publishes a GitHub prerelease with all current release platforms and never runs the AUR sync job.
- CI, stable release, and prerelease workflows call the shared `.github/workflows/quality-gate.yml`; update that reusable workflow when changing common test coverage. Its environment suite includes the Lua mpv plugin tests.
- Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication. - Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication.
- AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed. - Stable release tags update `https://docs.subminer.moe/` and `https://docs.subminer.moe/v/<version>/` through `.github/workflows/docs-pages.yml`; `/main/` continues to show development docs from `main`.
- Keep Cloudflare Pages Git auto-deploy disabled for `docs.subminer.moe`. Production docs are direct-uploaded by Wrangler from GitHub Actions with `--branch main`.
- AUR publish is best-effort: the workflow downloads the three known assets directly from the tagged release URLs, avoiding GitHub's sometimes-stale release asset listing. Downloads and SSH clone/push operations retry transient failures, then warn and skip AUR publication if retries are exhausted. Follow up with a manual `git push aur master` from the AUR checkout when needed.
- Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation. - Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation.
- Release and prerelease workflows upload updater metadata (`latest*.yml`) and blockmaps (`*.blockmap`) alongside platform artifacts. Do not remove those files while `electron-updater` is enabled.
- Release and prerelease workflows publish `subminer` for POSIX systems and `subminer.cmd` for Windows. Both locate a packaged app and use its private Bun runtime. Keep the corresponding-source archive named `bun-v1.3.5-source.tar.gz`.
- macOS tray app updates use the standard `electron-updater`/Squirrel path. Keep `latest-mac.yml`, the macOS `SubMiner-<version>-mac.zip`, and ZIP blockmap published; Squirrel uses the ZIP payload even when the DMG remains the user-facing installer.
- macOS update metadata and full ZIP downloads are routed through `/usr/bin/curl` before Squirrel installation to avoid Electron main-process network crashes on update checks.
- Windows tray app updates use the standard `electron-updater`/NSIS path. Keep `latest.yml`, the Windows NSIS installer, and installer blockmap published; updater HTTP is routed through main-process fetch to avoid Electron main-process network crashes during update checks.
- Build config emits distinct ZIP names: `SubMiner-<version>-mac.zip` for the macOS Squirrel updater payload and `SubMiner-<version>-win.zip` for the Windows portable fallback. The user-facing DMG and Windows installer keep the unqualified `SubMiner-<version>` basename.
- Linux GitHub release metadata and asset downloads also use `/usr/bin/curl` instead of Electron networking for the same reason.
- Local macOS build-output apps outside `/Applications` or `~/Applications` skip native update checks. Manual tray and launcher checks still use GitHub release metadata to report newer releases, but automatic notifications stay quiet when native app installation is unsupported. To validate auto-update end to end, install the signed and notarized app bundle into one of those Applications folders and point it at a published updater feed.
- The first updater-enabled release cannot update older installs automatically. Users need one manual install to get the updater code.
- Stable auto-update checks ignore beta/RC prereleases by default. Set `updates.channel` to `"prerelease"` on a test install when validating beta/RC updater behavior.
Loaded 100 of 1459 files, more files were not shown because too many files have changed in this diff. Show more