Compare commits
4 Commits
main
...
refactor-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
21c76c5097
|
|||
|
d10fda7136
|
|||
|
|
60cd1c8ac2 | ||
|
|
3da9d9e0e0 |
4
.github/workflows/release.yml
vendored
@@ -242,7 +242,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
tar -czf "release/subminer-assets.tar.gz" \
|
tar -czf "release/subminer-assets.tar.gz" \
|
||||||
config.example.jsonc \
|
config.example.jsonc \
|
||||||
plugin/subminer \
|
plugin/subminer.lua \
|
||||||
plugin/subminer.conf \
|
plugin/subminer.conf \
|
||||||
assets/themes/subminer.rasi
|
assets/themes/subminer.rasi
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ jobs:
|
|||||||
### Optional Assets (config example + mpv plugin + rofi theme)
|
### Optional Assets (config example + mpv plugin + rofi theme)
|
||||||
1. Download `subminer-assets.tar.gz`
|
1. Download `subminer-assets.tar.gz`
|
||||||
2. Extract and copy `config.example.jsonc` to `~/.config/SubMiner/config.jsonc`
|
2. Extract and copy `config.example.jsonc` to `~/.config/SubMiner/config.jsonc`
|
||||||
3. Copy `plugin/subminer/` directory contents to `~/.config/mpv/scripts/`
|
3. Copy `plugin/subminer.lua` to `~/.config/mpv/scripts/`
|
||||||
4. Copy `plugin/subminer.conf` to `~/.config/mpv/script-opts/`
|
4. Copy `plugin/subminer.conf` to `~/.config/mpv/script-opts/`
|
||||||
5. Copy `assets/themes/subminer.rasi` to:
|
5. Copy `assets/themes/subminer.rasi` to:
|
||||||
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
|
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
|
||||||
|
|||||||
4
.gitignore
vendored
@@ -7,7 +7,9 @@ dist/
|
|||||||
release/
|
release/
|
||||||
|
|
||||||
# Launcher build artifact (produced by make build-launcher)
|
# Launcher build artifact (produced by make build-launcher)
|
||||||
/subminer
|
subminer
|
||||||
|
!plugin/subminer/
|
||||||
|
!plugin/subminer/*.lua
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
<!-- BACKLOG.MD MCP GUIDELINES START -->
|
<!-- BACKLOG.MD MCP GUIDELINES START -->
|
||||||
|
|
||||||
<CRITICAL_INSTRUCTION>
|
<CRITICAL_INSTRUCTION>
|
||||||
@@ -16,7 +17,6 @@ This project uses Backlog.md MCP for all task and project management activities.
|
|||||||
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
||||||
|
|
||||||
These guides cover:
|
These guides cover:
|
||||||
|
|
||||||
- Decision framework for when to create tasks
|
- Decision framework for when to create tasks
|
||||||
- Search-first workflow to avoid duplicates
|
- Search-first workflow to avoid duplicates
|
||||||
- Links to detailed guides for task creation, execution, and finalization
|
- Links to detailed guides for task creation, execution, and finalization
|
||||||
|
|||||||
26
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-plugin uninstall uninstall-linux uninstall-macos print-dirs pretty ensure-bun generate-config generate-example-config docs-dev docs docs-preview docs-watch dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop
|
.PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-plugin uninstall uninstall-linux uninstall-macos uninstall-plugin print-dirs pretty ensure-bun generate-config generate-example-config docs-dev docs docs-preview dev-start dev-start-macos dev-toggle dev-stop
|
||||||
|
|
||||||
APP_NAME := subminer
|
APP_NAME := subminer
|
||||||
THEME_SOURCE := assets/themes/subminer.rasi
|
THEME_SOURCE := assets/themes/subminer.rasi
|
||||||
@@ -52,12 +52,9 @@ help:
|
|||||||
" clean Remove build artifacts (dist/, release/, AppImage, binary)" \
|
" clean Remove build artifacts (dist/, release/, AppImage, binary)" \
|
||||||
" dev-start Build and launch local Electron app" \
|
" dev-start Build and launch local Electron app" \
|
||||||
" dev-start-macos Build and launch local Electron app with macOS tracker backend" \
|
" dev-start-macos Build and launch local Electron app with macOS tracker backend" \
|
||||||
" dev-watch Start fast watch loop (tsc + renderer + Electron dev app)" \
|
|
||||||
" dev-watch-macos Start watch loop with forced macOS tracker backend" \
|
|
||||||
" dev-toggle Toggle overlay in a running local Electron app" \
|
" dev-toggle Toggle overlay in a running local Electron app" \
|
||||||
" dev-stop Stop a running local Electron app" \
|
" dev-stop Stop a running local Electron app" \
|
||||||
" docs-dev Run VitePress docs dev server" \
|
" docs-dev Run VitePress docs dev server" \
|
||||||
" docs-watch Run VitePress docs dev + Backlog browser together" \
|
|
||||||
" docs Build VitePress static docs" \
|
" docs Build VitePress static docs" \
|
||||||
" docs-preview Preview built VitePress docs" \
|
" docs-preview Preview built VitePress docs" \
|
||||||
" install-linux Install Linux wrapper/theme/app artifacts" \
|
" install-linux Install Linux wrapper/theme/app artifacts" \
|
||||||
@@ -69,6 +66,7 @@ help:
|
|||||||
" deps Install JS dependencies (root + texthooker-ui)" \
|
" deps Install JS dependencies (root + texthooker-ui)" \
|
||||||
" uninstall-linux Remove Linux install artifacts" \
|
" uninstall-linux Remove Linux install artifacts" \
|
||||||
" uninstall-macos Remove macOS install artifacts" \
|
" uninstall-macos Remove macOS install artifacts" \
|
||||||
|
" uninstall-plugin Remove mpv Lua plugin and plugin config" \
|
||||||
" print-dirs Show resolved install locations" \
|
" print-dirs Show resolved install locations" \
|
||||||
"" \
|
"" \
|
||||||
"Variables:" \
|
"Variables:" \
|
||||||
@@ -161,9 +159,6 @@ generate-example-config: ensure-bun
|
|||||||
docs-dev: ensure-bun
|
docs-dev: ensure-bun
|
||||||
@bun run docs:dev
|
@bun run docs:dev
|
||||||
|
|
||||||
docs-watch: ensure-bun
|
|
||||||
@bun run docs:watch
|
|
||||||
|
|
||||||
docs: ensure-bun
|
docs: ensure-bun
|
||||||
@bun run docs:build
|
@bun run docs:build
|
||||||
|
|
||||||
@@ -178,12 +173,6 @@ dev-start-macos: ensure-bun
|
|||||||
@bun run build
|
@bun run build
|
||||||
@bun run electron . --start --backend macos
|
@bun run electron . --start --backend macos
|
||||||
|
|
||||||
dev-watch: ensure-bun
|
|
||||||
@bash scripts/dev-watch.sh
|
|
||||||
|
|
||||||
dev-watch-macos: ensure-bun
|
|
||||||
@bash scripts/dev-watch.sh --start --dev --backend macos
|
|
||||||
|
|
||||||
dev-toggle: ensure-bun
|
dev-toggle: ensure-bun
|
||||||
@bun run electron . --toggle
|
@bun run electron . --toggle
|
||||||
|
|
||||||
@@ -229,22 +218,25 @@ install-macos: build-launcher
|
|||||||
install-plugin:
|
install-plugin:
|
||||||
@printf '%s\n' "[INFO] Installing mpv plugin artifacts"
|
@printf '%s\n' "[INFO] Installing mpv plugin artifacts"
|
||||||
@install -d "$(MPV_SCRIPTS_DIR)"
|
@install -d "$(MPV_SCRIPTS_DIR)"
|
||||||
@rm -f "$(MPV_SCRIPTS_DIR)/subminer.lua"
|
|
||||||
@install -d "$(MPV_SCRIPTS_DIR)/subminer"
|
@install -d "$(MPV_SCRIPTS_DIR)/subminer"
|
||||||
@install -d "$(MPV_SCRIPT_OPTS_DIR)"
|
@install -d "$(MPV_SCRIPT_OPTS_DIR)"
|
||||||
@cp -R ./plugin/subminer/. "$(MPV_SCRIPTS_DIR)/subminer/"
|
@cp -R ./plugin/subminer/. "$(MPV_SCRIPTS_DIR)/subminer/"
|
||||||
@install -m 0644 "./$(PLUGIN_CONF)" "$(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
@install -m 0644 "./$(PLUGIN_CONF)" "$(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
||||||
@printf '%s\n' "Installed to:" " $(MPV_SCRIPTS_DIR)/subminer/main.lua" " $(MPV_SCRIPTS_DIR)/subminer/" " $(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
@printf '%s\n' "Installed to:" " $(MPV_SCRIPTS_DIR)/subminer/main.lua" " $(MPV_SCRIPTS_DIR)/subminer/" " $(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
||||||
|
|
||||||
# Uninstall behavior kept unchanged by default.
|
|
||||||
uninstall: uninstall-linux
|
uninstall: uninstall-linux
|
||||||
|
|
||||||
uninstall-linux:
|
uninstall-plugin:
|
||||||
|
@rm -rf "$(MPV_SCRIPTS_DIR)/subminer"
|
||||||
|
@rm -f "$(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
||||||
|
@printf '%s\n' "Removed:" " $(MPV_SCRIPTS_DIR)/subminer/" " $(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
||||||
|
|
||||||
|
uninstall-linux: uninstall-plugin
|
||||||
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
||||||
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
|
|
||||||
uninstall-macos:
|
uninstall-macos: uninstall-plugin
|
||||||
@rm -f "$(BINDIR)/subminer"
|
@rm -f "$(BINDIR)/subminer"
|
||||||
@rm -f "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)"
|
@rm -f "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
@rm -rf "$(MACOS_APP_DEST)"
|
@rm -rf "$(MACOS_APP_DEST)"
|
||||||
|
|||||||
13
README.md
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](./assets/minecard.mp4)
|
[](./assets/minecard.mp4)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -26,9 +26,7 @@ SubMiner is an Electron overlay that sits on top of mpv. It turns your video pla
|
|||||||
|
|
||||||
- **Hover to look up** — Yomitan dictionary popups directly on subtitles
|
- **Hover to look up** — Yomitan dictionary popups directly on subtitles
|
||||||
- **One-key mining** — Creates Anki cards with sentence, audio, screenshot, and translation
|
- **One-key mining** — Creates Anki cards with sentence, audio, screenshot, and translation
|
||||||
- **Instant auto-enrichment** — Optional local AnkiConnect proxy enriches new Yomitan cards immediately
|
- **N+1 highlighting** — Marks known words from your Anki deck so unknown ones jump out
|
||||||
- **Reading annotations** — Combines N+1 targeting, frequency-dictionary highlighting, and JLPT underlining while you read
|
|
||||||
- **Hover-aware playback** — By default, hovering subtitle text pauses mpv and resumes on mouse leave (`subtitleStyle.autoPauseVideoOnHover`)
|
|
||||||
- **Subtitle tools** — Download from Jimaku, sync with alass/ffsubsync
|
- **Subtitle tools** — Download from Jimaku, sync with alass/ffsubsync
|
||||||
- **Immersion tracking** — SQLite-powered stats on your watch time and mining activity
|
- **Immersion tracking** — SQLite-powered stats on your watch time and mining activity
|
||||||
- **Custom texthooker page** — Built-in custom texthooker page and websocket, no extra setup
|
- **Custom texthooker page** — Built-in custom texthooker page and websocket, no extra setup
|
||||||
@@ -60,24 +58,23 @@ chmod +x ~/.local/bin/subminer
|
|||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
||||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
||||||
mkdir -p ~/.config/mpv/scripts/subminer
|
mkdir -p ~/.config/mpv/scripts/subminer
|
||||||
mkdir -p ~/.config/mpv/script-opts
|
|
||||||
cp -R /tmp/plugin/subminer/. ~/.config/mpv/scripts/subminer/
|
cp -R /tmp/plugin/subminer/. ~/.config/mpv/scripts/subminer/
|
||||||
cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
||||||
mkdir -p ~/.config/SubMiner && cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
mkdir -p ~/.config/SubMiner && cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### 3. Set up Yomitan Dictionaries
|
### 3. Set up Yomitan Dictionaries
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer app --yomitan
|
subminer app --start --yomitan
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Mine
|
### 4. Mine
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer app --start --background
|
subminer app --start --background
|
||||||
subminer video.mkv # default plugin config auto-starts visible overlay + resumes playback when ready
|
subminer video.mkv # toggle invisible overlay with y-i and visible overlay with y-t
|
||||||
subminer --start video.mkv # optional explicit overlay start when plugin auto_start=no
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 23 MiB After Width: | Height: | Size: 13 MiB |
|
Before Width: | Height: | Size: 303 KiB |
BIN
assets/minecard.png
Normal file
|
After Width: | Height: | Size: 523 KiB |
|
Before Width: | Height: | Size: 21 MiB |
@@ -1,11 +1,11 @@
|
|||||||
project_name: 'SubMiner'
|
project_name: "SubMiner"
|
||||||
default_status: 'To Do'
|
default_status: "To Do"
|
||||||
statuses: ['To Do', 'In Progress', 'Done']
|
statuses: ["To Do", "In Progress", "Done"]
|
||||||
labels: []
|
labels: []
|
||||||
definition_of_done: []
|
definition_of_done: []
|
||||||
date_format: yyyy-mm-dd
|
date_format: yyyy-mm-dd
|
||||||
max_column_width: 20
|
max_column_width: 20
|
||||||
default_editor: 'nvim'
|
default_editor: "nvim"
|
||||||
auto_open_browser: false
|
auto_open_browser: false
|
||||||
default_port: 6420
|
default_port: 6420
|
||||||
remote_operations: true
|
remote_operations: true
|
||||||
@@ -13,4 +13,4 @@ auto_commit: false
|
|||||||
bypass_git_hooks: false
|
bypass_git_hooks: false
|
||||||
check_active_branches: true
|
check_active_branches: true
|
||||||
active_branch_days: 30
|
active_branch_days: 30
|
||||||
task_prefix: 'task'
|
task_prefix: "task"
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: TASK-69
|
||||||
|
title: Refactor mpv plugin into modular script components
|
||||||
|
status: To Do
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-24 17:09'
|
||||||
|
labels: []
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Break plugin/subminer.lua into smaller Lua modules under mpv scripts subdirectory while preserving user-visible behavior and keybindings. Include migration + docs updates for install paths and smoke/regression checks.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [ ] #1 Plugin entrypoint stays at scripts/subminer.lua and loads modules from scripts/subminer/
|
||||||
|
- [ ] #2 No behavior change for keybindings, script messages, auto-start, AniSkip, hover highlight
|
||||||
|
- [ ] #3 Install/docs updated for recursive plugin copy
|
||||||
|
- [ ] #4 Add or update regression checks for start/stop + module loading
|
||||||
|
<!-- AC:END -->
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-70
|
|
||||||
title: >-
|
|
||||||
Overlay runtime refactor: remove invisible mode and bind visible overlay to
|
|
||||||
mpv subtitles
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 02:38'
|
|
||||||
updated_date: '2026-02-28 22:36'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- 'commit:a14c9da'
|
|
||||||
- 'commit:74554a3'
|
|
||||||
- 'commit:75442a4'
|
|
||||||
- 'commit:dde51f8'
|
|
||||||
- 'commit:9e4e588'
|
|
||||||
- src/main/overlay-runtime.ts
|
|
||||||
- src/main/runtime/overlay-mpv-sub-visibility.ts
|
|
||||||
- src/renderer/renderer.ts
|
|
||||||
- docs/plans/2026-02-26-secondary-subtitles-main-overlay.md
|
|
||||||
priority: medium
|
|
||||||
ordinal: 1000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Scope: Branch-only commits main..HEAD on refactor-overlay (a14c9da through 9e4e588) rebuilt overlay behavior around visible overlay mode and removed legacy invisible overlay paths.
|
|
||||||
|
|
||||||
Delivered behavior:
|
|
||||||
|
|
||||||
- Removed renderer invisible overlay layout/offset helpers and main hover-highlight runtime code paths.
|
|
||||||
- Added explicit overlay-to-mpv subtitle visibility synchronization so visible overlay state controls primary subtitle visibility consistently.
|
|
||||||
- Hardened overlay runtime/bootstrap lifecycle around modal fallback open state and bridge send path edge cases.
|
|
||||||
- Updated plugin/config/docs defaults to reflect visible-overlay-first behavior and subtitle binding controls.
|
|
||||||
|
|
||||||
Risk/impact context:
|
|
||||||
|
|
||||||
- Large cross-layer refactor touching runtime wiring, renderer event handling, and plugin behavior.
|
|
||||||
- Regression coverage added/updated for overlay runtime, mpv protocol handling, renderer cleanup, and subtitle rendering paths.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Completed and validated in branch commit set before merge. Refactor reduces dead overlay modes, centralizes subtitle visibility behavior, and documents new defaults/constraints.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-71
|
|
||||||
title: >-
|
|
||||||
Anki integration: add local AnkiConnect proxy transport for push-based
|
|
||||||
auto-enrichment
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 02:38'
|
|
||||||
updated_date: '2026-02-28 22:36'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- src/anki-integration/anki-connect-proxy.ts
|
|
||||||
- src/anki-integration/anki-connect-proxy.test.ts
|
|
||||||
- src/anki-integration.ts
|
|
||||||
- src/config/resolve/anki-connect.ts
|
|
||||||
- src/core/services/tokenizer/yomitan-parser-runtime.ts
|
|
||||||
- src/core/services/tokenizer/yomitan-parser-runtime.test.ts
|
|
||||||
- docs/anki-integration.md
|
|
||||||
- config.example.jsonc
|
|
||||||
priority: medium
|
|
||||||
ordinal: 2000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Scope: Current unmerged working-tree changes implement an optional local AnkiConnect-compatible proxy and transport switching for card enrichment.
|
|
||||||
|
|
||||||
Delivered behavior:
|
|
||||||
|
|
||||||
- Added proxy server that forwards AnkiConnect requests and enqueues addNote/addNotes note IDs for post-create enrichment, with de-duplication and loop-configuration protection.
|
|
||||||
- Added follow-up response-shape compatibility handling so proxy enqueue works for both envelope (`{result,error}`) and bare JSON payloads, including `multi` variants.
|
|
||||||
- Added config schema/defaults/resolution for ankiConnect.proxy (enabled, host, port, upstreamUrl) with validation warnings and fallback behavior.
|
|
||||||
- Runtime now supports transport switching (polling vs proxy) and restarts transport when runtime config patches change transport keys.
|
|
||||||
- Added Yomitan default-profile server sync helper to keep bundled parser profile aligned with configured Anki endpoint.
|
|
||||||
- Updated user docs/config examples for proxy mode setup, troubleshooting, and mining workflow behavior.
|
|
||||||
|
|
||||||
Risk/impact context:
|
|
||||||
|
|
||||||
- New network surface on local host/port; correctness depends on safe proxy upstream configuration and robust response handling.
|
|
||||||
- Tests added for proxy queue behavior, config resolution, and parser sync routines.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Completed implementation in branch working tree; ready to merge once local changes are committed and test gate passes.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-72
|
|
||||||
title: 'macOS config validation UX: show full warning details in native dialog'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 02:38'
|
|
||||||
updated_date: '2026-02-28 22:36'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- 'commit:cc2f9ef'
|
|
||||||
- src/main/config-validation.ts
|
|
||||||
- src/main/runtime/startup-config.ts
|
|
||||||
- docs/configuration.md
|
|
||||||
priority: low
|
|
||||||
ordinal: 3000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Scope: Commit cc2f9ef improves startup config-warning visibility on macOS by ensuring full details are surfaced in the native UI path and reflected in docs.
|
|
||||||
|
|
||||||
Delivered behavior:
|
|
||||||
|
|
||||||
- Config validation/runtime wiring updated so macOS users can access complete warning details instead of truncated notification-only text.
|
|
||||||
- Added/updated tests around config validation and startup config warning flows.
|
|
||||||
- Updated configuration docs to clarify platform-specific warning presentation behavior.
|
|
||||||
|
|
||||||
Risk/impact context:
|
|
||||||
|
|
||||||
- Low runtime risk; primarily user-facing diagnostics clarity improvement.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Completed small follow-up fix to reduce config-debug friction on macOS.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-73
|
|
||||||
title: 'MPV plugin: split into modules and optimize startup/command runtime'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 20:50'
|
|
||||||
updated_date: '2026-02-28 22:36'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- plugin/subminer/main.lua
|
|
||||||
- plugin/subminer/bootstrap.lua
|
|
||||||
- plugin/subminer/process.lua
|
|
||||||
- plugin/subminer/aniskip.lua
|
|
||||||
- plugin/subminer/environment.lua
|
|
||||||
- plugin/subminer/lifecycle.lua
|
|
||||||
- plugin/subminer/messages.lua
|
|
||||||
- plugin/subminer/ui.lua
|
|
||||||
- plugin/subminer/hover.lua
|
|
||||||
- plugin/subminer/options.lua
|
|
||||||
- plugin/subminer/state.lua
|
|
||||||
- plugin/subminer.conf
|
|
||||||
- scripts/test-plugin-start-gate.lua
|
|
||||||
- scripts/test-plugin-process-start-retries.lua
|
|
||||||
- launcher/commands/playback-command.ts
|
|
||||||
- launcher/mpv.ts
|
|
||||||
- launcher/mpv.test.ts
|
|
||||||
- launcher/smoke.e2e.test.ts
|
|
||||||
- Makefile
|
|
||||||
- package.json
|
|
||||||
- docs/mpv-plugin.md
|
|
||||||
- docs/installation.md
|
|
||||||
- docs/architecture.md
|
|
||||||
- README.md
|
|
||||||
priority: medium
|
|
||||||
ordinal: 4000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Scope: Replace monolithic `plugin/subminer.lua` with modular plugin runtime; optimize command execution paths; align install/docs/tests; fix launcher smoke instability.
|
|
||||||
|
|
||||||
Delivered behavior:
|
|
||||||
|
|
||||||
- Full plugin cutover to `plugin/subminer/main.lua` + module directory (no runtime compatibility shim with old monolith file).
|
|
||||||
- Process/control command path moved toward async subprocess usage for non-start actions (`stop`, `toggle`, `settings`, restart stop leg), reducing synchronous blocking in mpv script runtime.
|
|
||||||
- AniSkip path guarded: lookup runs only in SubMiner context (launcher metadata, explicit script-message refresh, or detected running app), instead of every opened file.
|
|
||||||
- AniSkip lookup pipeline moved to async subprocess calls (no sync `ps`/`curl` on `file-loaded`) with deferred fetch after auto-start and session-level MAL/title/payload caching.
|
|
||||||
- Startup/runtime loading updated with lazy module initialization via bootstrap proxies.
|
|
||||||
- Plugin install flow updated to copy `plugin/subminer/` directory and remove legacy `~/.config/mpv/scripts/subminer.lua` file.
|
|
||||||
- Added plugin gate script wiring to package scripts (`test:plugin:src`) and launcher test flow.
|
|
||||||
- Smoke tests stabilized across sandbox environments where UNIX socket bind can return `EPERM` while preserving normal-path assertions.
|
|
||||||
- Playback command cleanup race fixed when mpv exits before exit-listener registration.
|
|
||||||
|
|
||||||
Risk/impact context:
|
|
||||||
|
|
||||||
- mpv plugin loading path changed from single-file to module directory; packaging/install paths must stay consistent with release assets.
|
|
||||||
- Async control/AniSkip path changes reduce blocking but can surface timing differences; regression checks added for cold start, file-load gating, and explicit refresh behavior.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
AniSkip gate/async update delivered in plugin runtime:
|
|
||||||
|
|
||||||
- `plugin/subminer/lifecycle.lua`: deferred AniSkip fetch and overlay-start trigger.
|
|
||||||
- `plugin/subminer/aniskip.lua`: async lookup pipeline + context guard + session caches.
|
|
||||||
- `plugin/subminer/environment.lua`: async app-running detection with short cache.
|
|
||||||
- `plugin/subminer/messages.lua`: explicit script-message trigger wiring.
|
|
||||||
|
|
||||||
Regression coverage updated:
|
|
||||||
|
|
||||||
- `scripts/test-plugin-start-gate.lua` now verifies:
|
|
||||||
- no sync `ps`/`curl` on non-context file load
|
|
||||||
- no AniSkip network lookup on non-context file load
|
|
||||||
- script-message refresh forces async AniSkip lookup
|
|
||||||
|
|
||||||
Validation run:
|
|
||||||
|
|
||||||
- `bun run test:plugin:src` pass.
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-74
|
|
||||||
title: 'Startup warmups: configurable warmup vs defer with low-power mode'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-27 21:05'
|
|
||||||
updated_date: '2026-03-01 04:14'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- src/types.ts
|
|
||||||
- src/config/definitions/defaults-core.ts
|
|
||||||
- src/config/definitions/options-core.ts
|
|
||||||
- src/config/definitions/template-sections.ts
|
|
||||||
- src/config/resolve/core-domains.ts
|
|
||||||
- src/main/runtime/startup-warmups.ts
|
|
||||||
- src/main/runtime/startup-warmups-main-deps.ts
|
|
||||||
- src/main/runtime/composers/mpv-runtime-composer.ts
|
|
||||||
- src/core/services/startup.ts
|
|
||||||
- src/main.ts
|
|
||||||
- src/config/config.test.ts
|
|
||||||
- src/main/runtime/startup-warmups.test.ts
|
|
||||||
- src/main/runtime/startup-warmups-main-deps.test.ts
|
|
||||||
- src/core/services/app-ready.test.ts
|
|
||||||
priority: medium
|
|
||||||
ordinal: 7000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Add startup warmup controls to allow per-integration warmup or deferred first-use loading.
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- New config section `startupWarmups` with toggles for `mecab`, `yomitanExtension`, `subtitleDictionaries`, and `jellyfinRemoteSession`.
|
|
||||||
- New `startupWarmups.lowPowerMode` policy: defer everything except Yomitan extension.
|
|
||||||
- Keep default behavior as full warmup.
|
|
||||||
- Ensure deferred integrations lazy-load on first real usage path.
|
|
||||||
- Add test coverage for config parsing/defaults and warmup scheduling behavior.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Implemented:
|
|
||||||
|
|
||||||
- Added `startupWarmups` to config types/defaults/options/template/resolve.
|
|
||||||
- Warmup scheduler now uses per-integration gating functions.
|
|
||||||
- Low-power mode now defers MeCab, subtitle dictionaries, and Jellyfin remote session warmups while still warming Yomitan extension.
|
|
||||||
- Tokenization path guarantees lazy first-use init for deferred dependencies (Yomitan extension, MeCab when missing, subtitle dictionaries).
|
|
||||||
- Added/updated tests across config and runtime warmup modules.
|
|
||||||
|
|
||||||
Validation:
|
|
||||||
|
|
||||||
- `bun run test:config:src`
|
|
||||||
- `bun run test:core:src`
|
|
||||||
- `tsc --noEmit`
|
|
||||||
|
|
||||||
Follow-up updates:
|
|
||||||
|
|
||||||
- Startup now triggers warmups earlier in app-ready flow (right after config validation/log-level setup) instead of waiting for initial args/overlay actions. Goal: tokenization warmup is already done or mostly done by first visible-subs toggle.
|
|
||||||
- Tokenization warmup scheduling consolidated as `subtitle-tokenization` stage; when enabled by toggles, it runs Yomitan extension first, then MeCab/dictionary warmups.
|
|
||||||
- Added per-stage debug logs for warmup progress and skip reasons:
|
|
||||||
- `stage start/ready: yomitan-extension`
|
|
||||||
- `stage start/ready: mecab`
|
|
||||||
- `stage start/ready: subtitle-dictionaries`
|
|
||||||
- `stage start/ready: jellyfin-remote-session`
|
|
||||||
- `stage skipped: jellyfin-remote-session (disabled|auto-connect off)`
|
|
||||||
- Added regression tests for stage-level logging and earlier startup ordering:
|
|
||||||
- `src/main/runtime/startup-warmups.test.ts`
|
|
||||||
- `src/main/runtime/startup-warmups-main-deps.test.ts`
|
|
||||||
- `src/core/services/app-ready.test.ts`
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-75
|
|
||||||
title: 'Tokenizer: configurable POS exclusions for N+1 and frequency annotations'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-03-01 01:23'
|
|
||||||
updated_date: '2026-03-01 04:14'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 6000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
N+1 and frequency highlighting should ignore non-learning tokens (e.g., particles/auxiliary forms) based on MeCab POS1 tags, while remaining user-configurable.
|
|
||||||
|
|
||||||
Problem example: for subtitle phrase containing になれば, the highlighted N+1 target should not be the non-useful inflection/token piece when POS indicates an excluded class.
|
|
||||||
|
|
||||||
Implement configurable exclusion defaults with add/remove overrides so users can tune behavior without code changes.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 Default exclusion set omits non-useful POS1 classes from both N+1 candidate selection and frequency highlighting.
|
|
||||||
- [x] #2 Users can add extra POS1 exclusions and remove defaults via config.
|
|
||||||
- [x] #3 Tokenizer/annotation tests cover default behavior and config add/remove overrides.
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Implemented configurable annotation POS exclusions with defaults+add/remove for both MeCab POS1 and POS2, wired to N+1 candidate selection and frequency highlighting. Added POS2 default exclusion (非自立), expanded POS1 defaults for function words, added Yomitan->MeCab enrichment to carry pos2/pos3 metadata, updated config docs/examples, and added regression tests including になれば case.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-76
|
|
||||||
title: 'Tokenizer: remove POS exclusion config surface and keep hardcoded defaults'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-03-01 02:45'
|
|
||||||
updated_date: '2026-03-01 04:14'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 5000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Remove user-facing config keys for annotation POS exclusions. Keep N+1/frequency POS exclusion behavior as built-in defaults with no config required.
|
|
||||||
|
|
||||||
Scope: remove config parsing/registry/docs/example for annotationFilters.pos1Exclusions/pos2Exclusions while preserving runtime filtering behavior.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 No user-facing config option exists for annotation POS exclusions.
|
|
||||||
- [x] #2 Runtime N+1/frequency exclusion behavior remains active via built-in defaults.
|
|
||||||
- [x] #3 Config/docs/example/tests updated accordingly.
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Removed user-facing subtitleStyle.annotationFilters POS exclusion configuration (schema/resolver/options/docs/example). POS-based N+1/frequency filtering now always uses built-in defaults in runtime. Preserved robust exclusion behavior including merged-token overlap POS handling and N+1-only MeCab enrichment path.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-77
|
|
||||||
title: 'Subtitle hover: auto-pause playback with config toggle'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 22:43'
|
|
||||||
updated_date: '2026-02-28 22:43'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 8000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Add a user-facing subtitle config option to pause mpv playback when the cursor hovers subtitle text and resume playback when the cursor leaves.
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
- New config key: `subtitleStyle.autoPauseVideoOnHover`.
|
|
||||||
- Default should be enabled.
|
|
||||||
- Hover pause/resume must not unpause if playback was already paused before hover.
|
|
||||||
- Docs/examples/tests updated.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 `subtitleStyle.autoPauseVideoOnHover` exists and defaults to `true`.
|
|
||||||
- [x] #2 Overlay pauses playback on subtitle hover and resumes on leave only when hover-triggered pause occurred.
|
|
||||||
- [x] #3 Main/renderer IPC exposes pause-state query for safe hover behavior.
|
|
||||||
- [x] #4 Config docs/examples and user docs/readme mention the new behavior and toggle.
|
|
||||||
- [x] #5 Regression tests cover config parsing/validation and hover behavior edge cases.
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Implemented `subtitleStyle.autoPauseVideoOnHover` with default `true`, wired through config defaults/resolution/types, renderer state/style, and mouse hover handlers. Added playback pause-state IPC (`getPlaybackPaused`) to avoid false resume when media was already paused. Added renderer hover behavior tests (including race/cancel case) and config/resolve tests. Updated config examples and docs (`README`, usage, shortcuts, mining workflow, configuration) to document default hover pause/resume behavior and disable path.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-78
|
|
||||||
title: 'Launcher + mpv plugin: auto-start visible overlay pause-until-ready and single-start guard'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 22:45'
|
|
||||||
updated_date: '2026-02-28 22:45'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 9000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Add startup gating behavior for wrapper + mpv plugin flow so playback starts paused when visible overlay auto-start is enabled, then auto-resumes only after subtitle tokenization is ready.
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
- Plugin option `auto_start_pause_until_ready` (default `yes`).
|
|
||||||
- Launcher reads plugin runtime config and starts mpv paused when `auto_start=yes`, `auto_start_visible_overlay=yes`, and `auto_start_pause_until_ready=yes`.
|
|
||||||
- Main process signals readiness via mpv script message after tokenized subtitle delivery.
|
|
||||||
- Prevent duplicate auto-start attempts from showing `SubMiner already running` OSD.
|
|
||||||
- Keep startup/loading OSD messaging visible and update docs/tests.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 Launcher reads `auto_start`, `auto_start_visible_overlay`, and `auto_start_pause_until_ready` from `subminer.conf` and starts mpv with `--pause=yes` when all are enabled.
|
|
||||||
- [x] #2 Plugin pauses on eligible auto-start and resumes only on readiness signal or timeout fallback.
|
|
||||||
- [x] #3 Main process emits `script-message subminer-autoplay-ready` after subtitle tokenization is ready.
|
|
||||||
- [x] #4 Auto-start duplicate triggers are idempotent (no duplicate `--start` behavior and no spurious `Already running` OSD for auto-start path).
|
|
||||||
- [x] #5 Docs and regression tests cover defaults, startup gating behavior, and duplicate-start suppression.
|
|
||||||
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Implemented startup pause gate across launcher/plugin/main runtime:
|
|
||||||
- Added plugin runtime config parsing in launcher (`auto_start`, `auto_start_visible_overlay`, `auto_start_pause_until_ready`) and mpv start-paused behavior for eligible runs.
|
|
||||||
- Added plugin auto-play gate state, timeout fallback, and readiness release via `subminer-autoplay-ready` script message.
|
|
||||||
- Added main-process readiness signaling after tokenization delivery, including unpause fallback command path.
|
|
||||||
- Split auto-start visibility control into separate control commands and added duplicate auto-start idempotency guard to suppress repeated auto-start `Already running` noise.
|
|
||||||
- Updated plugin defaults to enabled (`auto_start=yes`, `auto_start_visible_overlay=yes`, `auto_start_pause_until_ready=yes`) and refreshed docs (`README`, usage, launcher, installation, plugin/config docs).
|
|
||||||
- Added/updated regression coverage (`scripts/test-plugin-start-gate.lua`, launcher smoke/unit tests) validating paused startup, readiness resume, and duplicate-start suppression.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -5,18 +5,26 @@
|
|||||||
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
|
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Overlay Auto-Start
|
// Overlay Auto-Start
|
||||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Visible Overlay Subtitle Binding
|
||||||
|
// Control whether visible overlay toggles also toggle MPV subtitle visibility.
|
||||||
|
// When enabled, visible overlay hides MPV subtitles; when disabled, MPV subtitles are left unchanged.
|
||||||
|
// ==========================================
|
||||||
|
"bind_visible_overlay_to_mpv_sub_visibility": true, // Link visible overlay toggles to MPV subtitle visibility (primary and secondary). Values: true | false
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Texthooker Server
|
// Texthooker Server
|
||||||
// Control whether browser opens automatically for texthooker.
|
// Control whether browser opens automatically for texthooker.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"texthooker": {
|
"texthooker": {
|
||||||
"openBrowser": true, // Open browser setting. Values: true | false
|
"openBrowser": true // Open browser setting. Values: true | false
|
||||||
}, // Control whether browser opens automatically for texthooker.
|
}, // Control whether browser opens automatically for texthooker.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -26,7 +34,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"websocket": {
|
"websocket": {
|
||||||
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
||||||
"port": 6677, // Built-in subtitle websocket server port.
|
"port": 6677 // Built-in subtitle websocket server port.
|
||||||
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -35,8 +43,8 @@
|
|||||||
// Set to debug for full runtime diagnostics.
|
// Set to debug for full runtime diagnostics.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"logging": {
|
"logging": {
|
||||||
"level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error
|
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||||
}, // Controls logging verbosity. Keep this as an object; do not replace with a bare string.
|
}, // Controls logging verbosity.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keyboard Shortcuts
|
// Keyboard Shortcuts
|
||||||
@@ -45,6 +53,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
||||||
|
"toggleInvisibleOverlayGlobal": "Alt+Shift+I", // Toggle invisible overlay global setting.
|
||||||
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
||||||
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
||||||
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
||||||
@@ -56,9 +65,19 @@
|
|||||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
||||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
||||||
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Invisible Overlay
|
||||||
|
// Startup behavior for the invisible interactive subtitle mining layer.
|
||||||
|
// Invisible subtitle position edit mode: Ctrl/Cmd+Shift+P to toggle, arrow keys to move, Enter or Ctrl/Cmd+S to save, Esc to cancel.
|
||||||
|
// This edit-mode shortcut is fixed and is not currently configurable.
|
||||||
|
// ==========================================
|
||||||
|
"invisibleOverlay": {
|
||||||
|
"startupVisibility": "platform-default" // Startup visibility setting.
|
||||||
|
}, // Startup behavior for the invisible interactive subtitle mining layer.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keybindings (MPV Commands)
|
// Keybindings (MPV Commands)
|
||||||
// Extra keybindings that are merged with built-in defaults.
|
// Extra keybindings that are merged with built-in defaults.
|
||||||
@@ -76,7 +95,7 @@
|
|||||||
"secondarySub": {
|
"secondarySub": {
|
||||||
"secondarySubLanguages": [], // Secondary sub languages setting.
|
"secondarySubLanguages": [], // Secondary sub languages setting.
|
||||||
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
||||||
"defaultMode": "hover", // Default mode setting.
|
"defaultMode": "hover" // Default mode setting.
|
||||||
}, // Dual subtitle track options.
|
}, // Dual subtitle track options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -87,7 +106,7 @@
|
|||||||
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
||||||
"alass_path": "", // Alass path setting.
|
"alass_path": "", // Alass path setting.
|
||||||
"ffsubsync_path": "", // Ffsubsync path setting.
|
"ffsubsync_path": "", // Ffsubsync path setting.
|
||||||
"ffmpeg_path": "", // Ffmpeg path setting.
|
"ffmpeg_path": "" // Ffmpeg path setting.
|
||||||
}, // Subsync engine and executable paths.
|
}, // Subsync engine and executable paths.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -95,7 +114,7 @@
|
|||||||
// Initial vertical subtitle position from the bottom.
|
// Initial vertical subtitle position from the bottom.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitlePosition": {
|
"subtitlePosition": {
|
||||||
"yPercent": 10, // Y percent setting.
|
"yPercent": 10 // Y percent setting.
|
||||||
}, // Initial vertical subtitle position from the bottom.
|
}, // Initial vertical subtitle position from the bottom.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -106,22 +125,13 @@
|
|||||||
"subtitleStyle": {
|
"subtitleStyle": {
|
||||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
"hoverTokenColor": "#c6a0f6", // Hex color used for hovered subtitle token highlight in mpv.
|
||||||
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
"fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif", // Font family setting.
|
||||||
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
|
||||||
"fontFamily": "M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
|
||||||
"fontSize": 35, // Font size setting.
|
"fontSize": 35, // Font size setting.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
"fontColor": "#cad3f5", // Font color setting.
|
||||||
"fontWeight": "600", // Font weight setting.
|
"fontWeight": "normal", // Font weight setting.
|
||||||
"lineHeight": 1.35, // Line height setting.
|
|
||||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
|
||||||
"wordSpacing": 0, // Word spacing setting.
|
|
||||||
"fontKerning": "normal", // Font kerning setting.
|
|
||||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
|
||||||
"textShadow": "0 3px 10px rgba(0,0,0,0.69)", // Text shadow setting.
|
|
||||||
"fontStyle": "normal", // Font style setting.
|
"fontStyle": "normal", // Font style setting.
|
||||||
"backgroundColor": "rgb(30, 32, 48, 0.88)", // Background color setting.
|
"backgroundColor": "rgb(30, 32, 48, 0.88)", // Background color setting.
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
|
||||||
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
|
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
|
||||||
"knownWordColor": "#a6da95", // Known word color setting.
|
"knownWordColor": "#a6da95", // Known word color setting.
|
||||||
"jlptColors": {
|
"jlptColors": {
|
||||||
@@ -129,32 +139,30 @@
|
|||||||
"N2": "#f5a97f", // N2 setting.
|
"N2": "#f5a97f", // N2 setting.
|
||||||
"N3": "#f9e2af", // N3 setting.
|
"N3": "#f9e2af", // N3 setting.
|
||||||
"N4": "#a6e3a1", // N4 setting.
|
"N4": "#a6e3a1", // N4 setting.
|
||||||
"N5": "#8aadf4", // N5 setting.
|
"N5": "#8aadf4" // N5 setting.
|
||||||
}, // Jlpt colors setting.
|
}, // Jlpt colors setting.
|
||||||
"frequencyDictionary": {
|
"frequencyDictionary": {
|
||||||
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
||||||
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, SubMiner searches installed/default frequency-dictionary locations.
|
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
|
||||||
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
|
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
|
||||||
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
||||||
"matchMode": "headword", // Frequency lookup text selection mode. Values: headword | surface
|
|
||||||
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
||||||
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
"bandedColors": [
|
||||||
|
"#ed8796",
|
||||||
|
"#f5a97f",
|
||||||
|
"#f9e2af",
|
||||||
|
"#a6e3a1",
|
||||||
|
"#8aadf4"
|
||||||
|
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||||
}, // Frequency dictionary setting.
|
}, // Frequency dictionary setting.
|
||||||
"secondary": {
|
"secondary": {
|
||||||
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
|
|
||||||
"fontSize": 24, // Font size setting.
|
"fontSize": 24, // Font size setting.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
"fontColor": "#ffffff", // Font color setting.
|
||||||
"lineHeight": 1.35, // Line height setting.
|
|
||||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
|
||||||
"wordSpacing": 0, // Word spacing setting.
|
|
||||||
"fontKerning": "normal", // Font kerning setting.
|
|
||||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
|
||||||
"textShadow": "0 3px 10px rgba(0,0,0,0.69)", // Text shadow setting.
|
|
||||||
"backgroundColor": "transparent", // Background color setting.
|
"backgroundColor": "transparent", // Background color setting.
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
|
||||||
"fontWeight": "normal", // Font weight setting.
|
"fontWeight": "normal", // Font weight setting.
|
||||||
"fontStyle": "normal", // Font style setting.
|
"fontStyle": "normal", // Font style setting.
|
||||||
}, // Secondary setting.
|
"fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif" // Font family setting.
|
||||||
|
} // Secondary setting.
|
||||||
}, // Primary and secondary subtitle styling.
|
}, // Primary and secondary subtitle styling.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -167,19 +175,15 @@
|
|||||||
"enabled": false, // Enable AnkiConnect integration. Values: true | false
|
"enabled": false, // Enable AnkiConnect integration. Values: true | false
|
||||||
"url": "http://127.0.0.1:8765", // Url setting.
|
"url": "http://127.0.0.1:8765", // Url setting.
|
||||||
"pollingRate": 3000, // Polling interval in milliseconds.
|
"pollingRate": 3000, // Polling interval in milliseconds.
|
||||||
"proxy": {
|
"tags": [
|
||||||
"enabled": false, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
"SubMiner"
|
||||||
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||||
"port": 8766, // Bind port for local AnkiConnect proxy.
|
|
||||||
"upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
|
||||||
}, // Proxy setting.
|
|
||||||
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
|
||||||
"fields": {
|
"fields": {
|
||||||
"audio": "ExpressionAudio", // Audio setting.
|
"audio": "ExpressionAudio", // Audio setting.
|
||||||
"image": "Picture", // Image setting.
|
"image": "Picture", // Image setting.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"sentence": "Sentence", // Sentence setting.
|
||||||
"miscInfo": "MiscInfo", // Misc info setting.
|
"miscInfo": "MiscInfo", // Misc info setting.
|
||||||
"translation": "SelectionText", // Translation setting.
|
"translation": "SelectionText" // Translation setting.
|
||||||
}, // Fields setting.
|
}, // Fields setting.
|
||||||
"ai": {
|
"ai": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
@@ -188,7 +192,7 @@
|
|||||||
"model": "openai/gpt-4o-mini", // Model setting.
|
"model": "openai/gpt-4o-mini", // Model setting.
|
||||||
"baseUrl": "https://openrouter.ai/api", // Base url setting.
|
"baseUrl": "https://openrouter.ai/api", // Base url setting.
|
||||||
"targetLanguage": "English", // Target language setting.
|
"targetLanguage": "English", // Target language setting.
|
||||||
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations." // System prompt setting.
|
||||||
}, // Ai setting.
|
}, // Ai setting.
|
||||||
"media": {
|
"media": {
|
||||||
"generateAudio": true, // Generate audio setting. Values: true | false
|
"generateAudio": true, // Generate audio setting. Values: true | false
|
||||||
@@ -201,7 +205,7 @@
|
|||||||
"animatedCrf": 35, // Animated crf setting.
|
"animatedCrf": 35, // Animated crf setting.
|
||||||
"audioPadding": 0.5, // Audio padding setting.
|
"audioPadding": 0.5, // Audio padding setting.
|
||||||
"fallbackDuration": 3, // Fallback duration setting.
|
"fallbackDuration": 3, // Fallback duration setting.
|
||||||
"maxMediaDuration": 30, // Max media duration setting.
|
"maxMediaDuration": 30 // Max media duration setting.
|
||||||
}, // Media setting.
|
}, // Media setting.
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
||||||
@@ -209,7 +213,7 @@
|
|||||||
"mediaInsertMode": "append", // Media insert mode setting.
|
"mediaInsertMode": "append", // Media insert mode setting.
|
||||||
"highlightWord": true, // Highlight word setting. Values: true | false
|
"highlightWord": true, // Highlight word setting. Values: true | false
|
||||||
"notificationType": "osd", // Notification type setting.
|
"notificationType": "osd", // Notification type setting.
|
||||||
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false
|
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
||||||
}, // Behavior setting.
|
}, // Behavior setting.
|
||||||
"nPlusOne": {
|
"nPlusOne": {
|
||||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||||
@@ -218,20 +222,20 @@
|
|||||||
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
||||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||||
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
||||||
"knownWord": "#a6da95", // Color used for legacy known-word highlights.
|
"knownWord": "#a6da95" // Color used for legacy known-word highlights.
|
||||||
}, // N plus one setting.
|
}, // N plus one setting.
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"pattern": "[SubMiner] %f (%t)", // Pattern setting.
|
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
|
||||||
}, // Metadata setting.
|
}, // Metadata setting.
|
||||||
"isLapis": {
|
"isLapis": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
"sentenceCardModel": "Japanese sentences", // Sentence card model setting.
|
"sentenceCardModel": "Japanese sentences" // Sentence card model setting.
|
||||||
}, // Is lapis setting.
|
}, // Is lapis setting.
|
||||||
"isKiku": {
|
"isKiku": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
||||||
"deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false
|
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
|
||||||
}, // Is kiku setting.
|
} // Is kiku setting.
|
||||||
}, // Automatic Anki updates and media generation options.
|
}, // Automatic Anki updates and media generation options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -241,7 +245,7 @@
|
|||||||
"jimaku": {
|
"jimaku": {
|
||||||
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
||||||
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
||||||
"maxEntryResults": 10, // Maximum Jimaku search results returned.
|
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
||||||
}, // Jimaku API configuration and defaults.
|
}, // Jimaku API configuration and defaults.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -252,7 +256,10 @@
|
|||||||
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
|
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
|
||||||
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
|
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
|
||||||
"whisperModel": "", // Path to whisper model used for fallback transcription.
|
"whisperModel": "", // Path to whisper model used for fallback transcription.
|
||||||
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher.
|
"primarySubLanguages": [
|
||||||
|
"ja",
|
||||||
|
"jpn"
|
||||||
|
] // Comma-separated primary subtitle language priority used by the launcher.
|
||||||
}, // Defaults for subminer YouTube subtitle extraction/transcription mode.
|
}, // Defaults for subminer YouTube subtitle extraction/transcription mode.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -261,7 +268,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"anilist": {
|
"anilist": {
|
||||||
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
||||||
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
"accessToken": "" // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
||||||
}, // Anilist API credentials and update behavior.
|
}, // Anilist API credentials and update behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -285,8 +292,16 @@
|
|||||||
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
||||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
||||||
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
||||||
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions.
|
"directPlayContainers": [
|
||||||
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
|
"mkv",
|
||||||
|
"mp4",
|
||||||
|
"webm",
|
||||||
|
"mov",
|
||||||
|
"flac",
|
||||||
|
"mp3",
|
||||||
|
"aac"
|
||||||
|
], // Container allowlist for direct play decisions.
|
||||||
|
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
|
||||||
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -297,7 +312,7 @@
|
|||||||
"discordPresence": {
|
"discordPresence": {
|
||||||
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||||
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates.
|
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||||
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -319,7 +334,7 @@
|
|||||||
"telemetryDays": 30, // Telemetry retention window in days.
|
"telemetryDays": 30, // Telemetry retention window in days.
|
||||||
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
||||||
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
||||||
"vacuumIntervalDays": 7, // Minimum days between VACUUM runs.
|
"vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
|
||||||
}, // Retention setting.
|
} // Retention setting.
|
||||||
}, // Enable/disable immersion tracking.
|
} // Enable/disable immersion tracking.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ export default {
|
|||||||
{ text: 'Launcher Script', link: '/launcher-script' },
|
{ text: 'Launcher Script', link: '/launcher-script' },
|
||||||
{ text: 'Usage', link: '/usage' },
|
{ text: 'Usage', link: '/usage' },
|
||||||
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
||||||
// { text: 'Feature Demos', link: '/demos' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ make docs-preview # Preview built site at http://localhost:4173
|
|||||||
|
|
||||||
- [Installation](/installation) — Requirements, Linux/macOS/Windows install, mpv plugin setup
|
- [Installation](/installation) — Requirements, Linux/macOS/Windows install, mpv plugin setup
|
||||||
- [Usage](/usage) — `subminer` wrapper + subcommands (`jellyfin`, `yt`, `doctor`, `config`, `mpv`, `texthooker`, `app`), mpv plugin, keybindings
|
- [Usage](/usage) — `subminer` wrapper + subcommands (`jellyfin`, `yt`, `doctor`, `config`, `mpv`, `texthooker`, `app`), mpv plugin, keybindings
|
||||||
- [Mining Workflow](/mining-workflow) — End-to-end sentence mining guide, single overlay + modals, card creation
|
- [Mining Workflow](/mining-workflow) — End-to-end sentence mining guide, overlay layers, card creation
|
||||||
|
|
||||||
### Reference
|
### Reference
|
||||||
|
|
||||||
- [Configuration](/configuration) — Full config file reference and option details
|
- [Configuration](/configuration) — Full config file reference and option details
|
||||||
- [Keyboard Shortcuts](/shortcuts) — All global, overlay, mining, and plugin chord shortcuts in one place
|
- [Keyboard Shortcuts](/shortcuts) — All global, overlay, mining, and plugin chord shortcuts in one place
|
||||||
- [Anki Integration](/anki-integration) — AnkiConnect setup, proxy/polling transport, field mapping, media generation, field grouping
|
- [Anki Integration](/anki-integration) — AnkiConnect setup, field mapping, media generation, field grouping
|
||||||
- [Jellyfin Integration](/jellyfin-integration) — Optional Jellyfin auth, cast discovery, remote control, and playback launch
|
- [Jellyfin Integration](/jellyfin-integration) — Optional Jellyfin auth, cast discovery, remote control, and playback launch
|
||||||
- [Immersion Tracking](/immersion-tracking) — SQLite schema, retention/rollup policies, query templates, and extension points
|
- [Immersion Tracking](/immersion-tracking) — SQLite schema, retention/rollup policies, query templates, and extension points
|
||||||
- [Performance & Tuning](/troubleshooting#performance-and-resource-impact) — Resource usage and practical low-impact profile
|
- [Performance & Tuning](/troubleshooting#performance-and-resource-impact) — Resource usage and practical low-impact profile
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Anki Integration
|
# Anki Integration
|
||||||
|
|
||||||
SubMiner uses the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on to create and update Anki cards with sentence context, audio, and screenshots.
|
SubMiner uses the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on to create and update Anki cards with sentence context, audio, and screenshots.
|
||||||
This project is built primarily for [Kiku](https://kiku.youyoumu.my.id/) and [Lapis](https://github.com/donkuri/lapis) note types, including sentence-card and field-grouping behavior.
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -11,14 +10,9 @@ This project is built primarily for [Kiku](https://kiku.youyoumu.my.id/) and [La
|
|||||||
|
|
||||||
AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the port in AnkiConnect's settings, update `ankiConnect.url` in your SubMiner config.
|
AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the port in AnkiConnect's settings, update `ankiConnect.url` in your SubMiner config.
|
||||||
|
|
||||||
## Auto-Enrichment Transport
|
## How Polling Works
|
||||||
|
|
||||||
SubMiner supports two auto-enrichment transport modes:
|
SubMiner polls AnkiConnect at a regular interval (default: 3 seconds, configurable via `ankiConnect.pollingRate`) to detect new cards. When it finds a card that was added since the last poll:
|
||||||
|
|
||||||
1. `proxy` (default): runs a local AnkiConnect-compatible proxy and enriches cards immediately after successful `addNote` / `addNotes` / `multi` responses.
|
|
||||||
2. `polling`: polls AnkiConnect at `ankiConnect.pollingRate` (default: 3s).
|
|
||||||
|
|
||||||
In both modes, the enrichment workflow is the same:
|
|
||||||
|
|
||||||
1. Checks if a duplicate expression already exists (for field grouping).
|
1. Checks if a duplicate expression already exists (for field grouping).
|
||||||
2. Updates the sentence field with the current subtitle.
|
2. Updates the sentence field with the current subtitle.
|
||||||
@@ -26,83 +20,7 @@ In both modes, the enrichment workflow is the same:
|
|||||||
4. Fills the translation field from the secondary subtitle or AI.
|
4. Fills the translation field from the secondary subtitle or AI.
|
||||||
5. Writes metadata to the miscInfo field.
|
5. Writes metadata to the miscInfo field.
|
||||||
|
|
||||||
Polling mode uses the query `"deck:<your-deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks.
|
Polling uses the query `"deck:<your-deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks.
|
||||||
|
|
||||||
### Proxy Mode Setup (Yomitan / Texthooker)
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
"ankiConnect": {
|
|
||||||
"url": "http://127.0.0.1:8765", // real AnkiConnect
|
|
||||||
"proxy": {
|
|
||||||
"enabled": true,
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 8766,
|
|
||||||
"upstreamUrl": "http://127.0.0.1:8765"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then point Yomitan/clients to `http://127.0.0.1:8766` instead of `8765`.
|
|
||||||
|
|
||||||
When SubMiner loads the bundled Yomitan extension, it also attempts to update the **default Yomitan profile** (`profiles[0].options.anki.server`) to the active SubMiner endpoint:
|
|
||||||
|
|
||||||
- proxy URL when `ankiConnect.proxy.enabled` is `true`
|
|
||||||
- direct `ankiConnect.url` when proxy mode is disabled
|
|
||||||
|
|
||||||
To avoid clobbering custom setups, this auto-update only changes the default profile when its current server is blank or the stock Yomitan default (`http://127.0.0.1:8765`).
|
|
||||||
|
|
||||||
For browser-based Yomitan or other external clients (for example Texthooker in a normal browser profile), set their Anki server to the same proxy URL separately: `http://127.0.0.1:8766` (or your configured `proxy.host` + `proxy.port`).
|
|
||||||
|
|
||||||
### Browser/Yomitan external setup (separate profile)
|
|
||||||
|
|
||||||
If you want SubMiner to use proxy mode without touching your main/default Yomitan profile, create or select a separate Yomitan profile just for SubMiner and set its Anki server to the proxy URL.
|
|
||||||
|
|
||||||
That profile isolation gives you both benefits:
|
|
||||||
|
|
||||||
- SubMiner can auto-enrich immediately via proxy.
|
|
||||||
- Your default Yomitan profile keeps its existing Anki server setting.
|
|
||||||
|
|
||||||
In Yomitan, go to Settings → Profile and:
|
|
||||||
|
|
||||||
1. Create a profile for SubMiner (or choose one dedicated profile).
|
|
||||||
2. Open Anki settings for that profile.
|
|
||||||
3. Set server to `http://127.0.0.1:8766` (or your configured proxy URL).
|
|
||||||
4. Save and make that profile active when using SubMiner.
|
|
||||||
|
|
||||||
This is only for non-bundled, external/browser Yomitan or other clients. The bundled profile auto-update logic only targets `profiles[0]` when it is blank or still default.
|
|
||||||
|
|
||||||
### Proxy Troubleshooting (quick checks)
|
|
||||||
|
|
||||||
If auto-enrichment appears to do nothing:
|
|
||||||
|
|
||||||
1. Confirm proxy listener is running while SubMiner is active:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ss -ltnp | rg 8766
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Confirm requests can pass through the proxy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sS http://127.0.0.1:8766 \
|
|
||||||
-H 'content-type: application/json' \
|
|
||||||
-d '{"action":"version","version":2}'
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Check both log sinks:
|
|
||||||
|
|
||||||
- Launcher/mpv-integrated log: `~/.cache/SubMiner/mp.log`
|
|
||||||
- App runtime log: `~/.config/SubMiner/logs/SubMiner-YYYY-MM-DD.log`
|
|
||||||
|
|
||||||
4. Ensure config JSONC is valid and logging shape is correct:
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
"logging": {
|
|
||||||
"level": "debug"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`"logging": "debug"` is invalid for current schema and can break reload/start behavior.
|
|
||||||
|
|
||||||
## Field Mapping
|
## Field Mapping
|
||||||
|
|
||||||
@@ -268,17 +186,17 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
|
|||||||
|
|
||||||
**Disabled** (`"disabled"`): No duplicate detection. Each card is independent.
|
**Disabled** (`"disabled"`): No duplicate detection. Each card is independent.
|
||||||
|
|
||||||
**Auto** (`"auto"`): When a duplicate expression is found, SubMiner merges the new card into the existing one automatically. Both sentences, audio clips, and images are preserved, and exact duplicate values are collapsed to one entry. If `deleteDuplicateInAuto` is true, the new card is deleted after merging.
|
**Auto** (`"auto"`): When a duplicate expression is found, SubMiner merges the new card into the existing one automatically. Both sentences, audio clips, and images are preserved. If `deleteDuplicateInAuto` is true, the new card is deleted after merging.
|
||||||
|
|
||||||
**Manual** (`"manual"`): A modal appears in the overlay showing both cards. You choose which card to keep, preview the merge result, then confirm. The modal has a 90-second timeout, after which it cancels automatically.
|
**Manual** (`"manual"`): A modal appears in the overlay showing both cards. You choose which card to keep, preview the merge result, then confirm. The modal has a 90-second timeout, after which it cancels automatically.
|
||||||
|
|
||||||
### What Gets Merged
|
### What Gets Merged
|
||||||
|
|
||||||
| Field | Merge behavior |
|
| Field | Merge behavior |
|
||||||
| -------- | --------------------------------------------------------------- |
|
| -------- | -------------------------------------------------------------- |
|
||||||
| Sentence | Both sentences preserved (exact duplicate text is deduplicated) |
|
| Sentence | Both sentences preserved, labeled `[Original]` / `[Duplicate]` |
|
||||||
| Audio | Both `[sound:...]` entries kept (exact duplicates deduplicated) |
|
| Audio | Both `[sound:...]` entries kept |
|
||||||
| Image | Both images kept (exact duplicates deduplicated) |
|
| Image | Both images kept |
|
||||||
|
|
||||||
### Keyboard Shortcuts in the Modal
|
### Keyboard Shortcuts in the Modal
|
||||||
|
|
||||||
@@ -296,12 +214,6 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "http://127.0.0.1:8765",
|
"url": "http://127.0.0.1:8765",
|
||||||
"pollingRate": 3000,
|
"pollingRate": 3000,
|
||||||
"proxy": {
|
|
||||||
"enabled": false,
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 8766,
|
|
||||||
"upstreamUrl": "http://127.0.0.1:8765",
|
|
||||||
},
|
|
||||||
"fields": {
|
"fields": {
|
||||||
"audio": "ExpressionAudio",
|
"audio": "ExpressionAudio",
|
||||||
"image": "Picture",
|
"image": "Picture",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ SubMiner is split into three cooperating runtimes:
|
|||||||
|
|
||||||
- Electron desktop app (`src/`) for overlay/UI/runtime orchestration.
|
- Electron desktop app (`src/`) for overlay/UI/runtime orchestration.
|
||||||
- Launcher CLI (`launcher/`) for mpv/app command workflows.
|
- Launcher CLI (`launcher/`) for mpv/app command workflows.
|
||||||
- mpv Lua plugin (`plugin/subminer/init.lua` + module files) for player-side controls and IPC handoff.
|
- mpv Lua plugin (`plugin/subminer.lua`) for player-side controls and IPC handoff.
|
||||||
|
|
||||||
Within the desktop app, `src/main.ts` is a composition root that wires small runtime/domain modules plus core services.
|
Within the desktop app, `src/main.ts` is a composition root that wires small runtime/domain modules plus core services.
|
||||||
|
|
||||||
@@ -26,9 +26,7 @@ launcher/ # Standalone CLI launcher wrapper and mpv helpers
|
|||||||
config/ # Launcher config parsers + CLI parser builder
|
config/ # Launcher config parsers + CLI parser builder
|
||||||
main.ts # Launcher entrypoint and command dispatch
|
main.ts # Launcher entrypoint and command dispatch
|
||||||
plugin/
|
plugin/
|
||||||
subminer/ # Modular mpv plugin (init · main · bootstrap · lifecycle · process
|
subminer.lua # mpv plugin (auto-start, IPC, AniSkip + hover controls)
|
||||||
# state · messages · hover · ui · options · environment · log
|
|
||||||
# binary · aniskip · aniskip_match)
|
|
||||||
src/
|
src/
|
||||||
main-entry.ts # Background-mode bootstrap wrapper before loading main.js
|
main-entry.ts # Background-mode bootstrap wrapper before loading main.js
|
||||||
main.ts # Entry point — delegates to runtime composers/domain modules
|
main.ts # Entry point — delegates to runtime composers/domain modules
|
||||||
@@ -68,26 +66,24 @@ src/
|
|||||||
renderer/ # Overlay renderer (modularized UI/runtime)
|
renderer/ # Overlay renderer (modularized UI/runtime)
|
||||||
handlers/ # Keyboard/mouse interaction modules
|
handlers/ # Keyboard/mouse interaction modules
|
||||||
modals/ # Jimaku/Kiku/subsync/runtime-options/session-help modals
|
modals/ # Jimaku/Kiku/subsync/runtime-options/session-help modals
|
||||||
positioning/ # Subtitle position controller (drag-to-reposition)
|
positioning/ # Invisible-layer layout + offset controllers
|
||||||
window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS)
|
window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS)
|
||||||
jimaku/ # Jimaku API integration helpers
|
jimaku/ # Jimaku API integration helpers
|
||||||
subsync/ # Subtitle sync (alass/ffsubsync) helpers
|
subsync/ # Subtitle sync (alass/ffsubsync) helpers
|
||||||
subtitle/ # Subtitle processing utilities
|
subtitle/ # Subtitle processing utilities
|
||||||
tokenizers/ # Tokenizer implementations
|
tokenizers/ # Tokenizer implementations
|
||||||
anki-integration/ # AnkiConnect proxy server + note-update enrichment workflow
|
|
||||||
token-mergers/ # Token merge strategies
|
token-mergers/ # Token merge strategies
|
||||||
translators/ # AI translation providers
|
translators/ # AI translation providers
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service Layer (`src/core/services/`)
|
### Service Layer (`src/core/services/`)
|
||||||
|
|
||||||
- **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts`
|
- **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-window-geometry.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts`, `overlay-drop.ts`
|
||||||
- **Shortcuts/input:** `shortcut.ts`, `overlay-shortcut.ts`, `overlay-shortcut-handler.ts`, `shortcut-fallback.ts`, `numeric-shortcut.ts`
|
- **Shortcuts/input:** `shortcut.ts`, `overlay-shortcut.ts`, `overlay-shortcut-handler.ts`, `shortcut-fallback.ts`, `numeric-shortcut.ts`
|
||||||
- **MPV runtime:** `mpv.ts`, `mpv-transport.ts`, `mpv-protocol.ts`, `mpv-properties.ts`, `mpv-render-metrics.ts`
|
- **MPV runtime:** `mpv.ts`, `mpv-transport.ts`, `mpv-protocol.ts`, `mpv-properties.ts`, `mpv-render-metrics.ts`
|
||||||
- **Mining + Anki/Jimaku runtime:** `mining.ts`, `field-grouping.ts`, `field-grouping-overlay.ts`, `anki-jimaku.ts`, `anki-jimaku-ipc.ts`
|
- **Mining + Anki/Jimaku runtime:** `mining.ts`, `field-grouping.ts`, `field-grouping-overlay.ts`, `anki-jimaku.ts`, `anki-jimaku-ipc.ts`
|
||||||
- **Subtitle/token pipeline:** `subtitle-processing-controller.ts`, `subtitle-position.ts`, `subtitle-ws.ts`, `tokenizer.ts` + `tokenizer/*` stage modules (including `parser-enrichment-worker-runtime.ts` for async MeCab enrichment and `yomitan-parser-runtime.ts`)
|
- **Subtitle/token pipeline:** `subtitle-processing-controller.ts`, `subtitle-position.ts`, `subtitle-ws.ts`, `tokenizer.ts` + `tokenizer/*` stage modules
|
||||||
- **Integrations:** `jimaku.ts`, `subsync.ts`, `subsync-runner.ts`, `texthooker.ts`, `jellyfin.ts`, `jellyfin-remote.ts`, `discord-presence.ts`, `yomitan-extension-loader.ts`, `yomitan-settings.ts`
|
- **Integrations:** `jimaku.ts`, `subsync.ts`, `subsync-runner.ts`, `texthooker.ts`, `jellyfin.ts`, `jellyfin-remote.ts`, `discord-presence.ts`, `yomitan-extension-loader.ts`, `yomitan-settings.ts`
|
||||||
- **Anki integration:** `anki-integration.ts`, `anki-integration/anki-connect-proxy.ts` (local proxy for push-based auto-enrichment), `anki-integration/note-update-workflow.ts`
|
|
||||||
- **Config/runtime controls:** `config-hot-reload.ts`, `runtime-options-ipc.ts`, `cli-command.ts`, `startup.ts`
|
- **Config/runtime controls:** `config-hot-reload.ts`, `runtime-options-ipc.ts`, `cli-command.ts`, `startup.ts`
|
||||||
- **Domain submodules:** `anilist/*` (token/update queue/updater), `immersion-tracker/*` (storage/session/metadata/query/reducer)
|
- **Domain submodules:** `anilist/*` (token/update queue/updater), `immersion-tracker/*` (storage/session/metadata/query/reducer)
|
||||||
|
|
||||||
@@ -99,15 +95,15 @@ The renderer keeps `renderer.ts` focused on orchestration. UI behavior is delega
|
|||||||
src/renderer/
|
src/renderer/
|
||||||
renderer.ts # Entrypoint/orchestration only
|
renderer.ts # Entrypoint/orchestration only
|
||||||
context.ts # Shared runtime context contract
|
context.ts # Shared runtime context contract
|
||||||
state.ts # Centralized renderer mutable state (visible overlay only)
|
state.ts # Centralized renderer mutable state
|
||||||
error-recovery.ts # Global renderer error boundary + recovery actions
|
error-recovery.ts # Global renderer error boundary + recovery actions
|
||||||
overlay-content-measurement.ts # Reports rendered bounds to main process
|
overlay-content-measurement.ts # Reports rendered bounds to main process
|
||||||
subtitle-render.ts # Primary/secondary subtitle rendering + style application
|
subtitle-render.ts # Primary/secondary subtitle rendering + style application
|
||||||
positioning.ts # Facade export for positioning controller
|
positioning.ts # Facade export for positioning controller
|
||||||
yomitan-popup.ts # Yomitan popup iframe detection utilities
|
|
||||||
positioning/
|
positioning/
|
||||||
controller.ts # Subtitle drag-position controller
|
controller.ts # Position controller orchestration
|
||||||
position-state.ts # Position state helpers (yPercent)
|
invisible-layout*.ts # Invisible layer layout computations
|
||||||
|
position-state.ts # Position state helpers
|
||||||
handlers/
|
handlers/
|
||||||
keyboard.ts # Keybindings, chord handling, modal key routing
|
keyboard.ts # Keybindings, chord handling, modal key routing
|
||||||
mouse.ts # Hover/drag behavior, selection + observer wiring
|
mouse.ts # Hover/drag behavior, selection + observer wiring
|
||||||
@@ -125,11 +121,11 @@ src/renderer/
|
|||||||
### Launcher + Plugin Runtimes
|
### Launcher + Plugin Runtimes
|
||||||
|
|
||||||
- `launcher/main.ts` dispatches commands through `launcher/commands/*` and shared config readers in `launcher/config/*`. It handles mpv startup, app passthrough, Jellyfin helper commands, and playback handoff.
|
- `launcher/main.ts` dispatches commands through `launcher/commands/*` and shared config readers in `launcher/config/*`. It handles mpv startup, app passthrough, Jellyfin helper commands, and playback handoff.
|
||||||
- `plugin/subminer/init.lua` runs inside mpv and loads modular Lua files: `main.lua` (orchestration), `bootstrap.lua` (startup), `lifecycle.lua` (connect/disconnect), `process.lua` (process management), `state.lua` (shared state), `messages.lua` (IPC), `hover.lua` (hover-token highlight rendering), `ui.lua` (OSD rendering), `options.lua` (config), `environment.lua` (detection), `log.lua` (logging), `binary.lua` (path resolution), `aniskip.lua` + `aniskip_match.lua` (intro-skip UX).
|
- `plugin/subminer.lua` runs inside mpv and handles IPC startup checks, overlay toggles, hover-token messages, and AniSkip intro-skip UX.
|
||||||
|
|
||||||
## Flow Diagram
|
## Flow Diagram
|
||||||
|
|
||||||
The main process orchestrates a single primary overlay window plus modal surfaces: `main.ts` delegates to composition modules that wire together domain services. Subtitle layers (primary + secondary bar) are rendered in the same overlay renderer process, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough.
|
The main process has three layers: `main.ts` delegates to composition modules that wire together domain services. Three overlay windows (visible, invisible, secondary) run in separate Electron renderer processes, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
@@ -143,7 +139,7 @@ flowchart LR
|
|||||||
|
|
||||||
subgraph ExtRt["External Runtimes"]
|
subgraph ExtRt["External Runtimes"]
|
||||||
Launcher["launcher/<br/>CLI dispatch"]:::extrt
|
Launcher["launcher/<br/>CLI dispatch"]:::extrt
|
||||||
Plugin["subminer/init.lua<br/>mpv plugin"]:::extrt
|
Plugin["subminer.lua<br/>mpv plugin"]:::extrt
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Ext["External Systems"]
|
subgraph Ext["External Systems"]
|
||||||
@@ -166,9 +162,8 @@ flowchart LR
|
|||||||
|
|
||||||
subgraph Svc["Services — src/core/services/"]
|
subgraph Svc["Services — src/core/services/"]
|
||||||
Mpv["MPV Stack<br/>transport · protocol<br/>properties · metrics"]:::svc
|
Mpv["MPV Stack<br/>transport · protocol<br/>properties · metrics"]:::svc
|
||||||
OverlaySvc["Overlay Manager<br/>window · visibility · bridge<br/>mpv-sub-visibility"]:::svc
|
Overlay["Overlay Manager<br/>window · geometry<br/>visibility · bridge"]:::svc
|
||||||
Mining["Mining & Subtitles<br/>mining · field-grouping<br/>subtitle-ws · tokenizer"]:::svc
|
Mining["Mining & Subtitles<br/>mining · field-grouping<br/>subtitle-ws · tokenizer"]:::svc
|
||||||
AnkiProxy["Anki Integration<br/>anki-connect-proxy<br/>note-update-workflow"]:::svc
|
|
||||||
Integrations["Integrations<br/>jimaku · subsync<br/>texthooker · yomitan"]:::svc
|
Integrations["Integrations<br/>jimaku · subsync<br/>texthooker · yomitan"]:::svc
|
||||||
Tracking["Tracking<br/>anilist · jellyfin<br/>immersion · discord"]:::svc
|
Tracking["Tracking<br/>anilist · jellyfin<br/>immersion · discord"]:::svc
|
||||||
Config["Config & Runtime<br/>hot-reload<br/>runtime-options"]:::svc
|
Config["Config & Runtime<br/>hot-reload<br/>runtime-options"]:::svc
|
||||||
@@ -177,7 +172,9 @@ flowchart LR
|
|||||||
Bridge(["preload.ts<br/>Electron IPC"]):::bridge
|
Bridge(["preload.ts<br/>Electron IPC"]):::bridge
|
||||||
|
|
||||||
subgraph Rend["Renderer — src/renderer/"]
|
subgraph Rend["Renderer — src/renderer/"]
|
||||||
OverlayWin["Main overlay window<br/>primary + secondary subtitles"]:::rend
|
Visible["Visible window<br/>Yomitan lookups"]:::rend
|
||||||
|
Invisible["Invisible window<br/>mpv positioning"]:::rend
|
||||||
|
Secondary["Secondary window<br/>subtitle bar"]:::rend
|
||||||
UI["subtitle-render<br/>positioning<br/>handlers · modals"]:::rend
|
UI["subtitle-render<br/>positioning<br/>handlers · modals"]:::rend
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -188,16 +185,18 @@ flowchart LR
|
|||||||
Comp --> Svc
|
Comp --> Svc
|
||||||
|
|
||||||
mpvExt <-->|"JSON socket"| Mpv
|
mpvExt <-->|"JSON socket"| Mpv
|
||||||
AnkiExt <-->|"HTTP"| AnkiProxy
|
AnkiExt <-->|"HTTP"| Mining
|
||||||
JimakuExt <-->|"HTTP"| Integrations
|
JimakuExt <-->|"HTTP"| Integrations
|
||||||
TrackerExt <-->|"platform"| OverlaySvc
|
TrackerExt <-->|"platform"| Overlay
|
||||||
AnilistExt <-->|"HTTP"| Tracking
|
AnilistExt <-->|"HTTP"| Tracking
|
||||||
JellyfinExt <-->|"HTTP"| Tracking
|
JellyfinExt <-->|"HTTP"| Tracking
|
||||||
DiscordExt <-->|"RPC"| Integrations
|
DiscordExt <-->|"RPC"| Integrations
|
||||||
|
|
||||||
OverlaySvc & Mining --> Bridge
|
Overlay & Mining --> Bridge
|
||||||
Bridge --> OverlayWin
|
Bridge --> Visible
|
||||||
OverlayWin --> UI
|
Bridge --> Invisible
|
||||||
|
Bridge --> Secondary
|
||||||
|
Visible & Invisible & Secondary --> UI
|
||||||
|
|
||||||
style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
|
style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
|
style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
@@ -265,10 +264,10 @@ For domains migrated to reducer-style transitions (for example AniList token/que
|
|||||||
- **Module-level init:** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs all services, and wires dependency injection. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
|
- **Module-level init:** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs all services, and wires dependency injection. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
|
||||||
- **Startup:** If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.
|
- **Startup:** If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.
|
||||||
- **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to 26 properties), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`.
|
- **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to 26 properties), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`.
|
||||||
- **Overlay runtime:** `initializeOverlayRuntime()` creates the primary overlay window (interactive Yomitan lookups and subtitle rendering), registers global shortcuts, and sets up bounds tracking via the active window tracker. mpv subtitle suppression is handled by a dedicated `overlay-mpv-sub-visibility` service.
|
- **Overlay runtime:** `initializeOverlayRuntime()` creates three overlay windows — **visible** (interactive Yomitan lookups), **invisible** (mpv-matched subtitle positioning), and **secondary** (secondary subtitle bar, top 20% via `splitOverlayGeometryForSecondaryBar`) — then registers global shortcuts and sets initial bounds from the window tracker.
|
||||||
- **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check (with async worker thread), Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, AniList token refresh, and optional AnkiConnect proxy server. Warmup coverage is configurable through `startupWarmups` (including low-power mode that defers all but Yomitan).
|
- **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check, Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, and AniList token refresh.
|
||||||
- **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, overlay shortcuts, and hot-reload notifications route through runtime handlers/composers. Subtitle text flows through `SubtitlePipeline` (normalize → tokenize → merge), and results are sent to the main overlay renderer and modal surfaces.
|
- **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, overlay shortcuts, and hot-reload notifications route through runtime handlers/composers. Subtitle text flows through `SubtitlePipeline` (normalize → tokenize → merge), and results broadcast to all overlay windows.
|
||||||
- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, stops the AnkiConnect proxy server, and cleans Anki/AniList state.
|
- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, and cleans Anki/AniList state.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
@@ -299,24 +298,27 @@ flowchart LR
|
|||||||
|
|
||||||
OverlayInit["initializeOverlay<br/>Runtime()"]:::phase
|
OverlayInit["initializeOverlay<br/>Runtime()"]:::phase
|
||||||
|
|
||||||
OverlayInit --> MainWin["Main overlay window<br/>primary + secondary subtitles"]:::init
|
OverlayInit --> VisWin["Visible window<br/>Yomitan lookups"]:::init
|
||||||
|
OverlayInit --> InvWin["Invisible window<br/>mpv positioning"]:::init
|
||||||
|
OverlayInit --> SecWin["Secondary window<br/>subtitle bar"]:::init
|
||||||
OverlayInit --> Shortcuts["Register global<br/>shortcuts"]:::init
|
OverlayInit --> Shortcuts["Register global<br/>shortcuts"]:::init
|
||||||
|
|
||||||
MainWin --> Warmups
|
VisWin --> Warmups
|
||||||
|
InvWin --> Warmups
|
||||||
|
SecWin --> Warmups
|
||||||
Shortcuts --> Warmups
|
Shortcuts --> Warmups
|
||||||
|
|
||||||
Warmups["Background<br/>warmups"]:::phase
|
Warmups["Background<br/>warmups"]:::phase
|
||||||
|
|
||||||
subgraph WarmupGroup[" "]
|
subgraph WarmupGroup[" "]
|
||||||
direction TB
|
direction TB
|
||||||
W1["MeCab<br/>+ worker thread"]:::warmup
|
W1["MeCab"]:::warmup
|
||||||
W2["Yomitan"]:::warmup
|
W2["Yomitan"]:::warmup
|
||||||
W3["JLPT + freq<br/>dictionaries"]:::warmup
|
W3["JLPT + freq<br/>dictionaries"]:::warmup
|
||||||
W4["Jellyfin"]:::warmup
|
W4["Jellyfin"]:::warmup
|
||||||
W5["Discord"]:::warmup
|
W5["Discord"]:::warmup
|
||||||
W6["AniList"]:::warmup
|
W6["AniList"]:::warmup
|
||||||
W7["AnkiConnect<br/>proxy"]:::warmup
|
W1 ~~~ W2 ~~~ W3 ~~~ W4 ~~~ W5 ~~~ W6
|
||||||
W1 ~~~ W2 ~~~ W3 ~~~ W4 ~~~ W5 ~~~ W6 ~~~ W7
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Warmups --> WarmupGroup
|
Warmups --> WarmupGroup
|
||||||
@@ -328,7 +330,7 @@ flowchart LR
|
|||||||
ExtEvt["Shortcuts · config hot-reload"]:::runtime
|
ExtEvt["Shortcuts · config hot-reload"]:::runtime
|
||||||
MpvEvt & IpcEvt & ExtEvt --> Route["Route via composers"]:::runtime
|
MpvEvt & IpcEvt & ExtEvt --> Route["Route via composers"]:::runtime
|
||||||
Route --> Process["SubtitlePipeline<br/>normalize → tokenize → merge"]:::runtime
|
Route --> Process["SubtitlePipeline<br/>normalize → tokenize → merge"]:::runtime
|
||||||
Process --> Broadcast["Update AppState<br/>broadcast to renderer + modals"]:::runtime
|
Process --> Broadcast["Update AppState<br/>broadcast to windows"]:::runtime
|
||||||
end
|
end
|
||||||
|
|
||||||
WarmupGroup --> Loop
|
WarmupGroup --> Loop
|
||||||
@@ -340,7 +342,7 @@ flowchart LR
|
|||||||
Quit --> T1["Tray · config watcher<br/>global shortcuts"]:::shutdown
|
Quit --> T1["Tray · config watcher<br/>global shortcuts"]:::shutdown
|
||||||
Quit --> T2["WebSocket · texthooker<br/>mpv socket · OSD log"]:::shutdown
|
Quit --> T2["WebSocket · texthooker<br/>mpv socket · OSD log"]:::shutdown
|
||||||
Quit --> T3["Window tracker<br/>Yomitan parser"]:::shutdown
|
Quit --> T3["Window tracker<br/>Yomitan parser"]:::shutdown
|
||||||
Quit --> T4["Immersion tracker<br/>Jellyfin · Discord<br/>Anki proxy · AniList"]:::shutdown
|
Quit --> T4["Immersion tracker<br/>Jellyfin · Discord<br/>Anki · AniList"]:::shutdown
|
||||||
|
|
||||||
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
|
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ Malformed config syntax (invalid JSON/JSONC) is startup-blocking: SubMiner shows
|
|||||||
|
|
||||||
For valid JSON/JSONC with invalid option values, SubMiner uses warn-and-fallback behavior: it logs the bad key/value and continues with the default for that option.
|
For valid JSON/JSONC with invalid option values, SubMiner uses warn-and-fallback behavior: it logs the bad key/value and continues with the default for that option.
|
||||||
|
|
||||||
On macOS, these validation warnings also open a native dialog with full details (desktop notification banners can truncate long messages).
|
|
||||||
|
|
||||||
### Hot-Reload Behavior
|
### Hot-Reload Behavior
|
||||||
|
|
||||||
SubMiner watches the active config file (`config.jsonc` or `config.json`) while running and applies supported updates automatically.
|
SubMiner watches the active config file (`config.jsonc` or `config.json`) while running and applies supported updates automatically.
|
||||||
@@ -72,467 +70,26 @@ Restart-required changes:
|
|||||||
|
|
||||||
The configuration file includes several main sections:
|
The configuration file includes several main sections:
|
||||||
|
|
||||||
**Core Settings**
|
|
||||||
|
|
||||||
- [**Logging**](#logging) - Runtime log level
|
|
||||||
- [**Auto-Start Overlay**](#auto-start-overlay) - Automatically show overlay on MPV connection
|
|
||||||
- [**Startup Warmups**](#startup-warmups) - Control what preloads on startup vs first-use defer
|
|
||||||
- [**WebSocket Server**](#websocket-server) - Built-in subtitle broadcasting server
|
|
||||||
- [**Texthooker**](#texthooker) - Control browser opening behavior
|
|
||||||
|
|
||||||
**Subtitle Display**
|
|
||||||
|
|
||||||
- [**Subtitle Style**](#subtitle-style) - Appearance customization
|
|
||||||
- [**Subtitle Position**](#subtitle-position) - Overlay vertical positioning
|
|
||||||
- [**Secondary Subtitles**](#secondary-subtitles) - Dual subtitle track support
|
|
||||||
|
|
||||||
**Keyboard & Controls**
|
|
||||||
|
|
||||||
- [**Keybindings**](#keybindings) - MPV command shortcuts
|
|
||||||
- [**Shortcuts Configuration**](#shortcuts-configuration) - Overlay keyboard shortcuts
|
|
||||||
- [**Manual Card Update Shortcuts**](#manual-card-update-shortcuts) - Shortcuts for manual Anki card workflows
|
|
||||||
- [**Session Help Modal**](#session-help-modal) - In-overlay shortcut reference
|
|
||||||
- [**Runtime Option Palette**](#runtime-option-palette) - Live, session-only option toggles
|
|
||||||
|
|
||||||
**Anki Integration**
|
|
||||||
|
|
||||||
- [**AnkiConnect**](#ankiconnect) - Automatic Anki card creation with media
|
- [**AnkiConnect**](#ankiconnect) - Automatic Anki card creation with media
|
||||||
- [**Kiku/Lapis Integration**](#kiku-lapis-integration) - Sentence cards and duplicate handling for Kiku/Lapis note types
|
- [**Auto-Start Overlay**](#auto-start-overlay) - Automatically show overlay on MPV connection
|
||||||
- [**N+1 Word Highlighting**](#n1-word-highlighting) - Known-word cache and single-target highlighting
|
- [**Visible Overlay Subtitle Binding**](#visible-overlay-subtitle-binding) - Link visible overlay toggles to MPV subtitle visibility
|
||||||
- [**Field Grouping Modes**](#field-grouping-modes) - Kiku/Lapis duplicate card merging
|
|
||||||
|
|
||||||
**External Integrations**
|
|
||||||
|
|
||||||
- [**Jimaku**](#jimaku) - Jimaku API configuration and defaults
|
|
||||||
- [**Auto Subtitle Sync**](#auto-subtitle-sync) - Sync current subtitle with `alass`/`ffsubsync`
|
- [**Auto Subtitle Sync**](#auto-subtitle-sync) - Sync current subtitle with `alass`/`ffsubsync`
|
||||||
|
- [**Invisible Overlay**](#invisible-overlay) - Startup visibility behavior for the invisible mining layer
|
||||||
|
- [**Jimaku**](#jimaku) - Jimaku API configuration and defaults
|
||||||
- [**AniList**](#anilist) - Optional post-watch progress updates
|
- [**AniList**](#anilist) - Optional post-watch progress updates
|
||||||
- [**Jellyfin**](#jellyfin) - Optional Jellyfin auth, library listing, and playback launch
|
- [**Jellyfin**](#jellyfin) - Optional Jellyfin auth, library listing, and playback launch
|
||||||
- [**Discord Rich Presence**](#discord-rich-presence) - Optional Discord activity card updates
|
- [**Discord Rich Presence**](#discord-rich-presence) - Optional Discord activity card updates
|
||||||
|
- [**Keybindings**](#keybindings) - MPV command shortcuts
|
||||||
|
- [**Runtime Option Palette**](#runtime-option-palette) - Live, session-only option toggles
|
||||||
|
- [**Secondary Subtitles**](#secondary-subtitles) - Dual subtitle track support
|
||||||
|
- [**Shortcuts Configuration**](#shortcuts-configuration) - Overlay keyboard shortcuts
|
||||||
|
- [**Subtitle Position**](#subtitle-position) - Overlay vertical positioning
|
||||||
|
- [**Subtitle Style**](#subtitle-style) - Appearance customization
|
||||||
|
- [**Texthooker**](#texthooker) - Control browser opening behavior
|
||||||
|
- [**WebSocket Server**](#websocket-server) - Built-in subtitle broadcasting server
|
||||||
- [**Immersion Tracking**](#immersion-tracking) - Track subtitle sessions and mining activity in SQLite
|
- [**Immersion Tracking**](#immersion-tracking) - Track subtitle sessions and mining activity in SQLite
|
||||||
- [**YouTube Subtitle Generation**](#youtube-subtitle-generation) - Launcher defaults for yt-dlp + local whisper fallback
|
- [**YouTube Subtitle Generation**](#youtube-subtitle-generation) - Launcher defaults for yt-dlp + local whisper fallback
|
||||||
|
|
||||||
## Core Settings
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
|
|
||||||
Control the minimum log level for runtime output:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"logging": {
|
|
||||||
"level": "info"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Values | Description |
|
|
||||||
| ------- | ----------------------------------- | ------------------------------------------------ |
|
|
||||||
| `level` | `"debug"`, `"info"`, `"warn"`, `"error"` | Minimum log level for runtime logging (default: `"info"`) |
|
|
||||||
|
|
||||||
### Auto-Start Overlay
|
|
||||||
|
|
||||||
Control whether the overlay automatically becomes visible when it connects to mpv:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"auto_start_overlay": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Values | Description |
|
|
||||||
| -------------------- | --------------- | ------------------------------------------------------ |
|
|
||||||
| `auto_start_overlay` | `true`, `false` | Auto-show overlay on mpv connection (default: `false`) |
|
|
||||||
|
|
||||||
The mpv plugin controls startup overlay visibility via `auto_start_visible_overlay` in `subminer.conf`.
|
|
||||||
For wrapper-driven playback, `subminer.conf` can also enable startup pause gating with
|
|
||||||
`auto_start_pause_until_ready` (requires `auto_start=yes` + `auto_start_visible_overlay=yes`).
|
|
||||||
Current plugin defaults in `subminer.conf` are:
|
|
||||||
|
|
||||||
- `auto_start=yes`
|
|
||||||
- `auto_start_visible_overlay=yes`
|
|
||||||
- `auto_start_pause_until_ready=yes`
|
|
||||||
|
|
||||||
### Startup Warmups
|
|
||||||
|
|
||||||
Control which startup warmups run in the background versus deferring to first real usage:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"startupWarmups": {
|
|
||||||
"lowPowerMode": false,
|
|
||||||
"mecab": true,
|
|
||||||
"yomitanExtension": true,
|
|
||||||
"subtitleDictionaries": true,
|
|
||||||
"jellyfinRemoteSession": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Values | Description |
|
|
||||||
| ----------------------- | --------------- | ------------------------------------------------------------------------------------------------- |
|
|
||||||
| `lowPowerMode` | `true`, `false` | Defer all warmups except Yomitan extension |
|
|
||||||
| `mecab` | `true`, `false` | Warm up MeCab tokenizer at startup |
|
|
||||||
| `yomitanExtension` | `true`, `false` | Warm up Yomitan extension at startup |
|
|
||||||
| `subtitleDictionaries` | `true`, `false` | Warm up JLPT + frequency dictionaries at startup |
|
|
||||||
| `jellyfinRemoteSession` | `true`, `false` | Warm up Jellyfin remote session at startup (still requires Jellyfin remote auto-connect settings) |
|
|
||||||
|
|
||||||
Defaults warm everything (`true` for all toggles, `lowPowerMode: false`). Setting a warmup toggle to `false` defers that work until first usage.
|
|
||||||
|
|
||||||
### WebSocket Server
|
|
||||||
|
|
||||||
The overlay includes a built-in WebSocket server that broadcasts subtitle text to connected clients (such as texthooker-ui) for external processing.
|
|
||||||
|
|
||||||
By default, the server uses "auto" mode: it starts automatically unless [mpv_websocket](https://github.com/kuroahna/mpv_websocket) is detected at `~/.config/mpv/mpv_websocket`. If you have mpv_websocket installed, the built-in server is skipped to avoid conflicts.
|
|
||||||
|
|
||||||
See `config.example.jsonc` for detailed configuration options.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"websocket": {
|
|
||||||
"enabled": "auto",
|
|
||||||
"port": 6677
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Values | Description |
|
|
||||||
| --------- | ------------------------- | -------------------------------------------------------- |
|
|
||||||
| `enabled` | `true`, `false`, `"auto"` | `"auto"` (default) disables if mpv_websocket is detected |
|
|
||||||
| `port` | number | WebSocket server port (default: 6677) |
|
|
||||||
|
|
||||||
### Texthooker
|
|
||||||
|
|
||||||
Control whether the browser opens automatically when texthooker starts:
|
|
||||||
|
|
||||||
See `config.example.jsonc` for detailed configuration options.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"texthooker": {
|
|
||||||
"openBrowser": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Subtitle Display
|
|
||||||
|
|
||||||
### Subtitle Style
|
|
||||||
|
|
||||||
Customize the appearance of primary and secondary subtitles:
|
|
||||||
|
|
||||||
See `config.example.jsonc` for detailed configuration options.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"subtitleStyle": {
|
|
||||||
"fontFamily": "M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP",
|
|
||||||
"fontSize": 35,
|
|
||||||
"fontColor": "#cad3f5",
|
|
||||||
"fontWeight": "600",
|
|
||||||
"lineHeight": 1.35,
|
|
||||||
"letterSpacing": "-0.01em",
|
|
||||||
"wordSpacing": 0,
|
|
||||||
"fontKerning": "normal",
|
|
||||||
"textRendering": "geometricPrecision",
|
|
||||||
"textShadow": "0 3px 10px rgba(0,0,0,0.69)",
|
|
||||||
"fontStyle": "normal",
|
|
||||||
"backgroundColor": "rgb(30, 32, 48, 0.88)",
|
|
||||||
"backdropFilter": "blur(6px)",
|
|
||||||
"secondary": {
|
|
||||||
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif",
|
|
||||||
"fontSize": 24,
|
|
||||||
"fontColor": "#cad3f5",
|
|
||||||
"backgroundColor": "transparent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Values | Description |
|
|
||||||
| ---------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `fontFamily` | string | CSS font-family value (default: `"M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP"`) |
|
|
||||||
| `fontSize` | number (px) | Font size in pixels (default: `35`) |
|
|
||||||
| `fontColor` | string | Any CSS color value (default: `"#cad3f5"`) |
|
|
||||||
| `fontWeight` | string | CSS font-weight, e.g. `"bold"`, `"normal"`, `"600"` (default: `"600"`) |
|
|
||||||
| `fontStyle` | string | `"normal"` or `"italic"` (default: `"normal"`) |
|
|
||||||
| `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"rgb(30, 32, 48, 0.88)"`) |
|
|
||||||
| `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). |
|
|
||||||
| `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) |
|
|
||||||
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
|
|
||||||
| `frequencyDictionary.sourcePath` | string | Path to a local frequency dictionary root. Leave empty or omit to use installed/default frequency-dictionary search paths. |
|
|
||||||
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`1000` by default) |
|
|
||||||
| `frequencyDictionary.mode` | string | `"single"` or `"banded"` (`"single"` by default) |
|
|
||||||
| `frequencyDictionary.matchMode` | string | `"headword"` or `"surface"` (`"headword"` by default) |
|
|
||||||
| `frequencyDictionary.singleColor` | string | Color used for all highlighted tokens in single mode |
|
|
||||||
| `frequencyDictionary.bandedColors` | string[] | Array of five hex colors used for ranked bands in banded mode |
|
|
||||||
| `nPlusOneColor` | string | Existing n+1 highlight color (default: `#c6a0f6`) |
|
|
||||||
| `knownWordColor` | string | Existing known-word highlight color (default: `#a6da95`) |
|
|
||||||
| `jlptColors` | object | JLPT level underline colors object (`N1`..`N5`) |
|
|
||||||
| `secondary` | object | Override any of the above for secondary subtitles (optional) |
|
|
||||||
|
|
||||||
JLPT underlining is powered by offline term-meta bank files at runtime. See [`docs/jlpt-vocab-bundle.md`](jlpt-vocab-bundle.md) for required files, source/version refresh steps, and deterministic fallback behavior.
|
|
||||||
|
|
||||||
Frequency dictionary highlighting uses the same dictionary file format as JLPT bundle lookups (`term_meta_bank_*.json` under discovered dictionary directories). A token is highlighted when it has a positive integer `frequencyRank` (lower is more common) and the rank is within `topX`.
|
|
||||||
|
|
||||||
Lookup behavior:
|
|
||||||
|
|
||||||
- Set `frequencyDictionary.sourcePath` to a directory containing `term_meta_bank_*.json` for a fully custom source.
|
|
||||||
- If `sourcePath` is missing or empty, SubMiner searches default install/runtime locations for `frequency-dictionary` directories (for example app resources, user data paths, and current working directory).
|
|
||||||
- In both cases, only terms with a valid `frequencyRank` are used; everything else falls back to no highlighting.
|
|
||||||
- `frequencyDictionary.matchMode` controls which token text is used for frequency lookups: `headword` (dictionary form) or `surface` (visible subtitle text).
|
|
||||||
- Frequency highlighting skips tokens that look like non-lexical SFX/interjection noise (for example kana reduplication or short kana endings like `っ`), even when dictionary ranks exist.
|
|
||||||
|
|
||||||
In `single` mode all highlights use `singleColor`; in `banded` mode tokens map to five ascending color bands from most common to least common inside the topX window.
|
|
||||||
|
|
||||||
Secondary subtitle defaults: `fontFamily: "Inter, Noto Sans, Helvetica Neue, sans-serif"`, `fontSize: 24`, `fontColor: "#cad3f5"`, `backgroundColor: "transparent"`. Any property not set in `secondary` falls back to the CSS defaults.
|
|
||||||
|
|
||||||
**See `config.example.jsonc`** for the complete list of subtitle style configuration options.
|
|
||||||
|
|
||||||
`jlptColors` keys are:
|
|
||||||
|
|
||||||
| Key | Default | Description |
|
|
||||||
| ---- | --------- | ----------------------- |
|
|
||||||
| `N1` | `#ed8796` | JLPT N1 underline color |
|
|
||||||
| `N2` | `#f5a97f` | JLPT N2 underline color |
|
|
||||||
| `N3` | `#f9e2af` | JLPT N3 underline color |
|
|
||||||
| `N4` | `#a6e3a1` | JLPT N4 underline color |
|
|
||||||
| `N5` | `#8aadf4` | JLPT N5 underline color |
|
|
||||||
|
|
||||||
**Image Quality Notes:**
|
|
||||||
|
|
||||||
- `imageQuality` affects JPG and WebP only; PNG is lossless and ignores this setting
|
|
||||||
- JPG quality is mapped to FFmpeg's scale (2-31, lower = better)
|
|
||||||
- WebP quality uses FFmpeg's native 0-100 scale
|
|
||||||
|
|
||||||
### Subtitle Position
|
|
||||||
|
|
||||||
Set the initial vertical subtitle position (measured from the bottom of the screen):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"subtitlePosition": {
|
|
||||||
"yPercent": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Values | Description |
|
|
||||||
| ---------- | ---------------- | ---------------------------------------------------------------------- |
|
|
||||||
| `yPercent` | number (0 - 100) | Distance from the bottom as a percent of screen height (default: `10`) |
|
|
||||||
In the overlay, you can fine-tune subtitle position at runtime with `Right-click + drag` on subtitle text.
|
|
||||||
|
|
||||||
### Secondary Subtitles
|
|
||||||
|
|
||||||
Display a second subtitle track (e.g., English alongside Japanese) in the overlay:
|
|
||||||
|
|
||||||
See `config.example.jsonc` for detailed configuration options.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"secondarySub": {
|
|
||||||
"secondarySubLanguages": ["eng", "en"],
|
|
||||||
"autoLoadSecondarySub": true,
|
|
||||||
"defaultMode": "hover"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Values | Description |
|
|
||||||
| ----------------------- | ---------------------------------- | ------------------------------------------------------ |
|
|
||||||
| `secondarySubLanguages` | string[] | Language codes to auto-load (e.g., `["eng", "en"]`) |
|
|
||||||
| `autoLoadSecondarySub` | `true`, `false` | Auto-detect and load matching secondary subtitle track |
|
|
||||||
| `defaultMode` | `"hidden"`, `"visible"`, `"hover"` | Initial display mode (default: `"hover"`) |
|
|
||||||
|
|
||||||
**Display modes:**
|
|
||||||
|
|
||||||
- **hidden** — Secondary subtitles not shown
|
|
||||||
- **visible** — Always visible at top of overlay
|
|
||||||
- **hover** — Only visible when hovering over the subtitle area (default)
|
|
||||||
|
|
||||||
**See `config.example.jsonc`** for additional secondary subtitle configuration options.
|
|
||||||
|
|
||||||
## Keyboard & Controls
|
|
||||||
|
|
||||||
### Keybindings
|
|
||||||
|
|
||||||
Add a `keybindings` array to configure keyboard shortcuts that send commands to mpv:
|
|
||||||
|
|
||||||
See `config.example.jsonc` for detailed configuration options and more examples.
|
|
||||||
|
|
||||||
**Default keybindings:**
|
|
||||||
|
|
||||||
| Key | Command | Description |
|
|
||||||
| ----------------- | ---------------------------- | ------------------------------------- |
|
|
||||||
| `Space` | `["cycle", "pause"]` | Toggle pause |
|
|
||||||
| `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track |
|
|
||||||
| `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track |
|
|
||||||
| `ArrowRight` | `["seek", 5]` | Seek forward 5 seconds |
|
|
||||||
| `ArrowLeft` | `["seek", -5]` | Seek backward 5 seconds |
|
|
||||||
| `ArrowUp` | `["seek", 60]` | Seek forward 60 seconds |
|
|
||||||
| `ArrowDown` | `["seek", -60]` | Seek backward 60 seconds |
|
|
||||||
| `Shift+KeyH` | `["sub-seek", -1]` | Jump to previous subtitle |
|
|
||||||
| `Shift+KeyL` | `["sub-seek", 1]` | Jump to next subtitle |
|
|
||||||
| `Ctrl+Shift+KeyH` | `["__replay-subtitle"]` | Replay current subtitle, pause at end |
|
|
||||||
| `Ctrl+Shift+KeyL` | `["__play-next-subtitle"]` | Play next subtitle, pause at end |
|
|
||||||
| `KeyQ` | `["quit"]` | Quit mpv |
|
|
||||||
| `Ctrl+KeyW` | `["quit"]` | Quit mpv |
|
|
||||||
|
|
||||||
**Custom keybindings example:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"keybindings": [
|
|
||||||
{ "key": "ArrowRight", "command": ["seek", 5] },
|
|
||||||
{ "key": "ArrowLeft", "command": ["seek", -5] },
|
|
||||||
{ "key": "Shift+ArrowRight", "command": ["seek", 30] },
|
|
||||||
{ "key": "KeyR", "command": ["script-binding", "immersive/auto-replay"] },
|
|
||||||
{ "key": "KeyA", "command": ["script-message", "ankiconnect-add-note"] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key format:** Use `KeyboardEvent.code` values (`Space`, `ArrowRight`, `KeyR`, etc.) with optional modifiers (`Ctrl+`, `Alt+`, `Shift+`, `Meta+`).
|
|
||||||
|
|
||||||
**Disable a default binding:** Set command to `null`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "key": "Space", "command": null }
|
|
||||||
```
|
|
||||||
|
|
||||||
**Special commands:** Commands prefixed with `__` are handled internally by the overlay rather than sent to mpv. `__replay-subtitle` replays the current subtitle and pauses at its end. `__play-next-subtitle` seeks to the next subtitle, plays it, and pauses at its end. `__runtime-options-open` opens the runtime options palette. `__runtime-option-cycle:<id>[:next|prev]` cycles a runtime option value.
|
|
||||||
|
|
||||||
**Supported commands:** Any valid mpv JSON IPC command array (`["cycle", "pause"]`, `["seek", 5]`, `["script-binding", "..."]`, etc.)
|
|
||||||
|
|
||||||
For subtitle-position and subtitle-track proxy commands (`sub-pos`, `sid`, `secondary-sid`), SubMiner also shows an mpv OSD notification after the command runs.
|
|
||||||
|
|
||||||
**See `config.example.jsonc`** for more keybinding examples and configuration options.
|
|
||||||
|
|
||||||
### Shortcuts Configuration
|
|
||||||
|
|
||||||
Customize or disable the overlay keyboard shortcuts:
|
|
||||||
|
|
||||||
See `config.example.jsonc` for detailed configuration options.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"shortcuts": {
|
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
|
|
||||||
"copySubtitle": "CommandOrControl+C",
|
|
||||||
"copySubtitleMultiple": "CommandOrControl+Shift+C",
|
|
||||||
"updateLastCardFromClipboard": "CommandOrControl+V",
|
|
||||||
"triggerFieldGrouping": "CommandOrControl+G",
|
|
||||||
"triggerSubsync": "Ctrl+Alt+S",
|
|
||||||
"mineSentence": "CommandOrControl+S",
|
|
||||||
"mineSentenceMultiple": "CommandOrControl+Shift+S",
|
|
||||||
"markAudioCard": "CommandOrControl+Shift+A",
|
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O",
|
|
||||||
"openJimaku": "Ctrl+Shift+J",
|
|
||||||
"multiCopyTimeoutMs": 3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Values | Description |
|
|
||||||
| ----------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `toggleVisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling visible subtitle overlay (default: `"Alt+Shift+O"`) |
|
|
||||||
| `copySubtitle` | string \| `null` | Accelerator for copying current subtitle (default: `"CommandOrControl+C"`) |
|
|
||||||
| `copySubtitleMultiple` | string \| `null` | Accelerator for multi-copy mode (default: `"CommandOrControl+Shift+C"`) |
|
|
||||||
| `updateLastCardFromClipboard` | string \| `null` | Accelerator for updating card from clipboard (default: `"CommandOrControl+V"`) |
|
|
||||||
| `triggerFieldGrouping` | string \| `null` | Accelerator for Kiku field grouping on last card (default: `"CommandOrControl+G"`; only active when `behavior.autoUpdateNewCards` is `false`) |
|
|
||||||
| `triggerSubsync` | string \| `null` | Accelerator for running Subsync (default: `"Ctrl+Alt+S"`) |
|
|
||||||
| `mineSentence` | string \| `null` | Accelerator for creating sentence card from current subtitle (default: `"CommandOrControl+S"`) |
|
|
||||||
| `mineSentenceMultiple` | string \| `null` | Accelerator for multi-mine sentence card mode (default: `"CommandOrControl+Shift+S"`) |
|
|
||||||
| `multiCopyTimeoutMs` | number | Timeout in ms for multi-copy/mine digit input (default: `3000`) |
|
|
||||||
| `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) |
|
|
||||||
| `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) |
|
|
||||||
| `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) |
|
|
||||||
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
|
|
||||||
|
|
||||||
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
|
|
||||||
|
|
||||||
Set any shortcut to `null` to disable it.
|
|
||||||
|
|
||||||
Feature-dependent shortcuts/keybindings only run when their related integration is enabled. For example, Anki/Kiku shortcuts require `ankiConnect.enabled` (and Kiku-specific behavior where applicable), and Jellyfin remote startup behavior requires Jellyfin to be enabled.
|
|
||||||
|
|
||||||
### Manual Card Update Shortcuts
|
|
||||||
|
|
||||||
When `behavior.autoUpdateNewCards` is set to `false`, new cards are detected but not automatically updated. Use these keyboard shortcuts for manual control:
|
|
||||||
|
|
||||||
| Shortcut | Action |
|
|
||||||
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| `Ctrl+C` | Copy the current subtitle line to clipboard (preserves line breaks) |
|
|
||||||
| `Ctrl+Shift+C` | Enter multi-copy mode. Press `1-9` to copy that many recent lines, or `Esc` to cancel. Timeout: 3 seconds |
|
|
||||||
| `Ctrl+V` | Update the last added Anki card using subtitles from clipboard |
|
|
||||||
| `Ctrl+G` | Trigger Kiku duplicate field grouping for the last added card (only when `behavior.autoUpdateNewCards` is `false`) |
|
|
||||||
| `Ctrl+S` | Create a sentence card from the current subtitle line |
|
|
||||||
| `Ctrl+Shift+S` | Enter multi-mine mode. Press `1-9` to create a sentence card from that many recent lines, or `Esc` to cancel |
|
|
||||||
| `Ctrl+Shift+V` | Cycle secondary subtitle display mode (hidden → visible → hover) |
|
|
||||||
| `Ctrl+Shift+A` | Mark the last added Anki card as an audio card (sets IsAudioCard, SentenceAudio, Sentence, Picture) |
|
|
||||||
| `Ctrl+Shift+O` | Open runtime options palette (session-only live toggles) |
|
|
||||||
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist (fixed, not currently configurable) |
|
|
||||||
|
|
||||||
**Multi-line copy workflow:**
|
|
||||||
|
|
||||||
1. Press `Ctrl+Shift+C`
|
|
||||||
2. Press a number key (`1-9`) within 3 seconds
|
|
||||||
3. The specified number of most recent subtitle lines are copied
|
|
||||||
4. Press `Ctrl+V` to update the last added card with the copied lines
|
|
||||||
|
|
||||||
These shortcuts are only active when the overlay window is visible and automatically disabled when hidden.
|
|
||||||
|
|
||||||
### Session Help Modal
|
|
||||||
|
|
||||||
The session help modal is opened with `Y-H` by default (falls back to `Y-K` if needed) and shows the current session keybindings and color legend.
|
|
||||||
|
|
||||||
You can filter the modal quickly with `/`:
|
|
||||||
|
|
||||||
- Type any part of the action name or shortcut in the search bar.
|
|
||||||
- Search is case-insensitive and ignores spaces/punctuation (`+`, `-`, `_`, `/`) so `ctrl w`, `ctrl+w`, and `ctrl+s` all match.
|
|
||||||
- Results are filtered across active MPV shortcuts, configured overlay shortcuts, and color legend items.
|
|
||||||
|
|
||||||
While the modal is open:
|
|
||||||
|
|
||||||
- `Esc`: close the modal (or clear the filter when text is entered)
|
|
||||||
- `↑/↓`, `j/k`: move selection
|
|
||||||
- Mouse/trackpad: click to select and activate rows
|
|
||||||
|
|
||||||
The list is generated at runtime from:
|
|
||||||
|
|
||||||
- Your active mpv keybindings (`keybindings`).
|
|
||||||
- Your configured overlay shortcuts (`shortcuts`, including runtime-loaded config values).
|
|
||||||
- Current subtitle color settings from `subtitleStyle`.
|
|
||||||
|
|
||||||
When config hot-reload updates shortcut/keybinding/style values, close and reopen the help modal to refresh the displayed entries.
|
|
||||||
|
|
||||||
### Runtime Option Palette
|
|
||||||
|
|
||||||
Use the runtime options palette to toggle settings live while SubMiner is running. These changes are session-only and reset on restart.
|
|
||||||
|
|
||||||
Current runtime options:
|
|
||||||
|
|
||||||
- `ankiConnect.behavior.autoUpdateNewCards` (`On` / `Off`)
|
|
||||||
- `ankiConnect.nPlusOne.highlightEnabled` (`On` / `Off`)
|
|
||||||
- `subtitleStyle.enableJlpt` (`On` / `Off`)
|
|
||||||
- `subtitleStyle.frequencyDictionary.enabled` (`On` / `Off`)
|
|
||||||
- `ankiConnect.nPlusOne.matchMode` (`headword` / `surface`)
|
|
||||||
- `ankiConnect.isKiku.fieldGrouping` (`auto` / `manual` / `disabled`)
|
|
||||||
|
|
||||||
Annotation toggles (`nPlusOne`, `enableJlpt`, `frequencyDictionary.enabled`) only apply to new subtitle lines after the toggle. The currently displayed line is not re-tokenized in place.
|
|
||||||
|
|
||||||
Default shortcut: `Ctrl+Shift+O`
|
|
||||||
|
|
||||||
Palette controls:
|
|
||||||
|
|
||||||
- `Arrow Up/Down`: select option
|
|
||||||
- `Arrow Left/Right`: change selected value
|
|
||||||
- `Enter`: apply selected value
|
|
||||||
- `Esc`: close
|
|
||||||
|
|
||||||
## Anki Integration
|
|
||||||
|
|
||||||
### AnkiConnect
|
### AnkiConnect
|
||||||
|
|
||||||
Enable automatic Anki card creation and updates with media generation:
|
Enable automatic Anki card creation and updates with media generation:
|
||||||
@@ -543,12 +100,6 @@ Enable automatic Anki card creation and updates with media generation:
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "http://127.0.0.1:8765",
|
"url": "http://127.0.0.1:8765",
|
||||||
"pollingRate": 3000,
|
"pollingRate": 3000,
|
||||||
"proxy": {
|
|
||||||
"enabled": false,
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 8766,
|
|
||||||
"upstreamUrl": "http://127.0.0.1:8765"
|
|
||||||
},
|
|
||||||
"tags": ["SubMiner"],
|
"tags": ["SubMiner"],
|
||||||
"deck": "Learning::Japanese",
|
"deck": "Learning::Japanese",
|
||||||
"fields": {
|
"fields": {
|
||||||
@@ -612,11 +163,7 @@ This example is intentionally compact. The option table below documents availabl
|
|||||||
| --------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `false`) |
|
| `enabled` | `true`, `false` | Enable AnkiConnect integration (default: `false`) |
|
||||||
| `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) |
|
| `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) |
|
| `pollingRate` | number (ms) | How often to check for new cards (default: `3000`) |
|
||||||
| `proxy.enabled` | `true`, `false` | Enable local AnkiConnect-compatible proxy for push-based auto-enrichment (default: `true`) |
|
|
||||||
| `proxy.host` | string | Bind host for local AnkiConnect proxy (default: `127.0.0.1`) |
|
|
||||||
| `proxy.port` | number | Bind port for local AnkiConnect proxy (default: `8766`) |
|
|
||||||
| `proxy.upstreamUrl` | string (URL) | Upstream AnkiConnect URL that proxy forwards to (default: `http://127.0.0.1:8765`) |
|
|
||||||
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
|
| `tags` | array of strings | Tags automatically added to cards mined/updated by SubMiner (default: `['SubMiner']`; set `[]` to disable automatic tagging). |
|
||||||
| `deck` | string | Anki deck to monitor for new cards |
|
| `deck` | string | Anki deck to monitor for new cards |
|
||||||
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. |
|
| `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. |
|
||||||
@@ -663,28 +210,13 @@ This example is intentionally compact. The option table below documents availabl
|
|||||||
| `isLapis` | object | Lapis/shared sentence-card config: `{ enabled, sentenceCardModel }`. Sentence/audio field names are fixed to `Sentence` and `SentenceAudio`. |
|
| `isLapis` | object | Lapis/shared sentence-card config: `{ enabled, sentenceCardModel }`. Sentence/audio field names are fixed to `Sentence` and `SentenceAudio`. |
|
||||||
| `isKiku` | object | Kiku-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }` (shared sentence/audio/model settings are inherited from `isLapis`) |
|
| `isKiku` | object | Kiku-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }` (shared sentence/audio/model settings are inherited from `isLapis`) |
|
||||||
|
|
||||||
### Kiku/Lapis Integration
|
**Kiku / Lapis Note Type Support:**
|
||||||
|
|
||||||
SubMiner is intentionally built for [Kiku](https://kiku.youyoumu.my.id/) and [Lapis](https://github.com/donkuri/lapis) workflows, with note-type-specific behavior built into Anki settings.
|
SubMiner supports the [Lapis](https://github.com/donkuri/lapis) and [Kiku](https://kiku.youyoumu.my.id/) note types. Both `isLapis.enabled` and `isKiku.enabled` can be true; Kiku takes precedence for grouping behavior, while sentence-card model/field settings come from `isLapis`.
|
||||||
|
|
||||||
```jsonc
|
When enabled, sentence cards automatically set `IsSentenceCard` to `"x"` and populate the `Expression` field. Audio cards set `IsAudioCard` to `"x"`.
|
||||||
"ankiConnect": {
|
|
||||||
"isLapis": {
|
|
||||||
"enabled": true,
|
|
||||||
"sentenceCardModel": "Japanese sentences"
|
|
||||||
},
|
|
||||||
"isKiku": {
|
|
||||||
"enabled": true,
|
|
||||||
"fieldGrouping": "manual",
|
|
||||||
"deleteDuplicateInAuto": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- Enable `isLapis` to mine dedicated sentence cards. SubMiner sets `IsSentenceCard` to `"x"` and fills the sentence fields for the configured model.
|
Kiku extends Lapis with **field grouping** — when a duplicate card is detected (same Word/Expression), SubMiner merges the two cards' content into one using Kiku's `data-group-id` HTML structure, organizing each mining instance into separate pages within the note.
|
||||||
- Enable `isKiku` to turn on duplicate merge behavior for mined Word/Expression hits.
|
|
||||||
- When both are enabled, Kiku behavior is applied for grouping while sentence-card model settings are still read from `isLapis`.
|
|
||||||
- `isKiku.fieldGrouping` supports `disabled`, `auto`, and `manual` merge modes; see [Field Grouping Modes](#field-grouping-modes).
|
|
||||||
|
|
||||||
### N+1 Word Highlighting
|
### N+1 Word Highlighting
|
||||||
|
|
||||||
@@ -736,27 +268,91 @@ To refresh roughly once per day, set:
|
|||||||
|
|
||||||
<a :href="'/assets/kiku-integration.webm'" target="_blank" rel="noreferrer">Open demo in a new tab</a>
|
<a :href="'/assets/kiku-integration.webm'" target="_blank" rel="noreferrer">Open demo in a new tab</a>
|
||||||
|
|
||||||
## External Integrations
|
**Image Quality Notes:**
|
||||||
|
|
||||||
### Jimaku
|
- `imageQuality` affects JPG and WebP only; PNG is lossless and ignores this setting
|
||||||
|
- JPG quality is mapped to FFmpeg's scale (2-31, lower = better)
|
||||||
|
- WebP quality uses FFmpeg's native 0-100 scale
|
||||||
|
|
||||||
Configure Jimaku API access and defaults:
|
### Manual Card Update Shortcuts
|
||||||
|
|
||||||
|
When `behavior.autoUpdateNewCards` is set to `false`, new cards are detected but not automatically updated. Use these keyboard shortcuts for manual control:
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `Ctrl+C` | Copy the current subtitle line to clipboard (preserves line breaks) |
|
||||||
|
| `Ctrl+Shift+C` | Enter multi-copy mode. Press `1-9` to copy that many recent lines, or `Esc` to cancel. Timeout: 3 seconds |
|
||||||
|
| `Ctrl+V` | Update the last added Anki card using subtitles from clipboard |
|
||||||
|
| `Ctrl+G` | Trigger Kiku duplicate field grouping for the last added card (only when `behavior.autoUpdateNewCards` is `false`) |
|
||||||
|
| `Ctrl+S` | Create a sentence card from the current subtitle line |
|
||||||
|
| `Ctrl+Shift+S` | Enter multi-mine mode. Press `1-9` to create a sentence card from that many recent lines, or `Esc` to cancel |
|
||||||
|
| `Ctrl+Shift+V` | Cycle secondary subtitle display mode (hidden → visible → hover) |
|
||||||
|
| `Ctrl+Shift+A` | Mark the last added Anki card as an audio card (sets IsAudioCard, SentenceAudio, Sentence, Picture) |
|
||||||
|
| `Ctrl+Shift+O` | Open runtime options palette (session-only live toggles) |
|
||||||
|
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist (fixed, not currently configurable) |
|
||||||
|
|
||||||
|
**Multi-line copy workflow:**
|
||||||
|
|
||||||
|
1. Press `Ctrl+Shift+C`
|
||||||
|
2. Press a number key (`1-9`) within 3 seconds
|
||||||
|
3. The specified number of most recent subtitle lines are copied
|
||||||
|
4. Press `Ctrl+V` to update the last added card with the copied lines
|
||||||
|
|
||||||
|
These shortcuts are only active when the overlay window is visible and automatically disabled when hidden.
|
||||||
|
|
||||||
|
### Session help modal
|
||||||
|
|
||||||
|
The session help modal is opened with `Y-H` by default (falls back to `Y-K` if needed) and shows the current session keybindings and color legend.
|
||||||
|
|
||||||
|
You can filter the modal quickly with `/`:
|
||||||
|
|
||||||
|
- Type any part of the action name or shortcut in the search bar.
|
||||||
|
- Search is case-insensitive and ignores spaces/punctuation (`+`, `-`, `_`, `/`) so `ctrl w`, `ctrl+w`, and `ctrl+s` all match.
|
||||||
|
- Results are filtered across active MPV shortcuts, configured overlay shortcuts, and color legend items.
|
||||||
|
|
||||||
|
While the modal is open:
|
||||||
|
|
||||||
|
- `Esc`: close the modal (or clear the filter when text is entered)
|
||||||
|
- `↑/↓`, `j/k`: move selection
|
||||||
|
- Mouse/trackpad: click to select and activate rows
|
||||||
|
|
||||||
|
The list is generated at runtime from:
|
||||||
|
|
||||||
|
- Your active mpv keybindings (`keybindings`).
|
||||||
|
- Your configured overlay shortcuts (`shortcuts`, including runtime-loaded config values).
|
||||||
|
- Current subtitle color settings from `subtitleStyle`.
|
||||||
|
|
||||||
|
When config hot-reload updates shortcut/keybinding/style values, close and reopen the help modal to refresh the displayed entries.
|
||||||
|
|
||||||
|
### Auto-Start Overlay
|
||||||
|
|
||||||
|
Control whether the overlay automatically becomes visible when it connects to mpv:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"jimaku": {
|
"auto_start_overlay": false
|
||||||
"apiKey": "YOUR_API_KEY",
|
|
||||||
"apiKeyCommand": "cat ~/.jimaku_key",
|
|
||||||
"apiBaseUrl": "https://jimaku.cc",
|
|
||||||
"languagePreference": "ja",
|
|
||||||
"maxEntryResults": 10
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Jimaku is rate limited; if you hit a limit, SubMiner will surface the retry delay from the API response.
|
| Option | Values | Description |
|
||||||
|
| -------------------- | --------------- | ------------------------------------------------------ |
|
||||||
|
| `auto_start_overlay` | `true`, `false` | Auto-show overlay on mpv connection (default: `false`) |
|
||||||
|
|
||||||
Set `openBrowser` to `false` to only print the URL without opening a browser.
|
The mpv plugin controls startup per layer via `auto_start_visible_overlay` and `auto_start_invisible_overlay` in `subminer.conf` (`platform-default` for invisible means hidden on Linux, visible on macOS/Windows).
|
||||||
|
|
||||||
|
### Visible Overlay Subtitle Binding
|
||||||
|
|
||||||
|
Control whether toggling the visible overlay also toggles MPV subtitle visibility:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bind_visible_overlay_to_mpv_sub_visibility": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Values | Description |
|
||||||
|
| -------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `bind_visible_overlay_to_mpv_sub_visibility` | `true`, `false` | When `true` (default), visible overlay hides MPV primary/secondary subtitles and restores them when hidden. When `false`, visible overlay toggles do not change MPV subtitle visibility. |
|
||||||
|
|
||||||
### Auto Subtitle Sync
|
### Auto Subtitle Sync
|
||||||
|
|
||||||
@@ -783,6 +379,43 @@ Sync the active subtitle track using `alass` (preferred) or `ffsubsync`:
|
|||||||
Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
|
Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
|
||||||
Customize it there, or set it to `null` to disable.
|
Customize it there, or set it to `null` to disable.
|
||||||
|
|
||||||
|
### Invisible Overlay
|
||||||
|
|
||||||
|
SubMiner includes a second subtitle mining layer that can be visually invisible while still interactive for Yomitan lookups.
|
||||||
|
|
||||||
|
- `invisibleOverlay.startupVisibility` values:
|
||||||
|
|
||||||
|
1. `"platform-default"`: hidden on Wayland, visible on Windows/macOS/other sessions.
|
||||||
|
2. `"visible"`: always shown on startup.
|
||||||
|
3. `"hidden"`: always hidden on startup.
|
||||||
|
|
||||||
|
Invisible subtitle positioning can be adjusted directly in the invisible layer:
|
||||||
|
|
||||||
|
- `Ctrl/Cmd+Shift+P` toggles position edit mode.
|
||||||
|
- Use arrow keys to move the invisible subtitle text.
|
||||||
|
- Press `Enter` or `Ctrl/Cmd+S` to save, or `Esc` to cancel.
|
||||||
|
- This edit-mode shortcut is fixed (not currently configurable in `shortcuts`/`keybindings`).
|
||||||
|
|
||||||
|
### Jimaku
|
||||||
|
|
||||||
|
Configure Jimaku API access and defaults:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jimaku": {
|
||||||
|
"apiKey": "YOUR_API_KEY",
|
||||||
|
"apiKeyCommand": "cat ~/.jimaku_key",
|
||||||
|
"apiBaseUrl": "https://jimaku.cc",
|
||||||
|
"languagePreference": "ja",
|
||||||
|
"maxEntryResults": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Jimaku is rate limited; if you hit a limit, SubMiner will surface the retry delay from the API response.
|
||||||
|
|
||||||
|
Set `openBrowser` to `false` to only print the URL without opening a browser.
|
||||||
|
|
||||||
### AniList
|
### AniList
|
||||||
|
|
||||||
AniList integration is opt-in and disabled by default. Enable it to allow SubMiner to update watched episode progress after playback.
|
AniList integration is opt-in and disabled by default. Enable it to allow SubMiner to update watched episode progress after playback.
|
||||||
@@ -875,7 +508,6 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
|
|||||||
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
|
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
|
||||||
|
|
||||||
Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted storage after login/setup.
|
Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted storage after login/setup.
|
||||||
|
|
||||||
- On Linux, token storage defaults to `gnome-libsecret` for `safeStorage`. Override with `--password-store=<backend>` on launcher/app invocations when needed.
|
- On Linux, token storage defaults to `gnome-libsecret` for `safeStorage`. Override with `--password-store=<backend>` on launcher/app invocations when needed.
|
||||||
|
|
||||||
Launcher subcommands:
|
Launcher subcommands:
|
||||||
@@ -930,6 +562,276 @@ Troubleshooting:
|
|||||||
- If images do not render, confirm asset keys exactly match uploaded Discord asset names.
|
- If images do not render, confirm asset keys exactly match uploaded Discord asset names.
|
||||||
- If Discord is closed/not installed/disconnects, SubMiner continues running and quietly skips presence updates.
|
- If Discord is closed/not installed/disconnects, SubMiner continues running and quietly skips presence updates.
|
||||||
|
|
||||||
|
### Keybindings
|
||||||
|
|
||||||
|
Add a `keybindings` array to configure keyboard shortcuts that send commands to mpv:
|
||||||
|
|
||||||
|
See `config.example.jsonc` for detailed configuration options and more examples.
|
||||||
|
|
||||||
|
**Default keybindings:**
|
||||||
|
|
||||||
|
| Key | Command | Description |
|
||||||
|
| ----------------- | -------------------------- | ------------------------------------- |
|
||||||
|
| `Space` | `["cycle", "pause"]` | Toggle pause |
|
||||||
|
| `ArrowRight` | `["seek", 5]` | Seek forward 5 seconds |
|
||||||
|
| `ArrowLeft` | `["seek", -5]` | Seek backward 5 seconds |
|
||||||
|
| `ArrowUp` | `["seek", 60]` | Seek forward 60 seconds |
|
||||||
|
| `ArrowDown` | `["seek", -60]` | Seek backward 60 seconds |
|
||||||
|
| `Shift+KeyH` | `["sub-seek", -1]` | Jump to previous subtitle |
|
||||||
|
| `Shift+KeyL` | `["sub-seek", 1]` | Jump to next subtitle |
|
||||||
|
| `Ctrl+Shift+KeyH` | `["__replay-subtitle"]` | Replay current subtitle, pause at end |
|
||||||
|
| `Ctrl+Shift+KeyL` | `["__play-next-subtitle"]` | Play next subtitle, pause at end |
|
||||||
|
| `KeyQ` | `["quit"]` | Quit mpv |
|
||||||
|
| `Ctrl+KeyW` | `["quit"]` | Quit mpv |
|
||||||
|
|
||||||
|
**Custom keybindings example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"keybindings": [
|
||||||
|
{ "key": "ArrowRight", "command": ["seek", 5] },
|
||||||
|
{ "key": "ArrowLeft", "command": ["seek", -5] },
|
||||||
|
{ "key": "Shift+ArrowRight", "command": ["seek", 30] },
|
||||||
|
{ "key": "KeyR", "command": ["script-binding", "immersive/auto-replay"] },
|
||||||
|
{ "key": "KeyA", "command": ["script-message", "ankiconnect-add-note"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key format:** Use `KeyboardEvent.code` values (`Space`, `ArrowRight`, `KeyR`, etc.) with optional modifiers (`Ctrl+`, `Alt+`, `Shift+`, `Meta+`).
|
||||||
|
|
||||||
|
**Disable a default binding:** Set command to `null`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "key": "Space", "command": null }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Special commands:** Commands prefixed with `__` are handled internally by the overlay rather than sent to mpv. `__replay-subtitle` replays the current subtitle and pauses at its end. `__play-next-subtitle` seeks to the next subtitle, plays it, and pauses at its end. `__runtime-options-open` opens the runtime options palette. `__runtime-option-cycle:<id>[:next|prev]` cycles a runtime option value.
|
||||||
|
|
||||||
|
**Supported commands:** Any valid mpv JSON IPC command array (`["cycle", "pause"]`, `["seek", 5]`, `["script-binding", "..."]`, etc.)
|
||||||
|
|
||||||
|
**See `config.example.jsonc`** for more keybinding examples and configuration options.
|
||||||
|
|
||||||
|
### Runtime Option Palette
|
||||||
|
|
||||||
|
Use the runtime options palette to toggle settings live while SubMiner is running. These changes are session-only and reset on restart.
|
||||||
|
|
||||||
|
Current runtime options:
|
||||||
|
|
||||||
|
- `ankiConnect.behavior.autoUpdateNewCards` (`On` / `Off`)
|
||||||
|
- `ankiConnect.isKiku.fieldGrouping` (`auto` / `manual` / `disabled`)
|
||||||
|
|
||||||
|
Default shortcut: `Ctrl+Shift+O`
|
||||||
|
|
||||||
|
Palette controls:
|
||||||
|
|
||||||
|
- `Arrow Up/Down`: select option
|
||||||
|
- `Arrow Left/Right`: change selected value
|
||||||
|
- `Enter`: apply selected value
|
||||||
|
- `Esc`: close
|
||||||
|
|
||||||
|
### Secondary Subtitles
|
||||||
|
|
||||||
|
Display a second subtitle track (e.g., English alongside Japanese) in the overlay:
|
||||||
|
|
||||||
|
See `config.example.jsonc` for detailed configuration options.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"secondarySub": {
|
||||||
|
"secondarySubLanguages": ["eng", "en"],
|
||||||
|
"autoLoadSecondarySub": true,
|
||||||
|
"defaultMode": "hover"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Values | Description |
|
||||||
|
| ----------------------- | ---------------------------------- | ------------------------------------------------------ |
|
||||||
|
| `secondarySubLanguages` | string[] | Language codes to auto-load (e.g., `["eng", "en"]`) |
|
||||||
|
| `autoLoadSecondarySub` | `true`, `false` | Auto-detect and load matching secondary subtitle track |
|
||||||
|
| `defaultMode` | `"hidden"`, `"visible"`, `"hover"` | Initial display mode (default: `"hover"`) |
|
||||||
|
|
||||||
|
**Display modes:**
|
||||||
|
|
||||||
|
- **hidden** — Secondary subtitles not shown
|
||||||
|
- **visible** — Always visible at top of overlay
|
||||||
|
- **hover** — Only visible when hovering over the subtitle area (default)
|
||||||
|
|
||||||
|
**See `config.example.jsonc`** for additional secondary subtitle configuration options.
|
||||||
|
|
||||||
|
### Shortcuts Configuration
|
||||||
|
|
||||||
|
Customize or disable the overlay keyboard shortcuts:
|
||||||
|
|
||||||
|
See `config.example.jsonc` for detailed configuration options.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"shortcuts": {
|
||||||
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
|
||||||
|
"toggleInvisibleOverlayGlobal": "Alt+Shift+I",
|
||||||
|
"copySubtitle": "CommandOrControl+C",
|
||||||
|
"copySubtitleMultiple": "CommandOrControl+Shift+C",
|
||||||
|
"updateLastCardFromClipboard": "CommandOrControl+V",
|
||||||
|
"triggerFieldGrouping": "CommandOrControl+G",
|
||||||
|
"triggerSubsync": "Ctrl+Alt+S",
|
||||||
|
"mineSentence": "CommandOrControl+S",
|
||||||
|
"mineSentenceMultiple": "CommandOrControl+Shift+S",
|
||||||
|
"markAudioCard": "CommandOrControl+Shift+A",
|
||||||
|
"openRuntimeOptions": "CommandOrControl+Shift+O",
|
||||||
|
"openJimaku": "Ctrl+Shift+J",
|
||||||
|
"multiCopyTimeoutMs": 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Values | Description |
|
||||||
|
| ------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `toggleVisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling visible subtitle overlay (default: `"Alt+Shift+O"`) |
|
||||||
|
| `toggleInvisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling invisible interactive overlay (default: `"Alt+Shift+I"`) |
|
||||||
|
| `copySubtitle` | string \| `null` | Accelerator for copying current subtitle (default: `"CommandOrControl+C"`) |
|
||||||
|
| `copySubtitleMultiple` | string \| `null` | Accelerator for multi-copy mode (default: `"CommandOrControl+Shift+C"`) |
|
||||||
|
| `updateLastCardFromClipboard` | string \| `null` | Accelerator for updating card from clipboard (default: `"CommandOrControl+V"`) |
|
||||||
|
| `triggerFieldGrouping` | string \| `null` | Accelerator for Kiku field grouping on last card (default: `"CommandOrControl+G"`; only active when `behavior.autoUpdateNewCards` is `false`) |
|
||||||
|
| `triggerSubsync` | string \| `null` | Accelerator for running Subsync (default: `"Ctrl+Alt+S"`) |
|
||||||
|
| `mineSentence` | string \| `null` | Accelerator for creating sentence card from current subtitle (default: `"CommandOrControl+S"`) |
|
||||||
|
| `mineSentenceMultiple` | string \| `null` | Accelerator for multi-mine sentence card mode (default: `"CommandOrControl+Shift+S"`) |
|
||||||
|
| `multiCopyTimeoutMs` | number | Timeout in ms for multi-copy/mine digit input (default: `3000`) |
|
||||||
|
| `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) |
|
||||||
|
| `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) |
|
||||||
|
| `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) |
|
||||||
|
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
|
||||||
|
|
||||||
|
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
|
||||||
|
|
||||||
|
Set any shortcut to `null` to disable it.
|
||||||
|
|
||||||
|
Feature-dependent shortcuts/keybindings only run when their related integration is enabled. For example, Anki/Kiku shortcuts require `ankiConnect.enabled` (and Kiku-specific behavior where applicable), and Jellyfin remote startup behavior requires Jellyfin to be enabled.
|
||||||
|
|
||||||
|
### Subtitle Position
|
||||||
|
|
||||||
|
Set the initial vertical subtitle position (measured from the bottom of the screen):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"subtitlePosition": {
|
||||||
|
"yPercent": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Values | Description |
|
||||||
|
| ---------- | ---------------- | ---------------------------------------------------------------------- |
|
||||||
|
| `yPercent` | number (0 - 100) | Distance from the bottom as a percent of screen height (default: `10`) |
|
||||||
|
|
||||||
|
### Subtitle Style
|
||||||
|
|
||||||
|
Customize the appearance of primary and secondary subtitles:
|
||||||
|
|
||||||
|
See `config.example.jsonc` for detailed configuration options.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"subtitleStyle": {
|
||||||
|
"fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif",
|
||||||
|
"fontSize": 35,
|
||||||
|
"fontColor": "#cad3f5",
|
||||||
|
"fontWeight": "normal",
|
||||||
|
"fontStyle": "normal",
|
||||||
|
"backgroundColor": "rgb(30, 32, 48, 0.88)",
|
||||||
|
"secondary": {
|
||||||
|
"fontSize": 24,
|
||||||
|
"fontColor": "#ffffff",
|
||||||
|
"backgroundColor": "transparent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Values | Description |
|
||||||
|
| ---------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `fontFamily` | string | CSS font-family value (default: `"Noto Sans CJK JP Regular, ..."`) |
|
||||||
|
| `fontSize` | number (px) | Font size in pixels (default: `35`) |
|
||||||
|
| `fontColor` | string | Any CSS color value (default: `"#cad3f5"`) |
|
||||||
|
| `fontWeight` | string | CSS font-weight, e.g. `"bold"`, `"normal"`, `"600"` (default: `"normal"`) |
|
||||||
|
| `fontStyle` | string | `"normal"` or `"italic"` (default: `"normal"`) |
|
||||||
|
| `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"rgb(30, 32, 48, 0.88)"`) |
|
||||||
|
| `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. |
|
||||||
|
| `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) |
|
||||||
|
| `frequencyDictionary.sourcePath` | string | Path to a local frequency dictionary root. Leave empty or omit to use the built-in bundled dictionary search paths. |
|
||||||
|
| `frequencyDictionary.topX` | number | Only color tokens whose frequency rank is `<= topX` (`1000` by default) |
|
||||||
|
| `frequencyDictionary.mode` | string | `"single"` or `"banded"` (`"single"` by default) |
|
||||||
|
| `frequencyDictionary.singleColor` | string | Color used for all highlighted tokens in single mode |
|
||||||
|
| `frequencyDictionary.bandedColors` | string[] | Array of five hex colors used for ranked bands in banded mode |
|
||||||
|
| `nPlusOneColor` | string | Existing n+1 highlight color (default: `#c6a0f6`) |
|
||||||
|
| `knownWordColor` | string | Existing known-word highlight color (default: `#a6da95`) |
|
||||||
|
| `jlptColors` | object | JLPT level underline colors object (`N1`..`N5`) |
|
||||||
|
| `secondary` | object | Override any of the above for secondary subtitles (optional) |
|
||||||
|
|
||||||
|
JLPT underlining is powered by offline term-meta bank files at runtime. See [`docs/jlpt-vocab-bundle.md`](jlpt-vocab-bundle.md) for required files, source/version refresh steps, and deterministic fallback behavior.
|
||||||
|
|
||||||
|
Frequency dictionary highlighting uses the same dictionary file format as JLPT bundle lookups (`term_meta_bank_*.json` under discovered dictionary directories). A token is highlighted when it has a positive integer `frequencyRank` (lower is more common) and the rank is within `topX`.
|
||||||
|
|
||||||
|
Lookup behavior:
|
||||||
|
|
||||||
|
- Set `frequencyDictionary.sourcePath` to a directory containing `term_meta_bank_*.json` for a fully custom source.
|
||||||
|
- If `sourcePath` is missing or empty, SubMiner uses bundled defaults from `vendor/jiten_freq_global` (packaged under `<resources>/jiten_freq_global` in distribution builds).
|
||||||
|
- In both cases, only terms with a valid `frequencyRank` are used; everything else falls back to no highlighting.
|
||||||
|
|
||||||
|
In `single` mode all highlights use `singleColor`; in `banded` mode tokens map to five ascending color bands from most common to least common inside the topX window.
|
||||||
|
|
||||||
|
Secondary subtitle defaults: `fontSize: 24`, `fontColor: "#ffffff"`, `backgroundColor: "transparent"`. Any property not set in `secondary` falls back to the CSS defaults.
|
||||||
|
|
||||||
|
**See `config.example.jsonc`** for the complete list of subtitle style configuration options.
|
||||||
|
|
||||||
|
`jlptColors` keys are:
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| ---- | --------- | ----------------------- |
|
||||||
|
| `N1` | `#ed8796` | JLPT N1 underline color |
|
||||||
|
| `N2` | `#f5a97f` | JLPT N2 underline color |
|
||||||
|
| `N3` | `#f9e2af` | JLPT N3 underline color |
|
||||||
|
| `N4` | `#a6e3a1` | JLPT N4 underline color |
|
||||||
|
| `N5` | `#8aadf4` | JLPT N5 underline color |
|
||||||
|
|
||||||
|
### Texthooker
|
||||||
|
|
||||||
|
Control whether the browser opens automatically when texthooker starts:
|
||||||
|
|
||||||
|
See `config.example.jsonc` for detailed configuration options.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"texthooker": {
|
||||||
|
"openBrowser": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket Server
|
||||||
|
|
||||||
|
The overlay includes a built-in WebSocket server that broadcasts subtitle text to connected clients (such as texthooker-ui) for external processing.
|
||||||
|
|
||||||
|
By default, the server uses "auto" mode: it starts automatically unless [mpv_websocket](https://github.com/kuroahna/mpv_websocket) is detected at `~/.config/mpv/mpv_websocket`. If you have mpv_websocket installed, the built-in server is skipped to avoid conflicts.
|
||||||
|
|
||||||
|
See `config.example.jsonc` for detailed configuration options.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"websocket": {
|
||||||
|
"enabled": "auto",
|
||||||
|
"port": 6677
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Values | Description |
|
||||||
|
| --------- | ------------------------- | -------------------------------------------------------- |
|
||||||
|
| `enabled` | `true`, `false`, `"auto"` | `"auto"` (default) disables if mpv_websocket is detected |
|
||||||
|
| `port` | number | WebSocket server port (default: 6677) |
|
||||||
|
|
||||||
### Immersion Tracking
|
### Immersion Tracking
|
||||||
|
|
||||||
Enable or disable local immersion analytics stored in SQLite for mined subtitles and media sessions:
|
Enable or disable local immersion analytics stored in SQLite for mined subtitles and media sessions:
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
# Feature Demos
|
|
||||||
|
|
||||||
Short recordings of SubMiner's key features and integrations from real playback sessions.
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const v = '20260301-1';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
## Anki Card Mining & Enrichment
|
|
||||||
|
|
||||||
Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner automatically attaches the sentence, a timing-accurate audio clip, a screenshot, and a translation.
|
|
||||||
|
|
||||||
<video controls playsinline preload="metadata" :poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`">
|
|
||||||
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
|
|
||||||
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
|
|
||||||
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
|
|
||||||
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
|
||||||
</a>
|
|
||||||
</video>
|
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Subtitle Download & Sync
|
|
||||||
|
|
||||||
Search and download subtitles from Jimaku, then automatically synchronize them with alass or ffsubsync — all from within SubMiner.
|
|
||||||
|
|
||||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/subtitle-sync-poster.jpg?v=${v}`">
|
|
||||||
<source :src="`/assets/demos/subtitle-sync.webm?v=${v}`" type="video/webm" />
|
|
||||||
<source :src="`/assets/demos/subtitle-sync.mp4?v=${v}`" type="video/mp4" />
|
|
||||||
</video> -->
|
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Jellyfin Integration
|
|
||||||
|
|
||||||
Browse your Jellyfin library, cast to devices, and launch playback directly from SubMiner. Watch progress syncs back to your Jellyfin server.
|
|
||||||
|
|
||||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/jellyfin-poster.jpg?v=${v}`">
|
|
||||||
<source :src="`/assets/demos/jellyfin.webm?v=${v}`" type="video/webm" />
|
|
||||||
<source :src="`/assets/demos/jellyfin.mp4?v=${v}`" type="video/mp4" />
|
|
||||||
</video> -->
|
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Texthooker
|
|
||||||
|
|
||||||
Open subtitles in an external texthooker page for use with browser-based tools and extensions alongside the overlay.
|
|
||||||
|
|
||||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/texthooker-poster.jpg?v=${v}`">
|
|
||||||
<source :src="`/assets/demos/texthooker.webm?v=${v}`" type="video/webm" />
|
|
||||||
<source :src="`/assets/demos/texthooker.mp4?v=${v}`" type="video/mp4" />
|
|
||||||
</video> -->
|
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
|
||||||
:::
|
|
||||||
|
|
||||||
<style>
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
|
||||||
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28);
|
|
||||||
margin: 0.75rem 0 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin-top: 2.5rem !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -60,15 +60,6 @@ bun run dev # builds + launches with --start --dev
|
|||||||
electron . --start --dev --log-level debug # equivalent Electron launch with verbose logging
|
electron . --start --dev --log-level debug # equivalent Electron launch with verbose logging
|
||||||
electron . --background # tray/background mode, minimal default logging
|
electron . --background # tray/background mode, minimal default logging
|
||||||
make dev-start # build + launch via Makefile
|
make dev-start # build + launch via Makefile
|
||||||
make dev-watch # watch TS + renderer and launch Electron (faster edit loop)
|
|
||||||
make dev-watch-macos # same as dev-watch, forcing --backend macos
|
|
||||||
```
|
|
||||||
|
|
||||||
For mpv-plugin-driven testing without exporting `SUBMINER_BINARY_PATH` each run, set a one-time
|
|
||||||
dev binary path in `~/.config/mpv/script-opts/subminer.conf`:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
binary_path=/absolute/path/to/SubMiner/scripts/subminer-dev.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|||||||
@@ -6,14 +6,11 @@ SubMiner stores immersion analytics in local SQLite (`immersion.sqlite`) by defa
|
|||||||
|
|
||||||
- Write path is asynchronous and queue-backed.
|
- Write path is asynchronous and queue-backed.
|
||||||
- Hot paths (subtitle parsing/render/token flows) enqueue telemetry/events and never await SQLite writes.
|
- Hot paths (subtitle parsing/render/token flows) enqueue telemetry/events and never await SQLite writes.
|
||||||
- Background line processing also upserts to `imm_words` and `imm_kanji`.
|
|
||||||
- Queue overflow policy is deterministic: drop oldest queued writes, keep newest.
|
- Queue overflow policy is deterministic: drop oldest queued writes, keep newest.
|
||||||
- Flush policy defaults to `25` writes or `500ms` max delay.
|
- Flush policy defaults to `25` writes or `500ms` max delay.
|
||||||
- SQLite pragmas: `journal_mode=WAL`, `synchronous=NORMAL`, `foreign_keys=ON`, `busy_timeout=2500`.
|
- SQLite pragmas: `journal_mode=WAL`, `synchronous=NORMAL`, `foreign_keys=ON`, `busy_timeout=2500`.
|
||||||
- Rollups now run incrementally from the last processed telemetry sample; startup performs a one-time bootstrap rebuild-equivalent pass.
|
|
||||||
- If retention pruning removes telemetry/session rows, maintenance triggers a full rollup rebuild to resync historical aggregates.
|
|
||||||
|
|
||||||
## Schema (v3)
|
## Schema (v1)
|
||||||
|
|
||||||
Schema versioning table:
|
Schema versioning table:
|
||||||
|
|
||||||
@@ -21,21 +18,15 @@ Schema versioning table:
|
|||||||
|
|
||||||
Core entities:
|
Core entities:
|
||||||
|
|
||||||
- `imm_videos`: video key/title/source metadata + optional media metadata fields, `CREATED_DATE`/`LAST_UPDATE_DATE`
|
- `imm_videos`: video key/title/source metadata + optional media metadata fields
|
||||||
- `imm_sessions`: session UUID, video reference, timing/status fields, `CREATED_DATE`/`LAST_UPDATE_DATE`
|
- `imm_sessions`: session UUID, video reference, timing/status fields
|
||||||
- `imm_session_telemetry`: high-frequency session aggregates over time, `CREATED_DATE`/`LAST_UPDATE_DATE`
|
- `imm_session_telemetry`: high-frequency session aggregates over time
|
||||||
- `imm_session_events`: event stream with compact numeric event types, `CREATED_DATE`/`LAST_UPDATE_DATE`
|
- `imm_session_events`: event stream with compact numeric event types
|
||||||
|
|
||||||
Rollups:
|
Rollups:
|
||||||
|
|
||||||
- `imm_daily_rollups`: includes `CREATED_DATE`/`LAST_UPDATE_DATE`
|
- `imm_daily_rollups`
|
||||||
- `imm_monthly_rollups`: includes `CREATED_DATE`/`LAST_UPDATE_DATE`
|
- `imm_monthly_rollups`
|
||||||
|
|
||||||
Vocabulary:
|
|
||||||
|
|
||||||
- `imm_words(id, headword, word, reading, first_seen, last_seen, frequency)`
|
|
||||||
- `imm_kanji(id, kanji, first_seen, last_seen, frequency)`
|
|
||||||
- `first_seen`/`last_seen` store Unix timestamps and are upserted with line ingestion
|
|
||||||
|
|
||||||
Primary index coverage:
|
Primary index coverage:
|
||||||
|
|
||||||
@@ -156,3 +147,4 @@ FROM imm_monthly_rollups
|
|||||||
ORDER BY rollup_month DESC, video_id DESC
|
ORDER BY rollup_month DESC, video_id DESC
|
||||||
LIMIT ?;
|
LIMIT ?;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,9 @@ const docsIndexContents = readFileSync(docsIndexPath, 'utf8');
|
|||||||
|
|
||||||
test('docs demo media uses shared cache-busting asset version token', () => {
|
test('docs demo media uses shared cache-busting asset version token', () => {
|
||||||
expect(docsIndexContents).toMatch(/const demoAssetVersion = ['"][^'"]+['"]/);
|
expect(docsIndexContents).toMatch(/const demoAssetVersion = ['"][^'"]+['"]/);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(':poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`"');
|
||||||
':poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`"',
|
expect(docsIndexContents).toContain('<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />');
|
||||||
);
|
expect(docsIndexContents).toContain('<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />');
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain('<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">');
|
||||||
'<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />',
|
expect(docsIndexContents).toContain('<img :src="`/assets/minecard.gif?v=${demoAssetVersion}`" alt="SubMiner demo GIF fallback" style="width: 100%; height: auto;" />');
|
||||||
);
|
|
||||||
expect(docsIndexContents).toContain(
|
|
||||||
'<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />',
|
|
||||||
);
|
|
||||||
expect(docsIndexContents).toContain(
|
|
||||||
'<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">',
|
|
||||||
);
|
|
||||||
expect(docsIndexContents).toContain(
|
|
||||||
'<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ titleTemplate: Immersion Mining Workflow for MPV
|
|||||||
hero:
|
hero:
|
||||||
name: SubMiner
|
name: SubMiner
|
||||||
text: Immersion Mining for MPV
|
text: Immersion Mining for MPV
|
||||||
tagline: Watch media, mine vocabulary, and craft anki cards without leaving the scene.
|
tagline: Watch media, mine vocabulary, and build cards without leaving the scene.
|
||||||
image:
|
image:
|
||||||
src: /assets/SubMiner.png
|
src: /assets/SubMiner.png
|
||||||
alt: SubMiner logo
|
alt: SubMiner logo
|
||||||
@@ -35,11 +35,16 @@ features:
|
|||||||
alt: Anki card icon
|
alt: Anki card icon
|
||||||
title: Anki Card Enrichment
|
title: Anki Card Enrichment
|
||||||
details: Auto-fills card fields with subtitle sentence, clipping, image, and translation so you can focus on learning.
|
details: Auto-fills card fields with subtitle sentence, clipping, image, and translation so you can focus on learning.
|
||||||
|
- icon:
|
||||||
|
src: /assets/dual-layer.svg
|
||||||
|
alt: Dual layer icon
|
||||||
|
title: Three-Plane Overlay Stack
|
||||||
|
details: Secondary context plane + visible interactive layer + invisible interaction plane, each with independent behavior and startup state.
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/highlight.svg
|
src: /assets/highlight.svg
|
||||||
alt: Highlight icon
|
alt: Highlight icon
|
||||||
title: Reading Annotations
|
title: N+1 Highlighting
|
||||||
details: Combines N+1 targeting, Jiten frequency highlighting, and JLPT tagging so useful cues stay visible while you read.
|
details: Surfaces known words from your deck so unknown targets stand out during immersion sessions.
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/tokenization.svg
|
src: /assets/tokenization.svg
|
||||||
alt: Tokenization icon
|
alt: Tokenization icon
|
||||||
@@ -50,6 +55,16 @@ features:
|
|||||||
alt: Subtitle download icon
|
alt: Subtitle download icon
|
||||||
title: Subtitle Download & Sync
|
title: Subtitle Download & Sync
|
||||||
details: Pull and synchronize subtitles with Jimaku plus alass/ffsubsync in one cohesive workflow.
|
details: Pull and synchronize subtitles with Jimaku plus alass/ffsubsync in one cohesive workflow.
|
||||||
|
- icon:
|
||||||
|
src: /assets/keyboard.svg
|
||||||
|
alt: Keyboard icon
|
||||||
|
title: Keyboard-Driven
|
||||||
|
details: Run lookups, mining actions, clipping, and workflow toggles with one configurable shortcut surface.
|
||||||
|
- icon:
|
||||||
|
src: /assets/texthooker.svg
|
||||||
|
alt: Texthooker icon
|
||||||
|
title: Texthooker & WebSocket
|
||||||
|
details: Stream subtitles in real time to browser tools via local WebSocket and keep your stack integrated.
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -95,7 +110,7 @@ const demoAssetVersion = '20260223-2';
|
|||||||
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
|
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
|
||||||
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
|
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
|
||||||
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
|
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
|
||||||
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
<img :src="`/assets/minecard.gif?v=${demoAssetVersion}`" alt="SubMiner demo GIF fallback" style="width: 100%; height: auto;" />
|
||||||
</a>
|
</a>
|
||||||
</video>
|
</video>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
|||||||
mkdir -p ~/.config/SubMiner
|
mkdir -p ~/.config/SubMiner
|
||||||
cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
||||||
mkdir -p ~/.config/mpv/scripts/subminer
|
mkdir -p ~/.config/mpv/scripts/subminer
|
||||||
mkdir -p ~/.config/mpv/script-opts
|
|
||||||
cp -R /tmp/plugin/subminer/. ~/.config/mpv/scripts/subminer/
|
cp -R /tmp/plugin/subminer/. ~/.config/mpv/scripts/subminer/
|
||||||
cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
||||||
|
|
||||||
@@ -183,6 +182,9 @@ All keybindings use a `y` chord prefix — press `y`, then the second key:
|
|||||||
| `y-s` | Start overlay |
|
| `y-s` | Start overlay |
|
||||||
| `y-S` | Stop overlay |
|
| `y-S` | Stop overlay |
|
||||||
| `y-t` | Toggle visible overlay |
|
| `y-t` | Toggle visible overlay |
|
||||||
|
| `y-i` | Toggle invisible overlay |
|
||||||
|
| `y-I` | Show invisible overlay |
|
||||||
|
| `y-u` | Hide invisible overlay |
|
||||||
| `y-o` | Open Yomitan settings |
|
| `y-o` | Open Yomitan settings |
|
||||||
| `y-r` | Restart overlay |
|
| `y-r` | Restart overlay |
|
||||||
| `y-c` | Check overlay status |
|
| `y-c` | Check overlay status |
|
||||||
@@ -194,10 +196,7 @@ See [MPV Plugin](/mpv-plugin) for the full configuration reference, script messa
|
|||||||
After installing, confirm SubMiner is working:
|
After installing, confirm SubMiner is working:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Play a file (default plugin config auto-starts visible overlay and waits for annotation readiness)
|
# Start the overlay (connects to mpv IPC)
|
||||||
subminer video.mkv
|
|
||||||
|
|
||||||
# Optional explicit overlay start for setups with plugin auto_start=no
|
|
||||||
subminer --start video.mkv
|
subminer --start video.mkv
|
||||||
|
|
||||||
# Useful launch modes for troubleshooting
|
# Useful launch modes for troubleshooting
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ The expected files are:
|
|||||||
|
|
||||||
Each bank maps terms to frequency metadata; only entries with a `frequency.displayValue` are considered for JLPT tagging.
|
Each bank maps terms to frequency metadata; only entries with a `frequency.displayValue` are considered for JLPT tagging.
|
||||||
|
|
||||||
SubMiner also reuses the same `term_meta_bank_*.json` format for frequency-based subtitle highlighting, using installed/default `frequency-dictionary` locations or an explicit `subtitleStyle.frequencyDictionary.sourcePath`.
|
SubMiner also reuses the same `term_meta_bank_*.json` format for frequency-based subtitle highlighting. The default frequency source is now bundled as `vendor/jiten_freq_global`, so users can enable `subtitleStyle.frequencyDictionary` without extra setup.
|
||||||
|
|
||||||
## Source and update process
|
## Source and update process
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ subminer -r -d ~/Anime # recursive search
|
|||||||
|
|
||||||
fzf shows video files in a fuzzy-searchable list. If `chafa` is installed, you get thumbnail previews in the right pane. Thumbnails are sourced from the freedesktop thumbnail cache first, then generated on the fly with `ffmpegthumbnailer` or `ffmpeg` as fallback.
|
fzf shows video files in a fuzzy-searchable list. If `chafa` is installed, you get thumbnail previews in the right pane. Thumbnails are sourced from the freedesktop thumbnail cache first, then generated on the fly with `ffmpegthumbnailer` or `ffmpeg` as fallback.
|
||||||
|
|
||||||
| Optional tool | Purpose |
|
| Optional tool | Purpose |
|
||||||
| ------------------- | --------------------------------- |
|
| --------------------- | -------------------------------- |
|
||||||
| `chafa` | Render thumbnails in the terminal |
|
| `chafa` | Render thumbnails in the terminal |
|
||||||
| `ffmpegthumbnailer` | Generate thumbnails on the fly |
|
| `ffmpegthumbnailer` | Generate thumbnails on the fly |
|
||||||
|
|
||||||
### rofi
|
### rofi
|
||||||
|
|
||||||
@@ -53,49 +53,46 @@ SUBMINER_ROFI_THEME=/path/to/custom-theme.rasi subminer -R
|
|||||||
## Common Commands
|
## Common Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer video.mkv # play a specific file (default plugin config auto-starts visible overlay)
|
subminer video.mkv # play a specific file
|
||||||
subminer --start video.mkv # optional explicit overlay start when plugin auto_start=no
|
subminer --start video.mkv # play + explicitly start overlay
|
||||||
subminer -S video.mkv # same as above via --start-overlay
|
|
||||||
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
|
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
|
||||||
subminer ytsearch:"jp news" # YouTube search
|
subminer ytsearch:"jp news" # YouTube search
|
||||||
```
|
```
|
||||||
|
|
||||||
## Subcommands
|
## Subcommands
|
||||||
|
|
||||||
| Subcommand | Purpose |
|
| Subcommand | Purpose |
|
||||||
| -------------------------- | ---------------------------------------------------------- |
|
| ------------------------- | ---------------------------------------------- |
|
||||||
| `subminer jellyfin` / `jf` | Jellyfin workflows (`-d` discovery, `-p` play, `-l` login) |
|
| `subminer jellyfin` / `jf` | Jellyfin workflows (`-d` discovery, `-p` play, `-l` login) |
|
||||||
| `subminer yt` / `youtube` | YouTube shorthand (`-o`, `-m`) |
|
| `subminer yt` / `youtube` | YouTube shorthand (`-o`, `-m`) |
|
||||||
| `subminer doctor` | Dependency + config + socket diagnostics |
|
| `subminer doctor` | Dependency + config + socket diagnostics |
|
||||||
| `subminer config path` | Print active config file path |
|
| `subminer config path` | Print active config file path |
|
||||||
| `subminer config show` | Print active config contents |
|
| `subminer config show` | Print active config contents |
|
||||||
| `subminer mpv status` | Check mpv socket readiness |
|
| `subminer mpv status` | Check mpv socket readiness |
|
||||||
| `subminer mpv socket` | Print active socket path |
|
| `subminer mpv socket` | Print active socket path |
|
||||||
| `subminer mpv idle` | Launch detached idle mpv instance |
|
| `subminer mpv idle` | Launch detached idle mpv instance |
|
||||||
| `subminer texthooker` | Launch texthooker-only mode |
|
| `subminer texthooker` | Launch texthooker-only mode |
|
||||||
| `subminer app` | Pass arguments directly to SubMiner binary |
|
| `subminer app` | Pass arguments directly to SubMiner binary |
|
||||||
|
|
||||||
Use `subminer <subcommand> -h` for command-specific help.
|
Use `subminer <subcommand> -h` for command-specific help.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
| ----------------------- | --------------------------------------------------- |
|
| -------------------- | -------------------------------------------- |
|
||||||
| `-d, --directory` | Video search directory (default: cwd) |
|
| `-d, --directory` | Video search directory (default: cwd) |
|
||||||
| `-r, --recursive` | Search directories recursively |
|
| `-r, --recursive` | Search directories recursively |
|
||||||
| `-R, --rofi` | Use rofi instead of fzf |
|
| `-R, --rofi` | Use rofi instead of fzf |
|
||||||
| `--start` | Explicitly start overlay after mpv launches |
|
| `-S, --start` | Start overlay after mpv launches |
|
||||||
| `-S, --start-overlay` | Explicitly start overlay after mpv launches |
|
| `-T, --no-texthooker`| Disable texthooker server |
|
||||||
| `-T, --no-texthooker` | Disable texthooker server |
|
| `-p, --profile` | mpv profile name (default: `subminer`) |
|
||||||
| `-p, --profile` | mpv profile name (default: `subminer`) |
|
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`) |
|
||||||
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`) |
|
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) |
|
||||||
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) |
|
| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) |
|
||||||
| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) |
|
|
||||||
|
|
||||||
With default plugin settings (`auto_start=yes`, `auto_start_visible_overlay=yes`, `auto_start_pause_until_ready=yes`), explicit start flags are usually unnecessary.
|
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
- Default log level is `info`
|
- Default log level is `info`
|
||||||
- `--background` mode defaults to `warn` unless `--log-level` is explicitly set
|
- `--background` mode defaults to `warn` unless `--log-level` is explicitly set
|
||||||
- `--dev` / `--debug` control app behavior, not logging verbosity — use `--log-level` for that
|
- `--dev` / `--debug` control app behavior, not logging verbosity — use `--log-level` for that
|
||||||
|
|
||||||
|
|||||||
@@ -20,36 +20,49 @@ SubMiner prioritizes subtitle responsiveness over heavy initialization:
|
|||||||
1. The first subtitle render is **plain text first** (no tokenization wait).
|
1. The first subtitle render is **plain text first** (no tokenization wait).
|
||||||
2. Tokenized enrichment (word spans, known-word flags, JLPT/frequency metadata) is applied right after parsing completes.
|
2. Tokenized enrichment (word spans, known-word flags, JLPT/frequency metadata) is applied right after parsing completes.
|
||||||
3. Under rapid subtitle churn, SubMiner uses a **latest-only tokenization queue** so stale lines are dropped instead of building lag.
|
3. Under rapid subtitle churn, SubMiner uses a **latest-only tokenization queue** so stale lines are dropped instead of building lag.
|
||||||
4. MeCab, Yomitan extension load, and dictionary prewarm run as background warmups after overlay initialization (configurable via `startupWarmups`, including low-power mode).
|
4. MeCab, Yomitan extension load, and dictionary prewarm run as background warmups after overlay initialization.
|
||||||
|
|
||||||
This keeps early playback snappy and avoids mpv-side sluggishness while startup work completes.
|
This keeps early playback snappy and avoids mpv-side sluggishness while startup work completes.
|
||||||
|
|
||||||
## Overlay Model
|
## The Three Overlay Planes
|
||||||
|
|
||||||
SubMiner uses one overlay window with modal surfaces.
|
SubMiner uses three overlay planes, each serving a different purpose.
|
||||||
|
|
||||||
### Primary Subtitle Layer
|
### Visible Overlay
|
||||||
|
|
||||||
The visible overlay renders subtitles as tokenized, clickable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
|
The visible overlay renders subtitles as tokenized, clickable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
|
||||||
|
|
||||||
- Word-level click targets for Yomitan lookup
|
- Word-level click targets for Yomitan lookup
|
||||||
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
|
|
||||||
- Right-click to pause/resume
|
- Right-click to pause/resume
|
||||||
- Right-click + drag to reposition subtitles
|
- Right-click + drag to reposition subtitles
|
||||||
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
|
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
|
||||||
- **N+1 highlighting** — known words from your Anki deck are visually highlighted
|
- **N+1 highlighting** — known words from your Anki deck are visually highlighted
|
||||||
|
|
||||||
Toggle visibility with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
|
Toggle with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
|
||||||
|
|
||||||
### Secondary Subtitle Bar
|
### Secondary Subtitle Plane
|
||||||
|
|
||||||
The secondary subtitle bar is a compact top-strip region in the same overlay window for translation/context visibility while keeping primary reading flow below. It mirrors your configured secondary subtitle preference and can be independently shown or hidden.
|
The secondary plane is a compact top-strip layer for translation and context visibility while keeping primary reading flow below. It mirrors your configured secondary subtitle preference and can be independently shown or hidden.
|
||||||
|
|
||||||
It is controlled by `secondarySub` configuration and shares lifecycle with the main overlay window.
|
It is controlled by `secondarySub` configuration and shares lifecycle with the overlay stack.
|
||||||
|
|
||||||
### Modal Surfaces
|
### Invisible Overlay
|
||||||
|
|
||||||
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
|
The invisible overlay is a transparent layer aligned with mpv's own subtitle rendering. It uses mpv's subtitle metrics (font size, margins, position, scaling) to map click targets accurately.
|
||||||
|
|
||||||
|
This layer still supports:
|
||||||
|
|
||||||
|
- Word-level click-through lookups over the text region
|
||||||
|
- Optional manual position fine-tuning in pixel mode
|
||||||
|
- Independent toggle behavior with global shortcuts
|
||||||
|
|
||||||
|
Position edit mode is available via `Ctrl/Cmd+Shift+P`, then arrow keys / `hjkl` to nudge position; `Shift` moves faster. Save with `Enter` or `Ctrl+S`, cancel with `Esc`.
|
||||||
|
|
||||||
|
Toggle controls:
|
||||||
|
|
||||||
|
- `Alt+Shift+O` / `y-t`: visible overlay
|
||||||
|
- `Alt+Shift+I` / `y-i`: invisible overlay
|
||||||
|
- Secondary plane visibility is controlled via `secondarySub` config and matching global shortcuts.
|
||||||
|
|
||||||
## Looking Up Words
|
## Looking Up Words
|
||||||
|
|
||||||
@@ -60,10 +73,10 @@ Jimaku search, field-grouping, runtime options, and manual subsync open as modal
|
|||||||
3. Yomitan detects the text selection and opens its popup with dictionary results.
|
3. Yomitan detects the text selection and opens its popup with dictionary results.
|
||||||
4. From the Yomitan popup, you can add the word directly to Anki.
|
4. From the Yomitan popup, you can add the word directly to Anki.
|
||||||
|
|
||||||
### On Overlay Subtitles
|
### On the Invisible Overlay
|
||||||
|
|
||||||
1. Subtitles are rendered directly in the overlay.
|
1. The invisible layer sits over mpv's own subtitle text.
|
||||||
2. Click on any word in the subtitle.
|
2. Click on any word in the subtitle — SubMiner maps your click position to the underlying text.
|
||||||
3. On macOS, word selection happens automatically on hover.
|
3. On macOS, word selection happens automatically on hover.
|
||||||
4. Yomitan popup appears for lookup and card creation.
|
4. Yomitan popup appears for lookup and card creation.
|
||||||
|
|
||||||
@@ -73,13 +86,11 @@ There are three ways to create cards, depending on your workflow.
|
|||||||
|
|
||||||
### 1. Auto-Update from Yomitan
|
### 1. Auto-Update from Yomitan
|
||||||
|
|
||||||
This is the most common flow. Yomitan creates a card in Anki, and SubMiner enriches it automatically.
|
This is the most common flow. Yomitan creates a card in Anki, and SubMiner detects it via polling and enriches it automatically.
|
||||||
|
|
||||||
1. Click a word → Yomitan popup appears.
|
1. Click a word → Yomitan popup appears.
|
||||||
2. Click the Anki icon in Yomitan to add the word.
|
2. Click the Anki icon in Yomitan to add the word.
|
||||||
3. SubMiner receives or detects the new card:
|
3. SubMiner detects the new card (polls AnkiConnect every 3 seconds by default).
|
||||||
- **Proxy mode** (`ankiConnect.proxy.enabled: true`): immediate enrich after successful `addNote` / `addNotes`.
|
|
||||||
- **Polling mode** (default): detects via AnkiConnect polling (`ankiConnect.pollingRate`, default 3 seconds).
|
|
||||||
4. SubMiner updates the card with:
|
4. SubMiner updates the card with:
|
||||||
- **Sentence**: The current subtitle line.
|
- **Sentence**: The current subtitle line.
|
||||||
- **Audio**: Extracted from the video using the subtitle's start/end timing (plus configurable padding).
|
- **Audio**: Extracted from the video using the subtitle's start/end timing (plus configurable padding).
|
||||||
@@ -98,13 +109,13 @@ If you prefer a hands-on approach (animecards-style), you can copy the current s
|
|||||||
- For multiple lines: press `Ctrl/Cmd+Shift+C`, then a digit `1`–`9` to select how many recent subtitle lines to combine. The combined text is copied to the clipboard.
|
- For multiple lines: press `Ctrl/Cmd+Shift+C`, then a digit `1`–`9` to select how many recent subtitle lines to combine. The combined text is copied to the clipboard.
|
||||||
3. Press `Ctrl/Cmd+V` to update the last-added card with the clipboard contents plus audio, image, and translation — the same fields auto-update would fill.
|
3. Press `Ctrl/Cmd+V` to update the last-added card with the clipboard contents plus audio, image, and translation — the same fields auto-update would fill.
|
||||||
|
|
||||||
This is useful when auto-update is disabled or when you want explicit control over which subtitle line gets attached to the card.
|
This is useful when auto-update polling is disabled or when you want explicit control over which subtitle line gets attached to the card.
|
||||||
|
|
||||||
| Shortcut | Action | Config key |
|
| Shortcut | Action | Config key |
|
||||||
| -------------------------- | ------------------------------- | --------------------------------------- |
|
| --------------------------- | ----------------------------------------- | ------------------------------------- |
|
||||||
| `Ctrl/Cmd+C` | Copy current subtitle | `shortcuts.copySubtitle` |
|
| `Ctrl/Cmd+C` | Copy current subtitle | `shortcuts.copySubtitle` |
|
||||||
| `Ctrl/Cmd+Shift+C` + digit | Copy multiple recent lines | `shortcuts.copySubtitleMultiple` |
|
| `Ctrl/Cmd+Shift+C` + digit | Copy multiple recent lines | `shortcuts.copySubtitleMultiple` |
|
||||||
| `Ctrl/Cmd+V` | Update last card from clipboard | `shortcuts.updateLastCardFromClipboard` |
|
| `Ctrl/Cmd+V` | Update last card from clipboard | `shortcuts.updateLastCardFromClipboard` |
|
||||||
|
|
||||||
### 3. Mine Sentence (Hotkey)
|
### 3. Mine Sentence (Hotkey)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
|||||||
mkdir -p ~/.config/SubMiner
|
mkdir -p ~/.config/SubMiner
|
||||||
cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
||||||
mkdir -p ~/.config/mpv/scripts/subminer
|
mkdir -p ~/.config/mpv/scripts/subminer
|
||||||
mkdir -p ~/.config/mpv/script-opts
|
|
||||||
cp -R /tmp/plugin/subminer/. ~/.config/mpv/scripts/subminer/
|
cp -R /tmp/plugin/subminer/. ~/.config/mpv/scripts/subminer/
|
||||||
cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
||||||
|
|
||||||
@@ -29,16 +28,19 @@ input-ipc-server=/tmp/subminer-socket
|
|||||||
|
|
||||||
All keybindings use a `y` chord prefix — press `y`, then the second key:
|
All keybindings use a `y` chord prefix — press `y`, then the second key:
|
||||||
|
|
||||||
| Chord | Action |
|
| Chord | Action |
|
||||||
| ----- | ---------------------- |
|
| ----- | ------------------------ |
|
||||||
| `y-y` | Open menu |
|
| `y-y` | Open menu |
|
||||||
| `y-s` | Start overlay |
|
| `y-s` | Start overlay |
|
||||||
| `y-S` | Stop overlay |
|
| `y-S` | Stop overlay |
|
||||||
| `y-t` | Toggle visible overlay |
|
| `y-t` | Toggle visible overlay |
|
||||||
| `y-o` | Open settings window |
|
| `y-i` | Toggle invisible overlay |
|
||||||
| `y-r` | Restart overlay |
|
| `y-I` | Show invisible overlay |
|
||||||
| `y-c` | Check status |
|
| `y-u` | Hide invisible overlay |
|
||||||
| `y-k` | Skip intro (AniSkip) |
|
| `y-o` | Open settings window |
|
||||||
|
| `y-r` | Restart overlay |
|
||||||
|
| `y-c` | Check status |
|
||||||
|
| `y-k` | Skip intro (AniSkip) |
|
||||||
|
|
||||||
## Menu
|
## Menu
|
||||||
|
|
||||||
@@ -49,9 +51,10 @@ SubMiner:
|
|||||||
1. Start overlay
|
1. Start overlay
|
||||||
2. Stop overlay
|
2. Stop overlay
|
||||||
3. Toggle overlay
|
3. Toggle overlay
|
||||||
4. Open options
|
4. Toggle invisible overlay
|
||||||
5. Restart overlay
|
5. Open options
|
||||||
6. Check status
|
6. Restart overlay
|
||||||
|
7. Check status
|
||||||
```
|
```
|
||||||
|
|
||||||
Select an item by pressing its number.
|
Select an item by pressing its number.
|
||||||
@@ -77,16 +80,14 @@ texthooker_port=5174
|
|||||||
backend=auto
|
backend=auto
|
||||||
|
|
||||||
# Start the overlay automatically when a file is loaded.
|
# Start the overlay automatically when a file is loaded.
|
||||||
# Runs only when mpv input-ipc-server matches socket_path.
|
auto_start=no
|
||||||
auto_start=yes
|
|
||||||
|
|
||||||
# Show the visible overlay on auto-start.
|
# Show the visible overlay on auto-start.
|
||||||
# Runs only when mpv input-ipc-server matches socket_path.
|
auto_start_visible_overlay=no
|
||||||
auto_start_visible_overlay=yes
|
|
||||||
|
|
||||||
# Pause mpv on visible auto-start until SubMiner signals overlay/tokenization readiness.
|
# Invisible overlay startup: platform-default, visible, hidden.
|
||||||
# Requires auto_start=yes and auto_start_visible_overlay=yes.
|
# platform-default = hidden on Linux, visible on macOS/Windows.
|
||||||
auto_start_pause_until_ready=yes
|
auto_start_invisible_overlay=platform-default
|
||||||
|
|
||||||
# Show OSD messages for overlay status changes.
|
# Show OSD messages for overlay status changes.
|
||||||
osd_messages=yes
|
osd_messages=yes
|
||||||
@@ -120,27 +121,27 @@ aniskip_button_duration=3
|
|||||||
|
|
||||||
### Option Reference
|
### Option Reference
|
||||||
|
|
||||||
| Option | Default | Values | Description |
|
| Option | Default | Values | Description |
|
||||||
| ---------------------------- | ----------------------------- | ------------------------------------------ | ---------------------------------------------------------------------- |
|
| ------------------------------ | ---------------------- | ------------------------------------------ | -------------------------------- |
|
||||||
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
|
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
|
||||||
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
|
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
|
||||||
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
|
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
|
||||||
| `texthooker_port` | `5174` | 1–65535 | Texthooker server port |
|
| `texthooker_port` | `5174` | 1–65535 | Texthooker server port |
|
||||||
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
|
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
|
||||||
| `auto_start` | `yes` | `yes` / `no` | Auto-start overlay on file load when mpv socket matches `socket_path` |
|
| `auto_start` | `no` | `yes` / `no` | Auto-start overlay on file load |
|
||||||
| `auto_start_visible_overlay` | `yes` | `yes` / `no` | Show visible layer on auto-start when mpv socket matches `socket_path` |
|
| `auto_start_visible_overlay` | `no` | `yes` / `no` | Show visible layer on auto-start |
|
||||||
| `auto_start_pause_until_ready` | `yes` | `yes` / `no` | Pause mpv on visible auto-start; resume when SubMiner signals tokenization-ready |
|
| `auto_start_invisible_overlay` | `platform-default` | `platform-default`, `visible`, `hidden` | Invisible layer on auto-start |
|
||||||
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
|
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
|
||||||
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
|
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
|
||||||
| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection |
|
| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection |
|
||||||
| `aniskip_title` | `""` | string | Override title used for lookup |
|
| `aniskip_title` | `""` | string | Override title used for lookup |
|
||||||
| `aniskip_season` | `""` | numeric season | Optional season hint |
|
| `aniskip_season` | `""` | numeric season | Optional season hint |
|
||||||
| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id |
|
| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id |
|
||||||
| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed |
|
| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed |
|
||||||
| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt |
|
| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt |
|
||||||
| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) |
|
| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) |
|
||||||
| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) |
|
| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) |
|
||||||
| `aniskip_button_duration` | `3` | float seconds | OSD hint duration |
|
| `aniskip_button_duration` | `3` | float seconds | OSD hint duration |
|
||||||
|
|
||||||
## Binary Auto-Detection
|
## Binary Auto-Detection
|
||||||
|
|
||||||
@@ -182,11 +183,13 @@ The plugin can be controlled from other mpv scripts or the mpv command line usin
|
|||||||
script-message subminer-start
|
script-message subminer-start
|
||||||
script-message subminer-stop
|
script-message subminer-stop
|
||||||
script-message subminer-toggle
|
script-message subminer-toggle
|
||||||
|
script-message subminer-toggle-invisible
|
||||||
|
script-message subminer-show-invisible
|
||||||
|
script-message subminer-hide-invisible
|
||||||
script-message subminer-menu
|
script-message subminer-menu
|
||||||
script-message subminer-options
|
script-message subminer-options
|
||||||
script-message subminer-restart
|
script-message subminer-restart
|
||||||
script-message subminer-status
|
script-message subminer-status
|
||||||
script-message subminer-autoplay-ready
|
|
||||||
script-message subminer-aniskip-refresh
|
script-message subminer-aniskip-refresh
|
||||||
script-message subminer-skip-intro
|
script-message subminer-skip-intro
|
||||||
```
|
```
|
||||||
@@ -202,12 +205,7 @@ script-message subminer-start backend=hyprland socket=/custom/path texthooker=no
|
|||||||
|
|
||||||
## AniSkip Intro Skip
|
## AniSkip Intro Skip
|
||||||
|
|
||||||
- AniSkip lookups are gated. The plugin only runs lookup when:
|
- On file load, plugin resolves title + episode, resolves MAL id, then calls AniSkip API.
|
||||||
- SubMiner launcher metadata is present, or
|
|
||||||
- SubMiner app process is already running, or
|
|
||||||
- You explicitly call `script-message subminer-aniskip-refresh`.
|
|
||||||
- Lookups are asynchronous (no blocking `ps`/`curl` on `file-loaded`).
|
|
||||||
- MAL/title resolution is cached for the current mpv session.
|
|
||||||
- When launched via `subminer`, launcher runs `guessit` first (file targets) and passes title/season/episode to the plugin; fallback is filename-derived title.
|
- When launched via `subminer`, launcher runs `guessit` first (file targets) and passes title/season/episode to the plugin; fallback is filename-derived title.
|
||||||
- Install `guessit` for best detection quality (`python3 -m pip install --user guessit`).
|
- Install `guessit` for best detection quality (`python3 -m pip install --user guessit`).
|
||||||
- If OP interval exists, plugin adds `AniSkip Intro Start` and `AniSkip Intro End` chapters.
|
- If OP interval exists, plugin adds `AniSkip Intro Start` and `AniSkip Intro End` chapters.
|
||||||
@@ -216,9 +214,7 @@ script-message subminer-start backend=hyprland socket=/custom/path texthooker=no
|
|||||||
|
|
||||||
## Lifecycle
|
## Lifecycle
|
||||||
|
|
||||||
- **File loaded**: If `auto_start=yes`, the plugin starts the overlay, then defers AniSkip lookup until after startup delay.
|
- **File loaded**: If `auto_start=yes`, the plugin starts the overlay and applies visibility preferences after a short delay.
|
||||||
- **Auto-start pause gate**: If `auto_start_visible_overlay=yes` and `auto_start_pause_until_ready=yes`, launcher starts mpv paused and the plugin resumes playback after SubMiner reports tokenization-ready (with timeout fallback).
|
|
||||||
- **Duplicate auto-start events**: Repeated `file-loaded` hooks while overlay is already running are ignored for auto-start triggers (prevents duplicate start attempts).
|
|
||||||
- **MPV shutdown**: The plugin sends a stop command to gracefully shut down both the overlay and the texthooker server.
|
- **MPV shutdown**: The plugin sends a stop command to gracefully shut down both the overlay and the texthooker server.
|
||||||
- **Texthooker**: Starts as a separate subprocess before the overlay to ensure the app lock is acquired first.
|
- **Texthooker**: Starts as a separate subprocess before the overlay to ensure the app lock is acquired first.
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
# Secondary Subtitles Main Overlay Implementation Plan
|
|
||||||
|
|
||||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
||||||
|
|
||||||
**Goal:** Ensure secondary subtitles render in the unified main overlay window and remove stale secondary-window/layer paths.
|
|
||||||
|
|
||||||
**Architecture:** Keep secondary subtitle DOM in the shared renderer tree, rely on mode classes (`secondary-sub-hidden|visible|hover`) for visibility, and remove obsolete legacy overlay-layer assumptions. Preserve modal behavior and existing subtitle rendering flow.
|
|
||||||
|
|
||||||
**Tech Stack:** TypeScript, Electron renderer CSS/DOM, Bun test runner.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Add Regression Tests For Main Overlay Secondary Rendering
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
|
|
||||||
- Modify: `src/renderer/subtitle-render.test.ts`
|
|
||||||
- Modify: `src/renderer/error-recovery.test.ts`
|
|
||||||
|
|
||||||
**Step 1: Write failing tests**
|
|
||||||
|
|
||||||
- Assert stylesheet no longer hides secondary subtitles in `layer-visible`.
|
|
||||||
- Assert renderer platform resolution ignores legacy `secondary` overlay layer.
|
|
||||||
|
|
||||||
**Step 2: Run tests to verify failures**
|
|
||||||
|
|
||||||
Run: `bun test src/renderer/subtitle-render.test.ts src/renderer/error-recovery.test.ts`
|
|
||||||
Expected: FAIL on secondary subtitle hide rule + legacy secondary layer handling.
|
|
||||||
|
|
||||||
### Task 2: Remove Secondary-Window CSS/Routing Assumptions
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
|
|
||||||
- Modify: `src/renderer/style.css`
|
|
||||||
- Modify: `src/renderer/utils/platform.ts`
|
|
||||||
- Modify: `src/renderer/error-recovery.ts`
|
|
||||||
- Modify: `src/types.ts`
|
|
||||||
|
|
||||||
**Step 1: Implement minimal changes**
|
|
||||||
|
|
||||||
- Remove legacy forced hide on `#secondarySubContainer`.
|
|
||||||
- Remove obsolete layer-specific secondary-subtitle CSS blocks.
|
|
||||||
- Drop legacy `secondary` overlay-layer parsing path from renderer platform resolver.
|
|
||||||
- Narrow related overlay layer type unions.
|
|
||||||
|
|
||||||
**Step 2: Run targeted tests**
|
|
||||||
|
|
||||||
Run: `bun test src/renderer/subtitle-render.test.ts src/renderer/error-recovery.test.ts`
|
|
||||||
Expected: PASS.
|
|
||||||
|
|
||||||
### Task 3: Validate Wider Related Surface
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
|
|
||||||
- No additional code changes required.
|
|
||||||
|
|
||||||
**Step 1: Run broader related tests**
|
|
||||||
|
|
||||||
Run: `bun test src/renderer/subtitle-render.test.ts src/renderer/error-recovery.test.ts src/main/runtime/overlay-window-runtime-handlers.test.ts src/main/runtime/overlay-window-factory.test.ts src/core/services/overlay-manager.test.ts`
|
|
||||||
Expected: Renderer tests pass; report any unrelated pre-existing failures.
|
|
||||||
@@ -1,40 +1,15 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="ac-card" x1="6" y1="8" x2="38" y2="44" gradientUnits="userSpaceOnUse">
|
<linearGradient id="ac" x1="6" y1="6" x2="36" y2="42" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#34d399"/>
|
<stop stop-color="#34d399"/>
|
||||||
<stop offset="1" stop-color="#059669"/>
|
<stop offset="1" stop-color="#059669"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="ac-glow" x1="8" y1="10" x2="36" y2="42" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#6ee7b7" stop-opacity="0.5"/>
|
|
||||||
<stop offset="1" stop-color="#059669" stop-opacity="0"/>
|
|
||||||
</linearGradient>
|
|
||||||
<filter id="ac-soft" x="-20%" y="-20%" width="140%" height="140%">
|
|
||||||
<feGaussianBlur in="SourceGraphic" stdDeviation="1.2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
</defs>
|
||||||
<!-- Glow aura behind card -->
|
<rect x="12" y="5" width="24" height="34" rx="3" fill="#059669" opacity="0.18"/>
|
||||||
<rect x="6" y="8" width="28" height="36" rx="5" fill="url(#ac-glow)" filter="url(#ac-soft)"/>
|
<rect x="8" y="9" width="24" height="34" rx="3" fill="url(#ac)"/>
|
||||||
<!-- Shadow card (back) -->
|
<rect x="13" y="18" width="14" height="2.5" rx="1.25" fill="white" opacity="0.85"/>
|
||||||
<rect x="14" y="5" width="26" height="34" rx="4" fill="#059669" opacity="0.15"/>
|
<rect x="13" y="24" width="10" height="2.5" rx="1.25" fill="white" opacity="0.4"/>
|
||||||
<!-- Main card -->
|
<rect x="13" y="30" width="12" height="2.5" rx="1.25" fill="white" opacity="0.4"/>
|
||||||
<rect x="6" y="9" width="26" height="34" rx="4" fill="url(#ac-card)"/>
|
<path d="M39.5 8l1.8 4.2 4.2 1.8-4.2 1.8L39.5 20l-1.8-4.2L33.5 14l4.2-1.8z" fill="#34d399"/>
|
||||||
<!-- Sentence line -->
|
<path d="M36 27l1 2.3 2.3 1-2.3 1L36 33.5l-1-2.2-2.3-1 2.3-1z" fill="#34d399" opacity="0.45"/>
|
||||||
<rect x="10" y="16" width="16" height="2.5" rx="1.25" fill="white" opacity="0.9"/>
|
|
||||||
<!-- Audio waveform mini -->
|
|
||||||
<rect x="10" y="22" width="1.8" height="5" rx="0.9" fill="white" opacity="0.55"/>
|
|
||||||
<rect x="13" y="20.5" width="1.8" height="8" rx="0.9" fill="white" opacity="0.55"/>
|
|
||||||
<rect x="16" y="21.5" width="1.8" height="6" rx="0.9" fill="white" opacity="0.55"/>
|
|
||||||
<rect x="19" y="23" width="1.8" height="3" rx="0.9" fill="white" opacity="0.55"/>
|
|
||||||
<rect x="22" y="21" width="1.8" height="7" rx="0.9" fill="white" opacity="0.55"/>
|
|
||||||
<rect x="25" y="22.5" width="1.8" height="4" rx="0.9" fill="white" opacity="0.55"/>
|
|
||||||
<!-- Image thumbnail placeholder -->
|
|
||||||
<rect x="10" y="30" width="10" height="8" rx="2" fill="white" opacity="0.25"/>
|
|
||||||
<path d="M12.5 35.5l2-2.5 2 1.8 1.5-1 2.5 3h-8z" fill="white" opacity="0.5"/>
|
|
||||||
<!-- Translation line -->
|
|
||||||
<rect x="22" y="32" width="7" height="2" rx="1" fill="white" opacity="0.35"/>
|
|
||||||
<rect x="22" y="35.5" width="5" height="2" rx="1" fill="white" opacity="0.25"/>
|
|
||||||
<!-- Enrichment sparkle burst -->
|
|
||||||
<path d="M40 10l1.6 3.8 3.8 1.6-3.8 1.6L40 20.8l-1.6-3.8L34.6 15.4l3.8-1.6z" fill="#6ee7b7"/>
|
|
||||||
<path d="M37 29l0.9 2.1 2.1 0.9-2.1 0.9L37 35l-0.9-2.1-2.1-0.9 2.1-0.9z" fill="#6ee7b7" opacity="0.5"/>
|
|
||||||
<circle cx="43" cy="25" r="1.2" fill="#34d399" opacity="0.4"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 914 B |
@@ -1,39 +1,13 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="hl-freq" x1="0" y1="0" x2="14" y2="8" gradientUnits="userSpaceOnUse">
|
<linearGradient id="hl" x1="20" y1="14" x2="38" y2="34" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#fbbf24"/>
|
<stop stop-color="#fbbf24"/>
|
||||||
<stop offset="1" stop-color="#f59e0b"/>
|
<stop offset="1" stop-color="#f59e0b"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="hl-n1" x1="0" y1="0" x2="10" y2="10" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#60a5fa"/>
|
|
||||||
<stop offset="1" stop-color="#3b82f6"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="hl-jlpt" x1="0" y1="0" x2="12" y2="8" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#a78bfa"/>
|
|
||||||
<stop offset="1" stop-color="#7c3aed"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
</defs>
|
||||||
<!-- Viewport / video frame background -->
|
<rect x="2" y="17" width="10" height="14" rx="3" fill="#fbbf24" opacity="0.3"/>
|
||||||
<rect x="1" y="5" width="46" height="38" rx="4" fill="#1e293b" opacity="0.55"/>
|
<rect x="14" y="17" width="7" height="14" rx="3" fill="#fbbf24" opacity="0.3"/>
|
||||||
<rect x="1" y="5" width="46" height="38" rx="4" stroke="#334155" stroke-width="0.8" fill="none" opacity="0.5"/>
|
<rect x="23" y="13" width="13" height="22" rx="3.5" fill="url(#hl)"/>
|
||||||
<!-- Subtitle line 1 — tokens with frequency highlight -->
|
<rect x="38" y="17" width="8" height="14" rx="3" fill="#fbbf24" opacity="0.3"/>
|
||||||
<rect x="6" y="18" width="9" height="5" rx="1.5" fill="#cbd5e1" opacity="0.2"/>
|
<path d="M28.2 4l1 2.4 2.4 1-2.4 1-1 2.4-1-2.4-2.4-1 2.4-1z" fill="#fbbf24" opacity="0.7"/>
|
||||||
<!-- Frequency-highlighted token -->
|
|
||||||
<rect x="17" y="17" width="14" height="7" rx="2" fill="url(#hl-freq)" opacity="0.2"/>
|
|
||||||
<rect x="17.5" y="17.5" width="13" height="6" rx="1.8" fill="url(#hl-freq)"/>
|
|
||||||
<rect x="20" y="19.5" width="8" height="2" rx="1" fill="white" opacity="0.85"/>
|
|
||||||
<rect x="33" y="18" width="8" height="5" rx="1.5" fill="#cbd5e1" opacity="0.2"/>
|
|
||||||
<!-- Subtitle line 2 — tokens with N+1 dot and JLPT badge -->
|
|
||||||
<rect x="8" y="28" width="8" height="5" rx="1.5" fill="#cbd5e1" opacity="0.2"/>
|
|
||||||
<!-- N+1 targeted token with dot -->
|
|
||||||
<rect x="18" y="28" width="10" height="5" rx="1.5" fill="#60a5fa" opacity="0.15"/>
|
|
||||||
<rect x="18.5" y="28.5" width="9" height="4" rx="1.2" fill="#cbd5e1" opacity="0.3"/>
|
|
||||||
<circle cx="16.5" cy="30.5" r="2.2" fill="url(#hl-n1)"/>
|
|
||||||
<text x="16.5" y="31.9" text-anchor="middle" font-size="2.6" font-weight="800" fill="white" font-family="sans-serif">+1</text>
|
|
||||||
<!-- JLPT badge token -->
|
|
||||||
<rect x="30" y="28" width="7" height="5" rx="1.5" fill="#cbd5e1" opacity="0.3"/>
|
|
||||||
<rect x="37.5" y="27" width="9" height="7" rx="2" fill="url(#hl-jlpt)"/>
|
|
||||||
<text x="42" y="31.8" text-anchor="middle" font-size="3.5" font-weight="700" fill="white" font-family="sans-serif">N2</text>
|
|
||||||
<!-- Subtle sparkle -->
|
|
||||||
<path d="M43 10l0.7 1.6 1.6 0.7-1.6 0.7L43 14.6l-0.7-1.6-1.6-0.7 1.6-0.7z" fill="#fbbf24" opacity="0.5"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 729 B |
@@ -1,31 +1,21 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="kb-main" x1="2" y1="10" x2="46" y2="42" gradientUnits="userSpaceOnUse">
|
<linearGradient id="kb" x1="2" y1="10" x2="46" y2="42" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#c084fc"/>
|
<stop stop-color="#c084fc"/>
|
||||||
<stop offset="1" stop-color="#7c3aed"/>
|
<stop offset="1" stop-color="#7c3aed"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<filter id="kb-glow" x="-50%" y="-50%" width="200%" height="200%">
|
|
||||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
</defs>
|
||||||
<!-- Keyboard body -->
|
<rect x="2" y="12" width="44" height="30" rx="5" fill="url(#kb)" opacity="0.12"/>
|
||||||
<rect x="2" y="14" width="44" height="28" rx="4.5" fill="url(#kb-main)" opacity="0.1"/>
|
<rect x="2" y="12" width="44" height="30" rx="5" stroke="url(#kb)" stroke-width="1.5" fill="none"/>
|
||||||
<rect x="2" y="14" width="44" height="28" rx="4.5" stroke="url(#kb-main)" stroke-width="1.4" fill="none"/>
|
<rect x="6" y="16" width="8" height="6" rx="2" fill="url(#kb)"/>
|
||||||
<!-- Row 1 -->
|
<rect x="16" y="16" width="8" height="6" rx="2" fill="url(#kb)" opacity="0.35"/>
|
||||||
<rect x="6" y="18" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
<rect x="26" y="16" width="8" height="6" rx="2" fill="url(#kb)" opacity="0.35"/>
|
||||||
<rect x="15" y="18" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
<rect x="36" y="16" width="8" height="6" rx="2" fill="url(#kb)" opacity="0.35"/>
|
||||||
<rect x="24" y="18" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
<rect x="6" y="24" width="8" height="6" rx="2" fill="url(#kb)" opacity="0.35"/>
|
||||||
<rect x="33" y="18" width="11" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
<rect x="16" y="24" width="8" height="6" rx="2" fill="url(#kb)" opacity="0.35"/>
|
||||||
<!-- Row 2 — active key with glow -->
|
<rect x="26" y="24" width="8" height="6" rx="2" fill="url(#kb)" opacity="0.35"/>
|
||||||
<rect x="6" y="25.5" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
<rect x="36" y="24" width="8" height="6" rx="2" fill="url(#kb)"/>
|
||||||
<!-- Active/pressed key glow -->
|
<rect x="6" y="32" width="8" height="6" rx="2" fill="url(#kb)" opacity="0.35"/>
|
||||||
<rect x="15" y="25.5" width="7" height="5.5" rx="1.8" fill="#c084fc" opacity="0.25" filter="url(#kb-glow)"/>
|
<rect x="16" y="32" width="16" height="6" rx="2" fill="url(#kb)" opacity="0.35"/>
|
||||||
<rect x="15" y="25.5" width="7" height="5.5" rx="1.8" fill="url(#kb-main)"/>
|
<rect x="34" y="32" width="10" height="6" rx="2" fill="url(#kb)" opacity="0.35"/>
|
||||||
<text x="18.5" y="30" text-anchor="middle" font-size="3.5" font-weight="700" fill="white" font-family="sans-serif">M</text>
|
|
||||||
<rect x="24" y="25.5" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
|
||||||
<rect x="33" y="25.5" width="11" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
|
||||||
<!-- Row 3 — spacebar -->
|
|
||||||
<rect x="6" y="33" width="7" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
|
||||||
<rect x="15" y="33" width="16" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.25"/>
|
|
||||||
<rect x="33" y="33" width="11" height="5.5" rx="1.8" fill="url(#kb-main)" opacity="0.3"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 23 MiB After Width: | Height: | Size: 13 MiB |
|
Before Width: | Height: | Size: 21 MiB |
@@ -1,35 +1,16 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="sd-main" x1="4" y1="4" x2="44" y2="44" gradientUnits="userSpaceOnUse">
|
<linearGradient id="sd" x1="4" y1="4" x2="44" y2="44" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#22d3ee"/>
|
<stop stop-color="#22d3ee"/>
|
||||||
<stop offset="1" stop-color="#0891b2"/>
|
<stop offset="1" stop-color="#0891b2"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="sd-sync" x1="30" y1="28" x2="46" y2="44" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#34d399"/>
|
|
||||||
<stop offset="1" stop-color="#059669"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
</defs>
|
||||||
<!-- Subtitle file -->
|
<rect x="8" y="4" width="24" height="32" rx="3" fill="url(#sd)" opacity="0.15"/>
|
||||||
<rect x="4" y="3" width="26" height="34" rx="3.5" fill="url(#sd-main)" opacity="0.12"/>
|
<rect x="8" y="4" width="24" height="32" rx="3" stroke="url(#sd)" stroke-width="1.5" fill="none"/>
|
||||||
<rect x="4" y="3" width="26" height="34" rx="3.5" stroke="url(#sd-main)" stroke-width="1.4" fill="none"/>
|
<rect x="13" y="12" width="14" height="2.5" rx="1.25" fill="#22d3ee" opacity="0.5"/>
|
||||||
<!-- SRT-style timing line -->
|
<rect x="13" y="18" width="10" height="2.5" rx="1.25" fill="#22d3ee" opacity="0.35"/>
|
||||||
<rect x="8.5" y="10" width="10" height="2" rx="1" fill="#22d3ee" opacity="0.35"/>
|
<rect x="13" y="24" width="12" height="2.5" rx="1.25" fill="#22d3ee" opacity="0.35"/>
|
||||||
<rect x="20" y="10" width="3" height="2" rx="1" fill="#22d3ee" opacity="0.25"/>
|
<line x1="38" y1="16" x2="38" y2="32" stroke="url(#sd)" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
<!-- Subtitle text lines -->
|
<path d="M33 28l5 5 5-5" stroke="url(#sd)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
<rect x="8.5" y="15" width="17" height="2.5" rx="1.25" fill="#22d3ee" opacity="0.6"/>
|
<line x1="33" y1="40" x2="43" y2="40" stroke="url(#sd)" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
|
||||||
<rect x="8.5" y="20" width="12" height="2.5" rx="1.25" fill="#22d3ee" opacity="0.4"/>
|
|
||||||
<!-- Divider -->
|
|
||||||
<line x1="8.5" y1="25.5" x2="26" y2="25.5" stroke="#22d3ee" stroke-width="0.6" opacity="0.2"/>
|
|
||||||
<!-- Second block timing -->
|
|
||||||
<rect x="8.5" y="28" width="10" height="2" rx="1" fill="#22d3ee" opacity="0.35"/>
|
|
||||||
<!-- Second block text -->
|
|
||||||
<rect x="8.5" y="32.5" width="14" height="2.5" rx="1.25" fill="#22d3ee" opacity="0.4"/>
|
|
||||||
<!-- Download arrow -->
|
|
||||||
<line x1="38" y1="6" x2="38" y2="20" stroke="url(#sd-main)" stroke-width="2.5" stroke-linecap="round"/>
|
|
||||||
<path d="M33 16.5l5 5.5 5-5.5" stroke="url(#sd-main)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
||||||
<!-- Sync arrows (circular) -->
|
|
||||||
<path d="M35 35a6 6 0 0 1 8.5-1.5" stroke="url(#sd-sync)" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
|
||||||
<path d="M44.5 35.5l-1-2.8-2.8 1" stroke="url(#sd-sync)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
||||||
<path d="M43.5 41a6 6 0 0 1-8.5 1.5" stroke="url(#sd-sync)" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
|
||||||
<path d="M34 40.5l1 2.8 2.8-1" stroke="url(#sd-sync)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,46 +1,19 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="th-main" x1="2" y1="6" x2="22" y2="42" gradientUnits="userSpaceOnUse">
|
<linearGradient id="th" x1="4" y1="6" x2="44" y2="42" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#f97316"/>
|
<stop stop-color="#f97316"/>
|
||||||
<stop offset="1" stop-color="#c2410c"/>
|
<stop offset="1" stop-color="#c2410c"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="th-browser" x1="28" y1="6" x2="46" y2="42" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#fb923c"/>
|
|
||||||
<stop offset="1" stop-color="#ea580c"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
</defs>
|
||||||
<!-- Source panel (subtitle/text source) -->
|
<rect x="4" y="6" width="30" height="36" rx="4" fill="url(#th)" opacity="0.12"/>
|
||||||
<rect x="2" y="8" width="18" height="32" rx="3" fill="url(#th-main)" opacity="0.12"/>
|
<rect x="4" y="6" width="30" height="36" rx="4" stroke="url(#th)" stroke-width="1.5" fill="none"/>
|
||||||
<rect x="2" y="8" width="18" height="32" rx="3" stroke="url(#th-main)" stroke-width="1.3" fill="none"/>
|
<rect x="9" y="14" width="14" height="2.5" rx="1.25" fill="#f97316" opacity="0.6"/>
|
||||||
<!-- Subtitle text lines streaming out -->
|
<rect x="9" y="20" width="18" height="2.5" rx="1.25" fill="#f97316" opacity="0.4"/>
|
||||||
<rect x="5" y="14" width="12" height="2" rx="1" fill="#f97316" opacity="0.6"/>
|
<rect x="9" y="26" width="12" height="2.5" rx="1.25" fill="#f97316" opacity="0.4"/>
|
||||||
<rect x="5" y="19" width="10" height="2" rx="1" fill="#f97316" opacity="0.5"/>
|
<rect x="9" y="32" width="16" height="2.5" rx="1.25" fill="#f97316" opacity="0.4"/>
|
||||||
<rect x="5" y="24" width="11" height="2" rx="1" fill="#f97316" opacity="0.4"/>
|
<circle cx="40" cy="18" r="3.5" fill="url(#th)" opacity="0.8"/>
|
||||||
<rect x="5" y="29" width="9" height="2" rx="1" fill="#f97316" opacity="0.35"/>
|
<circle cx="40" cy="30" r="3.5" fill="url(#th)" opacity="0.8"/>
|
||||||
<rect x="5" y="34" width="12" height="2" rx="1" fill="#f97316" opacity="0.3"/>
|
<line x1="36" y1="18" x2="34" y2="18" stroke="url(#th)" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
|
||||||
<!-- WebSocket stream particles -->
|
<line x1="36" y1="30" x2="34" y2="30" stroke="url(#th)" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
|
||||||
<circle cx="23" cy="18" r="1.2" fill="#fb923c" opacity="0.7"/>
|
<line x1="40" y1="21.5" x2="40" y2="26.5" stroke="url(#th)" stroke-width="1.5" stroke-linecap="round" opacity="0.5"/>
|
||||||
<circle cx="25" cy="24" r="1" fill="#fb923c" opacity="0.5"/>
|
|
||||||
<circle cx="23.5" cy="30" r="1.1" fill="#fb923c" opacity="0.4"/>
|
|
||||||
<!-- Connection line (wavy/flowing) -->
|
|
||||||
<path d="M20 15c2-1 4 2 6 1s3-3 5-2" stroke="#fb923c" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.3"/>
|
|
||||||
<path d="M20 24c2.5 0 3 2 5 1.5s3-2.5 5.5-1.5" stroke="#fb923c" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.25"/>
|
|
||||||
<path d="M20 33c2-1 3.5 1.5 5.5 0.5s3-2 5-1" stroke="#fb923c" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
|
|
||||||
<!-- Browser window (destination) -->
|
|
||||||
<rect x="28" y="8" width="18" height="32" rx="3" fill="url(#th-browser)" opacity="0.12"/>
|
|
||||||
<rect x="28" y="8" width="18" height="32" rx="3" stroke="url(#th-browser)" stroke-width="1.3" fill="none"/>
|
|
||||||
<!-- Browser chrome dots -->
|
|
||||||
<circle cx="32" cy="12" r="1.2" fill="#f97316" opacity="0.45"/>
|
|
||||||
<circle cx="35.5" cy="12" r="1.2" fill="#f97316" opacity="0.35"/>
|
|
||||||
<circle cx="39" cy="12" r="1.2" fill="#f97316" opacity="0.25"/>
|
|
||||||
<!-- Browser address bar -->
|
|
||||||
<rect x="31" y="15.5" width="12" height="2.5" rx="1.25" fill="#f97316" opacity="0.15"/>
|
|
||||||
<!-- Received text lines in browser -->
|
|
||||||
<rect x="31" y="21" width="11" height="2" rx="1" fill="#fb923c" opacity="0.55"/>
|
|
||||||
<rect x="31" y="25.5" width="9" height="2" rx="1" fill="#fb923c" opacity="0.45"/>
|
|
||||||
<rect x="31" y="30" width="10" height="2" rx="1" fill="#fb923c" opacity="0.35"/>
|
|
||||||
<rect x="31" y="34.5" width="8" height="2" rx="1" fill="#fb923c" opacity="0.25"/>
|
|
||||||
<!-- WS label -->
|
|
||||||
<rect x="21" y="5" width="8" height="5.5" rx="2.5" fill="#c2410c"/>
|
|
||||||
<text x="25" y="9.2" text-anchor="middle" font-size="3.2" font-weight="800" fill="white" font-family="sans-serif">WS</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,34 +1,16 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="tk-bar" x1="0" y1="40" x2="0" y2="10" gradientUnits="userSpaceOnUse">
|
<linearGradient id="tk" x1="0" y1="14" x2="48" y2="34" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#0891b2"/>
|
<stop stop-color="#22d3ee"/>
|
||||||
<stop offset="1" stop-color="#22d3ee"/>
|
<stop offset="1" stop-color="#0891b2"/>
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="tk-glow" x1="4" y1="40" x2="44" y2="10" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#22d3ee" stop-opacity="0.25"/>
|
|
||||||
<stop offset="1" stop-color="#06b6d4" stop-opacity="0"/>
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<!-- Subtle grid lines -->
|
<rect x="2" y="12" width="12" height="24" rx="3.5" fill="url(#tk)"/>
|
||||||
<line x1="4" y1="14" x2="44" y2="14" stroke="#22d3ee" stroke-width="0.5" opacity="0.12"/>
|
<rect x="18" y="12" width="12" height="24" rx="3.5" fill="url(#tk)"/>
|
||||||
<line x1="4" y1="22" x2="44" y2="22" stroke="#22d3ee" stroke-width="0.5" opacity="0.12"/>
|
<rect x="34" y="12" width="12" height="24" rx="3.5" fill="url(#tk)"/>
|
||||||
<line x1="4" y1="30" x2="44" y2="30" stroke="#22d3ee" stroke-width="0.5" opacity="0.12"/>
|
<line x1="15.5" y1="10" x2="15.5" y2="38" stroke="#22d3ee" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 3" opacity="0.45"/>
|
||||||
<!-- Base line -->
|
<line x1="32.5" y1="10" x2="32.5" y2="38" stroke="#22d3ee" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 3" opacity="0.45"/>
|
||||||
<line x1="4" y1="40" x2="44" y2="40" stroke="#0891b2" stroke-width="1" opacity="0.3"/>
|
<rect x="5" y="22" width="6" height="2.5" rx="1.25" fill="white" opacity="0.7"/>
|
||||||
<!-- Activity bars (daily rollups) -->
|
<rect x="21" y="22" width="6" height="2.5" rx="1.25" fill="white" opacity="0.7"/>
|
||||||
<rect x="5" y="30" width="4" height="10" rx="1.5" fill="url(#tk-bar)" opacity="0.4"/>
|
<rect x="37" y="22" width="6" height="2.5" rx="1.25" fill="white" opacity="0.7"/>
|
||||||
<rect x="11" y="24" width="4" height="16" rx="1.5" fill="url(#tk-bar)" opacity="0.55"/>
|
|
||||||
<rect x="17" y="28" width="4" height="12" rx="1.5" fill="url(#tk-bar)" opacity="0.5"/>
|
|
||||||
<rect x="23" y="18" width="4" height="22" rx="1.5" fill="url(#tk-bar)" opacity="0.7"/>
|
|
||||||
<rect x="29" y="22" width="4" height="18" rx="1.5" fill="url(#tk-bar)" opacity="0.6"/>
|
|
||||||
<rect x="35" y="14" width="4" height="26" rx="1.5" fill="url(#tk-bar)"/>
|
|
||||||
<rect x="41" y="20" width="4" height="20" rx="1.5" fill="url(#tk-bar)" opacity="0.65"/>
|
|
||||||
<!-- Trend line -->
|
|
||||||
<polyline points="7,28 13,22 19,25.5 25,16 31,20 37,12 43,18" stroke="#67e8f9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="0.7"/>
|
|
||||||
<!-- Trend dot on peak -->
|
|
||||||
<circle cx="37" cy="12" r="2.2" fill="#22d3ee" opacity="0.6"/>
|
|
||||||
<circle cx="37" cy="12" r="1" fill="white" opacity="0.9"/>
|
|
||||||
<!-- Mini counter badge -->
|
|
||||||
<rect x="33" y="4" width="12" height="7" rx="3.5" fill="#0891b2"/>
|
|
||||||
<text x="39" y="9" text-anchor="middle" font-size="4" font-weight="700" fill="white" font-family="sans-serif">42d</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -5,18 +5,26 @@
|
|||||||
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
|
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Overlay Auto-Start
|
// Overlay Auto-Start
|
||||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Visible Overlay Subtitle Binding
|
||||||
|
// Control whether visible overlay toggles also toggle MPV subtitle visibility.
|
||||||
|
// When enabled, visible overlay hides MPV subtitles; when disabled, MPV subtitles are left unchanged.
|
||||||
|
// ==========================================
|
||||||
|
"bind_visible_overlay_to_mpv_sub_visibility": true, // Link visible overlay toggles to MPV subtitle visibility (primary and secondary). Values: true | false
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Texthooker Server
|
// Texthooker Server
|
||||||
// Control whether browser opens automatically for texthooker.
|
// Control whether browser opens automatically for texthooker.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"texthooker": {
|
"texthooker": {
|
||||||
"openBrowser": true, // Open browser setting. Values: true | false
|
"openBrowser": true // Open browser setting. Values: true | false
|
||||||
}, // Control whether browser opens automatically for texthooker.
|
}, // Control whether browser opens automatically for texthooker.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -26,7 +34,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"websocket": {
|
"websocket": {
|
||||||
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
||||||
"port": 6677, // Built-in subtitle websocket server port.
|
"port": 6677 // Built-in subtitle websocket server port.
|
||||||
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -35,7 +43,7 @@
|
|||||||
// Set to debug for full runtime diagnostics.
|
// Set to debug for full runtime diagnostics.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"logging": {
|
"logging": {
|
||||||
"level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error
|
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||||
}, // Controls logging verbosity.
|
}, // Controls logging verbosity.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -45,6 +53,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
||||||
|
"toggleInvisibleOverlayGlobal": "Alt+Shift+I", // Toggle invisible overlay global setting.
|
||||||
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
||||||
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
||||||
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
||||||
@@ -56,9 +65,19 @@
|
|||||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
||||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
||||||
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Invisible Overlay
|
||||||
|
// Startup behavior for the invisible interactive subtitle mining layer.
|
||||||
|
// Invisible subtitle position edit mode: Ctrl/Cmd+Shift+P to toggle, arrow keys to move, Enter or Ctrl/Cmd+S to save, Esc to cancel.
|
||||||
|
// This edit-mode shortcut is fixed and is not currently configurable.
|
||||||
|
// ==========================================
|
||||||
|
"invisibleOverlay": {
|
||||||
|
"startupVisibility": "platform-default" // Startup visibility setting.
|
||||||
|
}, // Startup behavior for the invisible interactive subtitle mining layer.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keybindings (MPV Commands)
|
// Keybindings (MPV Commands)
|
||||||
// Extra keybindings that are merged with built-in defaults.
|
// Extra keybindings that are merged with built-in defaults.
|
||||||
@@ -76,7 +95,7 @@
|
|||||||
"secondarySub": {
|
"secondarySub": {
|
||||||
"secondarySubLanguages": [], // Secondary sub languages setting.
|
"secondarySubLanguages": [], // Secondary sub languages setting.
|
||||||
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
||||||
"defaultMode": "hover", // Default mode setting.
|
"defaultMode": "hover" // Default mode setting.
|
||||||
}, // Dual subtitle track options.
|
}, // Dual subtitle track options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -87,7 +106,7 @@
|
|||||||
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
||||||
"alass_path": "", // Alass path setting.
|
"alass_path": "", // Alass path setting.
|
||||||
"ffsubsync_path": "", // Ffsubsync path setting.
|
"ffsubsync_path": "", // Ffsubsync path setting.
|
||||||
"ffmpeg_path": "", // Ffmpeg path setting.
|
"ffmpeg_path": "" // Ffmpeg path setting.
|
||||||
}, // Subsync engine and executable paths.
|
}, // Subsync engine and executable paths.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -95,7 +114,7 @@
|
|||||||
// Initial vertical subtitle position from the bottom.
|
// Initial vertical subtitle position from the bottom.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitlePosition": {
|
"subtitlePosition": {
|
||||||
"yPercent": 10, // Y percent setting.
|
"yPercent": 10 // Y percent setting.
|
||||||
}, // Initial vertical subtitle position from the bottom.
|
}, // Initial vertical subtitle position from the bottom.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -106,22 +125,13 @@
|
|||||||
"subtitleStyle": {
|
"subtitleStyle": {
|
||||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
"hoverTokenColor": "#c6a0f6", // Hex color used for hovered subtitle token highlight in mpv.
|
||||||
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
"fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif", // Font family setting.
|
||||||
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
|
||||||
"fontFamily": "M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
|
||||||
"fontSize": 35, // Font size setting.
|
"fontSize": 35, // Font size setting.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
"fontColor": "#cad3f5", // Font color setting.
|
||||||
"fontWeight": "600", // Font weight setting.
|
"fontWeight": "normal", // Font weight setting.
|
||||||
"lineHeight": 1.35, // Line height setting.
|
|
||||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
|
||||||
"wordSpacing": 0, // Word spacing setting.
|
|
||||||
"fontKerning": "normal", // Font kerning setting.
|
|
||||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
|
||||||
"textShadow": "0 3px 10px rgba(0,0,0,0.69)", // Text shadow setting.
|
|
||||||
"fontStyle": "normal", // Font style setting.
|
"fontStyle": "normal", // Font style setting.
|
||||||
"backgroundColor": "rgb(30, 32, 48, 0.88)", // Background color setting.
|
"backgroundColor": "rgb(30, 32, 48, 0.88)", // Background color setting.
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
|
||||||
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
|
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
|
||||||
"knownWordColor": "#a6da95", // Known word color setting.
|
"knownWordColor": "#a6da95", // Known word color setting.
|
||||||
"jlptColors": {
|
"jlptColors": {
|
||||||
@@ -129,32 +139,30 @@
|
|||||||
"N2": "#f5a97f", // N2 setting.
|
"N2": "#f5a97f", // N2 setting.
|
||||||
"N3": "#f9e2af", // N3 setting.
|
"N3": "#f9e2af", // N3 setting.
|
||||||
"N4": "#a6e3a1", // N4 setting.
|
"N4": "#a6e3a1", // N4 setting.
|
||||||
"N5": "#8aadf4", // N5 setting.
|
"N5": "#8aadf4" // N5 setting.
|
||||||
}, // Jlpt colors setting.
|
}, // Jlpt colors setting.
|
||||||
"frequencyDictionary": {
|
"frequencyDictionary": {
|
||||||
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
||||||
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, SubMiner searches installed/default frequency-dictionary locations.
|
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
|
||||||
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
|
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
|
||||||
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
||||||
"matchMode": "headword", // Frequency lookup text selection mode. Values: headword | surface
|
|
||||||
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
||||||
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
"bandedColors": [
|
||||||
|
"#ed8796",
|
||||||
|
"#f5a97f",
|
||||||
|
"#f9e2af",
|
||||||
|
"#a6e3a1",
|
||||||
|
"#8aadf4"
|
||||||
|
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||||
}, // Frequency dictionary setting.
|
}, // Frequency dictionary setting.
|
||||||
"secondary": {
|
"secondary": {
|
||||||
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
|
|
||||||
"fontSize": 24, // Font size setting.
|
"fontSize": 24, // Font size setting.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
"fontColor": "#ffffff", // Font color setting.
|
||||||
"lineHeight": 1.35, // Line height setting.
|
|
||||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
|
||||||
"wordSpacing": 0, // Word spacing setting.
|
|
||||||
"fontKerning": "normal", // Font kerning setting.
|
|
||||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
|
||||||
"textShadow": "0 3px 10px rgba(0,0,0,0.69)", // Text shadow setting.
|
|
||||||
"backgroundColor": "transparent", // Background color setting.
|
"backgroundColor": "transparent", // Background color setting.
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
|
||||||
"fontWeight": "normal", // Font weight setting.
|
"fontWeight": "normal", // Font weight setting.
|
||||||
"fontStyle": "normal", // Font style setting.
|
"fontStyle": "normal", // Font style setting.
|
||||||
}, // Secondary setting.
|
"fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif" // Font family setting.
|
||||||
|
} // Secondary setting.
|
||||||
}, // Primary and secondary subtitle styling.
|
}, // Primary and secondary subtitle styling.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -167,19 +175,15 @@
|
|||||||
"enabled": false, // Enable AnkiConnect integration. Values: true | false
|
"enabled": false, // Enable AnkiConnect integration. Values: true | false
|
||||||
"url": "http://127.0.0.1:8765", // Url setting.
|
"url": "http://127.0.0.1:8765", // Url setting.
|
||||||
"pollingRate": 3000, // Polling interval in milliseconds.
|
"pollingRate": 3000, // Polling interval in milliseconds.
|
||||||
"proxy": {
|
"tags": [
|
||||||
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
"SubMiner"
|
||||||
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||||
"port": 8766, // Bind port for local AnkiConnect proxy.
|
|
||||||
"upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
|
||||||
}, // Proxy setting.
|
|
||||||
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
|
||||||
"fields": {
|
"fields": {
|
||||||
"audio": "ExpressionAudio", // Audio setting.
|
"audio": "ExpressionAudio", // Audio setting.
|
||||||
"image": "Picture", // Image setting.
|
"image": "Picture", // Image setting.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"sentence": "Sentence", // Sentence setting.
|
||||||
"miscInfo": "MiscInfo", // Misc info setting.
|
"miscInfo": "MiscInfo", // Misc info setting.
|
||||||
"translation": "SelectionText", // Translation setting.
|
"translation": "SelectionText" // Translation setting.
|
||||||
}, // Fields setting.
|
}, // Fields setting.
|
||||||
"ai": {
|
"ai": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
@@ -188,7 +192,7 @@
|
|||||||
"model": "openai/gpt-4o-mini", // Model setting.
|
"model": "openai/gpt-4o-mini", // Model setting.
|
||||||
"baseUrl": "https://openrouter.ai/api", // Base url setting.
|
"baseUrl": "https://openrouter.ai/api", // Base url setting.
|
||||||
"targetLanguage": "English", // Target language setting.
|
"targetLanguage": "English", // Target language setting.
|
||||||
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations." // System prompt setting.
|
||||||
}, // Ai setting.
|
}, // Ai setting.
|
||||||
"media": {
|
"media": {
|
||||||
"generateAudio": true, // Generate audio setting. Values: true | false
|
"generateAudio": true, // Generate audio setting. Values: true | false
|
||||||
@@ -201,7 +205,7 @@
|
|||||||
"animatedCrf": 35, // Animated crf setting.
|
"animatedCrf": 35, // Animated crf setting.
|
||||||
"audioPadding": 0.5, // Audio padding setting.
|
"audioPadding": 0.5, // Audio padding setting.
|
||||||
"fallbackDuration": 3, // Fallback duration setting.
|
"fallbackDuration": 3, // Fallback duration setting.
|
||||||
"maxMediaDuration": 30, // Max media duration setting.
|
"maxMediaDuration": 30 // Max media duration setting.
|
||||||
}, // Media setting.
|
}, // Media setting.
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
||||||
@@ -209,7 +213,7 @@
|
|||||||
"mediaInsertMode": "append", // Media insert mode setting.
|
"mediaInsertMode": "append", // Media insert mode setting.
|
||||||
"highlightWord": true, // Highlight word setting. Values: true | false
|
"highlightWord": true, // Highlight word setting. Values: true | false
|
||||||
"notificationType": "osd", // Notification type setting.
|
"notificationType": "osd", // Notification type setting.
|
||||||
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false
|
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
||||||
}, // Behavior setting.
|
}, // Behavior setting.
|
||||||
"nPlusOne": {
|
"nPlusOne": {
|
||||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||||
@@ -218,20 +222,20 @@
|
|||||||
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
||||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||||
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
||||||
"knownWord": "#a6da95", // Color used for legacy known-word highlights.
|
"knownWord": "#a6da95" // Color used for legacy known-word highlights.
|
||||||
}, // N plus one setting.
|
}, // N plus one setting.
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"pattern": "[SubMiner] %f (%t)", // Pattern setting.
|
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
|
||||||
}, // Metadata setting.
|
}, // Metadata setting.
|
||||||
"isLapis": {
|
"isLapis": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
"sentenceCardModel": "Japanese sentences", // Sentence card model setting.
|
"sentenceCardModel": "Japanese sentences" // Sentence card model setting.
|
||||||
}, // Is lapis setting.
|
}, // Is lapis setting.
|
||||||
"isKiku": {
|
"isKiku": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
||||||
"deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false
|
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
|
||||||
}, // Is kiku setting.
|
} // Is kiku setting.
|
||||||
}, // Automatic Anki updates and media generation options.
|
}, // Automatic Anki updates and media generation options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -241,7 +245,7 @@
|
|||||||
"jimaku": {
|
"jimaku": {
|
||||||
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
||||||
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
||||||
"maxEntryResults": 10, // Maximum Jimaku search results returned.
|
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
||||||
}, // Jimaku API configuration and defaults.
|
}, // Jimaku API configuration and defaults.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -252,7 +256,10 @@
|
|||||||
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
|
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
|
||||||
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
|
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
|
||||||
"whisperModel": "", // Path to whisper model used for fallback transcription.
|
"whisperModel": "", // Path to whisper model used for fallback transcription.
|
||||||
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher.
|
"primarySubLanguages": [
|
||||||
|
"ja",
|
||||||
|
"jpn"
|
||||||
|
] // Comma-separated primary subtitle language priority used by the launcher.
|
||||||
}, // Defaults for subminer YouTube subtitle extraction/transcription mode.
|
}, // Defaults for subminer YouTube subtitle extraction/transcription mode.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -261,7 +268,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"anilist": {
|
"anilist": {
|
||||||
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
||||||
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
"accessToken": "" // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
||||||
}, // Anilist API credentials and update behavior.
|
}, // Anilist API credentials and update behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -285,8 +292,16 @@
|
|||||||
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
||||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
||||||
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
||||||
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions.
|
"directPlayContainers": [
|
||||||
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
|
"mkv",
|
||||||
|
"mp4",
|
||||||
|
"webm",
|
||||||
|
"mov",
|
||||||
|
"flac",
|
||||||
|
"mp3",
|
||||||
|
"aac"
|
||||||
|
], // Container allowlist for direct play decisions.
|
||||||
|
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
|
||||||
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -297,7 +312,7 @@
|
|||||||
"discordPresence": {
|
"discordPresence": {
|
||||||
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||||
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates.
|
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||||
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -319,7 +334,7 @@
|
|||||||
"telemetryDays": 30, // Telemetry retention window in days.
|
"telemetryDays": 30, // Telemetry retention window in days.
|
||||||
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
||||||
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
||||||
"vacuumIntervalDays": 7, // Minimum days between VACUUM runs.
|
"vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
|
||||||
}, // Retention setting.
|
} // Retention setting.
|
||||||
}, // Enable/disable immersion tracking.
|
} // Enable/disable immersion tracking.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ All shortcuts are configurable in `config.jsonc` under `shortcuts` and `keybindi
|
|||||||
|
|
||||||
These work system-wide regardless of which window has focus.
|
These work system-wide regardless of which window has focus.
|
||||||
|
|
||||||
| Shortcut | Action | Configurable |
|
| Shortcut | Action | Configurable |
|
||||||
| ------------- | ---------------------- | -------------------------------------- |
|
| ------------- | ------------------------ | ---------------------------------------- |
|
||||||
| `Alt+Shift+O` | Toggle visible overlay | `shortcuts.toggleVisibleOverlayGlobal` |
|
| `Alt+Shift+O` | Toggle visible overlay | `shortcuts.toggleVisibleOverlayGlobal` |
|
||||||
| `Alt+Shift+Y` | Open Yomitan settings | Fixed (not configurable) |
|
| `Alt+Shift+I` | Toggle invisible overlay | `shortcuts.toggleInvisibleOverlayGlobal` |
|
||||||
|
| `Alt+Shift+Y` | Open Yomitan settings | Fixed (not configurable) |
|
||||||
|
|
||||||
::: tip
|
::: tip
|
||||||
Global shortcuts are registered with the OS. If they conflict with another application, update them in `shortcuts` config and restart SubMiner.
|
Global shortcuts are registered with the OS. If they conflict with another application, update them in `shortcuts` config and restart SubMiner.
|
||||||
@@ -38,8 +39,6 @@ These control playback and subtitle display. They require overlay window focus.
|
|||||||
| Shortcut | Action |
|
| Shortcut | Action |
|
||||||
| -------------------- | -------------------------------------------------- |
|
| -------------------- | -------------------------------------------------- |
|
||||||
| `Space` | Toggle mpv pause |
|
| `Space` | Toggle mpv pause |
|
||||||
| `J` | Cycle primary subtitle track |
|
|
||||||
| `Shift+J` | Cycle secondary subtitle track |
|
|
||||||
| `ArrowRight` | Seek forward 5 seconds |
|
| `ArrowRight` | Seek forward 5 seconds |
|
||||||
| `ArrowLeft` | Seek backward 5 seconds |
|
| `ArrowLeft` | Seek backward 5 seconds |
|
||||||
| `ArrowUp` | Seek forward 60 seconds |
|
| `ArrowUp` | Seek forward 60 seconds |
|
||||||
@@ -56,8 +55,6 @@ These control playback and subtitle display. They require overlay window focus.
|
|||||||
|
|
||||||
These keybindings can be overridden or disabled via the `keybindings` config array.
|
These keybindings can be overridden or disabled via the `keybindings` config array.
|
||||||
|
|
||||||
Mouse-hover playback behavior is configured separately from shortcuts: `subtitleStyle.autoPauseVideoOnHover` defaults to `true` (pause on subtitle hover, resume on leave).
|
|
||||||
|
|
||||||
## Subtitle & Feature Shortcuts
|
## Subtitle & Feature Shortcuts
|
||||||
|
|
||||||
| Shortcut | Action | Config key |
|
| Shortcut | Action | Config key |
|
||||||
@@ -67,19 +64,34 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle
|
|||||||
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
||||||
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
||||||
|
|
||||||
|
## Invisible Subtitle Position Edit Mode
|
||||||
|
|
||||||
|
Enter edit mode to fine-tune invisible overlay alignment with mpv's native subtitles.
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| --------------------- | -------------------------------- |
|
||||||
|
| `Ctrl/Cmd+Shift+P` | Toggle position edit mode |
|
||||||
|
| `ArrowKeys` or `hjkl` | Nudge position by 1 px |
|
||||||
|
| `Shift+Arrow` | Nudge position by 4 px |
|
||||||
|
| `Enter` or `Ctrl+S` | Save position and exit edit mode |
|
||||||
|
| `Esc` | Cancel and discard changes |
|
||||||
|
|
||||||
## MPV Plugin Chords
|
## MPV Plugin Chords
|
||||||
|
|
||||||
When the mpv plugin is installed, all commands use a `y` chord prefix — press `y`, then the second key within 1 second.
|
When the mpv plugin is installed, all commands use a `y` chord prefix — press `y`, then the second key within 1 second.
|
||||||
|
|
||||||
| Chord | Action |
|
| Chord | Action |
|
||||||
| ----- | ------------------------ |
|
| ----- | --------------------------------------- |
|
||||||
| `y-y` | Open SubMiner menu (OSD) |
|
| `y-y` | Open SubMiner menu (OSD) |
|
||||||
| `y-s` | Start overlay |
|
| `y-s` | Start overlay |
|
||||||
| `y-S` | Stop overlay |
|
| `y-S` | Stop overlay |
|
||||||
| `y-t` | Toggle visible overlay |
|
| `y-t` | Toggle visible overlay |
|
||||||
| `y-o` | Open Yomitan settings |
|
| `y-i` | Toggle invisible overlay |
|
||||||
| `y-r` | Restart overlay |
|
| `y-I` | Show invisible overlay |
|
||||||
| `y-c` | Check overlay status |
|
| `y-u` | Hide invisible overlay |
|
||||||
|
| `y-o` | Open Yomitan settings |
|
||||||
|
| `y-r` | Restart overlay |
|
||||||
|
| `y-c` | Check overlay status |
|
||||||
|
|
||||||
When the overlay has focus, press `y` then `d` to toggle DevTools (debugging helper).
|
When the overlay has focus, press `y` then `d` to toggle DevTools (debugging helper).
|
||||||
|
|
||||||
@@ -100,6 +112,7 @@ All `shortcuts.*` keys accept [Electron accelerator strings](https://www.electro
|
|||||||
"mineSentence": "CommandOrControl+S",
|
"mineSentence": "CommandOrControl+S",
|
||||||
"copySubtitle": "CommandOrControl+C",
|
"copySubtitle": "CommandOrControl+C",
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O",
|
||||||
|
"toggleInvisibleOverlayGlobal": "Alt+Shift+I",
|
||||||
"openJimaku": null, // disabled
|
"openJimaku": null, // disabled
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ SubMiner retries the connection automatically with increasing delays (200 ms, 50
|
|||||||
- first subtitle parse/tokenization bursts
|
- first subtitle parse/tokenization bursts
|
||||||
- media generation (`ffmpeg` audio/image and AVIF paths)
|
- media generation (`ffmpeg` audio/image and AVIF paths)
|
||||||
- media sync and subtitle tooling (`alass`, `ffsubsync`, `whisper` fallback path)
|
- media sync and subtitle tooling (`alass`, `ffsubsync`, `whisper` fallback path)
|
||||||
- `ankiConnect` enrichment (plus polling overhead when proxy mode is disabled)
|
- `ankiConnect` enrichment and frequent polling
|
||||||
|
|
||||||
### If playback feels sluggish
|
### If playback feels sluggish
|
||||||
|
|
||||||
@@ -104,17 +104,11 @@ Logged when a malformed JSON line arrives from the mpv socket. Usually harmless
|
|||||||
|
|
||||||
**"AnkiConnect: unable to connect"**
|
**"AnkiConnect: unable to connect"**
|
||||||
|
|
||||||
SubMiner connects to the active Anki endpoint:
|
SubMiner polls AnkiConnect at `http://127.0.0.1:8765` (configurable via `ankiConnect.url`). This error means Anki is not running or the AnkiConnect add-on is not installed.
|
||||||
|
|
||||||
- `ankiConnect.url` (direct mode, default `http://127.0.0.1:8765`)
|
|
||||||
- `http://<ankiConnect.proxy.host>:<ankiConnect.proxy.port>` (proxy mode)
|
|
||||||
|
|
||||||
This error means the active endpoint is unavailable, or (in proxy mode) the proxy cannot reach `ankiConnect.proxy.upstreamUrl`.
|
|
||||||
|
|
||||||
- Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on in Anki.
|
- Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on in Anki.
|
||||||
- Make sure Anki is running before you start mining.
|
- Make sure Anki is running before you start mining.
|
||||||
- If you changed the AnkiConnect port, update `ankiConnect.url` (or `ankiConnect.proxy.upstreamUrl` if using proxy mode).
|
- If you changed the AnkiConnect port, update `ankiConnect.url` in your config.
|
||||||
- If using external Yomitan/browser clients, confirm they point to your SubMiner proxy URL.
|
|
||||||
|
|
||||||
SubMiner retries with exponential backoff (up to 5 s) and suppresses repeated error logs after 5 consecutive failures. When Anki comes back, you will see "AnkiConnect connection restored".
|
SubMiner retries with exponential backoff (up to 5 s) and suppresses repeated error logs after 5 consecutive failures. When Anki comes back, you will see "AnkiConnect connection restored".
|
||||||
|
|
||||||
@@ -128,7 +122,7 @@ See [Anki Integration](/anki-integration) for the full field mapping reference.
|
|||||||
|
|
||||||
Shown when SubMiner tries to update a card that no longer exists, or when AnkiConnect rejects the update. Common causes:
|
Shown when SubMiner tries to update a card that no longer exists, or when AnkiConnect rejects the update. Common causes:
|
||||||
|
|
||||||
- The card was deleted in Anki between creation and enrichment update.
|
- The card was deleted in Anki between polling and update.
|
||||||
- The note type changed and a mapped field no longer exists.
|
- The note type changed and a mapped field no longer exists.
|
||||||
|
|
||||||
## Overlay
|
## Overlay
|
||||||
@@ -159,7 +153,7 @@ SubMiner positions the overlay by tracking the mpv window. If tracking fails:
|
|||||||
- Sway: Ensure `swaymsg` is available.
|
- Sway: Ensure `swaymsg` is available.
|
||||||
- X11: Ensure `xdotool` and `xwininfo` are installed.
|
- X11: Ensure `xdotool` and `xwininfo` are installed.
|
||||||
|
|
||||||
If the overlay position is slightly off, right-click and drag on subtitle text to fine-tune the overlay subtitle offset.
|
If the overlay position is slightly off, use invisible subtitle position edit mode (`Ctrl/Cmd+Shift+P`) to fine-tune the offset with arrow keys, then save with `Enter` or `Ctrl+S`.
|
||||||
|
|
||||||
## Yomitan
|
## Yomitan
|
||||||
|
|
||||||
@@ -223,10 +217,10 @@ Media generation has a 30-second timeout (60 seconds for animated AVIF). If your
|
|||||||
|
|
||||||
**"Failed to register global shortcut"**
|
**"Failed to register global shortcut"**
|
||||||
|
|
||||||
Global shortcuts (`Alt+Shift+O`, `Alt+Shift+Y`) may conflict with other applications or desktop environment keybindings.
|
Global shortcuts (`Alt+Shift+O`, `Alt+Shift+I`, `Alt+Shift+Y`) may conflict with other applications or desktop environment keybindings.
|
||||||
|
|
||||||
- Check your DE/WM keybinding settings for conflicts.
|
- Check your DE/WM keybinding settings for conflicts.
|
||||||
- Change the shortcut in your config under `shortcuts.toggleVisibleOverlayGlobal`.
|
- Change the shortcuts in your config under `shortcuts.toggleVisibleOverlayGlobal`, `shortcuts.toggleInvisibleOverlayGlobal`.
|
||||||
- On Wayland, global shortcut registration has limitations depending on the compositor.
|
- On Wayland, global shortcut registration has limitations depending on the compositor.
|
||||||
|
|
||||||
**Overlay keybindings not working**
|
**Overlay keybindings not working**
|
||||||
@@ -279,5 +273,5 @@ The Jimaku API has rate limits. If you see 429 errors, wait for the retry durati
|
|||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
- **Accessibility permission**: Required for window tracking. Grant it in System Settings > Privacy & Security > Accessibility.
|
- **Accessibility permission**: Required for window tracking. Grant it in System Settings > Privacy & Security > Accessibility.
|
||||||
- **Font rendering**: macOS uses a 0.87x font compensation factor for subtitle alignment between mpv and the overlay. If text alignment looks off, adjust subtitle offset by right-click dragging subtitle text.
|
- **Font rendering**: macOS uses a 0.87x font compensation factor for subtitle alignment between mpv and the overlay. If text alignment looks off, adjust the invisible subtitle offset.
|
||||||
- **Gatekeeper**: If macOS blocks SubMiner, right-click the app and select "Open" to bypass the warning, or remove the quarantine attribute: `xattr -d com.apple.quarantine /path/to/SubMiner.app`
|
- **Gatekeeper**: If macOS blocks SubMiner, right-click the app and select "Open" to bypass the warning, or remove the quarantine attribute: `xattr -d com.apple.quarantine /path/to/SubMiner.app`
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> SubMiner requires the bundled Yomitan instance to have at least one dictionary imported for lookups to work.
|
|
||||||
> See [Yomitan setup](#yomitan-setup) for details.
|
|
||||||
|
|
||||||
There are two ways to use SubMiner — the `subminer` wrapper script or the mpv plugin:
|
There are two ways to use SubMiner — the `subminer` wrapper script or the mpv plugin:
|
||||||
|
|
||||||
| Approach | Best For |
|
| Approach | Best For |
|
||||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, and manages app commands. With default plugin settings, overlay auto-starts visible and playback resumes after annotation readiness. |
|
| **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, and manages app commands. Overlay start is explicit (`--start`, `-S`, or `y-s`). |
|
||||||
| **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control overlay visibility. Requires `--input-ipc-server=/tmp/subminer-socket`. |
|
| **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control visible and invisible overlay layers. Requires `--input-ipc-server=/tmp/subminer-socket`. |
|
||||||
|
|
||||||
You can use both together—install the plugin for on-demand control, but use `subminer` when you want the streamlined workflow.
|
You can use both together—install the plugin for on-demand control, but use `subminer` when you want the streamlined workflow.
|
||||||
|
|
||||||
`subminer` is implemented as a Bun script and runs directly via shebang (no `bun run` needed), for example: `subminer video.mkv`.
|
`subminer` is implemented as a Bun script and runs directly via shebang (no `bun run` needed), for example: `subminer --start video.mkv`.
|
||||||
|
|
||||||
## Live Config Reload
|
## Live Config Reload
|
||||||
|
|
||||||
@@ -38,9 +34,8 @@ subminer # Current directory (uses fzf)
|
|||||||
subminer -R # Use rofi instead of fzf
|
subminer -R # Use rofi instead of fzf
|
||||||
subminer -d ~/Videos # Specific directory
|
subminer -d ~/Videos # Specific directory
|
||||||
subminer -r -d ~/Anime # Recursive search
|
subminer -r -d ~/Anime # Recursive search
|
||||||
subminer video.mkv # Play specific file (default plugin config auto-starts visible overlay)
|
subminer video.mkv # Play specific file
|
||||||
subminer --start video.mkv # Optional explicit overlay start (use when plugin auto_start=no)
|
subminer --start video.mkv # Play + explicitly start overlay
|
||||||
subminer -S video.mkv # Same as above via --start-overlay
|
|
||||||
subminer https://youtu.be/... # Play a YouTube URL
|
subminer https://youtu.be/... # Play a YouTube URL
|
||||||
subminer ytsearch:"jp news" # Play first YouTube search result
|
subminer ytsearch:"jp news" # Play first YouTube search result
|
||||||
subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging
|
subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging
|
||||||
@@ -73,8 +68,11 @@ SubMiner.AppImage --start --texthooker # Start overlay with texthooker
|
|||||||
SubMiner.AppImage --texthooker # Launch texthooker only (no overlay window)
|
SubMiner.AppImage --texthooker # Launch texthooker only (no overlay window)
|
||||||
SubMiner.AppImage --stop # Stop overlay
|
SubMiner.AppImage --stop # Stop overlay
|
||||||
SubMiner.AppImage --start --toggle # Start MPV IPC + toggle visibility
|
SubMiner.AppImage --start --toggle # Start MPV IPC + toggle visibility
|
||||||
|
SubMiner.AppImage --start --toggle-invisible-overlay # Start MPV IPC + toggle invisible layer
|
||||||
SubMiner.AppImage --show-visible-overlay # Force show visible overlay
|
SubMiner.AppImage --show-visible-overlay # Force show visible overlay
|
||||||
SubMiner.AppImage --hide-visible-overlay # Force hide visible overlay
|
SubMiner.AppImage --hide-visible-overlay # Force hide visible overlay
|
||||||
|
SubMiner.AppImage --show-invisible-overlay # Force show invisible overlay
|
||||||
|
SubMiner.AppImage --hide-invisible-overlay # Force hide invisible overlay
|
||||||
SubMiner.AppImage --start --dev # Enable app/dev mode only
|
SubMiner.AppImage --start --dev # Enable app/dev mode only
|
||||||
SubMiner.AppImage --start --debug # Alias for --dev
|
SubMiner.AppImage --start --debug # Alias for --dev
|
||||||
SubMiner.AppImage --start --log-level debug # Force verbose logging without app/dev mode
|
SubMiner.AppImage --start --log-level debug # Force verbose logging without app/dev mode
|
||||||
@@ -151,14 +149,6 @@ secondary-sub-visibility=no
|
|||||||
|
|
||||||
`secondary-slang` is not an mpv option; use `slang` with `sid=auto` / `secondary-sid=auto` instead.
|
`secondary-slang` is not an mpv option; use `slang` with `sid=auto` / `secondary-sid=auto` instead.
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
For SubMiner overlay lookups to work, open Yomitan settings (`subminer app --settings` or `SubMiner.AppImage --settings`) and import at least one dictionary in the bundled Yomitan instance.
|
|
||||||
|
|
||||||
If you also use Yomitan in a browser, configure that browser profile separately; it does not inherit dictionaries or settings from the bundled instance.
|
|
||||||
|
|
||||||
### YouTube Playback
|
### YouTube Playback
|
||||||
|
|
||||||
`subminer` accepts direct URLs (for example, YouTube links) and `ytsearch:` targets, and forwards them to mpv.
|
`subminer` accepts direct URLs (for example, YouTube links) and `ytsearch:` targets, and forwards them to mpv.
|
||||||
@@ -180,10 +170,11 @@ Notes:
|
|||||||
|
|
||||||
### Global Shortcuts
|
### Global Shortcuts
|
||||||
|
|
||||||
| Keybind | Action |
|
| Keybind | Action |
|
||||||
| ------------- | ---------------------- |
|
| ------------- | ------------------------ |
|
||||||
| `Alt+Shift+O` | Toggle visible overlay |
|
| `Alt+Shift+O` | Toggle visible overlay |
|
||||||
| `Alt+Shift+Y` | Open Yomitan settings |
|
| `Alt+Shift+I` | Toggle invisible overlay |
|
||||||
|
| `Alt+Shift+Y` | Open Yomitan settings |
|
||||||
|
|
||||||
`Alt+Shift+Y` is a fixed global shortcut; it is not part of `shortcuts` config.
|
`Alt+Shift+Y` is a fixed global shortcut; it is not part of `shortcuts` config.
|
||||||
|
|
||||||
@@ -204,12 +195,14 @@ Notes:
|
|||||||
| `Ctrl+W` | Quit mpv |
|
| `Ctrl+W` | Quit mpv |
|
||||||
| `Right-click` | Toggle MPV pause (outside subtitle area) |
|
| `Right-click` | Toggle MPV pause (outside subtitle area) |
|
||||||
| `Right-click + drag` | Move subtitle position (on subtitle) |
|
| `Right-click + drag` | Move subtitle position (on subtitle) |
|
||||||
|
| `Ctrl/Cmd+Shift+P` | Toggle invisible subtitle position edit mode |
|
||||||
|
| `Arrow keys` | Move invisible subtitles while edit mode is active |
|
||||||
|
| `Enter` / `Ctrl+S` | Save invisible subtitle position in edit mode |
|
||||||
|
| `Esc` | Cancel invisible subtitle position edit mode |
|
||||||
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist |
|
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist |
|
||||||
|
|
||||||
These keybindings only work when the overlay window has focus. See [Configuration](/configuration) for customization.
|
These keybindings only work when the overlay window has focus. See [Configuration](/configuration) for customization.
|
||||||
|
|
||||||
By default, hovering over subtitle text pauses mpv playback and leaving the subtitle area resumes playback. Set `subtitleStyle.autoPauseVideoOnHover` to `false` to disable this behavior.
|
|
||||||
|
|
||||||
### Drag-and-drop Queueing
|
### Drag-and-drop Queueing
|
||||||
|
|
||||||
- Drag and drop one or more video files onto the overlay to replace current playback (`loadfile ... replace` for first file, then append remainder).
|
- Drag and drop one or more video files onto the overlay to replace current playback (`loadfile ... replace` for first file, then append remainder).
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ test('inferAniSkipMetadataForFile falls back to anime directory title when filen
|
|||||||
|
|
||||||
test('buildSubminerScriptOpts includes aniskip metadata fields', () => {
|
test('buildSubminerScriptOpts includes aniskip metadata fields', () => {
|
||||||
const opts = buildSubminerScriptOpts('/tmp/SubMiner.AppImage', '/tmp/subminer.sock', {
|
const opts = buildSubminerScriptOpts('/tmp/SubMiner.AppImage', '/tmp/subminer.sock', {
|
||||||
title: "Frieren: Beyond Journey's End",
|
title: 'Frieren: Beyond Journey\'s End',
|
||||||
season: 1,
|
season: 1,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'guessit',
|
source: 'guessit',
|
||||||
|
|||||||
@@ -28,11 +28,7 @@ function toPositiveInt(value: unknown): number | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectEpisodeFromName(baseName: string): number | null {
|
function detectEpisodeFromName(baseName: string): number | null {
|
||||||
const patterns = [
|
const patterns = [/[Ss]\d+[Ee](\d{1,3})/, /(?:^|[\s._-])[Ee][Pp]?[\s._-]*(\d{1,3})(?:$|[\s._-])/, /[-\s](\d{1,3})$/];
|
||||||
/[Ss]\d+[Ee](\d{1,3})/,
|
|
||||||
/(?:^|[\s._-])[Ee][Pp]?[\s._-]*(\d{1,3})(?:$|[\s._-])/,
|
|
||||||
/[-\s](\d{1,3})$/,
|
|
||||||
];
|
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
const match = baseName.match(pattern);
|
const match = baseName.match(pattern);
|
||||||
if (!match || !match[1]) continue;
|
if (!match || !match[1]) continue;
|
||||||
@@ -175,11 +171,7 @@ export function inferAniSkipMetadataForFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeScriptOptValue(value: string): string {
|
function sanitizeScriptOptValue(value: string): string {
|
||||||
return value
|
return value.replace(/,/g, ' ').replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
.replace(/,/g, ' ')
|
|
||||||
.replace(/[\r\n]/g, ' ')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSubminerScriptOpts(
|
export function buildSubminerScriptOpts(
|
||||||
|
|||||||
@@ -33,12 +33,6 @@ function createContext(overrides: Partial<LauncherCommandContext> = {}): Launche
|
|||||||
scriptPath: '/tmp/subminer',
|
scriptPath: '/tmp/subminer',
|
||||||
scriptName: 'subminer',
|
scriptName: 'subminer',
|
||||||
mpvSocketPath: '/tmp/subminer.sock',
|
mpvSocketPath: '/tmp/subminer.sock',
|
||||||
pluginRuntimeConfig: {
|
|
||||||
socketPath: '/tmp/subminer.sock',
|
|
||||||
autoStart: true,
|
|
||||||
autoStartVisibleOverlay: true,
|
|
||||||
autoStartPauseUntilReady: true,
|
|
||||||
},
|
|
||||||
appPath: '/tmp/subminer.app',
|
appPath: '/tmp/subminer.app',
|
||||||
launcherJellyfinConfig: {},
|
launcherJellyfinConfig: {},
|
||||||
processAdapter: adapter,
|
processAdapter: adapter,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Args, LauncherJellyfinConfig, PluginRuntimeConfig } from '../types.js';
|
import type { Args, LauncherJellyfinConfig } from '../types.js';
|
||||||
import type { ProcessAdapter } from '../process-adapter.js';
|
import type { ProcessAdapter } from '../process-adapter.js';
|
||||||
|
|
||||||
export interface LauncherCommandContext {
|
export interface LauncherCommandContext {
|
||||||
@@ -6,7 +6,6 @@ export interface LauncherCommandContext {
|
|||||||
scriptPath: string;
|
scriptPath: string;
|
||||||
scriptName: string;
|
scriptName: string;
|
||||||
mpvSocketPath: string;
|
mpvSocketPath: string;
|
||||||
pluginRuntimeConfig: PluginRuntimeConfig;
|
|
||||||
appPath: string | null;
|
appPath: string | null;
|
||||||
launcherJellyfinConfig: LauncherJellyfinConfig;
|
launcherJellyfinConfig: LauncherJellyfinConfig;
|
||||||
processAdapter: ProcessAdapter;
|
processAdapter: ProcessAdapter;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function registerCleanup(context: LauncherCommandContext): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runPlaybackCommand(context: LauncherCommandContext): Promise<void> {
|
export async function runPlaybackCommand(context: LauncherCommandContext): Promise<void> {
|
||||||
const { args, appPath, scriptPath, mpvSocketPath, pluginRuntimeConfig, processAdapter } = context;
|
const { args, appPath, scriptPath, mpvSocketPath, processAdapter } = context;
|
||||||
if (!appPath) {
|
if (!appPath) {
|
||||||
fail('SubMiner AppImage not found. Install to ~/.local/bin/ or set SUBMINER_APPIMAGE_PATH.');
|
fail('SubMiner AppImage not found. Install to ~/.local/bin/ or set SUBMINER_APPIMAGE_PATH.');
|
||||||
}
|
}
|
||||||
@@ -137,19 +137,6 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi
|
|||||||
log('info', args.logLevel, 'YouTube subtitle mode: off');
|
log('info', args.logLevel, 'YouTube subtitle mode: off');
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldPauseUntilOverlayReady =
|
|
||||||
pluginRuntimeConfig.autoStart &&
|
|
||||||
pluginRuntimeConfig.autoStartVisibleOverlay &&
|
|
||||||
pluginRuntimeConfig.autoStartPauseUntilReady;
|
|
||||||
|
|
||||||
if (shouldPauseUntilOverlayReady) {
|
|
||||||
log(
|
|
||||||
'info',
|
|
||||||
args.logLevel,
|
|
||||||
'Configured to pause mpv until overlay and tokenization are ready',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
startMpv(
|
startMpv(
|
||||||
selectedTarget.target,
|
selectedTarget.target,
|
||||||
selectedTarget.kind,
|
selectedTarget.kind,
|
||||||
@@ -157,7 +144,6 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi
|
|||||||
mpvSocketPath,
|
mpvSocketPath,
|
||||||
appPath,
|
appPath,
|
||||||
preloadedSubtitles,
|
preloadedSubtitles,
|
||||||
{ startPaused: shouldPauseUntilOverlayReady },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isYoutubeUrl && args.youtubeSubgenMode === 'automatic') {
|
if (isYoutubeUrl && args.youtubeSubgenMode === 'automatic') {
|
||||||
@@ -181,7 +167,6 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ready = await waitForUnixSocketReady(mpvSocketPath, 10000);
|
const ready = await waitForUnixSocketReady(mpvSocketPath, 10000);
|
||||||
const pluginAutoStartEnabled = pluginRuntimeConfig.autoStart;
|
|
||||||
const shouldStartOverlay = args.startOverlay || args.autoStartOverlay;
|
const shouldStartOverlay = args.startOverlay || args.autoStartOverlay;
|
||||||
if (shouldStartOverlay) {
|
if (shouldStartOverlay) {
|
||||||
if (ready) {
|
if (ready) {
|
||||||
@@ -194,16 +179,6 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
await startOverlay(appPath, args, mpvSocketPath);
|
await startOverlay(appPath, args, mpvSocketPath);
|
||||||
} else if (pluginAutoStartEnabled) {
|
|
||||||
if (ready) {
|
|
||||||
log('info', args.logLevel, 'MPV IPC socket ready, relying on mpv plugin auto-start');
|
|
||||||
} else {
|
|
||||||
log(
|
|
||||||
'info',
|
|
||||||
args.logLevel,
|
|
||||||
'MPV IPC socket not ready yet, relying on mpv plugin auto-start',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (ready) {
|
} else if (ready) {
|
||||||
log(
|
log(
|
||||||
'info',
|
'info',
|
||||||
@@ -219,26 +194,15 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const mpvProc = state.mpvProc;
|
if (!state.mpvProc) {
|
||||||
if (!mpvProc) {
|
|
||||||
stopOverlay(args);
|
stopOverlay(args);
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
state.mpvProc.on('exit', (code) => {
|
||||||
const finalize = (code: number | null | undefined) => {
|
|
||||||
stopOverlay(args);
|
stopOverlay(args);
|
||||||
processAdapter.setExitCode(code ?? 0);
|
processAdapter.setExitCode(code ?? 0);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
|
||||||
|
|
||||||
if (mpvProc.exitCode !== null && mpvProc.exitCode !== undefined) {
|
|
||||||
finalize(mpvProc.exitCode);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mpvProc.once('exit', (code) => {
|
|
||||||
finalize(code);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,27 +51,10 @@ test('parseLauncherJellyfinConfig omits legacy token and user id fields', () =>
|
|||||||
assert.equal('userId' in parsed, false);
|
assert.equal('userId' in parsed, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parsePluginRuntimeConfigContent reads socket path and startup gate options', () => {
|
test('parsePluginRuntimeConfigContent reads socket_path and ignores inline comments', () => {
|
||||||
const parsed = parsePluginRuntimeConfigContent(`
|
const parsed = parsePluginRuntimeConfigContent(`
|
||||||
# comment
|
# comment
|
||||||
socket_path = /tmp/custom.sock # trailing comment
|
socket_path = /tmp/custom.sock # trailing comment
|
||||||
auto_start = yes
|
|
||||||
auto_start_visible_overlay = true
|
|
||||||
auto_start_pause_until_ready = 1
|
|
||||||
`);
|
`);
|
||||||
assert.equal(parsed.socketPath, '/tmp/custom.sock');
|
assert.equal(parsed.socketPath, '/tmp/custom.sock');
|
||||||
assert.equal(parsed.autoStart, true);
|
|
||||||
assert.equal(parsed.autoStartVisibleOverlay, true);
|
|
||||||
assert.equal(parsed.autoStartPauseUntilReady, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('parsePluginRuntimeConfigContent falls back to disabled startup gate options', () => {
|
|
||||||
const parsed = parsePluginRuntimeConfigContent(`
|
|
||||||
auto_start = maybe
|
|
||||||
auto_start_visible_overlay = no
|
|
||||||
auto_start_pause_until_ready = off
|
|
||||||
`);
|
|
||||||
assert.equal(parsed.autoStart, false);
|
|
||||||
assert.equal(parsed.autoStartVisibleOverlay, false);
|
|
||||||
assert.equal(parsed.autoStartPauseUntilReady, false);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { execFileSync } from 'node:child_process';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
test('launcher root help lists subcommands', () => {
|
test('launcher root help lists subcommands', () => {
|
||||||
const output = execFileSync('bun', ['run', path.join(process.cwd(), 'launcher/main.ts'), '-h'], {
|
const output = execFileSync(
|
||||||
encoding: 'utf8',
|
'bun',
|
||||||
});
|
['run', path.join(process.cwd(), 'launcher/main.ts'), '-h'],
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
);
|
||||||
|
|
||||||
assert.match(output, /Commands:/);
|
assert.match(output, /Commands:/);
|
||||||
assert.match(output, /jellyfin\|jf/);
|
assert.match(output, /jellyfin\|jf/);
|
||||||
|
|||||||
@@ -182,8 +182,7 @@ export function parseCliPrograms(
|
|||||||
server: typeof options.server === 'string' ? options.server : undefined,
|
server: typeof options.server === 'string' ? options.server : undefined,
|
||||||
username: typeof options.username === 'string' ? options.username : undefined,
|
username: typeof options.username === 'string' ? options.username : undefined,
|
||||||
password: typeof options.password === 'string' ? options.password : undefined,
|
password: typeof options.password === 'string' ? options.password : undefined,
|
||||||
passwordStore:
|
passwordStore: typeof options.passwordStore === 'string' ? options.passwordStore : undefined,
|
||||||
typeof options.passwordStore === 'string' ? options.passwordStore : undefined,
|
|
||||||
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,64 +15,22 @@ export function getPluginConfigCandidates(): string[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parsePluginRuntimeConfigContent(
|
export function parsePluginRuntimeConfigContent(content: string): PluginRuntimeConfig {
|
||||||
content: string,
|
const runtimeConfig: PluginRuntimeConfig = { socketPath: DEFAULT_SOCKET_PATH };
|
||||||
logLevel: LogLevel = 'warn',
|
|
||||||
): PluginRuntimeConfig {
|
|
||||||
const runtimeConfig: PluginRuntimeConfig = {
|
|
||||||
socketPath: DEFAULT_SOCKET_PATH,
|
|
||||||
autoStart: true,
|
|
||||||
autoStartVisibleOverlay: true,
|
|
||||||
autoStartPauseUntilReady: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseBooleanValue = (key: string, value: string): boolean => {
|
|
||||||
const normalized = value.trim().toLowerCase();
|
|
||||||
if (['yes', 'true', '1', 'on'].includes(normalized)) return true;
|
|
||||||
if (['no', 'false', '0', 'off'].includes(normalized)) return false;
|
|
||||||
log('warn', logLevel, `Invalid boolean value for ${key}: "${value}". Using false.`);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const line of content.split(/\r?\n/)) {
|
for (const line of content.split(/\r?\n/)) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (trimmed.length === 0 || trimmed.startsWith('#')) continue;
|
if (trimmed.length === 0 || trimmed.startsWith('#')) continue;
|
||||||
const keyValueMatch = trimmed.match(/^([a-z0-9_-]+)\s*=\s*(.+)$/i);
|
const socketMatch = trimmed.match(/^socket_path\s*=\s*(.+)$/i);
|
||||||
if (!keyValueMatch) continue;
|
if (!socketMatch) continue;
|
||||||
const key = (keyValueMatch[1] || '').toLowerCase();
|
const value = (socketMatch[1] || '').split('#', 1)[0]?.trim() || '';
|
||||||
const value = (keyValueMatch[2] || '').split('#', 1)[0]?.trim() || '';
|
if (value) runtimeConfig.socketPath = value;
|
||||||
if (!value) continue;
|
|
||||||
|
|
||||||
if (key === 'socket_path') {
|
|
||||||
runtimeConfig.socketPath = value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (key === 'auto_start') {
|
|
||||||
runtimeConfig.autoStart = parseBooleanValue('auto_start', value);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (key === 'auto_start_visible_overlay') {
|
|
||||||
runtimeConfig.autoStartVisibleOverlay = parseBooleanValue('auto_start_visible_overlay', value);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (key === 'auto_start_pause_until_ready') {
|
|
||||||
runtimeConfig.autoStartPauseUntilReady = parseBooleanValue(
|
|
||||||
'auto_start_pause_until_ready',
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return runtimeConfig;
|
return runtimeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig {
|
export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig {
|
||||||
const candidates = getPluginConfigCandidates();
|
const candidates = getPluginConfigCandidates();
|
||||||
const defaults: PluginRuntimeConfig = {
|
const defaults: PluginRuntimeConfig = { socketPath: DEFAULT_SOCKET_PATH };
|
||||||
socketPath: DEFAULT_SOCKET_PATH,
|
|
||||||
autoStart: true,
|
|
||||||
autoStartVisibleOverlay: true,
|
|
||||||
autoStartPauseUntilReady: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const configPath of candidates) {
|
for (const configPath of candidates) {
|
||||||
if (!fs.existsSync(configPath)) continue;
|
if (!fs.existsSync(configPath)) continue;
|
||||||
@@ -81,7 +39,7 @@ export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig
|
|||||||
log(
|
log(
|
||||||
'debug',
|
'debug',
|
||||||
logLevel,
|
logLevel,
|
||||||
`Using mpv plugin settings from ${configPath}: socket_path=${parsed.socketPath}, auto_start=${parsed.autoStart}, auto_start_visible_overlay=${parsed.autoStartVisibleOverlay}, auto_start_pause_until_ready=${parsed.autoStartPauseUntilReady}`,
|
`Using mpv plugin settings from ${configPath}: socket_path=${parsed.socketPath}`,
|
||||||
);
|
);
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -93,7 +51,7 @@ export function readPluginRuntimeConfig(logLevel: LogLevel): PluginRuntimeConfig
|
|||||||
log(
|
log(
|
||||||
'debug',
|
'debug',
|
||||||
logLevel,
|
logLevel,
|
||||||
`No mpv subminer.conf found; using launcher defaults (socket_path=${defaults.socketPath}, auto_start=${defaults.autoStart}, auto_start_visible_overlay=${defaults.autoStartVisibleOverlay}, auto_start_pause_until_ready=${defaults.autoStartPauseUntilReady})`,
|
`No mpv subminer.conf found; using launcher defaults (socket_path=${defaults.socketPath})`,
|
||||||
);
|
);
|
||||||
return defaults;
|
return defaults;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,10 @@ function withTempDir<T>(fn: (dir: string) => T): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runLauncher(argv: string[], env: NodeJS.ProcessEnv): RunResult {
|
function runLauncher(argv: string[], env: NodeJS.ProcessEnv): RunResult {
|
||||||
const result = spawnSync(
|
const result = spawnSync(process.execPath, ['run', path.join(process.cwd(), 'launcher/main.ts'), ...argv], {
|
||||||
process.execPath,
|
env,
|
||||||
['run', path.join(process.cwd(), 'launcher/main.ts'), ...argv],
|
encoding: 'utf8',
|
||||||
{
|
});
|
||||||
env,
|
|
||||||
encoding: 'utf8',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
status: result.status,
|
status: result.status,
|
||||||
stdout: result.stdout || '',
|
stdout: result.stdout || '',
|
||||||
@@ -229,7 +225,10 @@ test('jellyfin setup forwards password-store to app command', () => {
|
|||||||
SUBMINER_APPIMAGE_PATH: appPath,
|
SUBMINER_APPIMAGE_PATH: appPath,
|
||||||
SUBMINER_TEST_CAPTURE: capturePath,
|
SUBMINER_TEST_CAPTURE: capturePath,
|
||||||
};
|
};
|
||||||
const result = runLauncher(['jf', 'setup', '--password-store', 'gnome-libsecret'], env);
|
const result = runLauncher(
|
||||||
|
['jf', 'setup', '--password-store', 'gnome-libsecret'],
|
||||||
|
env,
|
||||||
|
);
|
||||||
|
|
||||||
assert.equal(result.status, 0);
|
assert.equal(result.status, 0);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|||||||
@@ -19,15 +19,14 @@ import { runPlaybackCommand } from './commands/playback-command.js';
|
|||||||
function createCommandContext(
|
function createCommandContext(
|
||||||
args: ReturnType<typeof parseArgs>,
|
args: ReturnType<typeof parseArgs>,
|
||||||
scriptPath: string,
|
scriptPath: string,
|
||||||
pluginRuntimeConfig: ReturnType<typeof readPluginRuntimeConfig>,
|
mpvSocketPath: string,
|
||||||
appPath: string | null,
|
appPath: string | null,
|
||||||
): LauncherCommandContext {
|
): LauncherCommandContext {
|
||||||
return {
|
return {
|
||||||
args,
|
args,
|
||||||
scriptPath,
|
scriptPath,
|
||||||
scriptName: path.basename(scriptPath),
|
scriptName: path.basename(scriptPath),
|
||||||
mpvSocketPath: pluginRuntimeConfig.socketPath,
|
mpvSocketPath,
|
||||||
pluginRuntimeConfig,
|
|
||||||
appPath,
|
appPath,
|
||||||
launcherJellyfinConfig: loadLauncherJellyfinConfig(),
|
launcherJellyfinConfig: loadLauncherJellyfinConfig(),
|
||||||
processAdapter: nodeProcessAdapter,
|
processAdapter: nodeProcessAdapter,
|
||||||
@@ -56,7 +55,7 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
log('debug', args.logLevel, `Wrapper log level set to: ${args.logLevel}`);
|
log('debug', args.logLevel, `Wrapper log level set to: ${args.logLevel}`);
|
||||||
|
|
||||||
const context = createCommandContext(args, scriptPath, pluginRuntimeConfig, appPath);
|
const context = createCommandContext(args, scriptPath, pluginRuntimeConfig.socketPath, appPath);
|
||||||
|
|
||||||
if (runDoctorCommand(context)) {
|
if (runDoctorCommand(context)) {
|
||||||
return;
|
return;
|
||||||
@@ -72,7 +71,6 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
const resolvedAppPath = ensureAppPath(context);
|
const resolvedAppPath = ensureAppPath(context);
|
||||||
state.appPath = resolvedAppPath;
|
state.appPath = resolvedAppPath;
|
||||||
log('debug', args.logLevel, `Using SubMiner app binary: ${resolvedAppPath}`);
|
|
||||||
const appContext: LauncherCommandContext = {
|
const appContext: LauncherCommandContext = {
|
||||||
...context,
|
...context,
|
||||||
appPath: resolvedAppPath,
|
appPath: resolvedAppPath,
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import net from 'node:net';
|
import net from 'node:net';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import type { Args } from './types';
|
import { waitForUnixSocketReady } from './mpv';
|
||||||
import { startOverlay, state, waitForUnixSocketReady } from './mpv';
|
|
||||||
import * as mpvModule from './mpv';
|
import * as mpvModule from './mpv';
|
||||||
|
|
||||||
function createTempSocketPath(): { dir: string; socketPath: string } {
|
function createTempSocketPath(): { dir: string; socketPath: string } {
|
||||||
@@ -60,82 +59,3 @@ test('waitForUnixSocketReady returns true when socket becomes connectable before
|
|||||||
fs.rmSync(dir, { recursive: true, force: true });
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeArgs(overrides: Partial<Args> = {}): Args {
|
|
||||||
return {
|
|
||||||
backend: 'x11',
|
|
||||||
directory: '.',
|
|
||||||
recursive: false,
|
|
||||||
profile: '',
|
|
||||||
startOverlay: false,
|
|
||||||
youtubeSubgenMode: 'off',
|
|
||||||
whisperBin: '',
|
|
||||||
whisperModel: '',
|
|
||||||
youtubeSubgenOutDir: '',
|
|
||||||
youtubeSubgenAudioFormat: 'wav',
|
|
||||||
youtubeSubgenKeepTemp: false,
|
|
||||||
youtubePrimarySubLangs: [],
|
|
||||||
youtubeSecondarySubLangs: [],
|
|
||||||
youtubeAudioLangs: [],
|
|
||||||
youtubeWhisperSourceLanguage: 'ja',
|
|
||||||
useTexthooker: false,
|
|
||||||
autoStartOverlay: false,
|
|
||||||
texthookerOnly: false,
|
|
||||||
useRofi: false,
|
|
||||||
logLevel: 'error',
|
|
||||||
passwordStore: '',
|
|
||||||
target: '',
|
|
||||||
targetKind: '',
|
|
||||||
jimakuApiKey: '',
|
|
||||||
jimakuApiKeyCommand: '',
|
|
||||||
jimakuApiBaseUrl: '',
|
|
||||||
jimakuLanguagePreference: 'none',
|
|
||||||
jimakuMaxEntryResults: 10,
|
|
||||||
jellyfin: false,
|
|
||||||
jellyfinLogin: false,
|
|
||||||
jellyfinLogout: false,
|
|
||||||
jellyfinPlay: false,
|
|
||||||
jellyfinDiscovery: false,
|
|
||||||
doctor: false,
|
|
||||||
configPath: false,
|
|
||||||
configShow: false,
|
|
||||||
mpvIdle: false,
|
|
||||||
mpvSocket: false,
|
|
||||||
mpvStatus: false,
|
|
||||||
appPassthrough: false,
|
|
||||||
appArgs: [],
|
|
||||||
jellyfinServer: '',
|
|
||||||
jellyfinUsername: '',
|
|
||||||
jellyfinPassword: '',
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('startOverlay resolves without fixed 2s sleep when readiness signals arrive quickly', async () => {
|
|
||||||
const { dir, socketPath } = createTempSocketPath();
|
|
||||||
const appPath = path.join(dir, 'fake-subminer.sh');
|
|
||||||
fs.writeFileSync(appPath, '#!/bin/sh\nexit 0\n');
|
|
||||||
fs.chmodSync(appPath, 0o755);
|
|
||||||
fs.writeFileSync(socketPath, '');
|
|
||||||
const originalCreateConnection = net.createConnection;
|
|
||||||
try {
|
|
||||||
net.createConnection = (() => {
|
|
||||||
const socket = new EventEmitter() as net.Socket;
|
|
||||||
socket.destroy = (() => socket) as net.Socket['destroy'];
|
|
||||||
socket.setTimeout = (() => socket) as net.Socket['setTimeout'];
|
|
||||||
setTimeout(() => socket.emit('connect'), 10);
|
|
||||||
return socket;
|
|
||||||
}) as typeof net.createConnection;
|
|
||||||
|
|
||||||
const startedAt = Date.now();
|
|
||||||
await startOverlay(appPath, makeArgs(), socketPath);
|
|
||||||
const elapsedMs = Date.now() - startedAt;
|
|
||||||
|
|
||||||
assert.ok(elapsedMs < 1200, `expected startOverlay <1200ms, got ${elapsedMs}ms`);
|
|
||||||
} finally {
|
|
||||||
net.createConnection = originalCreateConnection;
|
|
||||||
state.overlayProc = null;
|
|
||||||
state.overlayManagedByLauncher = false;
|
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ export const state = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DETACHED_IDLE_MPV_PID_FILE = path.join(os.tmpdir(), 'subminer-idle-mpv.pid');
|
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;
|
|
||||||
|
|
||||||
function readTrackedDetachedMpvPid(): number | null {
|
function readTrackedDetachedMpvPid(): number | null {
|
||||||
try {
|
try {
|
||||||
@@ -426,7 +424,6 @@ export function startMpv(
|
|||||||
socketPath: string,
|
socketPath: string,
|
||||||
appPath: string,
|
appPath: string,
|
||||||
preloadedSubtitles?: { primaryPath?: string; secondaryPath?: string },
|
preloadedSubtitles?: { primaryPath?: string; secondaryPath?: string },
|
||||||
options?: { startPaused?: boolean },
|
|
||||||
): void {
|
): void {
|
||||||
if (targetKind === 'file' && (!fs.existsSync(target) || !fs.statSync(target).isFile())) {
|
if (targetKind === 'file' && (!fs.existsSync(target) || !fs.statSync(target).isFile())) {
|
||||||
fail(`Video file not found: ${target}`);
|
fail(`Video file not found: ${target}`);
|
||||||
@@ -476,10 +473,8 @@ export function startMpv(
|
|||||||
if (preloadedSubtitles?.secondaryPath) {
|
if (preloadedSubtitles?.secondaryPath) {
|
||||||
mpvArgs.push(`--sub-file=${preloadedSubtitles.secondaryPath}`);
|
mpvArgs.push(`--sub-file=${preloadedSubtitles.secondaryPath}`);
|
||||||
}
|
}
|
||||||
if (options?.startPaused) {
|
const aniSkipMetadata =
|
||||||
mpvArgs.push('--pause=yes');
|
targetKind === 'file' ? inferAniSkipMetadataForFile(target) : null;
|
||||||
}
|
|
||||||
const aniSkipMetadata = targetKind === 'file' ? inferAniSkipMetadataForFile(target) : null;
|
|
||||||
const scriptOpts = buildSubminerScriptOpts(appPath, socketPath, aniSkipMetadata);
|
const scriptOpts = buildSubminerScriptOpts(appPath, socketPath, aniSkipMetadata);
|
||||||
if (aniSkipMetadata) {
|
if (aniSkipMetadata) {
|
||||||
log(
|
log(
|
||||||
@@ -503,47 +498,7 @@ export function startMpv(
|
|||||||
state.mpvProc = spawn('mpv', mpvArgs, { stdio: 'inherit' });
|
state.mpvProc = spawn('mpv', mpvArgs, { stdio: 'inherit' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForOverlayStartCommandSettled(
|
export function startOverlay(appPath: string, args: Args, socketPath: string): Promise<void> {
|
||||||
proc: ReturnType<typeof spawn>,
|
|
||||||
logLevel: LogLevel,
|
|
||||||
timeoutMs: number,
|
|
||||||
): Promise<void> {
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
let settled = false;
|
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
const finish = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
if (timer) clearTimeout(timer);
|
|
||||||
proc.off('exit', onExit);
|
|
||||||
proc.off('error', onError);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onExit = (code: number | null) => {
|
|
||||||
if (typeof code === 'number' && code !== 0) {
|
|
||||||
log('warn', logLevel, `Overlay start command exited with status ${code}`);
|
|
||||||
}
|
|
||||||
finish();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onError = (error: Error) => {
|
|
||||||
log('warn', logLevel, `Overlay start command failed: ${error.message}`);
|
|
||||||
finish();
|
|
||||||
};
|
|
||||||
|
|
||||||
proc.once('exit', onExit);
|
|
||||||
proc.once('error', onError);
|
|
||||||
timer = setTimeout(finish, timeoutMs);
|
|
||||||
|
|
||||||
if (proc.exitCode !== null && proc.exitCode !== undefined) {
|
|
||||||
onExit(proc.exitCode);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function startOverlay(appPath: string, args: Args, socketPath: string): Promise<void> {
|
|
||||||
const backend = detectBackend(args.backend);
|
const backend = detectBackend(args.backend);
|
||||||
log('info', args.logLevel, `Starting SubMiner overlay (backend: ${backend})...`);
|
log('info', args.logLevel, `Starting SubMiner overlay (backend: ${backend})...`);
|
||||||
|
|
||||||
@@ -557,22 +512,9 @@ export async function startOverlay(appPath: string, args: Args, socketPath: stri
|
|||||||
});
|
});
|
||||||
state.overlayManagedByLauncher = true;
|
state.overlayManagedByLauncher = true;
|
||||||
|
|
||||||
const [socketReady] = await Promise.all([
|
return new Promise((resolve) => {
|
||||||
waitForUnixSocketReady(socketPath, OVERLAY_START_SOCKET_READY_TIMEOUT_MS),
|
setTimeout(resolve, 2000);
|
||||||
waitForOverlayStartCommandSettled(
|
});
|
||||||
state.overlayProc,
|
|
||||||
args.logLevel,
|
|
||||||
OVERLAY_START_COMMAND_SETTLE_TIMEOUT_MS,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!socketReady) {
|
|
||||||
log(
|
|
||||||
'debug',
|
|
||||||
args.logLevel,
|
|
||||||
'Overlay start continuing before mpv socket readiness was confirmed',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function launchTexthookerOnly(appPath: string, args: Args): never {
|
export function launchTexthookerOnly(appPath: string, args: Args): never {
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ test('parseArgs maps jellyfin play action and log-level override', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('parseArgs forwards jellyfin password-store option', () => {
|
test('parseArgs forwards jellyfin password-store option', () => {
|
||||||
const parsed = parseArgs(['jf', 'setup', '--password-store', 'gnome-libsecret'], 'subminer', {});
|
const parsed = parseArgs(
|
||||||
|
['jf', 'setup', '--password-store', 'gnome-libsecret'],
|
||||||
|
'subminer',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
assert.equal(parsed.jellyfin, true);
|
assert.equal(parsed.jellyfin, true);
|
||||||
assert.equal(parsed.passwordStore, 'gnome-libsecret');
|
assert.equal(parsed.passwordStore, 'gnome-libsecret');
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function createSmokeCase(name: string): SmokeCase {
|
|||||||
|
|
||||||
writeExecutable(
|
writeExecutable(
|
||||||
fakeMpvPath,
|
fakeMpvPath,
|
||||||
`#!/usr/bin/env node
|
`#!/usr/bin/env bun
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const net = require('node:net');
|
const net = require('node:net');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
@@ -101,7 +101,7 @@ process.on('SIGTERM', closeAndExit);
|
|||||||
|
|
||||||
writeExecutable(
|
writeExecutable(
|
||||||
fakeAppPath,
|
fakeAppPath,
|
||||||
`#!/usr/bin/env node
|
`#!/usr/bin/env bun
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
|
|
||||||
const logPath = ${JSON.stringify(fakeAppLogPath)};
|
const logPath = ${JSON.stringify(fakeAppLogPath)};
|
||||||
@@ -237,20 +237,8 @@ test('launcher mpv status returns ready when socket is connectable', async () =>
|
|||||||
env,
|
env,
|
||||||
'mpv-status',
|
'mpv-status',
|
||||||
);
|
);
|
||||||
const fakeMpvEntries = readJsonLines(path.join(smokeCase.artifactsDir, 'fake-mpv.log'));
|
assert.equal(result.status, 0);
|
||||||
const fakeMpvError = fakeMpvEntries.find(
|
assert.match(result.stdout, /socket ready/i);
|
||||||
(entry): entry is { error: string } => typeof entry.error === 'string',
|
|
||||||
)?.error;
|
|
||||||
const unixSocketDenied =
|
|
||||||
typeof fakeMpvError === 'string' && /eperm|operation not permitted/i.test(fakeMpvError);
|
|
||||||
|
|
||||||
if (unixSocketDenied) {
|
|
||||||
assert.equal(result.status, 1);
|
|
||||||
assert.match(result.stdout, /socket not ready/i);
|
|
||||||
} else {
|
|
||||||
assert.equal(result.status, 0);
|
|
||||||
assert.match(result.stdout, /socket ready/i);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
if (fakeMpv.exitCode === null) {
|
if (fakeMpv.exitCode === null) {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
@@ -274,6 +262,9 @@ test(
|
|||||||
'overlay-start-stop',
|
'overlay-start-stop',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.equal(result.status, 0);
|
||||||
|
assert.match(result.stdout, /Starting SubMiner overlay/i);
|
||||||
|
|
||||||
const appStartPath = path.join(smokeCase.artifactsDir, 'fake-app-start.log');
|
const appStartPath = path.join(smokeCase.artifactsDir, 'fake-app-start.log');
|
||||||
const appStopPath = path.join(smokeCase.artifactsDir, 'fake-app-stop.log');
|
const appStopPath = path.join(smokeCase.artifactsDir, 'fake-app-stop.log');
|
||||||
await waitForJsonLines(appStartPath, 1);
|
await waitForJsonLines(appStartPath, 1);
|
||||||
@@ -282,14 +273,6 @@ test(
|
|||||||
const appStartEntries = readJsonLines(appStartPath);
|
const appStartEntries = readJsonLines(appStartPath);
|
||||||
const appStopEntries = readJsonLines(appStopPath);
|
const appStopEntries = readJsonLines(appStopPath);
|
||||||
const mpvEntries = readJsonLines(path.join(smokeCase.artifactsDir, 'fake-mpv.log'));
|
const mpvEntries = readJsonLines(path.join(smokeCase.artifactsDir, 'fake-mpv.log'));
|
||||||
const mpvError = mpvEntries.find(
|
|
||||||
(entry): entry is { error: string } => typeof entry.error === 'string',
|
|
||||||
)?.error;
|
|
||||||
const unixSocketDenied =
|
|
||||||
typeof mpvError === 'string' && /eperm|operation not permitted/i.test(mpvError);
|
|
||||||
|
|
||||||
assert.equal(result.status, unixSocketDenied ? 3 : 0);
|
|
||||||
assert.match(result.stdout, /Starting SubMiner overlay/i);
|
|
||||||
|
|
||||||
assert.equal(appStartEntries.length, 1);
|
assert.equal(appStartEntries.length, 1);
|
||||||
assert.equal(appStopEntries.length, 1);
|
assert.equal(appStopEntries.length, 1);
|
||||||
@@ -319,43 +302,3 @@ test(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
|
||||||
'launcher starts mpv paused when plugin auto-start visible overlay gate is enabled',
|
|
||||||
{ timeout: 20000 },
|
|
||||||
async () => {
|
|
||||||
await withSmokeCase('autoplay-ready-gate', async (smokeCase) => {
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(smokeCase.xdgConfigHome, 'mpv', 'script-opts', 'subminer.conf'),
|
|
||||||
[
|
|
||||||
`socket_path=${smokeCase.socketPath}`,
|
|
||||||
'auto_start=yes',
|
|
||||||
'auto_start_visible_overlay=yes',
|
|
||||||
'auto_start_pause_until_ready=yes',
|
|
||||||
'',
|
|
||||||
].join('\n'),
|
|
||||||
);
|
|
||||||
|
|
||||||
const env = makeTestEnv(smokeCase);
|
|
||||||
const result = runLauncher(
|
|
||||||
smokeCase,
|
|
||||||
[smokeCase.videoPath, '--log-level', 'debug'],
|
|
||||||
env,
|
|
||||||
'autoplay-ready-gate',
|
|
||||||
);
|
|
||||||
|
|
||||||
const mpvEntries = readJsonLines(path.join(smokeCase.artifactsDir, 'fake-mpv.log'));
|
|
||||||
const mpvError = mpvEntries.find(
|
|
||||||
(entry): entry is { error: string } => typeof entry.error === 'string',
|
|
||||||
)?.error;
|
|
||||||
const unixSocketDenied =
|
|
||||||
typeof mpvError === 'string' && /eperm|operation not permitted/i.test(mpvError);
|
|
||||||
const mpvFirstArgs = mpvEntries[0]?.argv;
|
|
||||||
|
|
||||||
assert.equal(result.status, unixSocketDenied ? 3 : 0);
|
|
||||||
assert.equal(Array.isArray(mpvFirstArgs), true);
|
|
||||||
assert.equal((mpvFirstArgs as string[]).includes('--pause=yes'), true);
|
|
||||||
assert.match(result.stdout, /pause mpv until overlay and tokenization are ready/i);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -129,9 +129,6 @@ export interface LauncherJellyfinConfig {
|
|||||||
|
|
||||||
export interface PluginRuntimeConfig {
|
export interface PluginRuntimeConfig {
|
||||||
socketPath: string;
|
socketPath: string;
|
||||||
autoStart: boolean;
|
|
||||||
autoStartVisibleOverlay: boolean;
|
|
||||||
autoStartPauseUntilReady: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommandExecOptions {
|
export interface CommandExecOptions {
|
||||||
|
|||||||
14
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "subminer",
|
"name": "subminer",
|
||||||
"version": "0.2.0",
|
"version": "0.1.2",
|
||||||
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
"main": "dist/main-entry.js",
|
"main": "dist/main-entry.js",
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
"build": "tsc && bun run build:renderer && cp src/renderer/index.html src/renderer/style.css dist/renderer/ && cp -r src/renderer/fonts dist/renderer/ && bash scripts/build-macos-helper.sh",
|
"build": "tsc && bun run build:renderer && cp src/renderer/index.html src/renderer/style.css dist/renderer/ && cp -r src/renderer/fonts dist/renderer/ && bash scripts/build-macos-helper.sh",
|
||||||
"build:renderer": "esbuild src/renderer/renderer.ts --bundle --platform=browser --format=esm --target=es2022 --outfile=dist/renderer/renderer.js --sourcemap",
|
"build:renderer": "esbuild src/renderer/renderer.ts --bundle --platform=browser --format=esm --target=es2022 --outfile=dist/renderer/renderer.js --sourcemap",
|
||||||
"docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev docs --host 0.0.0.0 --port 5173 --strictPort",
|
"docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev docs --host 0.0.0.0 --port 5173 --strictPort",
|
||||||
"docs:watch": "bunx concurrently -n docs,backlog \"bun run docs:dev\" \"backlog browser\"",
|
|
||||||
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build docs",
|
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build docs",
|
||||||
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
@@ -20,11 +19,10 @@
|
|||||||
"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",
|
"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",
|
||||||
"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",
|
"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",
|
||||||
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
||||||
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua",
|
|
||||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts",
|
||||||
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts",
|
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/renderer/error-recovery.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts",
|
||||||
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||||
"test:subtitle:dist": "echo \"Subtitle tests are currently not configured\"",
|
"test:subtitle:dist": "echo \"Subtitle tests are currently not configured\"",
|
||||||
@@ -120,6 +118,10 @@
|
|||||||
"from": "vendor/yomitan-jlpt-vocab",
|
"from": "vendor/yomitan-jlpt-vocab",
|
||||||
"to": "yomitan-jlpt-vocab"
|
"to": "yomitan-jlpt-vocab"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"from": "vendor/jiten_freq_global",
|
||||||
|
"to": "jiten_freq_global"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"from": "assets",
|
"from": "assets",
|
||||||
"to": "assets"
|
"to": "assets"
|
||||||
|
|||||||
@@ -21,16 +21,18 @@ texthooker_port=5174
|
|||||||
backend=auto
|
backend=auto
|
||||||
|
|
||||||
# Automatically start overlay when a file is loaded
|
# Automatically start overlay when a file is loaded
|
||||||
# Runs only when mpv input-ipc-server matches socket_path.
|
auto_start=no
|
||||||
auto_start=yes
|
|
||||||
|
|
||||||
# Automatically show visible overlay when overlay starts
|
# Automatically show visible overlay when overlay starts
|
||||||
# Runs only when mpv input-ipc-server matches socket_path.
|
auto_start_visible_overlay=no
|
||||||
auto_start_visible_overlay=yes
|
|
||||||
|
|
||||||
# Pause mpv on visible auto-start until SubMiner signals overlay/tokenization readiness.
|
# Automatically show invisible overlay when overlay starts
|
||||||
# Requires auto_start=yes and auto_start_visible_overlay=yes.
|
# Values: platform-default, visible, hidden
|
||||||
auto_start_pause_until_ready=yes
|
# platform-default => hidden on Linux, visible on macOS/Windows
|
||||||
|
auto_start_invisible_overlay=platform-default
|
||||||
|
|
||||||
|
# Legacy alias (maps to auto_start_visible_overlay)
|
||||||
|
# auto_start_overlay=no
|
||||||
|
|
||||||
# Show OSD messages for overlay status
|
# Show OSD messages for overlay status
|
||||||
osd_messages=yes
|
osd_messages=yes
|
||||||
@@ -66,5 +68,6 @@ aniskip_button_key=y-k
|
|||||||
# OSD hint duration in seconds (shown during first 3s of intro).
|
# OSD hint duration in seconds (shown during first 3s of intro).
|
||||||
aniskip_button_duration=3
|
aniskip_button_duration=3
|
||||||
|
|
||||||
# MPV keybindings provided by plugin/subminer/main.lua:
|
# MPV keybindings provided by plugin/subminer.lua:
|
||||||
# y-s start, y-S stop, y-t toggle visible overlay
|
# y-s start, y-S stop, y-t toggle visible overlay
|
||||||
|
# y-i toggle invisible overlay, y-I show invisible overlay, y-u hide invisible overlay
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ function M.create(ctx)
|
|||||||
local environment = ctx.environment
|
local environment = ctx.environment
|
||||||
local subminer_log = ctx.log.subminer_log
|
local subminer_log = ctx.log.subminer_log
|
||||||
local show_osd = ctx.log.show_osd
|
local show_osd = ctx.log.show_osd
|
||||||
local request_generation = 0
|
|
||||||
local mal_lookup_cache = {}
|
|
||||||
local payload_cache = {}
|
|
||||||
local title_context_cache = {}
|
|
||||||
|
|
||||||
local function url_encode(text)
|
local function url_encode(text)
|
||||||
if type(text) ~= "string" then
|
if type(text) ~= "string" then
|
||||||
@@ -25,26 +21,22 @@ function M.create(ctx)
|
|||||||
return encoded:gsub(" ", "%%20")
|
return encoded:gsub(" ", "%%20")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function run_json_curl_async(url, callback)
|
local function run_json_curl(url)
|
||||||
mp.command_native_async({
|
local result = mp.command_native({
|
||||||
name = "subprocess",
|
name = "subprocess",
|
||||||
args = { "curl", "-sL", "--connect-timeout", "5", "-A", "SubMiner-mpv/ani-skip", url },
|
args = { "curl", "-sL", "--connect-timeout", "5", "-A", "SubMiner-mpv/ani-skip", url },
|
||||||
playback_only = false,
|
playback_only = false,
|
||||||
capture_stdout = true,
|
capture_stdout = true,
|
||||||
capture_stderr = true,
|
capture_stderr = true,
|
||||||
}, function(success, result, error)
|
})
|
||||||
if not success or not result or result.status ~= 0 or type(result.stdout) ~= "string" or result.stdout == "" then
|
if not result or result.status ~= 0 or type(result.stdout) ~= "string" or result.stdout == "" then
|
||||||
local detail = error or (result and result.stderr) or "curl failed"
|
return nil, result and result.stderr or "curl failed"
|
||||||
callback(nil, detail)
|
end
|
||||||
return
|
local parsed, parse_error = utils.parse_json(result.stdout)
|
||||||
end
|
if type(parsed) ~= "table" then
|
||||||
local parsed, parse_error = utils.parse_json(result.stdout)
|
return nil, parse_error or "invalid json"
|
||||||
if type(parsed) ~= "table" then
|
end
|
||||||
callback(nil, parse_error or "invalid json")
|
return parsed, nil
|
||||||
return
|
|
||||||
end
|
|
||||||
callback(parsed, nil)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function parse_episode_hint(text)
|
local function parse_episode_hint(text)
|
||||||
@@ -115,18 +107,6 @@ function M.create(ctx)
|
|||||||
local media_title = mp.get_property("media-title")
|
local media_title = mp.get_property("media-title")
|
||||||
local filename = mp.get_property("filename/no-ext") or mp.get_property("filename") or ""
|
local filename = mp.get_property("filename/no-ext") or mp.get_property("filename") or ""
|
||||||
local path = mp.get_property("path") or ""
|
local path = mp.get_property("path") or ""
|
||||||
local cache_key = table.concat({
|
|
||||||
tostring(forced_title or ""),
|
|
||||||
tostring(forced_season or ""),
|
|
||||||
tostring(forced_episode or ""),
|
|
||||||
tostring(media_title or ""),
|
|
||||||
tostring(filename or ""),
|
|
||||||
tostring(path or ""),
|
|
||||||
}, "\31")
|
|
||||||
local cached = title_context_cache[cache_key]
|
|
||||||
if type(cached) == "table" then
|
|
||||||
return cached.title, cached.episode, cached.season
|
|
||||||
end
|
|
||||||
local path_show_title = extract_show_title_from_path(path)
|
local path_show_title = extract_show_title_from_path(path)
|
||||||
local candidate_title = nil
|
local candidate_title = nil
|
||||||
if path_show_title and path_show_title ~= "" then
|
if path_show_title and path_show_title ~= "" then
|
||||||
@@ -137,11 +117,6 @@ function M.create(ctx)
|
|||||||
candidate_title = cleanup_title(media_title) or cleanup_title(filename) or cleanup_title(path)
|
candidate_title = cleanup_title(media_title) or cleanup_title(filename) or cleanup_title(path)
|
||||||
end
|
end
|
||||||
local episode = forced_episode or parse_episode_hint(media_title) or parse_episode_hint(filename) or parse_episode_hint(path) or 1
|
local episode = forced_episode or parse_episode_hint(media_title) or parse_episode_hint(filename) or parse_episode_hint(path) or 1
|
||||||
title_context_cache[cache_key] = {
|
|
||||||
title = candidate_title,
|
|
||||||
episode = episode,
|
|
||||||
season = forced_season,
|
|
||||||
}
|
|
||||||
return candidate_title, episode, forced_season
|
return candidate_title, episode, forced_season
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -164,84 +139,59 @@ function M.create(ctx)
|
|||||||
return best_item
|
return best_item
|
||||||
end
|
end
|
||||||
|
|
||||||
local function resolve_mal_id_async(title, season, request_id, callback)
|
local function resolve_mal_id(title, season)
|
||||||
local forced_mal_id = tonumber(opts.aniskip_mal_id)
|
local forced_mal_id = tonumber(opts.aniskip_mal_id)
|
||||||
if forced_mal_id and forced_mal_id > 0 then
|
if forced_mal_id and forced_mal_id > 0 then
|
||||||
callback(forced_mal_id, "(forced-mal-id)")
|
return forced_mal_id, "(forced-mal-id)"
|
||||||
return
|
|
||||||
end
|
end
|
||||||
if type(title) == "string" and title:match("^%d+$") then
|
if type(title) == "string" and title:match("^%d+$") then
|
||||||
local numeric = tonumber(title)
|
local numeric = tonumber(title)
|
||||||
if numeric and numeric > 0 then
|
if numeric and numeric > 0 then
|
||||||
callback(numeric, title)
|
return numeric, title
|
||||||
return
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if type(title) ~= "string" or title == "" then
|
if type(title) ~= "string" or title == "" then
|
||||||
callback(nil, nil)
|
return nil, nil
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local lookup = title
|
local lookup = title
|
||||||
if season and season > 1 then
|
if season and season > 1 then
|
||||||
lookup = string.format("%s Season %d", lookup, season)
|
lookup = string.format("%s Season %d", lookup, season)
|
||||||
end
|
end
|
||||||
local cache_key = string.format("%s|%s", lookup:lower(), tostring(season or "-"))
|
local mal_url = "https://myanimelist.net/search/prefix.json?type=anime&keyword=" .. url_encode(lookup)
|
||||||
local cached = mal_lookup_cache[cache_key]
|
local mal_json, mal_error = run_json_curl(mal_url)
|
||||||
if cached ~= nil then
|
if not mal_json then
|
||||||
if cached == false then
|
subminer_log("warn", "aniskip", "MAL lookup failed: " .. tostring(mal_error))
|
||||||
callback(nil, lookup)
|
return nil, lookup
|
||||||
else
|
end
|
||||||
callback(cached, lookup)
|
local categories = mal_json.categories
|
||||||
end
|
if type(categories) ~= "table" then
|
||||||
return
|
return nil, lookup
|
||||||
end
|
end
|
||||||
|
|
||||||
local mal_url = "https://myanimelist.net/search/prefix.json?type=anime&keyword=" .. url_encode(lookup)
|
local all_items = {}
|
||||||
run_json_curl_async(mal_url, function(mal_json, mal_error)
|
for _, category in ipairs(categories) do
|
||||||
if request_id ~= request_generation then
|
if type(category) == "table" and type(category.items) == "table" then
|
||||||
return
|
for _, item in ipairs(category.items) do
|
||||||
end
|
all_items[#all_items + 1] = item
|
||||||
if not mal_json then
|
|
||||||
subminer_log("warn", "aniskip", "MAL lookup failed: " .. tostring(mal_error))
|
|
||||||
callback(nil, lookup)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local categories = mal_json.categories
|
|
||||||
if type(categories) ~= "table" then
|
|
||||||
mal_lookup_cache[cache_key] = false
|
|
||||||
callback(nil, lookup)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local all_items = {}
|
|
||||||
for _, category in ipairs(categories) do
|
|
||||||
if type(category) == "table" and type(category.items) == "table" then
|
|
||||||
for _, item in ipairs(category.items) do
|
|
||||||
all_items[#all_items + 1] = item
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local best_item = select_best_mal_item(all_items, title, season)
|
end
|
||||||
if best_item and tonumber(best_item.id) then
|
local best_item = select_best_mal_item(all_items, title, season)
|
||||||
local matched_id = tonumber(best_item.id)
|
if best_item and tonumber(best_item.id) then
|
||||||
mal_lookup_cache[cache_key] = matched_id
|
subminer_log(
|
||||||
subminer_log(
|
"info",
|
||||||
"info",
|
"aniskip",
|
||||||
"aniskip",
|
string.format(
|
||||||
string.format(
|
'MAL candidate selected (score-based): id=%s name="%s" season_hint=%s',
|
||||||
'MAL candidate selected (score-based): id=%s name="%s" season_hint=%s',
|
tostring(best_item.id),
|
||||||
tostring(best_item.id),
|
tostring(best_item.name or ""),
|
||||||
tostring(best_item.name or ""),
|
tostring(season or "-")
|
||||||
tostring(season or "-")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
callback(matched_id, lookup)
|
)
|
||||||
return
|
return tonumber(best_item.id), lookup
|
||||||
end
|
end
|
||||||
mal_lookup_cache[cache_key] = false
|
return nil, lookup
|
||||||
callback(nil, lookup)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function set_intro_chapters(intro_start, intro_end)
|
local function set_intro_chapters(intro_start, intro_end)
|
||||||
@@ -288,7 +238,7 @@ function M.create(ctx)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function reset_aniskip_fields()
|
local function clear_aniskip_state()
|
||||||
state.aniskip.prompt_shown = false
|
state.aniskip.prompt_shown = false
|
||||||
state.aniskip.found = false
|
state.aniskip.found = false
|
||||||
state.aniskip.mal_id = nil
|
state.aniskip.mal_id = nil
|
||||||
@@ -299,11 +249,6 @@ function M.create(ctx)
|
|||||||
remove_aniskip_chapters()
|
remove_aniskip_chapters()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function clear_aniskip_state()
|
|
||||||
request_generation = request_generation + 1
|
|
||||||
reset_aniskip_fields()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function skip_intro_now()
|
local function skip_intro_now()
|
||||||
if not state.aniskip.found then
|
if not state.aniskip.found then
|
||||||
show_osd("Intro skip unavailable")
|
show_osd("Intro skip unavailable")
|
||||||
@@ -374,53 +319,17 @@ function M.create(ctx)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function is_launcher_context()
|
local function fetch_aniskip_for_current_media()
|
||||||
local forced_title = type(opts.aniskip_title) == "string" and (opts.aniskip_title:match("^%s*(.-)%s*$") or "") or ""
|
if not environment.is_subminer_app_running() then
|
||||||
if forced_title ~= "" then
|
subminer_log("debug", "lifecycle", "Skipping aniskip lookup: SubMiner app not running")
|
||||||
return true
|
return
|
||||||
end
|
end
|
||||||
local forced_mal_id = tonumber(opts.aniskip_mal_id)
|
|
||||||
if forced_mal_id and forced_mal_id > 0 then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
local forced_episode = tonumber(opts.aniskip_episode)
|
|
||||||
if forced_episode and forced_episode > 0 then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
local forced_season = tonumber(opts.aniskip_season)
|
|
||||||
if forced_season and forced_season > 0 then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function should_fetch_aniskip_async(trigger_source, callback)
|
clear_aniskip_state()
|
||||||
if trigger_source == "script-message" or trigger_source == "overlay-start" then
|
if not opts.aniskip_enabled then
|
||||||
callback(true, trigger_source)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if is_launcher_context() then
|
local title, episode, season = resolve_title_and_episode()
|
||||||
callback(true, "launcher-context")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if type(environment.is_subminer_app_running_async) == "function" then
|
|
||||||
environment.is_subminer_app_running_async(function(running)
|
|
||||||
if running then
|
|
||||||
callback(true, "subminer-app-running")
|
|
||||||
else
|
|
||||||
callback(false, "subminer-context-missing")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if environment.is_subminer_app_running() then
|
|
||||||
callback(true, "subminer-app-running")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
callback(false, "subminer-context-missing")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function resolve_lookup_titles(primary_title)
|
|
||||||
local media_title_fallback = cleanup_title(mp.get_property("media-title"))
|
local media_title_fallback = cleanup_title(mp.get_property("media-title"))
|
||||||
local filename_fallback = cleanup_title(mp.get_property("filename/no-ext") or mp.get_property("filename") or "")
|
local filename_fallback = cleanup_title(mp.get_property("filename/no-ext") or mp.get_property("filename") or "")
|
||||||
local path_fallback = cleanup_title(mp.get_property("path") or "")
|
local path_fallback = cleanup_title(mp.get_property("path") or "")
|
||||||
@@ -441,129 +350,55 @@ function M.create(ctx)
|
|||||||
seen_titles[key] = true
|
seen_titles[key] = true
|
||||||
lookup_titles[#lookup_titles + 1] = trimmed
|
lookup_titles[#lookup_titles + 1] = trimmed
|
||||||
end
|
end
|
||||||
push_lookup_title(primary_title)
|
push_lookup_title(title)
|
||||||
push_lookup_title(media_title_fallback)
|
push_lookup_title(media_title_fallback)
|
||||||
push_lookup_title(filename_fallback)
|
push_lookup_title(filename_fallback)
|
||||||
push_lookup_title(path_fallback)
|
push_lookup_title(path_fallback)
|
||||||
return lookup_titles
|
|
||||||
end
|
|
||||||
|
|
||||||
local function resolve_mal_from_candidates_async(lookup_titles, season, request_id, callback, index, last_lookup)
|
subminer_log(
|
||||||
local current_index = index or 1
|
"info",
|
||||||
local current_lookup = last_lookup
|
"aniskip",
|
||||||
if current_index > #lookup_titles then
|
string.format(
|
||||||
callback(nil, current_lookup)
|
'Query context: title="%s" season=%s episode=%s (opts: title="%s" season=%s episode=%s mal_id=%s; fallback_titles=%d)',
|
||||||
return
|
tostring(title or ""),
|
||||||
|
tostring(season or "-"),
|
||||||
|
tostring(episode or "-"),
|
||||||
|
tostring(opts.aniskip_title or ""),
|
||||||
|
tostring(opts.aniskip_season or "-"),
|
||||||
|
tostring(opts.aniskip_episode or "-"),
|
||||||
|
tostring(opts.aniskip_mal_id or "-"),
|
||||||
|
#lookup_titles
|
||||||
|
)
|
||||||
|
)
|
||||||
|
local mal_id, mal_lookup = nil, nil
|
||||||
|
for index, lookup_title in ipairs(lookup_titles) do
|
||||||
|
subminer_log("info", "aniskip", string.format('MAL lookup attempt %d/%d using title="%s"', index, #lookup_titles, lookup_title))
|
||||||
|
local attempt_mal_id, attempt_lookup = resolve_mal_id(lookup_title, season)
|
||||||
|
if attempt_mal_id then
|
||||||
|
mal_id = attempt_mal_id
|
||||||
|
mal_lookup = attempt_lookup
|
||||||
|
break
|
||||||
|
end
|
||||||
|
mal_lookup = attempt_lookup or mal_lookup
|
||||||
end
|
end
|
||||||
local lookup_title = lookup_titles[current_index]
|
if not mal_id then
|
||||||
subminer_log("info", "aniskip", string.format('MAL lookup attempt %d/%d using title="%s"', current_index, #lookup_titles, lookup_title))
|
subminer_log("info", "aniskip", string.format('Skipped: MAL id unavailable for query="%s"', tostring(mal_lookup or "")))
|
||||||
resolve_mal_id_async(lookup_title, season, request_id, function(mal_id, lookup)
|
|
||||||
if request_id ~= request_generation then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if mal_id then
|
|
||||||
callback(mal_id, lookup)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
resolve_mal_from_candidates_async(lookup_titles, season, request_id, callback, current_index + 1, lookup or current_lookup)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function fetch_payload_for_episode_async(mal_id, episode, request_id, callback)
|
|
||||||
local payload_cache_key = string.format("%d:%d", mal_id, episode)
|
|
||||||
local cached_payload = payload_cache[payload_cache_key]
|
|
||||||
if cached_payload ~= nil then
|
|
||||||
if cached_payload == false then
|
|
||||||
callback(nil, nil, true)
|
|
||||||
else
|
|
||||||
callback(cached_payload, nil, true)
|
|
||||||
end
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local url = string.format("https://api.aniskip.com/v1/skip-times/%d/%d?types=op&types=ed", mal_id, episode)
|
local url = string.format("https://api.aniskip.com/v1/skip-times/%d/%d?types=op&types=ed", mal_id, episode)
|
||||||
subminer_log("info", "aniskip", string.format("AniSkip URL=%s", url))
|
subminer_log("info", "aniskip", string.format('Resolved MAL id=%d using query="%s"; AniSkip URL=%s', mal_id, tostring(mal_lookup or ""), url))
|
||||||
run_json_curl_async(url, function(payload, fetch_error)
|
local payload, fetch_error = run_json_curl(url)
|
||||||
if request_id ~= request_generation then
|
if not payload then
|
||||||
return
|
subminer_log("warn", "aniskip", "AniSkip fetch failed: " .. tostring(fetch_error))
|
||||||
end
|
|
||||||
if not payload then
|
|
||||||
callback(nil, fetch_error, false)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if payload.found ~= true then
|
|
||||||
payload_cache[payload_cache_key] = false
|
|
||||||
callback(nil, nil, false)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
payload_cache[payload_cache_key] = payload
|
|
||||||
callback(payload, nil, false)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function fetch_aniskip_for_current_media(trigger_source)
|
|
||||||
local trigger = type(trigger_source) == "string" and trigger_source or "manual"
|
|
||||||
if not opts.aniskip_enabled then
|
|
||||||
clear_aniskip_state()
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if payload.found ~= true then
|
||||||
should_fetch_aniskip_async(trigger, function(allowed, reason)
|
subminer_log("info", "aniskip", "AniSkip: no skip windows found")
|
||||||
if not allowed then
|
return
|
||||||
subminer_log("debug", "aniskip", "Skipping lookup: " .. tostring(reason))
|
end
|
||||||
return
|
if not apply_aniskip_payload(mal_id, title, episode, payload) then
|
||||||
end
|
subminer_log("info", "aniskip", "AniSkip payload did not include OP interval")
|
||||||
|
end
|
||||||
request_generation = request_generation + 1
|
|
||||||
local request_id = request_generation
|
|
||||||
reset_aniskip_fields()
|
|
||||||
local title, episode, season = resolve_title_and_episode()
|
|
||||||
local lookup_titles = resolve_lookup_titles(title)
|
|
||||||
|
|
||||||
subminer_log(
|
|
||||||
"info",
|
|
||||||
"aniskip",
|
|
||||||
string.format(
|
|
||||||
'Query context: trigger=%s reason=%s title="%s" season=%s episode=%s (opts: title="%s" season=%s episode=%s mal_id=%s; fallback_titles=%d)',
|
|
||||||
tostring(trigger),
|
|
||||||
tostring(reason or "-"),
|
|
||||||
tostring(title or ""),
|
|
||||||
tostring(season or "-"),
|
|
||||||
tostring(episode or "-"),
|
|
||||||
tostring(opts.aniskip_title or ""),
|
|
||||||
tostring(opts.aniskip_season or "-"),
|
|
||||||
tostring(opts.aniskip_episode or "-"),
|
|
||||||
tostring(opts.aniskip_mal_id or "-"),
|
|
||||||
#lookup_titles
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
resolve_mal_from_candidates_async(lookup_titles, season, request_id, function(mal_id, mal_lookup)
|
|
||||||
if request_id ~= request_generation then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not mal_id then
|
|
||||||
subminer_log("info", "aniskip", string.format('Skipped: MAL id unavailable for query="%s"', tostring(mal_lookup or "")))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
subminer_log("info", "aniskip", string.format('Resolved MAL id=%d using query="%s"', mal_id, tostring(mal_lookup or "")))
|
|
||||||
fetch_payload_for_episode_async(mal_id, episode, request_id, function(payload, fetch_error)
|
|
||||||
if request_id ~= request_generation then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not payload then
|
|
||||||
if fetch_error then
|
|
||||||
subminer_log("warn", "aniskip", "AniSkip fetch failed: " .. tostring(fetch_error))
|
|
||||||
else
|
|
||||||
subminer_log("info", "aniskip", "AniSkip: no skip windows found")
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not apply_aniskip_payload(mal_id, title, episode, payload) then
|
|
||||||
subminer_log("info", "aniskip", "AniSkip payload did not include OP interval")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -23,47 +23,14 @@ function M.init()
|
|||||||
environment = environment,
|
environment = environment,
|
||||||
}
|
}
|
||||||
|
|
||||||
local instances = {}
|
ctx.log = require("log").create(ctx)
|
||||||
|
ctx.binary = require("binary").create(ctx)
|
||||||
local function lazy_instance(key, factory)
|
ctx.aniskip = require("aniskip").create(ctx)
|
||||||
if instances[key] == nil then
|
ctx.hover = require("hover").create(ctx)
|
||||||
instances[key] = factory()
|
ctx.process = require("process").create(ctx)
|
||||||
end
|
ctx.ui = require("ui").create(ctx)
|
||||||
return instances[key]
|
ctx.messages = require("messages").create(ctx)
|
||||||
end
|
ctx.lifecycle = require("lifecycle").create(ctx)
|
||||||
|
|
||||||
local function make_lazy_proxy(key, factory)
|
|
||||||
return setmetatable({}, {
|
|
||||||
__index = function(_, member)
|
|
||||||
return lazy_instance(key, factory)[member]
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
ctx.log = make_lazy_proxy("log", function()
|
|
||||||
return require("log").create(ctx)
|
|
||||||
end)
|
|
||||||
ctx.binary = make_lazy_proxy("binary", function()
|
|
||||||
return require("binary").create(ctx)
|
|
||||||
end)
|
|
||||||
ctx.aniskip = make_lazy_proxy("aniskip", function()
|
|
||||||
return require("aniskip").create(ctx)
|
|
||||||
end)
|
|
||||||
ctx.hover = make_lazy_proxy("hover", function()
|
|
||||||
return require("hover").create(ctx)
|
|
||||||
end)
|
|
||||||
ctx.process = make_lazy_proxy("process", function()
|
|
||||||
return require("process").create(ctx)
|
|
||||||
end)
|
|
||||||
ctx.ui = make_lazy_proxy("ui", function()
|
|
||||||
return require("ui").create(ctx)
|
|
||||||
end)
|
|
||||||
ctx.messages = make_lazy_proxy("messages", function()
|
|
||||||
return require("messages").create(ctx)
|
|
||||||
end)
|
|
||||||
ctx.lifecycle = make_lazy_proxy("lifecycle", function()
|
|
||||||
return require("lifecycle").create(ctx)
|
|
||||||
end)
|
|
||||||
|
|
||||||
ctx.ui.register_keybindings()
|
ctx.ui.register_keybindings()
|
||||||
ctx.messages.register_script_messages()
|
ctx.messages.register_script_messages()
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ function M.create(ctx)
|
|||||||
local mp = ctx.mp
|
local mp = ctx.mp
|
||||||
|
|
||||||
local detected_backend = nil
|
local detected_backend = nil
|
||||||
local app_running_cache_value = nil
|
|
||||||
local app_running_cache_time = nil
|
|
||||||
local app_running_check_inflight = false
|
|
||||||
local app_running_waiters = {}
|
|
||||||
local APP_RUNNING_CACHE_TTL_SECONDS = 2
|
|
||||||
|
|
||||||
local function is_windows()
|
local function is_windows()
|
||||||
return package.config:sub(1, 1) == "\\"
|
return package.config:sub(1, 1) == "\\"
|
||||||
@@ -34,21 +29,20 @@ function M.create(ctx)
|
|||||||
return not is_windows() and not is_macos()
|
return not is_windows() and not is_macos()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function now_seconds()
|
local function is_subminer_process_running()
|
||||||
if type(mp.get_time) == "function" then
|
local command = is_windows() and { "tasklist", "/FO", "CSV", "/NH" } or { "ps", "-A", "-o", "args=" }
|
||||||
local value = tonumber(mp.get_time())
|
local result = mp.command_native({
|
||||||
if value then
|
name = "subprocess",
|
||||||
return value
|
args = command,
|
||||||
end
|
playback_only = false,
|
||||||
end
|
capture_stdout = true,
|
||||||
return os.time()
|
capture_stderr = false,
|
||||||
end
|
})
|
||||||
|
if not result or type(result.stdout) ~= "string" or result.status ~= 0 then
|
||||||
local function process_list_has_subminer(raw_process_list)
|
|
||||||
if type(raw_process_list) ~= "string" then
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local process_list = raw_process_list:lower()
|
|
||||||
|
local process_list = result.stdout:lower()
|
||||||
for line in process_list:gmatch("[^\n]+") do
|
for line in process_list:gmatch("[^\n]+") do
|
||||||
if is_windows() then
|
if is_windows() then
|
||||||
local image = line:match('^"([^"]+)","')
|
local image = line:match('^"([^"]+)","')
|
||||||
@@ -86,80 +80,8 @@ function M.create(ctx)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function process_scan_command()
|
|
||||||
if is_windows() then
|
|
||||||
return { "tasklist", "/FO", "CSV", "/NH" }
|
|
||||||
end
|
|
||||||
return { "ps", "-A", "-o", "args=" }
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_subminer_process_running()
|
|
||||||
local result = mp.command_native({
|
|
||||||
name = "subprocess",
|
|
||||||
args = process_scan_command(),
|
|
||||||
playback_only = false,
|
|
||||||
capture_stdout = true,
|
|
||||||
capture_stderr = false,
|
|
||||||
})
|
|
||||||
if not result or result.status ~= 0 then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
return process_list_has_subminer(result.stdout)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function flush_app_running_waiters(value)
|
|
||||||
local waiters = app_running_waiters
|
|
||||||
app_running_waiters = {}
|
|
||||||
for _, waiter in ipairs(waiters) do
|
|
||||||
waiter(value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_subminer_app_running_async(callback, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
local force_refresh = opts.force_refresh == true
|
|
||||||
local now = now_seconds()
|
|
||||||
if not force_refresh and app_running_cache_value ~= nil and app_running_cache_time ~= nil then
|
|
||||||
if (now - app_running_cache_time) <= APP_RUNNING_CACHE_TTL_SECONDS then
|
|
||||||
callback(app_running_cache_value)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
app_running_waiters[#app_running_waiters + 1] = callback
|
|
||||||
if app_running_check_inflight then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
app_running_check_inflight = true
|
|
||||||
|
|
||||||
mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = process_scan_command(),
|
|
||||||
playback_only = false,
|
|
||||||
capture_stdout = true,
|
|
||||||
capture_stderr = false,
|
|
||||||
}, function(success, result)
|
|
||||||
app_running_check_inflight = false
|
|
||||||
local running = false
|
|
||||||
if success and result and result.status == 0 then
|
|
||||||
running = process_list_has_subminer(result.stdout)
|
|
||||||
end
|
|
||||||
app_running_cache_value = running
|
|
||||||
app_running_cache_time = now_seconds()
|
|
||||||
flush_app_running_waiters(running)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_subminer_app_running()
|
local function is_subminer_app_running()
|
||||||
local running = is_subminer_process_running()
|
return is_subminer_process_running()
|
||||||
app_running_cache_value = running
|
|
||||||
app_running_cache_time = now_seconds()
|
|
||||||
return running
|
|
||||||
end
|
|
||||||
|
|
||||||
local function set_subminer_app_running_cache(running)
|
|
||||||
app_running_cache_value = running == true
|
|
||||||
app_running_cache_time = now_seconds()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function detect_backend()
|
local function detect_backend()
|
||||||
@@ -201,8 +123,6 @@ function M.create(ctx)
|
|||||||
default_socket_path = default_socket_path,
|
default_socket_path = default_socket_path,
|
||||||
is_subminer_process_running = is_subminer_process_running,
|
is_subminer_process_running = is_subminer_process_running,
|
||||||
is_subminer_app_running = is_subminer_app_running,
|
is_subminer_app_running = is_subminer_app_running,
|
||||||
is_subminer_app_running_async = is_subminer_app_running_async,
|
|
||||||
set_subminer_app_running_cache = set_subminer_app_running_cache,
|
|
||||||
detect_backend = detect_backend,
|
detect_backend = detect_backend,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -33,15 +33,6 @@ function M.create(ctx)
|
|||||||
return b .. g .. r
|
return b .. g .. r
|
||||||
end
|
end
|
||||||
|
|
||||||
local function sanitize_hover_ass_color(input, fallback_rgb)
|
|
||||||
local fallback = fix_ass_color(fallback_rgb or DEFAULT_HOVER_COLOR, DEFAULT_HOVER_COLOR)
|
|
||||||
local converted = fix_ass_color(input, fallback)
|
|
||||||
if converted == "000000" then
|
|
||||||
return fallback
|
|
||||||
end
|
|
||||||
return converted
|
|
||||||
end
|
|
||||||
|
|
||||||
local function escape_ass_text(text)
|
local function escape_ass_text(text)
|
||||||
return (text or ""):gsub("\\", "\\\\"):gsub("{", "\\{"):gsub("}", "\\}"):gsub("\n", "\\N")
|
return (text or ""):gsub("\\", "\\\\"):gsub("{", "\\{"):gsub("}", "\\}"):gsub("\n", "\\N")
|
||||||
end
|
end
|
||||||
@@ -100,7 +91,7 @@ function M.create(ctx)
|
|||||||
border = sub_border_size * window_scale,
|
border = sub_border_size * window_scale,
|
||||||
shadow = sub_shadow_offset * window_scale,
|
shadow = sub_shadow_offset * window_scale,
|
||||||
base_color = fix_ass_color(mp.get_property("sub-color"), DEFAULT_HOVER_BASE_COLOR),
|
base_color = fix_ass_color(mp.get_property("sub-color"), DEFAULT_HOVER_BASE_COLOR),
|
||||||
hover_color = sanitize_hover_ass_color(nil, DEFAULT_HOVER_COLOR),
|
hover_color = fix_ass_color(DEFAULT_HOVER_COLOR, DEFAULT_HOVER_COLOR),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -249,22 +240,36 @@ function M.create(ctx)
|
|||||||
raw_close_idx = #raw_ass + 1
|
raw_close_idx = #raw_ass + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local before = raw_ass:sub(1, raw_open_idx - 1)
|
|
||||||
local hovered = raw_ass:sub(raw_open_idx, raw_close_idx - 1)
|
|
||||||
local after = raw_ass:sub(raw_close_idx)
|
|
||||||
local hover_suffix = string.format("\\1c&H%s&", hover_color)
|
|
||||||
|
|
||||||
-- Keep hover foreground stable even when inline ASS override tags (\1c/\c/\r) appear inside token.
|
|
||||||
hovered = hovered:gsub("{([^}]*)}", function(inner)
|
|
||||||
if inner:find("\\1c&H", 1, true) or inner:find("\\c&H", 1, true) or inner:find("\\r", 1, true) then
|
|
||||||
return "{" .. inner .. hover_suffix .. "}"
|
|
||||||
end
|
|
||||||
return "{" .. inner .. "}"
|
|
||||||
end)
|
|
||||||
|
|
||||||
local open_tag = string.format("{\\1c&H%s&}", hover_color)
|
local open_tag = string.format("{\\1c&H%s&}", hover_color)
|
||||||
local close_tag = string.format("{\\1c&H%s&}", base_color)
|
local close_tag = string.format("{\\1c&H%s&}", base_color)
|
||||||
return before .. open_tag .. hovered .. close_tag .. after
|
local changes = {
|
||||||
|
{ idx = raw_open_idx, tag = open_tag },
|
||||||
|
{ idx = raw_close_idx, tag = close_tag },
|
||||||
|
}
|
||||||
|
table.sort(changes, function(a, b)
|
||||||
|
return a.idx < b.idx
|
||||||
|
end)
|
||||||
|
|
||||||
|
local output = {}
|
||||||
|
local cursor = 1
|
||||||
|
for _, change in ipairs(changes) do
|
||||||
|
if change.idx > #raw_ass + 1 then
|
||||||
|
change.idx = #raw_ass + 1
|
||||||
|
end
|
||||||
|
if change.idx < 1 then
|
||||||
|
change.idx = 1
|
||||||
|
end
|
||||||
|
if change.idx > cursor then
|
||||||
|
output[#output + 1] = raw_ass:sub(cursor, change.idx - 1)
|
||||||
|
end
|
||||||
|
output[#output + 1] = change.tag
|
||||||
|
cursor = change.idx
|
||||||
|
end
|
||||||
|
if cursor <= #raw_ass then
|
||||||
|
output[#output + 1] = raw_ass:sub(cursor)
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(output)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function build_hover_subtitle_content(payload)
|
local function build_hover_subtitle_content(payload)
|
||||||
@@ -289,7 +294,7 @@ function M.create(ctx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local metrics = resolve_metrics()
|
local metrics = resolve_metrics()
|
||||||
local hover_color = sanitize_hover_ass_color(payload.colors and payload.colors.hover or nil, DEFAULT_HOVER_COLOR)
|
local hover_color = fix_ass_color(payload.colors and payload.colors.hover or nil, metrics.hover_color)
|
||||||
local base_color = fix_ass_color(payload.colors and payload.colors.base or nil, metrics.base_color)
|
local base_color = fix_ass_color(payload.colors and payload.colors.base or nil, metrics.base_color)
|
||||||
return inject_hover_color_to_ass(source_ass, plain_map, hover_start, hover_end, hover_color, base_color)
|
return inject_hover_color_to_ass(source_ass, plain_map, hover_start, hover_end, hover_color, base_color)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
local M = {}
|
|
||||||
|
|
||||||
function M.init()
|
|
||||||
require("bootstrap").init()
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
@@ -4,6 +4,8 @@ function M.create(ctx)
|
|||||||
local mp = ctx.mp
|
local mp = ctx.mp
|
||||||
local opts = ctx.opts
|
local opts = ctx.opts
|
||||||
local state = ctx.state
|
local state = ctx.state
|
||||||
|
local environment = ctx.environment
|
||||||
|
local binary = ctx.binary
|
||||||
local options_helper = ctx.options_helper
|
local options_helper = ctx.options_helper
|
||||||
local process = ctx.process
|
local process = ctx.process
|
||||||
local aniskip = ctx.aniskip
|
local aniskip = ctx.aniskip
|
||||||
@@ -11,57 +13,36 @@ function M.create(ctx)
|
|||||||
local subminer_log = ctx.log.subminer_log
|
local subminer_log = ctx.log.subminer_log
|
||||||
local show_osd = ctx.log.show_osd
|
local show_osd = ctx.log.show_osd
|
||||||
|
|
||||||
local function schedule_aniskip_fetch(trigger_source, delay_seconds)
|
|
||||||
local delay = tonumber(delay_seconds) or 0
|
|
||||||
mp.add_timeout(delay, function()
|
|
||||||
aniskip.fetch_aniskip_for_current_media(trigger_source)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function resolve_auto_start_enabled()
|
|
||||||
local raw_auto_start = opts.auto_start
|
|
||||||
if raw_auto_start == nil then
|
|
||||||
raw_auto_start = opts.auto_start_overlay
|
|
||||||
end
|
|
||||||
if raw_auto_start == nil then
|
|
||||||
raw_auto_start = opts["auto-start"]
|
|
||||||
end
|
|
||||||
return options_helper.coerce_bool(raw_auto_start, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function on_file_loaded()
|
local function on_file_loaded()
|
||||||
aniskip.clear_aniskip_state()
|
if not environment.is_subminer_app_running() then
|
||||||
process.disarm_auto_play_ready_gate()
|
aniskip.clear_aniskip_state()
|
||||||
|
subminer_log("debug", "lifecycle", "Skipping file load hooks: SubMiner app not running")
|
||||||
local should_auto_start = resolve_auto_start_enabled()
|
return true
|
||||||
if should_auto_start then
|
|
||||||
if not process.has_matching_mpv_ipc_socket(opts.socket_path) then
|
|
||||||
subminer_log(
|
|
||||||
"info",
|
|
||||||
"lifecycle",
|
|
||||||
"Skipping auto-start: input-ipc-server does not match configured socket_path"
|
|
||||||
)
|
|
||||||
schedule_aniskip_fetch("file-loaded", 0)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
process.start_overlay({
|
|
||||||
auto_start_trigger = true,
|
|
||||||
socket_path = opts.socket_path,
|
|
||||||
})
|
|
||||||
-- Give the overlay process a moment to initialize before querying AniSkip.
|
|
||||||
schedule_aniskip_fetch("overlay-start", 0.8)
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
schedule_aniskip_fetch("file-loaded", 0)
|
aniskip.clear_aniskip_state()
|
||||||
|
aniskip.fetch_aniskip_for_current_media()
|
||||||
|
state.binary_path = binary.find_binary()
|
||||||
|
if state.binary_path then
|
||||||
|
state.binary_available = true
|
||||||
|
subminer_log("info", "lifecycle", "SubMiner ready (binary: " .. state.binary_path .. ")")
|
||||||
|
local should_auto_start = options_helper.coerce_bool(opts.auto_start, false)
|
||||||
|
if should_auto_start then
|
||||||
|
process.start_overlay()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
state.binary_available = false
|
||||||
|
subminer_log("warn", "binary", "SubMiner binary not found - overlay features disabled")
|
||||||
|
if opts.binary_path ~= "" then
|
||||||
|
subminer_log("warn", "binary", "Configured path '" .. opts.binary_path .. "' does not exist")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function on_shutdown()
|
local function on_shutdown()
|
||||||
aniskip.clear_aniskip_state()
|
aniskip.clear_aniskip_state()
|
||||||
hover.clear_hover_overlay()
|
hover.clear_hover_overlay()
|
||||||
process.disarm_auto_play_ready_gate()
|
if (state.overlay_running or state.texthooker_running) and state.binary_available then
|
||||||
if state.overlay_running or state.texthooker_running then
|
|
||||||
subminer_log("info", "lifecycle", "mpv shutting down, stopping SubMiner process")
|
subminer_log("info", "lifecycle", "mpv shutting down, stopping SubMiner process")
|
||||||
show_osd("Shutting down...")
|
show_osd("Shutting down...")
|
||||||
process.stop_overlay()
|
process.stop_overlay()
|
||||||
@@ -71,22 +52,11 @@ function M.create(ctx)
|
|||||||
local function register_lifecycle_hooks()
|
local function register_lifecycle_hooks()
|
||||||
mp.register_event("file-loaded", on_file_loaded)
|
mp.register_event("file-loaded", on_file_loaded)
|
||||||
mp.register_event("shutdown", on_shutdown)
|
mp.register_event("shutdown", on_shutdown)
|
||||||
mp.register_event("file-loaded", function()
|
mp.register_event("file-loaded", hover.clear_hover_overlay)
|
||||||
hover.clear_hover_overlay()
|
mp.register_event("end-file", hover.clear_hover_overlay)
|
||||||
end)
|
mp.register_event("shutdown", hover.clear_hover_overlay)
|
||||||
mp.register_event("end-file", function()
|
mp.register_event("end-file", aniskip.clear_aniskip_state)
|
||||||
process.disarm_auto_play_ready_gate()
|
mp.register_event("shutdown", aniskip.clear_aniskip_state)
|
||||||
hover.clear_hover_overlay()
|
|
||||||
end)
|
|
||||||
mp.register_event("shutdown", function()
|
|
||||||
hover.clear_hover_overlay()
|
|
||||||
end)
|
|
||||||
mp.register_event("end-file", function()
|
|
||||||
aniskip.clear_aniskip_state()
|
|
||||||
end)
|
|
||||||
mp.register_event("shutdown", function()
|
|
||||||
aniskip.clear_aniskip_state()
|
|
||||||
end)
|
|
||||||
mp.add_hook("on_unload", 10, function()
|
mp.add_hook("on_unload", 10, function()
|
||||||
hover.clear_hover_overlay()
|
hover.clear_hover_overlay()
|
||||||
aniskip.clear_aniskip_state()
|
aniskip.clear_aniskip_state()
|
||||||
|
|||||||
@@ -45,14 +45,7 @@ function M.create(ctx)
|
|||||||
|
|
||||||
local function show_osd(message)
|
local function show_osd(message)
|
||||||
if opts.osd_messages then
|
if opts.osd_messages then
|
||||||
local payload = "SubMiner: " .. message
|
mp.osd_message("SubMiner: " .. message, 3)
|
||||||
local sent = false
|
|
||||||
if type(mp.osd_message) == "function" then
|
|
||||||
sent = pcall(mp.osd_message, payload, 3)
|
|
||||||
end
|
|
||||||
if not sent and type(mp.commandv) == "function" then
|
|
||||||
pcall(mp.commandv, "show-text", payload, "3000")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,9 @@
|
|||||||
local mp = require("mp")
|
local mp = require("mp")
|
||||||
|
|
||||||
local function current_script_dir()
|
local script_dir = mp.get_script_directory() or "."
|
||||||
if type(mp.get_script_directory) == "function" then
|
|
||||||
local from_mpv = mp.get_script_directory()
|
|
||||||
if type(from_mpv) == "string" and from_mpv ~= "" then
|
|
||||||
return from_mpv
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local source = debug.getinfo(1, "S").source or ""
|
|
||||||
if source:sub(1, 1) == "@" then
|
|
||||||
local full = source:sub(2)
|
|
||||||
return full:match("^(.*)[/\\][^/\\]+$") or "."
|
|
||||||
end
|
|
||||||
return "."
|
|
||||||
end
|
|
||||||
|
|
||||||
local script_dir = current_script_dir()
|
|
||||||
local module_patterns = script_dir .. "/?.lua;" .. script_dir .. "/?/init.lua;"
|
local module_patterns = script_dir .. "/?.lua;" .. script_dir .. "/?/init.lua;"
|
||||||
if not package.path:find(module_patterns, 1, true) then
|
if not package.path:find(module_patterns, 1, true) then
|
||||||
package.path = module_patterns .. package.path
|
package.path = module_patterns .. package.path
|
||||||
end
|
end
|
||||||
|
|
||||||
require("init").init()
|
require("bootstrap").init()
|
||||||
|
|||||||
@@ -8,36 +8,18 @@ function M.create(ctx)
|
|||||||
local ui = ctx.ui
|
local ui = ctx.ui
|
||||||
|
|
||||||
local function register_script_messages()
|
local function register_script_messages()
|
||||||
mp.register_script_message("subminer-start", function(...)
|
mp.register_script_message("subminer-start", process.start_overlay_from_script_message)
|
||||||
process.start_overlay_from_script_message(...)
|
mp.register_script_message("subminer-stop", process.stop_overlay)
|
||||||
end)
|
mp.register_script_message("subminer-toggle", process.toggle_overlay)
|
||||||
mp.register_script_message("subminer-stop", function()
|
mp.register_script_message("subminer-toggle-invisible", process.toggle_invisible_overlay)
|
||||||
process.stop_overlay()
|
mp.register_script_message("subminer-show-invisible", process.show_invisible_overlay)
|
||||||
end)
|
mp.register_script_message("subminer-hide-invisible", process.hide_invisible_overlay)
|
||||||
mp.register_script_message("subminer-toggle", function()
|
mp.register_script_message("subminer-menu", ui.show_menu)
|
||||||
process.toggle_overlay()
|
mp.register_script_message("subminer-options", process.open_options)
|
||||||
end)
|
mp.register_script_message("subminer-restart", process.restart_overlay)
|
||||||
mp.register_script_message("subminer-menu", function()
|
mp.register_script_message("subminer-status", process.check_status)
|
||||||
ui.show_menu()
|
mp.register_script_message("subminer-aniskip-refresh", aniskip.fetch_aniskip_for_current_media)
|
||||||
end)
|
mp.register_script_message("subminer-skip-intro", aniskip.skip_intro_now)
|
||||||
mp.register_script_message("subminer-options", function()
|
|
||||||
process.open_options()
|
|
||||||
end)
|
|
||||||
mp.register_script_message("subminer-restart", function()
|
|
||||||
process.restart_overlay()
|
|
||||||
end)
|
|
||||||
mp.register_script_message("subminer-status", function()
|
|
||||||
process.check_status()
|
|
||||||
end)
|
|
||||||
mp.register_script_message("subminer-autoplay-ready", function()
|
|
||||||
process.notify_auto_play_ready()
|
|
||||||
end)
|
|
||||||
mp.register_script_message("subminer-aniskip-refresh", function()
|
|
||||||
aniskip.fetch_aniskip_for_current_media("script-message")
|
|
||||||
end)
|
|
||||||
mp.register_script_message("subminer-skip-intro", function()
|
|
||||||
aniskip.skip_intro_now()
|
|
||||||
end)
|
|
||||||
mp.register_script_message(hover.HOVER_MESSAGE_NAME, function(payload_json)
|
mp.register_script_message(hover.HOVER_MESSAGE_NAME, function(payload_json)
|
||||||
hover.handle_hover_message(payload_json)
|
hover.handle_hover_message(payload_json)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ function M.load(options_lib, default_socket_path)
|
|||||||
texthooker_port = 5174,
|
texthooker_port = 5174,
|
||||||
backend = "auto",
|
backend = "auto",
|
||||||
auto_start = true,
|
auto_start = true,
|
||||||
auto_start_visible_overlay = true,
|
auto_start_overlay = false,
|
||||||
auto_start_pause_until_ready = true,
|
auto_start_visible_overlay = false,
|
||||||
|
auto_start_invisible_overlay = "platform-default",
|
||||||
osd_messages = true,
|
osd_messages = true,
|
||||||
log_level = "info",
|
log_level = "info",
|
||||||
aniskip_enabled = true,
|
aniskip_enabled = true,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local OVERLAY_START_RETRY_DELAY_SECONDS = 0.2
|
|
||||||
local OVERLAY_START_MAX_ATTEMPTS = 6
|
|
||||||
local AUTO_PLAY_READY_TIMEOUT_SECONDS = 15
|
|
||||||
|
|
||||||
function M.create(ctx)
|
function M.create(ctx)
|
||||||
local mp = ctx.mp
|
local mp = ctx.mp
|
||||||
local opts = ctx.opts
|
local opts = ctx.opts
|
||||||
@@ -15,42 +11,6 @@ function M.create(ctx)
|
|||||||
local show_osd = ctx.log.show_osd
|
local show_osd = ctx.log.show_osd
|
||||||
local normalize_log_level = ctx.log.normalize_log_level
|
local normalize_log_level = ctx.log.normalize_log_level
|
||||||
|
|
||||||
local function resolve_visible_overlay_startup()
|
|
||||||
local raw_visible_overlay = opts.auto_start_visible_overlay
|
|
||||||
if raw_visible_overlay == nil then
|
|
||||||
raw_visible_overlay = opts["auto-start-visible-overlay"]
|
|
||||||
end
|
|
||||||
return options_helper.coerce_bool(raw_visible_overlay, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function resolve_pause_until_ready()
|
|
||||||
local raw_pause_until_ready = opts.auto_start_pause_until_ready
|
|
||||||
if raw_pause_until_ready == nil then
|
|
||||||
raw_pause_until_ready = opts["auto-start-pause-until-ready"]
|
|
||||||
end
|
|
||||||
return options_helper.coerce_bool(raw_pause_until_ready, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function normalize_socket_path(path)
|
|
||||||
if type(path) ~= "string" then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local trimmed = path:match("^%s*(.-)%s*$")
|
|
||||||
if trimmed == "" then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
return trimmed
|
|
||||||
end
|
|
||||||
|
|
||||||
local function has_matching_mpv_ipc_socket(target_socket_path)
|
|
||||||
local expected_socket = normalize_socket_path(target_socket_path or opts.socket_path)
|
|
||||||
local active_socket = normalize_socket_path(mp.get_property("input-ipc-server"))
|
|
||||||
if expected_socket == nil or active_socket == nil then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
return expected_socket == active_socket
|
|
||||||
end
|
|
||||||
|
|
||||||
local function resolve_backend(override_backend)
|
local function resolve_backend(override_backend)
|
||||||
local selected = override_backend
|
local selected = override_backend
|
||||||
if selected == nil or selected == "" then
|
if selected == nil or selected == "" then
|
||||||
@@ -62,54 +22,6 @@ function M.create(ctx)
|
|||||||
return selected
|
return selected
|
||||||
end
|
end
|
||||||
|
|
||||||
local function clear_auto_play_ready_timeout()
|
|
||||||
local timeout = state.auto_play_ready_timeout
|
|
||||||
if timeout and timeout.kill then
|
|
||||||
timeout:kill()
|
|
||||||
end
|
|
||||||
state.auto_play_ready_timeout = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function disarm_auto_play_ready_gate()
|
|
||||||
clear_auto_play_ready_timeout()
|
|
||||||
state.auto_play_ready_gate_armed = false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function release_auto_play_ready_gate(reason)
|
|
||||||
if not state.auto_play_ready_gate_armed then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
disarm_auto_play_ready_gate()
|
|
||||||
mp.set_property_native("pause", false)
|
|
||||||
show_osd("Subtitle annotations loaded")
|
|
||||||
subminer_log("info", "process", "Resuming playback after startup gate: " .. tostring(reason or "ready"))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function arm_auto_play_ready_gate()
|
|
||||||
if state.auto_play_ready_gate_armed then
|
|
||||||
clear_auto_play_ready_timeout()
|
|
||||||
end
|
|
||||||
state.auto_play_ready_gate_armed = true
|
|
||||||
mp.set_property_native("pause", true)
|
|
||||||
show_osd("Loading subtitle annotations...")
|
|
||||||
subminer_log("info", "process", "Pausing playback until SubMiner overlay/tokenization readiness signal")
|
|
||||||
state.auto_play_ready_timeout = mp.add_timeout(AUTO_PLAY_READY_TIMEOUT_SECONDS, function()
|
|
||||||
if not state.auto_play_ready_gate_armed then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
subminer_log(
|
|
||||||
"warn",
|
|
||||||
"process",
|
|
||||||
"Startup readiness signal timed out; resuming playback to avoid stalled pause"
|
|
||||||
)
|
|
||||||
release_auto_play_ready_gate("timeout")
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function notify_auto_play_ready()
|
|
||||||
release_auto_play_ready_gate("tokenization-ready")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function build_command_args(action, overrides)
|
local function build_command_args(action, overrides)
|
||||||
overrides = overrides or {}
|
overrides = overrides or {}
|
||||||
local args = { state.binary_path }
|
local args = { state.binary_path }
|
||||||
@@ -121,7 +33,8 @@ function M.create(ctx)
|
|||||||
table.insert(args, log_level)
|
table.insert(args, log_level)
|
||||||
end
|
end
|
||||||
|
|
||||||
if action == "start" then
|
local needs_start_context = action == "start"
|
||||||
|
if needs_start_context then
|
||||||
local backend = resolve_backend(overrides.backend)
|
local backend = resolve_backend(overrides.backend)
|
||||||
if backend and backend ~= "" then
|
if backend and backend ~= "" then
|
||||||
table.insert(args, "--backend")
|
table.insert(args, "--backend")
|
||||||
@@ -131,37 +44,22 @@ function M.create(ctx)
|
|||||||
local socket_path = overrides.socket_path or opts.socket_path
|
local socket_path = overrides.socket_path or opts.socket_path
|
||||||
table.insert(args, "--socket")
|
table.insert(args, "--socket")
|
||||||
table.insert(args, socket_path)
|
table.insert(args, socket_path)
|
||||||
|
|
||||||
-- Keep auto-start --start requests idempotent for second-instance handling.
|
|
||||||
-- Visibility is applied as a separate control command after startup.
|
|
||||||
if overrides.auto_start_trigger ~= true then
|
|
||||||
local should_show_visible = resolve_visible_overlay_startup()
|
|
||||||
if should_show_visible then
|
|
||||||
table.insert(args, "--show-visible-overlay")
|
|
||||||
else
|
|
||||||
table.insert(args, "--hide-visible-overlay")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return args
|
return args
|
||||||
end
|
end
|
||||||
|
|
||||||
local function run_control_command_async(action, overrides, callback)
|
local function run_control_command(action)
|
||||||
local args = build_command_args(action, overrides)
|
local args = build_command_args(action)
|
||||||
subminer_log("debug", "process", "Control command: " .. table.concat(args, " "))
|
subminer_log("debug", "process", "Control command: " .. table.concat(args, " "))
|
||||||
mp.command_native_async({
|
local result = mp.command_native({
|
||||||
name = "subprocess",
|
name = "subprocess",
|
||||||
args = args,
|
args = args,
|
||||||
playback_only = false,
|
playback_only = false,
|
||||||
capture_stdout = true,
|
capture_stdout = true,
|
||||||
capture_stderr = true,
|
capture_stderr = true,
|
||||||
}, function(success, result, error)
|
})
|
||||||
local ok = success and (result == nil or result.status == 0)
|
return result and result.status == 0
|
||||||
if callback then
|
|
||||||
callback(ok, result, error)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function parse_start_script_message_overrides(...)
|
local function parse_start_script_message_overrides(...)
|
||||||
@@ -193,6 +91,48 @@ function M.create(ctx)
|
|||||||
return overrides
|
return overrides
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function resolve_visible_overlay_startup()
|
||||||
|
local visible = options_helper.coerce_bool(opts.auto_start_visible_overlay, false)
|
||||||
|
if options_helper.coerce_bool(opts.auto_start_overlay, false) then
|
||||||
|
visible = true
|
||||||
|
end
|
||||||
|
return visible
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resolve_invisible_overlay_startup()
|
||||||
|
local raw = opts.auto_start_invisible_overlay
|
||||||
|
if type(raw) == "boolean" then
|
||||||
|
return raw
|
||||||
|
end
|
||||||
|
|
||||||
|
local mode = type(raw) == "string" and raw:lower() or "platform-default"
|
||||||
|
if mode == "visible" or mode == "show" or mode == "yes" or mode == "true" or mode == "on" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if mode == "hidden" or mode == "hide" or mode == "no" or mode == "false" or mode == "off" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return not environment.is_linux()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply_startup_overlay_preferences()
|
||||||
|
local should_show_visible = resolve_visible_overlay_startup()
|
||||||
|
local should_show_invisible = resolve_invisible_overlay_startup()
|
||||||
|
|
||||||
|
local visible_action = should_show_visible and "show-visible-overlay" or "hide-visible-overlay"
|
||||||
|
if not run_control_command(visible_action) then
|
||||||
|
subminer_log("warn", "process", "Failed to apply visible startup action: " .. visible_action)
|
||||||
|
end
|
||||||
|
|
||||||
|
local invisible_action = should_show_invisible and "show-invisible-overlay" or "hide-invisible-overlay"
|
||||||
|
if not run_control_command(invisible_action) then
|
||||||
|
subminer_log("warn", "process", "Failed to apply invisible startup action: " .. invisible_action)
|
||||||
|
end
|
||||||
|
|
||||||
|
state.invisible_overlay_visible = should_show_invisible
|
||||||
|
end
|
||||||
|
|
||||||
local function build_texthooker_args()
|
local function build_texthooker_args()
|
||||||
local args = { state.binary_path, "--texthooker", "--port", tostring(opts.texthooker_port) }
|
local args = { state.binary_path, "--texthooker", "--port", tostring(opts.texthooker_port) }
|
||||||
local log_level = normalize_log_level(opts.log_level)
|
local log_level = normalize_log_level(opts.log_level)
|
||||||
@@ -208,7 +148,6 @@ function M.create(ctx)
|
|||||||
callback()
|
callback()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if state.texthooker_running then
|
if state.texthooker_running then
|
||||||
callback()
|
callback()
|
||||||
return
|
return
|
||||||
@@ -227,69 +166,35 @@ function M.create(ctx)
|
|||||||
}, function(success, result, error)
|
}, function(success, result, error)
|
||||||
if not success or (result and result.status ~= 0) then
|
if not success or (result and result.status ~= 0) then
|
||||||
state.texthooker_running = false
|
state.texthooker_running = false
|
||||||
subminer_log(
|
subminer_log("warn", "texthooker", "Texthooker process exited unexpectedly: " .. (error or (result and result.stderr) or "unknown error"))
|
||||||
"warn",
|
|
||||||
"texthooker",
|
|
||||||
"Texthooker process exited unexpectedly: " .. (error or (result and result.stderr) or "unknown error")
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Start overlay immediately; overlay start path retries on readiness failures.
|
mp.add_timeout(0.35, callback)
|
||||||
callback()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function start_overlay(overrides)
|
local function start_overlay(overrides)
|
||||||
overrides = overrides or {}
|
|
||||||
|
|
||||||
if not binary.ensure_binary_available() then
|
if not binary.ensure_binary_available() then
|
||||||
subminer_log("error", "binary", "SubMiner binary not found")
|
subminer_log("error", "binary", "SubMiner binary not found")
|
||||||
show_osd("Error: binary not found")
|
show_osd("Error: binary not found")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if state.overlay_running then
|
if state.overlay_running then
|
||||||
if overrides.auto_start_trigger == true then
|
|
||||||
subminer_log("debug", "process", "Auto-start ignored because overlay is already running")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
subminer_log("info", "process", "Overlay already running")
|
subminer_log("info", "process", "Overlay already running")
|
||||||
show_osd("Already running")
|
show_osd("Already running")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
overrides = overrides or {}
|
||||||
local texthooker_enabled = overrides.texthooker_enabled
|
local texthooker_enabled = overrides.texthooker_enabled
|
||||||
if texthooker_enabled == nil then
|
if texthooker_enabled == nil then
|
||||||
texthooker_enabled = opts.texthooker_enabled
|
texthooker_enabled = opts.texthooker_enabled
|
||||||
end
|
end
|
||||||
local socket_path = overrides.socket_path or opts.socket_path
|
|
||||||
local should_pause_until_ready = (
|
|
||||||
overrides.auto_start_trigger == true
|
|
||||||
and resolve_visible_overlay_startup()
|
|
||||||
and resolve_pause_until_ready()
|
|
||||||
and has_matching_mpv_ipc_socket(socket_path)
|
|
||||||
)
|
|
||||||
if should_pause_until_ready then
|
|
||||||
arm_auto_play_ready_gate()
|
|
||||||
else
|
|
||||||
disarm_auto_play_ready_gate()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function launch_overlay_with_retry(attempt)
|
local function launch_overlay()
|
||||||
local args = build_command_args("start", overrides)
|
local args = build_command_args("start", overrides)
|
||||||
if attempt == 1 then
|
subminer_log("info", "process", "Starting overlay: " .. table.concat(args, " "))
|
||||||
subminer_log("info", "process", "Starting overlay: " .. table.concat(args, " "))
|
show_osd("Starting...")
|
||||||
else
|
|
||||||
subminer_log(
|
|
||||||
"warn",
|
|
||||||
"process",
|
|
||||||
"Retrying overlay start (attempt " .. tostring(attempt) .. "): " .. table.concat(args, " ")
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if attempt == 1 then
|
|
||||||
show_osd("Starting...")
|
|
||||||
end
|
|
||||||
state.overlay_running = true
|
state.overlay_running = true
|
||||||
|
|
||||||
mp.command_native_async({
|
mp.command_native_async({
|
||||||
@@ -300,39 +205,21 @@ function M.create(ctx)
|
|||||||
capture_stderr = true,
|
capture_stderr = true,
|
||||||
}, function(success, result, error)
|
}, function(success, result, error)
|
||||||
if not success or (result and result.status ~= 0) then
|
if not success or (result and result.status ~= 0) then
|
||||||
local reason = error or (result and result.stderr) or "unknown error"
|
|
||||||
if attempt < OVERLAY_START_MAX_ATTEMPTS then
|
|
||||||
mp.add_timeout(OVERLAY_START_RETRY_DELAY_SECONDS, function()
|
|
||||||
launch_overlay_with_retry(attempt + 1)
|
|
||||||
end)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
state.overlay_running = false
|
state.overlay_running = false
|
||||||
subminer_log("error", "process", "Overlay start failed after retries: " .. reason)
|
subminer_log("error", "process", "Overlay start failed: " .. (error or (result and result.stderr) or "unknown error"))
|
||||||
show_osd("Overlay start failed")
|
show_osd("Overlay start failed")
|
||||||
release_auto_play_ready_gate("overlay-start-failed")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if overrides.auto_start_trigger == true then
|
|
||||||
local visibility_action = resolve_visible_overlay_startup()
|
|
||||||
and "show-visible-overlay"
|
|
||||||
or "hide-visible-overlay"
|
|
||||||
run_control_command_async(visibility_action, {
|
|
||||||
log_level = overrides.log_level,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
mp.add_timeout(0.6, function()
|
||||||
|
apply_startup_overlay_preferences()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
if texthooker_enabled then
|
if texthooker_enabled then
|
||||||
ensure_texthooker_running(function()
|
ensure_texthooker_running(launch_overlay)
|
||||||
launch_overlay_with_retry(1)
|
|
||||||
end)
|
|
||||||
else
|
else
|
||||||
launch_overlay_with_retry(1)
|
launch_overlay()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -348,21 +235,23 @@ function M.create(ctx)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
run_control_command_async("stop", nil, function(ok, result)
|
local args = build_command_args("stop")
|
||||||
if ok then
|
subminer_log("info", "process", "Stopping overlay: " .. table.concat(args, " "))
|
||||||
subminer_log("info", "process", "Overlay stopped")
|
local result = mp.command_native({
|
||||||
else
|
name = "subprocess",
|
||||||
subminer_log(
|
args = args,
|
||||||
"warn",
|
playback_only = false,
|
||||||
"process",
|
capture_stdout = true,
|
||||||
"Stop command returned non-zero status: " .. tostring(result and result.status or "unknown")
|
capture_stderr = true,
|
||||||
)
|
})
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
state.overlay_running = false
|
state.overlay_running = false
|
||||||
state.texthooker_running = false
|
state.texthooker_running = false
|
||||||
disarm_auto_play_ready_gate()
|
if result.status == 0 then
|
||||||
|
subminer_log("info", "process", "Overlay stopped")
|
||||||
|
else
|
||||||
|
subminer_log("warn", "process", "Stop command returned non-zero status: " .. tostring(result.status))
|
||||||
|
end
|
||||||
show_osd("Stopped")
|
show_osd("Stopped")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -372,31 +261,115 @@ function M.create(ctx)
|
|||||||
show_osd("Error: binary not found")
|
show_osd("Error: binary not found")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local args = build_command_args("toggle")
|
||||||
run_control_command_async("toggle", nil, function(ok)
|
subminer_log("info", "process", "Toggling overlay: " .. table.concat(args, " "))
|
||||||
if not ok then
|
local result = mp.command_native({
|
||||||
subminer_log("warn", "process", "Toggle command failed")
|
name = "subprocess",
|
||||||
show_osd("Toggle failed")
|
args = args,
|
||||||
end
|
playback_only = false,
|
||||||
end)
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
})
|
||||||
|
if result and result.status ~= 0 then
|
||||||
|
subminer_log("warn", "process", "Toggle command failed")
|
||||||
|
show_osd("Toggle failed")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function open_options()
|
local function toggle_invisible_overlay()
|
||||||
if not binary.ensure_binary_available() then
|
if not binary.ensure_binary_available() then
|
||||||
subminer_log("error", "binary", "SubMiner binary not found")
|
subminer_log("error", "binary", "SubMiner binary not found")
|
||||||
show_osd("Error: binary not found")
|
show_osd("Error: binary not found")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local args = build_command_args("toggle-invisible-overlay")
|
||||||
|
subminer_log("info", "process", "Toggling invisible overlay: " .. table.concat(args, " "))
|
||||||
|
local result = mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
})
|
||||||
|
if result and result.status ~= 0 then
|
||||||
|
subminer_log("warn", "process", "Invisible toggle command failed")
|
||||||
|
show_osd("Invisible toggle failed")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
state.invisible_overlay_visible = not state.invisible_overlay_visible
|
||||||
|
show_osd("Invisible overlay: " .. (state.invisible_overlay_visible and "visible" or "hidden"))
|
||||||
|
end
|
||||||
|
|
||||||
run_control_command_async("settings", nil, function(ok)
|
local function show_invisible_overlay()
|
||||||
if ok then
|
if not binary.ensure_binary_available() then
|
||||||
subminer_log("info", "process", "Options window opened")
|
subminer_log("error", "binary", "SubMiner binary not found")
|
||||||
show_osd("Options opened")
|
show_osd("Error: binary not found")
|
||||||
else
|
return
|
||||||
subminer_log("warn", "process", "Failed to open options")
|
end
|
||||||
show_osd("Failed to open options")
|
local args = build_command_args("show-invisible-overlay")
|
||||||
end
|
subminer_log("info", "process", "Showing invisible overlay: " .. table.concat(args, " "))
|
||||||
end)
|
local result = mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
})
|
||||||
|
if result and result.status ~= 0 then
|
||||||
|
subminer_log("warn", "process", "Show invisible command failed")
|
||||||
|
show_osd("Show invisible failed")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
state.invisible_overlay_visible = true
|
||||||
|
show_osd("Invisible overlay: visible")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hide_invisible_overlay()
|
||||||
|
if not binary.ensure_binary_available() then
|
||||||
|
subminer_log("error", "binary", "SubMiner binary not found")
|
||||||
|
show_osd("Error: binary not found")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local args = build_command_args("hide-invisible-overlay")
|
||||||
|
subminer_log("info", "process", "Hiding invisible overlay: " .. table.concat(args, " "))
|
||||||
|
local result = mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
})
|
||||||
|
if result and result.status ~= 0 then
|
||||||
|
subminer_log("warn", "process", "Hide invisible command failed")
|
||||||
|
show_osd("Hide invisible failed")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
state.invisible_overlay_visible = false
|
||||||
|
show_osd("Invisible overlay: hidden")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function open_options()
|
||||||
|
if not state.binary_available then
|
||||||
|
subminer_log("error", "binary", "SubMiner binary not found")
|
||||||
|
show_osd("Error: binary not found")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local args = build_command_args("settings")
|
||||||
|
subminer_log("info", "process", "Opening options: " .. table.concat(args, " "))
|
||||||
|
local result = mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
})
|
||||||
|
if result.status == 0 then
|
||||||
|
subminer_log("info", "process", "Options window opened")
|
||||||
|
show_osd("Options opened")
|
||||||
|
else
|
||||||
|
subminer_log("warn", "process", "Failed to open options")
|
||||||
|
show_osd("Failed to open options")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function restart_overlay()
|
local function restart_overlay()
|
||||||
@@ -409,70 +382,67 @@ function M.create(ctx)
|
|||||||
subminer_log("info", "process", "Restarting overlay...")
|
subminer_log("info", "process", "Restarting overlay...")
|
||||||
show_osd("Restarting...")
|
show_osd("Restarting...")
|
||||||
|
|
||||||
run_control_command_async("stop", nil, function()
|
local stop_args = build_command_args("stop")
|
||||||
state.overlay_running = false
|
mp.command_native({
|
||||||
state.texthooker_running = false
|
name = "subprocess",
|
||||||
disarm_auto_play_ready_gate()
|
args = stop_args,
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
})
|
||||||
|
|
||||||
ensure_texthooker_running(function()
|
state.overlay_running = false
|
||||||
local start_args = build_command_args("start")
|
state.texthooker_running = false
|
||||||
subminer_log("info", "process", "Starting overlay: " .. table.concat(start_args, " "))
|
|
||||||
|
|
||||||
state.overlay_running = true
|
ensure_texthooker_running(function()
|
||||||
mp.command_native_async({
|
local start_args = build_command_args("start")
|
||||||
name = "subprocess",
|
subminer_log("info", "process", "Starting overlay: " .. table.concat(start_args, " "))
|
||||||
args = start_args,
|
|
||||||
playback_only = false,
|
state.overlay_running = true
|
||||||
capture_stdout = true,
|
mp.command_native_async({
|
||||||
capture_stderr = true,
|
name = "subprocess",
|
||||||
}, function(success, result, error)
|
args = start_args,
|
||||||
if not success or (result and result.status ~= 0) then
|
playback_only = false,
|
||||||
state.overlay_running = false
|
capture_stdout = true,
|
||||||
subminer_log(
|
capture_stderr = true,
|
||||||
"error",
|
}, function(success, result, error)
|
||||||
"process",
|
if not success or (result and result.status ~= 0) then
|
||||||
"Overlay start failed: " .. (error or (result and result.stderr) or "unknown error")
|
state.overlay_running = false
|
||||||
)
|
subminer_log("error", "process", "Overlay start failed: " .. (error or (result and result.stderr) or "unknown error"))
|
||||||
show_osd("Restart failed")
|
show_osd("Restart failed")
|
||||||
else
|
else
|
||||||
show_osd("Restarted successfully")
|
show_osd("Restarted successfully")
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function check_status()
|
local function check_status()
|
||||||
if not binary.ensure_binary_available() then
|
if not state.binary_available then
|
||||||
show_osd("Status: binary not found")
|
show_osd("Status: binary not found")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local status = state.overlay_running and "running" or "stopped"
|
local status = state.overlay_running and "running" or "stopped"
|
||||||
show_osd("Status: overlay is " .. status)
|
show_osd("Status: overlay is " .. status)
|
||||||
subminer_log("info", "process", "Status check: overlay is " .. status)
|
subminer_log("info", "process", "Status check: overlay is " .. status)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function check_binary_available()
|
|
||||||
return binary.ensure_binary_available()
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
build_command_args = build_command_args,
|
build_command_args = build_command_args,
|
||||||
has_matching_mpv_ipc_socket = has_matching_mpv_ipc_socket,
|
run_control_command = run_control_command,
|
||||||
run_control_command_async = run_control_command_async,
|
|
||||||
parse_start_script_message_overrides = parse_start_script_message_overrides,
|
parse_start_script_message_overrides = parse_start_script_message_overrides,
|
||||||
|
apply_startup_overlay_preferences = apply_startup_overlay_preferences,
|
||||||
ensure_texthooker_running = ensure_texthooker_running,
|
ensure_texthooker_running = ensure_texthooker_running,
|
||||||
start_overlay = start_overlay,
|
start_overlay = start_overlay,
|
||||||
start_overlay_from_script_message = start_overlay_from_script_message,
|
start_overlay_from_script_message = start_overlay_from_script_message,
|
||||||
stop_overlay = stop_overlay,
|
stop_overlay = stop_overlay,
|
||||||
toggle_overlay = toggle_overlay,
|
toggle_overlay = toggle_overlay,
|
||||||
|
toggle_invisible_overlay = toggle_invisible_overlay,
|
||||||
|
show_invisible_overlay = show_invisible_overlay,
|
||||||
|
hide_invisible_overlay = hide_invisible_overlay,
|
||||||
open_options = open_options,
|
open_options = open_options,
|
||||||
restart_overlay = restart_overlay,
|
restart_overlay = restart_overlay,
|
||||||
check_status = check_status,
|
check_status = check_status,
|
||||||
check_binary_available = check_binary_available,
|
|
||||||
notify_auto_play_ready = notify_auto_play_ready,
|
|
||||||
disarm_auto_play_ready_gate = disarm_auto_play_ready_gate,
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function M.new()
|
|||||||
overlay_process = nil,
|
overlay_process = nil,
|
||||||
binary_available = false,
|
binary_available = false,
|
||||||
binary_path = nil,
|
binary_path = nil,
|
||||||
detected_backend = nil,
|
invisible_overlay_visible = false,
|
||||||
hover_highlight = {
|
hover_highlight = {
|
||||||
revision = -1,
|
revision = -1,
|
||||||
payload = nil,
|
payload = nil,
|
||||||
@@ -27,8 +27,6 @@ function M.new()
|
|||||||
found = false,
|
found = false,
|
||||||
prompt_shown = false,
|
prompt_shown = false,
|
||||||
},
|
},
|
||||||
auto_play_ready_gate_armed = false,
|
|
||||||
auto_play_ready_timeout = nil,
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,16 @@ function M.create(ctx)
|
|||||||
local mp = ctx.mp
|
local mp = ctx.mp
|
||||||
local input = ctx.input
|
local input = ctx.input
|
||||||
local opts = ctx.opts
|
local opts = ctx.opts
|
||||||
|
local state = ctx.state
|
||||||
local process = ctx.process
|
local process = ctx.process
|
||||||
local aniskip = ctx.aniskip
|
local aniskip = ctx.aniskip
|
||||||
local subminer_log = ctx.log.subminer_log
|
local subminer_log = ctx.log.subminer_log
|
||||||
local show_osd = ctx.log.show_osd
|
local show_osd = ctx.log.show_osd
|
||||||
|
|
||||||
local function ensure_binary_for_menu()
|
|
||||||
if process.check_binary_available() then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
subminer_log("error", "binary", "SubMiner binary not found")
|
|
||||||
show_osd("Error: binary not found")
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function show_menu()
|
local function show_menu()
|
||||||
if not ensure_binary_for_menu() then
|
if not state.binary_available then
|
||||||
|
subminer_log("error", "binary", "SubMiner binary not found")
|
||||||
|
show_osd("Error: binary not found")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -27,30 +21,20 @@ function M.create(ctx)
|
|||||||
"Start overlay",
|
"Start overlay",
|
||||||
"Stop overlay",
|
"Stop overlay",
|
||||||
"Toggle overlay",
|
"Toggle overlay",
|
||||||
|
"Toggle invisible overlay",
|
||||||
"Open options",
|
"Open options",
|
||||||
"Restart overlay",
|
"Restart overlay",
|
||||||
"Check status",
|
"Check status",
|
||||||
}
|
}
|
||||||
|
|
||||||
local actions = {
|
local actions = {
|
||||||
function()
|
process.start_overlay,
|
||||||
process.start_overlay()
|
process.stop_overlay,
|
||||||
end,
|
process.toggle_overlay,
|
||||||
function()
|
process.toggle_invisible_overlay,
|
||||||
process.stop_overlay()
|
process.open_options,
|
||||||
end,
|
process.restart_overlay,
|
||||||
function()
|
process.check_status,
|
||||||
process.toggle_overlay()
|
|
||||||
end,
|
|
||||||
function()
|
|
||||||
process.open_options()
|
|
||||||
end,
|
|
||||||
function()
|
|
||||||
process.restart_overlay()
|
|
||||||
end,
|
|
||||||
function()
|
|
||||||
process.check_status()
|
|
||||||
end,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input.select({
|
input.select({
|
||||||
@@ -65,34 +49,21 @@ function M.create(ctx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function register_keybindings()
|
local function register_keybindings()
|
||||||
mp.add_key_binding("y-s", "subminer-start", function()
|
mp.add_key_binding("y-s", "subminer-start", process.start_overlay)
|
||||||
process.start_overlay()
|
mp.add_key_binding("y-S", "subminer-stop", process.stop_overlay)
|
||||||
end)
|
mp.add_key_binding("y-t", "subminer-toggle", process.toggle_overlay)
|
||||||
mp.add_key_binding("y-S", "subminer-stop", function()
|
mp.add_key_binding("y-i", "subminer-toggle-invisible", process.toggle_invisible_overlay)
|
||||||
process.stop_overlay()
|
mp.add_key_binding("y-I", "subminer-show-invisible", process.show_invisible_overlay)
|
||||||
end)
|
mp.add_key_binding("y-u", "subminer-hide-invisible", process.hide_invisible_overlay)
|
||||||
mp.add_key_binding("y-t", "subminer-toggle", function()
|
|
||||||
process.toggle_overlay()
|
|
||||||
end)
|
|
||||||
mp.add_key_binding("y-y", "subminer-menu", show_menu)
|
mp.add_key_binding("y-y", "subminer-menu", show_menu)
|
||||||
mp.add_key_binding("y-o", "subminer-options", function()
|
mp.add_key_binding("y-o", "subminer-options", process.open_options)
|
||||||
process.open_options()
|
mp.add_key_binding("y-r", "subminer-restart", process.restart_overlay)
|
||||||
end)
|
mp.add_key_binding("y-c", "subminer-status", process.check_status)
|
||||||
mp.add_key_binding("y-r", "subminer-restart", function()
|
|
||||||
process.restart_overlay()
|
|
||||||
end)
|
|
||||||
mp.add_key_binding("y-c", "subminer-status", function()
|
|
||||||
process.check_status()
|
|
||||||
end)
|
|
||||||
if type(opts.aniskip_button_key) == "string" and opts.aniskip_button_key ~= "" then
|
if type(opts.aniskip_button_key) == "string" and opts.aniskip_button_key ~= "" then
|
||||||
mp.add_key_binding(opts.aniskip_button_key, "subminer-skip-intro", function()
|
mp.add_key_binding(opts.aniskip_button_key, "subminer-skip-intro", aniskip.skip_intro_now)
|
||||||
aniskip.skip_intro_now()
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
if opts.aniskip_button_key ~= "y-k" then
|
if opts.aniskip_button_key ~= "y-k" then
|
||||||
mp.add_key_binding("y-k", "subminer-skip-intro-fallback", function()
|
mp.add_key_binding("y-k", "subminer-skip-intro-fallback", aniskip.skip_intro_now)
|
||||||
aniskip.skip_intro_now()
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
|
|
||||||
electron_args=("$@")
|
|
||||||
if [[ ${#electron_args[@]} -eq 0 ]]; then
|
|
||||||
electron_args=(--start --dev)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v bun >/dev/null 2>&1; then
|
|
||||||
echo "[ERROR] bun not found in PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TS_WATCH_PID=""
|
|
||||||
RENDER_WATCH_PID=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
local pids=("$TS_WATCH_PID" "$RENDER_WATCH_PID")
|
|
||||||
for pid in "${pids[@]}"; do
|
|
||||||
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
||||||
kill "$pid" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
trap cleanup EXIT INT TERM
|
|
||||||
|
|
||||||
sync_renderer_assets() {
|
|
||||||
mkdir -p dist/renderer
|
|
||||||
cp src/renderer/index.html src/renderer/style.css dist/renderer/
|
|
||||||
mkdir -p dist/renderer/fonts
|
|
||||||
cp -R src/renderer/fonts/. dist/renderer/fonts/
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "[INFO] Syncing renderer static assets"
|
|
||||||
sync_renderer_assets
|
|
||||||
|
|
||||||
echo "[INFO] Running initial compile"
|
|
||||||
bun run tsc
|
|
||||||
bun run build:renderer
|
|
||||||
|
|
||||||
echo "[INFO] Starting TypeScript watch"
|
|
||||||
bun run tsc --watch --preserveWatchOutput &
|
|
||||||
TS_WATCH_PID=$!
|
|
||||||
|
|
||||||
echo "[INFO] Starting renderer watch"
|
|
||||||
bunx esbuild src/renderer/renderer.ts \
|
|
||||||
--bundle \
|
|
||||||
--platform=browser \
|
|
||||||
--format=esm \
|
|
||||||
--target=es2022 \
|
|
||||||
--outfile=dist/renderer/renderer.js \
|
|
||||||
--sourcemap \
|
|
||||||
--watch &
|
|
||||||
RENDER_WATCH_PID=$!
|
|
||||||
|
|
||||||
echo "[INFO] Launching Electron with args: ${electron_args[*]}"
|
|
||||||
bun run electron . "${electron_args[@]}"
|
|
||||||
@@ -33,7 +33,7 @@ interface CliOptions {
|
|||||||
function parseCliArgs(argv: string[]): CliOptions {
|
function parseCliArgs(argv: string[]): CliOptions {
|
||||||
const args = [...argv];
|
const args = [...argv];
|
||||||
let inputParts: string[] = [];
|
let inputParts: string[] = [];
|
||||||
let dictionaryPath = path.join(process.cwd(), 'vendor', 'frequency-dictionary');
|
let dictionaryPath = path.join(process.cwd(), 'vendor', 'jiten_freq_global');
|
||||||
let emitPretty = false;
|
let emitPretty = false;
|
||||||
let emitDiagnostics = false;
|
let emitDiagnostics = false;
|
||||||
let mecabCommand: string | undefined;
|
let mecabCommand: string | undefined;
|
||||||
@@ -394,7 +394,7 @@ function printUsage(): void {
|
|||||||
--color-band-5 <#hex> Frequency band-5 color.
|
--color-band-5 <#hex> Frequency band-5 color.
|
||||||
--color-known <#hex> Known-word color (default: #a6da95).
|
--color-known <#hex> Known-word color (default: #a6da95).
|
||||||
--color-n-plus-one <#hex> N+1 target color (default: #c6a0f6).
|
--color-n-plus-one <#hex> N+1 target color (default: #c6a0f6).
|
||||||
--dictionary <path> Frequency dictionary root path (default: ./vendor/frequency-dictionary)
|
--dictionary <path> Frequency dictionary root path (default: ./vendor/jiten_freq_global)
|
||||||
--mecab-command <path> Optional MeCab binary path (default: mecab)
|
--mecab-command <path> Optional MeCab binary path (default: mecab)
|
||||||
--mecab-dictionary <path> Optional MeCab dictionary directory (default: system default)
|
--mecab-dictionary <path> Optional MeCab dictionary directory (default: system default)
|
||||||
-h, --help Show usage.
|
-h, --help Show usage.
|
||||||
|
|||||||
@@ -11,36 +11,30 @@ Description:
|
|||||||
Generates two browser-friendly files next to the input file:
|
Generates two browser-friendly files next to the input file:
|
||||||
- <name>.mp4 (H.264 + AAC, prefers NVIDIA GPU if available)
|
- <name>.mp4 (H.264 + AAC, prefers NVIDIA GPU if available)
|
||||||
- <name>.webm (AV1/VP9 + Opus, prefers NVIDIA GPU if available)
|
- <name>.webm (AV1/VP9 + Opus, prefers NVIDIA GPU if available)
|
||||||
- <name>-poster.jpg (single frame for video poster fallback)
|
- <name>.gif (palette-optimised, 15 fps)
|
||||||
- <name>.webp (animated, only when --webp is provided)
|
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-f, --force Overwrite existing output files
|
-f, --force Overwrite existing output files
|
||||||
-w, --webp Generate animated WebP preview
|
|
||||||
|
|
||||||
Encoding profile:
|
Encoding profile:
|
||||||
- Crop: 1920x1080 at x=760 y=200
|
- Crop: 1920x1080 at x=760 y=180
|
||||||
- MP4: H.264 + AAC
|
- MP4: H.264 + AAC
|
||||||
- WebM: AV1/VP9 + Opus at 30 fps
|
- WebM: AV1/VP9 + Opus at 30 fps
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
force=0
|
force=0
|
||||||
generate_webp=0
|
|
||||||
input=""
|
input=""
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h | --help)
|
-h|--help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
-f | --force)
|
-f|--force)
|
||||||
force=1
|
force=1
|
||||||
;;
|
;;
|
||||||
-w | --webp)
|
|
||||||
generate_webp=1
|
|
||||||
;;
|
|
||||||
-*)
|
-*)
|
||||||
echo "Error: unknown option: $1" >&2
|
echo "Error: unknown option: $1" >&2
|
||||||
usage
|
usage
|
||||||
@@ -79,8 +73,7 @@ base="${filename%.*}"
|
|||||||
|
|
||||||
mp4_out="$dir/$base.mp4"
|
mp4_out="$dir/$base.mp4"
|
||||||
webm_out="$dir/$base.webm"
|
webm_out="$dir/$base.webm"
|
||||||
webp_out="$dir/$base.webp"
|
gif_out="$dir/$base.gif"
|
||||||
poster_out="$dir/$base-poster.jpg"
|
|
||||||
|
|
||||||
overwrite_flag="-n"
|
overwrite_flag="-n"
|
||||||
if [[ "$force" -eq 1 ]]; then
|
if [[ "$force" -eq 1 ]]; then
|
||||||
@@ -88,11 +81,7 @@ if [[ "$force" -eq 1 ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$force" -eq 0 ]]; then
|
if [[ "$force" -eq 0 ]]; then
|
||||||
outputs=("$mp4_out" "$webm_out" "$poster_out")
|
for output in "$mp4_out" "$webm_out" "$gif_out"; do
|
||||||
if [[ "$generate_webp" -eq 1 ]]; then
|
|
||||||
outputs+=("$webp_out")
|
|
||||||
fi
|
|
||||||
for output in "${outputs[@]}"; do
|
|
||||||
if [[ -e "$output" ]]; then
|
if [[ -e "$output" ]]; then
|
||||||
echo "Error: output exists: $output (use --force to overwrite)" >&2
|
echo "Error: output exists: $output (use --force to overwrite)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -105,8 +94,9 @@ has_encoder() {
|
|||||||
ffmpeg -hide_banner -encoders 2> /dev/null | grep -qE "[[:space:]]${encoder}[[:space:]]"
|
ffmpeg -hide_banner -encoders 2> /dev/null | grep -qE "[[:space:]]${encoder}[[:space:]]"
|
||||||
}
|
}
|
||||||
|
|
||||||
crop_vf="crop=1920:1080:760:205"
|
crop_vf="crop=1920:1080:760:180"
|
||||||
webm_vf="${crop_vf},fps=30"
|
webm_vf="${crop_vf},fps=30"
|
||||||
|
gif_vf="${crop_vf},fps=15,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3"
|
||||||
|
|
||||||
echo "Generating MP4: $mp4_out"
|
echo "Generating MP4: $mp4_out"
|
||||||
if has_encoder "h264_nvenc"; then
|
if has_encoder "h264_nvenc"; then
|
||||||
@@ -167,32 +157,12 @@ else
|
|||||||
"$webm_out"
|
"$webm_out"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$generate_webp" -eq 1 ]]; then
|
echo "Generating GIF: $gif_out"
|
||||||
if ! has_encoder "libwebp"; then
|
ffmpeg "$overwrite_flag" -i "$input" \
|
||||||
echo "Error: encoder not found: libwebp" >&2
|
-vf "$gif_vf" \
|
||||||
exit 1
|
"$gif_out"
|
||||||
fi
|
|
||||||
echo "Generating animated WebP: $webp_out"
|
|
||||||
ffmpeg "$overwrite_flag" -i "$input" \
|
|
||||||
-vf "${crop_vf},fps=24,scale=960:-1:flags=lanczos" \
|
|
||||||
-c:v libwebp \
|
|
||||||
-q:v 80 \
|
|
||||||
-loop 0 \
|
|
||||||
-an \
|
|
||||||
"$webp_out"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Generating poster: $poster_out"
|
|
||||||
ffmpeg "$overwrite_flag" -ss 00:00:05 -i "$input" \
|
|
||||||
-vf "$crop_vf" \
|
|
||||||
-vframes 1 \
|
|
||||||
-q:v 2 \
|
|
||||||
"$poster_out"
|
|
||||||
|
|
||||||
echo "Done."
|
echo "Done."
|
||||||
echo "MP4: $mp4_out"
|
echo "MP4: $mp4_out"
|
||||||
echo "WebM: $webm_out"
|
echo "WebM: $webm_out"
|
||||||
if [[ "$generate_webp" -eq 1 ]]; then
|
echo "GIF: $gif_out"
|
||||||
echo "WebP: $webp_out"
|
|
||||||
fi
|
|
||||||
echo "Poster: $poster_out"
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
exec bun run electron . "$@"
|
|
||||||