mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-09 04:19:27 -07:00
Compare commits
11 Commits
v0.11.1
...
7a64488ed5
| Author | SHA1 | Date | |
|---|---|---|---|
|
7a64488ed5
|
|||
|
|
5f3c3871d3 | ||
| 4d24e22bb5 | |||
| c47cfb52af | |||
|
da0087bba6
|
|||
|
8338f27794
|
|||
|
b029d65c90
|
|||
|
c24f99899b
|
|||
|
3aca581764
|
|||
|
ba540d09b2
|
|||
|
6530d2ccbc
|
94
README.md
94
README.md
@@ -4,25 +4,21 @@
|
||||
|
||||
# SubMiner
|
||||
|
||||
## Turn mpv into a sentence-mining workstation.
|
||||
|
||||
Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv.
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/ksyasuda/SubMiner)
|
||||
[](https://docs.subminer.moe)
|
||||
[Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
|
||||
|
||||
[](https://github.com/ksyasuda/SubMiner/releases)
|
||||
[](https://github.com/ksyasuda/SubMiner/releases/latest)
|
||||
[](https://aur.archlinux.org/packages/subminer-bin)
|
||||
[](https://github.com/ksyasuda/SubMiner)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.typescriptlang.org)
|
||||
|
||||
[](https://github.com/user-attachments/assets/89e61895-e2b7-4b47-8d50-a35afe4132b2)
|
||||
|
||||
</div>
|
||||
|
||||
## How It Works
|
||||
|
||||
SubMiner runs as an invisible Electron overlay on top of mpv. Subtitles render as an interactive layer. Move your cursor over any word and trigger a [Yomitan](https://github.com/yomidevs/yomitan) lookup. Press one key to snapshot the sentence, audio, and screenshot into Anki via AnkiConnect.
|
||||
|
||||
First-run setup requires the mpv plugin before it can finish. On Windows, the optional `SubMiner mpv` shortcut created during setup is the recommended playback entry point because it launches `mpv` with SubMiner's defaults directly, so you do not need an `mpv.conf` profile just to use it.
|
||||
|
||||
## Features
|
||||
|
||||
### Dictionary Lookups
|
||||
@@ -69,7 +65,9 @@ Local stats dashboard — watch time, anime library, vocabulary growth, mining t
|
||||
|
||||
Browse sibling episode files and the active mpv queue in one overlay modal. Open it with `Ctrl+Alt+P` to append episodes from the current directory, jump to queued items, remove entries, or reorder the playlist without leaving playback.
|
||||
|
||||
Managed local playback now reapplies your configured subtitle language priorities after mpv loads track metadata, so mixed subtitle sets can settle onto the expected primary and secondary tracks instead of staying on mpv's initial `sid=auto` guess.
|
||||
<div align="center">
|
||||
<img src="docs-site/public/screenshots/playlist-browser.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -112,32 +110,38 @@ Managed local playback now reapplies your configured subtitle language prioritie
|
||||
|
||||
## Requirements
|
||||
|
||||
| | Required | Optional |
|
||||
| -------------- | --------------------------------------- | ---------------------------------------------------------- |
|
||||
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — |
|
||||
| **Processing** | `ffmpeg`, `mecab` + `mecab-ipadic` | `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
|
||||
| **Media** | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
|
||||
| **Selection** | — | `fzf` / `rofi` |
|
||||
| | Required | Recommended | Optional |
|
||||
| -------------- | --------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — | — |
|
||||
| **Processing** | — | `ffmpeg` (audio clips & screenshots) | `mecab` + `mecab-ipadic` (annotation POS filtering), `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
|
||||
| **Media** | — | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
|
||||
| **Selection** | — | — | `fzf` / `rofi` |
|
||||
|
||||
> [!TIP]
|
||||
> `ffmpeg` is not strictly required to run SubMiner, but without it audio clips and screenshots will not be attached to Anki cards. Most users will want it installed.
|
||||
|
||||
> [!NOTE]
|
||||
> [`bun`](https://bun.sh) is required if building from source or using the CLI wrapper: `subminer`. Pre-built releases (AppImage, DMG, installer) do not require it.
|
||||
|
||||
**Platform-specific:**
|
||||
|
||||
| Linux | macOS | Windows |
|
||||
| ----------------------------------- | ------------------------ | ------------- |
|
||||
| `hyprctl` or `xdotool` + `xwininfo` | Accessibility permission | No extra deps |
|
||||
| Linux | macOS | Windows |
|
||||
| ------------------------------------------------------------ | ------------------------ | ------------- |
|
||||
| Hyprland (`hyprctl`) · X11/Xwayland (`xdotool` + `xwininfo`) | Accessibility permission | No extra deps |
|
||||
|
||||
> [!NOTE]
|
||||
> **Wayland support is compositor-specific.** Wayland has no universal API for window positioning and each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Hyprland is the only native Wayland backend supported currenlty. All other Linux compositors require both mpv and SubMiner to run under X11 or Xwayland.
|
||||
|
||||
<details>
|
||||
<summary><b>Arch Linux</b></summary>
|
||||
|
||||
```bash
|
||||
paru -S --needed mpv ffmpeg mecab-git mecab-ipadic
|
||||
paru -S --needed mpv ffmpeg
|
||||
# Optional
|
||||
paru -S --needed yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo
|
||||
paru -S --needed mecab-git mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo
|
||||
# Optional: subtitle sync (install at least one for subtitle syncing to work)
|
||||
paru -S --needed alass python-ffsubsync
|
||||
# X11 / XWAYLAND
|
||||
# X11 / Xwayland (required for non-Hyprland compositors)
|
||||
paru -S --needed xdotool xorg-xwininfo
|
||||
```
|
||||
|
||||
@@ -147,9 +151,9 @@ paru -S --needed xdotool xorg-xwininfo
|
||||
<summary><b>macOS</b></summary>
|
||||
|
||||
```bash
|
||||
brew install mpv ffmpeg mecab mecab-ipadic
|
||||
brew install mpv ffmpeg
|
||||
# Optional
|
||||
brew install yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||
brew install mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||
# Optional: subtitle sync (install at least one for subtitle syncing to work)
|
||||
brew install alass
|
||||
pip install ffsubsync
|
||||
@@ -164,7 +168,7 @@ Grant Accessibility permission to SubMiner in **System Settings > Privacy & Secu
|
||||
|
||||
Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on your `PATH`.
|
||||
|
||||
For MeCab, install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary.
|
||||
Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for additional metadata enrichment.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -210,6 +214,16 @@ wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~
|
||||
|
||||
Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
|
||||
|
||||
Also download the `subminer` launcher (recommended):
|
||||
|
||||
```bash
|
||||
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer \
|
||||
&& sudo chmod +x /usr/local/bin/subminer
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The `subminer` launcher uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -217,6 +231,10 @@ Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasud
|
||||
|
||||
Download the latest installer or portable `.zip` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`.
|
||||
|
||||
**Windows support is experimental.** Core features such as mining, annotations, and dictionary lookups work, but some functionality may be missing or unstable. Bug reports welcome.
|
||||
|
||||
**Note:** On Windows the `subminer` launcher requires [`bun`](https://bun.sh) and must be invoked with `bun run subminer` instead of running the script directly. The recommended alternative is the **SubMiner mpv** shortcut created during first-run setup — double-click it, drag files onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See the [Windows mpv Shortcut](https://docs.subminer.moe/usage#windows-mpv-shortcut) section for details.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -228,9 +246,25 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
|
||||
|
||||
### 2. First Launch
|
||||
|
||||
Run the app. On first launch SubMiner starts in the system tray, creates a default config, and opens a setup popup to finish config, install the mpv plugin, and configure Yomitan dictionaries.
|
||||
```bash
|
||||
subminer app --setup # launch the first-run setup wizard
|
||||
```
|
||||
|
||||
### 3. Mine
|
||||
SubMiner creates a default config, starts in the system tray, and opens a setup popup that walks you through installing the mpv plugin and configuring Yomitan dictionaries. Follow the on-screen steps to complete setup.
|
||||
|
||||
> [!NOTE]
|
||||
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
|
||||
|
||||
### 3. Verify Setup
|
||||
|
||||
```bash
|
||||
subminer doctor # verify mpv, ffmpeg, config, and socket
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly — first-run setup will guide you through dependency checks.
|
||||
|
||||
### 4. Mine
|
||||
|
||||
```bash
|
||||
subminer video.mkv # play video with overlay
|
||||
@@ -240,6 +274,8 @@ subminer stats -b # stats daemon in background
|
||||
subminer stats -s # stop background stats daemon
|
||||
```
|
||||
|
||||
On **Windows**, the `subminer` script must be run with `bun run subminer` (e.g. `bun run subminer video.mkv`). The recommended alternative is the **SubMiner mpv** shortcut (created during setup) or `SubMiner.exe --launch-mpv`. Drag a video file onto the shortcut to play it, or double-click it to open mpv with SubMiner's defaults.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full guides on configuration, Anki setup, Jellyfin, immersion tracking, and more: **[docs.subminer.moe](https://docs.subminer.moe)**
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
id: TASK-279
|
||||
title: Fix Linux AppImage child-process libffmpeg resolution
|
||||
status: Done
|
||||
assignee:
|
||||
- '@codex'
|
||||
created_date: '2026-04-05 17:17'
|
||||
updated_date: '2026-04-05 17:56'
|
||||
labels: []
|
||||
dependencies: []
|
||||
references:
|
||||
- 'https://github.com/ksyasuda/SubMiner/issues/41'
|
||||
documentation:
|
||||
- docs/workflow/verification.md
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Fix the Linux AppImage packaging so Chromium child processes relaunched from the bundled binary can resolve the packaged libffmpeg shared library and SubMiner starts cleanly instead of crash-looping on network-service restarts.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Linux AppImage packaging ensures bundled Chromium child processes can resolve the packaged libffmpeg shared library during relaunch.
|
||||
- [x] #2 Regression coverage exercises the Linux packaging/build configuration that provides the AppImage shared-library path.
|
||||
- [x] #3 Release notes/changelog reflect the Linux AppImage startup fix.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add focused regression tests for Linux release packaging that assert the build config invokes an `afterPack` hook and that the hook stages bundled `libffmpeg.so` into `usr/lib` for AppImage runtime lookup.
|
||||
2. Implement a small electron-builder `afterPack` hook that runs only for Linux, copies `libffmpeg.so` from the packaged app root into `usr/lib`, and no-ops when the source library is absent.
|
||||
3. Wire the hook into `package.json` build config and add a changelog fragment for the Linux AppImage startup fix.
|
||||
4. Run the focused test lane first, then the default handoff gate because the change touches release-sensitive packaging behavior.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Chose a repo-local electron-builder `afterPack` hook instead of patching/forking `electron-builder`. The hook copies bundled `libffmpeg.so` from the packaged Linux app root into `usr/lib`, matching the AppImage runtime's existing `LD_LIBRARY_PATH` search path.
|
||||
|
||||
Added regression coverage for both config wiring (`src/release-workflow.test.ts`) and the hook behavior (`scripts/electron-builder-after-pack.test.ts`), then wired the new script test into `test:fast` so the maintained lane keeps exercising the fix.
|
||||
|
||||
Verification passed: `bun test scripts/electron-builder-after-pack.test.ts src/release-workflow.test.ts`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist`.
|
||||
|
||||
Addressed PR #45 CodeRabbit review thread: Linux `afterPack` staging now hard-fails when `libffmpeg.so` is missing instead of silently no-oping. Updated focused hook tests to assert the new failure contract and that `afterPack` propagates Linux staging errors.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Added a shared electron-builder `afterPack` hook at `scripts/electron-builder-after-pack.cjs` and wired it into `package.json` so Linux packaging stages the bundled `libffmpeg.so` into `usr/lib` inside the packaged app. This keeps Chromium child relaunches compatible with the AppImage runtime's existing `LD_LIBRARY_PATH` layout without forking or patching upstream `electron-builder`.
|
||||
|
||||
Regression coverage now checks both the packaging config and the hook behavior: `src/release-workflow.test.ts` asserts the hook stays wired into release config, and `scripts/electron-builder-after-pack.test.ts` verifies Linux copies `libffmpeg.so` into `usr/lib` while non-Linux and missing-library cases no-op safely. The new script test is included in `test:fast`, and a changelog fragment was added under `changes/fix-appimage-libffmpeg-path.md`.
|
||||
|
||||
Verification passed with `bun test scripts/electron-builder-after-pack.test.ts src/release-workflow.test.ts`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, and `bun run test:smoke:dist`.
|
||||
|
||||
Follow-up review fix on PR #45: Linux packaging now throws when `libffmpeg.so` is missing from the packaged app root, preventing silent shipment of a broken AppImage. Focused regression coverage was updated so the missing-library case rejects and `afterPack` propagates the failure.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
id: TASK-280
|
||||
title: >-
|
||||
Force launcher-spawned mpv onto X11 when backend resolves to x11 or no
|
||||
supported Wayland tracker is available
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-05 21:01'
|
||||
updated_date: '2026-04-05 21:05'
|
||||
labels:
|
||||
- bug
|
||||
- linux
|
||||
- launcher
|
||||
- overlay
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
On Linux Plasma Wayland and similar sessions, `subminer --backend=x11` currently only changes SubMiner's window-tracker override. The launcher still spawns mpv without forcing an X11/XWayland backend, so the X11 tracker cannot find the mpv window and the overlay remains hidden. Update launcher-side mpv spawn behavior so launcher-managed mpv runs under X11 when backend resolves to `x11`, and also when auto detection cannot resolve to a supported Wayland tracker. Preserve existing Hyprland/Sway behavior.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Launcher-managed mpv is spawned with X11/XWayland-forcing environment/config when backend resolves to `x11`.
|
||||
- [x] #2 Linux auto mode falls back to X11/XWayland-forced mpv when no supported Wayland tracker backend is detected.
|
||||
- [x] #3 Hyprland and Sway launcher flows do not regress to forced X11 mpv.
|
||||
- [x] #4 Regression tests cover launcher env/backend selection for these Linux cases.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add focused launcher tests that assert the mpv spawn environment forces X11 when backend resolves to `x11`, and when Linux auto mode cannot use a supported Wayland tracker.
|
||||
2. Refactor launcher mpv spawn code to compute an mpv-specific environment without changing existing Hyprland/Sway flows.
|
||||
3. Route all launcher-managed mpv spawns through the new environment helper.
|
||||
4. Run focused launcher tests, then summarize behavior and any remaining verification gaps.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
User approved scope: force launcher-managed mpv to X11 for explicit `--backend=x11` and for unsupported Linux Wayland auto-detect fallback; preserve Hyprland/Sway behavior.
|
||||
|
||||
Implemented launcher-side `buildMpvEnv` to strip Wayland hints and force X11/XWayland for launcher-managed mpv when `--backend=x11`, and for Linux auto mode on unsupported Wayland desktops with an X11 display available. Wired both normal mpv launches and idle detached mpv launches through the helper.
|
||||
|
||||
Verification: `bun test launcher/mpv.test.ts --test-name-pattern "buildMpvEnv"` passed; `bun run tsc --noEmit` passed. A broader `bun test launcher/mpv.test.ts` run still hits a pre-existing sandbox-specific failure in `launchAppCommandDetached handles child process spawn errors` because this environment cannot write the default app log path under `/home/sudacode/.config/SubMiner/logs`.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Updated the launcher so mpv gets an X11/XWayland-oriented spawn environment whenever the user explicitly requests `--backend=x11`, and when Linux auto mode is running under an unsupported Wayland desktop that still exposes an X11 display. The new helper reuses the launcher child-process base environment, strips Wayland-specific hints (`WAYLAND_DISPLAY`, Hyprland/Sway markers), and flips `XDG_SESSION_TYPE` to `x11` only for those fallback cases. Both foreground mpv launches and detached idle mpv launches now use the same helper so overlay-tracked playback stays consistent.
|
||||
|
||||
Added focused regression coverage in `launcher/mpv.test.ts` for three cases: explicit `x11` forcing, unsupported Wayland auto fallback (for example KDE Plasma Wayland), and preserving native Wayland env for supported Hyprland/Sway auto backends. Verification completed with `bun test launcher/mpv.test.ts --test-name-pattern "buildMpvEnv"` and `bun run tsc --noEmit`. A broader launcher mpv test run still shows an unrelated sandbox write failure for the default app log path in this environment.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
id: TASK-281
|
||||
title: Prevent Windows launcher tests from leaking backslash temp files on POSIX
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-05 21:13'
|
||||
updated_date: '2026-04-05 21:20'
|
||||
labels:
|
||||
- tests
|
||||
- launcher
|
||||
- bug
|
||||
dependencies: []
|
||||
documentation:
|
||||
- /home/sudacode/github/SubMiner2/AGENTS.md
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Windows-specific launcher tests in launcher/mpv.test.ts currently create real filesystem entries using path.win32.join(...) with a POSIX mkdtemp base. On Linux/macOS this produces literal backslash-named paths like \\tmp\\subminer-test-win-dir-* inside the repo/worktree, and the existing cleanup only removes the POSIX /tmp base directory. Fix the tests so they still cover Windows path resolution behavior without leaking stray files into the working tree.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Running the Windows findAppBinary tests on a POSIX host does not create new untracked \\tmp\\subminer-test-win-* files in the repository root.
|
||||
- [x] #2 The Windows launcher tests still validate PATH and install-directory resolution behavior.
|
||||
- [x] #3 Relevant launcher tests pass after the change.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add a regression test in launcher/mpv.test.ts that exercises the Windows findAppBinary cases on a POSIX host and asserts they do not leave new backslash-named temp artifacts in the repository root.
|
||||
2. Refactor the Windows launcher tests to avoid creating real filesystem paths from path.win32.join(...) on POSIX; keep Windows path assertions via stubs and only create real files with native POSIX paths where needed.
|
||||
3. Run the targeted launcher tests and confirm no new \\tmp\\subminer-test-win-* artifacts appear in git status.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Investigation: reproduced the leak locally. The source is launcher/mpv.test.ts Windows findAppBinary tests that combine a POSIX mkdtemp base with path.win32.join(...), creating literal backslash-named entries like \\tmp\\subminer-test-win-dir-* in the repo root. Existing cleanup only removes the POSIX /tmp base directory.
|
||||
|
||||
User approved implementation plan on 2026-04-05.
|
||||
|
||||
Implemented in launcher/mpv.test.ts by replacing the leaky Windows PATH/install-directory helpers with pure fs stubs (access/exists/stat) and fixed Windows path strings instead of creating real path.win32 filesystem entries on POSIX. Added a regression test that snapshots repo-root \\tmp\\subminer-test-win-* artifacts before/after running the Windows cases and asserts no new entries are created.
|
||||
|
||||
Verification: `bun test launcher/mpv.test.ts --test-name-pattern 'findAppBinary Windows cases do not leak backslash temp artifacts on POSIX|findAppBinary resolves SubMiner.exe on PATH on Windows|findAppBinary resolves a Windows install directory to SubMiner.exe'` passed (3/3). `bun test launcher/mpv.test.ts` still has one unrelated pre-existing sandbox failure in `launchAppCommandDetached handles child process spawn errors` because the test opens `~/.config/SubMiner/logs/app-2026-04-05.log` and hits `EROFS` in this environment.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Reworked the Windows `findAppBinary` tests in `launcher/mpv.test.ts` so they no longer create real backslash-named temp files on POSIX hosts. The PATH and install-directory cases now use synthetic Windows path strings plus `fs.accessSync` / `fs.existsSync` / `fs.statSync` stubs to exercise the same resolver behavior without writing `\\tmp\\subminer-test-win-*` entries into the repository root.
|
||||
|
||||
Added a POSIX regression test that snapshots existing repo-root `\\tmp\\subminer-test-win-*` artifacts, runs the Windows path-resolution cases, and asserts the artifact set is unchanged. This catches future regressions where a Windows-path test accidentally writes literal backslash paths on Linux/macOS.
|
||||
|
||||
Tests run:
|
||||
- `bun test launcher/mpv.test.ts --test-name-pattern 'findAppBinary Windows cases do not leak backslash temp artifacts on POSIX|findAppBinary resolves SubMiner.exe on PATH on Windows|findAppBinary resolves a Windows install directory to SubMiner.exe'`
|
||||
- `bun test launcher/mpv.test.ts` (all relevant `findAppBinary` tests passed; one unrelated existing sandbox failure remains in `launchAppCommandDetached handles child process spawn errors` due `EROFS` opening `~/.config/SubMiner/logs/...`)
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
id: TASK-282
|
||||
title: >-
|
||||
Force launcher-managed mpv to an explicit X11 GPU context when X11 fallback is
|
||||
active
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-05 21:14'
|
||||
updated_date: '2026-04-05 21:15'
|
||||
labels:
|
||||
- bug
|
||||
- linux
|
||||
- launcher
|
||||
- mpv
|
||||
- overlay
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Follow-up to the launcher X11 fallback work: on Plasma Wayland, stripping Wayland env vars alone is not sufficient for launcher-managed mpv. mpv can still fail GPU initialization under the default video output path (`vo/gpu-next`) unless an explicit X11 GPU context is selected. Update launcher-managed mpv startup so X11 fallback mode also appends explicit mpv options for an X11 GPU context, while preserving supported Hyprland/Sway Wayland flows.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Launcher-managed mpv appends explicit X11 GPU context args when explicit `--backend=x11` is used on Linux.
|
||||
- [x] #2 Launcher-managed mpv appends the same explicit X11 GPU context args for Linux auto-mode fallback on unsupported Wayland desktops.
|
||||
- [x] #3 Supported Hyprland/Sway Wayland flows do not receive the forced X11 GPU context args.
|
||||
- [x] #4 Regression tests cover the forced-X11 mpv arg selection.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add focused launcher tests for explicit X11 GPU-context arg selection, unsupported Wayland auto fallback, and Hyprland/Sway no-regression cases.
|
||||
2. Introduce a shared launcher helper that decides when mpv should be forced onto X11 fallback mode.
|
||||
3. Use that helper to append explicit mpv X11 GPU-context args in both normal and detached idle mpv launch paths.
|
||||
4. Run focused launcher tests plus TypeScript verification, then record remaining runtime follow-up guidance.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
User runtime log showed `vo/gpu-next` failing with `Failed initializing any suitable GPU context!` under forced-X11 playback, which indicates env forcing alone was insufficient. Selected `--gpu-context=x11egl,x11` as the explicit mpv fallback: prefer X11/EGL, with GLX as a compatibility fallback.
|
||||
|
||||
Verification: `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` passed. `bun run tsc --noEmit` passed.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Added explicit mpv backend args for launcher-managed X11 fallback mode. The launcher now uses a shared `shouldForceX11MpvBackend` decision for both env rewriting and mpv arg selection, so explicit `--backend=x11` and unsupported Linux Wayland auto fallback both append `--gpu-context=x11egl,x11` while still stripping Wayland env hints. This preserves supported Hyprland/Sway native Wayland flows and makes the X11 fallback more explicit for mpv's GPU initialization path.
|
||||
|
||||
Wired the new X11 GPU-context args into both the normal playback launch path and the detached idle mpv launch path. Added focused regression coverage for explicit `x11`, Plasma-style unsupported Wayland auto fallback, and Hyprland/Sway no-regression behavior. Verification completed with `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` and `bun run tsc --noEmit`.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: TASK-283
|
||||
title: Force launcher-managed X11 fallback to mpv vo=gpu with OpenGL on Linux
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-05 21:19'
|
||||
updated_date: '2026-04-05 21:20'
|
||||
labels:
|
||||
- bug
|
||||
- linux
|
||||
- launcher
|
||||
- mpv
|
||||
- overlay
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Follow-up to explicit X11 gpu-context forcing: Plasma Wayland runtime logs still show mpv using `vo/gpu-next` and failing to initialize any suitable GPU context under launcher-managed X11 fallback. Update launcher-managed mpv X11 fallback mode to force a more compatible renderer stack: `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11`, while preserving supported native Wayland flows and allowing explicit user mpv args to override later on the command line.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Launcher-managed mpv appends `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11` when explicit `--backend=x11` is used on Linux.
|
||||
- [x] #2 Launcher-managed mpv appends the same renderer args for Linux auto-mode fallback on unsupported Wayland desktops.
|
||||
- [x] #3 Supported Hyprland/Sway Wayland flows do not receive the forced X11 renderer args.
|
||||
- [x] #4 Regression tests cover the forced-X11 renderer arg selection.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Tighten launcher tests so forced-X11 renderer args require `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11`.
|
||||
2. Update the shared launcher X11-fallback helper to return the full renderer arg stack for explicit `x11` and unsupported Wayland auto fallback.
|
||||
3. Re-run focused launcher env/backend tests and TypeScript verification.
|
||||
4. Hand back with retry instructions and next debugging branch if runtime still fails.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Runtime log still showed `[vo/gpu-next] Failed initializing any suitable GPU context!`, which meant forcing only the context was not enough. Updated the fallback to force the classic OpenGL renderer path too: `--vo=gpu --gpu-api=opengl --gpu-context=x11egl,x11`.
|
||||
|
||||
Verification: `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` passed. `bun run tsc --noEmit` passed.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Updated launcher-managed X11 fallback mode to force a more compatible mpv renderer stack on Linux: `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11`. This applies both to explicit `--backend=x11` and to unsupported Wayland auto fallback, while supported Hyprland/Sway Wayland sessions still keep their native path. The renderer args are still inserted before user-supplied `--args`, so an explicit user override can win later on the command line if needed.
|
||||
|
||||
Adjusted regression coverage to require the full renderer stack for forced-X11 mode and verified the helper behavior with focused launcher tests plus TypeScript compilation. Verification completed with `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` and `bun run tsc --noEmit`.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
id: TASK-284
|
||||
title: Fix CI changelog fragment requirement for launcher X11 MPV fallback change
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-04-05 22:21'
|
||||
updated_date: '2026-04-05 22:22'
|
||||
labels:
|
||||
- ci
|
||||
- changelog
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Current CI is failing in `changelog:pr-check` because PR changes release-relevant files but does not include a required entry under `changes/` and lacks `skip-changelog` label. Add a release fragment describing the behavioral change and verify the CI gate passes.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Add a correctly formatted changelog fragment under `changes/` for the current change
|
||||
- [x] #2 Run the local changelog PR check (or equivalent) with passing result
|
||||
- [x] #3 Run required CI gate commands after change and confirm no regressions
|
||||
<!-- AC:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Added `changes/2026.04.05-mpv-x11-fallback.md` with launcher release-note metadata so `changelog:pr-check` can pass. Verified local CI gate commands: `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist` all passed. Ran manual PR changelog verification by invoking `verifyPullRequestChangelog` with current git diff plus the new fragment and confirmed it passes.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
4
changes/2026.04.05-mpv-x11-fallback.md
Normal file
4
changes/2026.04.05-mpv-x11-fallback.md
Normal file
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: launcher
|
||||
|
||||
- Fixed launcher-managed mpv spawning to force an explicit X11 GPU path when Wayland trackers are unavailable.
|
||||
4
changes/43-local-subtitle-sidecar.md
Normal file
4
changes/43-local-subtitle-sidecar.md
Normal file
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: launcher
|
||||
|
||||
Local playback now promotes a single unlabeled external subtitle sidecar to the primary slot instead of leaving mpv's embedded English auto-selection in place.
|
||||
4
changes/fix-appimage-libffmpeg-path.md
Normal file
4
changes/fix-appimage-libffmpeg-path.md
Normal file
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: release
|
||||
|
||||
- Fixed Linux AppImage startup packaging so Chromium child relaunches can resolve the bundled `libffmpeg.so` instead of crash-looping on startup.
|
||||
@@ -76,7 +76,6 @@ export default {
|
||||
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
||||
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
|
||||
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
||||
{ text: 'JLPT Vocabulary Bundle', link: '/jlpt-vocab-bundle' },
|
||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -220,6 +220,7 @@ button,
|
||||
color: var(--vp-c-brand-1);
|
||||
font-family: var(--tui-font-mono), 'M PLUS 1', 'Noto Sans CJK JP', 'Noto Sans JP',
|
||||
monospace;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
/* === Code blocks === */
|
||||
@@ -229,6 +230,7 @@ button,
|
||||
background: var(--vp-c-bg-alt) !important;
|
||||
font-family: var(--tui-font-mono), 'M PLUS 1', 'Noto Sans CJK JP', 'Noto Sans JP',
|
||||
monospace;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
.vp-doc div[class*='language-']::before {
|
||||
|
||||
@@ -41,28 +41,6 @@ The update flow:
|
||||
3. **Progress check** -- SubMiner fetches your current list entry for the matched media. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
|
||||
4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef action fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef result fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef ext fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
|
||||
Play["Media Plays"]:::step
|
||||
Detect["Episode Detected"]:::action
|
||||
Queue["Update Queue"]:::action
|
||||
Rate["Rate Limiter"]:::enrich
|
||||
GQL["GraphQL Mutation"]:::ext
|
||||
Done["Progress Updated"]:::result
|
||||
|
||||
Play --> Detect
|
||||
Detect --> Queue
|
||||
Queue --> Rate
|
||||
Rate --> GQL
|
||||
GQL --> Done
|
||||
```
|
||||
|
||||
## Update Queue and Retry
|
||||
|
||||
Failed AniList updates are persisted to a retry queue on disk and retried with exponential backoff.
|
||||
|
||||
@@ -250,6 +250,10 @@ The built-in translation request asks for English output by default. Customize t
|
||||
|
||||
SubMiner can create standalone sentence cards (without a word/expression) using a separate note type. This is designed for use with [Lapis](https://github.com/donkuri/Lapis) and similar sentence-focused note types.
|
||||
|
||||
::: warning Required config
|
||||
Sentence card creation and audio card marking both require `ankiConnect.isLapis.enabled: true` and a valid `sentenceCardModel` pointing to your Lapis/Kiku note type. Without this, the `Ctrl/Cmd+S` and `Ctrl/Cmd+Shift+A` shortcuts will not create cards.
|
||||
:::
|
||||
|
||||
```jsonc
|
||||
"ankiConnect": {
|
||||
"isLapis": {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v0.11.1 (2026-04-04)
|
||||
|
||||
- Fixed Linux packaged builds to expose the canonical `SubMiner` app identity to Electron's startup metadata so native Wayland compositors stop reporting the window class/app-id as lowercase `subminer`.
|
||||
- Fixed Linux to restore the runtime options, Jimaku, and Subsync shortcuts after the Electron 39 regression by routing those actions through the overlay's mpv/IPC shortcut path.
|
||||
|
||||
## v0.11.0 (2026-04-03)
|
||||
|
||||
- Added a playlist browser overlay modal for browsing sibling video files and the live mpv queue during playback, with a default `Ctrl+Alt+P` keybinding.
|
||||
- Made mpv plugin installation mandatory in first-run setup (removed skip path); Finish stays disabled until the plugin is installed.
|
||||
- Fixed the Windows `SubMiner mpv` shortcut to launch mpv with required default args directly instead of requiring an `mpv.conf` profile named `subminer`.
|
||||
@@ -17,6 +23,7 @@
|
||||
- Added a dedicated Subtitle Sidebar guide to the docs site with links from homepage and configuration docs.
|
||||
|
||||
## v0.10.0 (2026-03-29)
|
||||
|
||||
- Fixed stats startup so the immersion tracker can run when `Bun.serve` is unavailable.
|
||||
- Added a Node `http` fallback for Electron/runtime paths that do not expose Bun, so stats keeps working there too.
|
||||
- Updated Discord Rich Presence to the maintained `@xhayper/discord-rpc` wrapper.
|
||||
@@ -24,6 +31,7 @@
|
||||
- Restored macOS mpv passthrough while the overlay subtitle sidebar is open so clicks outside the sidebar can refocus mpv and keep native keybindings working.
|
||||
|
||||
## v0.9.3 (2026-03-25)
|
||||
|
||||
- Moved YouTube primary subtitle language defaults to `youtube.primarySubLanguages`.
|
||||
- Removed the placeholder YouTube subtitle retime step; downloaded primary subtitle tracks are now used directly.
|
||||
- Removed the old internal YouTube retime helper and its tests.
|
||||
@@ -31,6 +39,7 @@
|
||||
- Removed the legacy `youtubeSubgen.primarySubLanguages` config path from generated config and docs.
|
||||
|
||||
## v0.9.2 (2026-03-25)
|
||||
|
||||
- Fixed overlay pointer tracking so Windows click-through toggles immediately when the cursor enters or leaves subtitle regions.
|
||||
- Fixed Windows overlay window tracking on scaled displays by converting native tracked window bounds to Electron DIP coordinates.
|
||||
- Fixed Windows direct `--youtube-play` startup so MPV boots reliably, stays paused until the app-owned subtitle flow is ready, and reuses an already-running SubMiner instance.
|
||||
@@ -38,11 +47,13 @@
|
||||
- Fixed `subminer <youtube-url>` on Linux so the YouTube playback flow waits for Yomitan to load before creating the overlay window.
|
||||
|
||||
## v0.9.1 (2026-03-24)
|
||||
|
||||
- Reduced packaged release size by excluding duplicate `extraResources` payload and pruning docs, tests, sourcemaps, and other source-only files from Electron bundles.
|
||||
- Restored controller navigation and lookup/mining controls while the subtitle sidebar is open, while keeping true modal dialogs blocking controller actions.
|
||||
- Fixed subtitle annotation clearing so explanatory contrast endings like `んですけど` are excluded consistently across the shared tokenizer filter and annotation stage.
|
||||
|
||||
## v0.9.0 (2026-03-23)
|
||||
|
||||
- Added an app-owned YouTube subtitle flow with absPlayer-style timedtext parsing that auto-loads the default primary subtitle plus a best-effort secondary at startup and resumes once the primary is ready.
|
||||
- Added a manual YouTube subtitle picker on `Ctrl+Alt+C` so subtitle selection can be retried on demand during active YouTube playback.
|
||||
- Added yt-dlp metadata probing so YouTube playback and immersion tracking record canonical video title and channel metadata.
|
||||
@@ -57,6 +68,7 @@
|
||||
- Reused existing authoritative YouTube subtitle tracks when present, fell back only for missing sides, and kept native mpv secondary subtitle rendering hidden so the overlay remains the visible secondary subtitle surface.
|
||||
|
||||
## v0.8.0 (2026-03-22)
|
||||
|
||||
- Added a configurable subtitle sidebar feature (`subtitleSidebar`) with overlay/embedded rendering, click-to-seek cue list, and hot-reloadable visibility and behavior controls.
|
||||
- Added a rendered sidebar modal with cue list display, click-to-seek, active-cue highlighting, and embedded layout support.
|
||||
- Added sidebar snapshot plumbing between main and renderer for overlay/sidebar synchronization.
|
||||
@@ -68,6 +80,7 @@
|
||||
- Prevented stale subtitle refreshes from regressing active-cue state.
|
||||
|
||||
## v0.7.0 (2026-03-19)
|
||||
|
||||
- Added a full local immersion dashboard release line with Overview, Library, Trends, Vocabulary, and Sessions drill-down views backed by SQLite tracking data.
|
||||
- Added browser-first stats workflows: `subminer stats`, background stats daemon controls (`-b` / `-s`), stats cleanup, and dashboard-side mining actions with media enrichment.
|
||||
- Improved stats accuracy and scale handling with Yomitan token counts, full session timelines, known-word timeline fixes, cross-media vocabulary fixes, and clearer session charts.
|
||||
@@ -77,16 +90,20 @@
|
||||
- Excluded auxiliary-stem `そうだ` grammar tails (MeCab POS3 `助動詞語幹`) from subtitle annotation metadata so frequency, JLPT, and N+1 styling no longer bleed onto grammar-tail tokens.
|
||||
|
||||
## v0.6.5 (2026-03-15)
|
||||
|
||||
- Seeded the AUR checkout with the repo `.SRCINFO` template before rewriting metadata so tagged releases do not depend on prior AUR state.
|
||||
|
||||
## v0.6.4 (2026-03-15)
|
||||
|
||||
- Reworked AUR metadata generation to update `.SRCINFO` directly instead of depending on runner `makepkg`, fixing tagged release publishing for `subminer-bin`.
|
||||
|
||||
## v0.6.3 (2026-03-15)
|
||||
|
||||
- Expanded `Alt+C` into an inline controller config/remap flow with preferred-controller saving and per-action learn mode for buttons, triggers, and stick directions.
|
||||
- Automated `subminer-bin` AUR package updates from the tagged release workflow.
|
||||
|
||||
## v0.6.2 (2026-03-12)
|
||||
|
||||
- Added `yomitan.externalProfilePath` so SubMiner can reuse another Electron app's Yomitan profile in read-only mode.
|
||||
- Reused external Yomitan dictionaries/settings without writing back to that profile.
|
||||
- Let launcher-managed playback honor external Yomitan config instead of forcing first-run setup.
|
||||
@@ -94,6 +111,7 @@
|
||||
- Let first-run setup complete without internal dictionaries while external Yomitan is configured, then require an internal dictionary again only if that external profile is later removed.
|
||||
|
||||
## v0.6.1 (2026-03-12)
|
||||
|
||||
- Added Chrome Gamepad API controller support for keyboard-only overlay mode.
|
||||
- Added configurable controller bindings for lookup, mining, popup navigation, Yomitan audio, mpv pause, and d-pad fallback navigation.
|
||||
- Added smooth, slower popup scrolling for controller navigation.
|
||||
@@ -103,11 +121,13 @@
|
||||
- Added an enforced `verify:config-example` gate so checked-in example config artifacts cannot drift silently.
|
||||
|
||||
## v0.5.6 (2026-03-10)
|
||||
|
||||
- Persisted merged character-dictionary MRU state as soon as a new retained set is built so revisits do not get dropped if later Yomitan import work fails.
|
||||
- Fixed early Electron startup writing config and user data under a lowercase `~/.config/subminer` path instead of canonical `~/.config/SubMiner`.
|
||||
- Kept JLPT underline colors stable during Yomitan hover and selection states, even when tokens also use known, N+1, name-match, or frequency styling.
|
||||
|
||||
## v0.5.1 (2026-03-09)
|
||||
|
||||
- Removed the old YouTube subtitle-generation mode switch; YouTube playback now resolves subtitles before mpv starts.
|
||||
- Hardened YouTube AI subtitle fixing so fenced/text-only responses keep original cue timing.
|
||||
- Skipped AniSkip during URL/YouTube playback where anime metadata cannot be resolved reliably.
|
||||
@@ -116,12 +136,14 @@
|
||||
- Hardened the Windows signing/release workflow with SignPath retry handling for signed `.exe` and `.zip` artifacts.
|
||||
|
||||
## v0.5.0 (2026-03-08)
|
||||
|
||||
- Added the initial packaged Windows release.
|
||||
- Added Windows-native mpv window tracking, launcher/runtime plumbing, and packaged helper assets.
|
||||
- Improved close behavior so ending playback hides the visible overlay while the background app stays running.
|
||||
- Limited the native overlay outline/debug frame to debug mode on Windows.
|
||||
|
||||
## v0.3.0 (2026-03-05)
|
||||
|
||||
- Added keyboard-driven Yomitan navigation and popup controls, including optional auto-pause.
|
||||
- Added subtitle/jump keyboard handling fixes for smoother subtitle playback control.
|
||||
- Improved Anki/Yomitan reliability with stronger Yomitan proxy syncing and safer extension refresh logic.
|
||||
@@ -132,6 +154,7 @@
|
||||
- Removed docs Plausible integration and cleaned associated tracker settings.
|
||||
|
||||
## v0.2.3 (2026-03-02)
|
||||
|
||||
- Added performance and tokenization optimizations (faster warmup, persistent MeCab usage, reduced enrichment lookups).
|
||||
- Added subtitle controls for no-jump delay shifts.
|
||||
- Improved subtitle highlight logic with priority and reliability fixes.
|
||||
@@ -140,30 +163,36 @@
|
||||
- Updated startup flow to load dictionaries asynchronously and unblock first tokenization sooner.
|
||||
|
||||
## v0.2.2 (2026-03-01)
|
||||
|
||||
- Improved subtitle highlighting reliability for frequency modes.
|
||||
- Fixed Jellyfin misc info formatting cleanup.
|
||||
- Version bump maintenance for 0.2.2.
|
||||
|
||||
## v0.2.1 (2026-03-01)
|
||||
|
||||
- Delivered Jellyfin and Subsync fixes from release patch cycle.
|
||||
- Version bump maintenance for 0.2.1.
|
||||
|
||||
## v0.2.0 (2026-03-01)
|
||||
|
||||
- Added task-related release work for the overlay 2.0 cycle.
|
||||
- Introduced Overlay 2.0.
|
||||
- Improved release automation reliability.
|
||||
|
||||
## v0.1.2 (2026-02-24)
|
||||
|
||||
- Added encrypted AniList token handling and default GNOME keyring support.
|
||||
- Added launcher passthrough for password-store flows (Jellyfin path).
|
||||
- Updated docs for auth and integration behavior.
|
||||
- Version bump maintenance for 0.1.2.
|
||||
|
||||
## v0.1.1 (2026-02-23)
|
||||
|
||||
- Fixed overlay modal focus handling (`grab input`) behavior.
|
||||
- Version bump maintenance for 0.1.1.
|
||||
|
||||
## v0.1.0 (2026-02-23)
|
||||
|
||||
- Bootstrapped Electron runtime, services, and composition model.
|
||||
- Added runtime asset packaging and dependency vendoring.
|
||||
- Added project docs baseline, setup guides, architecture notes, and submodule/runtime assets.
|
||||
|
||||
@@ -4,23 +4,6 @@ SubMiner can build a Yomitan-compatible character dictionary from AniList metada
|
||||
|
||||
The dictionary is generated per-media, merged across your recently-watched titles, and auto-imported into Yomitan. When a character name appears in a subtitle line, it gets highlighted and becomes available for hover-driven Yomitan profile lookup.
|
||||
|
||||
## Stats Dashboard
|
||||
|
||||
The character dictionary and stats dashboard both read from the same local immersion data.
|
||||
|
||||
- Open the dashboard from overlay: press your configured `stats.toggleKey` (default: `` ` `` / `Backquote`).
|
||||
- Open from launcher/CLI: run `subminer stats`.
|
||||
- Open directly: visit `http://127.0.0.1:<stats.serverPort>` when the local server is running.
|
||||
|
||||
Useful config keys:
|
||||
|
||||
- `stats.autoStartServer` — start the local stats server automatically once immersion tracking starts.
|
||||
- `stats.serverPort` — local HTTP port for dashboard and API.
|
||||
- `stats.toggleKey` — key binding for overlay dashboard toggle.
|
||||
- `stats.autoOpenBrowser` — auto-open dashboard browser for `subminer stats`.
|
||||
|
||||
The dashboard gives quick visibility into episode summaries, watch-time rollups, session timelines, and vocabulary/kanji drill-down from the same DB used by character matching.
|
||||
|
||||
## How It Works
|
||||
|
||||
The feature has three stages: **snapshot**, **merge**, and **match**.
|
||||
@@ -31,30 +14,6 @@ The feature has three stages: **snapshot**, **merge**, and **match**.
|
||||
|
||||
3. **Match** — During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. Tokens that match a character entry are flagged with `isNameMatch` and highlighted in the overlay with a distinct color.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef api fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef store fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef build fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef dict fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef render fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
|
||||
AL["AniList API"]:::api
|
||||
Snap["Snapshot JSON"]:::store
|
||||
Merge["Merge"]:::build
|
||||
ZIP["Yomitan ZIP"]:::dict
|
||||
Yomi["Yomitan Import"]:::dict
|
||||
Sub["Subtitle Scan"]:::render
|
||||
HL["Name Highlight"]:::render
|
||||
|
||||
AL -->|"GraphQL"| Snap
|
||||
Snap --> Merge
|
||||
Merge --> ZIP
|
||||
ZIP --> Yomi
|
||||
Yomi --> Sub
|
||||
Sub --> HL
|
||||
```
|
||||
|
||||
## Enabling the Feature
|
||||
|
||||
Character dictionary sync is disabled by default. To turn it on:
|
||||
|
||||
@@ -324,8 +324,6 @@ See `config.example.jsonc` for detailed configuration options.
|
||||
| `jlptColors` | object | JLPT level underline colors object (`N1`..`N5`) |
|
||||
| `secondary` | object | Override any of the above for secondary subtitles (optional) |
|
||||
|
||||
JLPT underlining is powered by offline term-meta bank files at runtime. See [`docs/jlpt-vocab-bundle.md`](jlpt-vocab-bundle.md) for required files, source/version refresh steps, and deterministic fallback behavior.
|
||||
|
||||
Frequency dictionary highlighting uses the same dictionary file format as JLPT bundle lookups (`term_meta_bank_*.json` under discovered dictionary directories). A token is highlighted when it has a positive integer `frequencyRank` (lower is more common) and the rank is within `topX`.
|
||||
|
||||
Lookup behavior:
|
||||
|
||||
@@ -1,39 +1,114 @@
|
||||
# Installation
|
||||
|
||||
## How the Pieces Fit Together
|
||||
|
||||
SubMiner is an overlay that sits on top of mpv. It connects to mpv through an IPC socket, renders subtitles as interactive text using a bundled Yomitan dictionary engine, and optionally creates Anki flashcards via AnkiConnect.
|
||||
|
||||
To get a working setup you need:
|
||||
|
||||
1. **mpv** launched with an IPC socket so SubMiner can read subtitle data
|
||||
2. **SubMiner** (the Electron overlay app)
|
||||
3. **Dictionaries** imported into the bundled Yomitan instance (lookups won't work without at least one)
|
||||
4. **Anki + AnkiConnect** _(optional but recommended)_ for card creation and enrichment
|
||||
|
||||
The `subminer` launcher script handles step 1 automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or the equivalent named pipe on Windows) — without it the overlay will start but subtitles will never appear.
|
||||
|
||||
## Requirements
|
||||
|
||||
### System Dependencies
|
||||
|
||||
| Dependency | Required | Notes |
|
||||
| -------------------- | ---------- | -------------------------------------------------------- |
|
||||
| Bun | Yes | Required for `subminer` wrapper and source workflows |
|
||||
| mpv | Yes | Must support IPC sockets (`--input-ipc-server`) |
|
||||
| ffmpeg | For media | Audio extraction and screenshot generation |
|
||||
| MeCab + mecab-ipadic | No | Optional Japanese metadata enrichment (not the primary tokenizer) |
|
||||
| fuse2 | Linux only | Required for AppImage |
|
||||
| yt-dlp | No | Recommended for YouTube playback and subtitle extraction |
|
||||
| Dependency | Required | Notes |
|
||||
| -------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| mpv | Yes | Must support IPC sockets (`--input-ipc-server`) |
|
||||
| Bun | For wrapper | Required for `subminer` CLI wrapper and source builds. Pre-built releases (AppImage, DMG, installer) work without it — only the `subminer` wrapper script needs Bun on `PATH`. |
|
||||
| ffmpeg | Recommended | Audio extraction and screenshot generation. Without it SubMiner still runs, but audio and image fields on Anki cards will be empty. |
|
||||
| MeCab + mecab-ipadic | No | Adds part-of-speech data used to filter particles out of N+1, JLPT, and frequency annotations. Without it annotations still render, but POS-based filtering is less precise. |
|
||||
| fuse2 | Linux only | Required for AppImage |
|
||||
| yt-dlp | No | Recommended for YouTube playback and subtitle extraction |
|
||||
|
||||
### Platform-Specific
|
||||
|
||||
**Linux** — one of the following compositors:
|
||||
**Linux** — one of the following window backends:
|
||||
|
||||
- Hyprland (uses `hyprctl`)
|
||||
- Sway (uses `swaymsg`)
|
||||
- X11 (uses `xdotool` and `xwininfo`)
|
||||
- **Hyprland** — native Wayland support (uses `hyprctl`)
|
||||
- **Sway** — native Wayland support (uses `swaymsg`)
|
||||
- **X11 / Xwayland** — for X11 sessions or any other Wayland compositor (uses `xdotool` and `xwininfo`)
|
||||
|
||||
::: warning Wayland support is compositor-specific
|
||||
Wayland has no universal API for window positioning — each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Only Hyprland and Sway have native Wayland backends. If you run a different Wayland compositor (GNOME, KDE Plasma, river, etc.), both mpv **and** SubMiner must run under X11 or Xwayland. The `subminer` launcher handles this automatically when `--backend x11` is set or the X11 backend is auto-detected.
|
||||
:::
|
||||
|
||||
<details>
|
||||
<summary><b>Arch Linux</b></summary>
|
||||
|
||||
```bash
|
||||
sudo pacman -S --needed mpv ffmpeg
|
||||
# Optional
|
||||
sudo pacman -S --needed mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||
# Optional: subtitle sync (at least one needed for subtitle syncing)
|
||||
paru -S --needed alass python-ffsubsync
|
||||
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
||||
sudo pacman -S --needed xdotool xorg-xwininfo
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Ubuntu / Debian</b></summary>
|
||||
|
||||
```bash
|
||||
sudo apt install mpv ffmpeg
|
||||
# Optional
|
||||
sudo apt install mecab libmecab-dev mecab-ipadic-utf8 fzf rofi chafa ffmpegthumbnailer yt-dlp
|
||||
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
||||
sudo apt install xdotool x11-utils
|
||||
# Optional: subtitle sync
|
||||
pip install ffsubsync
|
||||
# alass is not in apt — install via cargo: cargo install alass-cli
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Fedora</b></summary>
|
||||
|
||||
```bash
|
||||
sudo dnf install mpv ffmpeg
|
||||
# Optional
|
||||
sudo dnf install mecab mecab-ipadic fzf rofi chafa ffmpegthumbnailer yt-dlp
|
||||
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
||||
sudo dnf install xdotool xorg-x11-utils
|
||||
# Optional: subtitle sync
|
||||
pip install ffsubsync
|
||||
# alass: cargo install alass-cli
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**macOS** — macOS 10.13 or later. Accessibility permission required for window tracking.
|
||||
|
||||
**Windows** — Windows 10 or later. Install `mpv`; keep it on `PATH` for auto-discovery or set `mpv.executablePath` in config if `mpv.exe` lives elsewhere. SubMiner's packaged build handles window tracking directly.
|
||||
```bash
|
||||
brew install mpv ffmpeg
|
||||
# Optional but recommended for annotations
|
||||
brew install mecab mecab-ipadic
|
||||
# Optional
|
||||
brew install yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||
# Optional: subtitle sync
|
||||
brew install alass
|
||||
pip install ffsubsync
|
||||
```
|
||||
|
||||
**Windows** — Windows 10 or later. Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on `PATH`. Keep `mpv.exe` on `PATH` for auto-discovery or set `mpv.executablePath` in config if it lives elsewhere. SubMiner's packaged build handles window tracking directly. Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary.
|
||||
|
||||
### Optional Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
| ----------------- | ------------------------------------------------------------- |
|
||||
| fzf | Terminal-based video picker (default) |
|
||||
| rofi | GUI-based video picker |
|
||||
| chafa | Thumbnail previews in fzf |
|
||||
| ffmpegthumbnailer | Generate video thumbnails for picker |
|
||||
| guessit | Better AniSkip title/season/episode parsing for file playback |
|
||||
| Tool | Purpose |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| fzf | Terminal-based video picker (default) |
|
||||
| rofi | GUI-based video picker |
|
||||
| chafa | Thumbnail previews in fzf |
|
||||
| ffmpegthumbnailer | Generate video thumbnails for picker |
|
||||
| guessit | Better AniSkip title/season/episode parsing for file playback |
|
||||
| alass | Subtitle sync engine (preferred) — must be on `PATH` or set `subsync.alass_path` in config; subtitle syncing is disabled without it or ffsubsync |
|
||||
| ffsubsync | Subtitle sync engine (fallback) — must be on `PATH` or set `subsync.ffsubsync_path` in config; subtitle syncing is disabled without it or alass |
|
||||
|
||||
@@ -57,20 +132,31 @@ makepkg -si
|
||||
|
||||
### AppImage (Recommended)
|
||||
|
||||
Download the latest AppImage from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
||||
Download the latest AppImage and the `subminer` launcher from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest).
|
||||
|
||||
**Step 1 — Install Bun** (required for the launcher):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
```
|
||||
|
||||
The `subminer` launcher uses a Bun shebang. The AppImage itself does **not** need Bun — only the launcher does. If you skip the launcher and run the AppImage directly (for example `SubMiner.AppImage --start`), you can skip this step, but you will need to configure `mpv.conf` with `input-ipc-server=/tmp/subminer-socket` manually.
|
||||
|
||||
**Step 2 — Download and install:**
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local/bin
|
||||
|
||||
# Download and install AppImage
|
||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage
|
||||
chmod +x ~/.local/bin/SubMiner.AppImage
|
||||
|
||||
# Download subminer wrapper script
|
||||
# Download and install the subminer launcher (recommended)
|
||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer
|
||||
chmod +x ~/.local/bin/subminer
|
||||
|
||||
```
|
||||
|
||||
The `subminer` wrapper uses a Bun shebang (`#!/usr/bin/env bun`), so [Bun](https://bun.sh) must be installed and available on `PATH`.
|
||||
The `subminer` launcher is the recommended way to use SubMiner on Linux. It ensures mpv is launched with the correct IPC socket and SubMiner defaults so you don't need to configure `mpv.conf` manually.
|
||||
|
||||
### From Source
|
||||
|
||||
@@ -103,9 +189,33 @@ A **ZIP** artifact is also available as a fallback — unzip and drag `SubMiner.
|
||||
Install dependencies using Homebrew:
|
||||
|
||||
```bash
|
||||
brew install mpv mecab mecab-ipadic
|
||||
brew install mpv ffmpeg
|
||||
# Optional but recommended if you use N+1, JLPT, or frequency annotations
|
||||
brew install mecab mecab-ipadic
|
||||
```
|
||||
|
||||
#### Install the `subminer` launcher (recommended)
|
||||
|
||||
The `subminer` launcher is the recommended way to use SubMiner on macOS. It launches mpv with the correct IPC socket and SubMiner defaults so you don't need to set up an `mpv.conf` profile manually.
|
||||
|
||||
Download it from the same [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) page:
|
||||
|
||||
```bash
|
||||
sudo wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O /usr/local/bin/subminer
|
||||
sudo chmod +x /usr/local/bin/subminer
|
||||
```
|
||||
|
||||
Or with curl:
|
||||
|
||||
```bash
|
||||
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer
|
||||
sudo chmod +x /usr/local/bin/subminer
|
||||
```
|
||||
|
||||
::: warning Bun required for the launcher
|
||||
The `subminer` launcher uses a Bun shebang (`#!/usr/bin/env bun`), so [Bun](https://bun.sh) must be installed and available on `PATH`. Install Bun if you haven't already: `curl -fsSL https://bun.sh/install | bash`.
|
||||
:::
|
||||
|
||||
### From Source (macOS)
|
||||
|
||||
```bash
|
||||
@@ -123,19 +233,44 @@ For unsigned local builds:
|
||||
bun run build:mac:unsigned
|
||||
```
|
||||
|
||||
Build and install the launcher alongside the app:
|
||||
|
||||
```bash
|
||||
make install-macos
|
||||
```
|
||||
|
||||
This builds the `subminer` launcher into `dist/launcher/subminer` and installs it to `~/.local/bin/subminer` along with the app bundle and rofi theme. To install to `/usr/local/bin` instead (already on the default macOS `PATH`):
|
||||
|
||||
```bash
|
||||
sudo make install-macos PREFIX=/usr/local
|
||||
```
|
||||
|
||||
### Gatekeeper
|
||||
|
||||
If macOS blocks SubMiner on first launch, right-click the app and select **Open** to bypass the warning. Alternatively, remove the quarantine attribute:
|
||||
|
||||
```bash
|
||||
xattr -d com.apple.quarantine /Applications/SubMiner.app
|
||||
```
|
||||
|
||||
### Accessibility Permission
|
||||
|
||||
After launching SubMiner for the first time, grant accessibility permission:
|
||||
|
||||
1. Open **System Preferences** → **Security & Privacy** → **Privacy** tab
|
||||
2. Select **Accessibility** from the left sidebar
|
||||
3. Add SubMiner to the list
|
||||
1. Open **System Settings** → **Privacy & Security** → **Accessibility**
|
||||
2. Enable SubMiner in the list (add it if it does not appear)
|
||||
|
||||
Without this permission, window tracking will not work and the overlay won't follow the mpv window.
|
||||
|
||||
### macOS Usage Notes
|
||||
|
||||
**Launching MPV with IPC:**
|
||||
**Launching with the `subminer` launcher (recommended):**
|
||||
|
||||
```bash
|
||||
subminer video.mkv
|
||||
```
|
||||
|
||||
The launcher handles the IPC socket and SubMiner defaults automatically. If you prefer to launch mpv manually:
|
||||
|
||||
```bash
|
||||
mpv --input-ipc-server=/tmp/subminer-socket video.mkv
|
||||
@@ -160,6 +295,17 @@ binary_path=/Applications/SubMiner.app/Contents/MacOS/subminer
|
||||
|
||||
## Windows
|
||||
|
||||
> [!WARNING]
|
||||
> **Windows support is experimental.** Core features — mining, annotations, and dictionary lookups — work, but some functionality may be missing or unstable. Bug reports welcome.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Install [`mpv`](https://mpv.io/installation/) and ensure `mpv.exe` is on `PATH`. If mpv is installed elsewhere, you can set `mpv.executablePath` in `config.jsonc` or use the first-run setup field to point at the executable.
|
||||
2. Install [`ffmpeg`](https://ffmpeg.org/download.html) and add it to `PATH` — recommended for audio/screenshot extraction (without it, media fields on Anki cards will be empty).
|
||||
3. _(Optional)_ Install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for annotation POS filtering.
|
||||
|
||||
No compositor tools or window helpers are needed — native window tracking is built in on Windows.
|
||||
|
||||
### Installer (Recommended)
|
||||
|
||||
Download the latest Windows installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
||||
@@ -167,16 +313,24 @@ Download the latest Windows installer from [GitHub Releases](https://github.com/
|
||||
- `SubMiner-<version>.exe` installs the app, Start menu shortcut, and default files under `Program Files`
|
||||
- `SubMiner-<version>.zip` is available as a portable fallback
|
||||
|
||||
Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still required for media extraction, and MeCab remains optional.
|
||||
### Getting Started on Windows
|
||||
|
||||
### Windows Usage Notes
|
||||
1. **Run `SubMiner.exe` once** — first-run setup creates `%APPDATA%\SubMiner\config.jsonc`, installs the mpv plugin, and opens Yomitan settings for dictionary import.
|
||||
2. **Create the SubMiner mpv shortcut** _(recommended)_ — the setup popup offers to create a `SubMiner mpv` Start Menu and/or Desktop shortcut. This is the recommended way to launch playback on Windows.
|
||||
3. **Play a video** — double-click the shortcut, drag a video file onto it, or run from a terminal:
|
||||
|
||||
- Launch `SubMiner.exe` once to let the first-run setup flow seed `%APPDATA%\\SubMiner\\config.jsonc`, require mpv plugin installation, and open bundled Yomitan settings. The optional `SubMiner mpv` Start Menu/Desktop shortcut can also be created during setup, and on Windows it is the recommended way to launch mpv playback with SubMiner defaults.
|
||||
- If `mpv.exe` is not on `PATH`, set `mpv.executablePath` in `config.jsonc` or use the first-run setup field to point at the executable. Leave it blank to keep PATH auto-discovery.
|
||||
- `SubMiner.exe --launch-mpv` and the optional `SubMiner mpv` shortcut pass SubMiner's default mpv socket/subtitle args directly and do not require an `mpv.conf` profile named `subminer`.
|
||||
- First-run mpv plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is installed in a non-standard location.
|
||||
- Windows plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket`; do not keep `/tmp/subminer-socket` on Windows.
|
||||
- Native window tracking is built in on Windows; no `xdotool`, `xwininfo`, or compositor-specific helper is required.
|
||||
```powershell
|
||||
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
|
||||
```
|
||||
|
||||
The shortcut and `--launch-mpv` pass SubMiner's default IPC socket and subtitle args directly — no `mpv.conf` profile is needed.
|
||||
|
||||
### Windows-Specific Notes
|
||||
|
||||
- The `subminer` launcher script requires [Bun](https://bun.sh) and must be invoked with `bun run subminer` on Windows since the shebang is not supported. The **SubMiner mpv** shortcut or `SubMiner.exe --launch-mpv` is the simpler alternative.
|
||||
- First-run plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is in a non-standard location.
|
||||
- Plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket` — do not keep `/tmp/subminer-socket` on Windows.
|
||||
- Config is stored at `%APPDATA%\SubMiner\config.jsonc`.
|
||||
|
||||
### From Source (Windows)
|
||||
|
||||
@@ -184,10 +338,14 @@ Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still re
|
||||
git clone https://github.com/ksyasuda/SubMiner.git
|
||||
cd SubMiner
|
||||
bun install
|
||||
|
||||
# Windows requires building the texthooker-ui submodule manually before
|
||||
# the main build (Linux/macOS handle this automatically during `bun run build`).
|
||||
Set-Location vendor/texthooker-ui
|
||||
bun install --frozen-lockfile
|
||||
bun run build
|
||||
Set-Location ../..
|
||||
|
||||
bun run build:win
|
||||
```
|
||||
|
||||
@@ -220,34 +378,55 @@ cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
||||
# make install-plugin
|
||||
```
|
||||
|
||||
## Rofi Theme (Linux Only)
|
||||
|
||||
SubMiner ships a default rofi theme at `assets/themes/subminer.rasi`.
|
||||
|
||||
Install path (default auto-detected by `subminer`):
|
||||
|
||||
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
|
||||
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local/share/SubMiner/themes
|
||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
||||
```
|
||||
|
||||
Override with `SUBMINER_ROFI_THEME=/absolute/path/to/theme.rasi`.
|
||||
|
||||
See [MPV Plugin](/mpv-plugin) for the full configuration reference, keybindings, script messages, and binary auto-detection details.
|
||||
|
||||
## Verify Installation
|
||||
## Anki Setup (Recommended)
|
||||
|
||||
After installing, confirm SubMiner is working:
|
||||
If you plan to mine Anki cards (the primary use case for most users):
|
||||
|
||||
On Windows, replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below.
|
||||
1. Install [Anki](https://apps.ankiweb.net/).
|
||||
2. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on — open Anki, go to **Tools → Add-ons → Get Add-ons**, enter code `2055492159`.
|
||||
3. Restart Anki and keep it running while using SubMiner.
|
||||
|
||||
AnkiConnect listens on `http://127.0.0.1:8765` by default. SubMiner will connect to it automatically with no extra config needed for basic card creation.
|
||||
|
||||
For enrichment configuration (sentence, audio, screenshot fields), see [Anki Integration](/anki-integration).
|
||||
|
||||
## First-Run Setup
|
||||
|
||||
Run the setup wizard to create a default config and finish initial configuration. You do **not** need to create the config manually — SubMiner handles it.
|
||||
|
||||
```bash
|
||||
# Play a file (default plugin config auto-starts visible overlay and waits for annotation readiness; first launch may open first-run setup popup)
|
||||
subminer video.mkv
|
||||
subminer app --setup
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
|
||||
|
||||
The setup popup walks you through:
|
||||
|
||||
- **Config file**: auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
|
||||
- **mpv plugin**: install the bundled Lua plugin for in-player keybindings
|
||||
- **Yomitan dictionaries**: import at least one dictionary so lookups work
|
||||
- **Windows shortcut** _(Windows only)_: optionally create a `SubMiner mpv` Start Menu/Desktop shortcut
|
||||
|
||||
The `Finish setup` button stays disabled until the plugin is installed and at least one dictionary is imported. Once you finish, SubMiner will not show the popup again.
|
||||
|
||||
> [!TIP]
|
||||
> You can re-open the setup popup at any time with `subminer app --setup` or `SubMiner.AppImage --setup`.
|
||||
|
||||
Once setup is complete, play a video to verify everything works:
|
||||
|
||||
```bash
|
||||
subminer video.mkv
|
||||
```
|
||||
|
||||
You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay.
|
||||
|
||||
<details>
|
||||
<summary><b>More launch examples</b></summary>
|
||||
|
||||
```bash
|
||||
# Optional explicit overlay start for setups with plugin auto_start=no
|
||||
subminer --start video.mkv
|
||||
|
||||
@@ -262,6 +441,39 @@ SubMiner.AppImage --start --dev
|
||||
SubMiner.AppImage --help # Show all CLI options
|
||||
```
|
||||
|
||||
You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay.
|
||||
</details>
|
||||
|
||||
## Verify Setup
|
||||
|
||||
After completing first-run setup, run the built-in diagnostic to confirm everything is in place:
|
||||
|
||||
```bash
|
||||
subminer doctor
|
||||
```
|
||||
|
||||
This checks for the app binary, mpv, ffmpeg, config file, and socket path. Fix any failures before continuing.
|
||||
|
||||
> [!NOTE]
|
||||
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly. Replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below.
|
||||
|
||||
## Optional Extras
|
||||
|
||||
### Rofi Theme (Linux Only)
|
||||
|
||||
SubMiner ships a custom rofi theme bundled in the release assets tarball.
|
||||
|
||||
Install path (default auto-detected by `subminer`):
|
||||
|
||||
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
|
||||
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
|
||||
|
||||
```bash
|
||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
||||
mkdir -p ~/.local/share/SubMiner/themes
|
||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
||||
```
|
||||
|
||||
Override with `SUBMINER_ROFI_THEME=/absolute/path/to/theme.rasi`.
|
||||
|
||||
Next: [Usage](/usage) — learn about the `subminer` wrapper, keybindings, and YouTube playback.
|
||||
|
||||
@@ -26,31 +26,6 @@ If no files match the current episode filter, a "Show all files" button lets you
|
||||
| `Arrow Up` / `Arrow Down` | Navigate entries or files |
|
||||
| `Escape` | Close modal |
|
||||
|
||||
### Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef action fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef result fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
|
||||
Open["Open Jimaku modal (Ctrl+Shift+J)"]:::step
|
||||
Parse["Auto-fill title, season, episode from filename"]:::enrich
|
||||
Search["Search Jimaku API"]:::action
|
||||
Entries["Browse matching entries"]:::action
|
||||
Files["Browse subtitle files"]:::action
|
||||
Download["Download selected file"]:::action
|
||||
Load["Load subtitle into mpv"]:::result
|
||||
|
||||
Open --> Parse
|
||||
Parse --> Search
|
||||
Search --> Entries
|
||||
Entries --> Files
|
||||
Files --> Download
|
||||
Download --> Load
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Add a `jimaku` section to your `config.jsonc`:
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# JLPT Vocabulary Bundle (Offline)
|
||||
|
||||
## Bundle location
|
||||
|
||||
SubMiner expects the JLPT term-meta bank files to be available locally at:
|
||||
|
||||
- `vendor/yomitan-jlpt-vocab`
|
||||
|
||||
At runtime, SubMiner also searches these derived locations:
|
||||
|
||||
- `vendor/yomitan-jlpt-vocab`
|
||||
- `vendor/yomitan-jlpt-vocab/vendor/yomitan-jlpt-vocab`
|
||||
- `vendor/yomitan-jlpt-vocab/yomitan-jlpt-vocab`
|
||||
|
||||
and user-data/config fallback paths (see `getJlptDictionarySearchPaths` in `src/main.ts`).
|
||||
|
||||
## Required files
|
||||
|
||||
The expected files are:
|
||||
|
||||
- `term_meta_bank_1.json`
|
||||
- `term_meta_bank_2.json`
|
||||
- `term_meta_bank_3.json`
|
||||
- `term_meta_bank_4.json`
|
||||
- `term_meta_bank_5.json`
|
||||
|
||||
Each bank maps terms to frequency metadata; only entries with a `frequency.displayValue` are considered for JLPT tagging.
|
||||
|
||||
SubMiner also reuses the same `term_meta_bank_*.json` format for frequency-based subtitle highlighting, using installed/default `frequency-dictionary` locations or an explicit `subtitleStyle.frequencyDictionary.sourcePath`.
|
||||
|
||||
## Source and update process
|
||||
|
||||
For reproducible updates:
|
||||
|
||||
1. Obtain the JLPT term-meta bank archive from the same upstream source that supplies the bundled Yomitan dictionary data.
|
||||
2. Extract the five `term_meta_bank_*.json` files.
|
||||
3. Place them into `vendor/yomitan-jlpt-vocab/`.
|
||||
4. Commit the update with the source URL/version in the task notes.
|
||||
|
||||
This repository currently ships the folder path in `electron-builder` `extraResources` as:
|
||||
`vendor/yomitan-jlpt-vocab -> yomitan-jlpt-vocab`.
|
||||
|
||||
## Fallback Behavior
|
||||
|
||||
If bank files are missing, malformed, or lack expected metadata, SubMiner skips them gracefully. When no usable entries are found, JLPT underlining is silently disabled and subtitle rendering remains unchanged.
|
||||
@@ -1,6 +1,10 @@
|
||||
# Launcher Script
|
||||
|
||||
The `subminer` wrapper script is an all-in-one launcher that handles video selection, mpv startup, and overlay management. It's a Bun script distributed alongside the AppImage.
|
||||
The `subminer` launcher is an all-in-one script that handles video selection, mpv startup, and overlay management. It is the recommended way to use SubMiner on Linux and macOS because it guarantees mpv is launched with the correct IPC socket and SubMiner defaults. It's a Bun script distributed as a release asset alongside the AppImage and DMG.
|
||||
|
||||
::: tip Windows users
|
||||
On Windows the `subminer` script cannot run directly via shebang — use `bun run subminer` instead (e.g. `bun run subminer video.mkv`). The recommended alternative is the **SubMiner mpv** shortcut created during first-run setup, or `SubMiner.exe --launch-mpv`. See [Windows mpv Shortcut](/usage#windows-mpv-shortcut) for details.
|
||||
:::
|
||||
|
||||
## Video Picker
|
||||
|
||||
@@ -9,9 +13,9 @@ When you run `subminer` without specifying a file, it opens an interactive video
|
||||
### fzf (default)
|
||||
|
||||
```bash
|
||||
subminer # pick from current directory
|
||||
subminer -d ~/Videos # pick from a specific directory
|
||||
subminer -r -d ~/Anime # recursive search
|
||||
subminer # pick from current directory
|
||||
subminer -d ~/Videos # pick from a specific directory
|
||||
subminer -r -d ~/Anime # recursive search
|
||||
```
|
||||
|
||||
fzf shows video files in a fuzzy-searchable list. If `chafa` is installed, you get thumbnail previews in the right pane. Thumbnails are sourced from the freedesktop thumbnail cache first, then generated on the fly with `ffmpegthumbnailer` or `ffmpeg` as fallback.
|
||||
@@ -24,14 +28,17 @@ fzf shows video files in a fuzzy-searchable list. If `chafa` is installed, you g
|
||||
### rofi
|
||||
|
||||
```bash
|
||||
subminer -R # rofi picker, current directory
|
||||
subminer -R -d ~/Videos # rofi picker, specific directory
|
||||
subminer -R -r -d ~/Anime # rofi picker, recursive
|
||||
subminer -R # rofi picker, current directory
|
||||
subminer -R -d ~/Videos # rofi picker, specific directory
|
||||
subminer -R -r -d ~/Anime # rofi picker, recursive
|
||||
subminer -R /directory # rofi picker, directory shortcut
|
||||
```
|
||||
|
||||
rofi shows a GUI menu with icon thumbnails when available. SubMiner ships a custom rofi theme that can be installed from the release assets:
|
||||
rofi shows a GUI menu with icon thumbnails when available. SubMiner ships a custom rofi theme bundled in the release assets tarball:
|
||||
|
||||
```bash
|
||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
||||
mkdir -p ~/.local/share/SubMiner/themes
|
||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
||||
```
|
||||
@@ -53,15 +60,11 @@ SUBMINER_ROFI_THEME=/path/to/custom-theme.rasi subminer -R
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
subminer video.mkv # play a specific file (default plugin config auto-starts visible overlay)
|
||||
subminer --start video.mkv # optional explicit overlay start when plugin auto_start=no
|
||||
subminer -S video.mkv # same as above via --start-overlay
|
||||
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
|
||||
subminer ytsearch:"jp news" # YouTube search
|
||||
subminer stats # open immersion dashboard
|
||||
subminer stats -b # start background stats daemon
|
||||
subminer stats -s # stop background stats daemon
|
||||
subminer --setup # Open first-run setup popup
|
||||
subminer video.mkv # play a specific file (default plugin config auto-starts visible overlay)
|
||||
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
|
||||
subminer --backend x11 video.mkv # Force x11 backend for a specific file
|
||||
subminer stats # open immersion dashboard
|
||||
subminer stats -b # start background stats daemon
|
||||
```
|
||||
|
||||
## Subcommands
|
||||
@@ -71,7 +74,6 @@ subminer --setup # Open first-run setup popup
|
||||
| `subminer jellyfin` / `jf` | Jellyfin workflows (`-d` discovery, `-p` play, `-l` login) |
|
||||
| `subminer stats` | Start stats server and open immersion dashboard in browser |
|
||||
| `subminer stats -b` | Start or reuse background stats daemon (non-blocking) |
|
||||
| `subminer stats -s` | Stop the background stats daemon |
|
||||
| `subminer stats cleanup` | Backfill vocabulary metadata and prune stale rows |
|
||||
| `subminer doctor` | Dependency + config + socket diagnostics |
|
||||
| `subminer config path` | Print active config file path |
|
||||
@@ -96,7 +98,7 @@ Use `subminer <subcommand> -h` for command-specific help.
|
||||
| `--start` | Explicitly start overlay after mpv launches |
|
||||
| `-S, --start-overlay` | Explicitly start overlay after mpv launches |
|
||||
| `-T, --no-texthooker` | Disable texthooker server |
|
||||
| `-p, --profile` | mpv profile name (default: `subminer`) |
|
||||
| `-p, --profile` | mpv profile name (no default; omitted unless set) |
|
||||
| `-a, --args` | Pass additional mpv arguments as a quoted string |
|
||||
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`, `macos`, `windows`) |
|
||||
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) |
|
||||
|
||||
@@ -6,30 +6,6 @@ This guide walks through the sentence mining loop — from watching a video to c
|
||||
|
||||
SubMiner runs as a transparent overlay on top of mpv. As subtitles play, the overlay displays them as interactive text. You hover a word, trigger Yomitan lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, audio clip, and screenshot.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef action fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef result fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||
|
||||
Watch["Watch Video"]:::step
|
||||
Sub["Subtitle Appears"]:::step
|
||||
Hover["Hover Word"]:::action
|
||||
Lookup["Trigger Lookup"]:::action
|
||||
Yomi["Yomitan Popup"]:::result
|
||||
Add["Add to Anki"]:::result
|
||||
|
||||
Watch --> Sub --> Hover --> Lookup --> Yomi --> Add
|
||||
|
||||
Add --> Enrich["SubMiner Enriches"]:::enrich
|
||||
|
||||
Enrich --> S["Sentence"]:::enrich
|
||||
Enrich --> A["Audio Clip"]:::enrich
|
||||
Enrich --> I["Screenshot"]:::enrich
|
||||
Enrich --> T["Translation"]:::enrich
|
||||
```
|
||||
|
||||
## Subtitle Delivery Path (Startup + Runtime)
|
||||
|
||||
SubMiner prioritizes subtitle responsiveness over heavy initialization:
|
||||
@@ -141,10 +117,18 @@ Create a standalone sentence card without going through Yomitan:
|
||||
|
||||
The sentence card uses the note type configured in `isLapis.sentenceCardModel` and always maps sentence/audio to `Sentence` and `SentenceAudio`.
|
||||
|
||||
::: warning Requires Lapis/Kiku note type
|
||||
Sentence card creation requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration — Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
|
||||
:::
|
||||
|
||||
### 4. Mark as Audio Card
|
||||
|
||||
After adding a word via Yomitan, press the audio card shortcut to overwrite the audio with a longer clip spanning the full subtitle timing.
|
||||
|
||||
::: warning Requires Lapis/Kiku note type
|
||||
Audio card marking requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration — Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
|
||||
:::
|
||||
|
||||
## Secondary Subtitles
|
||||
|
||||
SubMiner can display a secondary subtitle track (typically English) alongside the primary Japanese subtitles. This is useful for:
|
||||
|
||||
@@ -183,6 +183,10 @@ When `backend=auto`, the plugin detects the window manager:
|
||||
4. **X11** — detected via `XDG_SESSION_TYPE=x11` or `DISPLAY`.
|
||||
5. **Fallback** — defaults to X11 with a warning.
|
||||
|
||||
::: tip Wayland is compositor-specific
|
||||
Native Wayland support is only available for Hyprland and Sway. If you use a different Wayland compositor, auto-detection will fall back to X11 — both mpv and SubMiner must be running under Xwayland, and `xdotool` and `xwininfo` must be installed.
|
||||
:::
|
||||
|
||||
## Script Messages
|
||||
|
||||
The plugin can be controlled from other mpv scripts or the mpv command line using script messages:
|
||||
|
||||
BIN
docs-site/public/screenshots/playlist-browser.png
Normal file
BIN
docs-site/public/screenshots/playlist-browser.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 746 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 96 KiB |
@@ -113,10 +113,6 @@ All colors are customizable via the `subtitleStyle.jlptColors` object.
|
||||
| `subtitleStyle.enableJlpt` | `false` | Enable JLPT underline styling |
|
||||
| `subtitleStyle.jlptColors.N1`–`N5` | see above | Per-level underline colors |
|
||||
|
||||
::: tip
|
||||
JLPT tagging requires the offline vocabulary bundle. See [JLPT Vocabulary Bundle](jlpt-vocab-bundle) for setup instructions and file locations.
|
||||
:::
|
||||
|
||||
## Runtime Toggles
|
||||
|
||||
All annotation layers can be toggled at runtime via the mpv command menu without restarting:
|
||||
|
||||
@@ -60,8 +60,6 @@ Enable and configure the sidebar under `subtitleSidebar` in your config file:
|
||||
| `activeLineBackgroundColor` | string | — | Active cue background color |
|
||||
| `hoverLineBackgroundColor` | string | — | Hovered cue background color |
|
||||
|
||||
Default colors use Catppuccin Macchiato with a semi-transparent shell so the panel stays readable without feeling like a solid overlay.
|
||||
|
||||
## Keyboard Shortcut
|
||||
|
||||
| Key | Action | Config key |
|
||||
|
||||
@@ -136,7 +136,7 @@ Shown when SubMiner tries to update a card that no longer exists, or when AnkiCo
|
||||
**Overlay does not appear**
|
||||
|
||||
- Confirm SubMiner is running: `SubMiner.AppImage --start` or check for the process.
|
||||
- On Linux, the overlay requires a compositor. Hyprland and Sway are supported natively; X11 requires `xdotool` and `xwininfo`.
|
||||
- On Linux, the overlay requires a supported window backend. Hyprland and Sway have native Wayland support; all other compositors require both mpv and SubMiner to run under X11 or Xwayland (`xdotool` and `xwininfo` must be installed).
|
||||
- On macOS, grant Accessibility permission to SubMiner in System Settings > Privacy & Security > Accessibility.
|
||||
|
||||
**Overlay appears but clicks pass through / cannot interact**
|
||||
@@ -232,7 +232,7 @@ Global shortcuts (`Alt+Shift+O`, `Alt+Shift+Y`) may conflict with other applicat
|
||||
|
||||
- Check your DE/WM keybinding settings for conflicts.
|
||||
- Change the shortcut in your config under `shortcuts.toggleVisibleOverlayGlobal`.
|
||||
- On Wayland, global shortcut registration has limitations depending on the compositor.
|
||||
- On Wayland, global shortcut registration has limitations depending on the compositor. Only Hyprland and Sway are supported natively — see the [Hyprland](#hyprland) section below for shortcut passthrough rules. Other Wayland compositors require X11/Xwayland.
|
||||
|
||||
**Overlay keybindings not working**
|
||||
|
||||
@@ -289,8 +289,8 @@ The Jimaku API has rate limits. If you see 429 errors, wait for the retry durati
|
||||
|
||||
### Linux
|
||||
|
||||
- **Wayland (Hyprland/Sway)**: Window tracking uses compositor-specific commands. If `hyprctl` or `swaymsg` are not on `PATH`, tracking will fail silently.
|
||||
- **X11**: Requires `xdotool` and `xwininfo`. If missing, the overlay cannot track the mpv window position.
|
||||
- **Wayland (Hyprland/Sway only)**: Native Wayland support is limited to Hyprland and Sway. Window tracking uses compositor-specific commands (`hyprctl` / `swaymsg`). If these are not on `PATH`, tracking will fail silently. Other Wayland compositors are not supported — both mpv and SubMiner must run under X11 or Xwayland instead.
|
||||
- **X11 / Xwayland**: Requires `xdotool` and `xwininfo`. If missing, the overlay cannot track the mpv window position. This is the required backend for any Wayland compositor other than Hyprland or Sway — both mpv and SubMiner must be running under X11/Xwayland for window tracking to work.
|
||||
- **Mouse passthrough**: On Linux, Electron's mouse passthrough is unreliable. SubMiner keeps pointer events enabled, meaning you may need to toggle the overlay off to interact with mpv controls underneath.
|
||||
|
||||
### Hyprland
|
||||
@@ -329,5 +329,4 @@ For more details, see the Hyprland docs on [global keybinds](https://wiki.hypr.l
|
||||
### macOS
|
||||
|
||||
- **Accessibility permission**: Required for window tracking. Grant it in System Settings > Privacy & Security > Accessibility.
|
||||
- **Font rendering**: macOS uses a 0.87x font compensation factor for subtitle alignment between mpv and the overlay. If text alignment looks off, adjust subtitle offset by right-click dragging subtitle text.
|
||||
- **Gatekeeper**: If macOS blocks SubMiner, right-click the app and select "Open" to bypass the warning, or remove the quarantine attribute: `xattr -d com.apple.quarantine /path/to/SubMiner.app`
|
||||
|
||||
@@ -4,6 +4,26 @@
|
||||
> SubMiner requires the bundled Yomitan instance to have at least one dictionary imported for lookups to work.
|
||||
> See [Yomitan setup](#yomitan-setup) for details.
|
||||
|
||||
::: tip Just finished first-run setup?
|
||||
If you want Anki card enrichment (sentence, audio, screenshot), the only config you need is `ankiConnect` with your deck name and field names. Here is a minimal working example:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"ankiConnect": {
|
||||
"enabled": true,
|
||||
"deck": "Mining",
|
||||
"fields": {
|
||||
"sentence": "Sentence",
|
||||
"audio": "SentenceAudio",
|
||||
"image": "Picture",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Field names must match your Anki note type exactly (case-sensitive). See [Anki Integration](/anki-integration) for the full reference.
|
||||
:::
|
||||
|
||||
## How It Works
|
||||
|
||||
1. SubMiner starts the overlay app in the background
|
||||
@@ -14,14 +34,18 @@
|
||||
6. Hover a word, then trigger Yomitan lookup with your configured lookup key/modifier to open the Yomitan popup
|
||||
7. Optional [subtitle annotations](/subtitle-annotations) (N+1, character-name, frequency, JLPT) highlight useful cues in real time
|
||||
|
||||
There are two ways to use SubMiner:
|
||||
There are several ways to use SubMiner:
|
||||
|
||||
| Approach | Use when | How |
|
||||
| -------- | -------- | --- |
|
||||
| **`subminer` script** | You want SubMiner to handle everything — launch mpv, set up the socket, start the overlay. The simplest path. | `subminer video.mkv` |
|
||||
| **MPV plugin** | You launch mpv yourself or from another tool (file manager, Jellyfin, etc.). Requires `--input-ipc-server=/tmp/subminer-socket` in your mpv config. | Use `y` chord keybindings inside mpv |
|
||||
> [!TIP]
|
||||
> **New users: start with the `subminer` wrapper script** (or the **SubMiner mpv** shortcut on Windows). It handles mpv launch, IPC socket setup, and overlay lifecycle automatically so you don't need to configure anything in `mpv.conf`.
|
||||
|
||||
You can use both — the plugin provides in-player controls, while the `subminer` script is convenient for direct playback. The `subminer` script runs directly via shebang (no `bun run` needed).
|
||||
| Approach | Use when | How |
|
||||
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| **`subminer` script** | You want SubMiner to handle everything — launch mpv, set up the socket, start the overlay. **The simplest path and recommended starting point.** | `subminer video.mkv` |
|
||||
| **SubMiner mpv shortcut** (Windows) | The recommended Windows entry point. Created during first-run setup, launches mpv with SubMiner's defaults directly. | Double-click, drag a file onto it, or run `SubMiner.exe --launch-mpv` |
|
||||
| **MPV plugin** (all platforms) | You launch mpv yourself or from another tool (file manager, Jellyfin, etc.). Requires `--input-ipc-server=/tmp/subminer-socket` in your mpv config. | Use `y` chord keybindings inside mpv |
|
||||
|
||||
You can use both — the plugin provides in-player controls, while the `subminer` script (or the Windows shortcut) is convenient for direct playback. The `subminer` script runs directly via shebang on Linux and macOS (no `bun run` needed); on Windows it must be invoked with `bun run subminer` since the shebang is not supported.
|
||||
|
||||
## Live Config Reload
|
||||
|
||||
@@ -61,8 +85,8 @@ subminer --args '--fs=opengl-hq --ytdl-format=bestvideo*+bestaudio/best' video.m
|
||||
# Options
|
||||
subminer -T video.mkv # Disable texthooker server
|
||||
subminer -b x11 video.mkv # Force X11 backend
|
||||
subminer video.mkv # Uses mpv profile "subminer" by default
|
||||
subminer -p gpu-hq video.mkv # Override mpv profile
|
||||
subminer video.mkv # No mpv profile passed by default
|
||||
subminer -p gpu-hq video.mkv # Use a specific mpv profile
|
||||
subminer jellyfin # Open Jellyfin setup window (subcommand form)
|
||||
subminer jellyfin -l --server http://127.0.0.1:8096 --username me --password 'secret'
|
||||
subminer jellyfin --logout # Clear stored Jellyfin token/session data
|
||||
@@ -133,7 +157,7 @@ You can use it three ways:
|
||||
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
|
||||
```
|
||||
|
||||
This flow requires `mpv.exe` to be discoverable. Leave `mpv.executablePath` blank to auto-discover from `PATH`, or set it to the full `mpv.exe` path if mpv is installed elsewhere. `SUBMINER_MPV_PATH` is still honored as a fallback. On Windows, `--launch-mpv` does not require an `mpv.conf` profile named `subminer`.
|
||||
This flow requires `mpv.exe` to be discoverable. Leave `mpv.executablePath` blank to auto-discover from `PATH`, or set it to the full `mpv.exe` path if mpv is installed elsewhere. `SUBMINER_MPV_PATH` is still honored as a fallback.
|
||||
|
||||
### Launcher Subcommands
|
||||
|
||||
@@ -148,7 +172,7 @@ This flow requires `mpv.exe` to be discoverable. Leave `mpv.executablePath` blan
|
||||
|
||||
### First-Run Setup
|
||||
|
||||
SubMiner auto-opens the setup popup on fresh installs when launched with `--start` or `--background` and setup is incomplete.
|
||||
Setup popup appears on first launch, or when setup has not been completed.
|
||||
|
||||
You can also open it manually:
|
||||
|
||||
@@ -238,7 +262,7 @@ Notes:
|
||||
- Secondary target languages come from `secondarySub.secondarySubLanguages` (defaults to English if unset).
|
||||
- Configure defaults in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`) under `youtube` and `secondarySub`.
|
||||
|
||||
For local video files, SubMiner now uses those same config-driven language priorities after mpv finishes reporting subtitle tracks. That means mixed internal/external subtitle sets can correct an initial `sid=auto` guess and settle onto the expected primary and secondary tracks without manual cycling.
|
||||
For local video files, SubMiner uses the same config-driven language priorities to auto-select the primary and secondary subtitle tracks from internal and external subtitle sources.
|
||||
|
||||
## Controller Support
|
||||
|
||||
@@ -257,26 +281,26 @@ By default SubMiner uses the first connected controller. `Alt+C` opens the contr
|
||||
|
||||
### Default Button Mapping
|
||||
|
||||
| Button | Action |
|
||||
| ------ | ------ |
|
||||
| `A` (South) | Toggle lookup |
|
||||
| `B` (East) | Close lookup |
|
||||
| `Y` (North) | Toggle keyboard-only mode |
|
||||
| `X` (West) | Mine card |
|
||||
| `L1` | Play current Yomitan audio |
|
||||
| `R1` | Next Yomitan audio track |
|
||||
| `L3` (left stick press) | Toggle mpv pause |
|
||||
| `Select` / `Minus` | Quit mpv |
|
||||
| `L2` / `R2` | Unbound (available for custom bindings) |
|
||||
| Button | Action |
|
||||
| ----------------------- | --------------------------------------- |
|
||||
| `A` (South) | Toggle lookup |
|
||||
| `B` (East) | Close lookup |
|
||||
| `Y` (North) | Toggle keyboard-only mode |
|
||||
| `X` (West) | Mine card |
|
||||
| `L1` | Play current Yomitan audio |
|
||||
| `R1` | Next Yomitan audio track |
|
||||
| `L3` (left stick press) | Toggle mpv pause |
|
||||
| `Select` / `Minus` | Quit mpv |
|
||||
| `L2` / `R2` | Unbound (available for custom bindings) |
|
||||
|
||||
### Analog Controls
|
||||
|
||||
| Input | Action |
|
||||
| ----- | ------ |
|
||||
| Left stick horizontal | Move token selection left/right |
|
||||
| Left stick vertical | Scroll Yomitan popup |
|
||||
| Right stick vertical | Jump through Yomitan popup |
|
||||
| D-pad | Fallback for stick navigation when configured |
|
||||
| Input | Action |
|
||||
| --------------------- | --------------------------------------------- |
|
||||
| Left stick horizontal | Move token selection left/right |
|
||||
| Left stick vertical | Scroll Yomitan popup |
|
||||
| Right stick vertical | Jump through Yomitan popup |
|
||||
| D-pad | Fallback for stick navigation when configured |
|
||||
|
||||
Learn mode ignores already-held inputs and waits for the next fresh button press or axis direction, which avoids accidental captures when you open the modal mid-input.
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import net from 'node:net';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { Args } from './types';
|
||||
import {
|
||||
buildMpvBackendArgs,
|
||||
buildMpvEnv,
|
||||
cleanupPlaybackSession,
|
||||
detectBackend,
|
||||
findAppBinary,
|
||||
@@ -125,6 +127,113 @@ test('detectBackend resolves windows on win32 auto mode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvEnv forces X11 by dropping Wayland hints when backend resolves to x11', () => {
|
||||
withPlatform('linux', () => {
|
||||
const env = buildMpvEnv(makeArgs({ backend: 'x11' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
HYPRLAND_INSTANCE_SIGNATURE: 'hypr',
|
||||
SWAYSOCK: '/tmp/sway.sock',
|
||||
});
|
||||
|
||||
assert.equal(env.DISPLAY, ':1');
|
||||
assert.equal(env.WAYLAND_DISPLAY, undefined);
|
||||
assert.equal(env.XDG_SESSION_TYPE, 'x11');
|
||||
assert.equal(env.HYPRLAND_INSTANCE_SIGNATURE, undefined);
|
||||
assert.equal(env.SWAYSOCK, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvEnv auto mode falls back to X11 when no supported Wayland tracker backend is detected', () => {
|
||||
withPlatform('linux', () => {
|
||||
const env = buildMpvEnv(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
XDG_CURRENT_DESKTOP: 'KDE',
|
||||
XDG_SESSION_DESKTOP: 'plasma',
|
||||
});
|
||||
|
||||
assert.equal(env.DISPLAY, ':1');
|
||||
assert.equal(env.WAYLAND_DISPLAY, undefined);
|
||||
assert.equal(env.XDG_SESSION_TYPE, 'x11');
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvEnv preserves native Wayland env for supported Hyprland and Sway auto backends', () => {
|
||||
withPlatform('linux', () => {
|
||||
const hyprEnv = buildMpvEnv(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
HYPRLAND_INSTANCE_SIGNATURE: 'hypr',
|
||||
});
|
||||
assert.equal(hyprEnv.WAYLAND_DISPLAY, 'wayland-0');
|
||||
assert.equal(hyprEnv.XDG_SESSION_TYPE, 'wayland');
|
||||
|
||||
const swayEnv = buildMpvEnv(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
SWAYSOCK: '/tmp/sway.sock',
|
||||
});
|
||||
assert.equal(swayEnv.WAYLAND_DISPLAY, 'wayland-0');
|
||||
assert.equal(swayEnv.XDG_SESSION_TYPE, 'wayland');
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvBackendArgs forces an explicit X11 renderer stack when backend resolves to x11', () => {
|
||||
withPlatform('linux', () => {
|
||||
assert.deepEqual(
|
||||
buildMpvBackendArgs(makeArgs({ backend: 'x11' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
}),
|
||||
['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvBackendArgs forces the same X11 renderer stack for unsupported Wayland auto fallback', () => {
|
||||
withPlatform('linux', () => {
|
||||
assert.deepEqual(
|
||||
buildMpvBackendArgs(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
XDG_CURRENT_DESKTOP: 'KDE',
|
||||
XDG_SESSION_DESKTOP: 'plasma',
|
||||
}),
|
||||
['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvBackendArgs keeps supported Hyprland and Sway auto backends unchanged', () => {
|
||||
withPlatform('linux', () => {
|
||||
assert.deepEqual(
|
||||
buildMpvBackendArgs(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
HYPRLAND_INSTANCE_SIGNATURE: 'hypr',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildMpvBackendArgs(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
SWAYSOCK: '/tmp/sway.sock',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('launchTexthookerOnly exits non-zero when app binary cannot be spawned', () => {
|
||||
const error = withProcessExitIntercept(() => {
|
||||
launchTexthookerOnly('/definitely-missing-subminer-binary', makeArgs());
|
||||
@@ -485,6 +594,40 @@ function withAccessSyncStub(
|
||||
}
|
||||
}
|
||||
|
||||
function withExistsAndStatSyncStubs(
|
||||
options: {
|
||||
existingPaths?: string[];
|
||||
directoryPaths?: string[];
|
||||
},
|
||||
run: () => void,
|
||||
): void {
|
||||
const existingPaths = new Set(options.existingPaths ?? []);
|
||||
const directoryPaths = new Set(options.directoryPaths ?? []);
|
||||
const originalExistsSync = fs.existsSync;
|
||||
const originalStatSync = fs.statSync;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).existsSync = (filePath: string): boolean =>
|
||||
existingPaths.has(filePath) || directoryPaths.has(filePath);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).statSync = (filePath: string) => {
|
||||
if (directoryPaths.has(filePath)) {
|
||||
return { isDirectory: () => true };
|
||||
}
|
||||
if (existingPaths.has(filePath)) {
|
||||
return { isDirectory: () => false };
|
||||
}
|
||||
throw Object.assign(new Error(`ENOENT: ${filePath}`), { code: 'ENOENT' });
|
||||
};
|
||||
run();
|
||||
} finally {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).existsSync = originalExistsSync;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).statSync = originalStatSync;
|
||||
}
|
||||
}
|
||||
|
||||
function withRealpathSyncStub(resolvePath: (filePath: string) => string, run: () => void): void {
|
||||
const originalRealpathSync = fs.realpathSync;
|
||||
try {
|
||||
@@ -497,6 +640,75 @@ function withRealpathSyncStub(resolvePath: (filePath: string) => string, run: ()
|
||||
}
|
||||
}
|
||||
|
||||
function listRepoRootWindowsTempArtifacts(): string[] {
|
||||
return fs
|
||||
.readdirSync(process.cwd())
|
||||
.filter((entry) => /^\\tmp\\subminer-test-win-/.test(entry))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function runFindAppBinaryWindowsPathCase(): void {
|
||||
const baseDir = 'C:\\Users\\tester\\subminer-test-win-path';
|
||||
const originalHomedir = os.homedir;
|
||||
const originalPath = process.env.PATH;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
const binDir = path.win32.join(baseDir, 'bin');
|
||||
const wrapperPath = path.win32.join(binDir, 'SubMiner.exe');
|
||||
process.env.PATH = `${binDir}${path.win32.delimiter}${originalPath ?? ''}`;
|
||||
|
||||
withFindAppBinaryPlatformSandbox('win32', (pathModule) => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === wrapperPath,
|
||||
() => {
|
||||
const result = findAppBinary(
|
||||
pathModule.join(baseDir, 'launcher', 'SubMiner.exe'),
|
||||
pathModule,
|
||||
);
|
||||
assert.equal(result, wrapperPath);
|
||||
},
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
process.env.PATH = originalPath;
|
||||
}
|
||||
}
|
||||
|
||||
function runFindAppBinaryWindowsInstallDirCase(): void {
|
||||
const baseDir = 'C:\\Users\\tester\\subminer-test-win-dir';
|
||||
const originalHomedir = os.homedir;
|
||||
const originalSubminerBinaryPath = process.env.SUBMINER_BINARY_PATH;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
const installDir = path.win32.join(baseDir, 'Programs', 'SubMiner');
|
||||
const appExe = path.win32.join(installDir, 'SubMiner.exe');
|
||||
process.env.SUBMINER_BINARY_PATH = installDir;
|
||||
|
||||
withPlatform('win32', () => {
|
||||
withExistsAndStatSyncStubs(
|
||||
{ existingPaths: [appExe], directoryPaths: [installDir] },
|
||||
() => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === appExe,
|
||||
() => {
|
||||
const result = findAppBinary(path.win32.join(baseDir, 'launcher', 'SubMiner.exe'), path.win32);
|
||||
assert.equal(result, appExe);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
if (originalSubminerBinaryPath === undefined) {
|
||||
delete process.env.SUBMINER_BINARY_PATH;
|
||||
} else {
|
||||
process.env.SUBMINER_BINARY_PATH = originalSubminerBinaryPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test(
|
||||
'findAppBinary resolves ~/.local/bin/SubMiner.AppImage when it exists',
|
||||
{ concurrency: false },
|
||||
@@ -657,71 +869,31 @@ test('findAppBinary resolves Windows install paths when present', { concurrency:
|
||||
}
|
||||
});
|
||||
|
||||
test('findAppBinary resolves SubMiner.exe on PATH on Windows', { concurrency: false }, () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-path-'));
|
||||
const originalHomedir = os.homedir;
|
||||
const originalPath = process.env.PATH;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
const binDir = path.win32.join(baseDir, 'bin');
|
||||
const wrapperPath = path.win32.join(binDir, 'SubMiner.exe');
|
||||
makeExecutable(wrapperPath);
|
||||
process.env.PATH = `${binDir}${path.win32.delimiter}${originalPath ?? ''}`;
|
||||
test(
|
||||
'findAppBinary Windows cases do not leak backslash temp artifacts on POSIX',
|
||||
{ concurrency: false },
|
||||
() => {
|
||||
if (path.sep === '\\') {
|
||||
return;
|
||||
}
|
||||
|
||||
withFindAppBinaryPlatformSandbox('win32', (pathModule) => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === wrapperPath,
|
||||
() => {
|
||||
const result = findAppBinary(
|
||||
pathModule.join(baseDir, 'launcher', 'SubMiner.exe'),
|
||||
pathModule,
|
||||
);
|
||||
assert.equal(result, wrapperPath);
|
||||
},
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
process.env.PATH = originalPath;
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
const before = listRepoRootWindowsTempArtifacts();
|
||||
runFindAppBinaryWindowsPathCase();
|
||||
runFindAppBinaryWindowsInstallDirCase();
|
||||
const after = listRepoRootWindowsTempArtifacts();
|
||||
|
||||
assert.deepEqual(after, before);
|
||||
},
|
||||
);
|
||||
|
||||
test('findAppBinary resolves SubMiner.exe on PATH on Windows', { concurrency: false }, () => {
|
||||
runFindAppBinaryWindowsPathCase();
|
||||
});
|
||||
|
||||
test(
|
||||
'findAppBinary resolves a Windows install directory to SubMiner.exe',
|
||||
{ concurrency: false },
|
||||
() => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-dir-'));
|
||||
const originalHomedir = os.homedir;
|
||||
const originalSubminerBinaryPath = process.env.SUBMINER_BINARY_PATH;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
const installDir = path.win32.join(baseDir, 'Programs', 'SubMiner');
|
||||
const appExe = path.win32.join(installDir, 'SubMiner.exe');
|
||||
process.env.SUBMINER_BINARY_PATH = installDir;
|
||||
fs.mkdirSync(installDir, { recursive: true });
|
||||
fs.writeFileSync(appExe, '#!/bin/sh\nexit 0\n');
|
||||
fs.chmodSync(appExe, 0o755);
|
||||
|
||||
const originalPlatform = process.platform;
|
||||
try {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
||||
const result = findAppBinary(
|
||||
path.win32.join(baseDir, 'launcher', 'SubMiner.exe'),
|
||||
path.win32,
|
||||
);
|
||||
assert.equal(result, appExe);
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
||||
}
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
if (originalSubminerBinaryPath === undefined) {
|
||||
delete process.env.SUBMINER_BINARY_PATH;
|
||||
} else {
|
||||
process.env.SUBMINER_BINARY_PATH = originalSubminerBinaryPath;
|
||||
}
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
runFindAppBinaryWindowsInstallDirCase();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -225,27 +225,65 @@ export function makeTempDir(prefix: string): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
export function detectBackend(backend: Backend): Exclude<Backend, 'auto'> {
|
||||
export function detectBackend(
|
||||
backend: Backend,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Exclude<Backend, 'auto'> {
|
||||
if (backend !== 'auto') return backend;
|
||||
if (process.platform === 'win32') return 'windows';
|
||||
if (process.platform === 'darwin') return 'macos';
|
||||
const xdgCurrentDesktop = (process.env.XDG_CURRENT_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionDesktop = (process.env.XDG_SESSION_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionType = (process.env.XDG_SESSION_TYPE || '').toLowerCase();
|
||||
const hasWayland = Boolean(process.env.WAYLAND_DISPLAY) || xdgSessionType === 'wayland';
|
||||
const linuxDesktopEnv = getLinuxDesktopEnv(env);
|
||||
|
||||
if (
|
||||
process.env.HYPRLAND_INSTANCE_SIGNATURE ||
|
||||
xdgCurrentDesktop.includes('hyprland') ||
|
||||
xdgSessionDesktop.includes('hyprland')
|
||||
env.HYPRLAND_INSTANCE_SIGNATURE ||
|
||||
linuxDesktopEnv.xdgCurrentDesktop.includes('hyprland') ||
|
||||
linuxDesktopEnv.xdgSessionDesktop.includes('hyprland')
|
||||
) {
|
||||
return 'hyprland';
|
||||
}
|
||||
if (hasWayland && commandExists('hyprctl')) return 'hyprland';
|
||||
if (process.env.DISPLAY) return 'x11';
|
||||
if (linuxDesktopEnv.hasWayland && commandExists('hyprctl')) return 'hyprland';
|
||||
if (env.DISPLAY) return 'x11';
|
||||
fail('Could not detect display backend');
|
||||
}
|
||||
|
||||
type LinuxDesktopEnv = {
|
||||
xdgCurrentDesktop: string;
|
||||
xdgSessionDesktop: string;
|
||||
hasWayland: boolean;
|
||||
};
|
||||
|
||||
function getLinuxDesktopEnv(env: NodeJS.ProcessEnv): LinuxDesktopEnv {
|
||||
const xdgCurrentDesktop = (env.XDG_CURRENT_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionDesktop = (env.XDG_SESSION_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionType = (env.XDG_SESSION_TYPE || '').toLowerCase();
|
||||
return {
|
||||
xdgCurrentDesktop,
|
||||
xdgSessionDesktop,
|
||||
hasWayland: Boolean(env.WAYLAND_DISPLAY) || xdgSessionType === 'wayland',
|
||||
};
|
||||
}
|
||||
|
||||
function shouldForceX11MpvBackend(
|
||||
args: Pick<Args, 'backend'>,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): boolean {
|
||||
if (process.platform !== 'linux' || !env.DISPLAY?.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const linuxDesktopEnv = getLinuxDesktopEnv(env);
|
||||
const supportedWaylandBackend =
|
||||
Boolean(env.HYPRLAND_INSTANCE_SIGNATURE || env.SWAYSOCK) ||
|
||||
linuxDesktopEnv.xdgCurrentDesktop.includes('hyprland') ||
|
||||
linuxDesktopEnv.xdgCurrentDesktop.includes('sway') ||
|
||||
linuxDesktopEnv.xdgSessionDesktop.includes('hyprland') ||
|
||||
linuxDesktopEnv.xdgSessionDesktop.includes('sway');
|
||||
return (
|
||||
args.backend === 'x11' ||
|
||||
(args.backend === 'auto' && linuxDesktopEnv.hasWayland && !supportedWaylandBackend)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAppBinaryCandidate(candidate: string, pathModule: PathModule = path): string {
|
||||
const direct = resolveBinaryPathCandidate(candidate);
|
||||
if (!direct) return '';
|
||||
@@ -637,6 +675,7 @@ export async function startMpv(
|
||||
const mpvArgs: string[] = [];
|
||||
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
|
||||
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
|
||||
mpvArgs.push(...buildMpvBackendArgs(args));
|
||||
if (targetKind === 'url' && isYoutubeTarget(target)) {
|
||||
log('info', args.logLevel, 'Applying URL playback options');
|
||||
mpvArgs.push('--ytdl=yes');
|
||||
@@ -712,7 +751,10 @@ export async function startMpv(
|
||||
const mpvTarget = resolveCommandInvocation('mpv', mpvArgs, {
|
||||
normalizeWindowsShellArgs: false,
|
||||
});
|
||||
state.mpvProc = spawn(mpvTarget.command, mpvTarget.args, { stdio: 'inherit' });
|
||||
state.mpvProc = spawn(mpvTarget.command, mpvTarget.args, {
|
||||
stdio: 'inherit',
|
||||
env: buildMpvEnv(args),
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForOverlayStartCommandSettled(
|
||||
@@ -889,9 +931,9 @@ function stopManagedOverlayApp(args: Args): void {
|
||||
}
|
||||
}
|
||||
|
||||
function buildAppEnv(): NodeJS.ProcessEnv {
|
||||
function buildAppEnv(baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
||||
const env: Record<string, string | undefined> = {
|
||||
...process.env,
|
||||
...baseEnv,
|
||||
SUBMINER_APP_LOG: getAppLogPath(),
|
||||
SUBMINER_MPV_LOG: getMpvLogPath(),
|
||||
};
|
||||
@@ -911,6 +953,32 @@ function buildAppEnv(): NodeJS.ProcessEnv {
|
||||
return env;
|
||||
}
|
||||
|
||||
export function buildMpvEnv(
|
||||
args: Pick<Args, 'backend'>,
|
||||
baseEnv: NodeJS.ProcessEnv = process.env,
|
||||
): NodeJS.ProcessEnv {
|
||||
const env = buildAppEnv(baseEnv);
|
||||
if (!shouldForceX11MpvBackend(args, env)) {
|
||||
return env;
|
||||
}
|
||||
|
||||
delete env.WAYLAND_DISPLAY;
|
||||
delete env.HYPRLAND_INSTANCE_SIGNATURE;
|
||||
delete env.SWAYSOCK;
|
||||
env.XDG_SESSION_TYPE = 'x11';
|
||||
return env;
|
||||
}
|
||||
|
||||
export function buildMpvBackendArgs(
|
||||
args: Pick<Args, 'backend'>,
|
||||
baseEnv: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
if (!shouldForceX11MpvBackend(args, baseEnv)) {
|
||||
return [];
|
||||
}
|
||||
return ['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'];
|
||||
}
|
||||
|
||||
function appendCapturedAppOutput(kind: 'STDOUT' | 'STDERR', chunk: string): void {
|
||||
const normalized = chunk.replace(/\r\n/g, '\n');
|
||||
for (const line of normalized.split('\n')) {
|
||||
@@ -1144,6 +1212,7 @@ export function launchMpvIdleDetached(
|
||||
const mpvArgs: string[] = [];
|
||||
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
|
||||
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
|
||||
mpvArgs.push(...buildMpvBackendArgs(args));
|
||||
if (args.mpvArgs) {
|
||||
mpvArgs.push(...parseMpvArgString(args.mpvArgs));
|
||||
}
|
||||
@@ -1159,6 +1228,7 @@ export function launchMpvIdleDetached(
|
||||
const proc = spawn(mpvTarget.command, mpvTarget.args, {
|
||||
stdio: 'ignore',
|
||||
detached: true,
|
||||
env: buildMpvEnv(args),
|
||||
});
|
||||
if (typeof proc.pid === 'number' && proc.pid > 0) {
|
||||
trackDetachedMpvPid(proc.pid);
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"test:launcher": "bun run test:launcher:src",
|
||||
"test:core": "bun run test:core:src",
|
||||
"test:subtitle": "bun run test:subtitle:src",
|
||||
"test:fast": "bun run test:config:src && bun run test:core:src && bun run test:docs:kb && bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts && bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
||||
"test:fast": "bun run test:config:src && bun run test:core:src && bun run test:docs:kb && bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts && bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
||||
"generate:config-example": "bun run src/generate-config-example.ts",
|
||||
"verify:config-example": "bun run src/verify-config-example.ts",
|
||||
"start": "bun run build && electron . --start",
|
||||
@@ -128,6 +128,7 @@
|
||||
"productName": "SubMiner",
|
||||
"executableName": "SubMiner",
|
||||
"artifactName": "SubMiner-${version}.${ext}",
|
||||
"afterPack": "scripts/electron-builder-after-pack.cjs",
|
||||
"icon": "assets/SubMiner-square.png",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
|
||||
46
scripts/electron-builder-after-pack.cjs
Normal file
46
scripts/electron-builder-after-pack.cjs
Normal file
@@ -0,0 +1,46 @@
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
|
||||
const LINUX_FFMPEG_LIBRARY = 'libffmpeg.so';
|
||||
|
||||
async function stageLinuxAppImageSharedLibrary(
|
||||
context,
|
||||
deps = {
|
||||
access: (filePath) => fs.access(filePath),
|
||||
mkdir: (dirPath) => fs.mkdir(dirPath, { recursive: true }),
|
||||
copyFile: (from, to) => fs.copyFile(from, to),
|
||||
},
|
||||
) {
|
||||
if (context.electronPlatformName !== 'linux') return false;
|
||||
|
||||
const sourceLibraryPath = path.join(context.appOutDir, LINUX_FFMPEG_LIBRARY);
|
||||
|
||||
try {
|
||||
await deps.access(sourceLibraryPath);
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
||||
throw new Error(
|
||||
`Linux packaging requires ${LINUX_FFMPEG_LIBRARY} at ${sourceLibraryPath} so AppImage child processes can resolve it.`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const targetLibraryDir = path.join(context.appOutDir, 'usr', 'lib');
|
||||
const targetLibraryPath = path.join(targetLibraryDir, LINUX_FFMPEG_LIBRARY);
|
||||
|
||||
await deps.mkdir(targetLibraryDir);
|
||||
await deps.copyFile(sourceLibraryPath, targetLibraryPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function afterPack(context) {
|
||||
await stageLinuxAppImageSharedLibrary(context);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LINUX_FFMPEG_LIBRARY,
|
||||
stageLinuxAppImageSharedLibrary,
|
||||
default: afterPack,
|
||||
};
|
||||
104
scripts/electron-builder-after-pack.test.ts
Normal file
104
scripts/electron-builder-after-pack.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const {
|
||||
LINUX_FFMPEG_LIBRARY,
|
||||
default: afterPack,
|
||||
stageLinuxAppImageSharedLibrary,
|
||||
} = require('./electron-builder-after-pack.cjs') as {
|
||||
LINUX_FFMPEG_LIBRARY: string;
|
||||
default: (context: { appOutDir: string; electronPlatformName: string }) => Promise<void>;
|
||||
stageLinuxAppImageSharedLibrary: (context: {
|
||||
appOutDir: string;
|
||||
electronPlatformName: string;
|
||||
}) => Promise<boolean>;
|
||||
};
|
||||
|
||||
function createWorkspace(name: string): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), `${name}-`));
|
||||
}
|
||||
|
||||
test('stageLinuxAppImageSharedLibrary copies libffmpeg.so into usr/lib for Linux packaging', async () => {
|
||||
const workspace = createWorkspace('subminer-after-pack-linux');
|
||||
const appOutDir = path.join(workspace, 'SubMiner-linux-x64');
|
||||
const sourceLibraryPath = path.join(appOutDir, LINUX_FFMPEG_LIBRARY);
|
||||
const targetLibraryPath = path.join(appOutDir, 'usr', 'lib', LINUX_FFMPEG_LIBRARY);
|
||||
|
||||
fs.mkdirSync(appOutDir, { recursive: true });
|
||||
fs.writeFileSync(sourceLibraryPath, 'bundled ffmpeg', 'utf8');
|
||||
|
||||
try {
|
||||
const staged = await stageLinuxAppImageSharedLibrary({
|
||||
appOutDir,
|
||||
electronPlatformName: 'linux',
|
||||
});
|
||||
|
||||
assert.equal(staged, true);
|
||||
assert.equal(fs.readFileSync(targetLibraryPath, 'utf8'), 'bundled ffmpeg');
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('stageLinuxAppImageSharedLibrary skips non-Linux packaging contexts', async () => {
|
||||
const workspace = createWorkspace('subminer-after-pack-non-linux');
|
||||
const appOutDir = path.join(workspace, 'SubMiner-darwin-arm64');
|
||||
const sourceLibraryPath = path.join(appOutDir, LINUX_FFMPEG_LIBRARY);
|
||||
const targetLibraryPath = path.join(appOutDir, 'usr', 'lib', LINUX_FFMPEG_LIBRARY);
|
||||
|
||||
fs.mkdirSync(appOutDir, { recursive: true });
|
||||
fs.writeFileSync(sourceLibraryPath, 'bundled ffmpeg', 'utf8');
|
||||
|
||||
try {
|
||||
const staged = await stageLinuxAppImageSharedLibrary({
|
||||
appOutDir,
|
||||
electronPlatformName: 'darwin',
|
||||
});
|
||||
|
||||
assert.equal(staged, false);
|
||||
assert.equal(fs.existsSync(targetLibraryPath), false);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('stageLinuxAppImageSharedLibrary throws when Linux packaging is missing libffmpeg.so', async () => {
|
||||
const workspace = createWorkspace('subminer-after-pack-missing-library');
|
||||
const appOutDir = path.join(workspace, 'SubMiner-linux-x64');
|
||||
|
||||
fs.mkdirSync(appOutDir, { recursive: true });
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
stageLinuxAppImageSharedLibrary({
|
||||
appOutDir,
|
||||
electronPlatformName: 'linux',
|
||||
}),
|
||||
new RegExp(`Linux packaging requires ${LINUX_FFMPEG_LIBRARY} at .*${LINUX_FFMPEG_LIBRARY}`),
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('afterPack propagates Linux staging failures', async () => {
|
||||
const workspace = createWorkspace('subminer-after-pack-propagates-linux-failure');
|
||||
const appOutDir = path.join(workspace, 'SubMiner-linux-x64');
|
||||
|
||||
fs.mkdirSync(appOutDir, { recursive: true });
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
afterPack({
|
||||
appOutDir,
|
||||
electronPlatformName: 'linux',
|
||||
}),
|
||||
/Linux packaging requires libffmpeg\.so/,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
16
src/main.ts
16
src/main.ts
@@ -2318,8 +2318,8 @@ const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
|
||||
logError: (message, error) => logger.error(message, error),
|
||||
});
|
||||
|
||||
function openFirstRunSetupWindow(): void {
|
||||
if (firstRunSetupService.isSetupCompleted()) {
|
||||
function openFirstRunSetupWindow(force = false): void {
|
||||
if (!force && firstRunSetupService.isSetupCompleted()) {
|
||||
return;
|
||||
}
|
||||
openFirstRunSetupWindowHandler();
|
||||
@@ -3238,12 +3238,12 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
||||
handleFirstRunSetup: async () => {
|
||||
const snapshot = await firstRunSetupService.ensureSetupStateInitialized();
|
||||
appState.firstRunSetupCompleted = snapshot.state.status === 'completed';
|
||||
if (
|
||||
appState.initialArgs &&
|
||||
shouldAutoOpenFirstRunSetup(appState.initialArgs) &&
|
||||
snapshot.state.status !== 'completed'
|
||||
) {
|
||||
openFirstRunSetupWindow();
|
||||
const args = appState.initialArgs;
|
||||
if (args && shouldAutoOpenFirstRunSetup(args)) {
|
||||
const force = Boolean(args.setup);
|
||||
if (force || snapshot.state.status !== 'completed') {
|
||||
openFirstRunSetupWindow(force);
|
||||
}
|
||||
}
|
||||
},
|
||||
startJellyfinRemoteSession: async () => {
|
||||
|
||||
@@ -15,6 +15,11 @@ const mixedLanguageTrackList = [
|
||||
{ type: 'sub', id: 12, lang: 'ja', title: 'ja.srt', external: true },
|
||||
];
|
||||
|
||||
const unlabeledExternalSidecarTrackList = [
|
||||
{ type: 'sub', id: 1, lang: 'eng', title: 'English ASS', external: false, selected: true },
|
||||
{ type: 'sub', id: 2, title: 'srt', external: true },
|
||||
];
|
||||
|
||||
test('resolveManagedLocalSubtitleSelection prefers default Japanese primary and English secondary tracks', () => {
|
||||
const result = resolveManagedLocalSubtitleSelection({
|
||||
trackList: mixedLanguageTrackList,
|
||||
@@ -37,6 +42,31 @@ test('resolveManagedLocalSubtitleSelection respects configured language override
|
||||
assert.equal(result.secondaryTrackId, 12);
|
||||
});
|
||||
|
||||
test('resolveManagedLocalSubtitleSelection promotes a single unlabeled external sidecar to primary', () => {
|
||||
const result = resolveManagedLocalSubtitleSelection({
|
||||
trackList: unlabeledExternalSidecarTrackList,
|
||||
primaryLanguages: [],
|
||||
secondaryLanguages: [],
|
||||
});
|
||||
|
||||
assert.equal(result.primaryTrackId, 2);
|
||||
assert.equal(result.secondaryTrackId, 1);
|
||||
});
|
||||
|
||||
test('resolveManagedLocalSubtitleSelection does not guess between multiple unlabeled external sidecars', () => {
|
||||
const result = resolveManagedLocalSubtitleSelection({
|
||||
trackList: [
|
||||
...unlabeledExternalSidecarTrackList,
|
||||
{ type: 'sub', id: 3, title: 'subrip', external: true },
|
||||
],
|
||||
primaryLanguages: [],
|
||||
secondaryLanguages: [],
|
||||
});
|
||||
|
||||
assert.equal(result.primaryTrackId, null);
|
||||
assert.equal(result.secondaryTrackId, 1);
|
||||
});
|
||||
|
||||
test('managed local subtitle selection runtime applies preferred tracks once for a local media path', async () => {
|
||||
const commands: Array<Array<string | number>> = [];
|
||||
const scheduled: Array<() => void> = [];
|
||||
@@ -75,3 +105,42 @@ test('managed local subtitle selection runtime applies preferred tracks once for
|
||||
['set_property', 'secondary-sid', 11],
|
||||
]);
|
||||
});
|
||||
|
||||
test('managed local subtitle selection runtime promotes a single unlabeled external sidecar over embedded english', async () => {
|
||||
const commands: Array<Array<string | number>> = [];
|
||||
const scheduled: Array<() => void> = [];
|
||||
|
||||
const runtime = createManagedLocalSubtitleSelectionRuntime({
|
||||
getCurrentMediaPath: () => '/videos/example.mkv',
|
||||
getMpvClient: () =>
|
||||
({
|
||||
connected: true,
|
||||
requestProperty: async (name: string) => {
|
||||
if (name === 'track-list') {
|
||||
return unlabeledExternalSidecarTrackList;
|
||||
}
|
||||
throw new Error(`Unexpected property: ${name}`);
|
||||
},
|
||||
}) as never,
|
||||
getPrimarySubtitleLanguages: () => [],
|
||||
getSecondarySubtitleLanguages: () => [],
|
||||
sendMpvCommand: (command) => {
|
||||
commands.push(command);
|
||||
},
|
||||
schedule: (callback) => {
|
||||
scheduled.push(callback);
|
||||
return 1 as never;
|
||||
},
|
||||
clearScheduled: () => {},
|
||||
});
|
||||
|
||||
runtime.handleMediaPathChange('/videos/example.mkv');
|
||||
scheduled.shift()?.();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
runtime.handleSubtitleTrackListChange(unlabeledExternalSidecarTrackList);
|
||||
|
||||
assert.deepEqual(commands, [
|
||||
['set_property', 'sid', 2],
|
||||
['set_property', 'secondary-sid', 1],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -90,6 +90,10 @@ function isLikelyHearingImpaired(title: string): boolean {
|
||||
return HEARING_IMPAIRED_PATTERN.test(title);
|
||||
}
|
||||
|
||||
function isUnlabeledExternalTrack(track: NormalizedSubtitleTrack): boolean {
|
||||
return track.external && normalizeYoutubeLangCode(track.lang).length === 0;
|
||||
}
|
||||
|
||||
function pickBestTrackId(
|
||||
tracks: NormalizedSubtitleTrack[],
|
||||
preferredLanguages: string[],
|
||||
@@ -126,6 +130,19 @@ function pickBestTrackId(
|
||||
};
|
||||
}
|
||||
|
||||
function pickSingleUnlabeledExternalTrackId(
|
||||
tracks: NormalizedSubtitleTrack[],
|
||||
excludeId: number | null = null,
|
||||
): number | null {
|
||||
const fallbackCandidates = tracks.filter(
|
||||
(track) => track.id !== excludeId && isUnlabeledExternalTrack(track),
|
||||
);
|
||||
if (fallbackCandidates.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
return fallbackCandidates[0]?.id ?? null;
|
||||
}
|
||||
|
||||
export function resolveManagedLocalSubtitleSelection(input: {
|
||||
trackList: unknown[] | null;
|
||||
primaryLanguages: string[];
|
||||
@@ -146,12 +163,13 @@ export function resolveManagedLocalSubtitleSelection(input: {
|
||||
);
|
||||
|
||||
const primary = pickBestTrackId(tracks, preferredPrimaryLanguages);
|
||||
const secondary = pickBestTrackId(tracks, preferredSecondaryLanguages, primary.trackId);
|
||||
const primaryTrackId = primary.trackId ?? pickSingleUnlabeledExternalTrackId(tracks);
|
||||
const secondary = pickBestTrackId(tracks, preferredSecondaryLanguages, primaryTrackId);
|
||||
|
||||
return {
|
||||
primaryTrackId: primary.trackId,
|
||||
primaryTrackId,
|
||||
secondaryTrackId: secondary.trackId,
|
||||
hasPrimaryMatch: primary.hasMatch,
|
||||
hasPrimaryMatch: primary.hasMatch || primaryTrackId !== null,
|
||||
hasSecondaryMatch: secondary.hasMatch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
||||
productName?: string;
|
||||
scripts: Record<string, string>;
|
||||
build?: {
|
||||
afterPack?: string;
|
||||
files?: string[];
|
||||
};
|
||||
};
|
||||
@@ -77,6 +78,10 @@ test('release package scripts disable implicit electron-builder publishing', ()
|
||||
assert.match(packageJson.scripts['build:win:unsigned'] ?? '', /build-win-unsigned\.mjs/);
|
||||
});
|
||||
|
||||
test('release packaging wires a shared afterPack hook for Linux AppImage library staging', () => {
|
||||
assert.equal(packageJson.build?.afterPack, 'scripts/electron-builder-after-pack.cjs');
|
||||
});
|
||||
|
||||
test('top-level package metadata keeps Linux Electron runtime app identity canonical', () => {
|
||||
assert.equal(packageJson.productName, 'SubMiner');
|
||||
assert.equal(packageJson.desktopName, 'SubMiner.desktop');
|
||||
|
||||
Reference in New Issue
Block a user