mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-05 12:12:05 -07:00
Compare commits
5 Commits
codex/fix-
...
refactor-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
8520a4d068
|
|||
|
564a295e5f
|
|||
|
006ff22d42
|
|||
|
ec64eebb80
|
|||
|
983f3b38ee
|
58
CHANGELOG.md
58
CHANGELOG.md
@@ -1,63 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## v0.11.1 (2026-04-04)
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
- Release: Linux packaged builds now 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`.
|
||||
- Linux: Linux now restores 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
|
||||
- Overlay: Added a playlist browser overlay modal for browsing sibling video files and the live mpv queue during playback.
|
||||
- Overlay: Added the default `Ctrl+Alt+P` keybinding to open the playlist browser and manage queue order without leaving playback.
|
||||
|
||||
### Changed
|
||||
- Setup: Made mpv plugin installation mandatory in the first-run setup flow, removed the skip path, and kept Finish disabled until the plugin is installed.
|
||||
- Setup: Clarified that the mpv plugin requirement applies to setup on every platform, while the optional `SubMiner mpv` shortcut remains the recommended Windows playback entry point.
|
||||
- Launcher: Streamlined Windows setup and config by making the `SubMiner mpv` shortcut self-contained and keeping `mpv.executablePath` as the simple fallback when `mpv.exe` is not on `PATH`.
|
||||
- Overlay: Changed fresh-install default config to keep texthooker and stats from auto-opening browser tabs.
|
||||
- Overlay: Changed fresh-install default config to enable AnkiConnect, Discord Rich Presence, subtitle-sidebar, and Yomitan-popup auto-pause by default, while disabling controller input by default.
|
||||
|
||||
### Fixed
|
||||
- Main: Resolve the YouTube playback socket path lazily so startup honors CLI and config overrides.
|
||||
- Main: Add regression coverage for the lazy socket-path lookup during Windows mpv startup.
|
||||
- Main: Keep integrated `--start --texthooker` launches on the full app-ready startup path so the texthooker page and websocket servers start together during normal playback startup.
|
||||
- Main: Stop the mpv/plugin auto-start flow from spawning a separate standalone texthooker helper during normal `subminer <video>` launches.
|
||||
- Overlay: Keep tracked macOS visible overlays click-through by default so subtitle sidebar passthrough works immediately without requiring a subtitle hover cycle first.
|
||||
- Overlay: Add regression coverage for the macOS visible-overlay passthrough default.
|
||||
- Anilist: Stop AniList post-watch from sending a second progress update when the current episode was already satisfied by a ready retry item in the same watch-completion pass.
|
||||
- Anilist: Add regression coverage for the retry-queue plus live-update duplicate path.
|
||||
- Overlay: Fixed Kiku duplicate grouping to reuse duplicate note IDs from both generic sentence-card creation and Yomitan popup mining instead of running extra duplicate scans after add.
|
||||
- Overlay: Fixed the Yomitan popup mining flow to add cards in the background while keeping the stock popup progress feedback, then pause playback and close the lookup popup before the Kiku merge modal opens.
|
||||
- Overlay: Fixed configured subtitle-jump keybindings so backward and forward subtitle seeks keep playback paused when invoked from a paused state.
|
||||
- Launcher: Fixed the Windows `SubMiner mpv` shortcut and `SubMiner.exe --launch-mpv` flow to launch mpv with SubMiner's required default args directly instead of requiring an `mpv.conf` profile named `subminer`.
|
||||
- Launcher: Clarified the Windows install and usage docs so the shortcut path is documented as self-contained, while the optional `subminer` mpv profile remains available for manual mpv launches.
|
||||
- Launcher: Hardened the first-run setup blocker copy and stale custom-scheme handling so setup messages stay aligned with config, plugin, and dictionary readiness.
|
||||
- Launcher: Fixed the Windows `SubMiner mpv` shortcut idle launch so loading a video after opening the shortcut keeps mpv in the expected SubMiner-managed session, auto-starts the overlay, and re-arms subtitle auto-selection for the newly opened file.
|
||||
- Launcher: Removed the redundant `.` subtitle search path from the Windows shortcut launch args and deduped repeated subtitle source tracks in the manual sync picker so duplicate external subtitle entries no longer appear from the shortcut path.
|
||||
- Playback: Fixed managed local playback so duplicate startup-ready retries no longer unpause media after a later manual pause on the same file.
|
||||
- Playback: Fixed managed local subtitle auto-selection so local files reuse configured primary and secondary subtitle language priorities instead of staying on mpv's initial `sid=auto` guess.
|
||||
- Launcher: Added a blank-by-default `mpv.executablePath` override for Windows playback so users can point SubMiner at `mpv.exe` when it is not on `PATH`.
|
||||
- Launcher: Kept the Windows shortcut and `--launch-mpv` flow simple by preserving PATH auto-discovery as the default and exposing the override in first-run setup.
|
||||
- Launcher: Added `windows` as a recognized launcher backend option and auto-detection target on Windows.
|
||||
- Launcher: Honored `SUBMINER_YTDLP_BIN` consistently across YouTube playback URL resolution, track probing, subtitle downloads, and metadata probing.
|
||||
- Launcher: Kept the first-run setup window from navigating away on unexpected URLs.
|
||||
- Launcher: Made Windows mpv honor an explicitly configured executable path instead of silently falling back to PATH.
|
||||
- Launcher: Hardened `--launch-mpv` parsing and Windows binary resolution so valueless flags do not swallow media targets and symlinked launcher installs do not short-circuit PATH lookup.
|
||||
- Launcher: Fixed first-run setup blocking playback on macOS when the SubMiner mpv plugin was already installed at the canonical `~/.config/mpv` path.
|
||||
- Launcher: Fixed setup gating so stale cancelled setup state no longer prevents playback when the canonical mpv plugin entrypoint already exists.
|
||||
- Playback: Prevented stale async playlist-browser subtitle rearm callbacks from overriding newer subtitle selections during rapid file changes.
|
||||
|
||||
### Docs
|
||||
- Docs Site: Added a dedicated Subtitle Sidebar guide and linked it from the homepage and configuration docs.
|
||||
- Docs Site: Linked Jimaku integration from the homepage to its dedicated docs page.
|
||||
- Docs Site: Refreshed docs-site theme tokens and hover/selection styling for the updated pages.
|
||||
|
||||
### Internal
|
||||
- Release: Retried AUR clone and push operations in the tagged release workflow.
|
||||
- Release: Kept GitHub Releases green when AUR publish flakes and needs manual follow-up.
|
||||
- Release: Updated Electron to 39.8.6 and pinned patched transitive build dependencies to clear the reported high-severity audit findings.
|
||||
- AniList: Stopped post-watch tracking from sending a second progress update when the current episode was already satisfied by a ready retry item in the same watch-completion pass.
|
||||
|
||||
## v0.10.0 (2026-03-29)
|
||||
|
||||
|
||||
85
README.md
85
README.md
@@ -4,21 +4,23 @@
|
||||
|
||||
# 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.
|
||||
|
||||
[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://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/ksyasuda/SubMiner)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.typescriptlang.org)
|
||||
[](https://docs.subminer.moe)
|
||||
[](https://aur.archlinux.org/packages/subminer-bin)
|
||||
|
||||
[](https://github.com/user-attachments/assets/89e61895-e2b7-4b47-8d50-a35afe4132b2)
|
||||
[](./assets/minecard.mp4)
|
||||
|
||||
</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.
|
||||
|
||||
## Features
|
||||
|
||||
### Dictionary Lookups
|
||||
@@ -65,10 +67,6 @@ 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.
|
||||
|
||||
<div align="center">
|
||||
<img src="docs-site/public/screenshots/playlist-browser.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
### Integrations
|
||||
@@ -76,7 +74,7 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>YouTube</b></td>
|
||||
<td>Auto-loaded yt-dlp subtitle tracks at startup with config-driven primary/secondary language priorities and a manual overlay picker on demand (<code>Ctrl+Alt+C</code>)</td>
|
||||
<td>Auto-loaded yt-dlp subtitle tracks at startup with a manual overlay picker on demand (<code>Ctrl+Alt+C</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>AniList</b></td>
|
||||
@@ -110,15 +108,12 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
|
||||
|
||||
## Requirements
|
||||
|
||||
| | Required | Recommended | Optional |
|
||||
| -------------- | --------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — | — |
|
||||
| **Processing** | — | `ffmpeg` (audio clips & screenshots) | `mecab` + `mecab-ipadic` (annotation POS filtering), `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
|
||||
| **Media** | — | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
|
||||
| **Selection** | — | — | `fzf` / `rofi` |
|
||||
|
||||
> [!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.
|
||||
| | 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` |
|
||||
|
||||
> [!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.
|
||||
@@ -133,9 +128,9 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
|
||||
<summary><b>Arch Linux</b></summary>
|
||||
|
||||
```bash
|
||||
paru -S --needed mpv ffmpeg
|
||||
paru -S --needed mpv ffmpeg mecab-git mecab-ipadic
|
||||
# Optional
|
||||
paru -S --needed mecab-git mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo
|
||||
paru -S --needed 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
|
||||
@@ -148,9 +143,9 @@ paru -S --needed xdotool xorg-xwininfo
|
||||
<summary><b>macOS</b></summary>
|
||||
|
||||
```bash
|
||||
brew install mpv ffmpeg
|
||||
brew install mpv ffmpeg mecab mecab-ipadic
|
||||
# Optional
|
||||
brew install mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||
brew install yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||
# Optional: subtitle sync (install at least one for subtitle syncing to work)
|
||||
brew install alass
|
||||
pip install ffsubsync
|
||||
@@ -165,7 +160,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`.
|
||||
|
||||
Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for additional metadata enrichment.
|
||||
For MeCab, install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -211,16 +206,6 @@ 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>
|
||||
@@ -228,10 +213,6 @@ sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/sub
|
||||
|
||||
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>
|
||||
@@ -243,25 +224,9 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
|
||||
|
||||
### 2. First Launch
|
||||
|
||||
```bash
|
||||
subminer app --setup # launch the first-run setup wizard
|
||||
```
|
||||
Run the app. On first launch SubMiner starts in the system tray, creates a default config, and opens a setup popup to install the mpv plugin and configure Yomitan dictionaries.
|
||||
|
||||
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
|
||||
### 3. Mine
|
||||
|
||||
```bash
|
||||
subminer video.mkv # play video with overlay
|
||||
@@ -271,8 +236,6 @@ 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)**
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
@@ -1,23 +0,0 @@
|
||||
---
|
||||
id: TASK-279
|
||||
title: Prepare v0.11.2 release notes and verify release gate
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-04-04 07:38'
|
||||
labels: []
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Generate release metadata for the pending changelog fragments, review the resulting changelog/release notes output, and run the documented local release gate so the repo is ready for a v0.11.2 cut if checks pass.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 CHANGELOG.md and release/release-notes.md are generated for v0.11.2 using the pending changes fragments.
|
||||
- [ ] #2 The documented local release gate for release prep is run and the pass/fail state is recorded with any blockers.
|
||||
- [ ] #3 Any release-prep file updates needed for the generated notes are left in the worktree for review without tagging or pushing.
|
||||
<!-- AC:END -->
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: TASK-272
|
||||
title: 'Assess and address PR #40 CodeRabbit review follow-ups'
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-04-03 07:52'
|
||||
updated_date: '2026-04-03 08:04'
|
||||
labels:
|
||||
- coderabbit
|
||||
- review
|
||||
- launcher
|
||||
milestone: 'PR #40'
|
||||
dependencies: []
|
||||
references:
|
||||
- 'https://github.com/ksyasuda/SubMiner/pull/40'
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Implement the valid CodeRabbit findings on PR #40 and keep the Windows mpv shortcut / first-run setup flow consistent end to end.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Windows binary resolution does not return install directories as executable candidates
|
||||
- [ ] #2 Launch-mpv arg parsing preserves space-separated mpv option values and target separation
|
||||
- [ ] #3 Windows mpv launch args keep the final input-ipc-server and script-opts socket path in sync when custom values are supplied
|
||||
- [ ] #4 First-run setup navigation swallows stale or invalid custom-scheme actions without navigating away
|
||||
- [ ] #5 Setup messaging and footer copy reflect configReady, plugin, and dictionary gates consistently
|
||||
- [ ] #6 Regression tests cover the fixed behaviors
|
||||
<!-- AC:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Addressed CodeRabbit follow-ups for PR #40. Hardened launcher binary discovery on Windows and PATH resolution, fixed launch-mpv argument parsing for value-bearing flags, synced custom Windows mpv IPC socket values into script opts, and tightened first-run setup messaging/navigation to handle stale actions and blocker copy. Verified with `bun test src/main-entry-runtime.test.ts src/main/runtime/windows-mpv-launch.test.ts src/main/runtime/first-run-setup-window.test.ts launcher/mpv.test.ts`.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
id: TASK-238.8
|
||||
title: Refactor src/main.ts composition root into domain runtimes
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-03-31 06:28'
|
||||
updated_date: '2026-04-01 07:07'
|
||||
labels:
|
||||
- tech-debt
|
||||
- runtime
|
||||
- maintainability
|
||||
- composition-root
|
||||
dependencies: []
|
||||
references:
|
||||
- src/main.ts
|
||||
- src/main/boot/services
|
||||
- src/main/runtime/composers
|
||||
- docs/architecture/README.md
|
||||
parent_task_id: TASK-238
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Refactor `src/main.ts` so it becomes a thin composition root and the domain-specific runtime wiring moves into short wrapper modules under `src/main/`. Preserve all current behavior, IPC contracts, and config/schema semantics while reducing the entrypoint to boot services, grouped runtime instantiation, startup execution, and process-level quit handling.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 `src/main.ts` is bootstrap/composition only: platform preflight, boot services, runtime creation, startup execution, and top-level quit/signal handling.
|
||||
- [ ] #2 `src/main.ts` no longer imports `src/main/runtime/*-main-deps.ts` directly.
|
||||
- [ ] #3 `src/main.ts` has no local names like `build*MainDepsHandler`, `*MainDeps`, or trivial `*Handler` pass-through wrappers.
|
||||
- [ ] #4 New wrapper files stay under ~500 LOC each; if one exceeds that, split before merge.
|
||||
- [ ] #5 Cross-domain coordination stays in `main.ts`; wrapper modules stay acyclic and communicate via injected callbacks.
|
||||
- [ ] #6 No user-facing behavior, config fields, or IPC channel names change.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
CI follow-up: typecheck failed after the runtime split because playlist-browser IPC deps were not threaded through the new bootstrap/composer surfaces. Wiring the missing open action and registration deps now.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
id: TASK-262
|
||||
title: Create overlay UI bootstrap input helper
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2026-03-31 17:04'
|
||||
labels:
|
||||
- refactor
|
||||
- main
|
||||
- overlay-ui
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Add a coarse input-builder/helper module to reduce the large createOverlayUiRuntime(...) callsite in src/main.ts without changing runtime behavior. Do not edit src/main.ts in this task.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 New helper module(s) live under src/main/
|
||||
- [ ] #2 Helper accepts grouped overlay UI/domain inputs instead of giant inline literals
|
||||
- [ ] #3 Helper keeps files under 500 LOC
|
||||
- [ ] #4 Optional focused tests added if useful
|
||||
- [ ] #5 No runtime behavior changes
|
||||
<!-- AC:END -->
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
id: TASK-263
|
||||
title: Create coarse startup bootstrap wrapper
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2026-03-31 17:21'
|
||||
labels:
|
||||
- refactor
|
||||
- main
|
||||
- startup
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Move the large createMainStartupRuntime construction and its self-reference handling out of src/main.ts into a coarse startup bootstrap wrapper. Keep behavior identical and shrink the startup section materially.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 New wrapper module(s) live under src/main/
|
||||
- [ ] #2 Wrapper owns createMainStartupRuntime construction and self-reference handling
|
||||
- [ ] #3 src/main.ts startup section becomes materially smaller
|
||||
- [ ] #4 Files stay under 500 LOC
|
||||
- [ ] #5 Focused tests cover the wrapper if useful
|
||||
- [ ] #6 No runtime behavior changes
|
||||
<!-- AC:END -->
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
id: TASK-269
|
||||
title: 'Assess and address PR #39 latest CodeRabbit review round'
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-01 07:22'
|
||||
updated_date: '2026-04-01 07:55'
|
||||
labels: []
|
||||
dependencies: []
|
||||
references:
|
||||
- src/main
|
||||
- docs/architecture/README.md
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Assess unresolved CodeRabbit review threads on PR #39, fix valid findings in the current branch, and document any non-actionable findings so the review state is clear for follow-up.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 All unresolved CodeRabbit review threads on PR #39 are assessed and classified as fix or non-actionable with rationale
|
||||
- [x] #2 Valid findings are addressed in code without regressing current runtime behavior
|
||||
- [x] #3 Regression tests or targeted coverage are added when the reviewed behavior can be exercised locally
|
||||
- [x] #4 Relevant verification commands are run and results recorded in the task summary
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Confirm each unresolved CodeRabbit thread against current branch state and mark already-addressed items as non-actionable without code churn.
|
||||
2. Fix validated runtime issues in focused patches: AniList setup window stale close handler, headless known-word refresh effective config usage, main startup websocket probe wiring, startup warmup delegation, mpv media-path duplicate side effects, overlay visibility readiness guard, Discord presence service cleanup, and stats daemon self-stop handling.
|
||||
3. Tighten headless startup runtime typing so custom bootstrap deps require an explicit factory instead of unsafe casting.
|
||||
4. Add or extend targeted tests for behaviors that can be exercised locally, especially overlay visibility, Discord lifecycle cleanup, stats self-stop, and headless startup typing/runtime coverage.
|
||||
5. Run targeted tests first, then the relevant broader verification lane, and capture which CodeRabbit items were fixed versus assessed as already addressed.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Reassessed current branch after reset concern: most runtime/test fixes were still present; only live worktree delta was IPC playlist-browser cleanup plus the untracked backlog task record.
|
||||
|
||||
Validated and fixed CodeRabbit findings for stale AniList setup window clearing, effective headless Anki config usage, CLI websocket probe wiring, duplicate mpv media-path side effects, overlay visibility readiness, Discord presence service cleanup, stats self-stop, unsafe headless startup custom deps typing, and playlist-browser IPC wiring.
|
||||
|
||||
Assessed main-startup-runtime-bootstrap warmup comment as non-actionable at this layer: the guarded startBackgroundWarmupsIfAllowed wrapper exists in main-startup-bootstrap, while this lower bootstrap still needs to expose the raw mpv warmup command.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Assessed the unresolved CodeRabbit round on PR #39 and applied the confirmed fixes across the main-process runtime split. Updated AniList setup window teardown to avoid clearing newer windows, made headless known-word refresh use one effective Anki config end-to-end, wired the real mpv websocket probe into CLI startup, removed duplicate mpv media-path side effects, guarded overlay visibility changes behind overlay readiness, stopped Discord presence services before disable/replace, and prevented stats daemon stop from SIGTERMing the current Electron process. Also tightened headless startup typing so custom bootstrap deps require an explicit factory, normalized stats coordinator note-id typing, and completed playlist-browser IPC wiring/types so typecheck stays green.
|
||||
|
||||
Added targeted regression coverage for overlay visibility initialization, Discord lifecycle cleanup, stats self-stop, and headless startup custom-deps typing expectations. Verification run from the current tree: `bun run typecheck`, targeted `bun test` for overlay/discord/stats/headless startup files, and full `bun run test:fast` all passed.
|
||||
|
||||
Assessment outcome for remaining review noise: the playlist-browser open action was already wired in IPC bootstrap, and the warmup-delegation comment on `main-startup-runtime-bootstrap.ts` was not applied because the guarded wrapper lives one layer higher in `main-startup-bootstrap.ts`; this lower bootstrap still needs to surface the raw mpv warmup command consumed by that wrapper.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
id: TASK-270
|
||||
title: Make Windows mpv shortcut self-contained
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-04-02 07:13'
|
||||
updated_date: '2026-04-02 07:19'
|
||||
labels: []
|
||||
dependencies: []
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Remove the Windows mpv shortcut's dependency on a pre-existing mpv profile so the installer-created `SubMiner mpv` flow works out of the box without requiring the user to edit `mpv.conf`. Keep docs aligned with the new behavior and preserve the optional profile guidance for manual mpv usage.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 `SubMiner.exe --launch-mpv` launches mpv with SubMiner's required default args without requiring an mpv profile named `subminer`.
|
||||
- [x] #2 Windows shortcut/help/docs no longer describe `--launch-mpv` as depending on the SubMiner mpv profile.
|
||||
- [x] #3 Automated tests cover the Windows launch args behavior and pass after the change.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Updated the Windows `--launch-mpv` path to pass SubMiner's default mpv args directly instead of requiring `--profile=subminer`. Adjusted Windows shortcut/help text to describe the self-contained defaults-based launch, and updated Windows docs to state that `mpv.conf` is not required for the shortcut path while preserving the optional profile guidance for manual mpv launches.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
id: TASK-271
|
||||
title: Fix local playback subtitle auto-selection and startup pause release
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-03 07:47'
|
||||
updated_date: '2026-04-03 08:03'
|
||||
labels:
|
||||
- bug
|
||||
- playback
|
||||
- subtitles
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Investigate local-video startup on desktop playback where managed subtitle defaults can bind the wrong primary subtitle track and startup readiness retries can force playback to resume after the user manually pauses. Scope includes fixing the pause/unpause loop and making local subtitle auto-selection prefer the intended primary/secondary tracks for sentence mining sessions.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Desktop local playback no longer forces `pause=no` after the user manually pauses during or after startup readiness handling.
|
||||
- [x] #2 Managed local subtitle startup selects the expected primary track before secondary track selection for mixed-language subtitle sets like Japanese primary plus English secondary.
|
||||
- [x] #3 Regression tests cover the pause-release bug and the local subtitle auto-selection behavior.
|
||||
- [x] #4 Internal docs are updated if runtime behavior or operator expectations change.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add regression coverage for startup autoplay release so duplicate ready handling cannot unpause playback after the user manually pauses the same media.
|
||||
2. Add regression coverage for managed local subtitle startup selection using configured primary/secondary subtitle language preferences, with JA/EN remaining the default fallback behavior.
|
||||
3. Extract or add reusable subtitle track ranking logic that prefers configured primary and secondary subtitle languages, with stable scoring for external tracks and non-SDH labels.
|
||||
4. Update local playback startup/runtime wiring so managed subtitle defaults use explicit ranked selection instead of raw sid=auto / secondary-sid=auto while preserving config-driven language preference ordering.
|
||||
5. Run focused subtitle/playback tests, then update task notes/final summary with any behavior or docs impact.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Implemented config-aware managed local subtitle selection runtime for local media path changes and playlist-browser local playback rearm, using `youtube.primarySubLanguages` for primary preference and `secondarySub.secondarySubLanguages` for secondary preference with JA/EN fallback defaults.
|
||||
|
||||
Updated autoplay ready gate to ignore duplicate readiness signals for the same media so later manual pauses are not overridden by repeated tokenization-ready events.
|
||||
|
||||
Updated config/template wording to document that the existing subtitle language preferences now drive managed subtitle auto-selection beyond YouTube-only flows.
|
||||
|
||||
Verification: `bun test src/config/config.test.ts src/main/runtime/autoplay-ready-gate.test.ts src/main/runtime/local-subtitle-selection.test.ts src/main/runtime/playlist-browser-runtime.test.ts`; `bun run typecheck`.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Stopped startup readiness retries from re-unpausing the same media after a later manual pause, and added config-aware managed local subtitle selection so local playback prefers the configured primary/secondary subtitle languages instead of relying on raw mpv `sid=auto` behavior. Added regression coverage for autoplay-ready gating, local subtitle selection, playlist-browser local playback rearm, and config template wording updates.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
id: TASK-273
|
||||
title: >-
|
||||
Fix first-run setup false positive when canonical mpv plugin is already
|
||||
installed
|
||||
status: Done
|
||||
assignee:
|
||||
- Kyle Yasuda
|
||||
created_date: '2026-04-03 23:26'
|
||||
updated_date: '2026-04-04 00:31'
|
||||
labels:
|
||||
- bug
|
||||
- macos
|
||||
- first-run-setup
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Investigate and fix launcher/app first-run setup gating so playback does not block when the SubMiner mpv plugin is already installed at the canonical mpv config path on macOS. Align mpv path resolution with the actual install location, keep plugin detection scoped to the canonical plugin entrypoint, and make launcher setup gating resilient to stale cancelled setup state.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 `resolveDefaultMpvInstallPaths` resolves the canonical macOS mpv config path used by existing installs.
|
||||
- [ ] #2 Playback launcher bypasses first-run setup when the canonical `scripts/subminer/main.lua` plugin entrypoint already exists, even if `setup-state.json` is stale.
|
||||
- [ ] #3 Regression tests cover canonical plugin detection and launcher handling of stale cancelled setup state.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Root cause ended up split across path resolution and launcher gating. No automated test command was executed in this pass by request.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Updated macOS mpv install path resolution to use the canonical `~/.config/mpv` location so first-run plugin detection matches the actual installed plugin path.
|
||||
|
||||
Restricted plugin detection to the canonical `scripts/subminer/main.lua` entrypoint instead of config presence or legacy loader files.
|
||||
|
||||
Updated the launcher setup gate to bypass stale `setup-state.json` when the mpv plugin is already installed, and to ignore an initially stale `cancelled` state after spawning setup.
|
||||
|
||||
Added regression coverage for canonical macOS detection and launcher setup-gate bypass behavior. No automated test command was executed in this pass by request.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
## Definition of Done
|
||||
<!-- DOD:BEGIN -->
|
||||
- [ ] #1 Manual verification with scenario: existing plugin installed in custom mpv config path does not open first-run setup.
|
||||
<!-- DOD:END -->
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
id: TASK-274
|
||||
title: Stabilize current failing test regressions
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-04 04:40'
|
||||
updated_date: '2026-04-04 05:01'
|
||||
labels: []
|
||||
dependencies: []
|
||||
documentation:
|
||||
- docs/workflow/verification.md
|
||||
- docs/architecture/README.md
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Investigate and fix the current src/test failures across stats CLI lifetime rebuild handling, immersion tracker lifetime rebuild idempotency, renderer test environment cleanup helpers, subtitle sidebar auto-follow behavior, and log retention pruning so the maintained test lanes pass again.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Stats CLI lifetime rebuild behavior passes the current regression coverage.
|
||||
- [x] #2 Immersion tracker lifetime rebuild backfill remains idempotent under the existing runtime test.
|
||||
- [x] #3 Renderer modal test helpers restore injected globals exactly to prior state.
|
||||
- [x] #4 Log pruning removes files older than the configured retention window deterministically.
|
||||
- [x] #5 Relevant targeted test files pass after the fixes.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Reproduce the failing specs in isolation to separate deterministic regressions from suite-order pollution.
|
||||
2. Fix source or test-helper logic for the three isolated failures: log retention cutoff, stats CLI lifetime rebuild timestamp handling, and subtitle sidebar initial jump behavior.
|
||||
3. Harden renderer modal cleanup regressions so tests verify descriptor restoration without assuming global window/document start absent.
|
||||
4. Re-run the targeted failing files, then the required verification gate for the touched areas and record results.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Targeted regressions fixed in log pruning, stats CLI lifetime logging/tests, subtitle sidebar auto-follow timing, and renderer global cleanup test isolation.
|
||||
|
||||
Verification: `bun test src/main/runtime/stats-cli-command.test.ts src/shared/log-files.test.ts src/renderer/modals/playlist-browser.test.ts src/renderer/modals/youtube-track-picker.test.ts src/renderer/modals/subtitle-sidebar.test.ts` passed.
|
||||
|
||||
Verification: `bun run test:src` still exits non-zero because of unrelated existing errors in `src/core/services/anilist/anilist-token-store.test.ts` (`Bun.serve is not a function`) plus one remaining non-task failure elsewhere; the originally reported regressions are green in the maintained lane.
|
||||
|
||||
User reported `test:full` still failing after the first regression pass. Reopened to clear the remaining `test:src` fail plus the existing unhandled test errors before handoff.
|
||||
|
||||
Verified final gate with `bun run test:launcher:unit:src` and `bun run test:full`; both pass after fixing the launcher AniSkip fallback title regression and the earlier src-lane regressions.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Stabilized the failing test regressions across source and launcher lanes. Fixed log pruning cutoff math under Bun BigInt mtimes, subtitle sidebar auto-follow timing, renderer global cleanup test isolation, stats CLI lifetime rebuild logging/tests, stats-server node:http fallback isolation, and launcher AniSkip fallback title resolution so basename titles beat generic parent directories while episode-only filenames still prefer the series directory. Verification passed with `bun test launcher/aniskip-metadata.test.ts`, `bun run test:launcher:unit:src`, and `bun run test:full`.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
id: TASK-275
|
||||
title: Patch high-severity audit findings with minimal dependency changes
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-04 04:45'
|
||||
updated_date: '2026-04-04 04:50'
|
||||
labels:
|
||||
- security
|
||||
- dependencies
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Update SubMiner's direct Electron runtime and vulnerable build-time transitive dependencies to patched versions using the smallest safe version moves. Keep electron-builder on the current pinned line unless verification shows a blocker. Verify that bun audit no longer reports the current high-severity findings and that the standard project gate still passes.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Electron is updated to a patched supported release on the current supported line with no broader dependency refresh
|
||||
- [x] #2 Vulnerable transitive packages @xmldom/xmldom, lodash, and picomatch resolve to patched versions via targeted dependency changes
|
||||
- [x] #3 `bun audit --audit-level high` no longer reports the currently listed high-severity findings
|
||||
- [x] #4 The default handoff verification gate passes, or any failure is documented with the exact command and error output
|
||||
- [x] #5 Any dependency or lockfile changes remain minimal and do not change the pinned electron-builder line unless required
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Update package.json with the smallest set of dependency changes: bump electron from ^37.10.3 to 39.8.6 and add overrides for @xmldom/xmldom 0.8.12, lodash 4.18.0, and picomatch 4.0.4 while leaving electron-builder pinned at 26.8.2.
|
||||
2. Refresh bun.lock with a lockfile-only install/update and confirm the resolved versions for electron, @xmldom/xmldom, lodash, and picomatch.
|
||||
3. Run bun audit --audit-level high and verify the current high-severity findings are gone.
|
||||
4. Run the default verification gate: bun run typecheck, bun run test:fast, bun run test:env, bun run build, bun run test:smoke:dist.
|
||||
5. If any verification step fails, capture the exact failing command and error, assess whether it is caused by the dependency updates, and stop without broadening scope.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Updated package.json to pin electron 39.8.6 and add overrides for @xmldom/xmldom 0.8.12, lodash 4.18.0, and picomatch 4.0.4 while keeping electron-builder pinned at 26.8.2.
|
||||
|
||||
Refreshed bun.lock with bun install and confirmed the patched versions resolved in the lockfile.
|
||||
|
||||
Verification passed: bun audit --audit-level high, bun run typecheck, bun run test:fast, bun run test:env, bun run build, bun run test:smoke:dist.
|
||||
|
||||
Added changelog fragment changes/patch-audit-dependencies.md for the security/dependency maintenance update. No internal docs or docs-site updates were needed because the change does not alter user-facing behavior, configuration, or workflows.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Cleared the reported high-severity audit findings with minimal dependency churn by pinning electron to 39.8.6 and overriding @xmldom/xmldom, lodash, and picomatch to patched versions. Kept electron-builder on 26.8.2. bun audit is clean and the full default handoff gate passed: typecheck, fast tests, env tests, build, and dist smoke tests.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
id: TASK-276
|
||||
title: Restore canonical SubMiner Linux app-id metadata
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-04 06:31'
|
||||
updated_date: '2026-04-04 06:34'
|
||||
labels:
|
||||
- bug
|
||||
- linux
|
||||
- electron
|
||||
dependencies: []
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Fix the Linux/Wayland packaged app metadata so the OS-facing desktop/app-id metadata stays canonical `SubMiner` instead of the lowercase npm package name `subminer`. Add regression coverage around packaged metadata so future Electron/runtime changes do not silently reintroduce the lowercase class/app-id behavior.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Top-level package metadata provides the canonical capitalized app name used by Electron runtime bootstrap on Linux
|
||||
- [x] #2 Packaged Linux app metadata resolves to `SubMiner`/`SubMiner.desktop` instead of lowercase `subminer`
|
||||
- [x] #3 Regression coverage fails before the fix and passes after it
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add a focused packaging/runtime regression test that reads the packaged app metadata source and asserts the Linux Electron bootstrap-visible fields resolve to canonical `SubMiner` / `SubMiner.desktop`.
|
||||
2. Run the targeted test first to capture the failing pre-fix state.
|
||||
3. Update top-level package metadata in `package.json` with the canonical Electron runtime-facing fields needed for Linux bootstrap.
|
||||
4. Re-run the targeted test and a lightweight packaging validation to confirm the packaged metadata now stays canonical.
|
||||
5. Record verification notes and complete the task if all acceptance criteria pass.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Added a regression in `src/release-workflow.test.ts` asserting top-level `productName` and `desktopName` stay canonical for Linux Electron runtime bootstrap. Verified the new test failed before the fix because both fields were missing from top-level package metadata.
|
||||
|
||||
Updated top-level `package.json` metadata with `productName: SubMiner` and `desktopName: SubMiner.desktop` so packaged `app.asar` exposes the canonical Linux startup identity Electron reads before app code runs.
|
||||
|
||||
Verification passed with `bun test src/release-workflow.test.ts`, `bun run build && ./node_modules/.bin/electron-builder --linux dir --publish never`, packaged `release/linux-unpacked/resources/app.asar` inspection showing `{ name: subminer, productName: SubMiner, desktopName: SubMiner.desktop }`, and `bun run changelog:lint`.
|
||||
|
||||
Ran the full default handoff gate after the targeted/package verification: `bun run typecheck`, `bun run test:fast`, `bun run test:env`, and `bun run test:smoke:dist` all passed.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Restored canonical Linux/Wayland app identity metadata by adding top-level Electron runtime fields to `package.json`: `productName: SubMiner` and `desktopName: SubMiner.desktop`. This fixes the packaged app metadata Electron reads before user code runs, so native Wayland compositors no longer need to derive the app-id/class from the lowercase npm package name alone.
|
||||
|
||||
Added a regression test in `src/release-workflow.test.ts` that asserts the runtime-visible top-level metadata stays canonical. The new test was run first and failed before the fix because `productName` was missing, then passed after the metadata update.
|
||||
|
||||
Verification: `bun test src/release-workflow.test.ts`; `bun run build && ./node_modules/.bin/electron-builder --linux dir --publish never`; inspected `release/linux-unpacked/resources/app.asar` and confirmed `productName: SubMiner` plus `desktopName: SubMiner.desktop`; `bun run changelog:lint`. Added changelog fragment `changes/2026-04-04-linux-app-id-metadata.md`.
|
||||
|
||||
Full default handoff gate also passed: `bun run typecheck`; `bun run test:fast`; `bun run test:env`; `bun run test:smoke:dist`.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
id: TASK-277
|
||||
title: Restore Linux shortcut-backed modal actions after Electron 39 upgrade
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-04-04 06:49'
|
||||
updated_date: '2026-04-04 07:08'
|
||||
labels:
|
||||
- bug
|
||||
- linux
|
||||
- electron
|
||||
- shortcuts
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Linux builds on Electron 39 no longer open the runtime options, Jimaku, or Subsync flows from their configured shortcuts (`Ctrl/Cmd+Shift+O`, `Ctrl+Shift+J`, `Ctrl+Alt+S`). Other renderer-driven modals like session help, subtitle sidebar, and playlist browser still work, which points to the Linux `globalShortcut` / overlay shortcut path rather than modal rendering in general. Investigate the Electron 39 regression and restore these Linux shortcut-triggered actions without regressing macOS or Windows behavior.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 On Linux, the configured runtime options, Jimaku, and Subsync shortcuts trigger their existing actions again under Electron 39.
|
||||
- [x] #2 The fix is covered by automated tests around the Linux shortcut/backend behavior that regressed.
|
||||
- [x] #3 A changelog fragment is added for the Linux shortcut regression fix.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Extend the special-command / mpv IPC command path to cover Jimaku alongside the existing runtime-options and subsync commands.
|
||||
2. Add tests for IPC command dispatch and any keybinding/session-help metadata affected by the new special command.
|
||||
3. Route Linux modal-opening shortcuts through the working mpv/IPC command path instead of depending on Electron globalShortcut delivery for those actions.
|
||||
4. Re-run targeted tests, then the handoff verification gate, and update the changelog/task summary to reflect the final root cause and fix.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
User approved plan; proceeding with test-first implementation.
|
||||
|
||||
Root cause: Linux startup unconditionally enabled Electron's GlobalShortcutsPortal path, so Electron 39 X11 sessions were routed through the portal-backed globalShortcut path even though that path should only be used for Wayland-style launches. The affected runtime-options, Jimaku, and Subsync actions all depend on that shortcut path, while renderer-driven modals did not regress.
|
||||
|
||||
User retested after the portal gating fix; issue persists. Updating investigation scope from portal selection alone to Linux overlay shortcut delivery/registration under Electron 39.
|
||||
|
||||
Revised fix path: Linux renderer now matches configured overlay shortcuts for runtime options, subsync, and Jimaku locally, then dispatches the existing mpv/IPC special-command route instead of depending on Electron globalShortcut delivery.
|
||||
|
||||
Added Jimaku mpv special command plumbing through IPC/runtime deps and exposed an overlay-runtime helper for opening the Jimaku modal from that IPC path.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Restored Linux shortcut-backed modal actions by routing runtime options, Subsync, and Jimaku through the working mpv/IPC special-command path. Added renderer-side Linux shortcut matching for configured overlay shortcuts, threaded a new Jimaku special command through IPC/runtime deps, refreshed configured shortcuts on hot reload, and covered the regression with IPC/runtime keyboard tests. Verification: bun test src/core/services/ipc-command.test.ts src/renderer/handlers/keyboard.test.ts src/main/runtime/ipc-mpv-command-main-deps.test.ts; bun run typecheck; bun run test:fast; bun run test:env; bun run build; bun run test:smoke:dist.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
id: TASK-278
|
||||
title: Prepare patch release 0.11.1
|
||||
status: Done
|
||||
assignee:
|
||||
- '@codex'
|
||||
created_date: '2026-04-04 07:16'
|
||||
updated_date: '2026-04-04 07:41'
|
||||
labels:
|
||||
- release
|
||||
dependencies: []
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Bump SubMiner from 0.11.0 to 0.11.1, run the local release checklist, and confirm whether the branch is ready for tagging/publishing.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 package.json is bumped to 0.11.1 without overwriting unrelated local metadata edits.
|
||||
- [x] #2 Release prep checks are run and summarized, including changelog/build verification and local build/test gates.
|
||||
- [x] #3 Any remaining release blockers are called out explicitly.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Generate release metadata with `bun run changelog:build --version 0.11.1 --date 2026-04-04`.
|
||||
2. Review the resulting `CHANGELOG.md` and `release/release-notes.md` content for the 0.11.1 section.
|
||||
3. Rerun the documented local release gate: `bun run changelog:check --version 0.11.1`, `bun run verify:config-example`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, and `bun run build`.
|
||||
4. Record results, remaining blockers, and ready-for-tagging status in the task notes/final summary.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Bumped package.json from 0.11.0 to 0.11.1 while preserving the existing local productName/desktopName edits.
|
||||
|
||||
Fixed the Linux shortcut changelog fragment metadata so changelog lint now passes and the previously failing CI cause is addressed locally.
|
||||
|
||||
Release checks run: bun run changelog:lint, bun run changelog:check --version 0.11.1, bun run verify:config-example, bun run typecheck, bun run test:fast, bun run test:env, bun run build. All passed except changelog:check, which is expected until pending fragments are built into CHANGELOG.md/release/release-notes.md.
|
||||
|
||||
Current release blocker: pending changelog fragments /changes/2026-04-04-linux-app-id-metadata.md and /changes/2026-04-04-linux-shortcut-portal-regression.md still need bun run changelog:build --version 0.11.1 --date 2026-04-04 before tagging.
|
||||
|
||||
Reopened to complete the remaining release-prep work: generate changelog/release notes artifacts, rerun the documented release gate, and confirm ready-for-tagging status.
|
||||
|
||||
Completed the remaining release-prep step with `bun run changelog:build --version 0.11.1 --date 2026-04-04`, which prepended `CHANGELOG.md`, generated `release/release-notes.md`, and removed the two pending `changes/*.md` fragments.
|
||||
|
||||
Reran the release gate after changelog generation: `bun run changelog:check --version 0.11.1`, `bun run verify:config-example`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, and `bun run build`; all passed.
|
||||
|
||||
Extra confidence checks also passed: `bun run changelog:lint`, `bun run test:smoke:dist`, and `gh run list --workflow CI --limit 5` shows the two latest `main` CI runs succeeded on 2026-04-04 after the earlier pre-fix failure.
|
||||
|
||||
`release/release-notes.md` is intentionally generated under the gitignored `release/` directory, so readiness evidence in git status is `CHANGELOG.md` plus deletion of the released change fragments.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Finished the 0.11.1 release prep by generating the changelog artifacts from the pending Linux fix fragments and re-running the full documented local release gate. `CHANGELOG.md` now contains the `v0.11.1 (2026-04-04)` section, `release/release-notes.md` was regenerated in the ignored `release/` directory, and the released `changes/2026-04-04-linux-app-id-metadata.md` plus `changes/2026-04-04-linux-shortcut-portal-regression.md` fragments were removed.
|
||||
|
||||
Verification results: `bun run changelog:check --version 0.11.1`, `bun run verify:config-example`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run changelog:lint`, and `bun run test:smoke:dist` all passed locally. Remote CI also looks green for the latest release-prep head: `gh run list --workflow CI --limit 5` showed successful `main` runs for `chore: prep 0.11.1 release` and `Change demo image link to GitHub asset` on 2026-04-04, with the earlier `fix: restore linux modal shortcuts` failure already superseded by later green runs.
|
||||
|
||||
Remaining manual release step: commit the generated release-prep changes if desired, tag `v0.11.1`, and push the commit plus tag when ready.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
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 -->
|
||||
13
bun.lock
13
bun.lock
@@ -18,7 +18,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"electron": "39.8.6",
|
||||
"electron": "^37.10.3",
|
||||
"electron-builder": "26.8.2",
|
||||
"esbuild": "^0.25.12",
|
||||
"prettier": "^3.8.1",
|
||||
@@ -27,12 +27,9 @@
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"@xmldom/xmldom": "0.8.12",
|
||||
"app-builder-lib": "26.8.2",
|
||||
"electron-builder-squirrel-windows": "26.8.2",
|
||||
"lodash": "4.18.0",
|
||||
"minimatch": "10.2.3",
|
||||
"picomatch": "4.0.4",
|
||||
"tar": "7.5.11",
|
||||
},
|
||||
"packages": {
|
||||
@@ -188,7 +185,7 @@
|
||||
|
||||
"@xhayper/discord-rpc": ["@xhayper/discord-rpc@1.3.3", "", { "dependencies": { "@discordjs/rest": "^2.6.1", "@vladfrangu/async_event_emitter": "^2.4.7", "discord-api-types": "^0.38.42", "ws": "^8.20.0" } }, "sha512-Ih48GHiua7TtZgKO+f0uZPhCeQqb84fY2qUys/oMh8UbUfiUkUJLVCmd/v2AK0/pV33euh0aqSXo7+9LiPSwGw=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||
|
||||
"abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
|
||||
|
||||
@@ -324,7 +321,7 @@
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
"electron": ["electron@39.8.6", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-uWX6Jh5LmwL13VwOSKBjebI+ck+03GOwc8V2Sgbmr9pJVJ/cHfli/PkjXuRDr+hq+SLHQuT9mGHSIfScebApRA=="],
|
||||
"electron": ["electron@37.10.3", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g=="],
|
||||
|
||||
"electron-builder": ["electron-builder@26.8.2", "", { "dependencies": { "app-builder-lib": "26.8.2", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-ieiiXPdgH3qrG6lcvy2mtnI5iEmAopmLuVRMSJ5j40weU0tgpNx0OAk9J5X5nnO0j9+KIkxHzwFZVUDk1U3aGw=="],
|
||||
|
||||
@@ -482,7 +479,7 @@
|
||||
|
||||
"libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="],
|
||||
|
||||
"lodash": ["lodash@4.18.0", "", {}, "sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA=="],
|
||||
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
||||
|
||||
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
|
||||
|
||||
@@ -572,7 +569,7 @@
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
|
||||
|
||||
|
||||
6
changes/251-docs-site-sidebar.md
Normal file
6
changes/251-docs-site-sidebar.md
Normal file
@@ -0,0 +1,6 @@
|
||||
type: docs
|
||||
area: docs-site
|
||||
|
||||
- Added a dedicated Subtitle Sidebar guide and linked it from the homepage and configuration docs.
|
||||
- Linked Jimaku integration from the homepage to its dedicated docs page.
|
||||
- Refreshed docs-site theme tokens and hover/selection styling for the updated pages.
|
||||
5
changes/252-youtube-playback-socket-path.md
Normal file
5
changes/252-youtube-playback-socket-path.md
Normal file
@@ -0,0 +1,5 @@
|
||||
type: fixed
|
||||
area: main
|
||||
|
||||
- Resolve the YouTube playback socket path lazily so startup honors CLI and config overrides.
|
||||
- Add regression coverage for the lazy socket-path lookup during Windows mpv startup.
|
||||
5
changes/253-aur-release-best-effort.md
Normal file
5
changes/253-aur-release-best-effort.md
Normal file
@@ -0,0 +1,5 @@
|
||||
type: internal
|
||||
area: release
|
||||
|
||||
- Retried AUR clone and push operations in the tagged release workflow.
|
||||
- Kept GitHub Releases green when AUR publish flakes and needs manual follow-up.
|
||||
5
changes/259-texthooker-integrated-startup.md
Normal file
5
changes/259-texthooker-integrated-startup.md
Normal file
@@ -0,0 +1,5 @@
|
||||
type: fixed
|
||||
area: main
|
||||
|
||||
- Keep integrated `--start --texthooker` launches on the full app-ready startup path so the texthooker page and websocket servers start together during normal playback startup.
|
||||
- Stop the mpv/plugin auto-start flow from spawning a separate standalone texthooker helper during normal `subminer <video>` launches.
|
||||
6
changes/260-main-runtime-refactor.md
Normal file
6
changes/260-main-runtime-refactor.md
Normal file
@@ -0,0 +1,6 @@
|
||||
type: internal
|
||||
area: main
|
||||
|
||||
- Split `src/main.ts` into domain runtime wrappers and startup sequencing helpers.
|
||||
- Removed the last direct `src/main/runtime/*-main-deps.ts` imports from `src/main.ts`.
|
||||
- Kept startup behavior and IPC contracts stable while reducing composition-root size.
|
||||
5
changes/260-playlist-browser.md
Normal file
5
changes/260-playlist-browser.md
Normal file
@@ -0,0 +1,5 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- Added a playlist browser overlay modal for browsing sibling video files and the live mpv queue during playback.
|
||||
- Added the default `Ctrl+Alt+P` keybinding to open the playlist browser and manage queue order without leaving playback.
|
||||
5
changes/261-macos-overlay-passthrough.md
Normal file
5
changes/261-macos-overlay-passthrough.md
Normal file
@@ -0,0 +1,5 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Keep tracked macOS visible overlays click-through by default so subtitle sidebar passthrough works immediately without requiring a subtitle hover cycle first.
|
||||
- Add regression coverage for the macOS visible-overlay passthrough default.
|
||||
5
changes/262-anilist-post-watch-dedupe.md
Normal file
5
changes/262-anilist-post-watch-dedupe.md
Normal file
@@ -0,0 +1,5 @@
|
||||
type: fixed
|
||||
area: anilist
|
||||
|
||||
- Stop AniList post-watch from sending a second progress update when the current episode was already satisfied by a ready retry item in the same watch-completion pass.
|
||||
- Add regression coverage for the retry-queue plus live-update duplicate path.
|
||||
6
changes/267-yomitan-kiku-popup.md
Normal file
6
changes/267-yomitan-kiku-popup.md
Normal file
@@ -0,0 +1,6 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Fixed Kiku duplicate grouping to reuse duplicate note IDs from both generic sentence-card creation and Yomitan popup mining instead of running extra duplicate scans after add.
|
||||
- Fixed the Yomitan popup mining flow to add cards in the background while keeping the stock popup progress feedback, then pause playback and close the lookup popup before the Kiku merge modal opens.
|
||||
- Fixed configured subtitle-jump keybindings so backward and forward subtitle seeks keep playback paused when invoked from a paused state.
|
||||
@@ -1,4 +0,0 @@
|
||||
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.
|
||||
@@ -18,7 +18,7 @@
|
||||
// ==========================================
|
||||
"texthooker": {
|
||||
"launchAtStartup": true, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
||||
"openBrowser": false // Open browser setting. Values: true | false
|
||||
"openBrowser": true // Open browser setting. Values: true | false
|
||||
}, // Configure texthooker startup launch and browser opening behavior.
|
||||
|
||||
// ==========================================
|
||||
@@ -58,7 +58,7 @@
|
||||
// Override controller.buttonIndices when your pad reports non-standard raw button numbers.
|
||||
// ==========================================
|
||||
"controller": {
|
||||
"enabled": false, // Enable overlay controller support through the Chrome Gamepad API. Values: true | false
|
||||
"enabled": true, // Enable overlay controller support through the Chrome Gamepad API. Values: true | false
|
||||
"preferredGamepadId": "", // Preferred controller id saved from the controller config modal.
|
||||
"preferredGamepadLabel": "", // Preferred controller display label saved for diagnostics.
|
||||
"smoothScroll": true, // Use smooth scrolling for controller-driven popup scroll input. Values: true | false
|
||||
@@ -187,7 +187,7 @@
|
||||
// ==========================================
|
||||
// Secondary Subtitles
|
||||
// Dual subtitle track options.
|
||||
// Used by managed subtitle loading as secondary language preferences for local and YouTube playback.
|
||||
// Used by the YouTube subtitle loading flow as secondary language preferences.
|
||||
// Hot-reload: defaultMode updates live while SubMiner is running.
|
||||
// ==========================================
|
||||
"secondarySub": {
|
||||
@@ -225,7 +225,7 @@
|
||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
||||
"autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||
"autoPauseVideoOnYomitanPopup": false, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
||||
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
||||
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
|
||||
@@ -290,7 +290,7 @@
|
||||
// Hot-reload: subtitle sidebar changes apply live without restarting SubMiner.
|
||||
// ==========================================
|
||||
"subtitleSidebar": {
|
||||
"enabled": true, // Enable the subtitle sidebar feature for parsed subtitle sources. Values: true | false
|
||||
"enabled": false, // Enable the subtitle sidebar feature for parsed subtitle sources. Values: true | false
|
||||
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
|
||||
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
|
||||
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
|
||||
@@ -330,7 +330,7 @@
|
||||
// Most other AnkiConnect settings still require restart.
|
||||
// ==========================================
|
||||
"ankiConnect": {
|
||||
"enabled": true, // Enable AnkiConnect integration. Values: true | false
|
||||
"enabled": false, // Enable AnkiConnect integration. Values: true | false
|
||||
"url": "http://127.0.0.1:8765", // Url setting.
|
||||
"pollingRate": 3000, // Polling interval in milliseconds.
|
||||
"proxy": {
|
||||
@@ -415,14 +415,14 @@
|
||||
|
||||
// ==========================================
|
||||
// YouTube Playback Settings
|
||||
// Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
||||
// Defaults for SubMiner YouTube subtitle loading and languages.
|
||||
// ==========================================
|
||||
"youtube": {
|
||||
"primarySubLanguages": [
|
||||
"ja",
|
||||
"jpn"
|
||||
] // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
|
||||
}, // Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
||||
] // Comma-separated primary subtitle language priority for YouTube auto-loading.
|
||||
}, // Defaults for SubMiner YouTube subtitle loading and languages.
|
||||
|
||||
// ==========================================
|
||||
// Anilist
|
||||
@@ -458,15 +458,6 @@
|
||||
"externalProfilePath": "" // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
|
||||
}, // Optional external Yomitan profile integration.
|
||||
|
||||
// ==========================================
|
||||
// MPV Launcher
|
||||
// Optional mpv.exe override for Windows playback entry points.
|
||||
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
||||
// ==========================================
|
||||
"mpv": {
|
||||
"executablePath": "" // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||
}, // Optional mpv.exe override for Windows playback entry points.
|
||||
|
||||
// ==========================================
|
||||
// Jellyfin
|
||||
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||
@@ -506,7 +497,7 @@
|
||||
// Uses official SubMiner Discord app assets for polished card visuals.
|
||||
// ==========================================
|
||||
"discordPresence": {
|
||||
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal".
|
||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||
@@ -553,6 +544,6 @@
|
||||
"markWatchedKey": "KeyW", // Key code to mark the current video as watched and advance to the next playlist entry.
|
||||
"serverPort": 6969, // Port for the stats HTTP server.
|
||||
"autoStartServer": true, // Automatically start the stats server on launch. Values: true | false
|
||||
"autoOpenBrowser": false // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
|
||||
"autoOpenBrowser": true // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
|
||||
} // Local immersion stats dashboard served on localhost and available as an in-app overlay.
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ src/
|
||||
handlers/ # Keyboard/mouse interaction modules
|
||||
modals/ # Jimaku/Kiku/subsync/runtime-options/session-help modals
|
||||
positioning/ # Subtitle position controller (drag-to-reposition)
|
||||
window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS, Windows)
|
||||
window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS)
|
||||
jimaku/ # Jimaku API integration helpers
|
||||
subsync/ # Subtitle sync (alass/ffsubsync) helpers
|
||||
subtitle/ # Subtitle processing utilities
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
# 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`.
|
||||
- Fixed the Windows mpv idle launch so loading a video after opening the shortcut keeps mpv in the SubMiner-managed session and auto-starts the overlay.
|
||||
- Added a blank-by-default `mpv.executablePath` config override for Windows playback when mpv is not on `PATH`, exposed in first-run setup.
|
||||
- Fixed Kiku duplicate grouping to reuse duplicate note IDs from both sentence-card creation and Yomitan popup mining, with background card addition and proper merge-modal sequencing.
|
||||
- Fixed configured subtitle-jump keybindings to keep playback paused when invoked from a paused state.
|
||||
- Fixed managed local subtitle auto-selection to reuse configured language priorities instead of staying on mpv's initial `sid=auto` guess.
|
||||
- Kept tracked macOS visible overlays click-through by default so subtitle sidebar passthrough works immediately.
|
||||
- Stopped AniList post-watch from sending duplicate progress updates when already satisfied by a retry item.
|
||||
- Kept integrated `--start --texthooker` launches on the full app-ready startup path.
|
||||
- Honored `SUBMINER_YTDLP_BIN` consistently across all YouTube flows (playback URL resolution, track probing, subtitle downloads, metadata probing).
|
||||
- Added `windows` as a recognized launcher backend option and auto-detection target.
|
||||
- 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.
|
||||
@@ -31,7 +8,6 @@
|
||||
- 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.
|
||||
@@ -39,7 +15,6 @@
|
||||
- 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.
|
||||
@@ -47,13 +22,11 @@
|
||||
- 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.
|
||||
@@ -68,7 +41,6 @@
|
||||
- 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.
|
||||
@@ -80,7 +52,6 @@
|
||||
- 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.
|
||||
@@ -90,20 +61,16 @@
|
||||
- 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.
|
||||
@@ -111,7 +78,6 @@
|
||||
- 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.
|
||||
@@ -121,13 +87,11 @@
|
||||
- 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.
|
||||
@@ -136,14 +100,12 @@
|
||||
- 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.
|
||||
@@ -154,7 +116,6 @@
|
||||
- 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.
|
||||
@@ -163,36 +124,30 @@
|
||||
- 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.
|
||||
|
||||
@@ -252,7 +252,7 @@ See `config.example.jsonc` for detailed configuration options.
|
||||
{
|
||||
"texthooker": {
|
||||
"launchAtStartup": true,
|
||||
"openBrowser": false
|
||||
"openBrowser": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -260,7 +260,7 @@ See `config.example.jsonc` for detailed configuration options.
|
||||
| Option | Values | Description |
|
||||
| ---------------- | --------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `launchAtStartup`| `true`, `false` | Start texthooker automatically with SubMiner startup (default: `true`) |
|
||||
| `openBrowser` | `true`, `false` | Open browser tab when texthooker starts (default: `false`) |
|
||||
| `openBrowser` | `true`, `false` | Open browser tab when texthooker starts (default: `true`) |
|
||||
|
||||
## Subtitle Display
|
||||
|
||||
@@ -307,7 +307,7 @@ See `config.example.jsonc` for detailed configuration options.
|
||||
| `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) |
|
||||
| `preserveLineBreaks` | boolean | Preserve line breaks in visible overlay subtitle rendering (`false` by default). Enable to mirror mpv line layout. |
|
||||
| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text, then resume on leave (`true` by default). |
|
||||
| `autoPauseVideoOnYomitanPopup` | boolean | Pause playback while the Yomitan popup is open, then resume when the popup closes (`true` by default). |
|
||||
| `autoPauseVideoOnYomitanPopup` | boolean | Pause playback while the Yomitan popup is open, then resume when the popup closes (`false` by default). |
|
||||
| `hoverTokenColor` | string | Hex color used for hovered subtitle token highlight in mpv (default: catppuccin mauve) |
|
||||
| `hoverTokenBackgroundColor` | string | CSS color used for hovered subtitle token background highlight (default: semi-transparent dark) |
|
||||
| `nameMatchEnabled` | boolean | Enable subtitle token coloring for matches from the SubMiner character dictionary (`true` by default) |
|
||||
@@ -355,7 +355,7 @@ Configure the parsed-subtitle sidebar modal.
|
||||
```json
|
||||
{
|
||||
"subtitleSidebar": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"autoOpen": false,
|
||||
"layout": "overlay",
|
||||
"toggleKey": "Backslash",
|
||||
@@ -369,7 +369,7 @@ Configure the parsed-subtitle sidebar modal.
|
||||
|
||||
| Option | Values | Description |
|
||||
| --------------------------- | ---------------- | -------------------------------------------------------------------------------- |
|
||||
| `enabled` | boolean | Enable subtitle sidebar support (`true` by default) |
|
||||
| `enabled` | boolean | Enable subtitle sidebar support (`false` by default) |
|
||||
| `autoOpen` | boolean | Open sidebar automatically on overlay startup (`false` by default) |
|
||||
| `layout` | string | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space to mimic browser-like layout |
|
||||
| `toggleKey` | string | `KeyboardEvent.code` used to open/close the sidebar (default: `"Backslash"`) |
|
||||
@@ -448,8 +448,6 @@ See `config.example.jsonc` for detailed configuration options.
|
||||
| `autoLoadSecondarySub` | `true`, `false` | Auto-detect and load matching secondary subtitle track |
|
||||
| `defaultMode` | `"hidden"`, `"visible"`, `"hover"` | Initial display mode (default: `"hover"`) |
|
||||
|
||||
`secondarySub.secondarySubLanguages` also acts as the fallback secondary-language priority for managed startup subtitle selection on local playback and YouTube playback.
|
||||
|
||||
**Display modes:**
|
||||
|
||||
- **hidden** — Secondary subtitles not shown
|
||||
@@ -848,7 +846,7 @@ This example is intentionally compact. The option table below documents availabl
|
||||
|
||||
| Option | Values | Description |
|
||||
| --------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `true`) |
|
||||
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `false`) |
|
||||
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
|
||||
| `pollingRate` | number (ms) | How often to check for new cards in polling mode (default: `3000`; ignored for direct proxy `addNote`/`addNotes` updates) |
|
||||
| `proxy.enabled` | `true`, `false` | Enable local AnkiConnect-compatible proxy for push-based auto-enrichment (default: `true`) |
|
||||
@@ -1197,7 +1195,7 @@ Jellyfin remote auto-connect runs only when all three are `true`: `jellyfin.enab
|
||||
|
||||
### Discord Rich Presence
|
||||
|
||||
Discord Rich Presence is enabled by default. SubMiner publishes a polished activity card that reflects current media title, playback state, and session timer unless you turn it off.
|
||||
Discord Rich Presence is optional and disabled by default. When enabled, SubMiner publishes a polished activity card that reflects current media title, playback state, and session timer.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1212,14 +1210,14 @@ Discord Rich Presence is enabled by default. SubMiner publishes a polished activ
|
||||
|
||||
| Option | Values | Description |
|
||||
| ------------------ | ------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| `enabled` | `true`, `false` | Enable Discord Rich Presence updates (default: `true`) |
|
||||
| `enabled` | `true`, `false` | Enable Discord Rich Presence updates (default: `false`) |
|
||||
| `presenceStyle` | `"default"`, `"meme"`, `"japanese"`, `"minimal"` | Card text preset (default: `"default"`) |
|
||||
| `updateIntervalMs` | number | Minimum interval between activity updates in milliseconds |
|
||||
| `debounceMs` | number | Debounce window for bursty playback events in milliseconds |
|
||||
|
||||
Setup steps:
|
||||
|
||||
1. Leave `discordPresence.enabled` as `true` or set it explicitly if you previously disabled it.
|
||||
1. Set `discordPresence.enabled` to `true`.
|
||||
2. Optionally set `discordPresence.presenceStyle` to choose a card text preset.
|
||||
3. Restart SubMiner.
|
||||
|
||||
@@ -1323,7 +1321,7 @@ Configure the local stats UI served from SubMiner and the in-app stats overlay t
|
||||
"toggleKey": "Backquote",
|
||||
"serverPort": 6969,
|
||||
"autoStartServer": true,
|
||||
"autoOpenBrowser": false
|
||||
"autoOpenBrowser": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1333,7 +1331,7 @@ Configure the local stats UI served from SubMiner and the in-app stats overlay t
|
||||
| `toggleKey` | Electron key code | Overlay-local key code used to toggle the stats overlay. Default `Backquote`. |
|
||||
| `serverPort` | integer | Localhost port for the browser stats UI. Default `6969`. |
|
||||
| `autoStartServer` | `true`, `false` | Start the local stats HTTP server automatically once immersion tracking is active. Default `true`. |
|
||||
| `autoOpenBrowser` | `true`, `false` | When `subminer stats` starts the server on demand, also open the dashboard in your default browser. Default `false`. |
|
||||
| `autoOpenBrowser` | `true`, `false` | When `subminer stats` starts the server on demand, also open the dashboard in your default browser. Default `true`. |
|
||||
|
||||
Usage notes:
|
||||
|
||||
@@ -1344,7 +1342,7 @@ Usage notes:
|
||||
|
||||
### YouTube Playback Settings
|
||||
|
||||
Set defaults used by managed subtitle auto-selection and the `subminer` launcher YouTube flow:
|
||||
Set defaults used by the `subminer` launcher for YouTube subtitle loading:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1356,7 +1354,7 @@ Set defaults used by managed subtitle auto-selection and the `subminer` launcher
|
||||
|
||||
| Option | Values | Description |
|
||||
| --------------------- | -------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `primarySubLanguages` | string[] | Primary subtitle language priority for managed subtitle auto-selection (default `["ja", "jpn"]`) |
|
||||
| `primarySubLanguages` | string[] | Primary subtitle language priority for YouTube auto-loading (default `["ja", "jpn"]`) |
|
||||
|
||||
Current launcher behavior:
|
||||
|
||||
@@ -1372,7 +1370,6 @@ Language targets are derived from subtitle config:
|
||||
|
||||
- primary track: `youtube.primarySubLanguages` (falls back to `["ja","jpn"]`)
|
||||
- secondary track: `secondarySub.secondarySubLanguages` (falls back to English when empty)
|
||||
- Local playback uses the same priorities after mpv reports subtitle track metadata, so sidecar/internal mixed sets can override an incorrect initial `sid=auto` pick.
|
||||
- Tracks are resolved and loaded before mpv starts; the older launcher mode switch has been removed.
|
||||
|
||||
Precedence for launcher defaults is: CLI flag > environment variable > `config.jsonc` > built-in default.
|
||||
|
||||
@@ -72,7 +72,7 @@ Stats server config lives under `stats`:
|
||||
"toggleKey": "Backquote",
|
||||
"serverPort": 6969,
|
||||
"autoStartServer": true,
|
||||
"autoOpenBrowser": false
|
||||
"autoOpenBrowser": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
# 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`) |
|
||||
| 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. |
|
||||
| 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 |
|
||||
|
||||
@@ -34,72 +21,14 @@ The `subminer` launcher script handles step 1 automatically. If you launch mpv y
|
||||
- Sway (uses `swaymsg`)
|
||||
- X11 (uses `xdotool` and `xwininfo`)
|
||||
|
||||
<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
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
```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.
|
||||
**Windows** — Windows 10 or later. Install `mpv` and keep it available on `PATH`; SubMiner's packaged build handles window tracking directly.
|
||||
|
||||
### Optional Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| ----------------- | ------------------------------------------------------------- |
|
||||
| fzf | Terminal-based video picker (default) |
|
||||
| rofi | GUI-based video picker |
|
||||
| chafa | Thumbnail previews in fzf |
|
||||
@@ -128,31 +57,20 @@ makepkg -si
|
||||
|
||||
### AppImage (Recommended)
|
||||
|
||||
Download the latest AppImage and the `subminer` launcher from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest).
|
||||
|
||||
**Step 1 — Install Bun** (required for the launcher):
|
||||
Download the latest AppImage from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
||||
|
||||
```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 and install the subminer launcher (recommended)
|
||||
# Download subminer wrapper script
|
||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer
|
||||
chmod +x ~/.local/bin/subminer
|
||||
|
||||
```
|
||||
|
||||
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.
|
||||
The `subminer` wrapper uses a Bun shebang (`#!/usr/bin/env bun`), so [Bun](https://bun.sh) must be installed and available on `PATH`.
|
||||
|
||||
### From Source
|
||||
|
||||
@@ -185,33 +103,9 @@ A **ZIP** artifact is also available as a fallback — unzip and drag `SubMiner.
|
||||
Install dependencies using Homebrew:
|
||||
|
||||
```bash
|
||||
brew install mpv ffmpeg
|
||||
# Optional but recommended if you use N+1, JLPT, or frequency annotations
|
||||
brew install mecab mecab-ipadic
|
||||
brew install mpv 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
|
||||
@@ -229,44 +123,19 @@ 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 Settings** → **Privacy & Security** → **Accessibility**
|
||||
2. Enable SubMiner in the list (add it if it does not appear)
|
||||
1. Open **System Preferences** → **Security & Privacy** → **Privacy** tab
|
||||
2. Select **Accessibility** from the left sidebar
|
||||
3. Add SubMiner to the list
|
||||
|
||||
Without this permission, window tracking will not work and the overlay won't follow the mpv window.
|
||||
|
||||
### macOS Usage Notes
|
||||
|
||||
**Launching with the `subminer` launcher (recommended):**
|
||||
|
||||
```bash
|
||||
subminer video.mkv
|
||||
```
|
||||
|
||||
The launcher handles the IPC socket and SubMiner defaults automatically. If you prefer to launch mpv manually:
|
||||
**Launching MPV with IPC:**
|
||||
|
||||
```bash
|
||||
mpv --input-ipc-server=/tmp/subminer-socket video.mkv
|
||||
@@ -291,17 +160,6 @@ 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):
|
||||
@@ -309,24 +167,14 @@ 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
|
||||
|
||||
### Getting Started on Windows
|
||||
Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still required for media extraction, and MeCab remains optional.
|
||||
|
||||
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:
|
||||
### Windows Usage Notes
|
||||
|
||||
```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`.
|
||||
- Launch `SubMiner.exe` once to let the first-run setup flow seed `%APPDATA%\\SubMiner\\config.jsonc`, offer mpv plugin installation, open bundled Yomitan settings, and optionally create `SubMiner mpv` Start Menu/Desktop shortcuts.
|
||||
- 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.
|
||||
|
||||
### From Source (Windows)
|
||||
|
||||
@@ -334,14 +182,10 @@ The shortcut and `--launch-mpv` pass SubMiner's default IPC socket and subtitle
|
||||
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
|
||||
```
|
||||
|
||||
@@ -374,87 +218,7 @@ cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
||||
# make install-plugin
|
||||
```
|
||||
|
||||
See [MPV Plugin](/mpv-plugin) for the full configuration reference, keybindings, script messages, and binary auto-detection details.
|
||||
|
||||
## Anki Setup (Recommended)
|
||||
|
||||
If you plan to mine Anki cards (the primary use case for most users):
|
||||
|
||||
1. Install [Anki](https://apps.ankiweb.net/).
|
||||
2. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on — open Anki, go to **Tools → Add-ons → Get Add-ons**, enter code `2055492159`.
|
||||
3. Restart Anki and keep it running while using SubMiner.
|
||||
|
||||
AnkiConnect listens on `http://127.0.0.1:8765` by default. SubMiner will connect to it automatically with no extra config needed for basic card creation.
|
||||
|
||||
For enrichment configuration (sentence, audio, screenshot fields), see [Anki Integration](/anki-integration).
|
||||
|
||||
## First-Run Setup
|
||||
|
||||
Run the setup wizard to create a default config and finish initial configuration. You do **not** need to create the config manually — SubMiner handles it.
|
||||
|
||||
```bash
|
||||
subminer app --setup
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
|
||||
|
||||
The setup popup walks you through:
|
||||
|
||||
- **Config file**: auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
|
||||
- **mpv plugin**: install the bundled Lua plugin for in-player keybindings
|
||||
- **Yomitan dictionaries**: import at least one dictionary so lookups work
|
||||
- **Windows shortcut** _(Windows only)_: optionally create a `SubMiner mpv` Start Menu/Desktop shortcut
|
||||
|
||||
The `Finish setup` button stays disabled until the plugin is installed and at least one dictionary is imported. Once you finish, SubMiner will not show the popup again.
|
||||
|
||||
> [!TIP]
|
||||
> You can re-open the setup popup at any time with `subminer app --setup` or `SubMiner.AppImage --setup`.
|
||||
|
||||
Once setup is complete, play a video to verify everything works:
|
||||
|
||||
```bash
|
||||
subminer video.mkv
|
||||
```
|
||||
|
||||
You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay.
|
||||
|
||||
<details>
|
||||
<summary><b>More launch examples</b></summary>
|
||||
|
||||
```bash
|
||||
# Optional explicit overlay start for setups with plugin auto_start=no
|
||||
subminer --start video.mkv
|
||||
|
||||
# Useful launch modes for troubleshooting
|
||||
subminer --log-level debug video.mkv
|
||||
SubMiner.AppImage --start --log-level debug
|
||||
|
||||
# Or with direct AppImage control
|
||||
SubMiner.AppImage --background # Background tray service mode
|
||||
SubMiner.AppImage --start
|
||||
SubMiner.AppImage --start --dev
|
||||
SubMiner.AppImage --help # Show all CLI options
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Verify Setup
|
||||
|
||||
After completing first-run setup, run the built-in diagnostic to confirm everything is in place:
|
||||
|
||||
```bash
|
||||
subminer doctor
|
||||
```
|
||||
|
||||
This checks for the app binary, mpv, ffmpeg, config file, and socket path. Fix any failures before continuing.
|
||||
|
||||
> [!NOTE]
|
||||
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly. Replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below.
|
||||
|
||||
## Optional Extras
|
||||
|
||||
### Rofi Theme (Linux Only)
|
||||
## Rofi Theme (Linux Only)
|
||||
|
||||
SubMiner ships a default rofi theme at `assets/themes/subminer.rasi`.
|
||||
|
||||
@@ -470,4 +234,32 @@ 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
|
||||
|
||||
After installing, confirm SubMiner is working:
|
||||
|
||||
On Windows, replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below.
|
||||
|
||||
```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
|
||||
|
||||
# Optional explicit overlay start for setups with plugin auto_start=no
|
||||
subminer --start video.mkv
|
||||
|
||||
# Useful launch modes for troubleshooting
|
||||
subminer --log-level debug video.mkv
|
||||
SubMiner.AppImage --start --log-level debug
|
||||
|
||||
# Or with direct AppImage control
|
||||
SubMiner.AppImage --background # Background tray service mode
|
||||
SubMiner.AppImage --start
|
||||
SubMiner.AppImage --start --dev
|
||||
SubMiner.AppImage --help # Show all CLI options
|
||||
```
|
||||
|
||||
You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay.
|
||||
|
||||
Next: [Usage](/usage) — learn about the `subminer` wrapper, keybindings, and YouTube playback.
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
# Launcher Script
|
||||
|
||||
The `subminer` launcher is an all-in-one script that handles video selection, mpv startup, and overlay management. It is the recommended way to use SubMiner on Linux and macOS because it guarantees mpv is launched with the correct IPC socket and SubMiner defaults. It's a Bun script distributed as a release asset alongside the AppImage and DMG.
|
||||
|
||||
::: 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.
|
||||
:::
|
||||
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.
|
||||
|
||||
## Video Picker
|
||||
|
||||
@@ -102,7 +98,7 @@ Use `subminer <subcommand> -h` for command-specific help.
|
||||
| `-T, --no-texthooker` | Disable texthooker server |
|
||||
| `-p, --profile` | mpv profile name (default: `subminer`) |
|
||||
| `-a, --args` | Pass additional mpv arguments as a quoted string |
|
||||
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`, `macos`, `windows`) |
|
||||
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`) |
|
||||
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) |
|
||||
| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) |
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ The visible overlay renders subtitles as tokenized hoverable word spans. Each wo
|
||||
|
||||
- Word-level hover targets for Yomitan lookup
|
||||
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
|
||||
- Auto pause/resume while the Yomitan popup is open (enabled by default via `subtitleStyle.autoPauseVideoOnYomitanPopup`)
|
||||
- Optional pause while the Yomitan popup is open (`subtitleStyle.autoPauseVideoOnYomitanPopup`)
|
||||
- Right-click to pause/resume
|
||||
- Right-click + drag to reposition subtitles
|
||||
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
|
||||
|
||||
@@ -79,7 +79,7 @@ texthooker_enabled=yes
|
||||
# Port for the texthooker server.
|
||||
texthooker_port=5174
|
||||
|
||||
# Window manager backend: auto, hyprland, sway, x11, macos, windows.
|
||||
# Window manager backend: auto, hyprland, sway, x11, macos.
|
||||
backend=auto
|
||||
|
||||
# Start the overlay automatically when a file is loaded.
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
// ==========================================
|
||||
"texthooker": {
|
||||
"launchAtStartup": true, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
||||
"openBrowser": false // Open browser setting. Values: true | false
|
||||
"openBrowser": true // Open browser setting. Values: true | false
|
||||
}, // Configure texthooker startup launch and browser opening behavior.
|
||||
|
||||
// ==========================================
|
||||
@@ -58,7 +58,7 @@
|
||||
// Override controller.buttonIndices when your pad reports non-standard raw button numbers.
|
||||
// ==========================================
|
||||
"controller": {
|
||||
"enabled": false, // Enable overlay controller support through the Chrome Gamepad API. Values: true | false
|
||||
"enabled": true, // Enable overlay controller support through the Chrome Gamepad API. Values: true | false
|
||||
"preferredGamepadId": "", // Preferred controller id saved from the controller config modal.
|
||||
"preferredGamepadLabel": "", // Preferred controller display label saved for diagnostics.
|
||||
"smoothScroll": true, // Use smooth scrolling for controller-driven popup scroll input. Values: true | false
|
||||
@@ -187,7 +187,7 @@
|
||||
// ==========================================
|
||||
// Secondary Subtitles
|
||||
// Dual subtitle track options.
|
||||
// Used by managed subtitle loading as secondary language preferences for local and YouTube playback.
|
||||
// Used by the YouTube subtitle loading flow as secondary language preferences.
|
||||
// Hot-reload: defaultMode updates live while SubMiner is running.
|
||||
// ==========================================
|
||||
"secondarySub": {
|
||||
@@ -225,7 +225,7 @@
|
||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
||||
"autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||
"autoPauseVideoOnYomitanPopup": false, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
||||
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
||||
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
|
||||
@@ -290,7 +290,7 @@
|
||||
// Hot-reload: subtitle sidebar changes apply live without restarting SubMiner.
|
||||
// ==========================================
|
||||
"subtitleSidebar": {
|
||||
"enabled": true, // Enable the subtitle sidebar feature for parsed subtitle sources. Values: true | false
|
||||
"enabled": false, // Enable the subtitle sidebar feature for parsed subtitle sources. Values: true | false
|
||||
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
|
||||
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
|
||||
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
|
||||
@@ -330,7 +330,7 @@
|
||||
// Most other AnkiConnect settings still require restart.
|
||||
// ==========================================
|
||||
"ankiConnect": {
|
||||
"enabled": true, // Enable AnkiConnect integration. Values: true | false
|
||||
"enabled": false, // Enable AnkiConnect integration. Values: true | false
|
||||
"url": "http://127.0.0.1:8765", // Url setting.
|
||||
"pollingRate": 3000, // Polling interval in milliseconds.
|
||||
"proxy": {
|
||||
@@ -415,14 +415,14 @@
|
||||
|
||||
// ==========================================
|
||||
// YouTube Playback Settings
|
||||
// Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
||||
// Defaults for SubMiner YouTube subtitle loading and languages.
|
||||
// ==========================================
|
||||
"youtube": {
|
||||
"primarySubLanguages": [
|
||||
"ja",
|
||||
"jpn"
|
||||
] // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
|
||||
}, // Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
||||
] // Comma-separated primary subtitle language priority for YouTube auto-loading.
|
||||
}, // Defaults for SubMiner YouTube subtitle loading and languages.
|
||||
|
||||
// ==========================================
|
||||
// Anilist
|
||||
@@ -458,15 +458,6 @@
|
||||
"externalProfilePath": "" // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
|
||||
}, // Optional external Yomitan profile integration.
|
||||
|
||||
// ==========================================
|
||||
// MPV Launcher
|
||||
// Optional mpv.exe override for Windows playback entry points.
|
||||
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
||||
// ==========================================
|
||||
"mpv": {
|
||||
"executablePath": "" // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||
}, // Optional mpv.exe override for Windows playback entry points.
|
||||
|
||||
// ==========================================
|
||||
// Jellyfin
|
||||
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||
@@ -506,7 +497,7 @@
|
||||
// Uses official SubMiner Discord app assets for polished card visuals.
|
||||
// ==========================================
|
||||
"discordPresence": {
|
||||
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal".
|
||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||
@@ -553,6 +544,6 @@
|
||||
"markWatchedKey": "KeyW", // Key code to mark the current video as watched and advance to the next playlist entry.
|
||||
"serverPort": 6969, // Port for the stats HTTP server.
|
||||
"autoStartServer": true, // Automatically start the stats server on launch. Values: true | false
|
||||
"autoOpenBrowser": false // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
|
||||
"autoOpenBrowser": true // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
|
||||
} // Local immersion stats dashboard served on localhost and available as an in-app overlay.
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 746 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 64 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
The subtitle sidebar displays the full parsed cue list for the active subtitle file as a scrollable panel alongside mpv. It lets you review past and upcoming lines, click any cue to seek directly to that moment, and follow along without depending on the transient overlay subtitles.
|
||||
|
||||
The sidebar is enabled by default. Set `subtitleSidebar.enabled` to `false` if you want to turn it off.
|
||||
The sidebar is opt-in and disabled by default. Enable it under `subtitleSidebar.enabled` in your config.
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -29,7 +29,7 @@ Enable and configure the sidebar under `subtitleSidebar` in your config file:
|
||||
```json
|
||||
{
|
||||
"subtitleSidebar": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"autoOpen": false,
|
||||
"layout": "overlay",
|
||||
"toggleKey": "Backslash",
|
||||
@@ -43,7 +43,7 @@ Enable and configure the sidebar under `subtitleSidebar` in your config file:
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| --------------------------- | ------- | ------------ | -------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | boolean | `true` | Enable subtitle sidebar support |
|
||||
| `enabled` | boolean | `false` | Enable subtitle sidebar support |
|
||||
| `autoOpen` | boolean | `false` | Open the sidebar automatically on overlay startup |
|
||||
| `layout` | string | `"overlay"` | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space |
|
||||
| `toggleKey` | string | `"Backslash"` | `KeyboardEvent.code` for the toggle shortcut |
|
||||
|
||||
@@ -4,26 +4,6 @@
|
||||
> 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
|
||||
@@ -34,18 +14,14 @@ Field names must match your Anki note type exactly (case-sensitive). See [Anki I
|
||||
6. Hover a word, then trigger Yomitan lookup with your configured lookup key/modifier to open the Yomitan popup
|
||||
7. Optional [subtitle annotations](/subtitle-annotations) (N+1, character-name, frequency, JLPT) highlight useful cues in real time
|
||||
|
||||
There are several ways to use SubMiner:
|
||||
|
||||
> [!TIP]
|
||||
> **New users: start with the `subminer` wrapper script** (or the **SubMiner mpv** shortcut on Windows). It handles mpv launch, IPC socket setup, and overlay lifecycle automatically so you don't need to configure anything in `mpv.conf`. You can add the mpv plugin later for in-player keybindings.
|
||||
There are two 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 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 |
|
||||
| -------- | -------- | --- |
|
||||
| **`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 |
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
## Live Config Reload
|
||||
|
||||
@@ -141,15 +117,12 @@ SubMiner.AppImage --help # Show all options
|
||||
|
||||
### Windows mpv Shortcut
|
||||
|
||||
First-run setup creates the config file, then requires the mpv plugin and Yomitan dictionaries before it can finish.
|
||||
|
||||
If you enabled the optional Windows shortcut during install, SubMiner creates a `SubMiner mpv` shortcut in the Start menu and/or on the desktop. On Windows, that shortcut is the recommended way to launch local files with SubMiner because it starts `mpv.exe` with the right defaults directly.
|
||||
After setup completes, the shortcut is the normal Windows playback entry point.
|
||||
If you enabled the optional Windows shortcut during install, SubMiner creates a `SubMiner mpv` shortcut in the Start menu and/or on the desktop. It runs `SubMiner.exe --launch-mpv`, which starts `mpv.exe` with SubMiner's `subminer` profile.
|
||||
|
||||
You can use it three ways:
|
||||
|
||||
- Double-click `SubMiner mpv` to open `mpv` with SubMiner's default socket/subtitle args.
|
||||
- Drag a video file onto `SubMiner mpv` to launch that file with the same defaults.
|
||||
- Double-click `SubMiner mpv` to open `mpv` with the SubMiner profile.
|
||||
- Drag a video file onto `SubMiner mpv` to launch that file with the same profile.
|
||||
- Run it directly from Command Prompt or PowerShell with `--launch-mpv`.
|
||||
|
||||
```powershell
|
||||
@@ -157,7 +130,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 on `PATH`. If it is installed elsewhere, set `SUBMINER_MPV_PATH` to the full `mpv.exe` path before launching.
|
||||
|
||||
### Launcher Subcommands
|
||||
|
||||
@@ -184,13 +157,12 @@ SubMiner.AppImage --setup
|
||||
Setup flow:
|
||||
|
||||
- config file: create the default config directory and prefer `config.jsonc`
|
||||
- plugin status: install the bundled mpv plugin before finishing setup
|
||||
- plugin status: install or skip the bundled mpv plugin
|
||||
- Yomitan shortcut: open bundled Yomitan settings directly from the setup window
|
||||
- dictionary check: ensure at least one bundled Yomitan dictionary is available, unless an external Yomitan profile is configured
|
||||
- dictionary check: ensure at least one bundled Yomitan dictionary is available
|
||||
- Windows: optionally create or remove `SubMiner mpv` Start Menu/Desktop shortcuts (`SubMiner.exe --launch-mpv`)
|
||||
- Windows: optionally set `mpv.executablePath` if `mpv.exe` is not on `PATH`
|
||||
- refresh: re-check plugin + dictionary state without restarting
|
||||
- `Finish setup` stays disabled until the config, plugin, and dictionary gates are satisfied
|
||||
- `Finish setup` stays disabled until dictionary availability is detected
|
||||
- finish action writes setup completion state and suppresses future auto-open prompts
|
||||
|
||||
AniList character dictionary auto-sync (optional):
|
||||
@@ -217,7 +189,7 @@ Top-level launcher flags like `--jellyfin-*` are intentionally rejected.
|
||||
|
||||
You can append additional MPV arguments with launcher `-a/--args`, for example `--args "--ao=alsa --volume=80"`.
|
||||
|
||||
You can define a matching profile in `~/.config/mpv/mpv.conf` for consistency when launching `mpv` manually or from other tools. The Windows `SubMiner.exe --launch-mpv` shortcut path uses equivalent args directly, but skips the extra current-directory subtitle scan to avoid duplicate sidecar detection when you drag a video onto the shortcut; the optional profile remains useful for manual mpv launches and the `subminer` wrapper defaults to `--profile=subminer` (or override with `subminer -p <profile> ...`):
|
||||
You can define a matching profile in `~/.config/mpv/mpv.conf` for consistency when launching `mpv` manually or from other tools. `subminer` launches with `--profile=subminer` by default (or override with `subminer -p <profile> ...`):
|
||||
|
||||
```ini
|
||||
[subminer]
|
||||
@@ -238,6 +210,10 @@ secondary-sid=auto
|
||||
secondary-sub-visibility=no
|
||||
```
|
||||
|
||||
::: warning
|
||||
`secondary-slang` is not a valid mpv option. Use `slang` with `sid=auto` / `secondary-sid=auto` to set subtitle language preferences.
|
||||
:::
|
||||
|
||||
### Yomitan setup
|
||||
|
||||
SubMiner includes a bundled Yomitan extension for overlay word lookup. This bundled extension is separate from any Yomitan browser extension you may have installed.
|
||||
@@ -262,8 +238,6 @@ 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.
|
||||
|
||||
## Controller Support
|
||||
|
||||
SubMiner supports gamepad/controller input for couch-friendly usage via the Chrome Gamepad API. Controller input drives the overlay while keyboard-only mode is enabled.
|
||||
@@ -282,7 +256,7 @@ 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 |
|
||||
@@ -296,7 +270,7 @@ By default SubMiner uses the first connected controller. `Alt+C` opens the contr
|
||||
### 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 |
|
||||
@@ -317,11 +291,13 @@ See [Keyboard Shortcuts](/shortcuts) for the full reference, including mining sh
|
||||
| `Alt+Shift+O` | Toggle visible overlay |
|
||||
| `Alt+Shift+Y` | Open Yomitan settings |
|
||||
|
||||
::: tip
|
||||
`Alt+Shift+Y` is fixed and not configurable. All other shortcuts can be changed under `shortcuts` in your config.
|
||||
:::
|
||||
|
||||
Useful overlay-local default keybinding: `Ctrl+Alt+P` opens the playlist browser for the current video's parent directory and the live mpv queue so you can append, reorder, remove, or jump between episodes without leaving playback.
|
||||
|
||||
Hovering over subtitle text pauses mpv by default; leaving resumes it. Yomitan popups also pause playback by default. Set `subtitleStyle.autoPauseVideoOnHover: false` or `subtitleStyle.autoPauseVideoOnYomitanPopup: false` to disable either behavior.
|
||||
Hovering over subtitle text pauses mpv by default; leaving resumes it. Disable with `subtitleStyle.autoPauseVideoOnHover: false`. To also pause while the Yomitan popup is open, set `subtitleStyle.autoPauseVideoOnYomitanPopup: true`.
|
||||
|
||||
### Drag-and-Drop
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ SubMiner's integration ports are configured in `config.jsonc`.
|
||||
},
|
||||
"texthooker": {
|
||||
"launchAtStartup": true,
|
||||
"openBrowser": false
|
||||
"openBrowser": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Architecture Map
|
||||
|
||||
Status: active
|
||||
Last verified: 2026-03-26
|
||||
Last verified: 2026-03-31
|
||||
Owner: Kyle Yasuda
|
||||
Read when: runtime ownership, composition boundaries, or layering questions
|
||||
|
||||
@@ -24,6 +24,27 @@ The desktop app keeps `src/main.ts` as composition root and pushes behavior into
|
||||
## Current Shape
|
||||
|
||||
- `src/main/` owns composition, runtime setup, IPC wiring, and app lifecycle adapters.
|
||||
- `src/main/*.ts` wrapper runtimes sit between `src/main.ts` and `src/main/runtime/**`
|
||||
so the composition root stays thin while exported `createBuild*MainDepsHandler`
|
||||
APIs remain internal plumbing. Key domain runtimes:
|
||||
- `anilist-runtime` – AniList token management, media tracking, retry queue
|
||||
- `cli-startup-runtime` – CLI command dispatch and initial-args handling
|
||||
- `discord-presence-lifecycle-runtime` – Discord Rich Presence lifecycle
|
||||
- `first-run-runtime` – first-run setup wizard
|
||||
- `ipc-runtime` – IPC handler registration and composition
|
||||
- `jellyfin-runtime` – Jellyfin session, playback, mpv orchestration
|
||||
- `main-startup-runtime` – top-level startup orchestration (app-ready → CLI → headless)
|
||||
- `main-startup-bootstrap` – wiring helper that builds startup runtime inputs
|
||||
- `mining-runtime` – Anki card mining actions
|
||||
- `mpv-runtime` – mpv client lifecycle
|
||||
- `overlay-ui-runtime` – overlay window management, visibility, tray
|
||||
- `overlay-geometry-runtime` – overlay bounds resolution
|
||||
- `shortcuts-runtime` – global shortcut registration
|
||||
- `startup-sequence-runtime` – headless known-word refresh and deferred startup sequencing
|
||||
- `stats-runtime` – immersion tracker, stats server, stats CLI
|
||||
- `subtitle-runtime` – subtitle prefetch, tokenization, caching
|
||||
- `youtube-runtime` – YouTube playback flow
|
||||
- `yomitan-runtime` – Yomitan extension loading and settings
|
||||
- `src/main/boot/` owns boot-phase assembly seams so `src/main.ts` can stay focused on lifecycle coordination and startup-path selection.
|
||||
- `src/core/services/` owns focused runtime services plus pure or side-effect-bounded logic.
|
||||
- `src/renderer/` owns overlay rendering and input behavior.
|
||||
|
||||
@@ -125,12 +125,6 @@ function titleOverlapScore(expectedTitle: string, candidateTitle: string): numbe
|
||||
if (!expected || !candidate) return 0;
|
||||
|
||||
if (candidate.includes(expected)) return 120;
|
||||
if (
|
||||
candidate.split(' ').length >= 2 &&
|
||||
` ${expected} `.includes(` ${candidate} `)
|
||||
) {
|
||||
return 90;
|
||||
}
|
||||
|
||||
const expectedTokens = tokenizeMatchWords(expectedTitle);
|
||||
if (expectedTokens.length === 0) return 0;
|
||||
@@ -345,12 +339,6 @@ function isSeasonDirectoryName(value: string): boolean {
|
||||
return /^(?:season|s)[\s._-]*\d{1,2}$/i.test(value.trim());
|
||||
}
|
||||
|
||||
function isEpisodeOnlyBaseName(value: string): boolean {
|
||||
return /^(?:[Ss]\d{1,2}[Ee]\d{1,3}|[Ee][Pp]?[\s._-]*\d{1,3}|\d{1,3})(?:$|[\s._-])/.test(
|
||||
value.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
function inferTitleFromPath(mediaPath: string): string {
|
||||
const directory = path.dirname(mediaPath);
|
||||
const segments = directory.split(/[\\/]+/).filter((segment) => segment.length > 0);
|
||||
@@ -457,11 +445,8 @@ export function inferAniSkipMetadataForFile(
|
||||
}
|
||||
|
||||
const baseName = path.basename(mediaPath, path.extname(mediaPath));
|
||||
const cleanedBaseName = cleanupTitle(baseName);
|
||||
const pathTitle = inferTitleFromPath(mediaPath);
|
||||
const fallbackTitle = isEpisodeOnlyBaseName(baseName)
|
||||
? pathTitle || cleanedBaseName || baseName
|
||||
: cleanedBaseName || pathTitle || baseName;
|
||||
const fallbackTitle = pathTitle || cleanupTitle(baseName) || baseName;
|
||||
return {
|
||||
title: fallbackTitle,
|
||||
season: detectSeasonFromNameOrDir(mediaPath),
|
||||
|
||||
@@ -21,9 +21,7 @@ import {
|
||||
getDefaultConfigDir,
|
||||
getSetupStatePath,
|
||||
readSetupState,
|
||||
resolveDefaultMpvInstallPaths,
|
||||
} from '../../src/shared/setup-state.js';
|
||||
import { detectInstalledFirstRunPlugin } from '../../src/main/runtime/first-run-setup-plugin.js';
|
||||
import { hasLauncherExternalYomitanProfileConfig } from '../config.js';
|
||||
|
||||
const SETUP_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
@@ -107,14 +105,6 @@ async function ensurePlaybackSetupReady(context: LauncherCommandContext): Promis
|
||||
const ready = await ensureLauncherSetupReady({
|
||||
readSetupState: () => readSetupState(statePath),
|
||||
isExternalYomitanConfigured: () => hasLauncherExternalYomitanProfileConfig(),
|
||||
isPluginInstalled: () => {
|
||||
const installPaths = resolveDefaultMpvInstallPaths(
|
||||
process.platform,
|
||||
os.homedir(),
|
||||
process.env.XDG_CONFIG_HOME,
|
||||
);
|
||||
return detectInstalledFirstRunPlugin(installPaths);
|
||||
},
|
||||
launchSetupApp: () => {
|
||||
const setupArgs = ['--background', '--setup'];
|
||||
if (args.logLevel) {
|
||||
|
||||
@@ -62,7 +62,6 @@ test('createDefaultArgs normalizes configured language codes and env thread over
|
||||
assert.deepEqual(parsed.youtubeAudioLangs, ['ja', 'jpn', 'en', 'eng']);
|
||||
assert.equal(parsed.whisperThreads, 7);
|
||||
assert.equal(parsed.youtubeWhisperSourceLanguage, 'ja');
|
||||
assert.equal(parsed.profile, '');
|
||||
} finally {
|
||||
if (originalThreads === undefined) {
|
||||
delete process.env.SUBMINER_WHISPER_THREADS;
|
||||
|
||||
@@ -49,17 +49,10 @@ function parseLogLevel(value: string): LogLevel {
|
||||
}
|
||||
|
||||
function parseBackend(value: string): Backend {
|
||||
if (
|
||||
value === 'auto' ||
|
||||
value === 'hyprland' ||
|
||||
value === 'sway' ||
|
||||
value === 'x11' ||
|
||||
value === 'macos' ||
|
||||
value === 'windows'
|
||||
) {
|
||||
if (value === 'auto' || value === 'hyprland' || value === 'x11' || value === 'macos') {
|
||||
return value as Backend;
|
||||
}
|
||||
fail(`Invalid backend: ${value} (must be auto, hyprland, sway, x11, macos, or windows)`);
|
||||
fail(`Invalid backend: ${value} (must be auto, hyprland, x11, or macos)`);
|
||||
}
|
||||
|
||||
function parseDictionaryTarget(value: string): string {
|
||||
@@ -104,7 +97,7 @@ export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig):
|
||||
backend: 'auto',
|
||||
directory: '.',
|
||||
recursive: false,
|
||||
profile: '',
|
||||
profile: 'subminer',
|
||||
startOverlay: false,
|
||||
whisperBin: process.env.SUBMINER_WHISPER_BIN || launcherConfig.whisperBin || '',
|
||||
whisperModel: process.env.SUBMINER_WHISPER_MODEL || launcherConfig.whisperModel || '',
|
||||
|
||||
@@ -17,20 +17,20 @@ test('resolveTopLevelCommand respects the app alias after root options', () => {
|
||||
});
|
||||
|
||||
test('parseCliPrograms keeps root options and target when no command is present', () => {
|
||||
const result = parseCliPrograms(['--backend', 'windows', '/tmp/movie.mkv'], 'subminer');
|
||||
const result = parseCliPrograms(['--backend', 'x11', '/tmp/movie.mkv'], 'subminer');
|
||||
|
||||
assert.equal(result.options.backend, 'windows');
|
||||
assert.equal(result.options.backend, 'x11');
|
||||
assert.equal(result.rootTarget, '/tmp/movie.mkv');
|
||||
assert.equal(result.invocations.appInvocation, null);
|
||||
});
|
||||
|
||||
test('parseCliPrograms routes app alias arguments through passthrough mode', () => {
|
||||
const result = parseCliPrograms(
|
||||
['--backend', 'windows', 'bin', '--anilist', '--log-level', 'debug'],
|
||||
['--backend', 'macos', 'bin', '--anilist', '--log-level', 'debug'],
|
||||
'subminer',
|
||||
);
|
||||
|
||||
assert.equal(result.options.backend, 'windows');
|
||||
assert.equal(result.options.backend, 'macos');
|
||||
assert.deepEqual(result.invocations.appInvocation, {
|
||||
appArgs: ['--anilist', '--log-level', 'debug'],
|
||||
});
|
||||
|
||||
@@ -43,10 +43,7 @@ export interface CliInvocations {
|
||||
|
||||
function applyRootOptions(program: Command): void {
|
||||
program
|
||||
.option(
|
||||
'-b, --backend <backend>',
|
||||
'Display backend (auto, hyprland, sway, x11, macos, windows)',
|
||||
)
|
||||
.option('-b, --backend <backend>', 'Display backend')
|
||||
.option('-d, --directory <dir>', 'Directory to browse')
|
||||
.option('-a, --args <args>', 'Pass arguments to MPV')
|
||||
.option('-r, --recursive', 'Search directories recursively')
|
||||
|
||||
@@ -8,7 +8,6 @@ import { EventEmitter } from 'node:events';
|
||||
import type { Args } from './types';
|
||||
import {
|
||||
cleanupPlaybackSession,
|
||||
detectBackend,
|
||||
findAppBinary,
|
||||
launchAppCommandDetached,
|
||||
launchTexthookerOnly,
|
||||
@@ -57,22 +56,6 @@ function createTempSocketPath(): { dir: string; socketPath: string } {
|
||||
return { dir, socketPath: path.join(dir, 'mpv.sock') };
|
||||
}
|
||||
|
||||
function withPlatform<T>(platform: NodeJS.Platform, callback: () => T): T {
|
||||
const originalDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
value: platform,
|
||||
});
|
||||
|
||||
try {
|
||||
return callback();
|
||||
} finally {
|
||||
if (originalDescriptor) {
|
||||
Object.defineProperty(process, 'platform', originalDescriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('mpv module exposes only canonical socket readiness helper', () => {
|
||||
assert.equal('waitForSocket' in mpvModule, false);
|
||||
});
|
||||
@@ -119,12 +102,6 @@ test('parseMpvArgString preserves empty quoted tokens', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('detectBackend resolves windows on win32 auto mode', () => {
|
||||
withPlatform('win32', () => {
|
||||
assert.equal(detectBackend('auto'), 'windows');
|
||||
});
|
||||
});
|
||||
|
||||
test('launchTexthookerOnly exits non-zero when app binary cannot be spawned', () => {
|
||||
const error = withProcessExitIntercept(() => {
|
||||
launchTexthookerOnly('/definitely-missing-subminer-binary', makeArgs());
|
||||
@@ -450,21 +427,6 @@ function withFindAppBinaryEnvSandbox(run: () => void): void {
|
||||
}
|
||||
}
|
||||
|
||||
function withFindAppBinaryPlatformSandbox(
|
||||
platform: NodeJS.Platform,
|
||||
run: (pathModule: typeof path) => void,
|
||||
): void {
|
||||
const originalPlatform = process.platform;
|
||||
try {
|
||||
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
|
||||
withFindAppBinaryEnvSandbox(() =>
|
||||
run(platform === 'win32' ? (path.win32 as typeof path) : path),
|
||||
);
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
||||
}
|
||||
}
|
||||
|
||||
function withAccessSyncStub(
|
||||
isExecutablePath: (filePath: string) => boolean,
|
||||
run: () => void,
|
||||
@@ -485,22 +447,7 @@ function withAccessSyncStub(
|
||||
}
|
||||
}
|
||||
|
||||
function withRealpathSyncStub(resolvePath: (filePath: string) => string, run: () => void): void {
|
||||
const originalRealpathSync = fs.realpathSync;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).realpathSync = (filePath: string): string => resolvePath(filePath);
|
||||
run();
|
||||
} finally {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).realpathSync = originalRealpathSync;
|
||||
}
|
||||
}
|
||||
|
||||
test(
|
||||
'findAppBinary resolves ~/.local/bin/SubMiner.AppImage when it exists',
|
||||
{ concurrency: false },
|
||||
() => {
|
||||
test('findAppBinary resolves ~/.local/bin/SubMiner.AppImage when it exists', () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-home-'));
|
||||
const originalHomedir = os.homedir;
|
||||
try {
|
||||
@@ -508,30 +455,26 @@ test(
|
||||
const appImage = path.join(baseDir, '.local/bin/SubMiner.AppImage');
|
||||
makeExecutable(appImage);
|
||||
|
||||
withFindAppBinaryPlatformSandbox('linux', (pathModule) => {
|
||||
const result = findAppBinary('/some/other/path/subminer', pathModule);
|
||||
withFindAppBinaryEnvSandbox(() => {
|
||||
const result = findAppBinary('/some/other/path/subminer');
|
||||
assert.equal(result, appImage);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'findAppBinary resolves /opt/SubMiner/SubMiner.AppImage when ~/.local/bin candidate does not exist',
|
||||
{ concurrency: false },
|
||||
() => {
|
||||
test('findAppBinary resolves /opt/SubMiner/SubMiner.AppImage when ~/.local/bin candidate does not exist', () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-home-'));
|
||||
const originalHomedir = os.homedir;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
withFindAppBinaryPlatformSandbox('linux', (pathModule) => {
|
||||
withFindAppBinaryEnvSandbox(() => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === '/opt/SubMiner/SubMiner.AppImage',
|
||||
() => {
|
||||
const result = findAppBinary('/some/other/path/subminer', pathModule);
|
||||
const result = findAppBinary('/some/other/path/subminer');
|
||||
assert.equal(result, '/opt/SubMiner/SubMiner.AppImage');
|
||||
},
|
||||
);
|
||||
@@ -540,13 +483,9 @@ test(
|
||||
os.homedir = originalHomedir;
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'findAppBinary finds subminer on PATH when AppImage candidates do not exist',
|
||||
{ concurrency: false },
|
||||
() => {
|
||||
test('findAppBinary finds subminer on PATH when AppImage candidates do not exist', () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-path-'));
|
||||
const originalHomedir = os.homedir;
|
||||
const originalPath = process.env.PATH;
|
||||
@@ -558,124 +497,12 @@ test(
|
||||
makeExecutable(wrapperPath);
|
||||
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ''}`;
|
||||
|
||||
withFindAppBinaryPlatformSandbox('linux', (pathModule) => {
|
||||
withFindAppBinaryEnvSandbox(() => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === wrapperPath,
|
||||
() => {
|
||||
// selfPath must differ from wrapperPath so the self-check does not exclude it
|
||||
const result = findAppBinary(path.join(baseDir, 'launcher', 'subminer'), pathModule);
|
||||
assert.equal(result, wrapperPath);
|
||||
},
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
process.env.PATH = originalPath;
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'findAppBinary excludes PATH matches that canonicalize to the launcher path',
|
||||
{ concurrency: false },
|
||||
() => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-realpath-'));
|
||||
const originalHomedir = os.homedir;
|
||||
const originalPath = process.env.PATH;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
const binDir = path.join(baseDir, 'bin');
|
||||
const wrapperPath = path.join(binDir, 'subminer');
|
||||
const canonicalPath = path.join(baseDir, 'launch', 'subminer');
|
||||
makeExecutable(wrapperPath);
|
||||
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ''}`;
|
||||
|
||||
withFindAppBinaryPlatformSandbox('linux', (pathModule) => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === wrapperPath,
|
||||
() => {
|
||||
withRealpathSyncStub(
|
||||
(filePath) => {
|
||||
if (filePath === canonicalPath || filePath === wrapperPath) {
|
||||
return canonicalPath;
|
||||
}
|
||||
return filePath;
|
||||
},
|
||||
() => {
|
||||
const result = findAppBinary(canonicalPath, pathModule);
|
||||
assert.equal(result, null);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
process.env.PATH = originalPath;
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('findAppBinary resolves Windows install paths when present', { concurrency: false }, () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-'));
|
||||
const originalHomedir = os.homedir;
|
||||
const originalLocalAppData = process.env.LOCALAPPDATA;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
process.env.LOCALAPPDATA = path.win32.join(baseDir, 'AppData', 'Local');
|
||||
const appExe = path.win32.join(
|
||||
baseDir,
|
||||
'AppData',
|
||||
'Local',
|
||||
'Programs',
|
||||
'SubMiner',
|
||||
'SubMiner.exe',
|
||||
);
|
||||
|
||||
withFindAppBinaryPlatformSandbox('win32', (pathModule) => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === appExe,
|
||||
() => {
|
||||
const result = findAppBinary(
|
||||
pathModule.join(baseDir, 'launcher', 'SubMiner.exe'),
|
||||
pathModule,
|
||||
);
|
||||
assert.equal(result, appExe);
|
||||
},
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
if (originalLocalAppData === undefined) {
|
||||
delete process.env.LOCALAPPDATA;
|
||||
} else {
|
||||
process.env.LOCALAPPDATA = originalLocalAppData;
|
||||
}
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
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 ?? ''}`;
|
||||
|
||||
withFindAppBinaryPlatformSandbox('win32', (pathModule) => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === wrapperPath,
|
||||
() => {
|
||||
const result = findAppBinary(
|
||||
pathModule.join(baseDir, 'launcher', 'SubMiner.exe'),
|
||||
pathModule,
|
||||
);
|
||||
const result = findAppBinary(path.join(baseDir, 'launcher', 'subminer'));
|
||||
assert.equal(result, wrapperPath);
|
||||
},
|
||||
);
|
||||
@@ -686,42 +513,3 @@ test('findAppBinary resolves SubMiner.exe on PATH on Windows', { concurrency: fa
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
127
launcher/mpv.ts
127
launcher/mpv.ts
@@ -14,11 +14,11 @@ import {
|
||||
isExecutable,
|
||||
resolveBinaryPathCandidate,
|
||||
resolveCommandInvocation,
|
||||
realpathMaybe,
|
||||
isYoutubeTarget,
|
||||
uniqueNormalizedLangCodes,
|
||||
sleep,
|
||||
normalizeLangCode,
|
||||
realpathMaybe,
|
||||
} from './util.js';
|
||||
|
||||
export const state = {
|
||||
@@ -35,8 +35,6 @@ type SpawnTarget = {
|
||||
args: string[];
|
||||
};
|
||||
|
||||
type PathModule = Pick<typeof path, 'join' | 'extname' | 'delimiter' | 'sep' | 'resolve'>;
|
||||
|
||||
const DETACHED_IDLE_MPV_PID_FILE = path.join(os.tmpdir(), 'subminer-idle-mpv.pid');
|
||||
const OVERLAY_START_SOCKET_READY_TIMEOUT_MS = 900;
|
||||
const OVERLAY_START_COMMAND_SETTLE_TIMEOUT_MS = 700;
|
||||
@@ -227,7 +225,6 @@ export function makeTempDir(prefix: string): string {
|
||||
|
||||
export function detectBackend(backend: Backend): 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();
|
||||
@@ -246,47 +243,16 @@ export function detectBackend(backend: Backend): Exclude<Backend, 'auto'> {
|
||||
fail('Could not detect display backend');
|
||||
}
|
||||
|
||||
function resolveAppBinaryCandidate(candidate: string, pathModule: PathModule = path): string {
|
||||
function resolveMacAppBinaryCandidate(candidate: string): string {
|
||||
const direct = resolveBinaryPathCandidate(candidate);
|
||||
if (!direct) return '';
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
|
||||
for (const candidateBinary of ['SubMiner.exe', 'subminer.exe']) {
|
||||
const nestedCandidate = pathModule.join(direct, candidateBinary);
|
||||
if (isExecutable(nestedCandidate)) {
|
||||
return nestedCandidate;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (isExecutable(direct)) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
if (!pathModule.extname(direct)) {
|
||||
for (const extension of ['.exe', '.cmd', '.bat']) {
|
||||
const withExtension = `${direct}${extension}`;
|
||||
if (isExecutable(withExtension)) {
|
||||
return withExtension;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isExecutable(direct)) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
return '';
|
||||
return isExecutable(direct) ? direct : '';
|
||||
}
|
||||
|
||||
if (isExecutable(direct)) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const appIndex = direct.indexOf('.app/');
|
||||
@@ -299,8 +265,8 @@ function resolveAppBinaryCandidate(candidate: string, pathModule: PathModule = p
|
||||
if (!appPath) return '';
|
||||
|
||||
const candidates = [
|
||||
pathModule.join(appPath, 'Contents', 'MacOS', 'SubMiner'),
|
||||
pathModule.join(appPath, 'Contents', 'MacOS', 'subminer'),
|
||||
path.join(appPath, 'Contents', 'MacOS', 'SubMiner'),
|
||||
path.join(appPath, 'Contents', 'MacOS', 'subminer'),
|
||||
];
|
||||
|
||||
for (const candidateBinary of candidates) {
|
||||
@@ -312,78 +278,37 @@ function resolveAppBinaryCandidate(candidate: string, pathModule: PathModule = p
|
||||
return '';
|
||||
}
|
||||
|
||||
function findCommandOnPath(candidates: string[], pathModule: PathModule = path): string {
|
||||
const pathDirs = getPathEnv().split(pathModule.delimiter);
|
||||
for (const candidateName of candidates) {
|
||||
for (const dir of pathDirs) {
|
||||
if (!dir) continue;
|
||||
|
||||
const directCandidate = pathModule.join(dir, candidateName);
|
||||
if (isExecutable(directCandidate)) {
|
||||
return directCandidate;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' && !pathModule.extname(candidateName)) {
|
||||
for (const extension of ['.exe', '.cmd', '.bat']) {
|
||||
const extendedCandidate = pathModule.join(dir, `${candidateName}${extension}`);
|
||||
if (isExecutable(extendedCandidate)) {
|
||||
return extendedCandidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function findAppBinary(selfPath: string, pathModule: PathModule = path): string | null {
|
||||
export function findAppBinary(selfPath: string): string | null {
|
||||
const envPaths = [process.env.SUBMINER_APPIMAGE_PATH, process.env.SUBMINER_BINARY_PATH].filter(
|
||||
(candidate): candidate is string => Boolean(candidate),
|
||||
);
|
||||
|
||||
for (const envPath of envPaths) {
|
||||
const resolved = resolveAppBinaryCandidate(envPath, pathModule);
|
||||
const resolved = resolveMacAppBinaryCandidate(envPath);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
const candidates: string[] = [];
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData =
|
||||
process.env.LOCALAPPDATA?.trim() ||
|
||||
(process.env.APPDATA?.trim() || '').replace(/[\\/]Roaming$/i, `${pathModule.sep}Local`) ||
|
||||
pathModule.join(os.homedir(), 'AppData', 'Local');
|
||||
const programFiles = process.env.ProgramFiles?.trim() || 'C:\\Program Files';
|
||||
const programFilesX86 = process.env['ProgramFiles(x86)']?.trim() || 'C:\\Program Files (x86)';
|
||||
candidates.push(pathModule.join(localAppData, 'Programs', 'SubMiner', 'SubMiner.exe'));
|
||||
candidates.push(pathModule.join(programFiles, 'SubMiner', 'SubMiner.exe'));
|
||||
candidates.push(pathModule.join(programFilesX86, 'SubMiner', 'SubMiner.exe'));
|
||||
candidates.push('C:\\SubMiner\\SubMiner.exe');
|
||||
} else if (process.platform === 'darwin') {
|
||||
if (process.platform === 'darwin') {
|
||||
candidates.push('/Applications/SubMiner.app/Contents/MacOS/SubMiner');
|
||||
candidates.push('/Applications/SubMiner.app/Contents/MacOS/subminer');
|
||||
candidates.push(
|
||||
pathModule.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/SubMiner'),
|
||||
);
|
||||
candidates.push(
|
||||
pathModule.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/subminer'),
|
||||
);
|
||||
} else {
|
||||
candidates.push(pathModule.join(os.homedir(), '.local/bin/SubMiner.AppImage'));
|
||||
candidates.push('/opt/SubMiner/SubMiner.AppImage');
|
||||
candidates.push(path.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/SubMiner'));
|
||||
candidates.push(path.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/subminer'));
|
||||
}
|
||||
|
||||
candidates.push(path.join(os.homedir(), '.local/bin/SubMiner.AppImage'));
|
||||
candidates.push('/opt/SubMiner/SubMiner.AppImage');
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const resolved = resolveAppBinaryCandidate(candidate, pathModule);
|
||||
if (resolved) return resolved;
|
||||
if (isExecutable(candidate)) return candidate;
|
||||
}
|
||||
|
||||
const fromPath = findCommandOnPath(
|
||||
process.platform === 'win32' ? ['SubMiner', 'subminer'] : ['subminer'],
|
||||
pathModule,
|
||||
);
|
||||
const fromPath = getPathEnv()
|
||||
.split(path.delimiter)
|
||||
.map((dir) => path.join(dir, 'subminer'))
|
||||
.find((candidate) => isExecutable(candidate));
|
||||
|
||||
if (fromPath) {
|
||||
const resolvedSelf = realpathMaybe(selfPath);
|
||||
@@ -709,9 +634,7 @@ export async function startMpv(
|
||||
mpvArgs.push(`--input-ipc-server=${socketPath}`);
|
||||
mpvArgs.push(target);
|
||||
|
||||
const mpvTarget = resolveCommandInvocation('mpv', mpvArgs, {
|
||||
normalizeWindowsShellArgs: false,
|
||||
});
|
||||
const mpvTarget = resolveCommandInvocation('mpv', mpvArgs);
|
||||
state.mpvProc = spawn(mpvTarget.command, mpvTarget.args, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
@@ -1153,9 +1076,7 @@ export function launchMpvIdleDetached(
|
||||
);
|
||||
mpvArgs.push(`--log-file=${getMpvLogPath()}`);
|
||||
mpvArgs.push(`--input-ipc-server=${socketPath}`);
|
||||
const mpvTarget = resolveCommandInvocation('mpv', mpvArgs, {
|
||||
normalizeWindowsShellArgs: false,
|
||||
});
|
||||
const mpvTarget = resolveCommandInvocation('mpv', mpvArgs);
|
||||
const proc = spawn(mpvTarget.command, mpvTarget.args, {
|
||||
stdio: 'ignore',
|
||||
detached: true,
|
||||
|
||||
@@ -116,36 +116,6 @@ test('ensureLauncherSetupReady bypasses setup gate when external yomitan is conf
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('ensureLauncherSetupReady bypasses setup gate when plugin is already installed', async () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const ready = await ensureLauncherSetupReady({
|
||||
readSetupState: () => ({
|
||||
version: 3,
|
||||
status: 'cancelled',
|
||||
completedAt: null,
|
||||
completionSource: null,
|
||||
yomitanSetupMode: null,
|
||||
lastSeenYomitanDictionaryCount: 0,
|
||||
pluginInstallStatus: 'unknown',
|
||||
pluginInstallPathSummary: null,
|
||||
windowsMpvShortcutPreferences: { startMenuEnabled: true, desktopEnabled: true },
|
||||
windowsMpvShortcutLastStatus: 'unknown',
|
||||
}),
|
||||
isPluginInstalled: () => true,
|
||||
launchSetupApp: () => {
|
||||
calls.push('launch');
|
||||
},
|
||||
sleep: async () => undefined,
|
||||
now: () => 0,
|
||||
timeoutMs: 5_000,
|
||||
pollIntervalMs: 100,
|
||||
});
|
||||
|
||||
assert.equal(ready, true);
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('ensureLauncherSetupReady fails on timeout/cancelled state', async () => {
|
||||
const result = await ensureLauncherSetupReady({
|
||||
readSetupState: () => ({
|
||||
@@ -162,73 +132,10 @@ test('ensureLauncherSetupReady fails on timeout/cancelled state', async () => {
|
||||
}),
|
||||
launchSetupApp: () => undefined,
|
||||
sleep: async () => undefined,
|
||||
now: (() => {
|
||||
let value = 0;
|
||||
return () => (value += 100);
|
||||
})(),
|
||||
now: () => 0,
|
||||
timeoutMs: 5_000,
|
||||
pollIntervalMs: 100,
|
||||
});
|
||||
|
||||
assert.equal(result, false);
|
||||
});
|
||||
|
||||
test('ensureLauncherSetupReady ignores stale cancelled state after launching setup app', async () => {
|
||||
let reads = 0;
|
||||
|
||||
const result = await ensureLauncherSetupReady({
|
||||
readSetupState: () => {
|
||||
reads += 1;
|
||||
if (reads <= 2) {
|
||||
return {
|
||||
version: 3,
|
||||
status: 'cancelled',
|
||||
completedAt: null,
|
||||
completionSource: null,
|
||||
yomitanSetupMode: null,
|
||||
lastSeenYomitanDictionaryCount: 0,
|
||||
pluginInstallStatus: 'unknown',
|
||||
pluginInstallPathSummary: null,
|
||||
windowsMpvShortcutPreferences: { startMenuEnabled: true, desktopEnabled: true },
|
||||
windowsMpvShortcutLastStatus: 'unknown',
|
||||
};
|
||||
}
|
||||
if (reads === 3) {
|
||||
return {
|
||||
version: 3,
|
||||
status: 'in_progress',
|
||||
completedAt: null,
|
||||
completionSource: null,
|
||||
yomitanSetupMode: null,
|
||||
lastSeenYomitanDictionaryCount: 0,
|
||||
pluginInstallStatus: 'unknown',
|
||||
pluginInstallPathSummary: null,
|
||||
windowsMpvShortcutPreferences: { startMenuEnabled: true, desktopEnabled: true },
|
||||
windowsMpvShortcutLastStatus: 'unknown',
|
||||
};
|
||||
}
|
||||
return {
|
||||
version: 3,
|
||||
status: 'completed',
|
||||
completedAt: '2026-03-07T00:00:00.000Z',
|
||||
completionSource: 'legacy_auto_detected',
|
||||
yomitanSetupMode: 'internal',
|
||||
lastSeenYomitanDictionaryCount: 1,
|
||||
pluginInstallStatus: 'installed',
|
||||
pluginInstallPathSummary: '/tmp/mpv',
|
||||
windowsMpvShortcutPreferences: { startMenuEnabled: true, desktopEnabled: true },
|
||||
windowsMpvShortcutLastStatus: 'unknown',
|
||||
};
|
||||
},
|
||||
launchSetupApp: () => undefined,
|
||||
sleep: async () => undefined,
|
||||
now: (() => {
|
||||
let value = 0;
|
||||
return () => (value += 100);
|
||||
})(),
|
||||
timeoutMs: 5_000,
|
||||
pollIntervalMs: 100,
|
||||
});
|
||||
|
||||
assert.equal(result, true);
|
||||
});
|
||||
|
||||
@@ -6,24 +6,15 @@ export async function waitForSetupCompletion(deps: {
|
||||
now: () => number;
|
||||
timeoutMs: number;
|
||||
pollIntervalMs: number;
|
||||
ignoreInitialCancelledState?: boolean;
|
||||
}): Promise<'completed' | 'cancelled' | 'timeout'> {
|
||||
const deadline = deps.now() + deps.timeoutMs;
|
||||
let ignoringCancelled = deps.ignoreInitialCancelledState === true;
|
||||
|
||||
while (deps.now() <= deadline) {
|
||||
const state = deps.readSetupState();
|
||||
if (isSetupCompleted(state)) {
|
||||
return 'completed';
|
||||
}
|
||||
if (ignoringCancelled && state != null && state.status !== 'cancelled') {
|
||||
ignoringCancelled = false;
|
||||
}
|
||||
if (state?.status === 'cancelled') {
|
||||
if (ignoringCancelled) {
|
||||
await deps.sleep(deps.pollIntervalMs);
|
||||
continue;
|
||||
}
|
||||
return 'cancelled';
|
||||
}
|
||||
await deps.sleep(deps.pollIntervalMs);
|
||||
@@ -35,7 +26,6 @@ export async function waitForSetupCompletion(deps: {
|
||||
export async function ensureLauncherSetupReady(deps: {
|
||||
readSetupState: () => SetupState | null;
|
||||
isExternalYomitanConfigured?: () => boolean;
|
||||
isPluginInstalled?: () => boolean;
|
||||
launchSetupApp: () => void;
|
||||
sleep: (ms: number) => Promise<void>;
|
||||
now: () => number;
|
||||
@@ -45,18 +35,11 @@ export async function ensureLauncherSetupReady(deps: {
|
||||
if (deps.isExternalYomitanConfigured?.()) {
|
||||
return true;
|
||||
}
|
||||
if (deps.isPluginInstalled?.()) {
|
||||
return true;
|
||||
}
|
||||
const initialState = deps.readSetupState();
|
||||
if (isSetupCompleted(initialState)) {
|
||||
if (isSetupCompleted(deps.readSetupState())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
deps.launchSetupApp();
|
||||
const result = await waitForSetupCompletion({
|
||||
...deps,
|
||||
ignoreInitialCancelledState: initialState?.status === 'cancelled',
|
||||
});
|
||||
const result = await waitForSetupCompletion(deps);
|
||||
return result === 'completed';
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ function createSmokeCase(name: string): SmokeCase {
|
||||
|
||||
writeExecutable(
|
||||
fakeMpvPath,
|
||||
`#!/usr/bin/env bun
|
||||
`#!/usr/bin/env node
|
||||
const fs = require('node:fs');
|
||||
const net = require('node:net');
|
||||
const path = require('node:path');
|
||||
@@ -118,7 +118,7 @@ process.on('SIGTERM', closeAndExit);
|
||||
|
||||
writeExecutable(
|
||||
fakeAppPath,
|
||||
`#!/usr/bin/env bun
|
||||
`#!/usr/bin/env node
|
||||
const fs = require('node:fs');
|
||||
|
||||
const logPath = ${JSON.stringify(fakeAppLogPath)};
|
||||
|
||||
@@ -68,7 +68,7 @@ export const DEFAULT_MPV_SUBMINER_ARGS = [
|
||||
] as const;
|
||||
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
export type Backend = 'auto' | 'hyprland' | 'sway' | 'x11' | 'macos' | 'windows';
|
||||
export type Backend = 'auto' | 'hyprland' | 'x11' | 'macos';
|
||||
export type JimakuLanguagePreference = 'ja' | 'en' | 'none';
|
||||
|
||||
export interface LauncherAiConfig {
|
||||
|
||||
@@ -244,19 +244,13 @@ export function inferWhisperLanguage(langCodes: string[], fallback: string): str
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export interface CommandInvocationOptions {
|
||||
normalizeWindowsShellArgs?: boolean;
|
||||
}
|
||||
|
||||
export function resolveCommandInvocation(
|
||||
executable: string,
|
||||
args: string[],
|
||||
options: CommandInvocationOptions = {},
|
||||
): { command: string; args: string[] } {
|
||||
if (process.platform !== 'win32') {
|
||||
return { command: executable, args };
|
||||
}
|
||||
const { normalizeWindowsShellArgs = true } = options;
|
||||
|
||||
const resolvedExecutable = resolveExecutablePath(executable) ?? executable;
|
||||
const extension = path.extname(resolvedExecutable).toLowerCase();
|
||||
@@ -273,9 +267,7 @@ export function resolveCommandInvocation(
|
||||
command: bashTarget.command,
|
||||
args: [
|
||||
normalizeWindowsShellArg(resolvedExecutable, bashTarget.flavor),
|
||||
...args.map((arg) =>
|
||||
normalizeWindowsShellArgs ? normalizeWindowsShellArg(arg, bashTarget.flavor) : arg,
|
||||
),
|
||||
...args.map((arg) => normalizeWindowsShellArg(arg, bashTarget.flavor)),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -288,9 +280,7 @@ export function resolveCommandInvocation(
|
||||
command: bashTarget.command,
|
||||
args: [
|
||||
normalizeWindowsShellArg(resolvedExecutable, bashTarget.flavor),
|
||||
...args.map((arg) =>
|
||||
normalizeWindowsShellArgs ? normalizeWindowsShellArg(arg, bashTarget.flavor) : arg,
|
||||
),
|
||||
...args.map((arg) => normalizeWindowsShellArg(arg, bashTarget.flavor)),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
25
package.json
25
package.json
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"name": "subminer",
|
||||
"productName": "SubMiner",
|
||||
"desktopName": "SubMiner.desktop",
|
||||
"version": "0.11.1",
|
||||
"version": "0.10.0",
|
||||
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"main": "dist/main-entry.js",
|
||||
@@ -15,10 +13,9 @@
|
||||
"test-yomitan-parser:electron": "bun run build:yomitan && bun build scripts/test-yomitan-parser.ts --format=cjs --target=node --outfile dist/scripts/test-yomitan-parser.js --external electron && env -u ELECTRON_RUN_AS_NODE electron dist/scripts/test-yomitan-parser.js",
|
||||
"build:yomitan": "bun scripts/build-yomitan.mjs",
|
||||
"build:assets": "bun scripts/prepare-build-assets.mjs",
|
||||
"build:launcher": "bun build ./launcher/main.ts --target=bun --packages=bundle --outfile=dist/launcher/subminer",
|
||||
"build:stats": "cd stats && bun run build",
|
||||
"dev:stats": "cd stats && bun run dev",
|
||||
"build": "bun run build:yomitan && bun run build:stats && tsc -p tsconfig.json && bun run build:renderer && bun run build:launcher && bun run build:assets",
|
||||
"build": "bun run build:yomitan && bun run build:stats && tsc -p tsconfig.json && bun run build:renderer && bun run build:assets",
|
||||
"build:renderer": "esbuild src/renderer/renderer.ts --bundle --platform=browser --format=esm --target=es2022 --outfile=dist/renderer/renderer.js --sourcemap",
|
||||
"changelog:build": "bun run scripts/build-changelog.ts build",
|
||||
"changelog:check": "bun run scripts/build-changelog.ts check",
|
||||
@@ -40,8 +37,8 @@
|
||||
"docs:preview": "bun run --cwd docs-site docs:preview",
|
||||
"docs:test": "bun run --cwd docs-site test",
|
||||
"test:docs:kb": "bun test scripts/docs-knowledge-base.test.ts",
|
||||
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/integrations.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts src/verify-config-example.test.ts",
|
||||
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/integrations.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js dist/verify-config-example.test.js",
|
||||
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts src/verify-config-example.test.ts",
|
||||
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js dist/verify-config-example.test.js",
|
||||
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
||||
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-binary-windows.lua",
|
||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||
@@ -68,7 +65,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/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",
|
||||
"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",
|
||||
"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",
|
||||
@@ -83,12 +80,9 @@
|
||||
"build:win:unsigned": "bun run build && node scripts/build-win-unsigned.mjs"
|
||||
},
|
||||
"overrides": {
|
||||
"@xmldom/xmldom": "0.8.12",
|
||||
"app-builder-lib": "26.8.2",
|
||||
"electron-builder-squirrel-windows": "26.8.2",
|
||||
"lodash": "4.18.0",
|
||||
"minimatch": "10.2.3",
|
||||
"picomatch": "4.0.4",
|
||||
"tar": "7.5.11"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -117,7 +111,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"electron": "39.8.6",
|
||||
"electron": "^37.10.3",
|
||||
"electron-builder": "26.8.2",
|
||||
"esbuild": "^0.25.12",
|
||||
"prettier": "^3.8.1",
|
||||
@@ -128,8 +122,7 @@
|
||||
"productName": "SubMiner",
|
||||
"executableName": "SubMiner",
|
||||
"artifactName": "SubMiner-${version}.${ext}",
|
||||
"afterPack": "scripts/electron-builder-after-pack.cjs",
|
||||
"icon": "assets/SubMiner-square.png",
|
||||
"icon": "assets/SubMiner.png",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
@@ -148,7 +141,7 @@
|
||||
"zip"
|
||||
],
|
||||
"category": "public.app-category.video",
|
||||
"icon": "assets/SubMiner-square.png",
|
||||
"icon": "assets/SubMiner.png",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "build/entitlements.mac.plist",
|
||||
"entitlementsInherit": "build/entitlements.mac.plist",
|
||||
@@ -164,7 +157,7 @@
|
||||
"nsis",
|
||||
"zip"
|
||||
],
|
||||
"icon": "assets/SubMiner.ico"
|
||||
"icon": "assets/SubMiner.png"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
|
||||
@@ -18,7 +18,7 @@ texthooker_enabled=yes
|
||||
# Texthooker WebSocket port
|
||||
texthooker_port=5174
|
||||
|
||||
# Window manager backend: auto, hyprland, sway, x11, macos, windows
|
||||
# Window manager backend: auto, hyprland, sway, x11
|
||||
# "auto" detects based on environment variables
|
||||
backend=auto
|
||||
|
||||
|
||||
@@ -29,25 +29,13 @@ function M.create(ctx)
|
||||
return options_helper.coerce_bool(raw_auto_start, false)
|
||||
end
|
||||
|
||||
local function rearm_managed_subtitle_defaults()
|
||||
if not process.has_matching_mpv_ipc_socket(opts.socket_path) then
|
||||
return false
|
||||
end
|
||||
|
||||
mp.set_property_native("sub-auto", "fuzzy")
|
||||
mp.set_property_native("sid", "auto")
|
||||
mp.set_property_native("secondary-sid", "auto")
|
||||
return true
|
||||
end
|
||||
|
||||
local function on_file_loaded()
|
||||
aniskip.clear_aniskip_state()
|
||||
process.disarm_auto_play_ready_gate()
|
||||
local has_matching_socket = rearm_managed_subtitle_defaults()
|
||||
|
||||
local should_auto_start = resolve_auto_start_enabled()
|
||||
if should_auto_start then
|
||||
if not has_matching_socket then
|
||||
if not process.has_matching_mpv_ipc_socket(opts.socket_path) then
|
||||
subminer_log(
|
||||
"info",
|
||||
"lifecycle",
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
});
|
||||
@@ -38,11 +38,7 @@ const lanes: Record<string, LaneConfig> = {
|
||||
},
|
||||
};
|
||||
|
||||
function collectFiles(
|
||||
rootDir: string,
|
||||
includeSuffixes: string[],
|
||||
excludeSet: Set<string>,
|
||||
): string[] {
|
||||
function collectFiles(rootDir: string, includeSuffixes: string[], excludeSet: Set<string>): string[] {
|
||||
const out: string[] = [];
|
||||
const visit = (currentDir: string) => {
|
||||
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
||||
@@ -149,12 +145,7 @@ function parseLcovReport(report: string): LcovRecord[] {
|
||||
}
|
||||
if (line.startsWith('BRDA:')) {
|
||||
const [lineNumber, block, branch, hits] = line.slice(5).split(',');
|
||||
if (
|
||||
lineNumber === undefined ||
|
||||
block === undefined ||
|
||||
branch === undefined ||
|
||||
hits === undefined
|
||||
) {
|
||||
if (lineNumber === undefined || block === undefined || branch === undefined || hits === undefined) {
|
||||
continue;
|
||||
}
|
||||
ensureCurrent().branches.set(`${lineNumber}:${block}:${branch}`, {
|
||||
@@ -233,9 +224,7 @@ export function mergeLcovReports(reports: string[]): string {
|
||||
chunks.push(`FNDA:${record.functionHits.get(name) ?? 0},${name}`);
|
||||
}
|
||||
chunks.push(`FNF:${functions.length}`);
|
||||
chunks.push(
|
||||
`FNH:${functions.filter(([name]) => (record.functionHits.get(name) ?? 0) > 0).length}`,
|
||||
);
|
||||
chunks.push(`FNH:${functions.filter(([name]) => (record.functionHits.get(name) ?? 0) > 0).length}`);
|
||||
|
||||
const branches = [...record.branches.values()].sort((a, b) =>
|
||||
a.line === b.line
|
||||
@@ -309,9 +298,7 @@ function runCoverageLane(): number {
|
||||
}
|
||||
|
||||
writeFileSync(join(coverageDir, 'lcov.info'), mergeLcovReports(reports), 'utf8');
|
||||
process.stdout.write(
|
||||
`Merged LCOV written to ${relative(repoRoot, join(coverageDir, 'lcov.info'))}\n`,
|
||||
);
|
||||
process.stdout.write(`Merged LCOV written to ${relative(repoRoot, join(coverageDir, 'lcov.info'))}\n`);
|
||||
return 0;
|
||||
} finally {
|
||||
rmSync(shardRoot, { recursive: true, force: true });
|
||||
|
||||
@@ -178,12 +178,6 @@ local function run_plugin_scenario(config)
|
||||
value = value,
|
||||
}
|
||||
end
|
||||
function mp.set_property(name, value)
|
||||
recorded.property_sets[#recorded.property_sets + 1] = {
|
||||
name = name,
|
||||
value = value,
|
||||
}
|
||||
end
|
||||
function mp.get_script_name()
|
||||
return "subminer"
|
||||
end
|
||||
@@ -537,38 +531,6 @@ do
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "yes",
|
||||
auto_start_pause_until_ready = "no",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
media_title = "Random Movie",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for subtitle rearm scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(
|
||||
has_property_set(recorded.property_sets, "sub-auto", "fuzzy"),
|
||||
"managed file-loaded should rearm sub-auto for idle mpv sessions"
|
||||
)
|
||||
assert_true(
|
||||
has_property_set(recorded.property_sets, "sid", "auto"),
|
||||
"managed file-loaded should rearm primary subtitle selection for idle mpv sessions"
|
||||
)
|
||||
assert_true(
|
||||
has_property_set(recorded.property_sets, "secondary-sid", "auto"),
|
||||
"managed file-loaded should rearm secondary subtitle selection for idle mpv sessions"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
@@ -1075,10 +1037,6 @@ do
|
||||
start_call == nil,
|
||||
"auto-start should be skipped when mpv input-ipc-server does not match configured socket_path"
|
||||
)
|
||||
assert_true(
|
||||
not has_property_set(recorded.property_sets, "sid", "auto"),
|
||||
"subtitle rearm should not run when mpv input-ipc-server does not match configured socket_path"
|
||||
)
|
||||
assert_true(
|
||||
not has_property_set(recorded.property_sets, "pause", true),
|
||||
"pause-until-ready gate should not arm when socket_path does not match"
|
||||
|
||||
@@ -369,8 +369,7 @@ export class AnkiIntegration {
|
||||
trackLastAddedDuplicateNoteIds: (noteId, duplicateNoteIds) => {
|
||||
this.trackedDuplicateNoteIds.set(noteId, [...duplicateNoteIds]);
|
||||
},
|
||||
findDuplicateNoteIds: (expression, noteInfo) =>
|
||||
this.findDuplicateNoteIds(expression, noteInfo),
|
||||
findDuplicateNoteIds: (expression, noteInfo) => this.findDuplicateNoteIds(expression, noteInfo),
|
||||
recordCardsMinedCallback: (count, noteIds) => {
|
||||
this.recordCardsMinedSafely(count, noteIds, 'card creation');
|
||||
},
|
||||
@@ -1083,7 +1082,10 @@ export class AnkiIntegration {
|
||||
});
|
||||
}
|
||||
|
||||
private async findDuplicateNoteIds(expression: string, noteInfo: NoteInfo): Promise<number[]> {
|
||||
private async findDuplicateNoteIds(
|
||||
expression: string,
|
||||
noteInfo: NoteInfo,
|
||||
): Promise<number[]> {
|
||||
return findDuplicateNoteIdsForAnkiIntegration(expression, -1, noteInfo, {
|
||||
findNotes: async (query, options) => (await this.client.findNotes(query, options)) as unknown,
|
||||
notesInfo: async (noteIds) => (await this.client.notesInfo(noteIds)) as unknown,
|
||||
|
||||
@@ -162,8 +162,7 @@ export class AnkiConnectProxyServer {
|
||||
}
|
||||
|
||||
try {
|
||||
const forwardedBody =
|
||||
req.method === 'POST' ? this.getForwardRequestBody(rawBody, requestJson) : rawBody;
|
||||
const forwardedBody = req.method === 'POST' ? this.getForwardRequestBody(rawBody, requestJson) : rawBody;
|
||||
const targetUrl = new URL(req.url || '/', upstreamUrl).toString();
|
||||
const contentType =
|
||||
typeof req.headers['content-type'] === 'string'
|
||||
@@ -273,9 +272,7 @@ export class AnkiConnectProxyServer {
|
||||
|
||||
private sanitizeRequestJson(requestJson: Record<string, unknown>): Record<string, unknown> {
|
||||
const action =
|
||||
typeof requestJson.action === 'string'
|
||||
? requestJson.action
|
||||
: String(requestJson.action ?? '');
|
||||
typeof requestJson.action === 'string' ? requestJson.action : String(requestJson.action ?? '');
|
||||
if (action !== 'addNote') {
|
||||
return requestJson;
|
||||
}
|
||||
@@ -304,13 +301,9 @@ export class AnkiConnectProxyServer {
|
||||
const rawNoteIds = Array.isArray(params?.subminerDuplicateNoteIds)
|
||||
? params.subminerDuplicateNoteIds
|
||||
: [];
|
||||
return [
|
||||
...new Set(
|
||||
rawNoteIds.filter((entry): entry is number => {
|
||||
return [...new Set(rawNoteIds.filter((entry): entry is number => {
|
||||
return typeof entry === 'number' && Number.isInteger(entry) && entry > 0;
|
||||
}),
|
||||
),
|
||||
].sort((left, right) => left - right);
|
||||
}))].sort((left, right) => left - right);
|
||||
}
|
||||
|
||||
private requestIncludesAddAction(action: string, requestJson: Record<string, unknown>): boolean {
|
||||
|
||||
@@ -113,7 +113,10 @@ interface CardCreationDeps {
|
||||
setUpdateInProgress: (value: boolean) => void;
|
||||
trackLastAddedNoteId?: (noteId: number) => void;
|
||||
trackLastAddedDuplicateNoteIds?: (noteId: number, duplicateNoteIds: number[]) => void;
|
||||
findDuplicateNoteIds?: (expression: string, noteInfo: CardCreationNoteInfo) => Promise<number[]>;
|
||||
findDuplicateNoteIds?: (
|
||||
expression: string,
|
||||
noteInfo: CardCreationNoteInfo,
|
||||
) => Promise<number[]>;
|
||||
recordCardsMinedCallback?: (count: number, noteIds?: number[]) => void;
|
||||
}
|
||||
|
||||
@@ -570,7 +573,10 @@ export class CardCreationService {
|
||||
await this.deps.findDuplicateNoteIds(pendingExpressionText, pendingNoteInfo),
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn('Failed to capture pre-add duplicate note ids:', (error as Error).message);
|
||||
log.warn(
|
||||
'Failed to capture pre-add duplicate note ids:',
|
||||
(error as Error).message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,7 +728,9 @@ export class CardCreationService {
|
||||
private createPendingNoteInfo(fields: Record<string, string>): CardCreationNoteInfo {
|
||||
return {
|
||||
noteId: -1,
|
||||
fields: Object.fromEntries(Object.entries(fields).map(([name, value]) => [name, { value }])),
|
||||
fields: Object.fromEntries(
|
||||
Object.entries(fields).map(([name, value]) => [name, { value }]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -307,11 +307,7 @@ test('findDuplicateNoteIds returns no matches when maxMatches is zero', async ()
|
||||
};
|
||||
|
||||
let notesInfoCalls = 0;
|
||||
const duplicateIds = await findDuplicateNoteIds(
|
||||
'貴様',
|
||||
100,
|
||||
currentNote,
|
||||
{
|
||||
const duplicateIds = await findDuplicateNoteIds('貴様', 100, currentNote, {
|
||||
findNotes: async () => [200],
|
||||
notesInfo: async (noteIds) => {
|
||||
notesInfoCalls += 1;
|
||||
@@ -325,9 +321,7 @@ test('findDuplicateNoteIds returns no matches when maxMatches is zero', async ()
|
||||
getDeck: () => 'Japanese::Mining',
|
||||
resolveFieldName: (noteInfo, preferredName) => createFieldResolver(noteInfo, preferredName),
|
||||
logWarn: () => {},
|
||||
},
|
||||
0,
|
||||
);
|
||||
}, 0);
|
||||
|
||||
assert.deepEqual(duplicateIds, []);
|
||||
assert.equal(notesInfoCalls, 0);
|
||||
|
||||
@@ -24,7 +24,13 @@ export async function findDuplicateNote(
|
||||
noteInfo: NoteInfo,
|
||||
deps: DuplicateDetectionDeps,
|
||||
): Promise<number | null> {
|
||||
const duplicateNoteIds = await findDuplicateNoteIds(expression, excludeNoteId, noteInfo, deps, 1);
|
||||
const duplicateNoteIds = await findDuplicateNoteIds(
|
||||
expression,
|
||||
excludeNoteId,
|
||||
noteInfo,
|
||||
deps,
|
||||
1,
|
||||
);
|
||||
return duplicateNoteIds[0] ?? null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ test('printHelp includes configured texthooker port', () => {
|
||||
|
||||
assert.match(output, /--help\s+Show this help/);
|
||||
assert.match(output, /default: 7777/);
|
||||
assert.match(output, /--launch-mpv.*Launch mpv with SubMiner defaults and exit/);
|
||||
assert.match(output, /--launch-mpv/);
|
||||
assert.match(output, /--stats\s+Open the stats dashboard in your browser/);
|
||||
assert.doesNotMatch(output, /--refresh-known-words/);
|
||||
assert.match(output, /--setup\s+Open first-run setup window/);
|
||||
|
||||
@@ -12,7 +12,7 @@ ${B}Usage:${R} subminer ${D}[command] [options]${R}
|
||||
${B}Session${R}
|
||||
--background Start in tray/background mode
|
||||
--start Connect to mpv and launch overlay
|
||||
--launch-mpv ${D}[targets...]${R} Launch mpv with SubMiner defaults and exit
|
||||
--launch-mpv ${D}[targets...]${R} Launch mpv with the SubMiner mpv profile and exit
|
||||
--stop Stop the running instance
|
||||
--stats Open the stats dashboard in your browser
|
||||
--texthooker Start texthooker server only ${D}(no overlay)${R}
|
||||
|
||||
@@ -37,9 +37,6 @@ test('loads defaults when config is missing', () => {
|
||||
assert.equal(config.jellyfin.remoteControlDeviceName, 'SubMiner');
|
||||
assert.equal(config.ai.enabled, false);
|
||||
assert.equal(config.ai.apiKeyCommand, '');
|
||||
assert.equal(config.texthooker.openBrowser, false);
|
||||
assert.equal(config.controller.enabled, false);
|
||||
assert.equal(config.ankiConnect.enabled, true);
|
||||
assert.deepEqual(config.ankiConnect.ai, {
|
||||
enabled: false,
|
||||
model: '',
|
||||
@@ -50,13 +47,12 @@ test('loads defaults when config is missing', () => {
|
||||
assert.equal(config.startupWarmups.yomitanExtension, true);
|
||||
assert.equal(config.startupWarmups.subtitleDictionaries, true);
|
||||
assert.equal(config.startupWarmups.jellyfinRemoteSession, true);
|
||||
assert.equal(config.discordPresence.enabled, true);
|
||||
assert.equal(config.discordPresence.enabled, false);
|
||||
assert.equal(config.discordPresence.updateIntervalMs, 3_000);
|
||||
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
|
||||
assert.equal(config.subtitleStyle.preserveLineBreaks, false);
|
||||
assert.equal(config.subtitleStyle.autoPauseVideoOnHover, true);
|
||||
assert.equal(config.subtitleStyle.autoPauseVideoOnYomitanPopup, true);
|
||||
assert.equal(config.subtitleSidebar.enabled, true);
|
||||
assert.equal(config.subtitleStyle.autoPauseVideoOnYomitanPopup, false);
|
||||
assert.equal(config.subtitleStyle.hoverTokenColor, '#f4dbd6');
|
||||
assert.equal(config.subtitleStyle.hoverTokenBackgroundColor, 'rgba(54, 58, 79, 0.84)');
|
||||
assert.equal(
|
||||
@@ -100,7 +96,6 @@ test('loads defaults when config is missing', () => {
|
||||
assert.equal(config.immersionTracking.lifetimeSummaries?.global, true);
|
||||
assert.equal(config.immersionTracking.lifetimeSummaries?.anime, true);
|
||||
assert.equal(config.immersionTracking.lifetimeSummaries?.media, true);
|
||||
assert.equal(config.stats.autoOpenBrowser, false);
|
||||
});
|
||||
|
||||
test('throws actionable startup parse error for malformed config at construction time', () => {
|
||||
@@ -2127,23 +2122,7 @@ test('template generator includes known keys', () => {
|
||||
assert.match(output, /"port": 6678,? \/\/ Annotated subtitle websocket server port\./);
|
||||
assert.match(
|
||||
output,
|
||||
/"openBrowser": false,? \/\/ Open browser setting\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"enabled": false,? \/\/ Enable overlay controller support through the Chrome Gamepad API\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"autoPauseVideoOnYomitanPopup": true,? \/\/ Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"enabled": true,? \/\/ Enable the subtitle sidebar feature for parsed subtitle sources\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"enabled": true,? \/\/ Enable AnkiConnect integration\. Values: true \| false/,
|
||||
/"enabled": false,? \/\/ Enable AnkiConnect integration\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
@@ -2159,15 +2138,7 @@ test('template generator includes known keys', () => {
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"enabled": true,? \/\/ Enable optional Discord Rich Presence updates\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"autoOpenBrowser": false,? \/\/ Automatically open the stats dashboard in a browser when the server starts\. Values: true \| false/,
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"primarySubLanguages": \[\s*"ja",\s*"jpn"\s*\],? \/\/ Comma-separated primary subtitle language priority for managed subtitle auto-selection\./,
|
||||
/"primarySubLanguages": \[\s*"ja",\s*"jpn"\s*\],? \/\/ Comma-separated primary subtitle language priority for YouTube auto-loading\./,
|
||||
);
|
||||
assert.doesNotMatch(output, /"mode": "automatic"/);
|
||||
assert.doesNotMatch(output, /"fixWithAi": false/);
|
||||
|
||||
@@ -35,7 +35,7 @@ const {
|
||||
startupWarmups,
|
||||
auto_start_overlay,
|
||||
} = CORE_DEFAULT_CONFIG;
|
||||
const { ankiConnect, jimaku, anilist, mpv, yomitan, jellyfin, discordPresence, ai, youtubeSubgen } =
|
||||
const { ankiConnect, jimaku, anilist, yomitan, jellyfin, discordPresence, ai, youtubeSubgen } =
|
||||
INTEGRATIONS_DEFAULT_CONFIG;
|
||||
const { subtitleStyle, subtitleSidebar } = SUBTITLE_DEFAULT_CONFIG;
|
||||
const { immersionTracking } = IMMERSION_DEFAULT_CONFIG;
|
||||
@@ -60,7 +60,6 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
auto_start_overlay,
|
||||
jimaku,
|
||||
anilist,
|
||||
mpv,
|
||||
yomitan,
|
||||
jellyfin,
|
||||
discordPresence,
|
||||
|
||||
@@ -31,10 +31,10 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
||||
},
|
||||
texthooker: {
|
||||
launchAtStartup: true,
|
||||
openBrowser: false,
|
||||
openBrowser: true,
|
||||
},
|
||||
controller: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
preferredGamepadId: '',
|
||||
preferredGamepadLabel: '',
|
||||
smoothScroll: true,
|
||||
|
||||
@@ -5,7 +5,6 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
| 'ankiConnect'
|
||||
| 'jimaku'
|
||||
| 'anilist'
|
||||
| 'mpv'
|
||||
| 'yomitan'
|
||||
| 'jellyfin'
|
||||
| 'discordPresence'
|
||||
@@ -13,7 +12,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
| 'youtubeSubgen'
|
||||
> = {
|
||||
ankiConnect: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
url: 'http://127.0.0.1:8765',
|
||||
pollingRate: 3000,
|
||||
proxy: {
|
||||
@@ -91,9 +90,6 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
languagePreference: 'ja',
|
||||
maxEntryResults: 10,
|
||||
},
|
||||
mpv: {
|
||||
executablePath: '',
|
||||
},
|
||||
anilist: {
|
||||
enabled: false,
|
||||
accessToken: '',
|
||||
@@ -132,7 +128,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
transcodeVideoCodec: 'h264',
|
||||
},
|
||||
discordPresence: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
presenceStyle: 'default' as const,
|
||||
updateIntervalMs: 3_000,
|
||||
debounceMs: 750,
|
||||
|
||||
@@ -6,6 +6,6 @@ export const STATS_DEFAULT_CONFIG: Pick<ResolvedConfig, 'stats'> = {
|
||||
markWatchedKey: 'KeyW',
|
||||
serverPort: 6969,
|
||||
autoStartServer: true,
|
||||
autoOpenBrowser: false,
|
||||
autoOpenBrowser: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
|
||||
enableJlpt: false,
|
||||
preserveLineBreaks: false,
|
||||
autoPauseVideoOnHover: true,
|
||||
autoPauseVideoOnYomitanPopup: true,
|
||||
autoPauseVideoOnYomitanPopup: false,
|
||||
hoverTokenColor: '#f4dbd6',
|
||||
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||
nameMatchEnabled: true,
|
||||
@@ -58,7 +58,7 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
|
||||
},
|
||||
},
|
||||
subtitleSidebar: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
autoOpen: false,
|
||||
layout: 'overlay',
|
||||
toggleKey: 'Backslash',
|
||||
|
||||
@@ -28,7 +28,6 @@ test('config option registry includes critical paths and has unique entries', ()
|
||||
'ankiConnect.enabled',
|
||||
'anilist.characterDictionary.enabled',
|
||||
'anilist.characterDictionary.collapsibleSections.description',
|
||||
'mpv.executablePath',
|
||||
'yomitan.externalProfilePath',
|
||||
'immersionTracking.enabled',
|
||||
]) {
|
||||
@@ -49,7 +48,6 @@ test('config template sections include expected domains and unique keys', () =>
|
||||
'subtitleStyle',
|
||||
'ankiConnect',
|
||||
'yomitan',
|
||||
'mpv',
|
||||
'immersionTracking',
|
||||
];
|
||||
|
||||
|
||||
@@ -87,8 +87,7 @@ export function buildCoreConfigOptionRegistry(
|
||||
path: 'youtube.primarySubLanguages',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.youtube.primarySubLanguages.join(','),
|
||||
description:
|
||||
'Comma-separated primary subtitle language priority for managed subtitle auto-selection.',
|
||||
description: 'Comma-separated primary subtitle language priority for YouTube auto-loading.',
|
||||
},
|
||||
{
|
||||
path: 'controller.enabled',
|
||||
|
||||
@@ -238,13 +238,6 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
description:
|
||||
'Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay',
|
||||
},
|
||||
{
|
||||
path: 'mpv.executablePath',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.mpv.executablePath,
|
||||
description:
|
||||
'Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.',
|
||||
},
|
||||
{
|
||||
path: 'jellyfin.enabled',
|
||||
kind: 'boolean',
|
||||
|
||||
@@ -41,7 +41,6 @@ export interface ConfigTemplateSection {
|
||||
export const SPECIAL_COMMANDS = {
|
||||
SUBSYNC_TRIGGER: '__subsync-trigger',
|
||||
RUNTIME_OPTIONS_OPEN: '__runtime-options-open',
|
||||
JIMAKU_OPEN: '__jimaku-open',
|
||||
RUNTIME_OPTION_CYCLE_PREFIX: '__runtime-option-cycle:',
|
||||
REPLAY_SUBTITLE: '__replay-subtitle',
|
||||
PLAY_NEXT_SUBTITLE: '__play-next-subtitle',
|
||||
|
||||
@@ -74,7 +74,7 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
title: 'Secondary Subtitles',
|
||||
description: [
|
||||
'Dual subtitle track options.',
|
||||
'Used by managed subtitle loading as secondary language preferences for local and YouTube playback.',
|
||||
'Used by the YouTube subtitle loading flow as secondary language preferences.',
|
||||
],
|
||||
notes: ['Hot-reload: defaultMode updates live while SubMiner is running.'],
|
||||
key: 'secondarySub',
|
||||
@@ -131,9 +131,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
},
|
||||
{
|
||||
title: 'YouTube Playback Settings',
|
||||
description: [
|
||||
'Defaults for managed subtitle language preferences and YouTube subtitle loading.',
|
||||
],
|
||||
description: ['Defaults for SubMiner YouTube subtitle loading and languages.'],
|
||||
key: 'youtube',
|
||||
},
|
||||
{
|
||||
@@ -155,14 +153,6 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
],
|
||||
key: 'yomitan',
|
||||
},
|
||||
{
|
||||
title: 'MPV Launcher',
|
||||
description: [
|
||||
'Optional mpv.exe override for Windows playback entry points.',
|
||||
'Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.',
|
||||
],
|
||||
key: 'mpv',
|
||||
},
|
||||
{
|
||||
title: 'Jellyfin',
|
||||
description: [
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { resolveConfig } from '../resolve';
|
||||
|
||||
test('resolveConfig trims configured mpv executable path', () => {
|
||||
const { resolved, warnings } = resolveConfig({
|
||||
mpv: {
|
||||
executablePath: ' C:\\Program Files\\mpv\\mpv.exe ',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(resolved.mpv.executablePath, 'C:\\Program Files\\mpv\\mpv.exe');
|
||||
assert.deepEqual(warnings, []);
|
||||
});
|
||||
|
||||
test('resolveConfig warns for invalid mpv executable path type', () => {
|
||||
const { resolved, warnings } = resolveConfig({
|
||||
mpv: {
|
||||
executablePath: 42 as never,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(resolved.mpv.executablePath, '');
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.deepEqual(warnings[0], {
|
||||
path: 'mpv.executablePath',
|
||||
value: 42,
|
||||
fallback: '',
|
||||
message: 'Expected string.',
|
||||
});
|
||||
});
|
||||
@@ -228,22 +228,6 @@ export function applyIntegrationConfig(context: ResolveContext): void {
|
||||
warn('yomitan', src.yomitan, resolved.yomitan, 'Expected object.');
|
||||
}
|
||||
|
||||
if (isObject(src.mpv)) {
|
||||
const executablePath = asString(src.mpv.executablePath);
|
||||
if (executablePath !== undefined) {
|
||||
resolved.mpv.executablePath = executablePath.trim();
|
||||
} else if (src.mpv.executablePath !== undefined) {
|
||||
warn(
|
||||
'mpv.executablePath',
|
||||
src.mpv.executablePath,
|
||||
resolved.mpv.executablePath,
|
||||
'Expected string.',
|
||||
);
|
||||
}
|
||||
} else if (src.mpv !== undefined) {
|
||||
warn('mpv', src.mpv, resolved.mpv, 'Expected object.');
|
||||
}
|
||||
|
||||
if (isObject(src.jellyfin)) {
|
||||
const enabled = asBoolean(src.jellyfin.enabled);
|
||||
if (enabled !== undefined) {
|
||||
|
||||
@@ -55,7 +55,7 @@ test('discordPresence invalid values warn and keep defaults', () => {
|
||||
|
||||
applyIntegrationConfig(context);
|
||||
|
||||
assert.equal(context.resolved.discordPresence.enabled, true);
|
||||
assert.equal(context.resolved.discordPresence.enabled, false);
|
||||
assert.equal(context.resolved.discordPresence.updateIntervalMs, 3_000);
|
||||
assert.equal(context.resolved.discordPresence.debounceMs, 750);
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ test('subtitleSidebar falls back and warns on invalid values', () => {
|
||||
|
||||
applySubtitleDomainConfig(context);
|
||||
|
||||
assert.equal(context.resolved.subtitleSidebar.enabled, true);
|
||||
assert.equal(context.resolved.subtitleSidebar.enabled, false);
|
||||
assert.equal(context.resolved.subtitleSidebar.autoOpen, false);
|
||||
assert.equal(context.resolved.subtitleSidebar.layout, 'overlay');
|
||||
assert.equal(context.resolved.subtitleSidebar.maxWidth, 420);
|
||||
|
||||
@@ -56,7 +56,7 @@ test('subtitleStyle autoPauseVideoOnYomitanPopup falls back on invalid value', (
|
||||
|
||||
applySubtitleDomainConfig(context);
|
||||
|
||||
assert.equal(context.resolved.subtitleStyle.autoPauseVideoOnYomitanPopup, true);
|
||||
assert.equal(context.resolved.subtitleStyle.autoPauseVideoOnYomitanPopup, false);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) =>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { createStatsApp, startStatsServer } from '../stats-server.js';
|
||||
@@ -1173,23 +1172,7 @@ describe('stats server API routes', () => {
|
||||
|
||||
const bun = globalThis as typeof globalThis & BunRuntime;
|
||||
const originalServe = bun.Bun.serve;
|
||||
const originalCreateServer = http.createServer;
|
||||
let listenedWith: { port: number; hostname: string } | null = null;
|
||||
let closeCalls = 0;
|
||||
bun.Bun.serve = undefined;
|
||||
(
|
||||
http as typeof http & {
|
||||
createServer: typeof http.createServer;
|
||||
}
|
||||
).createServer = (() =>
|
||||
({
|
||||
listen: (port: number, hostname: string) => {
|
||||
listenedWith = { port, hostname };
|
||||
},
|
||||
close: () => {
|
||||
closeCalls += 1;
|
||||
},
|
||||
}) as unknown as ReturnType<typeof http.createServer>) as typeof http.createServer;
|
||||
|
||||
try {
|
||||
const server = startStatsServer({
|
||||
@@ -1198,16 +1181,9 @@ describe('stats server API routes', () => {
|
||||
tracker: createMockTracker(),
|
||||
});
|
||||
|
||||
assert.deepEqual(listenedWith, { port: 0, hostname: '127.0.0.1' });
|
||||
server.close();
|
||||
assert.equal(closeCalls, 1);
|
||||
} finally {
|
||||
bun.Bun.serve = originalServe;
|
||||
(
|
||||
http as typeof http & {
|
||||
createServer: typeof http.createServer;
|
||||
}
|
||||
).createServer = originalCreateServer;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,9 +83,7 @@ const PRESENCE_STYLES: Record<DiscordPresenceStylePreset, PresenceStyleDefinitio
|
||||
},
|
||||
};
|
||||
|
||||
function resolvePresenceStyle(
|
||||
preset: DiscordPresenceStylePreset | undefined,
|
||||
): PresenceStyleDefinition {
|
||||
function resolvePresenceStyle(preset: DiscordPresenceStylePreset | undefined): PresenceStyleDefinition {
|
||||
return PRESENCE_STYLES[preset ?? 'default'] ?? PRESENCE_STYLES.default;
|
||||
}
|
||||
|
||||
@@ -132,7 +130,9 @@ export function buildDiscordPresenceActivity(
|
||||
const status = buildStatus(snapshot);
|
||||
const title = sanitizeText(snapshot.mediaTitle, basename(snapshot.mediaPath) || 'Unknown media');
|
||||
const details =
|
||||
snapshot.connected && snapshot.mediaPath ? trimField(title) : style.fallbackDetails;
|
||||
snapshot.connected && snapshot.mediaPath
|
||||
? trimField(title)
|
||||
: style.fallbackDetails;
|
||||
const timeline = `${formatClock(snapshot.currentTimeSec)} / ${formatClock(snapshot.mediaDurationSec)}`;
|
||||
const state =
|
||||
snapshot.connected && snapshot.mediaPath
|
||||
@@ -157,7 +157,10 @@ export function buildDiscordPresenceActivity(
|
||||
if (style.smallImageText.trim().length > 0) {
|
||||
activity.smallImageText = trimField(style.smallImageText.trim());
|
||||
}
|
||||
if (style.buttonLabel.trim().length > 0 && /^https?:\/\//.test(style.buttonUrl.trim())) {
|
||||
if (
|
||||
style.buttonLabel.trim().length > 0 &&
|
||||
/^https?:\/\//.test(style.buttonUrl.trim())
|
||||
) {
|
||||
activity.buttons = [
|
||||
{
|
||||
label: trimField(style.buttonLabel.trim(), 32),
|
||||
|
||||
@@ -380,22 +380,42 @@ export class ImmersionTrackerService {
|
||||
};
|
||||
};
|
||||
|
||||
const eventsRetention = daysToRetentionWindow(retention.eventsDays, 7, 3650);
|
||||
const telemetryRetention = daysToRetentionWindow(retention.telemetryDays, 30, 3650);
|
||||
const sessionsRetention = daysToRetentionWindow(retention.sessionsDays, 30, 3650);
|
||||
const eventsRetention = daysToRetentionWindow(
|
||||
retention.eventsDays,
|
||||
7,
|
||||
3650,
|
||||
);
|
||||
const telemetryRetention = daysToRetentionWindow(
|
||||
retention.telemetryDays,
|
||||
30,
|
||||
3650,
|
||||
);
|
||||
const sessionsRetention = daysToRetentionWindow(
|
||||
retention.sessionsDays,
|
||||
30,
|
||||
3650,
|
||||
);
|
||||
this.eventsRetentionMs = eventsRetention.ms;
|
||||
this.eventsRetentionDays = eventsRetention.days;
|
||||
this.telemetryRetentionMs = telemetryRetention.ms;
|
||||
this.telemetryRetentionDays = telemetryRetention.days;
|
||||
this.sessionsRetentionMs = sessionsRetention.ms;
|
||||
this.sessionsRetentionDays = sessionsRetention.days;
|
||||
this.dailyRollupRetentionMs = daysToRetentionWindow(retention.dailyRollupsDays, 365, 36500).ms;
|
||||
this.dailyRollupRetentionMs = daysToRetentionWindow(
|
||||
retention.dailyRollupsDays,
|
||||
365,
|
||||
36500,
|
||||
).ms;
|
||||
this.monthlyRollupRetentionMs = daysToRetentionWindow(
|
||||
retention.monthlyRollupsDays,
|
||||
5 * 365,
|
||||
36500,
|
||||
).ms;
|
||||
this.vacuumIntervalMs = daysToRetentionWindow(retention.vacuumIntervalDays, 7, 3650).ms;
|
||||
this.vacuumIntervalMs = daysToRetentionWindow(
|
||||
retention.vacuumIntervalDays,
|
||||
7,
|
||||
3650,
|
||||
).ms;
|
||||
this.db = new Database(this.dbPath);
|
||||
applyPragmas(this.db);
|
||||
ensureSchema(this.db);
|
||||
|
||||
@@ -1230,7 +1230,18 @@ test('getQueryHints counts new words by distinct headword first-seen time', () =
|
||||
headword, word, reading, part_of_speech, pos1, pos2, pos3, first_seen, last_seen, frequency
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run('猫', '猫', 'ねこ', 'noun', '名詞', '', '', String(twoDaysAgo), String(twoDaysAgo), 1);
|
||||
).run(
|
||||
'猫',
|
||||
'猫',
|
||||
'ねこ',
|
||||
'noun',
|
||||
'名詞',
|
||||
'',
|
||||
'',
|
||||
String(twoDaysAgo),
|
||||
String(twoDaysAgo),
|
||||
1,
|
||||
);
|
||||
|
||||
const hints = getQueryHints(db);
|
||||
assert.equal(hints.newWordsToday, 1);
|
||||
|
||||
@@ -82,9 +82,12 @@ function hasRetainedPriorSession(
|
||||
LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get(videoId, toDbTimestamp(startedAtMs), toDbTimestamp(startedAtMs), currentSessionId) as {
|
||||
found: number;
|
||||
} | null;
|
||||
.get(
|
||||
videoId,
|
||||
toDbTimestamp(startedAtMs),
|
||||
toDbTimestamp(startedAtMs),
|
||||
currentSessionId,
|
||||
) as { found: number } | null;
|
||||
return Boolean(row);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,9 +126,9 @@ test('pruneRawRetention skips disabled retention windows', () => {
|
||||
const remainingTelemetry = db
|
||||
.prepare('SELECT COUNT(*) AS count FROM imm_session_telemetry')
|
||||
.get() as { count: number };
|
||||
const remainingSessions = db.prepare('SELECT COUNT(*) AS count FROM imm_sessions').get() as {
|
||||
count: number;
|
||||
};
|
||||
const remainingSessions = db
|
||||
.prepare('SELECT COUNT(*) AS count FROM imm_sessions')
|
||||
.get() as { count: number };
|
||||
|
||||
assert.equal(result.deletedSessionEvents, 0);
|
||||
assert.equal(result.deletedTelemetryRows, 0);
|
||||
|
||||
@@ -56,7 +56,10 @@ export function pruneRawRetention(
|
||||
sessionsRetentionDays?: number;
|
||||
},
|
||||
): RawRetentionResult {
|
||||
const resolveCutoff = (retentionMs: number, retentionDays: number | undefined): string => {
|
||||
const resolveCutoff = (
|
||||
retentionMs: number,
|
||||
retentionDays: number | undefined,
|
||||
): string => {
|
||||
if (retentionDays !== undefined) {
|
||||
return subtractDbTimestamp(currentMs, BigInt(retentionDays) * 86_400_000n);
|
||||
}
|
||||
@@ -65,11 +68,9 @@ export function pruneRawRetention(
|
||||
|
||||
const deletedSessionEvents = Number.isFinite(policy.eventsRetentionMs)
|
||||
? (
|
||||
db
|
||||
.prepare(`DELETE FROM imm_session_events WHERE ts_ms < ?`)
|
||||
.run(resolveCutoff(policy.eventsRetentionMs, policy.eventsRetentionDays)) as {
|
||||
changes: number;
|
||||
}
|
||||
db.prepare(`DELETE FROM imm_session_events WHERE ts_ms < ?`).run(
|
||||
resolveCutoff(policy.eventsRetentionMs, policy.eventsRetentionDays),
|
||||
) as { changes: number }
|
||||
).changes
|
||||
: 0;
|
||||
const deletedTelemetryRows = Number.isFinite(policy.telemetryRetentionMs)
|
||||
|
||||
@@ -150,11 +150,9 @@ export function getSessionEvents(
|
||||
ORDER BY ts_ms ASC
|
||||
LIMIT ?
|
||||
`);
|
||||
const rows = stmt.all(sessionId, ...eventTypes, limit) as Array<
|
||||
SessionEventRow & {
|
||||
const rows = stmt.all(sessionId, ...eventTypes, limit) as Array<SessionEventRow & {
|
||||
tsMs: number | string;
|
||||
}
|
||||
>;
|
||||
}>;
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
tsMs: fromDbTimestamp(row.tsMs) ?? 0,
|
||||
|
||||
@@ -355,7 +355,9 @@ export function upsertCoverArt(
|
||||
const fetchedAtMs = toDbTimestamp(nowMs());
|
||||
const coverBlob = normalizeCoverBlobBytes(art.coverBlob);
|
||||
const computedCoverBlobHash =
|
||||
coverBlob && coverBlob.length > 0 ? createHash('sha256').update(coverBlob).digest('hex') : null;
|
||||
coverBlob && coverBlob.length > 0
|
||||
? createHash('sha256').update(coverBlob).digest('hex')
|
||||
: null;
|
||||
let coverBlobHash = computedCoverBlobHash ?? sharedCoverBlobHash ?? null;
|
||||
if (!coverBlobHash && (!coverBlob || coverBlob.length === 0)) {
|
||||
coverBlobHash = existing?.coverBlobHash ?? null;
|
||||
|
||||
@@ -39,12 +39,10 @@ export function getSessionSummaries(db: DatabaseSync, limit = 50): SessionSummar
|
||||
ORDER BY s.started_at_ms DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
const rows = prepared.all(limit) as Array<
|
||||
SessionSummaryQueryRow & {
|
||||
const rows = prepared.all(limit) as Array<SessionSummaryQueryRow & {
|
||||
startedAtMs: number | string;
|
||||
endedAtMs: number | string | null;
|
||||
}
|
||||
>;
|
||||
}>;
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
startedAtMs: fromDbTimestamp(row.startedAtMs) ?? 0,
|
||||
@@ -71,21 +69,19 @@ export function getSessionTimeline(
|
||||
`;
|
||||
|
||||
if (limit === undefined) {
|
||||
const rows = db.prepare(select).all(sessionId) as Array<
|
||||
SessionTimelineRow & {
|
||||
const rows = db.prepare(select).all(sessionId) as Array<SessionTimelineRow & {
|
||||
sampleMs: number | string;
|
||||
}
|
||||
>;
|
||||
}>;
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
sampleMs: fromDbTimestamp(row.sampleMs) ?? 0,
|
||||
}));
|
||||
}
|
||||
const rows = db.prepare(`${select}\n LIMIT ?`).all(sessionId, limit) as Array<
|
||||
SessionTimelineRow & {
|
||||
const rows = db
|
||||
.prepare(`${select}\n LIMIT ?`)
|
||||
.all(sessionId, limit) as Array<SessionTimelineRow & {
|
||||
sampleMs: number | string;
|
||||
}
|
||||
>;
|
||||
}>;
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
sampleMs: fromDbTimestamp(row.sampleMs) ?? 0,
|
||||
|
||||
@@ -359,7 +359,10 @@ function getNumericCalendarValue(
|
||||
return Number(row?.value ?? 0);
|
||||
}
|
||||
|
||||
export function getLocalEpochDay(db: DatabaseSync, timestampMs: number | bigint | string): number {
|
||||
export function getLocalEpochDay(
|
||||
db: DatabaseSync,
|
||||
timestampMs: number | bigint | string,
|
||||
): number {
|
||||
return getNumericCalendarValue(
|
||||
db,
|
||||
`
|
||||
@@ -372,7 +375,10 @@ export function getLocalEpochDay(db: DatabaseSync, timestampMs: number | bigint
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocalMonthKey(db: DatabaseSync, timestampMs: number | bigint | string): number {
|
||||
export function getLocalMonthKey(
|
||||
db: DatabaseSync,
|
||||
timestampMs: number | bigint | string,
|
||||
): number {
|
||||
return getNumericCalendarValue(
|
||||
db,
|
||||
`
|
||||
@@ -385,7 +391,10 @@ export function getLocalMonthKey(db: DatabaseSync, timestampMs: number | bigint
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocalDayOfWeek(db: DatabaseSync, timestampMs: number | bigint | string): number {
|
||||
export function getLocalDayOfWeek(
|
||||
db: DatabaseSync,
|
||||
timestampMs: number | bigint | string,
|
||||
): number {
|
||||
return getNumericCalendarValue(
|
||||
db,
|
||||
`
|
||||
@@ -398,7 +407,10 @@ export function getLocalDayOfWeek(db: DatabaseSync, timestampMs: number | bigint
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocalHourOfDay(db: DatabaseSync, timestampMs: number | bigint | string): number {
|
||||
export function getLocalHourOfDay(
|
||||
db: DatabaseSync,
|
||||
timestampMs: number | bigint | string,
|
||||
): number {
|
||||
return getNumericCalendarValue(
|
||||
db,
|
||||
`
|
||||
@@ -446,8 +458,7 @@ export function getShiftedLocalDayTimestamp(
|
||||
dayOffset: number,
|
||||
): string {
|
||||
const normalizedDayOffset = Math.trunc(dayOffset);
|
||||
const modifier =
|
||||
normalizedDayOffset >= 0 ? `+${normalizedDayOffset} days` : `${normalizedDayOffset} days`;
|
||||
const modifier = normalizedDayOffset >= 0 ? `+${normalizedDayOffset} days` : `${normalizedDayOffset} days`;
|
||||
const row = db
|
||||
.prepare(
|
||||
`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user