Compare commits

..
Author SHA1 Message Date
sudacode 4209134399 feat(subtitles): optimize anime stream audio extraction
- Prefer matching audio and lower-resolution sources with HLS sampling
- Reuse verified audio within sessions and clean temporary files on shutdown
- Allow character dictionary sync without loaded subtitles
2026-09-22 00:20:54 -07:00
sudacode 2c89bc1754 feat(subtitles): generate subtitles from anime streams
- Support finite HTTP/HTTPS streams with cached generated SRT files
- Document stream requirements and limitations
2026-09-21 21:28:15 -07:00
sudacode 625f1ad238 feat(stats): add live-action library support and harden stats runtime
- Add TMDB metadata, linking, filtering, and live-action library merging
- Harden stats request validation, lifecycle handling, and compiled runtime coverage
- Fix Anki media synchronization and configuration validation
2026-09-21 00:18:32 -07:00
sudacode d1aa279830 feat: expand subtitle and media tracking workflows
- Add reference-guided subtitle timing, frame picking, live-action Jimaku search, and YouTube library kinds
- Harden Jellyfin media identity handling and live settings feedback
- Refresh user-facing documentation and changelog fragments
2026-09-20 20:10:03 -07:00
sudacode ca40cd6267 fix(anime): clarify bridge failures and update guidance
- Show actionable bridge errors and preserve episodes when details fail
- Check external bridges for updates without offering unsafe in-app installs
- Complete cleanup and ignore callbacks from closed Tsukihime sessions
2026-09-16 23:25:21 -07:00
sudacode 93fb4eff3a feat: add subtitle generation and bundle Bun launcher runtime
- Add local subtitle generation and card timing review workflows
- Package cross-platform Bun runtimes, launchers, licenses, and source
- Consolidate release packaging and refresh v0.19.6 documentation
2026-09-15 21:59:10 -07:00
sudacode 01fcacb6cf test(anime): synchronize stale Jimaku open test 2026-09-03 01:40:28 -07:00
sudacode cd7a65ec80 fix(anime): wait for mpv window before opening Jimaku
- Hide the standalone browser during the Jimaku handoff
- Skip stale or windowless playback flows safely
2026-09-03 01:03:16 -07:00
sudacode 89479f7045 feat(anime): set default source from Extensions tab
- Add default controls for installed sources and All sources
- Expose source metadata for extension rows
2026-09-02 23:50:11 -07:00
sudacode 7bc9a07a52 fix(anime): rescan runtime after preference cleanup failures
- Keep extension removal reflected in the runtime when preference cleanup fails
- Clarify auto-open playback resume behavior in configuration docs
2026-09-02 23:28:32 -07:00
sudacode 68dd789fbf feat(anime): remember the default Anime Browser source
- Add `anime.defaultSource` with installed-source fallback
- Let the source picker save a source or All sources as default
2026-09-02 23:17:20 -07:00
sudacode 516fadd530 feat(anime): auto-open Jimaku for Anime Browser playback
- Add hot-reloadable `anime.autoOpenJimaku` setting
- Pause and resume playback around subtitle selection
- Brand Anime Browser surfaces with the SubMiner logo
2026-09-02 23:04:39 -07:00
sudacode 18beac13f4 feat(anime): add version-aware extension updates
- Compare installed APK version codes before offering updates
- Add update status labels and an Update all action
2026-09-02 19:10:03 -07:00
sudacode 484a9e047d fix(immersion): drain writes after finalizing tracker shutdown
- Ensure queued telemetry and events persist during destroy
- Restore mocked flush methods in queue-drain tests
2026-09-02 12:59:25 -07:00
sudacode 0d66747f2e fix(immersion): await lexical rollup shutdown during app quit
- Rebuild lifetime completion after stream watch-state changes
- Update release packaging metadata and fast-uri
2026-09-02 10:51:20 -07:00
sudacode b01f0bcb4f feat(release): prepare v0.19.5 with subtitle pipeline improvements
- Rework ASS, YouTube, overlapping, and secondary subtitle handling
- Keep overlay, mining, sidebar, and stats aligned with visible cues
- Harden release-note validation and package Linux thumbnail support
2026-09-02 09:27:06 -07:00
sudacode 9a1b6e650f fix(anime): restore bridge install after validation failure
- Preserve the previous install when post-swap validation fails
- Cover rollback behavior with an installer regression test
2026-09-02 01:06:08 -07:00
sudacode 835a09fa67 fix(anime): harden bridge updates and startup lifecycle
- Wait for in-flight starts and stop sidecars before replacement
- Restore the previous bundle when activation fails
- Clarify unchecked managed bridge status and bump the AUR package release
2026-09-02 00:40:58 -07:00
sudacode 2bcb6c7e98 feat(anime): support shared bridge installs and updates
- Prefer configured or package-managed bridge bundles before downloading
- Select compatible upstream releases and offer managed-bundle updates
- Document bridge configuration and package dependencies
2026-09-01 23:14:16 -07:00
sudacode 1d1575f414 fix(anime): preserve HLS playlist rewriting and fetch timeouts
- Force identity encoding for upstream playlist responses
- Keep inactivity timeouts active while streaming response bodies
2026-08-16 14:20:08 -07:00
sudacode f6993ae507 fix(anime): harden stream proxy and session lifecycle
- Reject non-origin-form proxy targets
- Release stale anime browser sessions when senders move
2026-08-16 01:46:16 -07:00
sudacode 4300517da7 fix(anime): harden browser playback and modal lifecycle
- Abort upstream fetches when clients disconnect
- Validate embedded modal readiness before opening
- Centralize browser IPC and window lifecycle
2026-08-16 01:21:07 -07:00
sudacode 877f350353 fix(anime): keep browser state in sync and alias HLS segments
- Toggle the player Anime Browser without losing its state
- Share active playback state and support rotating fake segment extensions
2026-08-15 23:35:37 -07:00
sudacode 7a1450ddb7 feat(anime): open Anime Browser in player modal
- Add the Ctrl+Alt+A shortcut and dedicated overlay modal
- Share queue, sources, and watch history while isolating browser state
2026-08-15 21:44:52 -07:00
sudacode cc43d3007a fix(anime): alias disguised HLS segments for ffmpeg
- Expose local `.ts` aliases for `.image` MPEG-TS segments
- Cover proxy rewriting and upstream alias removal with tests
2026-08-15 21:44:51 -07:00
sudacode 333ee5eea4 fix(anime): clean up discarded queued subtitles in background
- Let queued episode discard return before subtitle preparation finishes
- Remove the redundant queued subtitle loading command
2026-08-15 21:44:51 -07:00
sudacode 935e4c145f feat(anime): append queued episodes to mpv playlist
- Resolve queued streams immediately and cache subtitles in the background
- Attach prepared tracks as mpv advances through the playlist
2026-08-15 21:44:51 -07:00
sudacode 183560d2c3 fix(macos): bring app forward when opening settings/sync/anime windows
- add activateMacOSApp to steal app focus before window.focus(), since show()/focus() only reorder windows within an already-active app
- wire activateApp through config-settings-window/runtime into the settings, sync, and anime browser window handlers
- restore the anime browser's Dock icon before showing the window instead of after, since an accessory process cannot become frontmost
2026-08-15 21:44:51 -07:00
sudacode 3ca7dcd664 feat(anime): queue episodes to play next across anime
- Episode rows gain Play/Queue actions (and matching context-menu items); queued rows show their place in line, with a queue count and Clear queue in the episode header
- Queue lives in the main process (`anime-browser-queue.ts`) so it survives the browser window closing and advances on mpv's end-file even when nobody is watching; streams resolve at play time so a signed URL cannot expire while queued
- Holds mpv's keep-open off while the queue waits and restores it once empty; queueing with nothing playing just plays immediately
- Adds anime-browser-queue and episode-queue unit tests, IPC channels/contracts, and doc updates
2026-08-15 21:44:51 -07:00
sudacode 51fc9034f7 fix(subsync): decode file:// tracks mpv reports for dropped subtitles
- Sync a subtitle dropped onto mpv without hitting "Protocol file: not supported"; decode file:// external-filename/path back to a real path for target, reference, and video
- Stream strip proxy: destroy the connection instead of retrying/502 once a response is handed off, and cap buffered playlist bodies
- Bridge installer: cap downloaded bundle size to guard against a lying/missing content-length
- Anime browser playback: bump generation on dispose so a stale in-flight playEpisode cleans up its own subtitle cache
- Fix a flaky sidecar-process test by binding to an OS-assigned port instead of pre-allocating one
2026-08-15 21:44:51 -07:00
sudacode 8c22567da9 feat(anime): show extension and repo icons in extensions panel
- Add extension-icons module (buildIconIndex, isSafeIconUrl, repoFaviconUrl, iconMonogram) with tests
- Render each row's icon with a monogram fallback while loading or missing
- Installed extensions borrow their icon from the catalogue; repo rows use the index host's favicon
- Restrict icon loads to https URLs
- Thread iconUrl through AvailableExtension and the runtime, document the feature
2026-08-15 21:44:51 -07:00
sudacode b22001f68e fix(anime): isolate mark-watched requests and batch marks atomically
- Track mark writes with their own LatestRequest so a concurrent watch-state
  refresh can't supersede them, and re-read the store after a write to settle
  stale repaints
- Wrap batch markEpisodesWatched in a single transaction so partial rows and
  their lifetime summary rebuild can't land separately; defer clearing the
  active session's markedWatched flag until after commit
2026-08-15 21:44:51 -07:00
sudacode 8d55d64ee0 feat(anime): filter episodes and mark them watched by hand
- Add a filter box above the episode list for a number, range, or name substring, with a "N of M" counter
- Read watch marks from the immersion tracker stats and show them per episode, with a watched count in the header, refreshed on window focus
- Add a right-click menu to mark one episode or a whole catch-up span (this and everything below) watched/unwatched
- Add setWatched/getWatchState IPC plumbing so a manual mark creates the stats row for an episode that was never played
2026-08-15 21:44:51 -07:00
sudacode 3dd0750b98 fix(anime): stop superseded playEpisode calls from touching mpv
- Extend the generation guard from subtitleCacheDir to the whole
  playback: a stale playEpisode call now bails out before sending mpv
  commands, showing the overlay, or reading a playback outcome that
  belongs to a newer episode
- Add a regression test where a slow-resolving first call must not
  drive mpv after a second call for a newer episode has already
  started playing
2026-08-15 21:44:51 -07:00
sudacode 8aff2ae505 fix(anime): guard playback races and harden install/extract tests
- Tag subtitle cache writes with a generation counter so overlapping playEpisode calls can't clobber the shared cache dir, and don't fail playback when track setup errors
- Ignore stale episode clicks in the detail panel via a LatestRequest guard on playback
- Extract a resetGrid helper in animeui to dedupe grid-clearing logic
- Assert reader cancellation and extracted-file writes actually happen in installer/subsync tests
2026-08-15 21:44:51 -07:00
sudacode 289c74da35 fix(anime): scope preferences, paginate sources, harden installs
- Preference store keys entries by extension package + bridge source id; legacy unscoped entries are discarded once instead of being handed to whichever extension asks first
- Source picker's "Load more" appends the next page without duplicating streamed results
- Repository index fetches and subtitle/APK downloads now time out and are size-bounded instead of hanging or growing unbounded
- APK installs are staged to a temp file and renamed into place
- Stream metadata lookup matches the requested path, not only the currently playing one
- Reworked animeui into browse-state/detail-panel/panels.css modules
- Reverted premature CHANGELOG unreleased entries; refreshed anime-browser docs
2026-08-15 21:44:51 -07:00
sudacode f5369b8b24 docs: catch up docs for anilist, anime browser, and subsync fixes
- Add changelog entries for the split-cour AniList season resolution fix and the subsync stream-track fixes
- Document anime browser language filter, stream subtitle download-to-disk behavior, and series/season/episode metadata threading
- Update domain ownership and knowledge-base catalog for the anime browser modules
- Trim the credits table and clarify the M-Extension-Server licensing note in README
2026-08-15 21:44:51 -07:00
sudacode c94830531c fix(anime): thread episode metadata through stream playback and fix seas
- Split anime browser stream titles into series/season/episode fields (episode-metadata.ts) instead of one joined string, so stats grouping, the mpv title, and the Jimaku/TsukiHime modals all get real values instead of a filename-derived "m3u8"
- Fix AniList season resolver to walk through split-cour sequels and specials without counting them as a season, and to surface rate-limit/network failures instead of falling through to a wrong air-order guess
- Add a Season field to the TsukiHime modal and include later seasons in its search query
- Rewrite WebVTT subtitle tracks as SRT before handing them to alass, since alass has no VTT support and streams serve VTT
- Update CHANGELOG and troubleshooting docs
2026-08-15 21:44:51 -07:00
sudacode 035ff8e7bf fix(subsync): resolve ffmpeg only when a track needs extraction
Stream subtitles arrive as URLs and are downloaded, never piped through ffmpeg, so demanding an ffmpeg install up front broke stream syncs (and CI, which has no ffmpeg). Resolution is now lazy, scoped to embedded tracks.
2026-08-15 21:44:51 -07:00
sudacode 4abbe8d567 fix(subsync): sync subtitles from stream URLs, auto-discover tool paths
- Download URL-loaded subtitle tracks (Aniyomi extension streams, Jellyfin) to a temp file first, reusing mpv's own request headers, instead of rejecting them with "Subtitle file not found"
- Pass mpv's headers through to ffmpeg for internal track extraction from streams too
- Auto-discover alass/ffsubsync/ffmpeg on PATH and common install prefixes when the config path is empty, instead of a hard-coded /usr/bin fallback that never exists on macOS
- Log subsync failures to the app log, not just a toast that vanishes in seconds
- Update docs-site and README credits for the new behavior
2026-08-15 21:44:51 -07:00
sudacode feb8d5a55c fix(anime): cap outcome polling and upstream stalls by wall clock
- watchPlaybackOutcome now tracks a real deadline so slow property reads eat the timeout budget instead of extending it, and a zero probe interval still terminates
- stream strip proxy aborts upstream GETs that go silent for 15s instead of hanging mpv on that segment forever
- anime browser runtime drops a stream proxy that finishes starting after its bridge already died, instead of leaking a listener pointed at nothing
2026-08-15 21:44:51 -07:00
sudacode d1d128b3f2 feat(anime): cache stream subtitles to disk for the alass reference
- Download a stream's subtitle tracks to a temp dir and load them into mpv as files instead of streaming from the source URL, so Subsync can use them as the alass reference (mirrors the Jellyfin subtitle preload)
- Detect format from content first, falling back to the URL extension
- A track that fails to download falls back to its URL so the episode still plays
- Remove the temp dir when the next episode loads or the runtime disposes
2026-08-15 21:44:51 -07:00
sudacode 2fbf27faff fix(anime): report a failed bridge kill instead of a generic timeout
- Track kill errors separately from exit/spawn so a signal failure (e.g. EPERM) surfaces its own message instead of a bare "did not exit after SIGKILL"
- Attach stop() failures as the cause on the readiness-timeout error so a surviving child's kill failure isn't dropped
- Skip stop() entirely when spawn itself failed, and stop polling once the ready deadline has passed
- Add stopTimeoutMs test hook and a test covering a kill that fails without the child exiting
2026-08-15 21:44:51 -07:00
sudacode 2e54a3ac16 style(animeui): tighten source picker and cue rail layout
- Lay source picker label + input inline instead of stacked
- Let detail cover keep natural aspect ratio instead of forced 2:3 crop
- Widen cue rail gutter to match new cue number column width
2026-08-15 21:44:51 -07:00
sudacode 891c4a13d5 fix(overlay): fall back to did-finish-load when ready-to-show never fires
A hidden overlay window stops producing frames once it is explicitly
hidden and then reloaded (the Yomitan content-script reload right after
startup), so ready-to-show never fires and the content-ready gate keeps
the window hidden forever behind the Overlay loading spinner. Mark
content ready from did-finish-load after a 1.5s grace period so the
overlay always becomes showable; ready-to-show still wins when it fires.
2026-08-15 21:44:51 -07:00
sudacode 4739c3da78 fix(anime): restart the bridge on the next request after it dies
The reported 'APK crash' turned out not to exist: repeated full scans of
all installed extensions load cleanly. The real incident was the app's
quit sequence stopping the bridge (its /stop endpoint) while requests
were in flight - and it exposed that a bridge dying for any reason left
the anime browser failing every call with a raw 'fetch failed' until
the whole app was restarted.

SidecarHandle now reports the child's exit (including to late
subscribers), and the runtime detects a death it did not order: it
drops the dead handle, closes the strip proxy, tells the UI, and the
next request transparently starts a fresh bridge. requireBridge became
an async bridge() that starts on demand, which also covers first use.

Verified live: search -> SIGKILL the JVM -> search recovers.
2026-08-15 21:44:51 -07:00
sudacode ec66f59257 fix(anime): never forward Range to the bridge so 206 replies cannot skip the strip
ffmpeg opens every HLS segment with 'Range: bytes=0-'. The bridge
answers some of those 206 (depending on its cache state), and the proxy
only rewrites full 200 bodies, so the PNG disguise passed through
untouched and whether an episode played was a coin flip: first attempts
died with "no audio or video data played", the same URL played fine
once the bridge had the segments cached.

Reproduced end to end against a private bridge + mpv driven with the
app's exact commands; with Range dropped every segment is stripped and
playback runs.
2026-08-15 21:44:51 -07:00
sudacode ec9da6bed3 fix(anime): retry cold upstream fetches in the stream strip proxy
Right after an episode resolves, the bridge (or the host behind it) can
error on the very first segment fetches and be fine a moment later; mpv
sweeps the playlist of failing segments in ~2s and gives up with "no
audio or video data played" while a manual reload of the same URL plays.
Retry a failed GET once after 400ms and log upstream error statuses so
the next failure names the real cause in the app log.
2026-08-15 21:44:51 -07:00
sudacode 7643855e61 fix(anime): strip disguised HLS segments and report real playback errors
Some hosts prepend a fake image header (a real 1x1 PNG) to every HLS
segment; ffmpeg probes the segment as a picture and mpv drops back to
idle with no window while the browser claims the episode is playing.

- Route bridge-served m3u8 streams through a local strip proxy that
  scans each segment for the first genuine MPEG-TS packet run and drops
  the junk in front of it; playlists get absolute origins rewritten so
  segment requests come back through the proxy. Non-TS bodies pass
  through untouched.
- Only report ok from playEpisode once mpv configures a video output;
  an end-file with reason error surfaces mpv's own message (e.g. "no
  audio or video data played") in the browser status bar instead of
  "Playing". New end-file event plumbed through the mpv IPC client.
2026-08-15 21:44:51 -07:00
sudacode f8a8235b7d fix(anime): pin linux-x64 bridge checksum and fetch release by tag
- Add PINNED_BUNDLE_SHA256 entry for linux-x64-bundle.zip so Linux no longer refuses to start with "No pinned checksum"
- Fetch the bridge release by its pinned tag (releases/tags/<tag>) instead of listing newest releases, so an upstream publish can't swap in an unverified asset
- Update docs and changelog to describe the pin/tag behavior
2026-08-15 21:44:51 -07:00
sudacode 7220f6fc9b feat(anime): add language filter to extensions available list
- Add a language chip row above the Available list; pick one or more languages to narrow the repo index, or "All" to clear
- Show language display names (e.g. "Japanese") instead of raw tags, and an "N of M" count on the Available heading
- Add language-filter.ts with collectLanguages/toggleLanguage/pruneSelection/filterByLanguage/languageLabel, plus tests
2026-08-15 21:44:51 -07:00
sudacode c201520da3 fix(anime): honor the sidecar readiness deadline and confirm shutdown
The capabilities probe used a fixed 5s timeout, so a readiness budget
shorter than that could be overrun by one stalled request. Pass the
remaining deadline down instead.

stop() also resolved after the SIGKILL wait even when the child had not
exited, letting a restart race a process still holding the port. Throw
in that case, and keep a failed shutdown from masking the readiness
error at startup.
2026-08-15 21:44:51 -07:00
sudacode a612d44535 fix(anime): harden the extension bridge against untrusted repos and hangs
Addresses CodeRabbit review feedback on the Anime Browser:

- reject repository package/apk names that are not plain identifiers, and
  verify the install target resolves inside the extensions directory
- count mpv's %n% option escape in UTF-8 bytes, and escape backslashes in
  header values so a trailing one cannot eat the list separator
- key the bridge extension-id cache by APK content, so an in-place upgrade
  re-uploads instead of running the previous build
- bound every bridge, release-listing, and download request with a timeout
- enforce the APK size limit while streaming rather than after buffering
- read APK bytes on demand instead of holding a base64 copy per extension
  for the lifetime of the browser
- serialize preference mutations and write the file atomically
- handle the sidecar spawn error event, and wait for the child to exit in
  stop() before returning
- report a failed Anime Browser bootstrap instead of showing the starting
  banner forever
- keep the preferences panel's save confirmation and in-flight multi-select
  edits by re-rendering only on a structural schema change
2026-08-15 21:44:51 -07:00
sudacode ec48f90cd4 feat(anime): add anime browser powered by Aniyomi extensions
- Add `subminer anime` / `--anime` and a tray entry to open a browser that searches installed Aniyomi extension sources, shows cover art and episodes, and plays into mpv with overlay/mining attached
- Add an Extensions tab to add repos and install/update/remove sources, and per-source settings for sources needing config
- Support searching all sources at once with streaming, per-source results and status
- Prefer Japanese audio/subtitle tracks from the source and keep the primary subtitle slot reserved for Japanese
- Fix window/tray/Dock handling so the browser and mpv can be switched between without quitting the app or losing the Dock icon
- Add anime.repos, anime.extensionsDir, anime.preferredQuality config keys (no bundled repos or discovery)
2026-08-15 21:44:51 -07:00
488 changed files with 26015 additions and 9371 deletions
+36
View File
@@ -59,6 +59,17 @@ jobs:
bun install --frozen-lockfile bun install --frozen-lockfile
bun run build bun run build
- name: Download previous package size reports
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p .tmp/package-baseline
previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty')
if [ -n "$previous" ]; then
gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.'
fi
- name: Build AppImage - name: Build AppImage
run: bun run build:appimage run: bun run build:appimage
env: env:
@@ -87,6 +98,7 @@ jobs:
release/*.AppImage release/*.AppImage
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/package-size-*.json
if-no-files-found: error if-no-files-found: error
build-macos: build-macos:
@@ -147,6 +159,17 @@ jobs:
bun install --frozen-lockfile bun install --frozen-lockfile
bun run build bun run build
- name: Download previous package size reports
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p .tmp/package-baseline
previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty')
if [ -n "$previous" ]; then
gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.'
fi
- name: Build signed + notarized macOS artifacts - name: Build signed + notarized macOS artifacts
run: bun run build:mac run: bun run build:mac
env: env:
@@ -170,6 +193,7 @@ jobs:
release/*.zip release/*.zip
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/package-size-*.json
if-no-files-found: error if-no-files-found: error
build-windows: build-windows:
@@ -211,6 +235,17 @@ jobs:
bun install --frozen-lockfile bun install --frozen-lockfile
bun run build bun run build
- name: Download previous package size reports
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p .tmp/package-baseline
previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty')
if [ -n "$previous" ]; then
gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.'
fi
- name: Verify managed Windows launcher - name: Verify managed Windows launcher
run: bun test src/main/runtime/managed-launcher.test.ts run: bun test src/main/runtime/managed-launcher.test.ts
@@ -235,4 +270,5 @@ jobs:
release/*.zip release/*.zip
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/package-size-*.json
if-no-files-found: error if-no-files-found: error
+2
View File
@@ -105,6 +105,7 @@ jobs:
run: | run: |
shopt -s nullglob shopt -s nullglob
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd) files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
files+=(release/package-size-*.json)
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
@@ -151,6 +152,7 @@ jobs:
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/SHA256SUMS.txt release/SHA256SUMS.txt
release/package-size-*.json
dist/launcher/subminer dist/launcher/subminer
dist/launcher/subminer.cmd dist/launcher/subminer.cmd
) )
-3
View File
@@ -29,9 +29,6 @@ jobs:
- name: Verify Windows launcher bootstrap - name: Verify Windows launcher bootstrap
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts 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 - name: Verify POSIX launcher bootstrap
run: bun test src/main/runtime/posix-launcher-bootstrap.test.ts run: bun test src/main/runtime/posix-launcher-bootstrap.test.ts
+10 -17
View File
@@ -106,6 +106,7 @@ jobs:
run: | run: |
shopt -s nullglob shopt -s nullglob
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd) files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
files+=(release/package-size-*.json)
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
@@ -170,6 +171,7 @@ jobs:
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/SHA256SUMS.txt release/SHA256SUMS.txt
release/package-size-*.json
dist/launcher/subminer dist/launcher/subminer
dist/launcher/subminer.cmd dist/launcher/subminer.cmd
) )
@@ -253,31 +255,22 @@ 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 }} RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: | run: |
set -euo pipefail set -euo pipefail
version="$RELEASE_VERSION" version="$RELEASE_VERSION"
install -dm755 .tmp/aur-release-assets install -dm755 .tmp/aur-release-assets
for asset in "SubMiner-${version#v}.AppImage" subminer subminer-assets.tar.gz; do gh release download "$version" \
destination=".tmp/aur-release-assets/$asset" --dir .tmp/aur-release-assets \
if ! curl --fail --silent --show-error --location \ --pattern "SubMiner-${version#v}.AppImage" \
--retry 3 --retry-delay 1 --retry-all-errors \ --pattern "subminer" \
--connect-timeout 30 --max-time 600 \ --pattern "subminer-assets.tar.gz"
--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' && steps.aur_assets.outputs.skip != 'true' if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
env: env:
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }} RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: | run: |
@@ -294,7 +287,7 @@ jobs:
--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' && steps.aur_assets.outputs.skip != 'true' if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.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
-2
View File
@@ -20,8 +20,6 @@ coverage/
/main-entry.js /main-entry.js
/main-entry.js.map /main-entry.js.map
/build/hachidori/
# Logs # Logs
*.log *.log
npm-debug.log* npm-debug.log*
-4
View File
@@ -8,7 +8,3 @@
[submodule "vendor/subminer-yomitan"] [submodule "vendor/subminer-yomitan"]
path = vendor/subminer-yomitan path = vendor/subminer-yomitan
url = https://github.com/ksyasuda/subminer-yomitan url = https://github.com/ksyasuda/subminer-yomitan
[submodule "vendor/hachidori"]
path = vendor/hachidori
url = https://github.com/ksyasuda/hachidori.git
branch = subminer
-121
View File
@@ -1,126 +1,5 @@
# Changelog # Changelog
## 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) ## v0.19.6 (2026-09-04)
### Added ### Added
+30 -9
View File
@@ -4,7 +4,7 @@
# SubMiner # SubMiner
Integrates Yomitan or Hachidori with mpv - on-screen lookups, mine to Anki, and track immersion without leaving the player Integrates Yomitan and mpv - on-screen lookups, mine to Anki, and track immersion without leaving the player
[Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe) [Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
@@ -25,8 +25,6 @@ Integrates Yomitan or Hachidori with mpv - on-screen lookups, mine to Anki, and
Hover over any word and trigger a lookup to get the full Yomitan popup - definitions, pitch accent, and frequency data - without ever leaving mpv. Hover over any word and trigger a lookup to get the full Yomitan popup - definitions, pitch accent, and frequency data - without ever leaving mpv.
Yomitan remains the default. Select the bundled Hachidori backend with `dictionaryBackend: "hachidori"` and restart SubMiner. The tray opens the selected backend's settings. See [dictionary setup](https://docs.subminer.moe/usage#hachidori-setup) for importing dictionaries, linking an external Hachidori host, and configuring Anki.
<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">
</div> </div>
@@ -88,13 +86,17 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
<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, launch, and cast media from your Jellyfin server with setup and discovery controls in the app tray</td>
</tr> </tr>
<tr>
<td><b>Anime Browser</b></td>
<td>Search anime sources you supply as <a href="https://github.com/aniyomiorg/aniyomi">Aniyomi</a> extension APKs and play an episode in mpv with the overlay attached (<code>subminer anime</code>); SubMiner ships no repositories and bundles no sources</td>
</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</td>
</tr> </tr>
<tr> <tr>
<td><b>Local Subtitle Generation</b></td> <td><b>Japanese Subtitle Generation</b></td>
<td>Generate Japanese subtitles from local audio in a standalone modal (<code>Ctrl+Shift+G</code>), the sidebar button, or launcher, with progress and optional managed model downloads. Requires whisper.cpp and FFmpeg. Optional Silero speech detection prioritizes dialogue in separately timed passages. <a href="https://docs.subminer.moe/main/subtitle-generation">Setup guide</a></td> <td>Generate Japanese subtitles from local files or finite anime streams in a standalone modal (<code>Ctrl+Shift+G</code>) or the sidebar button. The launcher supports local files. Transcription runs locally with whisper.cpp and FFmpeg, with progress and optional managed model downloads. Stream subtitles are saved in the application cache. Optional Silero speech detection prioritizes dialogue in separately timed passages. <a href="https://docs.subminer.moe/main/subtitle-generation">Setup guide</a></td>
</tr> </tr>
<tr> <tr>
<td><b>TsukiHime</b></td> <td><b>TsukiHime</b></td>
@@ -184,6 +186,8 @@ See the [full requirements list](https://docs.subminer.moe/installation#_1-insta
```bash ```bash
paru -S subminer-bin paru -S subminer-bin
# optional: the anime browser bridge, shared with Mangatan and updated by pacman
paru -S mangatan-extension-server
``` ```
</details> </details>
@@ -267,22 +271,39 @@ Full guides on configuration, Anki setup, Jellyfin, immersion tracking, and more
SubMiner builds on the work of these open-source projects: 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 | | [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 |
| [Aniyomi](https://github.com/aniyomiorg/aniyomi) | Anime extension API and data model the anime browser targets |
| [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 | | [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 |
| [M-Extension-Server](https://github.com/1Selxo/M-Extension-Server) | Runs Aniyomi extension APKs off Android; the bridge the anime browser drives |
| [Mangatan](https://github.com/1Selxo/Mangatan) | Reference client for the bridge protocol the anime browser speaks |
| [Renji's Texthooker Page](https://github.com/Renji-XD/texthooker-ui) | Base for the WebSocket texthooker integration | | [Renji's Texthooker Page](https://github.com/Renji-XD/texthooker-ui) | Base for the WebSocket texthooker integration |
| [Yomitan](https://github.com/yomidevs/yomitan) | Default dictionary engine and morphological parser | | [Yomitan](https://github.com/yomidevs/yomitan) | Dictionary engine powering all lookups and the morphological parser |
| [Hachidori](https://github.com/bee-san/hachidori) | Alternative dictionary backend, powered by HoshiDicts |
| [yomitan-jlpt-vocab](https://github.com/stephenmk/yomitan-jlpt-vocab) | JLPT level tags for vocabulary | | [yomitan-jlpt-vocab](https://github.com/stephenmk/yomitan-jlpt-vocab) | JLPT level tags for vocabulary |
## Disclaimer
SubMiner is a media player and language-learning tool. The project does not
host, supply, or link to anime, video streams, or subtitle libraries. The anime
browser runs Aniyomi extensions that you install yourself from repositories you
choose; SubMiner bundles none and recommends none. The SubMiner developers are
not affiliated with any content provider and have no control over what those
sources offer, whether they stay available, or the quality of what they serve.
## License ## License
SubMiner is released under the [GNU General Public License v3.0](LICENSE). [GNU General Public License v3.0](LICENSE)
The anime browser drives [M-Extension-Server](https://github.com/1Selxo/M-Extension-Server),
downloaded from its upstream releases at runtime rather than
bundled or redistributed here; its bundles carry their own dependencies, including a JRE and
GPL-3.0 NewPipe Extractor. SubMiner includes none of them and talks to the bridge
over its own HTTP protocol. It ships no extensions or repositories by default.
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). 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).
+10 -10
View File
@@ -22,7 +22,7 @@
"@types/node": "^24.10.0", "@types/node": "^24.10.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"electron": "42.6.0", "electron": "42.6.0",
"electron-builder": "26.16.1", "electron-builder": "26.15.3",
"esbuild": "^0.25.12", "esbuild": "^0.25.12",
"eslint": "^10.8.0", "eslint": "^10.8.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
@@ -36,9 +36,9 @@
}, },
"overrides": { "overrides": {
"@xmldom/xmldom": "0.8.15", "@xmldom/xmldom": "0.8.15",
"app-builder-lib": "26.16.1", "app-builder-lib": "26.15.3",
"brace-expansion": "5.0.9", "brace-expansion": "5.0.9",
"electron-builder-squirrel-windows": "26.16.1", "electron-builder-squirrel-windows": "26.15.3",
"fast-uri": "3.1.6", "fast-uri": "3.1.6",
"form-data": "4.0.6", "form-data": "4.0.6",
"ip-address": "10.2.0", "ip-address": "10.2.0",
@@ -181,7 +181,7 @@
"@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=="],
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
"@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=="], "@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=="],
@@ -243,7 +243,7 @@
"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-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.15.3", "", { "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": "^2.2.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.15.3", "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.15.3", "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.15.3", "electron-builder-squirrel-windows": "26.15.3" } }, "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -273,7 +273,7 @@
"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.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": ["builder-util@26.15.3", "", { "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-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw=="],
"builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="], "builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
@@ -335,7 +335,7 @@
"discord-api-types": ["discord-api-types@0.38.49", "", {}, "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg=="], "discord-api-types": ["discord-api-types@0.38.49", "", {}, "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg=="],
"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-builder": ["dmg-builder@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
@@ -349,11 +349,11 @@
"electron": ["electron@42.6.0", "", { "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-axGNgd+yCTg+vi1VEGrQqAj9WVWkePKwbICSAvMiT2eTaxhij9a/xhBHD6rXV8wrlW9ZfJzE5+xg752ImxrmTw=="], "electron": ["electron@42.6.0", "", { "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-axGNgd+yCTg+vi1VEGrQqAj9WVWkePKwbICSAvMiT2eTaxhij9a/xhBHD6rXV8wrlW9ZfJzE5+xg752ImxrmTw=="],
"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": ["electron-builder@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.15.3", "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-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA=="],
"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-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "electron-winstaller": "5.4.0" } }, "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA=="],
"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-publish": ["electron-publish@26.15.3", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.15.3", "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-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q=="],
"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-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=="],
+7
View File
@@ -0,0 +1,7 @@
type: fixed
area: anilist
- Resolving "season N" against AniList no longer lands one season short for franchises whose broadcast seasons are listed as several entries. AniList records the back half of a split cour ("… Season 2 Part 2", "… Cour 2", "第2クール") as its own sequel, and the resolver counted each as a season — so Mushoku Tensei season 3 resolved to the season 2 entry and Re:ZERO season 4 to season 3, which then drove the character dictionary and AniList progress updates to the wrong series. A sequel carrying a part or cour marker that matches the season it continues is now followed without advancing the season count, in both the relation walk and the air-order fallback. Titles are compared ignoring punctuation, and the marker is looked for in every title and synonym rather than just the display title. A wrong match cached before this fix is remembered in `character-dictionaries/anilist-resolution-cache.json` and needs removing (or overriding from the character dictionary picker) for the affected series.
- A special or OVA sitting in a franchise's sequel chain is walked through without counting as a season. Dr. STONE links STONE WARS to New World through a one-episode special, which made New World resolve as season 4 and every later season shift with it.
- An AniList rate limit or network failure while walking sequel relations is reported so the caller can retry, instead of falling through to the air-date fallback. That fallback is for a franchise with missing relation edges; running it after a failed lookup turned a transient 429 into a confidently wrong season.
- The shared title parser recognizes spelled-out episode labels (`Episode 4`, `第4話`) and a season named at the end of the title (`… Season 3`, `… 3rd Season`, `… S3`), which now fill the Season field instead of being searched for as part of the series name. The TsukiHime modal gained a Season field to match Jimaku, and seasons after the first are included in its search query so a later season's releases are actually found.
+4
View File
@@ -0,0 +1,4 @@
type: docs
area: anime
- Added a README disclaimer stating that SubMiner hosts no content, ships no anime sources or repositories, is not affiliated with any content provider, and that users are responsible for the content they access. The anime browser docs no longer describe SubMiner as a "host" of extensions.
+36
View File
@@ -0,0 +1,36 @@
type: added
area: anime
- Added an anime browser window that searches Aniyomi extension sources, shows cover art and episode lists, and plays an episode in mpv so the overlay and mining tools attach as usual.
- Browser failures show a dismissible explanation with next steps and expandable technical details, including the bridge's error response instead of only its HTTP status.
- Added `subminer anime` and the `--anime` flag to open the browser, plus a "Browse Anime" tray entry.
- Anime extensions are read from `<userData>/anime-extensions`; drop Aniyomi `.apk` files there to add sources.
- Added a source settings tab so extensions that need configuration (server address, credentials, quality) can be set up from the browser; values persist per extension and source, and updated extension schemas replace stale saved field definitions without losing values. Older unscoped preferences are discarded once because their package ownership cannot be proven safely.
- Added an Extensions tab for adding repository URLs and installing, updating, or removing extensions in place; extensions that fail to load are listed with the reason. Repository requests time out instead of hanging, APK updates are staged before replacing the installed copy, and removing an extension refreshes the runtime even when saved credential cleanup fails while still reporting that failure.
- Browse, Extensions, and Source settings are tabs, so each one gets the full window instead of sharing it with the search results.
- Repository URLs only need to be an https URL to a `.json` index; the file name is not restricted to `index.min.json`.
- The source picker offers "All sources", which searches every installed source at once. Results stream in as each source answers, with per-source progress in the status bar. Results are tagged with their source, failures do not blank the grid, and **Load more** appends later pages without duplicating streamed entries.
- The Extensions tab opens with an Installed section listing every extension on disk with Remove, including ones added by hand or whose repository has since been removed. It compares APK and repository version codes, enables Update only for newer builds, marks current extensions as Up to date, and offers Update all when multiple updates are waiting.
- Added `anime.repos`, `anime.extensionsDir`, `anime.preferredQuality`, and `anime.defaultSource` config keys; **Set default** on an Installed row in the Extensions tab makes that source (or All sources) the one the browser opens on, and the current default carries a tag. SubMiner ships no extension repositories and performs no discovery.
- The settings app now groups Anime Browser config under an **Aniyomi** section in Integrations. A new `anime.autoOpenJimaku` option pauses newly loaded Anime Browser episodes, waits for mpv's video window to appear, closes the in-player browser, hides the standalone browser window so it is not pulled in front of mpv when the modals close, opens Jimaku with the episode details filled in, and resumes after a subtitle loads or the modal closes without overriding playback that was already paused.
- Anime playback targets Japanese audio: dub-labelled entries are skipped when the source offers an alternative, `alang` prefers Japanese, and the source's own audio and subtitle tracks are loaded into mpv (Japanese selected) instead of being discarded, so all of them can be switched from mpv's track menu.
- The primary subtitle slot stays reserved for Japanese: a source that only has, say, English subtitles gets them added with a normalized language tag (`English` → `en`) but not selected, so the regular `secondarySub` auto-load can route them to the secondary slot instead.
- HLS streams pass through a local strip proxy that removes fake image headers some hosts glue onto their video segments and gives disguised segment URLs (`.image`, `.jpg`, `.css`, and other rotating fake extensions) a media-safe local alias, so those streams play in mpv and support Anki audio and image extraction with current ffmpeg releases. Upstream responses use identity encoding so playlists remain readable for URL rewriting.
- The strip proxy retries a failed segment fetch once after a short pause and logs upstream error statuses; a host that errors on the very first fetches right after an episode resolves no longer kills the whole playback, disconnecting clients release their active upstream fetch instead of consuming the socket and bandwidth in the background, stalled response bodies retain the upstream inactivity timeout, and only origin-form requests can reach the configured local bridge.
- The strip proxy no longer forwards `Range` headers to the bridge: ffmpeg opens every HLS segment with `Range: bytes=0-`, the bridge answers some of those with 206, and a partial response bypassed the disguise strip, so whether an episode played depended on the bridge's cache state.
- A bridge that dies out from under the app (killed, crashed, or stopped mid-operation) no longer leaves the browser failing every request until an app restart: the exit is detected, surfaced in the status bar, and the bridge restarts on the next request.
- "Playing" is only reported once mpv actually configures a video output; slow streams remain loading while mpv opens them, and failed streams show mpv's error.
- The bridge bundle is downloaded from the newest upstream release that ships a bundle for the platform (skipping releases older than the oldest server SubMiner is known to work with), the same way Mangatan fetches it, so all four published bundles (macOS arm64 and x64, Linux x64, Windows x64) work without a maintainer hashing each one first.
- The Extensions tab's available list has a language chip row: pick one or more languages to narrow a repository index that otherwise lists every language it knows, or "All" to clear the filter. Rows name the language ("Japanese" instead of `ja`) and the Available heading counts what the filter leaves.
- A stream's subtitle tracks are downloaded to a temp directory and loaded into mpv as files rather than streamed from the source URL, so they can serve as the alass reference in Subsync the way Jellyfin subtitles do. The format is detected from the file's own content, a track that fails to download falls back to its URL so the episode still plays, and the directory is removed when the next episode starts or the app exits.
- An episode launched from the browser carries its series, season and episode number through the app instead of a single joined string: stats group by series (every stream previously landed in one entry named `m3u8`), rewatching reuses the existing entry, the Jimaku and TsukiHime modals prefill Title/Season/Episode from the source's own listing, AniList updates use those fields directly, and the mpv title reads `Series S03E04 - Episode Name`.
- Opening the browser shows a tray icon on every platform and — on macOS — puts the app in the Cmd+Tab switcher and Dock, so you can switch between it and mpv; the Dock icon is released again when the window closes during playback. Launching a video starts a regular SubMiner session, and in `subminer anime` standalone mode closing the window during playback no longer quits the app and kills the stream.
- The episode list has a filter box: type an episode number (`12`), a range (`12-18`), or part of an episode name to narrow a long list, with a `6 of 25` counter while it is applied. Escape inside the box clears the filter instead of leaving the detail page.
- Episodes already watched are dimmed and marked in the episode list, with a watched count in the header. The marks come from the stats history playback already writes (an episode is marked once a session passes the completion threshold), so they match the stats window and survive the stream URL changing between playbacks. They refresh when the browser window comes back to the front, and stay empty when immersion tracking is disabled.
- Right-clicking an episode opens a menu for marking it watched or unwatched by hand, plus "Mark this and N below watched/unwatched" for the episode and every episode listed under it. Sources list newest first, so a span covers the back catalogue, which is how a series watched elsewhere gets caught up. A filter never narrows what a span covers, and the status bar reports how many episodes were touched.
- Marking an episode that was never played creates its stats row, carrying the same series, season and episode fields playback would have recorded. Both stats library views join the lifetime tables, so a manual mark does not show up there as watch time nobody spent, and clearing a mark creates nothing.
- Episodes can be queued instead of replacing what is playing. Every episode row has **Play** and **Queue** buttons (clicking the row still plays now), the right-click menu offers the same two, and a queued episode shows its place in line ("next up", "#2 in queue") with a queue count and **Clear queue** in the episode header. The queue spans anime, resolves and appends each episode to mpv's real playlist as soon as it is queued while subtitle tracks cache in the background, so next/previous navigation works immediately and the next episode starts without a resolution pause when the current one ends. Queueing with nothing playing just plays.
- Added an in-player Anime Browser modal on `Ctrl+Alt+A`. The shortcut toggles it without losing its page or scroll position. It stays within the player bounds and shares the active episode, playback queue, source configuration, and watch history with the standalone browser, while each surface keeps independent search and navigation state. The modal validates its embedded page before changing overlay state, so a load setup failure leaves it closed rather than revealing a broken modal. The SubMiner logo identifies the Anime Browser in both the modal and standalone window.
- The bridge is reused from a package-manager install when one exists: on Arch the AUR `mangatan-extension-server` package (shared with Mangatan) is picked up from `/usr/share/mangatan/extension_server`, so nothing is downloaded and pacman keeps it current. `anime.bridgeDir` points SubMiner at a bundle anywhere else. `subminer-bin` lists the package as an optional dependency.
- Once the bridge is running, SubMiner checks GitHub for a newer release, including for AUR and custom installations. AUR update notices direct users to their AUR helper; custom installs get manual-update guidance. Only SubMiner-managed copies offer an **Update to vX** button. The new release is downloaded beside the running bridge, then the bridge restarts on it. The updater waits for a bridge still starting to stop and keeps the previous bundle until the replacement is active. The Extensions tab shows the bridge version, where it lives, and who updates it without describing an unchecked install as current.
- The detail page keeps its episode list when only the details call fails: the episodes render with a "Details unavailable" note and the error in the status bar, instead of an empty page.
+4
View File
@@ -0,0 +1,4 @@
type: docs
area: anime
- Document how the anime browser waits for slow streams to start without reporting a premature playback failure.
@@ -0,0 +1,5 @@
type: docs
area: subtitles
- Document anime stream generation, audio-only and lower-resolution extraction candidates, concurrent HLS downloads, temporary disk usage, audio cache reuse and limits, full-episode processing, supported audio tracks, and stream access limitations.
- Explain HLS source-check diagnostics and session-only audio cleanup on quit and after crashes.
@@ -0,0 +1,6 @@
type: added
area: subtitles
- Generate Japanese subtitles from finite HTTP/HTTPS anime episode streams in the existing generation modal. Reuse mpv's supported request headers and the anime proxy, transcribe downloaded audio locally, retain generated SRT files in the application cache, and load them only into the matching stream. Local-file generation and the launcher keep their existing behavior.
- Prepare plain HLS episodes with up to four concurrent segment downloads, preserving audio order and timing. Show download progress, cancel active requests together, and retain FFmpeg extraction for complex playlists.
- Reduce anime-stream extraction downloads by preferring separate Japanese audio or lower-resolution video when duration and sampled audio match playback. Sample HLS through short local segment windows, account for timestamp origins, retry empty original samples, and report why candidates are rejected. Selected external HTTP audio tracks are extracted directly with their playback delay preserved. Reuse verified completed audio across transcription retries and model changes within the current app session. Remove downloaded audio and working files on quit, recover abandoned sessions on startup, and preserve saved subtitles.
+5
View File
@@ -0,0 +1,5 @@
type: docs
area: launcher
- Documented private-runtime launcher installation, simplified first-run setup, custom app locations, legacy launcher migration, and package-managed updates. Updated release-note download guidance for the bundled runtime and Windows launcher.
- Documented the bundled Bun runtime's MIT and LGPL licensing, where the notices live in the app, and the corresponding-source release asset. The AUR package now lists the bundled runtime licenses and installs their notices under `/usr/share/licenses/subminer-bin`, and `subminer-assets.tar.gz` includes the same notices.
+5
View File
@@ -0,0 +1,5 @@
type: changed
area: launcher
- Made every installed and downloadable launcher use the Bun runtime included with SubMiner. Added a Windows `subminer.cmd` download, persistent private runtime caches for Linux and Windows, and migration for recognized legacy launchers. Deferred migrations survive overlapping update checks and remain pending when startup cannot safely refresh the launcher.
- Simplified first-run setup to a single optional launcher control, with runtime repair guidance shown only when needed.
@@ -0,0 +1,4 @@
type: docs
area: subtitles
- Clarify that character dictionary sync works without a subtitle track and explain readiness and import timeouts.
@@ -0,0 +1,4 @@
type: fixed
area: subtitles
- Let character dictionaries finish syncing when no Japanese subtitles are loaded. Wait for Yomitan extension readiness instead of the first processed subtitle, and report readiness failures instead of leaving the notification stuck on Checking.
+4
View File
@@ -0,0 +1,4 @@
type: internal
area: ci
- Removed duplicate source and launcher smoke executions from the reusable quality gate while preserving every distinct test lane and failure artifact.
+4
View File
@@ -0,0 +1,4 @@
type: internal
area: verification
- Replaced mislabeled dist source reruns with a small Electron-runtime smoke check for compiled stats startup, HTTP service, native SQLite, port conflicts, and cleanup.
+6
View File
@@ -0,0 +1,6 @@
type: changed
area: sync
- Sync uses compressed, incremental rsync transfers on compatible macOS and Linux machines, caching the last received snapshot per peer to reduce traffic on subsequent syncs. Cache helpers work through the launcher; older apps and launchers fall back to compressed transfers without an upload cache.
- Machines without compatible rsync, including Windows endpoints, automatically use compressed scp transfers.
- Rsync explicitly uses SSH and aborts transfers that exceed 30 minutes before merging.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: anki
- Added `ankiConnect.fields.wordAudio` to read word audio separately from the generated sentence-audio destination, fixing animated images that start moving immediately when `fields.audio` points to `SentenceAudio`.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: config
- Validate direct AnkiConnect, Kiku, and Senren settings before admitting them to runtime config, with warnings and defaults for invalid values.
@@ -0,0 +1,5 @@
type: fixed
area: runtime
- Shutdown finishes independent media and Discord cleanup and waits for sync shutdown even when Jellyfin cleanup fails, then reports the first cleanup error.
- Tsukihime ignores delayed media-info results and errors from a closed modal session.
@@ -0,0 +1,4 @@
type: fixed
area: startup
- Fixed first launch exiting on macOS when the SubMiner config directory did not yet exist by creating it before acquiring the startup lock.
+7
View File
@@ -0,0 +1,7 @@
type: fixed
area: jellyfin
- Set the mpv title before loading Jellyfin streams and reject URL-derived titles from metadata lookups, Anki source fields, Discord presence, and stats.
- Keep authenticated stream URLs out of stats identities even when playback metadata has not arrived.
- Remove previously cached credential-bearing Jellyfin stream parser metadata without changing unrelated anime metadata, watch history, or library assignments.
- Use safe media identities for persisted AniList retry keys, discard URL-derived queued searches, and skip updates and retries whose media identity is empty.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Clicking a subtitle sidebar cue releases row focus, and Space no longer seeks back to a focused cue. Enter still seeks the focused cue, and Space keeps its configured playback action.
@@ -0,0 +1,4 @@
type: fixed
area: stats
- Reject malformed resource IDs and partly invalid ID lists before stats library mutations or cover backfills run.
+5
View File
@@ -0,0 +1,5 @@
type: fixed
area: stats
- Stats server startup reports port conflicts without crashing SubMiner, shares concurrent startup requests, and shows in-app startup errors through configured status notifications.
- Background stop cancels pending background startup without disconnecting foreground-only dashboards. Shutdown bounds the wait for active HTTP requests and awaits tracker finalization before exit, with a deadline for forced application exit.
@@ -0,0 +1,4 @@
type: fixed
area: anki
- Closing the overlay while media timing review is still loading now cancels setup and modal retries, restores playback if the review paused it, and cleans up the hidden preview player.
@@ -1,4 +0,0 @@
type: fixed
area: anki
- Media timing review now keeps playback paused while it is open when the dictionary popup or subtitle hover pause ends, as with Hachidori popups closing when the review appears. Playback resumes after the review closes unless a dictionary popup is still open.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: anki
- Treat `ankiConnect.media.maxMediaDuration: 0` as unlimited for stats dashboard mining, matching overlay mining and configuration.
-14
View File
@@ -1,14 +0,0 @@
type: added
area: dictionary
- Added a bundled Hachidori dictionary backend alongside the default Yomitan backend. Select it with `dictionaryBackend` and restart SubMiner.
- The tray and dictionary-settings shortcut follow the selected backend. `--hachidori` opens Hachidori settings, while `--yomitan` opens Yomitan settings unless a read-only external Yomitan profile is configured. This restriction also applies with Hachidori active, without blocking Hachidori settings.
- Hachidori integrates with subtitle scanning, popup controls, lookup tracking, character dictionaries, and Anki media enrichment, with separate dictionaries and settings for each backend. Linked Docker hosts receive character dictionary uploads through `hachidori.externalHostManagementUrl`, retry busy imports, and replace the previous dictionary only after a successful import.
- Hachidori auto-populates its first Anki template from SubMiner's deck, tags, and field mappings, detects an unambiguous matching note type, and preserves existing custom templates apart from the deck, which follows `ankiConnect.deck` so polling mode enriches Hachidori cards. Anki discovery retries after an unavailable connection.
- Hachidori saves downloadable word audio before sending a note through SubMiner's Anki proxy, so freshly mined animated cards include the word-audio delay.
- Both bundled dictionary backends reload their background code on startup so extension updates take effect while preserving installed dictionaries and settings. Failed Yomitan connection-setting updates can be retried without manually resetting the managed Anki endpoint.
- First-run setup remembers each backend that finished it, including when reopened for legacy plugin cleanup, so switching back does not repeat setup, and the launcher gates playback on the backend the running app started with. Current incomplete or cancelled setup takes precedence over stale completion history. A running Yomitan session keeps using its external profile until it restarts into Hachidori.
- Stats dashboard mining and deck lookup use the selected backend, with the Anki proxy enabled or disabled. Dashboard cards retain the selected history line and media instead of being processed again with the current mpv subtitle, with both Yomitan and Hachidori. Stats cards carry a `SubMiner::Stats` tag so polling also preserves their context. Hachidori word mining supplies dictionary aliases, IDs, and frequency metadata to its native Anki renderer. Settings labels for popup pause and the dictionary deck no longer name Yomitan, and the backend selector sits with the other dictionary settings.
- Hachidori scans, dictionary counts, and settings reads wait for the dictionary engine to finish loading or importing instead of caching empty results.
- First-run setup can link an external Hachidori dictionary host in an app, browser, or Docker container, verify its library, and unlink back to local dictionaries. The optional host controls are collapsed by default and explain which apps or containers must stay running. Unresponsive connection checks time out so setup remains usable. Anki mining, media enrichment, and custom toolbar buttons stay in SubMiner, including when editing buttons while linked.
- Hachidori frequency highlighting reuses dictionary-entry ranks and fills missing ranks through its existing API. Entries without a matching definition may remain unranked. The bundled integration is maintained in a pinned fork submodule using upstream HoshiDicts and WASM binaries.
-4
View File
@@ -1,4 +0,0 @@
type: docs
area: dictionary
- Documented backend selection, separate Hachidori dictionary and Anki setup, the settings flags and shortcuts, and external host linking with local Anki mining and the running requirements for each host type.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Keep Hyprland recovery dialogs above SubMiner windows so overlay placement updates do not cover their Wait and Close buttons.
@@ -0,0 +1,5 @@
type: docs
area: subtitles
- Explain how dialogue generation retains uncertain audible sections, why songs may also be transcribed, how detected speech starts guide long-passage cuts to reduce early subtitles, and why each passage uses a fresh Whisper process.
- Document automatic loaded subtitle references, English dialogue preference, signs/songs filtering, fallback behavior, and the limits of reference-guided timing.
+8
View File
@@ -0,0 +1,8 @@
type: added
area: subtitles
- Generate local Japanese SRT subtitles with whisper.cpp from a standalone modal opened with Ctrl+Shift+G, the empty subtitle sidebar's generation button, or `subminer generate-subs`, with shared progress reporting, cancellation, safe output files, and automatic loading into the matching mpv video. The sidebar button hides while subtitle lines are loaded.
- Configure an existing multilingual model in Settings or choose an official multilingual model, including quantized variants, in the modal or launcher. The modal shows download sizes, speed and accuracy guidance, and a recommended starting model before explicitly downloading a verified SubMiner-managed model. Executable paths are optional overrides; empty fields find whisper-cli, ffmpeg, and ffprobe on PATH. The modal's Local tools check and the launcher name any missing executable and its setting before downloading a model or extracting audio, and generation confirms the destination directory grants write and search permissions up front.
- Optionally select Focus on spoken dialogue in the modal and use Download speech detection model to install the separate Silero model with progress and cancellation. The choice lasts for the session; a configured VAD model path sets the default. Retain uncertain audible sections so VAD rejection does not discard dialogue under music, accepting that songs may also be transcribed. Keep passages intact within Whisper's audio window, split longer passages near detected speech starts or quiet pauses with overlapping context to reduce early subtitle timing, and combine overlapping duplicate cues even when punctuation differs. Run each passage in a fresh Whisper process to prevent repeated-character output caused by state carried between files, at the cost of reloading the model per passage. Preserve original media timing and separate repeated dialogue.
- Recommend large-v3-turbo in the model picker when NVIDIA hardware and CUDA device support in the selected Whisper executable are confirmed; recommend small otherwise. Preserve configured and session model selections. Use a wider generation modal to reduce text wrapping and scrolling, while keeping it within smaller player windows.
- Automatically use an eligible embedded or external subtitle track loaded in mpv to guide long-passage cuts, preferring English dialogue and excluding tracks marked signs, songs, forced, or generated. Honor active subtitle delays, retain unreferenced audio, and fall back to audio timing when references cannot be read. Available in the overlay and launcher, with or without VAD. The launcher captures audio selection, reference tracks, and delays before model setup so playback changes during a download do not mix inputs from different times, and rejects an unverified media snapshot before using mpv's selected audio stream.
+5
View File
@@ -0,0 +1,5 @@
type: docs
area: jellyfin
- Explain how Jellyfin media titles, unknown-title fallbacks, and stats identities keep stream credentials out of metadata.
- Document AniList rejection of empty media identities and the conditions for clearing cached Jellyfin parser metadata.
+4
View File
@@ -0,0 +1,4 @@
type: added
area: jimaku
- Jimaku modal gains Anime / Live action tabs so searches can pull from Jimaku's live action catalogue instead of only anime entries (Arrow Left / Right switch tabs).
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed the overlay never loading (stuck on the "Overlay loading" OSD spinner) when the Yomitan content-script reload raced overlay window creation at startup, most visible when playing from the anime browser on Linux/Wayland: a hidden window stops painting after that reload, so the ready-to-show signal that gates showing the overlay never fired. Content-ready now falls back to did-finish-load after a short grace period.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: setup
- Fixed the command-line launcher showing "Not on PATH" on macOS when SubMiner was opened from Finder or the Dock, even though the launcher directory was already on your terminal PATH.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: macos
- `subminer anime` (and `--settings` / `--sync`) now bring their window to the front on macOS. `show()`/`focus()` only reorder windows inside the app that is already active, so the window opened behind the terminal that launched it; SubMiner now activates itself when opening one. The anime browser also restores its Dock icon before showing rather than after, because the overlay's fullscreen transform leaves the app as an accessory process that macOS refuses to bring forward at all.
@@ -0,0 +1,4 @@
type: docs
area: mining
- Document independent screenshot selection in media timing review.
+4
View File
@@ -0,0 +1,4 @@
type: added
area: mining
- Choose a still screenshot independently of the audio in media timing review, with a live preview, a separate time slider, and decoded-frame stepping. Local video and seekable remote streams such as Jellyfin are supported.
+4
View File
@@ -0,0 +1,4 @@
type: added
area: overlay
- The overlay discovers non-conflicting keyboard bindings from mpv defaults, input.conf, and loaded scripts in the background. SubMiner controls and explicitly disabled bindings take precedence. Discovered bindings stay session-only and do not appear in SubMiner's help menu.
-4
View File
@@ -1,4 +0,0 @@
type: fixed
area: overlay
- Overlay modals such as the media timing review open on the first attempt when their dedicated window is created on demand, instead of after a four-second retry.
+5
View File
@@ -0,0 +1,5 @@
type: changed
area: release
- Reduced installer and unpacked app size by excluding documentation demo media, dependency source maps, TypeScript files, test and fixture directories, other development files, and unused Koffi platform binaries, and sharing the existing Japanese UI font across windows.
- Added package content checks, published size reports with release comparisons, and packaged asset/native-module smoke checks to the shared stable and prerelease build workflow. Size growth is reported without blocking releases.
-4
View File
@@ -1,4 +0,0 @@
type: changed
area: release
- Removed package-size JSON reports from future releases and their CI size comparisons. Package-content validation remains enabled.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Cancel pending Linux overlay window replacements during teardown so a delayed close callback cannot reopen the overlay.
@@ -0,0 +1,4 @@
type: docs
area: config
- Clarified live-setting save feedback, mixed restart warnings, and subtitle-generation reload behavior.
+5
View File
@@ -0,0 +1,5 @@
type: fixed
area: config
- Settings marked LIVE now use the same reload policy as save results, fixing false restart warnings for notifications and subtitle generation.
- Mixed saves apply live changes and list only sections with changed fields that require a restart.
+4
View File
@@ -0,0 +1,4 @@
type: added
area: overlay
- Select dialogue across subtitle sidebar rows and copy it without timestamps using Ctrl/Cmd+C or the Copy button. Selection keeps the excerpt in view during playback and does not seek or require mining a card.
+6
View File
@@ -0,0 +1,6 @@
type: changed
breaking: true
area: stats
- Reject requests from untrusted browser origins and hosts before stats data, media, or Anki operations run, and require JSON for mutation bodies.
- Load the in-app stats overlay from the local server so it uses the same origin protection as the browser dashboard.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: subsync
- Subsync no longer fails with `Protocol "file:" not supported` on a subtitle that was dropped onto mpv. mpv reports such a track as a percent-encoded `file://` URL, which subsync read as a stream and tried to fetch over HTTP; the URL is now decoded back to its path, so both the retimed target and an alass reference work. A `file://` video path is treated as local too, which restores the video reference and ffsubsync for a dropped file.
+7
View File
@@ -0,0 +1,7 @@
type: fixed
area: subsync
- Subsync now works when mpv loaded a subtitle track from a URL, which is how Aniyomi extension streams and Jellyfin add theirs. The track is downloaded to a temporary file first — reusing mpv's own request headers, so authenticated and referer-gated hosts stay reachable — instead of being rejected with "Subtitle file not found: https://…". This applies to both the sync target and the alass reference, so a Jimaku or TsukiHime download can be retimed against a stream's own subtitles. Internal tracks of a stream also pass mpv's headers through to `ffmpeg`.
- Alass now works on tracks that arrive as WebVTT, which is what Aniyomi extension streams serve. Alass picks its parser from the file extension and has no WebVTT support, so it treated a `.vtt` reference as a video file and failed with "no audio stream in file". Both the target and the reference are rewritten as SRT for alass, keeping the cue text as-is, and the originals are left untouched.
- Leaving `subsync.alass_path`, `ffsubsync_path`, or `ffmpeg_path` empty now actually auto-discovers the binary, as the config help has always claimed. Previously it fell back to a hard-coded `/usr/bin/<tool>`, which does not exist on macOS and broke subsync for every default-config install there. Discovery searches `PATH` plus the usual install prefixes (a GUI launch inherits a minimal `PATH`) and accepts `alass-cli` as well as `alass`. An explicitly configured path is still used verbatim and never silently substituted.
- Subsync failures are written to the application log. Previously the only trace was an OSD toast that vanished after a few seconds, leaving nothing to diagnose from.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: subtitles
- Keep the subtitle sidebar near playback during gaps when the subtitle file has a cue starting at zero.
+4
View File
@@ -0,0 +1,4 @@
type: docs
area: sync
- Documented compressed transfers, incremental sync cache storage, and compatibility with older peers.
+4
View File
@@ -0,0 +1,4 @@
type: docs
area: stats
- Documented provider reassignment, merge compatibility, and TMDB credential command caching and retry cooldown.
+8
View File
@@ -0,0 +1,8 @@
type: added
area: stats
- Live-action dramas and movies in the stats Library now get posters, synopses, and titles from TMDB. Release builds include a project key, so it works out of the box; `tmdb.apiKey` (or `tmdb.apiKeyCommand`) overrides it, and is required when running from source.
- Unlinked titles that AniList cannot match are looked up on TMDB automatically when the parsed filename matches a Japanese live-action title exactly; otherwise use the new **Link to TMDB** action on a title to pick it by hand.
- Entries linked to the same TMDB title are merged into one card even when they came from different season folders, and the Library kind selector gained a Live Action option alongside Anime and YouTube.
- Provider reassignment preserves the previous link and artwork if the replacement download fails, and refreshes completion totals when the episode count changes. Merges and sync keep conflicting AniList and TMDB identities separate, and the merge dialog explains when a selection mixes the two instead of failing.
- TMDB credential commands cache successful output and wait 30 seconds before retrying failed or empty output, using the bundled key in the meantime when available.
+4
View File
@@ -0,0 +1,4 @@
type: docs
area: anki
- Documented the separate word-audio mapping for animated-image synchronization and that existing images need regeneration to pick up the corrected freeze.
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: yomitan
- Updated bundled Yomitan with upstream 26.9.8 changes, including historical Japanese kana transformations, Ukrainian language support, and improvements to Anki duplicate searches and audio retrieval.
+4
View File
@@ -0,0 +1,4 @@
type: docs
area: stats
- Document YouTube channel filtering and video statistics in the Library.
+7
View File
@@ -0,0 +1,7 @@
type: added
area: stats
- Store YouTube channels as a separate media kind and migrate existing channel entries without changing viewing history or manual video assignments, including channels created after temporarily returning to an older build.
- Add All Titles, Anime, and YouTube Library filters, identify channel pages, and keep channels out of AniList matching, season repair, and duplicate recommendations.
- Keep same-title anime and YouTube records separate in storage and sync while repairing legacy channel classification.
- Refuse merges and video moves that would mix an anime entry with a YouTube channel; the move picker and merge selection only combine entries of the same kind.
+21 -26
View File
@@ -6,31 +6,6 @@
*/ */
{ {
// ==========================================
// Dictionary Backend
// Select the dictionary lookup backend: yomitan or hachidori.
// Restart SubMiner after changing the backend. Each backend keeps separate settings and dictionaries.
// ==========================================
"dictionaryBackend": "yomitan", // Dictionary lookup backend. Restart SubMiner after changing this setting. Values: yomitan | hachidori
// ==========================================
// Hachidori External Dictionary Imports
// Configure the linked Docker host management URL, for example http://127.0.0.1:8780.
// Used only while Hachidori is linked to an external host.
// ==========================================
"hachidori": {
"externalHostManagementUrl": "" // Docker host management URL for automatic character dictionary uploads and replacement. Empty disables external uploads.
}, // Configure the linked Docker host management URL, for example http://127.0.0.1:8780.
// ==========================================
// Subtitle Selection
// Select primary and secondary mpv subtitle tracks from the overlay.
// Hot-reload: enabling or disabling updates the session shortcut immediately.
// ==========================================
"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 // Japanese Subtitle Generation
// Generate timed Japanese subtitles from local audio using whisper.cpp. // Generate timed Japanese subtitles from local audio using whisper.cpp.
@@ -248,7 +223,6 @@
"openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal. "openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
"openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal. "openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
"openTsukihime": "Ctrl+Shift+T", // Accelerator that opens the TsukiHime subtitle search modal (configured secondary/Japanese primary tabs). "openTsukihime": "Ctrl+Shift+T", // Accelerator that opens the TsukiHime subtitle search modal (configured secondary/Japanese primary tabs).
"openSubtitleSelection": "g-s", // Open subtitle selection when enabled. Use g-s to press g then s. Set null to unbind.
"openSubtitleGeneration": "Ctrl+Shift+G", // Accelerator that opens the standalone Japanese subtitle generation modal. "openSubtitleGeneration": "Ctrl+Shift+G", // Accelerator that opens the standalone Japanese subtitle generation modal.
"openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet. "openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet.
"openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal. "openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal.
@@ -385,6 +359,12 @@
"__playlist-browser-open" "__playlist-browser-open"
] // Command setting. ] // Command setting.
}, },
{
"key": "Ctrl+Alt+KeyA", // Key setting.
"command": [
"__anime-browser-open"
] // Command setting.
},
{ {
"key": "Ctrl+Shift+KeyH", // Key setting. "key": "Ctrl+Shift+KeyH", // Key setting.
"command": [ "command": [
@@ -662,6 +642,21 @@
} // Lapis kiku setting. } // Lapis kiku setting.
}, // Automatic Anki updates and media generation options. }, // Automatic Anki updates and media generation options.
// ==========================================
// Anime Browser
// Anime browser sources. SubMiner ships no extension repositories and bundles no sources;
// add a repository index URL here (or drop .apk files in the extensions directory) to have any.
// Hot-reload: autoOpenJimaku applies to the next episode; other anime changes apply the next time the anime browser opens.
// ==========================================
"anime": {
"autoOpenJimaku": false, // Pause Anime Browser playback and open Jimaku when an episode loads. Playback resumes after a subtitle loads or the modal closes only when auto-open initiated the pause. Values: true | false
"extensionsDir": "", // Directory holding Aniyomi extension .apk files. Empty uses <userData>/anime-extensions.
"repos": [], // Extension repository index URLs (any https .json index, e.g. https://.../index.min.json). Empty by default; SubMiner ships no repositories.
"preferredQuality": "", // Preferred stream quality label, matched as a substring (for example: 1080). Empty uses the source order.
"defaultSource": "", // Source the Anime Browser selects when it opens: a source id (<package>:<source>) or "all" for every installed source. Empty selects the first installed source. "Set default" in the Extensions tab writes this value.
"bridgeDir": "" // Directory holding an M-Extension-Server bundle (java runtime plus server jar) to run instead of the copy SubMiner downloads. Empty checks the package-manager install (Arch: mangatan-extension-server), then <userData>/anime-bridge.
}, // Anime browser sources. SubMiner ships no extension repositories and bundles no sources;
// ========================================== // ==========================================
// Jimaku // Jimaku
// Jimaku API configuration and defaults. // Jimaku API configuration and defaults.
+1
View File
@@ -368,6 +368,7 @@ const sidebar: DefaultTheme.SidebarItem[] = [
{ text: 'Anki', link: '/anki-integration' }, { text: 'Anki', link: '/anki-integration' },
{ text: 'Jellyfin', link: '/jellyfin-integration' }, { text: 'Jellyfin', link: '/jellyfin-integration' },
{ text: 'YouTube', link: '/youtube-integration' }, { text: 'YouTube', link: '/youtube-integration' },
{ text: 'Anime Browser', link: '/anime-browser' },
{ text: 'Jimaku', link: '/jimaku-integration' }, { text: 'Jimaku', link: '/jimaku-integration' },
{ text: 'Subtitle Generation', link: '/subtitle-generation' }, { text: 'Subtitle Generation', link: '/subtitle-generation' },
{ text: 'TsukiHime', link: '/tsukihime-integration' }, { text: 'TsukiHime', link: '/tsukihime-integration' },
+419
View File
@@ -0,0 +1,419 @@
# Anime Browser
Search anime sources, pick an episode, and play it in mpv with SubMiner's overlay
and mining tools attached — the same way a local file or a Jellyfin stream works.
Open it with `subminer anime`, with `SubMiner.AppImage --anime`, or from
**Browse Anime** in the tray menu. The window stays open while you watch, so you
can queue the next episode without reopening it.
During playback, `Ctrl+Alt+A` toggles the same browser as a modal inside the mpv
player bounds. It uses a dedicated modal surface, so it stays above fullscreen
playback and closes like the other in-player tools. Toggling it off keeps its
current page and scroll position ready for the next toggle. The standalone
window and the modal keep their own search, selected source, tab, and scroll
state, so using one does not replace or cancel what you were doing in the other.
Both surfaces use the same playback queue, source configuration, and stats
history. Queue changes and the currently playing episode appear in both
immediately, and watched marks come from the same history that playback and the
stats window update. Closing and reopening the modal therefore picks up progress
made from either browser surface.
While the window is open, SubMiner shows a tray icon and — on macOS — appears
in the Cmd+Tab switcher and the Dock (macOS ties the two together), so you can
flip between the browser and mpv. SubMiner normally hides itself from the Dock
because the subtitle overlay needs that to float above fullscreen video; it
hides again when the window closes during playback.
Launching an episode starts a full SubMiner playback session, the same as
playing a local file: the overlay and mining tools attach, and the tray icon
stays available. In standalone `subminer anime` mode, closing the window while
a video is playing leaves playback running — reopen the browser from the tray
(**Browse Anime**). The app only exits with the window when nothing is playing.
## How it works
SubMiner does not implement any anime source itself. It runs **Aniyomi extension
APKs** through a bundled JVM sidecar ([M-Extension-Server][mes]), asks the
selected extension to resolve an episode, and hands the resulting URL to mpv.
```
extension APK → bridge (JVM) → { url, headers } → mpv → SubMiner overlay
```
Because the extension resolves the stream, whichever sources you install decide
what is available. SubMiner runs the installed extensions locally and nothing
more.
## Installing extensions
**SubMiner ships no extension repositories and bundles no sources.** There is no
default repository, no suggested list, and no discovery. Until you add one, the
browser has nothing to search — that is deliberate. Every repository and
extension comes from you, and SubMiner is not affiliated with any of the sites
they connect to.
There are two ways to add extensions.
### From a repository
The window has three tabs — **Browse**, **Extensions**, and **Source settings** —
and each one fills the window, so a long extension list is not squeezed in above
the search results.
Open the **Extensions** tab, paste a repository index URL, and choose
**Add repository**. The URL must be `https` and point at a `.json` index file —
`index.min.json` is the common Aniyomi name, but repositories are free to publish
under another one (for example `video.min.json`). Anything else is rejected
immediately rather than failing later. Everything before the file name is treated
as the repository root, so `.apk` and icon URLs are resolved relative to it.
Extensions your repositories offer but you do not have appear under
**Available**, each with **Install**. Repositories are stored in config under
`anime.repos`, so you can also manage them there and keep them in a dotfile.
Every row carries the extension's icon, as the repository publishes it, so a
site is recognisable before you read the name. A repository row shows its host's
favicon instead. Icons are the only part of a row that is fetched from the
network, and a row whose icon is missing falls back to the first letter of its
name rather than an empty box.
A repository index lists every language it knows about, which is far more than
any one person reads, so the **Available** list has a language chip row above
it. Pick one or more languages to narrow it, or **All** to clear the filter;
picking a language replaces **All** rather than sitting beside it. Rows name the
language in full ("Japanese" rather than `ja`), extensions whose sources span
languages are grouped under **Multi-language**, and the Available heading counts
how many of the offered extensions the filter leaves.
### Managing what is installed
The Extensions tab opens with an **Installed** section listing everything in the
extensions directory, with the sources each one provides and a **Remove**
button. It is built from the directory rather than from a repository, so an
extension you dropped in by hand — or one whose repository you have since
removed — is still listed and still removable. An installed extension borrows
its icon from the catalogue, so one no repository carries shows its monogram.
SubMiner compares the APK's Android version code with the newest build in the
configured repositories. **Update** is clickable only when the repository has a
newer build. A current extension says **Up to date**. If an unusual APK has no
readable version code, its disabled button says **Version unknown**. **Update
all** installs every newer build in one pass and disables itself when there is
nothing to update.
### From a file
Drop Aniyomi `.apk` files into the extensions directory, shown at the top of the
Extensions tab. It defaults to `<userData>/anime-extensions` — on macOS,
`~/Library/Application Support/SubMiner/anime-extensions` — and can be moved with
`anime.extensionsDir`.
A single APK may provide several sources; each appears separately in the
**Source** picker. Extensions that fail to load are listed in the Installed
section with the reason, so a broken APK is visible rather than silently
missing.
An extension that fails to load is skipped rather than blocking the others, so
one bad APK will not hide the rest.
### Default source
The browser opens on the first installed source. To open on a different one,
press **Set default** on its row in the Installed list; the row then carries a
**default** tag, and only one source can carry it. The choice is written to
`anime.defaultSource` and applies the next time a browser window or the
in-player modal opens. With more than one source installed, an **All sources**
row at the top of the list can be the default too. A default that is no longer
installed falls back to the first source.
## Searching every source at once
With more than one source installed, the **Source** picker gains an
**All sources** entry. Searching with it selected runs the query against every
installed source at once, and each source's results appear the moment that
source answers — a fast source is on screen while a slow one is still
resolving. The status bar counts sources as they finish
(`Searching… 3/5 sources · 42 results`).
Each cover is labelled with the source it came from, and opening one always
queries that source, whatever the picker says afterwards.
A source that errors is named in the status bar and the rest still show their
results; one extension that needs a login cannot blank the grid. If every
source fails, the first error is shown in full.
Typing a new search while one is still running simply starts over: results
from the superseded search are discarded, even if its sources answer late.
When a source reports another page, **Load more** appears below the covers.
It appends the next page without duplicating entries that already arrived in
the live result stream. A failed next-page request remains available to retry.
Source settings belong to a single extension, so the **Source settings** tab
asks you to pick one while **All sources** is selected.
## Finding an episode, and what you have watched
An episode list can run to hundreds of entries, so the episode header carries a
filter box:
- A number, `12`, keeps that episode. Sources that report no numbers at all
are still searched by name, so `12` also matches `Episode 12` in a title.
- A range, `12-18`, keeps the episodes between the two, in either order
(`18-12` reads the same). Episodes the source gave no number are left out of
a range.
- Anything else is a case-insensitive substring of the episode name, so `beach`
finds `OVA: Beach Special`.
The counter next to **Episodes** reads `6 of 25` while a filter is applied.
Pressing Escape in the filter box clears it; pressing it anywhere else goes back
to the results grid.
Episodes you have already watched are dimmed and marked `✓ watched`, with a
count in the header. This is not a separate list the browser keeps: it reads the
same stats history the rest of SubMiner writes to, where an episode is marked
watched once a session runs past the completion threshold. Streams are recorded
under a stable per-episode identity, so the mark survives the stream URL
changing between playbacks, and it is the same mark the stats window and
`--mark-watched` use.
Because playback marks an episode partway through the session, the marks
refresh when the browser window comes back to the front: finish an episode in
mpv, switch back, and it is marked. With
[immersion tracking](configuration.md) disabled there is no history to read, so
no episode is marked.
### Marking by hand, and catching up
Right-click an episode for:
- **Mark watched** / **Mark unwatched**: the single episode, whichever way it
is not already.
- **Mark this and N below watched** / **... unwatched**: that episode and every
episode listed below it. Sources list newest first, so "below" is the back
catalogue: right-click the last episode you saw and mark everything down to
the start, which is how you catch up a series you watched somewhere else.
A filter narrows what you are looking at, not what you mark: a span always
covers the full episode list, and the status bar says how many episodes it
touched. The oldest episode has nothing below it, so it only offers the single
entry. Escape closes the menu.
Marking an episode you have never played creates its stats row so the mark has
somewhere to live. That row carries the same series, season and episode fields
playback would have recorded, and both stats library views join the lifetime
tables, so a manually marked episode does not appear there as watch time you
never spent. Clearing a mark never creates anything.
Marks are written to the stats history, so with immersion tracking disabled
there is nowhere to write them and the status bar says so.
## Settings
| Key | Purpose |
| ------------------------ | ------------------------------------------------------------------------ |
| `anime.autoOpenJimaku` | Pause new episodes and open Jimaku for Japanese subtitles. |
| `anime.repos` | Repository index URLs. Empty by default. |
| `anime.extensionsDir` | Where APKs are read from. Empty uses `<userData>/anime-extensions`. |
| `anime.preferredQuality` | Preferred stream label, matched as a substring (for example `1080`). |
| `anime.defaultSource` | Source the browser opens on: a source id or `all`. Empty uses the first. |
| `anime.bridgeDir` | A bridge bundle to run instead of the downloaded one. Empty by default. |
Enable `anime.autoOpenJimaku` to hand each newly loaded Anime Browser episode
to Jimaku. SubMiner pauses playback on its first frame, waits for mpv's video
window to appear, closes the in-player browser if it is open, hides the
standalone browser window, and opens Jimaku with the source title, season, and
episode already filled in. Playback resumes after the selected subtitle loads.
Closing Jimaku also releases the automatic pause, while playback that was
already paused stays paused. A stream that never shows a window resumes without
opening Jimaku. Reopen the browser window with `subminer anime`, the tray
entry, or the shortcut to keep browsing.
## Source settings
Most extensions need configuration before they return anything — a server
address and credentials, a preferred quality, a language filter. Open the
**Source settings** tab to edit them. Changes save as you make them and
persist across restarts in `<userData>/anime-source-preferences.json`.
Each save is handed back to the extension, so it can react: the Jellyfin source
logs in when the address and password land, then fills in its media-library
picker. Password-like fields are masked. Because that file can hold
credentials, it is written with owner-only permissions. Values are scoped to
the exact extension package and source, so two extensions that reuse the same
internal source ID cannot read each other's settings. Preferences saved by an
older build without package ownership are discarded; re-enter those source
settings once after upgrading.
## The bridge
SubMiner looks for the server in three places, in order, and runs the first
one it finds:
1. `anime.bridgeDir`, if set. It must hold a Java runtime and the
`MExtensionServer-*.jar`, laid out as upstream ships them. A directory that
holds neither is an error rather than a silent fallback.
2. A package-manager install. On Arch that is the AUR
[`mangatan-extension-server`](https://aur.archlinux.org/packages/mangatan-extension-server)
package, which Mangatan also uses, at `/usr/share/mangatan/extension_server`.
Installing it means no download, and pacman keeps the bridge current. The
`subminer-bin` package lists it as an optional dependency.
3. SubMiner's own copy in `<userData>/anime-bridge`. The first launch downloads
the newest upstream release that ships a bundle for your platform (~130 MB,
containing the server and a matching Java runtime, so no system JDK is
required), the same way Mangatan does. Releases older than the oldest server
SubMiner is known to work with are skipped. Progress appears in the banner
at the top of the window.
The **Bridge** note at the bottom of the Extensions tab says which one is in
use, its version, and who updates it.
### Updating the bridge
Only the copy SubMiner downloaded is ever updated by SubMiner. A package-manager
install or an `anime.bridgeDir` belongs to whoever put it there.
Once the bridge is running, SubMiner asks GitHub for the newest release and
compares it with the installed version, including package-manager installs and
custom bridge directories. When upstream has published a newer release, the
banner reads "Extension bridge v… is installed; v… is available".
For an AUR bridge, the banner directs you to update `mangatan-extension-server`
through your AUR helper, for example `paru -S mangatan-extension-server`, then
restart SubMiner. Custom installations get a reminder to use their package
manager or original installation method. These notices have no in-app update
button. The check uses upstream releases, so AUR packaging may lag behind the
notice. External installations with an unreadable version do not show a notice.
For SubMiner's own copy, the banner includes an **Update to v…** button.
Clicking it downloads the new release beside the
running bridge, so a failed download changes nothing, then stops the bridge,
swaps the directories, and starts it again. The restart takes a few seconds
and kills the stream of an episode that is playing, the same as when the
bridge exits for any other reason; the queue and browser state survive. The
check is one unauthenticated GitHub API call per bridge start; if it fails
(offline, rate limited) it is logged and no update is offered until the next
start.
Upstream publishes no checksums, so the download is trusted the way Mangatan
and the AUR package trust it: TLS to GitHub and the maintainer's account. That
is the same trust running the server implies in the first place.
The bridge stays running while the window is open. Resolved video URLs point at
its own loopback proxy so the extension's cookies and headers apply, which means
those URLs stop working once it exits — the window keeps it alive for the whole
session.
If the bridge dies anyway (killed by hand, crashed, or stopped mid-operation),
the exit is detected and named in the status bar, and the next request starts a
new one. Playback already in flight still ends when its stream URL dies, but the
browser recovers without an app restart.
Two known limits:
- There is no Android WebView, so extensions that need one (typically for
Cloudflare challenges) will fail with an error from the source.
- Bundles are published for macOS (arm64, x64), Linux (x64), and Windows (x64).
Other platforms are unsupported outright.
## Playback
Selecting an episode resolves the best available stream, applies the source's
required headers as mpv `http-header-fields`, and loads it. The headers are
readable back off mpv, so Anki card audio and screenshots fetch correctly too.
HLS streams are routed through a small local proxy before mpv sees them. Some
hosts disguise their video segments by prepending a fake image header (a real
1x1 PNG) so scrapers back off; Aniyomi's own player strips this, but ffmpeg
probes the segment as a picture and playback dies with "no audio or video data
played". The proxy scans each segment for the first genuine MPEG-TS packet run
and drops whatever junk sits in front of it. Segments that are not TS (fMP4,
subtitles, encryption keys) pass through untouched, and direct-file streams
skip the proxy entirely. Segment URLs disguised behind fake extensions
(`.image`, `.jpg`, `.css`, and friends) are exposed locally with a `.ts`
suffix so current ffmpeg releases accept them when Anki extracts audio,
screenshots, or animated images from the playing stream.
"Playing" in the status bar means playing: after handing mpv the stream,
SubMiner waits until mpv actually configures a video output before reporting
success. If mpv gives up instead — a dead host, an undecodable stream — the
browser shows mpv's error rather than pretending playback started (a failed
load leaves no mpv window, because the player idles windowless).
Slow streams stay in the loading state while mpv is still opening them, even
after the initial playback confirmation deadline. Loading ends when video
appears, mpv reports an error or returns to idle, or another episode replaces
the request.
Failed requests appear in a dismissible panel at the bottom of the browser.
It explains the failed action and suggests a next step. Expand **Technical
details** to read the bridge or mpv error when reporting a problem. Missing
bridge APIs and incomplete extension data have distinct explanations; an
update may be needed, but a compatible release may not yet be available.
Choosing **Queue** resolves the episode and appends the playable stream to mpv's
own playlist immediately, while its subtitle tracks cache in the background.
That makes mpv's next command available at once and lets the next episode begin
automatically when the current one ends, without waiting for another source
request. Resolved stream URLs can be short-lived, so a very long queue can still
outlive what its source issued; dequeue and queue that episode again to refresh
it.
### Japanese audio, and switching tracks
Sources often return a dub and the original audio as two separate entries — or
as two audio tracks of one stream — and the dub is frequently listed first.
SubMiner always aims at the Japanese audio:
- Entries labelled as a dub are skipped as long as another entry exists. This
outranks `anime.preferredQuality`: a 1080p dub is the wrong file, not a better
one. If every entry is a dub, it still plays.
- mpv's `alang` is set to `ja,jpn,jp,japanese` before the file loads, so a
stream carrying several audio tracks starts on the Japanese one. With no
Japanese track, mpv falls back to the first one as usual.
- Any audio or subtitle tracks the extension supplies separately are added to
mpv with `audio-add` / `sub-add`, tagged with their language, and the
Japanese one is selected.
The primary subtitle slot is reserved for Japanese — it is what the overlay
mines. A source that only carries, say, English subtitles does not get them
promoted to primary; instead the track is added with a normalized language tag
(`English` → `en`), and the regular [dual-subtitle settings](configuration.md)
apply: with `secondarySub.autoLoadSecondarySub` enabled and the language listed
in `secondarySub.secondarySubLanguages`, it is picked up as the secondary
subtitle, exactly as it would be for a local file.
Every track is added, including the ones that are not selected, so all of them
appear in mpv's track menu and can be switched by hand while watching
(`#` cycles audio, `j` cycles subtitles by default).
The extension hands over subtitle tracks as URLs, but SubMiner downloads each
one to a temporary directory and gives mpv the local file. mpv is happy either
way; [Subsync](/troubleshooting#subtitle-sync-subsync) is not, because alass
needs a file on disk to use as the timing reference. A track that fails to
download falls back to its URL so the episode still plays, the format is
detected from the file's own content rather than its URL, and the directory is
removed when the next episode starts or the app exits.
### Generating Japanese subtitles
While an episode is playing, press **Ctrl+Shift+G** to [generate Japanese subtitles](/subtitle-generation#generating-for-anime-streams) from its audio. SubMiner fetches the complete episode audio, transcribes it locally with whisper.cpp, and loads the resulting SRT if the same stream is still playing. Generated subtitles stay in the application cache. This requires a finite HTTP/HTTPS stream with an internal audio track; live streams and separately attached audio tracks are not supported.
### Series, season, and episode
The episode's identity travels with it instead of being guessed back out of the
stream URL, which carries nothing but a proxy path and a file extension. The
title and episode label from the source's own listing are split into series,
season, and episode number once, at launch, and everything downstream reads
those fields:
- mpv's title reads `Series S03E04 - Episode Name`.
- Stats group by series, and rewatching an episode reuses its entry instead of
creating a new one.
- The [Jimaku](/jimaku-integration) and [TsukiHime](/tsukihime-integration)
modals open with Title, Season, and Episode already filled in, so a subtitle
search is one keypress rather than a retype.
- [AniList](/anilist-integration) progress updates use those fields directly.
[mes]: https://github.com/1Selxo/M-Extension-Server
-16
View File
@@ -21,14 +21,6 @@ AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the po
AnkiConnect and Kiku/Senren settings follow the [configuration validation rules](/configuration#configuration-file): invalid values produce a warning and fall back to the option's default. Use JSON booleans such as `true`, not strings such as `"true"`, and a positive number for `ankiConnect.pollingRate`. AnkiConnect and Kiku/Senren settings follow the [configuration validation rules](/configuration#configuration-file): invalid values produce a warning and fall back to the option's default. Use JSON booleans such as `true`, not strings such as `"true"`, and a positive number for `ankiConnect.pollingRate`.
### Reusing SubMiner settings in Hachidori
When Hachidori is the selected backend, SubMiner uses its Anki settings to populate Hachidori's first Anki template on startup and when opening its settings. It sets the template's deck to `ankiConnect.deck` when one is configured, copies the configured tags into untouched defaults, then fills missing word, sentence, pronunciation-audio, and picture mappings with fields that exist in Anki. Pronunciation uses `ankiConnect.fields.wordAudio`, falling back to `fields.audio` when no word-audio field is set.
If the note type is unset, SubMiner looks for a unique match containing its configured word and sentence fields. Enabled Lapis, Kiku, or Senren integration narrows the search; Lapis uses its configured model name. A fresh mapping also receives Hachidori's matching preset for readings, definitions, and other recognized fields. If several note types match, choose one in Hachidori Settings. If Anki is closed, open Hachidori Settings again after starting Anki to retry.
The deck always follows `ankiConnect.deck`, as it does for Yomitan's mining deck, because polling mode only looks for new cards in that deck. Existing custom tags, field mappings, advanced templates, and additional templates stay intact. Apart from the deck, this fills missing settings rather than continually overwriting Hachidori choices. The Anki endpoint continues to follow SubMiner's proxy configuration. Sentence audio, image timing, translation, metadata, and duplicate field grouping remain controlled by SubMiner; pronunciation sources are configured in Hachidori. Linking an external dictionary host does not change this behavior.
## Auto-enrichment transport ## Auto-enrichment transport
When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, and image fields automatically. Two detection methods are available: When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, and image fields automatically. Two detection methods are available:
@@ -149,8 +141,6 @@ Field names are matched against your Anki note type case-insensitively (an exact
`fields.wordAudio` selects the existing dictionary-audio field used to calculate the animated image's opening freeze. This mapping only reads audio; `fields.audio` still controls where generated sentence audio is written. See [config.example.jsonc](/config.example.jsonc) for defaults. `fields.wordAudio` selects the existing dictionary-audio field used to calculate the animated image's opening freeze. This mapping only reads audio; `fields.audio` still controls where generated sentence audio is written. See [config.example.jsonc](/config.example.jsonc) for defaults.
When Hachidori mines through SubMiner's Anki proxy, it prepares downloadable word audio before saving the note so the animation delay can be measured on the first mine. Configure a downloadable pronunciation source in Hachidori's Audio settings; browser speech cannot be saved into Anki by the SubMiner overlay. If pronunciation is unavailable, Hachidori reports a warning and the card has no word-audio hold.
These mappings always control normal word-card enrichment, including Yomitan proxy/polling updates and manual clipboard updates. Enabling Lapis or Kiku does not replace the configured word-card sentence and audio fields with `Sentence` and `SentenceAudio`. The dedicated sentence-card and audio-card shortcuts still use those Lapis/Kiku field names. These mappings always control normal word-card enrichment, including Yomitan proxy/polling updates and manual clipboard updates. Enabling Lapis or Kiku does not replace the configured word-card sentence and audio fields with `Sentence` and `SentenceAudio`. The dedicated sentence-card and audio-card shortcuts still use those Lapis/Kiku field names.
Two related options live alongside `fields`: `ankiConnect.deck` (target deck; empty falls back as described above) and `ankiConnect.tags` (tags added to mined cards, default `["SubMiner"]`; set `[]` to disable tagging). The `miscInfo` content is controlled by `ankiConnect.metadata.pattern` (default `[SubMiner] %f (%t)`; tokens: `%f` filename, `%F` filename with extension, `%t` timestamp, `%T` timestamp with milliseconds, `<br>` newline). Two related options live alongside `fields`: `ankiConnect.deck` (target deck; empty falls back as described above) and `ankiConnect.tags` (tags added to mined cards, default `["SubMiner"]`; set `[]` to disable tagging). The `miscInfo` content is controlled by `ankiConnect.metadata.pattern` (default `[SubMiner] %f (%t)`; tokens: `%f` filename, `%F` filename with extension, `%t` timestamp, `%T` timestamp with milliseconds, `<br>` newline).
@@ -202,8 +192,6 @@ Overlay and stats-dashboard mining use the same `media.maxMediaDuration` limit.
Set `media.reviewTiming` to `true` to pause playback and check the clip before its media is generated. It applies to word, sentence, and audio cards. Set `media.reviewTiming` to `true` to pause playback and check the clip before its media is generated. It applies to word, sentence, and audio cards.
Playback stays paused while the review is open, even if the dictionary popup or subtitle hover that paused it goes away. When the review closes, playback resumes if it was playing before the review or if the popup closed in the meantime. A dictionary popup that is still open keeps playback paused.
The review opens on the subtitle range plus your configured audio padding. Subtitles usually hang around after the dialogue has stopped, so once the waveform loads, an untouched clip end pulls back to just after the last speech in the line. The Line end rail still marks the original subtitle timing, Reset puts it back, and a line whose speech runs right through its end is left alone. The review opens on the subtitle range plus your configured audio padding. Subtitles usually hang around after the dialogue has stopped, so once the waveform loads, an untouched clip end pulls back to just after the last speech in the line. The Line end rail still marks the original subtitle timing, Reset puts it back, and a line whose speech runs right through its end is left alone.
**Adjusting the clip.** Drag either edge to trim, drag the middle to slide the whole clip without changing its length, or click anywhere on the waveform to snap the nearer edge there. A focused edge also moves with the arrow keys: 100 ms per press, or 500 ms with Shift. The 100 ms buttons do the same thing. Earlier and Later each reveal two more seconds of timeline without moving the selection. **Adjusting the clip.** Drag either edge to trim, drag the middle to slide the whole clip without changing its length, or click anywhere on the waveform to snap the nearer edge there. A focused edge also moves with the arrow keys: 100 ms per press, or 500 ms with Shift. The 100 ms buttons do the same thing. Earlier and Later each reveal two more seconds of timeline without moving the selection.
@@ -224,8 +212,6 @@ Confirming writes the combined lines to the sentence field. Reset drops the adde
**Canceling.** You can go back to editing, finish with the original timing, create the card without audio or an image, or discard it. Discard deletes an existing Yomitan or audio card, and skips creation entirely for a direct sentence card. A failed audio preview does not block confirmation or card creation. **Canceling.** You can go back to editing, finish with the original timing, create the card without audio or an image, or discard it. Discard deletes an existing Yomitan or audio card, and skips creation entirely for a direct sentence card. A failed audio preview does not block confirmation or card creation.
When word-card enrichment changes the sentence context, including an expanded timing-review selection, SubMiner regenerates `SentenceFurigana` from the final sentence. Unchanged sentences keep their existing furigana formatting. If generation fails, SubMiner clears stale furigana so compatible templates can fall back to `Sentence`.
Clipboard updates and stats-dashboard mining never open timing review. The option is off by default and hot-reloads. **Review Media Timing** in the runtime options palette (`Ctrl/Cmd+Shift+O`) toggles it for the current session. Clipboard updates and stats-dashboard mining never open timing review. The option is off by default and hot-reloads. **Review Media Timing** in the runtime options palette (`Ctrl/Cmd+Shift+O`) toggles it for the current session.
If SubMiner closes the overlay while a timing review is still loading, it cancels pending setup and modal retries and restores playback if the review paused it. A new timing review can start after the overlay reopens. If SubMiner closes the overlay while a timing review is still loading, it cancels pending setup and modal retries and restores playback if the review paused it. A new timing review can start after the overlay reopens.
@@ -329,8 +315,6 @@ Word cards get a card-type flag when SubMiner fills their sentence, whether that
When you mine the same word multiple times, SubMiner can merge the cards instead of creating duplicates. This is designed for note types that support grouped fields: [Kiku](https://github.com/youyoumu/kiku) and [Senren](https://github.com/BrenoAqua/Senren) (which calls the feature scene switching). When you mine the same word multiple times, SubMiner can merge the cards instead of creating duplicates. This is designed for note types that support grouped fields: [Kiku](https://github.com/youyoumu/kiku) and [Senren](https://github.com/BrenoAqua/Senren) (which calls the feature scene switching).
Field grouping runs when a new note is added with known duplicates. With the Hachidori backend that is the popup's **Add anyway** choice; **Overwrite** updates the existing note in place and only receives media enrichment.
```jsonc ```jsonc
"ankiConnect": { "ankiConnect": {
"isKiku": { "isKiku": {
+2 -2
View File
@@ -73,8 +73,8 @@ src/
shared/ipc/ # Cross-process IPC channel constants + payload validators shared/ipc/ # Cross-process IPC channel constants + payload validators
renderer/ # Overlay renderer (modularized UI/runtime) renderer/ # Overlay renderer (modularized UI/runtime)
handlers/ # Keyboard/mouse/gamepad interaction modules handlers/ # Keyboard/mouse/gamepad interaction modules
modals/ # Modal flows (Jimaku, Kiku, subsync, runtime options, session help, modals/ # Modal flows (Anime Browser, Jimaku, Kiku, subsync, runtime options,
# changelog, character dictionary, playlist browser, subtitle # session help, changelog, character dictionary, playlist browser, subtitle
# sidebar, YouTube track picker, controller config/debug/select) # sidebar, YouTube track picker, controller config/debug/select)
positioning/ # Subtitle position controller (drag-to-reposition) positioning/ # Subtitle position controller (drag-to-reposition)
settings/ # Settings window UI (model, controls, markup) settings/ # Settings window UI (model, controls, markup)
+8 -134
View File
@@ -1,132 +1,6 @@
# Changelog # Changelog
## v0.20.0 (2026-09-23) ## v0.19.6 (2026-09-04)
**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**
@@ -157,7 +31,7 @@
- **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. - **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. - **Secondary Subtitle Visibility**: Native mpv secondary subtitles stay hidden when switching secondary subtitle tracks during playback.
<h2>v0.19.5 (2026-08-30)</h2> ## v0.19.5 (2026-08-30)
**Fixed** **Fixed**
@@ -172,7 +46,7 @@
- Long speech is paged instead of covering the video with a wall of text. - Long speech is paged instead of covering the video with a wall of text.
- Explicitly timed sound cues like `[音楽]` no longer cover later dialogue. - Explicitly timed sound cues like `[音楽]` no longer cover later dialogue.
<h2>v0.19.4 (2026-08-25)</h2> ## v0.19.4 (2026-08-25)
**Added** **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. - **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.
@@ -213,7 +87,7 @@
</details> </details>
<h2>v0.19.3 (2026-08-13)</h2> ## v0.19.3 (2026-08-13)
**Added** **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`). - 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`).
@@ -237,7 +111,7 @@
</details> </details>
<h2>v0.19.2 (2026-08-04)</h2> ## v0.19.2 (2026-08-04)
**Changed** **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. - 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.
@@ -255,7 +129,7 @@
</details> </details>
<h2>v0.19.1 (2026-08-01)</h2> ## v0.19.1 (2026-08-01)
**Added** **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. - 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.
@@ -264,7 +138,7 @@
- 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. - 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. - 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> ## v0.19.0 (2026-07-29)
**Added** **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. - 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.
@@ -300,7 +174,7 @@
</details> </details>
</details> ## Previous Versions
<details> <details>
<summary>v0.18.x</summary> <summary>v0.18.x</summary>
+3 -1
View File
@@ -35,6 +35,8 @@ Character dictionary sync is disabled by default. To turn it on:
::: tip ::: tip
The first sync for a media title takes a few seconds while character data and portraits are fetched from AniList. Subsequent launches reuse the cached media match and snapshot without a fresh AniList lookup. The first sync for a media title takes a few seconds while character data and portraits are fetched from AniList. Subsequent launches reuse the cached media match and snapshot without a fresh AniList lookup.
Dictionary sync does not require a loaded Japanese subtitle track. Once Yomitan is ready, SubMiner can reuse an installed character dictionary even before the first subtitle is processed. If Yomitan readiness times out, the status reports a failure instead of staying on **Checking**.
::: :::
::: info ::: info
@@ -168,7 +170,7 @@ These phases are emitted through the configured notification surface. Some phase
2. **generating** - No cache hit: fetch characters from AniList GraphQL, download portraits (250ms throttle between image requests), save snapshot JSON. 2. **generating** - No cache hit: fetch characters from AniList GraphQL, download portraits (250ms throttle between image requests), save snapshot JSON.
3. MRU update (no notification) - add the media ID to the most-recently-used list and evict old entries beyond `maxLoaded`. 3. MRU update (no notification) - add the media ID to the most-recently-used list and 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. 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). 5. **importing** - Push the ZIP into Yomitan when the installed revision differs. Dictionary checks and settings updates wait for the extension to be ready, independently of subtitle processing. Readiness and quick operations have bounded timeouts; imports have a separate budget that grows with the ZIP size.
6. **ready** - Dictionary is live. Character names will match on the next subtitle line. 6. **ready** - Dictionary is live. Character names will match on the next subtitle line.
**State tracking** is persisted in `character-dictionaries/auto-sync-state.json`. AniList media matches are cached separately in `character-dictionaries/anilist-resolution-cache.json` so snapshot hits do not need another AniList search. **State tracking** is persisted in `character-dictionaries/auto-sync-state.json`. AniList media matches are cached separately in `character-dictionaries/anilist-resolution-cache.json` so snapshot hits do not need another AniList search.
+41 -20
View File
@@ -154,6 +154,7 @@ The configuration file includes several main sections:
**External Integrations** **External Integrations**
- [**Anime Browser**](#anime-browser) - Extension repositories and stream preferences for the anime browser
- [**Jimaku**](#jimaku) - Jimaku API configuration and defaults - [**Jimaku**](#jimaku) - Jimaku API configuration and defaults
- [**TsukiHime**](#tsukihime) - Multi-language subtitle search and download - [**TsukiHime**](#tsukihime) - Multi-language subtitle search and download
- [**TMDB**](#tmdb) - Posters and synopses for live-action dramas and movies in the stats Library - [**TMDB**](#tmdb) - Posters and synopses for live-action dramas and movies in the stats Library
@@ -171,14 +172,6 @@ The configuration file includes several main sections:
## Core settings ## Core settings
### Dictionary backend
`dictionaryBackend` accepts `"yomitan"` or `"hachidori"`. The default is defined in the [generated configuration example](/config.example.jsonc). Restart SubMiner after changing it. The tray shows the selected backend's settings, and the global dictionary-settings shortcut follows the same selection.
Each backend stores its own dictionaries and mining settings. `yomitan.externalProfilePath` applies only to Yomitan. See [Hachidori setup](./usage.md#hachidori-setup) before switching an existing installation.
`hachidori.externalHostManagementUrl` specifies the linked Docker host's HTTP(S) management origin for automatic character dictionary uploads and replacement. Use the management port, not the sharing or dictionary API port. See [Hachidori setup](./usage.md#hachidori-setup) for an example and [the generated configuration example](/config.example.jsonc) for the default.
### Logging ### Logging
Control the minimum log level for runtime output: Control the minimum log level for runtime output:
@@ -599,6 +592,7 @@ See `config.example.jsonc` for detailed configuration options and more examples.
| `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track | | `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track |
| `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track | | `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track |
| `Ctrl+Alt+KeyP` | `["__playlist-browser-open"]` | Open playlist browser | | `Ctrl+Alt+KeyP` | `["__playlist-browser-open"]` | Open playlist browser |
| `Ctrl+Alt+KeyA` | `["__anime-browser-open"]` | Toggle Anime Browser in the player |
| `Ctrl+Alt+KeyC` | `["__youtube-picker-open"]` | Open the manual YouTube subtitle picker | | `Ctrl+Alt+KeyC` | `["__youtube-picker-open"]` | Open the manual YouTube subtitle picker |
| `ArrowRight` | `["seek", 5]` | Seek forward 5 seconds | | `ArrowRight` | `["seek", 5]` | Seek forward 5 seconds |
| `ArrowLeft` | `["seek", -5]` | Seek backward 5 seconds | | `ArrowLeft` | `["seek", -5]` | Seek backward 5 seconds |
@@ -640,7 +634,7 @@ See `config.example.jsonc` for detailed configuration options and more examples.
{ "key": "Space", "command": null } { "key": "Space", "command": null }
``` ```
**Special commands:** Commands prefixed with `__` are handled internally by the overlay rather than sent to mpv. `__playlist-browser-open` opens the split-pane playlist browser for the current file's parent directory and the live mpv queue. `__replay-subtitle` replays the current subtitle and pauses at its end. `__play-next-subtitle` seeks to the next subtitle, plays it, and pauses at its end. `__runtime-options-open` opens the runtime options palette. `__runtime-option-cycle:<id>[:next|prev]` cycles a runtime option value. **Special commands:** Commands prefixed with `__` are handled internally by the overlay rather than sent to mpv. `__playlist-browser-open` opens the split-pane playlist browser for the current file's parent directory and the live mpv queue. `__anime-browser-open` opens the Anime Browser inside the player bounds. `__replay-subtitle` replays the current subtitle and pauses at its end. `__play-next-subtitle` seeks to the next subtitle, plays it, and pauses at its end. `__runtime-options-open` opens the runtime options palette. `__runtime-option-cycle:<id>[:next|prev]` cycles a runtime option value.
**Supported commands:** Any valid mpv JSON IPC command array (`["cycle", "pause"]`, `["seek", 5]`, `["script-binding", "..."]`, etc.) **Supported commands:** Any valid mpv JSON IPC command array (`["cycle", "pause"]`, `["seek", 5]`, `["script-binding", "..."]`, etc.)
@@ -1125,16 +1119,37 @@ When the manual merge popup opens, SubMiner pauses playback and closes any open
<a :href="withBase('/assets/kiku-integration.webm')" target="_blank" rel="noreferrer">Open demo in a new tab</a> <a :href="withBase('/assets/kiku-integration.webm')" target="_blank" rel="noreferrer">Open demo in a new tab</a>
## Subtitle Selection
Enable **Settings → Behavior → Subtitle Selection → Enabled** to choose mpv's primary and secondary subtitle tracks from a SubMiner modal. The feature is disabled by default. The dialog uses the same overlay focus and subtitle suppression behavior as the other modals.
Press `g` then `s` to open it. Both selectors include **None**. Choose different tracks and click **Apply** to load them into mpv, or close the dialog to keep the current selection. Embedded and already-loaded external subtitle tracks are listed with their title, language, and codec when available.
`subtitleSelection.enabled` controls the feature. `shortcuts.openSubtitleSelection` changes its shortcut, or accepts `null` to unbind it. Enabling the feature overrides mpv's binding for that shortcut when its first key is free; disabling it restores mpv's binding. Existing single-key actions take priority over sequences; see [shortcut conflicts](/shortcuts). Both settings apply immediately. See the [generated configuration example](/config.example.jsonc) for defaults.
## External integrations ## External integrations
### Anime Browser
Sources for the [anime browser](/anime-browser). SubMiner ships no extension repositories and bundles no sources, so these are empty until you add one:
```json
{
"anime": {
"autoOpenJimaku": false,
"extensionsDir": "",
"repos": [],
"preferredQuality": "",
"defaultSource": ""
}
}
```
| Option | Type | Default | Description |
| ------------------------ | ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `anime.autoOpenJimaku` | `boolean` | `false` | Pause Anime Browser playback and open Jimaku when an episode loads. Playback resumes after a subtitle loads or the modal closes. Playback that was already paused stays paused. |
| `anime.extensionsDir` | `string` | `""` | Directory holding Aniyomi extension `.apk` files. Empty uses `<userData>/anime-extensions`. |
| `anime.repos` | `string[]` | `[]` | Extension repository index URLs. Any `https` URL ending in `.json` works; `index.min.json` is only the common name. |
| `anime.preferredQuality` | `string` | `""` | Preferred stream quality label, matched as a substring (for example `1080`). Empty keeps the source's own order. A Japanese-audio entry always outranks a higher-quality dub. |
| `anime.defaultSource` | `string` | `""` | Source the Anime Browser selects when it opens: a source id (`<package>:<source>`) or `all` for every installed source. Empty selects the first installed source. **Set default** in the Extensions tab writes this value. |
| `anime.bridgeDir` | `string` | `""` | Directory holding an M-Extension-Server bundle (Java runtime plus server jar) to run instead of the downloaded copy. Empty checks the package-manager install first, then `<userData>/anime-bridge`. See [the bridge](anime-browser.md#the-bridge). |
Repositories added from the browser's Extensions tab are written back to `anime.repos`, so the list can also be kept in a dotfile. Changes apply the next time the anime browser opens.
Per-source settings (server addresses, credentials, per-extension quality or language options) are not part of `config.jsonc`. They belong to the extension, are edited in the browser's **Source settings** tab, and persist in `<userData>/anime-source-preferences.json` with owner-only permissions.
### Jimaku ### Jimaku
Configure Jimaku API access and defaults: Configure Jimaku API access and defaults:
@@ -1229,11 +1244,17 @@ Sync a subtitle track from the overlay picker using `alass` or `ffsubsync`. The
| Option | Values | Description | | Option | Values | Description |
| ---------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------- | | ---------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `alass_path` | string path | Path to `alass` executable. Empty falls back to `/usr/bin/alass`. `alass` must be installed separately. | | `alass_path` | string path | Path to `alass` executable. Empty auto-discovers `alass` or `alass-cli`. `alass` must be installed separately. |
| `ffsubsync_path` | string path | Path to `ffsubsync` executable. Empty falls back to `/usr/bin/ffsubsync`. `ffsubsync` must be installed separately. | | `ffsubsync_path` | string path | Path to `ffsubsync` executable. Empty auto-discovers `ffsubsync`. `ffsubsync` must be installed separately. |
| `ffmpeg_path` | string path | Path to `ffmpeg` (used for internal subtitle extraction). Empty or `null` falls back to `/usr/bin/ffmpeg`. | | `ffmpeg_path` | string path | Path to `ffmpeg` (used for internal subtitle extraction). Empty or `null` auto-discovers `ffmpeg`. |
| `replace` | `true`, `false` | When `true` (default), overwrite the active subtitle file on successful sync. When `false`, write `<name>_retimed.<ext>`. | | `replace` | `true`, `false` | When `true` (default), overwrite the active subtitle file on successful sync. When `false`, write `<name>_retimed.<ext>`. |
Auto-discovery searches `PATH`, then the usual install prefixes (`/opt/homebrew/bin`, `/usr/local/bin`, `/opt/local/bin`, `/usr/bin`, `/bin`) — a GUI launch inherits a minimal `PATH` that often omits the first two. Set the option explicitly if your binary lives elsewhere.
Subtitle tracks that mpv loaded from a URL (Aniyomi extension streams, Jellyfin) are downloaded to a temporary file first, reusing mpv's own request headers, so they can be used as either the sync target or the alass reference.
Stats dashboard sentence mining also uses `alass_path` when available to align a local English sidecar against the local Japanese sidecar before filling the card translation field. This stats-only retime writes a temporary cached copy and never edits the original subtitle files.
Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`. Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
Customize it there, or set it to `null` to disable. Customize it there, or set it to `null` to disable.
+1 -3
View File
@@ -152,14 +152,12 @@ Stats server config lives under `stats`:
The Search tab and the Vocabulary tab's word detail panel both mine from subtitle lines in your viewing history. Search matches sentence text and media titles, and **Search by headword** is enabled by default so dictionary-form searches such as `知らない` can find tracked subtitle lines with inflected variants. Turn that toggle off for exact text/title matching only. Each line with a valid source file offers sentence-card mining; word/audio mining is available when the selected word or searched word appears in the sentence: The Search tab and the Vocabulary tab's word detail panel both mine from subtitle lines in your viewing history. Search matches sentence text and media titles, and **Search by headword** is enabled by default so dictionary-form searches such as `知らない` can find tracked subtitle lines with inflected variants. Turn that toggle off for exact text/title matching only. Each line with a valid source file offers sentence-card mining; word/audio mining is available when the selected word or searched word appears in the sentence:
- **Mine Word** - looks up the word with the selected dictionary backend, Yomitan or Hachidori, then enriches the card with sentence audio, a screenshot or animated AVIF clip, the highlighted sentence, full-sentence readings in `SentenceFurigana` when that field exists, and metadata extracted from the source video file. The selected history line supplies the card's context even while another subtitle is playing in mpv. Hachidori uses its configured Anki template, dictionary aliases, and frequency metadata. Requires Anki and the selected backend's dictionaries to be loaded. - **Mine Word** - performs a full Yomitan dictionary lookup for the word (definition, reading, pitch accent, etc.) via a short-lived hidden helper, then enriches the card with sentence audio, a screenshot or animated AVIF clip, the highlighted sentence, and metadata extracted from the source video file. Requires Anki and Yomitan dictionaries to be loaded.
- **Mine Sentence** - creates a sentence card directly with the `IsSentenceCard` flag set (for Lapis/Kiku workflows), along with audio and image from the source video. - **Mine Sentence** - creates a sentence card directly with the `IsSentenceCard` flag set (for Lapis/Kiku workflows), along with audio and image from the source video.
- **Mine Audio** - creates an audio-only card with the `IsAudioCard` flag, attaching only the sentence audio clip. - **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. 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.
Stats cards also receive the `SubMiner::Stats` tag. SubMiner uses it to preserve their selected history context when detecting new cards through polling with the Anki proxy disabled.
Secondary subtitle text is stored alongside primary subtitles during playback, but the Search tab does not use it for display or matching. Secondary subtitle text is stored alongside primary subtitles during playback, but the Search tab does not use it for display or matching.
### Word exclusion list ### Word exclusion list
+6 -2
View File
@@ -120,10 +120,10 @@ pip install ffsubsync
Windows 10 or later. No compositor tools or window helpers are needed - native window tracking is built in. Windows 10 or later. No compositor tools or window helpers are needed - native window tracking is built in.
You need **mpv** (required) and **ffmpeg** (strongly recommended, for card audio and screenshots). Put mpv on `PATH` or set `mpv.executablePath` during setup. ffmpeg must be on `PATH`. You need **mpv** (required) and **ffmpeg** (strongly recommended, for card audio and screenshots), and both must be on your `PATH`.
::: tip What is PATH? ::: tip What is PATH?
`PATH` is the list of folders Windows searches when a program asks to run another program by name. SubMiner uses it to find ffmpeg and, unless an executable path is configured, mpv. The routes below mostly handle `PATH` for you; the manual route explains how to add a folder yourself. `PATH` is the list of folders Windows searches when a program asks to run another program by name. SubMiner runs `mpv` and `ffmpeg` by name, so if their folders are not on `PATH`, SubMiner cannot find them even though they are installed. The routes below mostly handle `PATH` for you; the manual route explains how to add a folder yourself.
::: :::
You can install these with a package manager or by hand. Coverage differs, so pick based on what you need: You can install these with a package manager or by hand. Coverage differs, so pick based on what you need:
@@ -221,6 +221,10 @@ Install [`subminer-bin`](https://aur.archlinux.org/packages/subminer-bin) from t
paru -S subminer-bin paru -S subminer-bin
``` ```
For the [anime browser](anime-browser.md#the-bridge), optionally add
`mangatan-extension-server`. SubMiner picks the package up instead of
downloading its own copy of the bridge, and pacman keeps it updated.
Or manually: Or manually:
```bash ```bash
+1 -2
View File
@@ -12,7 +12,7 @@ This is the recommended way to use Jellyfin with SubMiner. A terminal-only optio
## Requirements ## Requirements
- A Jellyfin server plus your username and password (Jellyfin 12, which disables legacy authorization by default, is supported) - A Jellyfin server plus your username and password
- SubMiner installed and running (see [Installation](/installation)) - SubMiner installed and running (see [Installation](/installation))
- On Linux, the session token is stored with `gnome-libsecret` by default - On Linux, the session token is stored with `gnome-libsecret` by default
@@ -50,7 +50,6 @@ From then on, pause / resume / seek / stop and audio or subtitle track changes y
## What happens during playback ## What happens during playback
- **mpv launches automatically.** If mpv isn't already running when you cast, SubMiner starts it with SubMiner defaults and the bundled mpv plugin, so keybindings work right away. - **mpv launches automatically.** If mpv isn't already running when you cast, SubMiner starts it with SubMiner defaults and the bundled mpv plugin, so keybindings work right away.
- **Windows respects your mpv settings.** Casting checks `mpv.executablePath`, then `SUBMINER_MPV_PATH`, then `PATH`. An invalid configured path prevents automatic startup.
- **The overlay is managed by SubMiner,** so your configured `subtitleStyle` controls how subtitles look. Use the [overlay-toggle shortcut](/shortcuts) to hide it for a session. - **The overlay is managed by SubMiner,** so your configured `subtitleStyle` controls how subtitles look. Use the [overlay-toggle shortcut](/shortcuts) to hide it for a session.
- **Resume works.** If Jellyfin has a saved position for the item, SubMiner seeks there on load. - **Resume works.** If Jellyfin has a saved position for the item, SubMiner seeks there on load.
- **Titles and credentials stay separate.** AniList, character dictionaries, Anki source fields, and Discord presence use media titles, never authenticated stream URLs. If a usable title is unavailable, lookups are skipped and source fields show an unknown-media label. Stats identifies Jellyfin videos by server and item ID without the stream URL or API key. - **Titles and credentials stay separate.** AniList, character dictionaries, Anki source fields, and Discord presence use media titles, never authenticated stream URLs. If a usable title is unavailable, lookups are skipped and source fields show an unknown-media label. Stats identifies Jellyfin videos by server and item ID without the stream URL or API key.
+4
View File
@@ -81,6 +81,8 @@ SubMiner extracts media info from the current video path to pre-fill the search
- **Season + episode patterns:** `S01E03`, `1x03` - **Season + episode patterns:** `S01E03`, `1x03`
- **Episode-only patterns:** `E03`, `EP03`, or dash-separated numbers like `Title - 03 -` - **Episode-only patterns:** `E03`, `EP03`, or dash-separated numbers like `Title - 03 -`
- **Spelled-out episode labels:** `Episode 4`, `第4話`
- **Season named at the end of the title:** `… Season 3`, `… 3rd Season`, `… S3`, `… 第3期` - the season goes in the Season field instead of being searched for as part of the series name
- **Season folders:** a parent directory named `Season 2` or `S2` fills in the season when the filename lacks one - **Season folders:** a parent directory named `Season 2` or `S2` fills in the season when the filename lacks one
- **Bracket tags:** `[SubGroup]`, `[1080p]`, `[HEVC]` - stripped before title extraction - **Bracket tags:** `[SubGroup]`, `[1080p]`, `[HEVC]` - stripped before title extraction
- **Year tags:** `(2024)` - stripped - **Year tags:** `(2024)` - stripped
@@ -89,6 +91,8 @@ SubMiner extracts media info from the current video path to pre-fill the search
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. 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.
Episodes launched from the [anime browser](/anime-browser) skip parsing entirely: Title, Season, and Episode come from the source's own listing, which is why the search runs immediately even though the stream URL says nothing about the episode.
## Troubleshooting ## Troubleshooting
**"Jimaku API key not set"** **"Jimaku API key not set"**
+21 -26
View File
@@ -6,31 +6,6 @@
*/ */
{ {
// ==========================================
// Dictionary Backend
// Select the dictionary lookup backend: yomitan or hachidori.
// Restart SubMiner after changing the backend. Each backend keeps separate settings and dictionaries.
// ==========================================
"dictionaryBackend": "yomitan", // Dictionary lookup backend. Restart SubMiner after changing this setting. Values: yomitan | hachidori
// ==========================================
// Hachidori External Dictionary Imports
// Configure the linked Docker host management URL, for example http://127.0.0.1:8780.
// Used only while Hachidori is linked to an external host.
// ==========================================
"hachidori": {
"externalHostManagementUrl": "" // Docker host management URL for automatic character dictionary uploads and replacement. Empty disables external uploads.
}, // Configure the linked Docker host management URL, for example http://127.0.0.1:8780.
// ==========================================
// Subtitle Selection
// Select primary and secondary mpv subtitle tracks from the overlay.
// Hot-reload: enabling or disabling updates the session shortcut immediately.
// ==========================================
"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 // Japanese Subtitle Generation
// Generate timed Japanese subtitles from local audio using whisper.cpp. // Generate timed Japanese subtitles from local audio using whisper.cpp.
@@ -248,7 +223,6 @@
"openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal. "openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
"openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal. "openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
"openTsukihime": "Ctrl+Shift+T", // Accelerator that opens the TsukiHime subtitle search modal (configured secondary/Japanese primary tabs). "openTsukihime": "Ctrl+Shift+T", // Accelerator that opens the TsukiHime subtitle search modal (configured secondary/Japanese primary tabs).
"openSubtitleSelection": "g-s", // Open subtitle selection when enabled. Use g-s to press g then s. Set null to unbind.
"openSubtitleGeneration": "Ctrl+Shift+G", // Accelerator that opens the standalone Japanese subtitle generation modal. "openSubtitleGeneration": "Ctrl+Shift+G", // Accelerator that opens the standalone Japanese subtitle generation modal.
"openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet. "openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet.
"openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal. "openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal.
@@ -385,6 +359,12 @@
"__playlist-browser-open" "__playlist-browser-open"
] // Command setting. ] // Command setting.
}, },
{
"key": "Ctrl+Alt+KeyA", // Key setting.
"command": [
"__anime-browser-open"
] // Command setting.
},
{ {
"key": "Ctrl+Shift+KeyH", // Key setting. "key": "Ctrl+Shift+KeyH", // Key setting.
"command": [ "command": [
@@ -662,6 +642,21 @@
} // Lapis kiku setting. } // Lapis kiku setting.
}, // Automatic Anki updates and media generation options. }, // Automatic Anki updates and media generation options.
// ==========================================
// Anime Browser
// Anime browser sources. SubMiner ships no extension repositories and bundles no sources;
// add a repository index URL here (or drop .apk files in the extensions directory) to have any.
// Hot-reload: autoOpenJimaku applies to the next episode; other anime changes apply the next time the anime browser opens.
// ==========================================
"anime": {
"autoOpenJimaku": false, // Pause Anime Browser playback and open Jimaku when an episode loads. Playback resumes after a subtitle loads or the modal closes only when auto-open initiated the pause. Values: true | false
"extensionsDir": "", // Directory holding Aniyomi extension .apk files. Empty uses <userData>/anime-extensions.
"repos": [], // Extension repository index URLs (any https .json index, e.g. https://.../index.min.json). Empty by default; SubMiner ships no repositories.
"preferredQuality": "", // Preferred stream quality label, matched as a substring (for example: 1080). Empty uses the source order.
"defaultSource": "", // Source the Anime Browser selects when it opens: a source id (<package>:<source>) or "all" for every installed source. Empty selects the first installed source. "Set default" in the Extensions tab writes this value.
"bridgeDir": "" // Directory holding an M-Extension-Server bundle (java runtime plus server jar) to run instead of the copy SubMiner downloads. Empty checks the package-manager install (Arch: mangatan-extension-server), then <userData>/anime-bridge.
}, // Anime browser sources. SubMiner ships no extension repositories and bundles no sources;
// ========================================== // ==========================================
// Jimaku // Jimaku
// Jimaku API configuration and defaults. // Jimaku API configuration and defaults.
+4 -6
View File
@@ -15,7 +15,7 @@ All shortcuts are configurable in `config.jsonc` under `shortcuts` and `keybindi
| Shortcut | Action | Scope | Configurable | | Shortcut | Action | Scope | Configurable |
| ------------- | ---------------------- | -------------------------------------------- | -------------------------------------- | | ------------- | ---------------------- | -------------------------------------------- | -------------------------------------- |
| `Alt+Shift+O` | Toggle visible overlay | Works while the overlay or mpv has focus | `shortcuts.toggleVisibleOverlayGlobal` | | `Alt+Shift+O` | Toggle visible overlay | Works while the overlay or mpv has focus | `shortcuts.toggleVisibleOverlayGlobal` |
| `Alt+Shift+Y` | Open active dictionary settings | OS-global (registered with the OS) | Fixed (not configurable) | | `Alt+Shift+Y` | Open Yomitan settings | OS-global (registered with the OS) | Fixed (not configurable) |
::: tip ::: tip
`Alt+Shift+O` is dispatched by the overlay window and the mpv plugin, so it works from either surface without OS registration. Only `Alt+Shift+Y` is registered with the OS; if it conflicts with another application, that binding cannot be changed. All `shortcuts.*` keys hot-reload - no restart needed. `Alt+Shift+O` is dispatched by the overlay window and the mpv plugin, so it works from either surface without OS registration. Only `Alt+Shift+Y` is registered with the OS; if it conflicts with another application, that binding cannot be changed. All `shortcuts.*` keys hot-reload - no restart needed.
@@ -51,6 +51,7 @@ These control playback and subtitle display. They require overlay window focus.
| `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 | | `Ctrl+Alt+P` | Open playlist browser for current directory + queue |
| `Ctrl+Alt+A` | Toggle Anime Browser inside the player |
| `ArrowRight` | Seek forward 5 seconds | | `ArrowRight` | Seek forward 5 seconds |
| `ArrowLeft` | Seek backward 5 seconds | | `ArrowLeft` | Seek backward 5 seconds |
| `ArrowUp` | Seek forward 60 seconds | | `ArrowUp` | Seek forward 60 seconds |
@@ -69,7 +70,7 @@ These control playback and subtitle display. They require overlay window focus.
| `Right-click` | Toggle pause (outside subtitle area) | | `Right-click` | Toggle pause (outside subtitle area) |
| `Right-click + drag` | Reposition subtitles (on subtitle area) | | `Right-click + drag` | Reposition subtitles (on subtitle area) |
The mpv-command rows above (`Space`, `F`, `J`, `Shift+J`, the seek/sub-seek/sub-step/sub-delay keys, replay/play-next, and quit) are merged from the `keybindings` config array and can be remapped or disabled there. `V` and the mouse actions are built-in overlay behaviors and are not part of the `keybindings` array. The playlist browser opens a split overlay modal with sibling video files on the left and the live mpv playlist on the right. The mpv-command rows above (`Space`, `F`, `J`, `Shift+J`, the seek/sub-seek/sub-step/sub-delay keys, replay/play-next, and quit) are merged from the `keybindings` config array and can be remapped or disabled there. `V` and the mouse actions are built-in overlay behaviors and are not part of the `keybindings` array. The playlist browser opens a split overlay modal with sibling video files on the left and the live mpv playlist on the right. The Anime Browser shortcut toggles a dedicated overlay modal bounded to the player; the standalone Anime Browser window remains independent, while active playback is shared.
On macOS managed playback, SubMiner disables mpv's menu-bar shortcuts so configured SubMiner shortcuts like `Cmd+Shift+O` reach the mpv plugin instead of opening native mpv menu actions. On macOS managed playback, SubMiner disables mpv's menu-bar shortcuts so configured SubMiner shortcuts like `Cmd+Shift+O` reach the mpv plugin instead of opening native mpv menu actions.
@@ -88,7 +89,6 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle
| `Ctrl+Shift+T` | Open TsukiHime subtitle search modal (EN/JA tabs) | `shortcuts.openTsukihime` | | `Ctrl+Shift+T` | Open TsukiHime subtitle search modal (EN/JA tabs) | `shortcuts.openTsukihime` |
| `Ctrl/Cmd+N` | Toggle overlay notification history panel | `shortcuts.toggleNotificationHistory` | | `Ctrl/Cmd+N` | Toggle overlay notification history panel | `shortcuts.toggleNotificationHistory` |
| `Ctrl+Alt+C` | Open the manual YouTube subtitle picker | `keybindings` | | `Ctrl+Alt+C` | Open the manual YouTube subtitle picker | `keybindings` |
| `g` then `s` | Select primary and secondary subtitles, when enabled | `shortcuts.openSubtitleSelection` |
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` | | `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
| `Ctrl/Cmd+A` | Append clipboard video path to mpv playlist | `shortcuts.appendClipboardVideoToQueue` | | `Ctrl/Cmd+A` | Append clipboard video path to mpv playlist | `shortcuts.appendClipboardVideoToQueue` |
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` (overlay) / `shortcuts.toggleSubtitleSidebar` (mpv session binding) | | `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` (overlay) / `shortcuts.toggleSubtitleSidebar` (mpv session binding) |
@@ -99,8 +99,6 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle
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`. 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`.
Enable the subtitle selector in **Settings → Behavior → Subtitle Selection**. Its shortcut overrides mpv subtitle selection only while enabled. In the focused overlay, press the second key within one second. Single-key bindings take priority: if `g` already has an action in SubMiner or mpv, `g-s` is disabled with a conflict warning, and `g` still runs immediately. Remap the sequence or remove the conflicting single-key binding. The existing `y` prefix is reserved for its built-in commands. mpv bindings are checked on connection, configuration changes, and overlay focus; refresh the overlay after changing another script's bindings. See [subtitle selection](/configuration#subtitle-selection).
The subtitle sidebar toggle is overlay-local and only opens when SubMiner has a parsed cue list for the active subtitle source. The subtitle sidebar toggle is overlay-local and only opens when SubMiner has a parsed cue list for the active subtitle source.
In the sidebar, `Enter` seeks the keyboard-focused cue. `Space` keeps its configured playback action, normally pause/resume, even when a cue has focus. In the sidebar, `Enter` seeks the keyboard-focused cue. `Space` keeps its configured playback action, normally pause/resume, even when a cue has focus.
@@ -127,7 +125,7 @@ When the mpv plugin is installed, all commands use a `y` chord prefix - press `y
| `y-S` | Stop overlay | | `y-S` | Stop overlay |
| `y-t` | Toggle visible overlay | | `y-t` | Toggle visible overlay |
| `v` | Cycle primary subtitle bar mode (hidden → visible → hover) | | `v` | Cycle primary subtitle bar mode (hidden → visible → hover) |
| `y-o` | Open active dictionary settings | | `y-o` | Open Yomitan settings |
| `y-r` | Restart overlay | | `y-r` | Restart overlay |
| `y-c` | Check overlay status | | `y-c` | Check overlay status |
| `y-h` | Open session help | | `y-h` | Open session help |
+21 -3
View File
@@ -1,6 +1,6 @@
# Japanese subtitle generation # Japanese subtitle generation
Generate Japanese SRT subtitles from a local video's audio using [whisper.cpp](https://github.com/ggml-org/whisper.cpp). The launcher and overlay use the same local generation service. Audio stays on your computer. Model downloads require an internet connection; generation with an installed model does not. Generate Japanese SRT subtitles from a local video's audio or an anime stream using [whisper.cpp](https://github.com/ggml-org/whisper.cpp). The launcher and overlay use the same local generation service. Transcription runs on your computer without uploading audio. Model downloads and stream audio extraction require an internet connection; local-file generation with an installed model does not.
## Setup ## Setup
@@ -51,7 +51,7 @@ A configured external Model Path takes precedence and hides the managed model pi
## From the overlay ## From the overlay
1. Open a local video in mpv and select its Japanese audio track. 1. Open a local video or an anime episode stream in mpv and select its Japanese audio track.
2. Press **Ctrl+Shift+G** to open the standalone generation modal. When the subtitle sidebar has no subtitle lines loaded, it also offers a **Generate Japanese subtitles** button. Neither an open sidebar nor an existing subtitle track is required for the shortcut. 2. Press **Ctrl+Shift+G** to open the standalone generation modal. When the subtitle sidebar has no subtitle lines loaded, it also offers a **Generate Japanese subtitles** button. Neither an open sidebar nor an existing subtitle track is required for the shortcut.
3. Choose a model and download it if prompted, or configure your existing model path in Settings and click **Check again**. 3. Choose a model and download it if prompted, or configure your existing model path in Settings and click **Check again**.
4. Optionally check **Focus on spoken dialogue** and click **Download speech detection model** if prompted. 4. Optionally check **Focus on spoken dialogue** and click **Download speech detection model** if prompted.
@@ -63,6 +63,24 @@ The modal adapts to the player window, using a wider layout when space allows an
SubMiner saves `<video>.ja.generated.srt` beside the media, adding a numeric suffix if that name already exists. It selects the generated Japanese subtitle track and resets the subtitle delay when mpv is still playing the same file. If playback changes, the subtitles remain saved and are not attached to the new video. The result includes the saved path even if mpv cannot load it. SubMiner saves `<video>.ja.generated.srt` beside the media, adding a numeric suffix if that name already exists. It selects the generated Japanese subtitle track and resets the subtitle delay when mpv is still playing the same file. If playback changes, the subtitles remain saved and are not attached to the new video. The result includes the saved path even if mpv cannot load it.
### Generating for anime streams
Start the episode from the [anime browser](/anime-browser), then open the generation modal as usual. HTTP and HTTPS episode streams, including HLS through the anime browser's proxy, can use the same models and dialogue options as local files. The selected audio may be inside the stream or a separately attached HTTP/HTTPS audio track. For a selected external audio stream, SubMiner downloads that track directly and includes mpv's current audio delay in the subtitle timing. Local external audio files remain unsupported.
SubMiner fetches the episode's audio into a temporary WAV before transcription. It reuses mpv's supported request headers, referrer, and user agent. The existing media-fetch policy excludes Cookie and Authorization headers, so sources requiring those directly may fail unless the anime bridge handles authentication. Expired URLs or a stopped bridge can also interrupt extraction; reopen the episode and retry.
For anime-browser episodes, extraction first considers separate Japanese audio tracks returned by the source, then video streams with lower resolution than playback. It checks duration, starting offset, and short audio samples at three points against the selected playback audio. Different dialogue, shifted timing, silence, or failed checks keep the original stream. Checking alternatives has a 20-second budget. For plain HLS, it downloads only the segments covering each sample and aligns their timestamps to playback before comparing audio. Empty original samples are retried once and never reused. Failed checks log the candidate number, phase, reason, and elapsed time without exposing stream URLs or request headers. Progress names the source used for extraction; playback keeps its selected quality. Sources that expose only one combined stream still require downloading its video bytes.
For plain HLS episodes with MPEG-TS segments, SubMiner downloads up to four segments at once, then extracts audio in playback order. Progress shows segment downloading followed by audio extraction. The temporary segments require space for the episode's combined video and audio and are removed after extraction, or when generation fails or is cancelled. Encrypted playlists, variant playlists, byte ranges, and other complex stream formats use the existing FFmpeg extraction path. Download speed still depends on the source and proxy.
Generated SRT files are retained under `cache/generated-subtitles` in SubMiner's application data directory, using a URL hash for the filename and a numeric suffix for repeated generation. The result shows the saved path. Playback can continue during generation; the completed SRT is selected only if the same stream is still playing. Cached SRT results are not automatically loaded on a later visit.
Completed audio extracts can be reused for retries and model changes during the current app session. They live with temporary job files under `cache/generated-subtitles/sessions`, separate from saved SRT files. The cache distinguishes the stream URL, selected audio track, and request headers, verifies contents before reuse, and evicts older audio above 512 MiB. Partial downloads are not cached.
Quitting cancels the active job, waits for its cleanup, and removes the session's downloaded audio and working files. On startup, SubMiner removes abandoned session directories from processes that have exited, plus audio retained by the older persistent cache. Audio is never reused across app restarts. Generated SRT files and installed speech models remain available.
This processes the complete episode, so subtitles appear after extraction and transcription finish. Streams must report a finite duration; live streams are not supported. A stream with combined video and audio segments may require downloading those segments even though only audio is retained.
## From the launcher ## From the launcher
```bash ```bash
@@ -77,4 +95,4 @@ With no file argument, the command uses the current local mpv media and its sele
The SRT includes whisper.cpp's timestamps, adjusted for the audio stream's position on the media timeline and, when speech detection is configured, each passage's original start time. No alass step is required to load it. This version uses native Whisper timing; it does not run WhisperX or another forced aligner. Recognition can repeat or invent lines, and timing can be imperfect, especially with music or overlapping speech. Review generated text and audio boundaries when mining. The SRT includes whisper.cpp's timestamps, adjusted for the audio stream's position on the media timeline and, when speech detection is configured, each passage's original start time. No alass step is required to load it. This version uses native Whisper timing; it does not run WhisperX or another forced aligner. Recognition can repeat or invent lines, and timing can be imperfect, especially with music or overlapping speech. Review generated text and audio boundaries when mining.
Generation supports local files and internal audio tracks. Remote URLs, subtitle translation, and transcription of a separately attached mpv audio track are not supported by the modal. Pass a separate local audio file to the launcher if needed. The destination directory needs writable space for subtitles; temporary storage needs enough space for the extracted mono audio. The overlay supports local files and finite HTTP/HTTPS streams with internal or selected external HTTP audio tracks. The launcher remains local-file only. Subtitle translation and separately attached local audio files are not supported by the modal. Pass a separate local audio file to the launcher if needed. The destination directory needs writable space for subtitles; temporary storage needs enough space for the extracted mono audio. Cached local subtitle tracks and embedded text subtitles can guide stream transcription; unreadable references fall back to audio timing.
+40 -3
View File
@@ -211,21 +211,26 @@ Resume playback and wait for the next subtitle to appear, then try mining again.
Both **alass** and **ffsubsync** are optional external dependencies. Subtitle syncing requires at least one of them to be installed. Both **alass** and **ffsubsync** are optional external dependencies. Subtitle syncing requires at least one of them to be installed.
**"Configured alass executable not found"** Subsync writes to the application log under the `subsync` scope, so the full command failure — exit code, stderr, resolved file paths — is recorded there as well as on the OSD.
**"Could not find alass" / "Configured alass executable not found"**
Install alass or configure the path: Install alass or configure the path:
- **Homebrew**: `brew install alass`
- **Arch Linux (AUR)**: `paru -S alass` - **Arch Linux (AUR)**: `paru -S alass`
- **Cargo**: `cargo install alass-cli` - **Cargo**: `cargo install alass-cli`
- Set the path: `subsync.alass_path` in your config. - Set the path: `subsync.alass_path` in your config.
**"Configured ffsubsync executable not found"** Leaving the option empty searches `PATH` plus the usual install prefixes, and accepts either `alass` or `alass-cli`. Set the option explicitly when the binary lives somewhere else. The second message means the configured path itself does not exist — SubMiner never silently substitutes a different binary for one you named.
**"Could not find ffsubsync" / "Configured ffsubsync executable not found"**
Install ffsubsync or configure the path: Install ffsubsync or configure the path:
- **Arch Linux (AUR)**: `paru -S python-ffsubsync` - **Arch Linux (AUR)**: `paru -S python-ffsubsync`
- **pip**: `pip install ffsubsync` - **pip**: `pip install ffsubsync`
- Must be on `PATH` or configured via `subsync.ffsubsync_path` in your config. - Must be discoverable, or configured via `subsync.ffsubsync_path` in your config.
**"alass synchronization failed" / "ffsubsync synchronization failed"** **"alass synchronization failed" / "ffsubsync synchronization failed"**
@@ -236,6 +241,38 @@ If subtitle sync fails (the error message is prefixed with the engine name):
- Try running the sync tool manually to see detailed error output. - 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). - ffsubsync requires local files and cannot handle remote media streams (e.g., streaming URLs).
**Syncing subtitles on a stream (Aniyomi extensions, Jellyfin)**
Subtitle tracks mpv loaded from a URL are downloaded to a temporary file first, reusing mpv's own request headers, so they work as either the sync target or the alass reference. Downloading a Japanese track from Jimaku or TsukiHime while a stream is playing also works — it becomes the primary track and therefore the sync target.
Internal subtitle tracks of a stream still go through `ffmpeg`, which has to reach the origin itself. If that fails, prefer an external track or a Jimaku download as the reference.
Streams usually serve WebVTT, which alass cannot parse — it picks its parser from the file extension and treats a `.vtt` file as a video, failing with "no audio stream in file". SubMiner rewrites both the target and the reference as SRT for alass, so this is handled automatically; the retimed track mpv loads is that SRT.
## Anime Browser
See the [anime browser guide](/anime-browser) for how sources and the bridge work.
**Nothing to search**
SubMiner ships no extension repositories and bundles no sources. Until you add a repository index URL in the **Extensions** tab (or drop Aniyomi `.apk` files into the extensions directory) the browser has nothing to query.
**"No pinned checksum for …"**
The bridge bundle is verified against a SHA-256 that a maintainer has checked by hand. Bundles exist for macOS (arm64, x64), Linux (x64), and Windows (x64), but only macOS arm64 and Linux x64 are pinned so far; the rest stop with this message rather than running an unverified download. Other platforms are unsupported outright.
**A source returns nothing, or asks for a login**
Most extensions need configuration first - a server address, credentials, a preferred quality. Open the **Source settings** tab and fill them in; each save is handed straight back to the extension. Sources that need an Android WebView (typically for Cloudflare challenges) cannot work here, because the bridge has none.
**"Playback failed" with an mpv error**
"Playing" is only reported once mpv actually configures a video output, so this is a real failure rather than a silent one: a dead host, an expired stream URL, or an undecodable stream. Resolve the episode again; if it keeps failing, try another source or quality entry.
**The browser starts failing every request**
A bridge that dies (killed, crashed, stopped mid-operation) is named in the status bar and restarted on the next request. Anything already playing ends with it, since stream URLs point at its loopback proxy.
## TsukiHime ## TsukiHime
**"xz binary not found"** **"xz binary not found"**
+2 -2
View File
@@ -16,11 +16,11 @@ Unlike Jimaku, TsukiHime needs no account or API key. The only requirement is th
The integration runs through an in-overlay modal opened with `Ctrl+Shift+T` by default. The modal has two tabs that filter both the release list and the subtitle tracks of the selected release by role: the first follows `secondarySub.secondarySubLanguages` (English when unset), and the second is always **Japanese**, the currently supported primary subtitle language. Each tab lists only the releases whose reported subtitle languages include the tab's language, so the Japanese tab hides the many releases that ship English subtitles only. Releases and tracks with no language tag stay visible on the secondary tab. If nothing on the active tab qualifies, the status line says so and points at the other tab. The integration runs through an in-overlay modal opened with `Ctrl+Shift+T` by default. The modal has two tabs that filter both the release list and the subtitle tracks of the selected release by role: the first follows `secondarySub.secondarySubLanguages` (English when unset), and the second is always **Japanese**, the currently supported primary subtitle language. Each tab lists only the releases whose reported subtitle languages include the tab's language, so the Japanese tab hides the many releases that ship English subtitles only. Releases and tracks with no language tag stay visible on the secondary tab. If nothing on the active tab qualifies, the status line says so and points at the other tab.
When you open the modal, SubMiner parses the current video filename to extract a title and episode number (same parser as Jimaku - `S01E03`, `1x03`, `E03`, and dash-separated numbers all work). If the filename yields a high-confidence match, SubMiner auto-searches immediately. When you open the modal, SubMiner parses the current video filename to extract a title, season, and episode number (same parser as Jimaku - `S01E03`, `1x03`, `E03`, and dash-separated numbers all work). If the filename yields a high-confidence match, SubMiner auto-searches immediately. Episodes launched from the [anime browser](/anime-browser) fill all three fields from the source's own listing instead of from a filename.
From there: From there:
1. **Search** - SubMiner queries TsukiHime with `<title> <episode>`. Results appear as a list of releases (e.g. `[SubsPlease] ... - 28 (1080p)`), each showing size, file count, and the subtitle languages the release carries. 1. **Search** - SubMiner queries TsukiHime with `<title> <season> <episode>`. Season 1 is left out on purpose: releases of a first season almost never carry `S01` in their name, so including it would match nothing. Later seasons are included, which is what makes their releases findable at all. Results appear as a list of releases (e.g. `[SubsPlease] ... - 28 (1080p)`), each showing size, file count, and the subtitle languages the release carries.
2. **Browse releases** - Select a release to list the text subtitle tracks extracted from its files. English tracks sort first; image-based tracks (PGS/VobSub) are filtered out. 2. **Browse releases** - Select a release to list the text subtitle tracks extracted from its files. English tracks sort first; image-based tracks (PGS/VobSub) are filtered out.
3. **Download** - Selecting a track downloads the xz-compressed subtitle from TsukiHime's storage, decompresses it, saves it next to the video (or a temp directory for remote/streamed media), and loads it into mpv. Japanese tracks are selected as mpv's **primary** subtitle. Tracks from the configured secondary tab are assigned to mpv's **secondary** subtitle slot without replacing the primary. The filename carries the track's language - `<video basename>.en.<ext>` for English, `.ja` for Japanese, and so on - so mpv and media servers detect the language correctly. 3. **Download** - Selecting a track downloads the xz-compressed subtitle from TsukiHime's storage, decompresses it, saves it next to the video (or a temp directory for remote/streamed media), and loads it into mpv. Japanese tracks are selected as mpv's **primary** subtitle. Tracks from the configured secondary tab are assigned to mpv's **secondary** subtitle slot without replacing the primary. The filename carries the track's language - `<video basename>.en.<ext>` for English, `.ja` for Japanese, and so on - so mpv and media servers detect the language correctly.
+6 -27
View File
@@ -15,8 +15,8 @@ That is the whole setup. The `subminer` launcher starts mpv, opens the IPC socke
Every current launcher wrapper uses the Bun runtime included with the SubMiner app. This includes setup installs, release downloads, `make install`, and the AUR package. You only need the wrapper directory on your terminal `PATH`. Building SubMiner from source still requires Bun on the development machine. Every current launcher wrapper uses the Bun runtime included with the SubMiner app. This includes setup installs, release downloads, `make install`, and the AUR package. You only need the wrapper directory on your terminal `PATH`. Building SubMiner from source still requires Bun on the development machine.
> [!IMPORTANT] > [!IMPORTANT]
> SubMiner requires at least one dictionary in the selected lookup backend. > SubMiner requires the bundled Yomitan instance to have at least one dictionary imported for lookups to work.
> See [Yomitan setup](#yomitan-setup) or [Hachidori setup](#hachidori-setup). > See [Yomitan setup](#yomitan-setup) for details.
::: tip Anki card enrichment ::: tip Anki card enrichment
If you want sentence, audio, and screenshot fields on your Anki cards, add this to your config: If you want sentence, audio, and screenshot fields on your Anki cards, add this to your config:
@@ -72,6 +72,7 @@ subminer https://youtu.be/... # Play a YouTube URL
subminer stats # Open the immersion stats dashboard subminer stats # Open the immersion stats dashboard
subminer doctor # Check dependencies, config, and the mpv socket subminer doctor # Check dependencies, config, and the mpv socket
subminer settings # Open the SubMiner settings window subminer settings # Open the SubMiner settings window
subminer anime # Open the anime browser window
subminer generate-subs video.mkv # Generate Japanese subtitles from local audio subminer generate-subs video.mkv # Generate Japanese subtitles from local audio
subminer app --setup # Re-open first-run setup subminer app --setup # Re-open first-run setup
subminer -u # Check for updates subminer -u # Check for updates
@@ -174,9 +175,9 @@ SubMiner.AppImage --toggle-primary-subtitle-bar # Toggle the primary subtitle b
SubMiner.AppImage --toggle-subtitle-sidebar # Toggle the subtitle sidebar SubMiner.AppImage --toggle-subtitle-sidebar # Toggle the subtitle sidebar
SubMiner.AppImage --open-tsukihime # Open TsukiHime subtitle search SubMiner.AppImage --open-tsukihime # Open TsukiHime subtitle search
SubMiner.AppImage --yomitan # Open Yomitan settings SubMiner.AppImage --yomitan # Open Yomitan settings
SubMiner.AppImage --hachidori # Open Hachidori settings
SubMiner.AppImage --settings # Open the SubMiner settings window SubMiner.AppImage --settings # Open the SubMiner settings window
SubMiner.AppImage --jellyfin # Open the Jellyfin setup window SubMiner.AppImage --jellyfin # Open the Jellyfin setup window
SubMiner.AppImage --anime # Open the anime browser window
SubMiner.AppImage --dictionary # Generate a character dictionary ZIP SubMiner.AppImage --dictionary # Generate a character dictionary ZIP
SubMiner.AppImage --start --dev # Enable app/dev mode SubMiner.AppImage --start --dev # Enable app/dev mode
SubMiner.AppImage --start --log-level debug # Verbose logging without dev mode SubMiner.AppImage --start --log-level debug # Verbose logging without dev mode
@@ -209,7 +210,7 @@ The tray menu also includes `View Changelog`, which opens the in-app changelog m
### Windows mpv shortcut ### Windows mpv shortcut
First-run setup creates the config file, then requires dictionaries in the selected backend before it can finish. First-run setup creates the config file, then requires Yomitan dictionaries before it can finish.
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. 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. After setup completes, the shortcut is the normal Windows playback entry point.
@@ -315,28 +316,6 @@ For SubMiner overlay lookups to work, open Yomitan settings (`subminer app --yom
If you also use Yomitan in a browser, set that profile up separately. It inherits nothing from the bundled instance. If you also use Yomitan in a browser, set that profile up separately. It inherits nothing from the bundled instance.
### Hachidori setup
Set `dictionaryBackend` to `"hachidori"` in SubMiner settings or `config.jsonc`, then restart SubMiner. The tray's dictionary settings entry changes to **Open Hachidori Settings**. Switching to `"yomitan"` restores the Yomitan entry after restarting.
Open Hachidori settings with `subminer app --hachidori` or `SubMiner.AppImage --hachidori`. Import your dictionary ZIPs or use Hachidori's recommended dictionary installer, then configure its Anki templates. Yomitan and Hachidori keep separate dictionaries and settings. Yomitan profiles, custom Handlebars templates, and `yomitan.externalProfilePath` do not transfer to Hachidori.
First-run setup also offers **Dictionary source → Use an external dictionary host → Link host**. Enable sharing in the other Hachidori app or browser, or start a compatible Docker dictionary host, then enter its sharing address, such as `127.0.0.1:8771` or `ws://host:8771/link`. Use the WebSocket sharing port, not the management page or HTTP API port. The external host section is collapsed until you expand it or a host is linked. Browser hosts need the browser, Hachidori extension, and relay running. Electron hosts need the host app and any required relay running. Docker hosts need the container running; no browser needs to stay open.
Setup checks the host connection and dictionary inventory before enabling Finish. Import at least one dictionary on the host and refresh status. The link persists across restarts. **Unlink and use local dictionaries** restores SubMiner's local library. Anki templates, pronunciation sources, custom buttons, and SubMiner's audio/image processing remain local while linked. Dictionary settings and dictionary edits use the host. Frequency annotations use the frequencies returned with Hachidori dictionary entries. SubMiner keeps ranks found during scanning and queries the existing term-entry API for missing ranks. Words without a matching definition entry may remain unranked, even if a frequency dictionary contains them.
Both named settings flags work independently of the selected backend. Opening settings does not switch the overlay backend. The global dictionary-settings shortcut opens the selected backend.
Hachidori uses SubMiner's subtitle scanning, lookup counter, popup pause behavior, controller commands, character dictionaries, and Anki media enrichment. Keep SubMiner's AnkiConnect proxy enabled for screenshots and sentence audio. SubMiner routes Hachidori to that proxy when it is active; Hachidori's own screen recorder and screenshot capture are disabled in the embedded app.
For automatic character dictionary sync with a Docker host, set `hachidori.externalHostManagementUrl` to that same host's management origin, for example `"http://127.0.0.1:8780"`. This is separate from the WebSocket sharing address. SubMiner uploads the generated ZIP directly and replaces its previous dictionary after a successful import; busy imports are retried. Keep this URL pointed at the currently linked Docker host if you change hosts. An empty value disables external uploads and reports a configuration error when sync is attempted. Local Hachidori dictionaries do not need this setting. External browser/app hosts without the Docker management API do not support this automatic upload path.
Existing controls such as `startupWarmups.yomitanExtension` and `subtitleStyle.autoPauseVideoOnYomitanPopup` apply to the selected backend. Hachidori has one dictionary configuration, so character-dictionary profile scope applies to that configuration.
First-run setup remembers each backend that finished it, including when setup is reopened for legacy plugin cleanup. Switching to a backend for the first time asks for that backend's dictionaries; switching back to one that already finished does not repeat setup. Until SubMiner restarts, it keeps running the backend it started with, and the launcher gates playback on that running backend and logs a restart reminder. A running Yomitan session continues using its external profile until the restart. When `yomitan.externalProfilePath` is configured, `--yomitan` is disabled to preserve read-only external-profile mode, including while Hachidori is active. Hachidori settings remain available.
Hachidori's own duplicate handling differs from Yomitan's. Choosing **Overwrite** in the Hachidori popup updates the existing note and SubMiner enriches its media, while **Add anyway** creates a new note and runs SubMiner's Kiku/Senren [field grouping](./anki-integration.md#field-grouping-kiku-senren). Mining from the stats dashboard uses the selected backend as well.
### YouTube playback ### YouTube playback
`subminer` accepts direct URLs (for example, YouTube links) and `ytsearch:` targets. `subminer` accepts direct URLs (for example, YouTube links) and `ytsearch:` targets.
@@ -431,7 +410,7 @@ See [Keyboard Shortcuts](/shortcuts) for the full reference, including mining sh
| Keybind | Action | Scope | | Keybind | Action | Scope |
| ------------- | ---------------------- | -------------------------------------------------------------------------------------------------- | | ------------- | ---------------------- | -------------------------------------------------------------------------------------------------- |
| `Alt+Shift+O` | Toggle visible overlay | Works while the overlay or mpv has focus (configurable via `shortcuts.toggleVisibleOverlayGlobal`) | | `Alt+Shift+O` | Toggle visible overlay | Works while the overlay or mpv has focus (configurable via `shortcuts.toggleVisibleOverlayGlobal`) |
| `Alt+Shift+Y` | Open active dictionary settings | OS-global - registered with the system, works from any window | | `Alt+Shift+Y` | Open Yomitan settings | OS-global - registered with the system, works from any window |
`Alt+Shift+Y` is fixed and not configurable. All other shortcuts can be changed under `shortcuts` in your config. `Alt+Shift+Y` is fixed and not configurable. All other shortcuts can be changed under `shortcuts` in your config.
+16 -6
View File
@@ -6,12 +6,12 @@
- `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 opus --effort medium` to merge and rewrite `changes/*.md` fragments into `claude -p --model sonnet` 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 ## Package contents and size checks
Stable and prerelease workflows share `.github/workflows/package-release.yml`. Stable and prerelease workflows share `.github/workflows/package-release.yml`.
Both callers explicitly pass the five required macOS signing/notarization Both callers explicitly pass the five required macOS signing/notarization
@@ -19,9 +19,11 @@ 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`; `scripts/prepare-build-assets.mjs` stages into `dist/bundled-integration-keys.json`;
artifacts built without it simply require users to set `tmdb.apiKey`). artifacts built without it simply require users to set `tmdb.apiKey`).
`GITHUB_TOKEN` remains automatically available to the reusable workflow. `GITHUB_TOKEN` remains automatically available to the reusable workflow.
Each platform verifies its ASAR and external resources before signing. Missing Each platform verifies its ASAR and external resources before signing, then
runtime assets, foreign SQLite/Koffi binaries, duplicate UI fonts, demo media, measures the signed app and installers before upload. Missing runtime assets,
source maps, TypeScript files, and nested test or fixture directories fail the build. foreign SQLite/Koffi binaries, duplicate UI fonts, demo media, source maps,
TypeScript files, and nested test or fixture directories
fail the build. Size measurements are informational and do not block releases.
Current targets are Linux x64, macOS arm64, and Windows x64. Current targets are Linux x64, macOS arm64, and Windows x64.
The runtime allowlist includes `dist/`, `stats/dist/`, and The runtime allowlist includes `dist/`, `stats/dist/`, and
@@ -32,6 +34,14 @@ 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 only its target Koffi binary; other platforms omit Koffi. Desktop UIs share the
original M PLUS 1 TTF in `dist/fonts/`. original M PLUS 1 TTF in `dist/fonts/`.
`release/package-size-<platform>-<arch>.json` reports unpacked bytes, largest
files inside and outside ASAR, native binaries, and compressed artifact sizes.
Framework symlinks are not counted twice. Reports are checksummed and published.
CI downloads the preceding release's reports for comparison; older releases
without reports skip comparison. Review the inventory and reason for growth
when comparing releases. An AppImage normally
runs compressed; its extracted size is a separate measurement.
The shared workflow runs `bun run test:package <resources-directory>` with the The shared workflow runs `bun run test:package <resources-directory>` with the
pinned Electron runtime and temporary user data. On headless Linux, prefix it pinned Electron runtime and temporary user data. On headless Linux, prefix it
with `xvfb-run -a`. This checks packaged SQLite, Windows FFI loading/polling, with `xvfb-run -a`. This checks packaged SQLite, Windows FFI loading/polling,
@@ -142,7 +152,7 @@ Notes:
- 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.
- 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`. - 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`. - 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. - 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.
- 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 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`. - 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`.
@@ -64,7 +64,6 @@ External subtitle files only (SRT, VTT, ASS). Embedded subtitle tracks are out o
A cue parser extracts both timing and text content from subtitle files for prefetching. A cue parser extracts both timing and text content from subtitle files for prefetching.
**Parsed cue structure:** **Parsed cue structure:**
```typescript ```typescript
interface SubtitleCue { interface SubtitleCue {
startTime: number; // seconds startTime: number; // seconds
@@ -77,7 +76,6 @@ interface SubtitleCue {
``` ```
**Supported formats:** **Supported formats:**
- SRT/VTT: Regex-based parsing of timing lines + text content between timing blocks. - SRT/VTT: Regex-based parsing of timing lines + text content between timing blocks.
- ASS: Parse the `[Events]` section, read the field order from the `Format:` row, and extract timed `Dialogue:` lines. Timed `Comment:` lines are normally ignored, but can supply canonical authored text when they match a nearby generated animation from the same style and actor. Text can itself contain commas. - ASS: Parse the `[Events]` section, read the field order from the `Format:` row, and extract timed `Dialogue:` lines. Timed `Comment:` lines are normally ignored, but can supply canonical authored text when they match a nearby generated animation from the same style and actor. Text can itself contain commas.
@@ -167,7 +165,6 @@ tokens (already have frequencyRank values from parser-level applyFrequencyRanks)
### Dependency Analysis ### Dependency Analysis
All annotations either depend on MeCab POS data or benefit from running after it: All annotations either depend on MeCab POS data or benefit from running after it:
- **Known word marking:** Needs base tokens (surface/headword). No POS dependency, but no reason to run separately. - **Known word marking:** Needs base tokens (surface/headword). No POS dependency, but no reason to run separately.
- **Frequency filtering:** Uses `pos1Exclusions` and `pos2Exclusions` to clear frequency ranks on excluded tokens (particles, noise). Depends on MeCab POS data. - **Frequency filtering:** Uses `pos1Exclusions` and `pos2Exclusions` to clear frequency ranks on excluded tokens (particles, noise). Depends on MeCab POS data.
- **JLPT marking:** Uses `shouldIgnoreJlptForMecabPos1` to filter. Depends on MeCab POS data. - **JLPT marking:** Uses `shouldIgnoreJlptForMecabPos1` to filter. Depends on MeCab POS data.
@@ -184,14 +181,18 @@ function annotateTokens(tokens, deps, options): MergedToken[] {
// Single pass: known word + frequency filtering + JLPT computed together // Single pass: known word + frequency filtering + JLPT computed together
const annotated = tokens.map((token) => { const annotated = tokens.map((token) => {
const isKnown = nPlusOneEnabled ? token.isKnown || computeIsKnown(token, deps) : false; const isKnown = nPlusOneEnabled
? token.isKnown || computeIsKnown(token, deps)
: false;
// Filter frequency rank using POS exclusions (rank values already set at parser level) // Filter frequency rank using POS exclusions (rank values already set at parser level)
const frequencyRank = frequencyEnabled const frequencyRank = frequencyEnabled
? filterFrequencyRank(token, pos1Exclusions, pos2Exclusions) ? filterFrequencyRank(token, pos1Exclusions, pos2Exclusions)
: undefined; : undefined;
const jlptLevel = jlptEnabled ? computeJlptLevel(token, deps.getJlptLevel) : undefined; const jlptLevel = jlptEnabled
? computeJlptLevel(token, deps.getJlptLevel)
: undefined;
return { ...token, isKnown, frequencyRank, jlptLevel }; return { ...token, isKnown, frequencyRank, jlptLevel };
}); });
@@ -232,7 +233,6 @@ Replace `document.createElement('span')` calls in the renderer with `templateSpa
### Current Behavior ### Current Behavior
In `renderWithTokens` (`subtitle-render.ts`), each render cycle: In `renderWithTokens` (`subtitle-render.ts`), each render cycle:
1. Clears DOM with `innerHTML = ''` 1. Clears DOM with `innerHTML = ''`
2. Creates a `DocumentFragment` 2. Creates a `DocumentFragment`
3. Calls `document.createElement('span')` for each token (~10-15 per subtitle) 3. Calls `document.createElement('span')` for each token (~10-15 per subtitle)
@@ -269,7 +269,7 @@ Full recycling (collecting old nodes, clearing attributes, reusing them) require
## Combined Impact Summary ## Combined Impact Summary
| Scenario | Before | After | Improvement | | Scenario | Before | After | Improvement |
| --------------------------------- | ---------- | ---------- | ----------- | |----------|--------|-------|-------------|
| Normal playback (prefetch-warmed) | ~200-320ms | ~30-50ms | ~80-85% | | Normal playback (prefetch-warmed) | ~200-320ms | ~30-50ms | ~80-85% |
| Cache hit (repeated subtitle) | ~72ms | ~55-65ms | ~10-20% | | Cache hit (repeated subtitle) | ~72ms | ~55-65ms | ~10-20% |
| Cache miss (immediate seek) | ~200-320ms | ~150-260ms | ~20-25% | | Cache miss (immediate seek) | ~200-320ms | ~150-260ms | ~20-25% |
@@ -279,19 +279,16 @@ Full recycling (collecting old nodes, clearing attributes, reusing them) require
## Files Summary ## Files Summary
### New Files ### New Files
- `src/core/services/subtitle-prefetch.ts` - `src/core/services/subtitle-prefetch.ts`
- `src/core/services/subtitle-cue-parser.ts` - `src/core/services/subtitle-cue-parser.ts`
### Modified Files ### Modified Files
- `src/core/services/subtitle-processing-controller.ts` (expose `preCacheTokenization`) - `src/core/services/subtitle-processing-controller.ts` (expose `preCacheTokenization`)
- `src/core/services/tokenizer/annotation-stage.ts` (batched single-pass) - `src/core/services/tokenizer/annotation-stage.ts` (batched single-pass)
- `src/renderer/subtitle-render.ts` (template cloneNode) - `src/renderer/subtitle-render.ts` (template cloneNode)
- `src/main.ts` (wire up prefetch service) - `src/main.ts` (wire up prefetch service)
### Test Files ### Test Files
- New tests for subtitle cue parser (SRT, VTT, ASS formats) - New tests for subtitle cue parser (SRT, VTT, ASS formats)
- New tests for subtitle prefetch service (priority window, seek, pause/resume) - New tests for subtitle prefetch service (priority window, seek, pause/resume)
- Updated tests for annotation stage (same behavior, new implementation) - Updated tests for annotation stage (same behavior, new implementation)
+1 -9
View File
@@ -32,11 +32,9 @@ Update checks and startup launcher migration share a serialized update-state sto
- `src/main/boot/` owns boot-phase assembly seams so `src/main.ts` can stay focused on lifecycle coordination and startup-path selection. - `src/main/boot/` owns boot-phase assembly seams so `src/main.ts` can stay focused on lifecycle coordination and startup-path selection.
- `src/main/runtime/linux-overlay-mode-runtime.ts` owns Linux fullscreen mode state and window replacement. App cleanup cancels pending replacements; `main.ts` supplies window creation and subtitle refresh hooks. - `src/main/runtime/linux-overlay-mode-runtime.ts` owns Linux fullscreen mode state and window replacement. App cleanup cancels pending replacements; `main.ts` supplies window creation and subtitle refresh hooks.
- `src/core/services/` owns focused runtime services plus pure or side-effect-bounded logic. - `src/core/services/` owns focused runtime services plus pure or side-effect-bounded logic.
- `src/core/services/subtitle-generation*.ts` shares local whisper.cpp transcription, safe model downloads, and progress between the launcher and Electron. Optional dialogue mode retains both Silero-detected speech and other audible sections, omits confidently silent gaps, decodes passages independently, and restores original media timing. `src/main/runtime/subtitle-generation-runtime.ts` owns the overlay job lifecycle and only loads completed subtitles into the same local media; `src/shared/subtitle-generation*.ts` owns configuration, the multilingual model catalog, and IPC contracts. The overlay runtime retains a session model selection, validates picker requests through IPC, and keeps external model paths authoritative. - `src/core/services/subtitle-generation*.ts` shares local whisper.cpp transcription, safe model downloads, and progress between the launcher and Electron. The overlay also accepts finite HTTP/HTTPS episode streams, snapshots mpv's supported HTTP headers, and extracts audio through FFmpeg with bounded network reads. Network reference extraction uses the same headers. Generated stream subtitles persist under the injected application cache directory with hashed filenames. `subtitle-generation-audio.ts` owns probing, extraction, and completed-audio reuse. Anime playback retains bounded per-URL alternative-source metadata; generation checks candidate duration, offset, and decoded audio samples before selecting separate Japanese audio or lower-resolution video. Verified extracts are reused only within the current app session, keyed by playback URL, audio stream index, and request headers. `subtitle-generation-session.ts` owns temporary audio and job directories, removes abandoned process directories on startup, and drains cancellation before shutdown cleanup. Saved SRT files remain separate. Plain HLS samples use short segment windows and container timestamps to avoid empty remote seeks; source rejection diagnostics omit URLs and headers. The launcher remains local-file only. Optional dialogue mode retains both Silero-detected speech and other audible sections, omits confidently silent gaps, decodes passages independently, and restores original media timing. `src/main/runtime/subtitle-generation-runtime.ts` owns the overlay job lifecycle and only loads completed subtitles into the same media; `src/shared/subtitle-generation*.ts` owns configuration, the multilingual model catalog, and IPC contracts. The overlay runtime retains a session model selection, validates picker requests through IPC, and keeps external model paths authoritative.
- Subtitle model recommendations use bounded `nvidia-smi` and Whisper CUDA discovery probes in `subtitle-generation-acceleration.ts`. The overlay runtime caches results by executable path for 30 seconds and exposes acceleration status through the existing status IPC. Recommendations do not alter model selection or transcription arguments. - Subtitle model recommendations use bounded `nvidia-smi` and Whisper CUDA discovery probes in `subtitle-generation-acceleration.ts`. The overlay runtime caches results by executable path for 30 seconds and exposes acceleration status through the existing status IPC. Recommendations do not alter model selection or transcription arguments.
- `subtitle-generation-reference.ts` ranks mpv's loaded text subtitle tracks, excludes signs/songs and forced references, and extracts timing hints with FFmpeg. The overlay and launcher snapshot references only for matching media, including active subtitle delays. Hints guide long-passage cuts with or without VAD; they never limit audio coverage or replace Whisper timestamps. - `subtitle-generation-reference.ts` ranks mpv's loaded text subtitle tracks, excludes signs/songs and forced references, and extracts timing hints with FFmpeg. The overlay and launcher snapshot references only for matching media, including active subtitle delays. Hints guide long-passage cuts with or without VAD; they never limit audio coverage or replace Whisper timestamps.
- `src/main/runtime/subtitle-selection.ts` reads and validates mpv subtitle tracks and applies primary/secondary selections. Its opt-in session shortcut opens the shared overlay modal window, with renderer focus and subtitle suppression handled by the modal registry.
- `src/shared/session-key-sequences.ts` rejects sequence prefixes reserved by single-key actions. The session-binding compiler reserves configured and built-in overlay keys; `src/main/runtime/session-bindings-runtime.ts` adds active mpv bindings and publishes the effective list to both the plugin artifact and the renderer through `session-bindings:changed`. mpv no-op `ignore` bindings do not reserve prefixes.
- `src/renderer/` owns overlay rendering and input behavior. - `src/renderer/` owns overlay rendering and input behavior.
- `src/config/` owns config definitions, defaults, loading, and resolution. - `src/config/` owns config definitions, defaults, loading, and resolution.
- `src/types/` owns shared cross-runtime contracts via domain entrypoints; `src/types.ts` stays a compatibility barrel. - `src/types/` owns shared cross-runtime contracts via domain entrypoints; `src/types.ts` stays a compatibility barrel.
@@ -46,12 +44,6 @@ Update checks and startup launcher migration share a serialized update-state sto
## Architecture Intent ## Architecture Intent
The dictionary backend is selected once at startup by `dictionaryBackend`. Yomitan keeps its existing session and external-profile policy. Hachidori uses `persist:hachidori`; overlay windows select that session before extension loading, including deferred startup. Named settings flags can open either backend without injecting a second reader into the active overlay. The detached stats word helper reads the same config key so dashboard mining uses the active backend.
`setup-state.json` records one backend's status at a time plus `completedDictionaryBackends`, the backends that finished setup before. The app projects the file onto its active backend on startup and stamps that backend into the file. The launcher gates playback on the stamped backend when an app is already running, since a config edit takes effect only after restart.
`vendor/hachidori/` is a submodule of `ksyasuda/hachidori`, tracking the `subminer` branch and pinned to a tested commit. Its nested HoshiDicts submodule and WASM binaries remain upstream versions. Initialize sources with `git submodule update --init --recursive`; merge upstream updates in the fork, test them, then update SubMiner's submodule commit. `SOURCE.json` records the upstream base and artifact checksums; the submodule commit identifies the integrated version. `build:hachidori` verifies recorded artifact checksums and stages the extension for development and packaging. It enables overlay mode, disables custom JavaScript, keeps the lookup highlight on in the overlay first-install options (SubMiner captures media from mpv, not the overlay viewport), removes the unsupported `userScripts` permission, and sets a fixed manifest `key` so the extension ID (and the storage origin holding its dictionaries and settings) does not depend on the userData path, only in that staged copy; the fork keeps upstream browser defaults. Before loading the extension, its session clears service worker registrations so Electron uses the current bundled code; dictionary databases and settings remain intact. First-run setup uses Hachidori sharing messages to link or unlink external dictionary hosts and checks their live inventory. Linked dictionaries and dictionary edits use the host, while Anki configuration, pronunciation sources, custom buttons, and mining stay local to SubMiner. The parser bridge adapts its runtime messages to the existing subtitle scanner and dictionary automation. Scanning retains term-entry frequencies, and only tokens without ranks need further frequency lookups through the existing term-entry API. This requires a matching definition entry and does not preserve the frequency source's reading provenance. SubMiner consumes native `hachidori-popup-shown` and `hachidori-popup-hidden` attention events for mouse handling, keyboard focus, and the subtitle sidebar. Attention also covers a left press anywhere on the overlay that may start a selection, and the host element only exists after the first lookup, so once a Hachidori event has been seen popup auto-pause requires an unhidden popup pane in the host's shadow root and rechecks after each successful lookup. The fork retains host attributes, hover and successful-lookup notifications, and commands that need private reader state. The Anki proxy strips local duplicate/overwrite metadata before forwarding requests and enriches only confirmed writes.
- Small units, explicit boundaries - Small units, explicit boundaries
- Composition over monoliths - Composition over monoliths
- Pure helpers where possible - Pure helpers where possible
+12 -2
View File
@@ -3,7 +3,7 @@
# Domain Ownership # Domain Ownership
Status: active Status: active
Last verified: 2026-07-15 Last verified: 2026-08-02
Owner: Kyle Yasuda Owner: Kyle Yasuda
Read when: you need to find the owner module for a behavior or test surface Read when: you need to find the owner module for a behavior or test surface
@@ -21,7 +21,6 @@ Read when: you need to find the owner module for a behavior or test surface
`src/config/resolve/anki-connect/` `src/config/resolve/anki-connect/`
- Overlay/window state: `src/core/services/overlay-*`, `src/main/overlay-*.ts` - Overlay/window state: `src/core/services/overlay-*`, `src/main/overlay-*.ts`
- MPV runtime and protocol: `src/core/services/mpv*.ts` - MPV runtime and protocol: `src/core/services/mpv*.ts`
Windows executable lookup and detached process creation are shared in `src/main/runtime/mpv-process.ts`. The Windows launcher and Jellyfin handlers retain their own playback and connection workflows.
- Subtitle/token pipeline: `src/core/services/subtitle-*.ts`, `src/core/services/tokenizer*`, `src/core/services/tokenizer/`, `src/subsync/` - Subtitle/token pipeline: `src/core/services/subtitle-*.ts`, `src/core/services/tokenizer*`, `src/core/services/tokenizer/`, `src/subsync/`
- Anki workflow: `src/anki-integration/`, `src/core/services/anki-jimaku*.ts` - Anki workflow: `src/anki-integration/`, `src/core/services/anki-jimaku*.ts`
- Immersion tracking: `src/core/services/immersion-tracker/` - Immersion tracking: `src/core/services/immersion-tracker/`
@@ -33,6 +32,16 @@ Read when: you need to find the owner module for a behavior or test surface
- AniList tracking + character dictionary: `src/core/services/anilist/`, `src/main/runtime/composers/anilist-*`, `src/main/character-dictionary-runtime.ts`, `src/main/character-dictionary-runtime/` - AniList tracking + character dictionary: `src/core/services/anilist/`, `src/main/runtime/composers/anilist-*`, `src/main/character-dictionary-runtime.ts`, `src/main/character-dictionary-runtime/`
- TMDB live-action metadata: `src/core/services/tmdb/` (client + exact-title resolver), `src/core/services/immersion-tracker/live-action-link.ts` (links an entry to a TMDB title and merges other holders of the same title). The AniList cover-art fetcher calls the resolver as its fallback; `imm_anime.media_kind` marks the result and keeps the entry out of AniList season repair. - TMDB live-action metadata: `src/core/services/tmdb/` (client + exact-title resolver), `src/core/services/immersion-tracker/live-action-link.ts` (links an entry to a TMDB title and merges other holders of the same title). The AniList cover-art fetcher calls the resolver as its fallback; `imm_anime.media_kind` marks the result and keeps the entry out of AniList season repair.
- Jellyfin integration: `src/core/services/jellyfin*.ts`, `src/main/runtime/composers/jellyfin-*` - Jellyfin integration: `src/core/services/jellyfin*.ts`, `src/main/runtime/composers/jellyfin-*`
- Anime browser: extension bridge client, sidecar, and stream handling in `src/anime-bridge/`;
the loopback stream proxy separates request transport/retry from response transformation in
`stream-strip-transport.ts` and `stream-strip-response.ts`;
browser window UI in `src/animeui/` (preload `src/preload-animeui.ts`); runtime wiring in
`src/main/runtime/anime-browser-application-runtime.ts`, `src/main/runtime/anime-browser-runtime.ts`,
`src/main/runtime/anime-browser-ipc-handlers.ts`, `src/main/runtime/anime-browser-sessions.ts`,
`src/main/runtime/anime-bridge-installer.ts`, and `src/main/runtime/stream-playback-metadata.ts`.
The play queue resolves episodes on click and appends them to mpv's real playlist
(`src/main/runtime/anime-browser-queue.ts`), then observes media-path changes to
attach prepared external tracks and update the browser queue state.
- Window trackers: `src/window-trackers/` - Window trackers: `src/window-trackers/`
- Stats HTTP app: `src/core/services/stats-server.ts`, with route groups and shared route support - Stats HTTP app: `src/core/services/stats-server.ts`, with route groups and shared route support
in `src/core/services/stats-server/` in `src/core/services/stats-server/`
@@ -69,6 +78,7 @@ on Chromium document focus or require activating the overlay window.
- Settings UI contracts: `src/types/settings.ts` - Settings UI contracts: `src/types/settings.ts`
- Session-binding contracts: `src/types/session-bindings.ts` - Session-binding contracts: `src/types/session-bindings.ts`
- Stats HTTP wire contracts: `src/types/stats-wire.ts`, `src/types/stats-http-contract.ts` - Stats HTTP wire contracts: `src/types/stats-wire.ts`, `src/types/stats-http-contract.ts`
- Anime browser contracts: `src/types/anime-browser.ts`, bridge wire types in `src/anime-bridge/types.ts`
- Compatibility-only barrel: `src/types.ts` - Compatibility-only barrel: `src/types.ts`
## Ownership Heuristics ## Ownership Heuristics
+1 -1
View File
@@ -11,7 +11,7 @@ Read when: finding internal docs or checking verification status
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| KB home | `docs/README.md` | active | 2026-05-23 | internal entrypoint | | KB home | `docs/README.md` | active | 2026-05-23 | internal entrypoint |
| Architecture index | `docs/architecture/README.md` | active | 2026-05-23 | top-level runtime map | | Architecture index | `docs/architecture/README.md` | active | 2026-05-23 | top-level runtime map |
| Domain ownership | `docs/architecture/domains.md` | active | 2026-05-23 | runtime and feature ownership | | Domain ownership | `docs/architecture/domains.md` | active | 2026-08-02 | runtime and feature ownership |
| Layering rules | `docs/architecture/layering.md` | active | 2026-05-23 | dependency direction and smells | | Layering rules | `docs/architecture/layering.md` | active | 2026-05-23 | dependency direction and smells |
| Subtitle overlay priming | `docs/architecture/subtitle-overlay-priming.md` | active | 2026-06-01 | visible-overlay subtitle startup flow | | Subtitle overlay priming | `docs/architecture/subtitle-overlay-priming.md` | active | 2026-06-01 | visible-overlay subtitle startup flow |
| KB rules | `docs/knowledge-base/README.md` | active | 2026-05-23 | maintenance policy | | KB rules | `docs/knowledge-base/README.md` | active | 2026-05-23 | maintenance policy |
+3 -4
View File
@@ -60,10 +60,9 @@ bun run docs:build
- Build/release scripts (`scripts/**`): `bun run test:scripts` - Build/release scripts (`scripts/**`): `bun run test:scripts`
- Packaging: build the platform package, then run `bun run test:package <resources-directory>`. - Packaging: build the platform package, then run `bun run test:package <resources-directory>`.
On headless Linux: `xvfb-run -a bun run test:package release/linux-unpacked/resources`. On headless Linux: `xvfb-run -a bun run test:package release/linux-unpacked/resources`.
Content checks run inside the electron-builder afterPack hook. See the Content checks and informational size reporting run inside electron-builder hooks. See the
[release guide](../RELEASING.md#package-contents-checks) for the [release guide](../RELEASING.md#package-contents-and-size-checks) for size reports
installed-app verification checklist. and the installed-app verification checklist.
- Dictionary backend windows: after a full build, run `xvfb-run -a bun run test:dictionary:electron` on headless Linux. It uses temporary profiles to check both named settings commands, backend session isolation, the overlay's external-link bridge, and Hachidori's native dictionary parser.
- Coverage for the maintained source lane: `bun run test:coverage:src` - Coverage for the maintained source lane: `bun run test:coverage:src`
- Deep/local full gate: default handoff gate above - Deep/local full gate: default handoff gate above
+11
View File
@@ -16,6 +16,10 @@ type AppCommandDeps = {
appPath: string, appPath: string,
logLevel: LauncherCommandContext['args']['logLevel'], logLevel: LauncherCommandContext['args']['logLevel'],
) => void; ) => void;
launchAnimeBrowserDetached: (
appPath: string,
logLevel: LauncherCommandContext['args']['logLevel'],
) => void;
}; };
const defaultAppCommandDeps: AppCommandDeps = { const defaultAppCommandDeps: AppCommandDeps = {
@@ -23,6 +27,8 @@ const defaultAppCommandDeps: AppCommandDeps = {
launchSyncUiDetached: (appPath, logLevel) => launchSyncUiDetached: (appPath, logLevel) =>
launchAppCommandDetached(appPath, ['--sync-window'], logLevel, 'sync-ui'), launchAppCommandDetached(appPath, ['--sync-window'], logLevel, 'sync-ui'),
launchAppBackgroundDetached, launchAppBackgroundDetached,
launchAnimeBrowserDetached: (appPath, logLevel) =>
launchAppCommandDetached(appPath, ['--anime'], logLevel, 'anime'),
}; };
export function runAppPassthroughCommand( export function runAppPassthroughCommand(
@@ -37,6 +43,11 @@ export function runAppPassthroughCommand(
deps.runAppCommandWithInherit(appPath, ['--settings']); deps.runAppCommandWithInherit(appPath, ['--settings']);
return true; return true;
} }
if (args.animeBrowser) {
// Detached: the browser window is long-lived and owns the bridge process.
deps.launchAnimeBrowserDetached(appPath, args.logLevel);
return true;
}
if (args.syncUi) { if (args.syncUi) {
deps.launchSyncUiDetached(appPath, args.logLevel); deps.launchSyncUiDetached(appPath, args.logLevel);
return true; return true;
@@ -207,6 +207,7 @@ test('app command starts default macOS background app detached from launcher', (
calls.push('attached'); calls.push('attached');
}, },
launchSyncUiDetached: () => calls.push('sync-ui'), launchSyncUiDetached: () => calls.push('sync-ui'),
launchAnimeBrowserDetached: () => {},
launchAppBackgroundDetached: (appPath, logLevel) => { launchAppBackgroundDetached: (appPath, logLevel) => {
calls.push(`detached:${appPath}:${logLevel}`); calls.push(`detached:${appPath}:${logLevel}`);
}, },
@@ -227,6 +228,7 @@ test('app command starts default Linux background app detached from launcher', (
calls.push('attached'); calls.push('attached');
}, },
launchSyncUiDetached: () => calls.push('sync-ui'), launchSyncUiDetached: () => calls.push('sync-ui'),
launchAnimeBrowserDetached: () => {},
launchAppBackgroundDetached: (appPath, logLevel) => { launchAppBackgroundDetached: (appPath, logLevel) => {
calls.push(`detached:${appPath}:${logLevel}`); calls.push(`detached:${appPath}:${logLevel}`);
}, },
@@ -248,6 +250,7 @@ test('app command keeps explicit passthrough args attached', () => {
forwarded.push(appArgs); forwarded.push(appArgs);
}, },
launchSyncUiDetached: () => detached.push('sync-ui'), launchSyncUiDetached: () => detached.push('sync-ui'),
launchAnimeBrowserDetached: () => {},
launchAppBackgroundDetached: () => { launchAppBackgroundDetached: () => {
detached.push('detached'); detached.push('detached');
}, },
@@ -266,6 +269,7 @@ test('sync UI command launches the app detached from the terminal', () => {
const handled = runAppPassthroughCommand(context, { const handled = runAppPassthroughCommand(context, {
runAppCommandWithInherit: () => calls.push('piped'), runAppCommandWithInherit: () => calls.push('piped'),
launchSyncUiDetached: (appPath, logLevel) => calls.push(`sync-ui:${appPath}:${logLevel}`), launchSyncUiDetached: (appPath, logLevel) => calls.push(`sync-ui:${appPath}:${logLevel}`),
launchAnimeBrowserDetached: () => calls.push('anime'),
launchAppBackgroundDetached: () => calls.push('detached'), launchAppBackgroundDetached: () => calls.push('detached'),
}); });
@@ -63,6 +63,7 @@ function createContext(): LauncherCommandContext {
logsExport: false, logsExport: false,
version: false, version: false,
settings: false, settings: false,
animeBrowser: false,
configPath: false, configPath: false,
configShow: false, configShow: false,
mpvIdle: false, mpvIdle: false,
+1 -7
View File
@@ -26,10 +26,7 @@ import {
readSetupState, readSetupState,
} from '../../src/shared/setup-state.js'; } from '../../src/shared/setup-state.js';
import { detectInstalledFirstRunPluginCandidates } from '../../src/main/runtime/first-run-setup-plugin.js'; import { detectInstalledFirstRunPluginCandidates } from '../../src/main/runtime/first-run-setup-plugin.js';
import { import { hasLauncherExternalYomitanProfileConfig } from '../config.js';
hasLauncherExternalYomitanProfileConfig,
loadLauncherDictionaryBackend,
} from '../config.js';
const SETUP_WAIT_TIMEOUT_MS = 10 * 60 * 1000; const SETUP_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
const SETUP_POLL_INTERVAL_MS = 500; const SETUP_POLL_INTERVAL_MS = 500;
@@ -118,9 +115,6 @@ async function ensurePlaybackSetupReady(context: LauncherCommandContext): Promis
const configDir = getLauncherConfigDir(); const configDir = getLauncherConfigDir();
const statePath = getSetupStatePath(configDir); const statePath = getSetupStatePath(configDir);
const ready = await ensureLauncherSetupReady({ const ready = await ensureLauncherSetupReady({
dictionaryBackend: loadLauncherDictionaryBackend(),
isAppRunning: () => isRunningAppControlServerAvailable(args.logLevel, configDir),
warn: (message) => log('warn', args.logLevel, message),
readSetupState: () => readSetupState(statePath), readSetupState: () => readSetupState(statePath),
isExternalYomitanConfigured: () => hasLauncherExternalYomitanProfileConfig(), isExternalYomitanConfigured: () => hasLauncherExternalYomitanProfileConfig(),
hasLegacyMpvPlugin: () => hasLegacyMpvPlugin: () =>
-7
View File
@@ -1,5 +1,4 @@
import { fail } from './log.js'; import { fail } from './log.js';
import type { DictionaryBackend } from '../src/types/config.js';
import type { import type {
Args, Args,
LauncherLoggingConfig, LauncherLoggingConfig,
@@ -101,12 +100,6 @@ export function loadLauncherLoggingConfig(): LauncherLoggingConfig {
}; };
} }
export function loadLauncherDictionaryBackend(): DictionaryBackend {
return readLauncherMainConfigObject()?.dictionaryBackend === 'hachidori'
? 'hachidori'
: 'yomitan';
}
export function hasLauncherExternalYomitanProfileConfig(): boolean { export function hasLauncherExternalYomitanProfileConfig(): boolean {
return readExternalYomitanProfilePath(readLauncherMainConfigObject()) !== null; return readExternalYomitanProfilePath(readLauncherMainConfigObject()) !== null;
} }
+4
View File
@@ -120,6 +120,7 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => {
logLevel: 'warn', logLevel: 'warn',
}, },
settingsInvocation: null, settingsInvocation: null,
animeInvocation: null,
mpvInvocation: null, mpvInvocation: null,
appInvocation: null, appInvocation: null,
dictionaryTriggered: false, dictionaryTriggered: false,
@@ -174,6 +175,7 @@ test('applyInvocationsToArgs maps settings invocation to settings window', () =>
settingsInvocation: { settingsInvocation: {
logLevel: undefined, logLevel: undefined,
}, },
animeInvocation: null,
mpvInvocation: null, mpvInvocation: null,
appInvocation: null, appInvocation: null,
dictionaryTriggered: false, dictionaryTriggered: false,
@@ -221,6 +223,7 @@ test('applyInvocationsToArgs fails when config invocation has no action', () =>
action: undefined, action: undefined,
}, },
settingsInvocation: null, settingsInvocation: null,
animeInvocation: null,
mpvInvocation: null, mpvInvocation: null,
appInvocation: null, appInvocation: null,
dictionaryTriggered: false, dictionaryTriggered: false,
@@ -266,6 +269,7 @@ test('applyInvocationsToArgs maps texthooker browser-open request', () => {
jellyfinInvocation: null, jellyfinInvocation: null,
configInvocation: null, configInvocation: null,
settingsInvocation: null, settingsInvocation: null,
animeInvocation: null,
mpvInvocation: null, mpvInvocation: null,
appInvocation: null, appInvocation: null,
dictionaryTriggered: false, dictionaryTriggered: false,
+7 -4
View File
@@ -170,6 +170,7 @@ export function createDefaultArgs(
version: false, version: false,
update: false, update: false,
settings: false, settings: false,
animeBrowser: false,
configPath: false, configPath: false,
configShow: false, configShow: false,
mpvIdle: false, mpvIdle: false,
@@ -241,10 +242,6 @@ export function applyRootOptionsToArgs(
if (options.update === true) parsed.update = true; if (options.update === true) parsed.update = true;
if (options.version === true) parsed.version = true; if (options.version === true) parsed.version = true;
if (options.settings === true) parsed.settings = true; if (options.settings === true) parsed.settings = true;
if (options.yomitan === true || options.hachidori === true) {
parsed.appPassthrough = true;
parsed.appArgs = [options.yomitan === true ? '--yomitan' : '--hachidori'];
}
if (options.startOverlay === true) parsed.autoStartOverlay = true; if (options.startOverlay === true) parsed.autoStartOverlay = true;
if (options.texthooker === false) parsed.useTexthooker = false; if (options.texthooker === false) parsed.useTexthooker = false;
if (typeof options.args === 'string') parsed.mpvArgs = options.args; if (typeof options.args === 'string') parsed.mpvArgs = options.args;
@@ -360,6 +357,12 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
); );
} }
if (invocations.animeInvocation) {
if (invocations.animeInvocation.logLevel) {
parsed.logLevel = parseLogLevel(invocations.animeInvocation.logLevel);
}
parsed.animeBrowser = true;
}
if (invocations.settingsInvocation) { if (invocations.settingsInvocation) {
if (invocations.settingsInvocation.logLevel) { if (invocations.settingsInvocation.logLevel) {
parsed.logLevel = parseLogLevel(invocations.settingsInvocation.logLevel); parsed.logLevel = parseLogLevel(invocations.settingsInvocation.logLevel);
+14 -2
View File
@@ -29,6 +29,7 @@ export interface CliInvocations {
jellyfinInvocation: JellyfinInvocation | null; jellyfinInvocation: JellyfinInvocation | null;
configInvocation: CommandActionInvocation | null; configInvocation: CommandActionInvocation | null;
settingsInvocation: CommandActionInvocation | null; settingsInvocation: CommandActionInvocation | null;
animeInvocation: CommandActionInvocation | null;
mpvInvocation: CommandActionInvocation | null; mpvInvocation: CommandActionInvocation | null;
appInvocation: { appArgs: string[] } | null; appInvocation: { appArgs: string[] } | null;
dictionaryTriggered: boolean; dictionaryTriggered: boolean;
@@ -86,8 +87,6 @@ function applyRootOptions(program: Command): void {
.option('--log-level <level>', 'Log level') .option('--log-level <level>', 'Log level')
.option('-v, --version', 'Show SubMiner version') .option('-v, --version', 'Show SubMiner version')
.option('--settings', 'Open settings window') .option('--settings', 'Open settings window')
.option('--yomitan', 'Open Yomitan settings window')
.option('--hachidori', 'Open Hachidori settings window')
.option('-u, --update', 'Check for updates') .option('-u, --update', 'Check for updates')
.option('-R, --rofi', 'Use rofi picker') .option('-R, --rofi', 'Use rofi picker')
.option('-H, --history', 'Browse local watch history') .option('-H, --history', 'Browse local watch history')
@@ -123,6 +122,7 @@ function getTopLevelCommand(argv: string[]): { name: string; index: number } | n
'doctor', 'doctor',
'config', 'config',
'settings', 'settings',
'anime',
'mpv', 'mpv',
'logs', 'logs',
'dictionary', 'dictionary',
@@ -177,6 +177,7 @@ export function parseCliPrograms(
let jellyfinInvocation: JellyfinInvocation | null = null; let jellyfinInvocation: JellyfinInvocation | null = null;
let configInvocation: CommandActionInvocation | null = null; let configInvocation: CommandActionInvocation | null = null;
let settingsInvocation: CommandActionInvocation | null = null; let settingsInvocation: CommandActionInvocation | null = null;
let animeInvocation: CommandActionInvocation | null = null;
let mpvInvocation: CommandActionInvocation | null = null; let mpvInvocation: CommandActionInvocation | null = null;
let appInvocation: { appArgs: string[] } | null = null; let appInvocation: { appArgs: string[] } | null = null;
let dictionaryTriggered = false; let dictionaryTriggered = false;
@@ -514,6 +515,16 @@ export function parseCliPrograms(
}; };
}); });
commandProgram
.command('anime')
.description('Open the anime browser window')
.option('--log-level <level>', 'Log level')
.action((options: Record<string, unknown>) => {
animeInvocation = {
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
};
});
commandProgram commandProgram
.command('mpv') .command('mpv')
.description('MPV helpers') .description('MPV helpers')
@@ -569,6 +580,7 @@ export function parseCliPrograms(
jellyfinInvocation, jellyfinInvocation,
configInvocation, configInvocation,
settingsInvocation, settingsInvocation,
animeInvocation,
mpvInvocation, mpvInvocation,
appInvocation, appInvocation,
dictionaryTriggered, dictionaryTriggered,
+1
View File
@@ -57,6 +57,7 @@ function createArgs(): Args {
logsExport: false, logsExport: false,
version: false, version: false,
settings: false, settings: false,
animeBrowser: false,
configPath: false, configPath: false,
configShow: false, configShow: false,
mpvIdle: false, mpvIdle: false,
+2 -1
View File
@@ -89,6 +89,7 @@ export async function jellyfinApiRequest<T>(
const url = `${session.serverUrl}${requestPath}`; const url = `${session.serverUrl}${requestPath}`;
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
'X-Emby-Token': session.accessToken,
Authorization: `MediaBrowser Token="${session.accessToken}"`, Authorization: `MediaBrowser Token="${session.accessToken}"`,
}, },
}); });
@@ -102,7 +103,7 @@ export async function jellyfinApiRequest<T>(
} }
function itemPreviewUrl(session: JellyfinSessionConfig, id: string): string { function itemPreviewUrl(session: JellyfinSessionConfig, id: string): string {
return `${session.serverUrl}/Items/${id}/Images/Primary?maxHeight=720&quality=85&ApiKey=${encodeURIComponent(session.accessToken)}`; return `${session.serverUrl}/Items/${id}/Images/Primary?maxHeight=720&quality=85&api_key=${encodeURIComponent(session.accessToken)}`;
} }
function jellyfinIconCacheDir(session: JellyfinSessionConfig): string { function jellyfinIconCacheDir(session: JellyfinSessionConfig): string {
+4 -57
View File
@@ -282,8 +282,7 @@ test('doctor refresh-known-words forwards app refresh command without requiring
}); });
}); });
for (const flag of ['--settings', '--yomitan', '--hachidori']) { test('launcher settings option forwards app settings window command', () => {
test(`launcher ${flag} forwards the matching app settings command`, () => {
withTempDir((root) => { withTempDir((root) => {
const homeDir = path.join(root, 'home'); const homeDir = path.join(root, 'home');
const xdgConfigHome = path.join(root, 'xdg'); const xdgConfigHome = path.join(root, 'xdg');
@@ -300,13 +299,12 @@ for (const flag of ['--settings', '--yomitan', '--hachidori']) {
SUBMINER_APPIMAGE_PATH: appPath, SUBMINER_APPIMAGE_PATH: appPath,
SUBMINER_TEST_CAPTURE: capturePath, SUBMINER_TEST_CAPTURE: capturePath,
}; };
const result = runLauncher([flag], env); const result = runLauncher(['--settings'], env);
assert.equal(result.status, 0); assert.equal(result.status, 0);
assert.equal(fs.readFileSync(capturePath, 'utf8'), `${flag}\n`); assert.equal(fs.readFileSync(capturePath, 'utf8'), '--settings\n');
}); });
}); });
}
test('launcher settings command forwards app settings window command', () => { test('launcher settings command forwards app settings window command', () => {
withTempDir((root) => { withTempDir((root) => {
@@ -1112,54 +1110,3 @@ test('classifyJellyfinChildSelection keeps container drilldown state instead of
id: 'season-2', id: 'season-2',
}); });
}); });
test('external Yomitan profile remains available while a running app awaits a backend switch', () => {
withTempDir((dir) => {
const env = makeTestEnv(dir, path.join(dir, 'config'));
const configPath = resolveConfigFilePath({
appDataDir: env.APPDATA,
xdgConfigHome: env.XDG_CONFIG_HOME,
homeDir: dir,
existsSync: () => false,
});
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(
configPath,
JSON.stringify({
dictionaryBackend: 'hachidori',
yomitan: { externalProfilePath: '/external/yomitan-profile' },
}),
);
const result = spawnSync(
process.execPath,
[
'--eval',
`
import assert from 'node:assert/strict';
import { hasLauncherExternalYomitanProfileConfig } from './launcher/config.ts';
import { ensureLauncherSetupReady } from './launcher/setup-gate.ts';
import { createDefaultSetupState } from './src/shared/setup-state.ts';
for (const running of [true, false]) {
let launches = 0;
let tick = 0;
const ready = await ensureLauncherSetupReady({
dictionaryBackend: 'hachidori',
isAppRunning: async () => running,
readSetupState: () => ({ ...createDefaultSetupState(), dictionaryBackend: 'yomitan' }),
isExternalYomitanConfigured: hasLauncherExternalYomitanProfileConfig,
launchSetupApp: () => { launches += 1; },
sleep: async () => {},
now: () => tick++,
timeoutMs: 2,
pollIntervalMs: 1,
});
assert.equal(ready, running);
assert.equal(launches, running ? 0 : 1);
}
`,
],
{ cwd: process.cwd(), env, encoding: 'utf8', timeout: LAUNCHER_RUN_TIMEOUT_MS },
);
assert.equal(result.status, 0, result.stderr);
});
});
+1
View File
@@ -627,6 +627,7 @@ function makeArgs(overrides: Partial<Args> = {}): Args {
logsExport: false, logsExport: false,
version: false, version: false,
settings: false, settings: false,
animeBrowser: false,
configPath: false, configPath: false,
configShow: false, configShow: false,
mpvIdle: false, mpvIdle: false,

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