mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-28 12:55:17 -07:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
75f9b8a803
|
|||
|
c30cce0a14
|
|||
| 3e6591e390 | |||
| f033f87329 | |||
|
29cb8c7fb4
|
|||
| 1dcfed86ab | |||
| efe50ed1e4 | |||
| 5b44981688 | |||
|
2add95d541
|
|||
|
f62fff2585
|
|||
| 11c196821d | |||
|
43ebc7d371
|
|||
| 639e331f24 | |||
|
78be72e32f
|
|||
| 3932e53ced | |||
| 097b619d71 | |||
|
f7abcedd75
|
|||
| 807c0ff3db | |||
| 7e6f9672cf | |||
|
9fe13601fb
|
|||
| 920cbab1bc | |||
| 17d97f0b7e | |||
|
10463e7348
|
|||
|
e9abbd5f05
|
|||
|
d6ff50455a
|
|||
| b1bdeabca8 | |||
| da3c971ee6 | |||
|
c02edc90cc
|
|||
|
4d1a20d69b
|
|||
| 7e86c4ea3d | |||
| c4f99fec2f | |||
|
c6328eef09
|
|||
| dc52bc2fba | |||
| a54f03f0cd | |||
| 799cce6991 | |||
| 6b2cb002ac | |||
| e84674e3b5 | |||
|
6ca5cede3e
|
|||
|
4d010e6a18
|
|||
| 5250ca8214 | |||
| 49f89e6452 | |||
|
89723e2ccb
|
|||
|
d05e2bd8ec
|
|||
|
7484d3c102
|
|||
|
f78a875ba3
|
|||
|
a025652542
|
|||
| 91a01b86a9 | |||
| 105713361e | |||
| 4cb0dbfaad | |||
|
801cdcafca
|
|||
|
094bcce0dc
|
|||
|
d1ec678d7a
|
|||
|
f0324cd93a
|
|||
|
1b2ee03678
|
@@ -0,0 +1,76 @@
|
|||||||
|
name: Docs Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
paths:
|
||||||
|
- 'docs-site/**'
|
||||||
|
- 'scripts/docs-versioning.ts'
|
||||||
|
- 'scripts/build-versioned-docs.ts'
|
||||||
|
- '.github/workflows/docs-pages.yml'
|
||||||
|
- 'package.json'
|
||||||
|
- 'bun.lock'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docs-pages-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
if: ${{ github.ref_type != 'tag' || !contains(github.ref_name, '-') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Guard stable docs tag shape
|
||||||
|
id: tag_guard
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
run: |
|
||||||
|
if [[ ! "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "::notice::Skipping non-stable docs tag ${{ github.ref_name }}"
|
||||||
|
echo "stable_tag=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "stable_tag=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd docs-site && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Cache versioned docs archives
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: .tmp/docs-versioned-archive-cache
|
||||||
|
key: docs-versioned-archives-${{ runner.os }}-${{ hashFiles('docs-site/.vitepress/**', 'docs-site/public/assets/fonts/**', 'docs-site/package.json', 'docs-site/bun.lock', 'scripts/build-versioned-docs.ts', 'scripts/docs-versioning.ts') }}
|
||||||
|
|
||||||
|
- name: Test docs
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
run: bun run docs:test
|
||||||
|
|
||||||
|
- name: Build versioned docs
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
run: bun run docs:build:versioned
|
||||||
|
|
||||||
|
- name: Deploy docs to Cloudflare Pages
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
command: pages deploy .tmp/docs-versioned-site --project-name "${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME }}" --branch main
|
||||||
@@ -47,6 +47,13 @@ jobs:
|
|||||||
- name: Build (TypeScript check)
|
- name: Build (TypeScript check)
|
||||||
run: bun run typecheck
|
run: bun run typecheck
|
||||||
|
|
||||||
|
- name: Install Lua
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y lua5.4
|
||||||
|
sudo ln -sf /usr/bin/lua5.4 /usr/local/bin/lua
|
||||||
|
lua -v
|
||||||
|
|
||||||
- name: Test suite (source)
|
- name: Test suite (source)
|
||||||
run: bun run test:fast
|
run: bun run test:fast
|
||||||
|
|
||||||
@@ -139,7 +146,10 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: appimage
|
name: appimage
|
||||||
path: release/*.AppImage
|
path: |
|
||||||
|
release/*.AppImage
|
||||||
|
release/*.yml
|
||||||
|
release/*.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
@@ -216,6 +226,8 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
release/*.dmg
|
release/*.dmg
|
||||||
release/*.zip
|
release/*.zip
|
||||||
|
release/*.yml
|
||||||
|
release/*.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
@@ -267,6 +279,8 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
release/*.exe
|
release/*.exe
|
||||||
release/*.zip
|
release/*.zip
|
||||||
|
release/*.yml
|
||||||
|
release/*.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -339,7 +353,7 @@ jobs:
|
|||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
run: |
|
run: |
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz dist/launcher/subminer)
|
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/*.yml release/*.blockmap dist/launcher/subminer)
|
||||||
if [ "${#files[@]}" -eq 0 ]; then
|
if [ "${#files[@]}" -eq 0 ]; then
|
||||||
echo "No release artifacts found for checksum generation."
|
echo "No release artifacts found for checksum generation."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -355,8 +369,12 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Generate prerelease notes from pending fragments
|
- name: Verify committed prerelease notes
|
||||||
run: bun run changelog:prerelease-notes --version "${{ steps.version.outputs.VERSION }}"
|
run: |
|
||||||
|
if [ ! -s release/prerelease-notes.md ]; then
|
||||||
|
echo "::error::release/prerelease-notes.md is missing or empty. Run 'bun run changelog:prerelease-notes --version <version>' locally and commit the file before tagging."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Publish Prerelease
|
- name: Publish Prerelease
|
||||||
env:
|
env:
|
||||||
@@ -371,6 +389,8 @@ jobs:
|
|||||||
release/*.exe
|
release/*.exe
|
||||||
release/*.zip
|
release/*.zip
|
||||||
release/*.tar.gz
|
release/*.tar.gz
|
||||||
|
release/*.yml
|
||||||
|
release/*.blockmap
|
||||||
release/SHA256SUMS.txt
|
release/SHA256SUMS.txt
|
||||||
dist/launcher/subminer
|
dist/launcher/subminer
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -137,7 +137,10 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: appimage
|
name: appimage
|
||||||
path: release/*.AppImage
|
path: |
|
||||||
|
release/*.AppImage
|
||||||
|
release/*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
needs: [quality-gate]
|
needs: [quality-gate]
|
||||||
@@ -213,6 +216,8 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
release/*.dmg
|
release/*.dmg
|
||||||
release/*.zip
|
release/*.zip
|
||||||
|
release/*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
needs: [quality-gate]
|
needs: [quality-gate]
|
||||||
@@ -263,6 +268,8 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
release/*.exe
|
release/*.exe
|
||||||
release/*.zip
|
release/*.zip
|
||||||
|
release/*.yml
|
||||||
|
release/*.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -335,7 +342,7 @@ jobs:
|
|||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
run: |
|
run: |
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz dist/launcher/subminer)
|
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/*.yml release/*.blockmap dist/launcher/subminer)
|
||||||
if [ "${#files[@]}" -eq 0 ]; then
|
if [ "${#files[@]}" -eq 0 ]; then
|
||||||
echo "No release artifacts found for checksum generation."
|
echo "No release artifacts found for checksum generation."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -389,6 +396,8 @@ jobs:
|
|||||||
release/*.exe
|
release/*.exe
|
||||||
release/*.zip
|
release/*.zip
|
||||||
release/*.tar.gz
|
release/*.tar.gz
|
||||||
|
release/*.yml
|
||||||
|
release/*.blockmap
|
||||||
release/SHA256SUMS.txt
|
release/SHA256SUMS.txt
|
||||||
dist/launcher/subminer
|
dist/launcher/subminer
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ dist/
|
|||||||
release/*
|
release/*
|
||||||
!release/
|
!release/
|
||||||
!release/release-notes.md
|
!release/release-notes.md
|
||||||
|
!release/prerelease-notes.md
|
||||||
build/yomitan/
|
build/yomitan/
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-windows uninstall uninstall-linux uninstall-macos uninstall-windows print-dirs pretty lint ensure-bun generate-config generate-example-config dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop
|
.PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-windows uninstall uninstall-linux uninstall-macos uninstall-windows print-dirs pretty lint ensure-bun generate-config generate-example-config dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop docs-test docs-build docs-build-versioned docs-dev
|
||||||
|
|
||||||
APP_NAME := subminer
|
APP_NAME := subminer
|
||||||
THEME_SOURCE := assets/themes/subminer.rasi
|
THEME_SOURCE := assets/themes/subminer.rasi
|
||||||
@@ -20,9 +20,10 @@ MACOS_APP_DIR ?= $(HOME)/Applications
|
|||||||
MACOS_APP_DEST ?= $(MACOS_APP_DIR)/SubMiner.app
|
MACOS_APP_DEST ?= $(MACOS_APP_DIR)/SubMiner.app
|
||||||
|
|
||||||
# If building from source, the AppImage will typically land in release/.
|
# If building from source, the AppImage will typically land in release/.
|
||||||
APPIMAGE_SRC := $(firstword $(wildcard release/SubMiner-*.AppImage))
|
APPIMAGE_SRC = $(firstword $(wildcard release/SubMiner-*.AppImage))
|
||||||
MACOS_APP_SRC := $(firstword $(wildcard release/*.app release/*/*.app))
|
MACOS_APP_SRC = $(firstword $(wildcard release/*.app release/*/*.app))
|
||||||
MACOS_ZIP_SRC := $(firstword $(wildcard release/SubMiner-*.zip))
|
MACOS_ZIP_SRC = $(firstword $(wildcard release/SubMiner-*.zip))
|
||||||
|
PRERELEASE_NOTES := release/prerelease-notes.md
|
||||||
|
|
||||||
UNAME_S := $(shell uname -s 2>/dev/null || echo Unknown)
|
UNAME_S := $(shell uname -s 2>/dev/null || echo Unknown)
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
@@ -61,6 +62,10 @@ help:
|
|||||||
" dev-watch-macos Start watch loop with forced macOS tracker backend" \
|
" dev-watch-macos Start watch loop with forced macOS tracker backend" \
|
||||||
" dev-toggle Toggle overlay in a running local Electron app" \
|
" dev-toggle Toggle overlay in a running local Electron app" \
|
||||||
" dev-stop Stop a running local Electron app" \
|
" dev-stop Stop a running local Electron app" \
|
||||||
|
" docs-test Run docs tests" \
|
||||||
|
" docs-build Build the docs site" \
|
||||||
|
" docs-build-versioned Build production versioned docs site" \
|
||||||
|
" docs-dev Start the docs dev server" \
|
||||||
" install-linux Install Linux wrapper/theme/app artifacts" \
|
" install-linux Install Linux wrapper/theme/app artifacts" \
|
||||||
" install-macos Install macOS wrapper/theme/app artifacts" \
|
" install-macos Install macOS wrapper/theme/app artifacts" \
|
||||||
" install-windows Print Windows packaging/install guidance" \
|
" install-windows Print Windows packaging/install guidance" \
|
||||||
@@ -161,7 +166,15 @@ build-launcher:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
@printf '%s\n' "[INFO] Removing build artifacts"
|
@printf '%s\n' "[INFO] Removing build artifacts"
|
||||||
@rm -rf dist release
|
@if [ -f "$(PRERELEASE_NOTES)" ]; then \
|
||||||
|
PRERELEASE_NOTES_BACKUP="$$(mktemp -t subminer-prerelease-notes.XXXXXX)" && \
|
||||||
|
cp "$(PRERELEASE_NOTES)" "$$PRERELEASE_NOTES_BACKUP" && \
|
||||||
|
rm -rf dist release && \
|
||||||
|
install -d release && \
|
||||||
|
mv "$$PRERELEASE_NOTES_BACKUP" "$(PRERELEASE_NOTES)"; \
|
||||||
|
else \
|
||||||
|
rm -rf dist release; \
|
||||||
|
fi
|
||||||
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
||||||
|
|
||||||
generate-config: ensure-bun
|
generate-config: ensure-bun
|
||||||
@@ -191,6 +204,18 @@ dev-toggle: ensure-bun
|
|||||||
dev-stop: ensure-bun
|
dev-stop: ensure-bun
|
||||||
@bun run electron . --stop
|
@bun run electron . --stop
|
||||||
|
|
||||||
|
docs-test: ensure-bun
|
||||||
|
@bun run docs:test
|
||||||
|
|
||||||
|
docs-build: ensure-bun
|
||||||
|
@bun run docs:build
|
||||||
|
|
||||||
|
docs-build-versioned: ensure-bun
|
||||||
|
@bun run docs:build:versioned
|
||||||
|
|
||||||
|
docs-dev: ensure-bun
|
||||||
|
@bun run docs:dev
|
||||||
|
|
||||||
|
|
||||||
install-linux: build-launcher
|
install-linux: build-launcher
|
||||||
@printf '%s\n' "[INFO] Installing Linux wrapper/theme artifacts"
|
@printf '%s\n' "[INFO] Installing Linux wrapper/theme artifacts"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
# SubMiner
|
# SubMiner
|
||||||
|
|
||||||
Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv.
|
Integrates Yomitan and mpv - on-screen lookups, mine to Anki, and track immersion without leaving the player
|
||||||
|
|
||||||
[Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
|
[Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ Look up words with Yomitan, export to Anki in one key, track your immersion —
|
|||||||
|
|
||||||
### Dictionary Lookups
|
### Dictionary Lookups
|
||||||
|
|
||||||
Yomitan runs inside the overlay. Trigger a lookup on any word for full dictionary popups — definitions, pitch accent, frequency data — without ever leaving mpv.
|
Hover over any word and trigger a lookup to get the full Yomitan popup - definitions, pitch accent, and frequency data - without ever leaving mpv.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="docs-site/public/screenshots/yomitan-lookup.png" width="800" alt="Yomitan dictionary popup over annotated subtitles in mpv">
|
<img src="docs-site/public/screenshots/yomitan-lookup.png" width="800" alt="Yomitan dictionary popup over annotated subtitles in mpv">
|
||||||
@@ -43,7 +43,7 @@ Create an Anki card with the sentence, audio clip, screenshot, and machine trans
|
|||||||
|
|
||||||
### Reading Annotations
|
### Reading Annotations
|
||||||
|
|
||||||
Real-time subtitle annotations with frequency highlighting, JLPT tags, N+1 targeting, and a character name dictionary. Known words fade back; new words stand out. Grammar-only tokens render as plain text so you focus on what matters.
|
Real-time subtitle annotations with frequency highlighting, JLPT tags, N+1 targeting, and a character name dictionary. Grammar-only tokens and particles render as plain text so you focus on what matters.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="docs-site/public/screenshots/annotations.png" width="800" alt="Annotated subtitles with frequency coloring, JLPT underlines, and N+1 targets">
|
<img src="docs-site/public/screenshots/annotations.png" width="800" alt="Annotated subtitles with frequency coloring, JLPT underlines, and N+1 targets">
|
||||||
@@ -53,7 +53,7 @@ Real-time subtitle annotations with frequency highlighting, JLPT tags, N+1 targe
|
|||||||
|
|
||||||
### Immersion Dashboard
|
### Immersion Dashboard
|
||||||
|
|
||||||
Local stats dashboard — watch time, anime library, vocabulary growth, mining throughput, session history, and trends. All stored locally, no third-party tracking.
|
Local stats dashboard tracking watch time, vocabulary growth, mining throughput, session history, and trends. All stored locally, no third-party tracking.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="docs-site/public/screenshots/stats-overview.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
|
<img src="docs-site/public/screenshots/stats-overview.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
|
||||||
@@ -92,11 +92,11 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>alass / ffsubsync</b></td>
|
<td><b>alass / ffsubsync</b></td>
|
||||||
<td>Automatic subtitle retiming — requires <code>alass</code> or <code>ffsubsync</code> on your <code>PATH</code> (optional; subtitle syncing is disabled without them)</td>
|
<td>Manual subtitle retiming — requires <code>alass</code> or <code>ffsubsync</code> on your <code>PATH</code> (optional; subtitle syncing is disabled without them)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>WebSocket</b></td>
|
<td><b>WebSocket</b></td>
|
||||||
<td>Annotated subtitle feed for external clients (texthooker pages, custom tools)</td>
|
<td>Plain subtitle feed plus a dedicated annotated feed for texthooker pages and custom tools</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -110,65 +110,36 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
| | Required | Recommended | Optional |
|
Only **mpv** and Anki+AnkiConnect are required. Everything else is optional but enhances the experience.
|
||||||
| -------------- | --------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — | — |
|
|
||||||
| **Processing** | — | `ffmpeg` (audio clips & screenshots) | `mecab` + `mecab-ipadic` (annotation POS filtering), `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
|
|
||||||
| **Media** | — | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
|
|
||||||
| **Selection** | — | — | `fzf` / `rofi` |
|
|
||||||
|
|
||||||
> [!TIP]
|
| Dependency | Status | What it does |
|
||||||
> `ffmpeg` is not strictly required to run SubMiner, but without it audio clips and screenshots will not be attached to Anki cards. Most users will want it installed.
|
| -------------------- | ----------- | ---------------------------------------- |
|
||||||
|
| mpv | Required | The video player SubMiner overlays on |
|
||||||
> [!NOTE]
|
| Anki + AnkiConnect | Required | Card creation from the Yomitan popup |
|
||||||
> [`bun`](https://bun.sh) is required if building from source or using the CLI wrapper: `subminer`. Pre-built releases (AppImage, DMG, installer) do not require it.
|
| ffmpeg | Recommended | Audio clips & screenshots for Anki cards |
|
||||||
|
| MeCab + mecab-ipadic | Recommended | More precise annotations and filtering |
|
||||||
**Platform-specific:**
|
| yt-dlp | Optional | YouTube playback |
|
||||||
|
| fzf / rofi | Optional | Video picker in the launcher |
|
||||||
| Linux | macOS | Windows |
|
| alass / ffsubsync | Optional | Subtitle sync |
|
||||||
| ------------------------------------------------------------ | ------------------------ | ------------- |
|
|
||||||
| Hyprland (`hyprctl`) · X11/Xwayland (`xdotool` + `xwininfo`) | Accessibility permission | No extra deps |
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> **Wayland support is compositor-specific.** Wayland has no universal API for window positioning and each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Hyprland is the only native Wayland backend supported currenlty. All other Linux compositors require both mpv and SubMiner to run under X11 or Xwayland. The launcher detects your compositor and configures this automatically.
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Arch Linux</b></summary>
|
<summary><b>Platform-specific install commands</b></summary>
|
||||||
|
|
||||||
|
**Arch Linux:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
paru -S --needed mpv ffmpeg
|
sudo pacman -S --needed mpv ffmpeg mecab mecab-ipadic
|
||||||
# Optional
|
|
||||||
paru -S --needed mecab-git mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo
|
|
||||||
# Optional: subtitle sync (install at least one for subtitle syncing to work)
|
|
||||||
paru -S --needed alass python-ffsubsync
|
|
||||||
# X11 / Xwayland (required for non-Hyprland compositors)
|
|
||||||
paru -S --needed xdotool xorg-xwininfo
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
**macOS:**
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><b>macOS</b></summary>
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install mpv ffmpeg
|
brew install mpv ffmpeg mecab mecab-ipadic
|
||||||
# Optional
|
|
||||||
brew install mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
|
|
||||||
# Optional: subtitle sync (install at least one for subtitle syncing to work)
|
|
||||||
brew install alass
|
|
||||||
pip install ffsubsync
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Grant Accessibility permission to SubMiner in **System Settings > Privacy & Security > Accessibility**.
|
**Windows:** Install [mpv](https://mpv.io/installation/) and [ffmpeg](https://ffmpeg.org/download.html) and ensure both are on `PATH`.
|
||||||
|
|
||||||
</details>
|
See the [full requirements list](https://docs.subminer.moe/installation#1-install-requirements) for optional dependencies.
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><b>Windows</b></summary>
|
|
||||||
|
|
||||||
Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on your `PATH`.
|
|
||||||
|
|
||||||
Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for additional metadata enrichment.
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -176,7 +147,7 @@ Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Install
|
### 1. Install SubMiner
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Arch Linux (AUR)</b></summary>
|
<summary><b>Arch Linux (AUR)</b></summary>
|
||||||
@@ -185,12 +156,6 @@ Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download
|
|||||||
paru -S subminer-bin
|
paru -S subminer-bin
|
||||||
```
|
```
|
||||||
|
|
||||||
Or manually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://aur.archlinux.org/subminer-bin.git && cd subminer-bin && makepkg -si
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -199,41 +164,24 @@ git clone https://aur.archlinux.org/subminer-bin.git && cd subminer-bin && makep
|
|||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.local/bin
|
mkdir -p ~/.local/bin
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage \
|
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage \
|
||||||
&& chmod +x ~/.local/bin/SubMiner.AppImage
|
&& chmod +x ~/.local/bin/SubMiner.AppImage
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer \
|
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer \
|
||||||
&& chmod +x ~/.local/bin/subminer
|
&& chmod +x ~/.local/bin/subminer
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The `subminer` wrapper uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>macOS</b></summary>
|
<summary><b>macOS (DMG)</b></summary>
|
||||||
|
|
||||||
Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
|
Download the latest DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
|
||||||
|
|
||||||
Also download the `subminer` launcher (recommended):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer \
|
|
||||||
&& sudo chmod +x /usr/local/bin/subminer
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The `subminer` launcher uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Windows</b></summary>
|
<summary><b>Windows</b></summary>
|
||||||
|
|
||||||
Download the latest installer or portable `.zip` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`.
|
Download and run the latest installer (`.exe`) from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest).
|
||||||
|
|
||||||
**Windows support is experimental.** Core features such as mining, annotations, and dictionary lookups work, but some functionality may be missing or unstable. Bug reports welcome.
|
|
||||||
|
|
||||||
**Note:** On Windows the `subminer` launcher requires [`bun`](https://bun.sh) and must be invoked with `bun run subminer` instead of running the script directly. The recommended alternative is the **SubMiner mpv** shortcut created during first-run setup — double-click it, drag files onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See the [Windows mpv Shortcut](https://docs.subminer.moe/usage#windows-mpv-shortcut) section for details.
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -244,39 +192,29 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### 2. First Launch
|
### 2. Launch & Set Up
|
||||||
|
|
||||||
|
Run SubMiner and the first-run setup wizard will guide you through importing Yomitan dictionaries and optionally installing the `subminer` command-line launcher.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer app --setup # launch the first-run setup wizard
|
# Linux
|
||||||
|
subminer app --setup
|
||||||
|
|
||||||
|
# macOS — open SubMiner.app, or:
|
||||||
|
subminer app --setup
|
||||||
```
|
```
|
||||||
|
|
||||||
SubMiner creates a default config, starts in the system tray, and opens a setup popup that walks you through installing the mpv plugin and configuring Yomitan dictionaries. Follow the on-screen steps to complete setup.
|
On **Windows**, just run `SubMiner.exe` and the setup will open automatically on first launch.
|
||||||
|
|
||||||
Jellyfin setup is available from the tray or `subminer jellyfin`; once Jellyfin is enabled with a server URL, the tray can toggle Jellyfin Discovery for the current app session.
|
### 3. Mine
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
|
|
||||||
|
|
||||||
### 3. Verify Setup
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer doctor # verify mpv, ffmpeg, config, and socket
|
subminer video.mkv # launch mpv with SubMiner
|
||||||
|
subminer /path/to/dir # pick a file with fzf
|
||||||
|
subminer -R /path/to/dir # pick a file with rofi (Linux only)
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
On **Windows**, use the **SubMiner mpv** shortcut created during setup. Double-click it or drag a video file onto it.
|
||||||
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly — first-run setup will guide you through dependency checks.
|
|
||||||
|
|
||||||
### 4. Mine
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer video.mkv # play video with overlay
|
|
||||||
subminer --start video.mkv # explicit overlay start
|
|
||||||
subminer stats # open immersion dashboard
|
|
||||||
subminer stats -b # stats daemon in background
|
|
||||||
subminer stats -s # stop background stats daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
On **Windows**, the `subminer` script must be run with `bun run subminer` (e.g. `bun run subminer video.mkv`). The recommended alternative is the **SubMiner mpv** shortcut (created during setup) or `SubMiner.exe --launch-mpv`. Drag a video file onto the shortcut to play it, or double-click it to open mpv with SubMiner's defaults.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"@xhayper/discord-rpc": "^1.3.3",
|
"@xhayper/discord-rpc": "^1.3.3",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
|
"electron-updater": "^6.8.3",
|
||||||
"hono": "^4.12.7",
|
"hono": "^4.12.7",
|
||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"koffi": "^2.15.6",
|
"koffi": "^2.15.6",
|
||||||
@@ -17,11 +18,12 @@
|
|||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.3.0",
|
"@types/node": "^24.10.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"electron": "39.8.6",
|
"electron": "42.2.0",
|
||||||
"electron-builder": "26.8.2",
|
"electron-builder": "26.8.2",
|
||||||
"esbuild": "^0.25.12",
|
"esbuild": "^0.25.12",
|
||||||
|
"eslint": "^10.4.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
},
|
},
|
||||||
@@ -51,7 +53,7 @@
|
|||||||
|
|
||||||
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
|
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
|
||||||
|
|
||||||
"@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
|
"@electron/get": ["@electron/get@5.0.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA=="],
|
||||||
|
|
||||||
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],
|
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],
|
||||||
|
|
||||||
@@ -115,10 +117,34 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||||
|
|
||||||
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
|
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
||||||
|
|
||||||
|
"@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
|
||||||
|
|
||||||
|
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
||||||
|
|
||||||
|
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
||||||
|
|
||||||
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
|
||||||
|
|
||||||
"@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="],
|
"@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="],
|
||||||
|
|
||||||
"@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.2.7", "", {}, "sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA=="],
|
"@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.2.7", "", {}, "sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA=="],
|
||||||
|
|
||||||
|
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||||
|
|
||||||
|
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||||
|
|
||||||
|
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||||
|
|
||||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||||
|
|
||||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||||
@@ -165,15 +191,21 @@
|
|||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||||
|
|
||||||
|
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||||
|
|
||||||
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
|
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
|
||||||
|
|
||||||
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
|
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
|
||||||
|
|
||||||
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
"@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
|
||||||
|
|
||||||
"@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
|
"@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
|
||||||
|
|
||||||
@@ -193,6 +225,10 @@
|
|||||||
|
|
||||||
"abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
|
"abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||||
@@ -293,6 +329,8 @@
|
|||||||
|
|
||||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||||
|
|
||||||
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
|
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
|
||||||
|
|
||||||
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
||||||
@@ -325,7 +363,7 @@
|
|||||||
|
|
||||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
"electron": ["electron@39.8.6", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-uWX6Jh5LmwL13VwOSKBjebI+ck+03GOwc8V2Sgbmr9pJVJ/cHfli/PkjXuRDr+hq+SLHQuT9mGHSIfScebApRA=="],
|
"electron": ["electron@42.2.0", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-b2Tc7sIKiZEl0tBVwFM5GJ+FT5KYhmy9QJHjx8BGVZPVW2SctXWEvrE959ElB56qw7H05dBkhlikDA1DmpaAMw=="],
|
||||||
|
|
||||||
"electron-builder": ["electron-builder@26.8.2", "", { "dependencies": { "app-builder-lib": "26.8.2", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-ieiiXPdgH3qrG6lcvy2mtnI5iEmAopmLuVRMSJ5j40weU0tgpNx0OAk9J5X5nnO0j9+KIkxHzwFZVUDk1U3aGw=="],
|
"electron-builder": ["electron-builder@26.8.2", "", { "dependencies": { "app-builder-lib": "26.8.2", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-ieiiXPdgH3qrG6lcvy2mtnI5iEmAopmLuVRMSJ5j40weU0tgpNx0OAk9J5X5nnO0j9+KIkxHzwFZVUDk1U3aGw=="],
|
||||||
|
|
||||||
@@ -333,6 +371,8 @@
|
|||||||
|
|
||||||
"electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="],
|
"electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="],
|
||||||
|
|
||||||
|
"electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="],
|
||||||
|
|
||||||
"electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="],
|
"electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
@@ -341,7 +381,7 @@
|
|||||||
|
|
||||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||||
|
|
||||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||||
|
|
||||||
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
||||||
|
|
||||||
@@ -361,6 +401,22 @@
|
|||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
|
"eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="],
|
||||||
|
|
||||||
|
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||||
|
|
||||||
|
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||||
|
|
||||||
|
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||||
|
|
||||||
|
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||||
|
|
||||||
|
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||||
|
|
||||||
|
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
||||||
|
|
||||||
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
||||||
@@ -371,12 +427,22 @@
|
|||||||
|
|
||||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||||
|
|
||||||
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
|
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
|
||||||
|
|
||||||
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
|
|
||||||
|
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||||
|
|
||||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
@@ -401,6 +467,8 @@
|
|||||||
|
|
||||||
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
|
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
|
||||||
|
|
||||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||||
@@ -439,6 +507,8 @@
|
|||||||
|
|
||||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
|
|
||||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||||
@@ -447,8 +517,12 @@
|
|||||||
|
|
||||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
|
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
|
||||||
|
|
||||||
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
||||||
@@ -469,6 +543,8 @@
|
|||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
|
|
||||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
@@ -483,10 +559,18 @@
|
|||||||
|
|
||||||
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
|
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
|
||||||
|
|
||||||
|
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||||
|
|
||||||
"libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="],
|
"libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="],
|
||||||
|
|
||||||
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
"lodash": ["lodash@4.18.0", "", {}, "sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA=="],
|
"lodash": ["lodash@4.18.0", "", {}, "sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA=="],
|
||||||
|
|
||||||
|
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
|
||||||
|
|
||||||
|
"lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
|
||||||
|
|
||||||
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
|
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
|
||||||
|
|
||||||
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
|
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
|
||||||
@@ -533,6 +617,8 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
"node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="],
|
"node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="],
|
||||||
@@ -553,16 +639,22 @@
|
|||||||
|
|
||||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||||
|
|
||||||
|
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||||
|
|
||||||
"ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
|
"ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
|
||||||
|
|
||||||
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
||||||
|
|
||||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
|
|
||||||
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
|
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
|
||||||
|
|
||||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
|
|
||||||
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
@@ -581,6 +673,8 @@
|
|||||||
|
|
||||||
"postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="],
|
"postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="],
|
||||||
|
|
||||||
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||||
|
|
||||||
"proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
|
"proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
|
||||||
@@ -627,7 +721,7 @@
|
|||||||
|
|
||||||
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
||||||
|
|
||||||
@@ -681,6 +775,8 @@
|
|||||||
|
|
||||||
"tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="],
|
"tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="],
|
||||||
|
|
||||||
|
"tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
|
"tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
|
||||||
@@ -691,13 +787,15 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
"undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
"unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="],
|
"unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="],
|
||||||
|
|
||||||
@@ -717,6 +815,8 @@
|
|||||||
|
|
||||||
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
|
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
|
||||||
|
|
||||||
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
@@ -739,22 +839,22 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"@discordjs/rest/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
||||||
|
|
||||||
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||||
|
|
||||||
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
|
|
||||||
"@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
|
||||||
|
|
||||||
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
|
|
||||||
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
|
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
|
||||||
|
|
||||||
"@electron/rebuild/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
||||||
|
|
||||||
"@electron/universal/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
|
"@electron/universal/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
|
||||||
|
|
||||||
"@electron/windows-sign/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
|
"@electron/windows-sign/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||||
|
|
||||||
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||||
@@ -765,14 +865,24 @@
|
|||||||
|
|
||||||
"@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"@npmcli/fs/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"@types/cacheable-request/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
|
"@types/fs-extra/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
|
"@types/keyv/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
|
"@types/plist/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
|
"@types/responselike/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
|
"@types/ws/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
|
"@types/yauzl/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
|
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
|
||||||
|
|
||||||
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
||||||
|
|
||||||
"app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
||||||
|
|
||||||
"cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
"cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||||
|
|
||||||
"cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
@@ -781,14 +891,10 @@
|
|||||||
|
|
||||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"electron/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
|
|
||||||
|
|
||||||
"electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
|
"electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
|
||||||
|
|
||||||
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"global-agent/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
||||||
|
|
||||||
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
@@ -797,30 +903,36 @@
|
|||||||
|
|
||||||
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
"node-abi/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
"node-api-version/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
||||||
|
|
||||||
"node-gyp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
||||||
|
|
||||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
|
"postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
|
||||||
|
|
||||||
"simple-update-notifier/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
||||||
|
|
||||||
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||||
|
|
||||||
"@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
|
||||||
|
|
||||||
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
|
||||||
|
|
||||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|
||||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
|
"@types/cacheable-request/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"@types/fs-extra/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"@types/keyv/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"@types/plist/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"@types/responselike/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"@types/ws/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"@types/yauzl/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
"app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
@@ -831,8 +943,6 @@
|
|||||||
|
|
||||||
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
|
|
||||||
"electron/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|||||||
@@ -34,11 +34,13 @@ Rules:
|
|||||||
How fragments turn into a release:
|
How fragments turn into a release:
|
||||||
|
|
||||||
- At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that.
|
- At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that.
|
||||||
|
- The polish step treats pending fragments as the final release outcome, not prerelease history. If a feature is added and then renamed or fixed before the stable cut, ship the final feature bullet instead of separate prerelease-only breaking/fix entries.
|
||||||
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely.
|
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely.
|
||||||
- The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
|
- The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
|
||||||
|
|
||||||
Prerelease notes:
|
Prerelease notes:
|
||||||
|
|
||||||
- prerelease tags like `v0.11.3-beta.1` and `v0.11.3-rc.1` reuse the current pending fragments to generate `release/prerelease-notes.md`
|
- prerelease tags like `v0.11.3-beta.1` and `v0.11.3-rc.1` reuse the current pending fragments to generate `release/prerelease-notes.md`
|
||||||
|
- existing prerelease notes are a reviewed baseline; later prerelease runs should replace stale beta/RC wording with the current outcome instead of appending fix churn
|
||||||
- prerelease note generation does not consume fragments and does not update `CHANGELOG.md` or `docs-site/changelog.md`
|
- prerelease note generation does not consume fragments and does not update `CHANGELOG.md` or `docs-site/changelog.md`
|
||||||
- the final stable release is the point where `bun run changelog:build` consumes fragments into the stable changelog and release notes
|
- the final stable release is the point where `bun run changelog:build` consumes fragments into the stable changelog and release notes
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: fixed
|
||||||
|
area: anilist
|
||||||
|
|
||||||
|
- Used fresh mpv time-position, duration, and subtitle timing events for AniList post-watch threshold checks so progress updates still fire when playback reaches or skips past the watched threshold.
|
||||||
|
- Prefer season-specific AniList search results for multi-season files before falling back to the base title, and show a clear message when the matched season is not in Planning or Watching instead of silently queueing an impossible update.
|
||||||
|
- Prevent repeated missing-token checks from rapidly exhausting AniList retry attempts or duplicating dead-letter entries for the same episode.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
type: fixed
|
||||||
|
area: anki
|
||||||
|
|
||||||
|
- Made sentence-audio padding opt-in by default, and kept animated AVIF freeze-frame duration aligned to the word audio length without double-counting sentence audio padding.
|
||||||
|
- Kept multi-line sentence mining aligned when repeated subtitle text appears in the selected history range.
|
||||||
|
- Fixed Kiku duplicate-card detection so local duplicate sentence cards trigger the manual modal or auto merge, modal-open acknowledgement races no longer cancel the flow, and merged fields follow Kiku's group ordering, sentence-audio, furigana, and tag semantics.
|
||||||
|
- Fixed manual clipboard card updates from YouTube playback so generated audio and images use mpv's resolved stream URLs instead of the YouTube page URL.
|
||||||
|
- Sentence cards now refresh the current secondary subtitle before saving, so the translation field uses the loaded subtitle instead of repeating the primary text.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: added
|
||||||
|
area: updater
|
||||||
|
|
||||||
|
- Added tray and `subminer -u` update checks for SubMiner releases, including app update prompts, launcher and Linux rofi theme updates, checksum verification, configurable update notifications, and an opt-in prerelease channel. Set `updates.channel` to `"prerelease"` to receive beta/RC builds.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
type: fixed
|
||||||
|
area: character-dictionary
|
||||||
|
|
||||||
|
- Reused cached character-dictionary media matches so loading a title with an existing snapshot no longer sends another AniList search request.
|
||||||
|
- Block the character dictionary manager when annotations are disabled, with a notice through the configured OSD/system notification surfaces.
|
||||||
|
- Added surname honorific matches for Japanese localized character aliases embedded in AniList alternative names (e.g. Korean-source characters with Japanese names in parentheses), and refresh cached snapshots so those aliases are regenerated.
|
||||||
|
- Use `subtitleStyle.nameMatchEnabled` as the only switch for character-dictionary sync/builds, hiding the legacy `anilist.characterDictionary.enabled` option.
|
||||||
|
- Forward character dictionary manager session-action keybindings to the mpv plugin.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: changed
|
||||||
|
area: character-dictionary
|
||||||
|
|
||||||
|
- Character dictionary entries are now scoped to the current AniList media for name matching and inline portraits, and generate Japanese name aliases only so raw romanized/English aliases no longer surface as separate results.
|
||||||
|
- Added a `Ctrl/Cmd+D` manager modal to remove, reorder, or override loaded dictionary entries.
|
||||||
|
- The in-app AniList selector now waits for an explicit title search, with the search box prefilled from the current filename guess so you can edit it before choosing an override.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: added
|
||||||
|
area: subtitles
|
||||||
|
|
||||||
|
- Added optional inline AniList portraits for character-name subtitle matches, including automatic refresh of cached character dictionary snapshots that do not contain portrait data.
|
||||||
|
- Scoped manual AniList overrides by parent media directory, so separate season folders can keep separate character dictionary selections.
|
||||||
|
- Fixed large character dictionary imports by serving the merged ZIP through a local URL when supported, with a base64 fallback for older bundled Yomitan builds.
|
||||||
|
- Allowed subtitle overlay data image sources so inline character portraits render instead of showing a broken image icon.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Controller config and debug shortcuts now stay closed while controller support is disabled and show a notice to enable `controller.enabled` manually.
|
||||||
|
- Controller binding rows now start learn mode from the edit pencil, so clicking edit and pressing a controller button saves the remap.
|
||||||
|
- Controller remaps are now saved per controller profile, binding badges also start learn mode, and row reset buttons restore individual bindings to their defaults.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: changed
|
||||||
|
area: config
|
||||||
|
|
||||||
|
- Defaulted Jellyfin remote-session startup warmup and character-name subtitle highlighting to off.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: docs
|
||||||
|
area: docs
|
||||||
|
|
||||||
|
- Published stable docs at the site root with current development docs under `/main/`, and fixed versioned docs navigation so archived pages keep local links under the selected version, the version switcher no longer nests paths incorrectly, local dev version routes serve warmed archive files, and internal README files no longer break archived builds.
|
||||||
|
- Documented all previously undocumented config options, including `subtitleStyle.primaryDefaultMode`, `stats.markWatchedKey`, `immersionTracking.lifetimeSummaries.*`, and all seven `mpv.*` launcher options.
|
||||||
|
- Added a Playback Startup Flow diagram and a Runtime Sockets section/diagram to the architecture docs, with cross-reference pointers in the MPV Plugin and Troubleshooting pages.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: changed
|
||||||
|
area: runtime
|
||||||
|
|
||||||
|
- Updated the bundled Electron runtime from 39.8.6 to 42.2.0, moving SubMiner back onto a supported Electron release line.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: integrations
|
||||||
|
|
||||||
|
- Prevented Discord Rich Presence from falling back to Jellyfin stream URLs, and primed Jellyfin playback titles before loading tokenized streams so presence shows the show/episode title
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: changed
|
||||||
|
area: setup
|
||||||
|
|
||||||
|
- Setup: Removed the bundled mpv runtime plugin readiness card; legacy mpv plugin removal still appears when needed.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: fixed
|
||||||
|
area: jellyfin
|
||||||
|
|
||||||
|
- Fixed Jellyfin discovery playback: the active item is no longer reloaded on startup, paused mpv is no longer misreported as playing, startup unpause no longer repeats after a manual pause or `y-t` toggle, duplicate ready signals no longer re-show the overlay, delayed Japanese subtitle selection is handled correctly, later-loading foreign tracks no longer steal the active Japanese track, and long-lived sidebar ffmpeg extractors no longer run against stream URLs.
|
||||||
|
- Fixed discovery resume when a remote play command sends `StartPositionTicks: 0` despite saved progress on the item.
|
||||||
|
- Kept Jellyfin picker library discovery working when the app log level is above info.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
type: fixed
|
||||||
|
area: jellyfin
|
||||||
|
|
||||||
|
- Keep the discovery tray checkbox in sync on Linux after tray, CLI, or startup remote-session changes, and restart stale discovery sessions when the server no longer lists the SubMiner cast target.
|
||||||
|
- Fixed remote controller visibility and progress sync for mpv/SubMiner seek jumps, stopped sessions, startup path changes, and Linux websocket reconnect windows.
|
||||||
|
- Kept Play and Resume distinct: Play starts from the beginning while Resume starts at the saved position, and final progress reports reuse SubMiner's last known position when mpv resets during stop.
|
||||||
|
- Derived cast device identity from the OS hostname and always report the client as SubMiner, ignoring legacy configurable identity fields so multiple installs no longer share a remote-session identity.
|
||||||
|
- Fixed the Windows setup login flow with an IPC bridge, immediate progress feedback, and a timeout with an inline error for unreachable servers.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: fixed
|
||||||
|
area: jellyfin
|
||||||
|
|
||||||
|
- Show the visible subtitle overlay automatically during Jellyfin playback so `subtitleStyle` appearance applies, and inject the bundled mpv plugin when SubMiner auto-launches mpv so mpv-side keybindings work without overlay focus.
|
||||||
|
- Made the `y-t` overlay toggle reliable and sticky across stream redirects that change mpv's path, re-arming managed subtitle defaults on redirect so Japanese primary subtitles load, collapsing duplicate toggle events on Hyprland, and keeping passive Linux/Hyprland overlay shows from stealing keyboard focus from mpv.
|
||||||
|
- Improved subtitle timing by preferring default embedded streams over external sidecars, stripping Jellyfin's server-selected stream from playback URLs, suppressing mpv auto-selection while SubMiner stages managed tracks, correcting clear Japanese-vs-English cue offsets, and restoring per-stream subtitle delay shifts. Track selection tolerates transient `track-list` read failures and numeric string track IDs on Linux.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
type: fixed
|
||||||
|
area: launcher
|
||||||
|
|
||||||
|
- Launcher-opened videos reuse an already-running background SubMiner instance, reapply preferred subtitles on warm launches, and close launcher-owned tray apps after playback ends.
|
||||||
|
- Videos stay paused when attaching to a running background app until subtitle priming and tokenization readiness complete, with mpv plugin subtitle auto-selection moved to pre-load so launch-time choices are not reset.
|
||||||
|
- `subminer settings` on macOS no longer emits Electron menu diagnostics and exits cleanly when the window is closed.
|
||||||
|
- `subminer app` on Linux returns control to the terminal immediately, and Linux first-run launcher installs build with a valid Bun shebang.
|
||||||
|
- `subminer app --setup` opens the setup flow when SubMiner is already running in the background.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: added
|
||||||
|
area: launcher
|
||||||
|
|
||||||
|
- Added `subminer --version` / `subminer -v` to print the installed app version.
|
||||||
|
- Added `mpv.profile` config and Settings support for passing an mpv profile to SubMiner-managed mpv launches.
|
||||||
|
- Made bundled mpv plugin startup options configurable from SubMiner config.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: changed
|
||||||
|
area: updater
|
||||||
|
|
||||||
|
- Linux tray "Check for Updates" now installs the new AppImage automatically via `electron-updater`, matching the macOS and Windows tray flow, instead of stopping at a "manual update required" dialog. AppImages managed by a system package (AUR `/opt/SubMiner/SubMiner.AppImage`) and non-AppImage launches (no `APPIMAGE` env) still fall back to the GitHub-asset flow.
|
||||||
|
- Routed `electron-updater` HTTP through `/usr/bin/curl` on Linux and disabled differential downloads, matching the macOS path, so background update checks stay off Electron's network service.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: added
|
||||||
|
area: logs
|
||||||
|
|
||||||
|
- Add sanitized log ZIP exports from the tray menu and `subminer logs -e`, with home-directory usernames redacted from exported log contents.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: fixed
|
||||||
|
area: logging
|
||||||
|
|
||||||
|
- Forward SubMiner `logging.level` into launcher-started and Windows shortcut-started mpv sessions, covering mpv log verbosity, plugin script logging, and plugin-launched app logging.
|
||||||
|
- Added numeric `logging.rotation` (default 7 days of retained daily app, launcher, and mpv logs) and `logging.files` toggles per component, with mpv logs disabled by default unless explicitly enabled for debugging.
|
||||||
|
- Added Windows mpv launch, IPC socket, subtitle track, and Yomitan/dictionary diagnostics, including expected/active IPC socket values when plugin auto-start skips on a socket mismatch.
|
||||||
|
- Stop repeated mpv IPC socket warning spam while the app waits in the background for mpv to recreate the socket.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: changed
|
||||||
|
area: config
|
||||||
|
|
||||||
|
- `ankiConnect.nPlusOne.enabled` is no longer implicitly set to `true` when known-word highlighting is enabled; existing configs that already had N+1 highlighting keep it, but new configs leave it disabled unless `ankiConnect.nPlusOne.enabled` is set explicitly.
|
||||||
|
- Updated known-word cache docs and examples to recommend expression/word fields and removed legacy-option references from user-facing config docs.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Primed the first startup subtitle before autoplay resumes so the overlay renders text before video playback begins.
|
||||||
|
- Kept the visible overlay and subtitle stream alive after restarting SubMiner from the mpv `y-r` shortcut, with correct Linux bounds reapplication and user-paused playback preserved through readiness gates.
|
||||||
|
- Fixed managed mpv startup so launcher-owned videos quit SubMiner when playback ends while background/tray sessions stay alive, with pause-until-ready waiting for overlay and tokenization readiness.
|
||||||
|
- Fixed the subtitle sync modal on macOS so it no longer flashes and hides on the first attempt or leaves stale state after syncing.
|
||||||
|
- Fixed Windows managed mpv launches from a background instance so the warm app receives the start command, retargets the new mpv socket, binds to the player window, and receives startup overlay options.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Refreshed overlay placement after leaving mpv fullscreen so the visible overlay stays aligned to the player on Hyprland.
|
||||||
|
- Kept the visible overlay stacked above mpv after mpv regains focus from clicks or overlay movement, and suspended it while the in-player stats window is open, restoring it mouse-passive afterward.
|
||||||
|
- Promoted SubMiner and Yomitan settings windows above the subtitle overlay on Hyprland instead of opening behind it, without hiding subtitles.
|
||||||
|
- Hid the visible overlay as soon as the character dictionary modal opens, including while AniList lookup is loading or returns no results.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Hid the macOS visible overlay when mpv loses focus, is minimized, or is no longer the foreground target so other apps and Spaces are not covered, treating the frontmost mpv app as the focus signal.
|
||||||
|
- Kept the overlay stable through transient window-tracking misses, when mpv remains frontmost but window geometry temporarily disappears from macOS APIs, and when clicking from the overlay back into mpv.
|
||||||
|
- Kept the overlay correctly layered during stats mouse passthrough, opened the stats overlay inactive so it appears over fullscreen mpv without switching Spaces, and fixed passthrough so mpv controls stay clickable before hovering a subtitle bar.
|
||||||
|
- Reduced window-tracker background work by preferring the compiled helper and slowing polls while mpv is stably focused.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Kept playback paused for Yomitan lookup popups opened from the subtitle sidebar when popup auto-pause is enabled.
|
||||||
|
- Fixed Yomitan popups not opening when playback/overlay startup races the Yomitan extension load.
|
||||||
|
- Fixed subtitle sidebar mining so Yomitan-enriched cards use audio and images from the clicked sidebar line instead of the current primary subtitle line.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: release
|
||||||
|
|
||||||
|
- Fixed macOS packaging so the compiled mpv window helper is built into `dist/scripts` and bundled, preventing the overlay from falling back to slow Swift source startup, and removed a stale Windows helper resource entry that produced harmless missing-file warnings.
|
||||||
|
- Fixed one-shot `make clean build install` flows so install picks up the AppImage built earlier in the same make invocation.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: websocket
|
||||||
|
|
||||||
|
- WebSocket: Kept the regular subtitle websocket plain-text only; annotation spans and token metadata now stay on the annotation websocket.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: added
|
||||||
|
area: config
|
||||||
|
|
||||||
|
- Added `subtitleStyle.primaryVisibleOnYomitanPopup` to keep hover-mode primary subtitles visible while a Yomitan popup is open.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: internal
|
||||||
|
area: release
|
||||||
|
|
||||||
|
- Release-note polishing treats pending fragments and reviewed prerelease notes as a cumulative final outcome, collapsing prerelease-only fixes or breakages into the final user-facing change.
|
||||||
|
- Prerelease note generation reuses existing reviewed notes and merges only new fragment material, and `make clean` preserves `release/prerelease-notes.md`.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: changed
|
||||||
|
area: jellyfin
|
||||||
|
|
||||||
|
- Removed the Jellyfin setup server presets dropdown; setup now shows a single editable server URL field.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: internal
|
||||||
|
area: tests
|
||||||
|
|
||||||
|
- Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
type: added
|
||||||
|
area: config
|
||||||
|
|
||||||
|
- Added a dedicated Settings window via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections with click-to-learn keybinding controls (including the AniSkip button key) and AnkiConnect-backed deck, field, and note-type pickers.
|
||||||
|
- Expanded live reload so Settings saves apply immediately for stats keys, logging level, Jimaku, Subsync, YouTube language defaults, Anki field mappings, sentence card model, and selected annotation/runtime options.
|
||||||
|
- Settings search works across all categories, narrows on multi-word terms, and hides settings owned by richer editors.
|
||||||
|
- The note-fields note type picker defaults to the configured Anki deck's note type, then exact `Kiku`, then exact `Lapis`, leaving it blank for manual selection otherwise.
|
||||||
|
- AI and translation settings remain config-file only.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: added
|
||||||
|
area: setup
|
||||||
|
|
||||||
|
- Added optional first-run setup controls to install Bun and the `subminer` command-line launcher on Linux, macOS, and Windows, with a Windows `subminer.cmd` PATH shim so `subminer` works without manually adding `SubMiner.exe` to PATH.
|
||||||
|
- Added an Open SubMiner Settings button to first-run setup and moved Finish to the right-side action slot.
|
||||||
|
- First-run setup recognizes existing `subminer` installs in Homebrew or user PATH directories, while manual setup avoids writing into Homebrew-owned paths.
|
||||||
|
- The standalone setup app quits after completing first-run setup, returning the terminal instead of leaving the process open.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: fixed
|
||||||
|
area: shortcuts
|
||||||
|
|
||||||
|
- Disabled native mpv menu shortcuts during managed macOS playback so configured SubMiner shortcuts work while mpv has focus.
|
||||||
|
- Wired configured session shortcuts, including `stats.markWatchedKey`, through mpv so custom changes work while mpv has focus.
|
||||||
|
- Focus the visible overlay when entering multi-line copy/mine selection so number keys choose the line count on macOS and Windows.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: stats
|
||||||
|
|
||||||
|
- Fixed in-player stats layering so delete confirmations, overlay modals, and update-check dialogs appear above the stats window.
|
||||||
|
- Grouped Jellyfin playback stats under item metadata instead of stream URLs, so watched episodes merge with matching local library titles and keep clean display names.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: changed
|
||||||
|
area: subtitles
|
||||||
|
|
||||||
|
- Subsync now always opens the manual picker and the `subsync.defaultMode` config/settings option has been removed.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
type: changed
|
||||||
|
area: config
|
||||||
|
|
||||||
|
- Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css` and `subtitleStyle.secondary.css`; sidebar appearance uses `subtitleSidebar.css`.
|
||||||
|
- Moved known-word and N+1 annotation colors to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`; legacy Anki color keys are still accepted with deprecation warnings.
|
||||||
|
- Updated subtitle font defaults to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`.
|
||||||
|
- Existing configs are migrated automatically: legacy primary/secondary appearance options and hover token colors fold into `subtitleStyle.css`, and user config files are preserved during legacy compatibility handling.
|
||||||
|
- Live Settings saves apply subtitle CSS declarations immediately to open video overlays, and the generated example config uses the same CSS declaration paths.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: subtitles
|
||||||
|
|
||||||
|
- Kept frequency highlighting for determiner-led noun compounds like `その場` while still filtering standalone determiners.
|
||||||
|
- Fixed frequency annotations for Yomitan single-token compounds with internal particles such as `目の前`, while keeping pure grammar/kana helper spans unannotated.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: fixed
|
||||||
|
area: tray
|
||||||
|
|
||||||
|
- Kept the tray app running when closing tray-launched Yomitan settings, with a close-only menu so closing settings does not quit the tray, and an in-page close button on Hyprland where native window controls are unavailable.
|
||||||
|
- Kept settings loading from blocking other tray actions, serialized copied Yomitan extension refreshes at startup, and disabled the embedded popup preview to avoid renderer hangs during sidebar navigation.
|
||||||
|
- Fixed session help focus handling so the modal can close without mpv running.
|
||||||
|
- Fixed the Windows tray "Open SubMiner Setup" action so it opens the setup window after first-run setup is complete.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: fixed
|
||||||
|
area: updater
|
||||||
|
|
||||||
|
- Linux: `subminer -u` performs release updates independently of any running tray app (reporting `up to date` without downloading when not newer), and update checks use GitHub release metadata/assets instead of the native Electron updater to avoid network-service crashes during startup.
|
||||||
|
- macOS: update dialogs from `subminer -u` reliably appear in the foreground; builds that cannot apply native updates show a manual-install message instead of a restart prompt; `electron-updater` metadata and ZIP downloads route through `/usr/bin/curl` to avoid Electron network crashes while preserving the Squirrel install path; and metadata mismatches from conflicting ZIP filenames are resolved.
|
||||||
|
- Windows: automatic updates keep the native `electron-updater`/NSIS install path while routing updater HTTP through main-process fetch, avoiding the delayed app exit after launch.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: windows
|
||||||
|
|
||||||
|
- Windows startup failures now show a native error dialog and write fatal details to the SubMiner app log instead of exiting silently.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: fixed
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- Downloaded selected YouTube primary subtitles to temporary local files so the primary bar and sidebar read the same source, with cleanup on reload and quit, and suppressed false load-failure notifications by re-checking live mpv subtitle state.
|
||||||
|
- Launcher-managed playback commands create the tray icon even when attaching to an already-running process, and app-owned YouTube playback no longer lets the mpv plugin start a second SubMiner instance.
|
||||||
|
- Logged Linux tray registration failures with a StatusNotifier/AppIndicator hint and documented the Hyprland tray-host requirement.
|
||||||
+164
-124
@@ -7,10 +7,11 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Overlay Auto-Start
|
// Visible Overlay Auto-Start
|
||||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
// Show the visible subtitle overlay automatically after managed mpv playback starts SubMiner.
|
||||||
|
// SubMiner can still auto-start in the background when this is false.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
"auto_start_overlay": true, // Show the visible subtitle overlay automatically when the bundled mpv plugin starts SubMiner. Values: true | false
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Texthooker Server
|
// Texthooker Server
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"texthooker": {
|
"texthooker": {
|
||||||
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
||||||
"openBrowser": false // Open browser setting. Values: true | false
|
"openBrowser": false // Open the texthooker page in the default browser when the server starts. Values: true | false
|
||||||
}, // Configure texthooker startup launch and browser opening behavior.
|
}, // Configure texthooker startup launch and browser opening behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -45,9 +46,16 @@
|
|||||||
// Logging
|
// Logging
|
||||||
// Controls logging verbosity.
|
// Controls logging verbosity.
|
||||||
// Set to debug for full runtime diagnostics.
|
// Set to debug for full runtime diagnostics.
|
||||||
|
// Hot-reload: logging.level and logging.files apply live while SubMiner is running.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"logging": {
|
"logging": {
|
||||||
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
|
"level": "warn", // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||||
|
"rotation": 7, // Number of days of app, launcher, and mpv logs to retain.
|
||||||
|
"files": {
|
||||||
|
"app": true, // Write SubMiner app runtime logs. Values: true | false
|
||||||
|
"launcher": true, // Write launcher command logs. Values: true | false
|
||||||
|
"mpv": false // Write mpv player logs. Enable temporarily when debugging mpv/plugin startup. Values: true | false
|
||||||
|
} // Files setting.
|
||||||
}, // Controls logging verbosity.
|
}, // Controls logging verbosity.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -81,7 +89,7 @@
|
|||||||
"rightStickPress": 10, // Raw button index used for controller R3 input.
|
"rightStickPress": 10, // Raw button index used for controller R3 input.
|
||||||
"leftTrigger": 6, // Raw button index used for controller L2 input.
|
"leftTrigger": 6, // Raw button index used for controller L2 input.
|
||||||
"rightTrigger": 7 // Raw button index used for controller R2 input.
|
"rightTrigger": 7 // Raw button index used for controller R2 input.
|
||||||
}, // Semantic button-name reference mapping used for legacy configs and debug output. Updating it does not rewrite existing raw binding descriptors.
|
}, // Semantic button-name reference mapping used for debug output. Updating it does not rewrite existing raw binding descriptors.
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"toggleLookup": {
|
"toggleLookup": {
|
||||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
@@ -138,7 +146,8 @@
|
|||||||
"axisIndex": 4, // Raw axis index captured for this analog controller action.
|
"axisIndex": 4, // Raw axis index captured for this analog controller action.
|
||||||
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||||
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
|
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||||
} // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
|
}, // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
|
||||||
|
"profiles": {} // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
|
||||||
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -152,33 +161,45 @@
|
|||||||
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
|
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
|
||||||
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
|
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
|
||||||
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
|
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
|
||||||
"jellyfinRemoteSession": true // Warm up Jellyfin remote session at startup. Values: true | false
|
"jellyfinRemoteSession": false // Warm up Jellyfin remote session at startup. Values: true | false
|
||||||
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Updates
|
||||||
|
// Automatic update check behavior.
|
||||||
|
// Manual checks from the tray or launcher are always allowed.
|
||||||
|
// ==========================================
|
||||||
|
"updates": {
|
||||||
|
"enabled": true, // Run automatic update checks in the background. Values: true | false
|
||||||
|
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
|
||||||
|
"notificationType": "system", // How SubMiner announces available updates. Values: system | osd | both | none
|
||||||
|
"channel": "stable" // Release channel used for update checks. Values: stable | prerelease
|
||||||
|
}, // Automatic update check behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keyboard Shortcuts
|
// Keyboard Shortcuts
|
||||||
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
// Hot-reload: shortcut changes apply live and update the session help modal on reopen.
|
// Hot-reload: shortcut changes apply live and update the session help modal on reopen.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Global accelerator that toggles overlay visibility from anywhere on the system. Use null to disable.
|
||||||
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
"copySubtitle": "CommandOrControl+C", // Accelerator that copies the current subtitle line to the clipboard.
|
||||||
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Accelerator that copies consecutive subtitle lines while the multi-copy window stays open.
|
||||||
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
"updateLastCardFromClipboard": "CommandOrControl+V", // Accelerator that updates the last mined Anki card using the current clipboard contents.
|
||||||
"triggerFieldGrouping": "CommandOrControl+G", // Trigger field grouping setting.
|
"triggerFieldGrouping": "CommandOrControl+G", // Accelerator that triggers Kiku field grouping on duplicate cards.
|
||||||
"triggerSubsync": "Ctrl+Alt+S", // Trigger subsync setting.
|
"triggerSubsync": "Ctrl+Alt+S", // Accelerator that triggers subsync against the active subtitle file.
|
||||||
"mineSentence": "CommandOrControl+S", // Mine sentence setting.
|
"mineSentence": "CommandOrControl+S", // Accelerator that mines the current sentence as a new Anki card.
|
||||||
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Mine sentence multiple setting.
|
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Accelerator that mines consecutive sentences while the multi-mine window stays open.
|
||||||
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
|
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
|
||||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
"toggleSecondarySub": "CommandOrControl+Shift+V", // Accelerator that toggles the secondary subtitle bar visibility.
|
||||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
"markAudioCard": "CommandOrControl+Shift+A", // Accelerator that marks the last mined card as an audio card.
|
||||||
"openCharacterDictionary": "CommandOrControl+Alt+A", // Open character dictionary setting.
|
"openCharacterDictionaryManager": "CommandOrControl+D", // Accelerator that opens the character dictionary manager modal.
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
"openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
|
||||||
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
"openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
|
||||||
"openSessionHelp": "CommandOrControl+Slash", // Open session help setting.
|
"openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet.
|
||||||
"openControllerSelect": "Alt+C", // Open controller select setting.
|
"openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal.
|
||||||
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
|
"openControllerDebug": "Alt+Shift+C", // Accelerator that opens the controller debug modal with live axis/button readouts.
|
||||||
"toggleSubtitleSidebar": "Backslash" // Toggle subtitle sidebar setting.
|
"toggleSubtitleSidebar": "Backslash" // Accelerator that toggles the subtitle sidebar visibility.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -315,20 +336,20 @@
|
|||||||
// Hot-reload: defaultMode updates live while SubMiner is running.
|
// Hot-reload: defaultMode updates live while SubMiner is running.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"secondarySub": {
|
"secondarySub": {
|
||||||
"secondarySubLanguages": [], // Secondary sub languages setting.
|
"secondarySubLanguages": [], // Language code priority list used to auto-select a secondary subtitle track when available.
|
||||||
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
"autoLoadSecondarySub": false, // Automatically load a matching secondary subtitle when the primary subtitle loads. Values: true | false
|
||||||
"defaultMode": "hover" // Default mode setting.
|
"defaultMode": "hover" // Default visibility mode for the secondary subtitle bar. Values: hidden | visible | hover
|
||||||
}, // Dual subtitle track options.
|
}, // Dual subtitle track options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Auto Subtitle Sync
|
// Subtitle Sync
|
||||||
// Subsync engine and executable paths.
|
// Subsync engine and executable paths.
|
||||||
|
// Hot-reload: subsync changes apply to the next subtitle sync run.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subsync": {
|
"subsync": {
|
||||||
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
"alass_path": "", // Optional absolute path to the alass binary used by subsync. Leave empty to auto-discover from PATH.
|
||||||
"alass_path": "", // Alass path setting.
|
"ffsubsync_path": "", // Optional absolute path to the ffsubsync binary used by subsync. Leave empty to auto-discover from PATH.
|
||||||
"ffsubsync_path": "", // Ffsubsync path setting.
|
"ffmpeg_path": "", // Optional absolute path to the ffmpeg binary used by subsync. Leave empty to auto-discover from PATH.
|
||||||
"ffmpeg_path": "", // Ffmpeg path setting.
|
|
||||||
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
|
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
|
||||||
}, // Subsync engine and executable paths.
|
}, // Subsync engine and executable paths.
|
||||||
|
|
||||||
@@ -337,7 +358,7 @@
|
|||||||
// Initial vertical subtitle position from the bottom.
|
// Initial vertical subtitle position from the bottom.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitlePosition": {
|
"subtitlePosition": {
|
||||||
"yPercent": 10 // Y percent setting.
|
"yPercent": 10 // Vertical position of the subtitle overlay expressed as a percentage from the bottom of the screen.
|
||||||
}, // Initial vertical subtitle position from the bottom.
|
}, // Initial vertical subtitle position from the bottom.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -347,29 +368,33 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitleStyle": {
|
"subtitleStyle": {
|
||||||
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
|
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
|
||||||
|
"css": {
|
||||||
|
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
|
"color": "#cad3f5", // Color setting.
|
||||||
|
"background-color": "transparent", // Background color setting.
|
||||||
|
"font-size": "35px", // Font size setting.
|
||||||
|
"font-weight": "600", // Font weight setting.
|
||||||
|
"font-style": "normal", // Font style setting.
|
||||||
|
"line-height": "1.35", // Line height setting.
|
||||||
|
"letter-spacing": "-0.01em", // Letter spacing setting.
|
||||||
|
"word-spacing": "0", // Word spacing setting.
|
||||||
|
"font-kerning": "normal", // Font kerning setting.
|
||||||
|
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||||
|
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
||||||
|
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
|
||||||
|
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
|
||||||
|
"--subtitle-hover-token-background-color": "transparent" // Subtitle hover token background color setting.
|
||||||
|
}, // CSS declaration object applied to primary subtitles after normal subtitle style defaults.
|
||||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
||||||
"autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
"autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||||
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
"primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
|
||||||
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
"nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
|
||||||
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
|
"nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
|
||||||
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
|
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
|
||||||
"fontFamily": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
"nPlusOneColor": "#c6a0f6", // Color used for the single N+1 target token subtitle highlight.
|
||||||
"fontSize": 35, // Font size setting.
|
"knownWordColor": "#a6da95", // Color used for known-word subtitle highlights.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
|
||||||
"fontWeight": "600", // 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 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
|
||||||
"fontStyle": "normal", // Font style setting.
|
|
||||||
"backgroundColor": "transparent", // Background color setting.
|
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
|
||||||
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
|
|
||||||
"knownWordColor": "#a6da95", // Known word color setting.
|
|
||||||
"jlptColors": {
|
"jlptColors": {
|
||||||
"N1": "#ed8796", // N1 setting.
|
"N1": "#ed8796", // N1 setting.
|
||||||
"N2": "#f5a97f", // N2 setting.
|
"N2": "#f5a97f", // N2 setting.
|
||||||
@@ -393,19 +418,21 @@
|
|||||||
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||||
}, // Frequency dictionary setting.
|
}, // Frequency dictionary setting.
|
||||||
"secondary": {
|
"secondary": {
|
||||||
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
|
"css": {
|
||||||
"fontSize": 24, // Font size setting.
|
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
"color": "#cad3f5", // Color setting.
|
||||||
"lineHeight": 1.35, // Line height setting.
|
"background-color": "transparent", // Background color setting.
|
||||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
"font-size": "24px", // Font size setting.
|
||||||
"wordSpacing": 0, // Word spacing setting.
|
"font-weight": "600", // Font weight setting.
|
||||||
"fontKerning": "normal", // Font kerning setting.
|
"font-style": "normal", // Font style setting.
|
||||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
"line-height": "1.35", // Line height setting.
|
||||||
"textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
"letter-spacing": "-0.01em", // Letter spacing setting.
|
||||||
"backgroundColor": "transparent", // Background color setting.
|
"word-spacing": "0", // Word spacing setting.
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
"font-kerning": "normal", // Font kerning setting.
|
||||||
"fontWeight": "600", // Font weight setting.
|
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||||
"fontStyle": "normal" // Font style setting.
|
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
||||||
|
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
|
||||||
|
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
|
||||||
} // Secondary setting.
|
} // Secondary setting.
|
||||||
}, // Primary and secondary subtitle styling.
|
}, // Primary and secondary subtitle styling.
|
||||||
|
|
||||||
@@ -419,18 +446,20 @@
|
|||||||
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
|
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
|
||||||
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
|
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
|
||||||
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
|
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
|
||||||
"pauseVideoOnHover": false, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
|
"pauseVideoOnHover": true, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
|
||||||
"autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
|
"autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
|
||||||
"maxWidth": 420, // Maximum sidebar width in CSS pixels.
|
"css": {
|
||||||
"opacity": 0.95, // Base opacity applied to the sidebar shell.
|
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
"backgroundColor": "rgba(73, 77, 100, 0.9)", // Background color for the subtitle sidebar shell.
|
"color": "#cad3f5", // Color setting.
|
||||||
"textColor": "#cad3f5", // Default cue text color in the subtitle sidebar.
|
"background-color": "rgba(73, 77, 100, 0.9)", // Background color setting.
|
||||||
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif", // Font family used for subtitle sidebar cue text.
|
"font-size": "16px", // Font size setting.
|
||||||
"fontSize": 16, // Base font size for subtitle sidebar cue text in CSS pixels.
|
"opacity": "0.95", // Opacity setting.
|
||||||
"timestampColor": "#a5adcb", // Timestamp color in the subtitle sidebar.
|
"--subtitle-sidebar-max-width": "420px", // Subtitle sidebar max width setting.
|
||||||
"activeLineColor": "#f5bde6", // Text color for the active subtitle cue.
|
"--subtitle-sidebar-timestamp-color": "#a5adcb", // Subtitle sidebar timestamp color setting.
|
||||||
"activeLineBackgroundColor": "rgba(138, 173, 244, 0.22)", // Background color for the active subtitle cue.
|
"--subtitle-sidebar-active-line-color": "#f5bde6", // Subtitle sidebar active line color setting.
|
||||||
"hoverLineBackgroundColor": "rgba(54, 58, 79, 0.84)" // Background color for hovered subtitle cues.
|
"--subtitle-sidebar-active-background-color": "rgba(138, 173, 244, 0.22)", // Subtitle sidebar active background color setting.
|
||||||
|
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)" // Subtitle sidebar hover background color setting.
|
||||||
|
} // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
|
||||||
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -441,22 +470,22 @@
|
|||||||
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
|
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
|
||||||
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
|
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
|
||||||
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
|
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
|
||||||
"model": "openai/gpt-4o-mini", // Model setting.
|
"model": "openai/gpt-4o-mini", // Default model identifier requested from the shared AI provider.
|
||||||
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
||||||
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // Default system prompt sent with shared AI provider requests.
|
||||||
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
||||||
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// AnkiConnect Integration
|
// AnkiConnect Integration
|
||||||
// Automatic Anki updates and media generation options.
|
// Automatic Anki updates and media generation options.
|
||||||
// Hot-reload: ankiConnect.ai.enabled updates live while SubMiner is running.
|
// Hot-reload: ankiConnect.ai.enabled, knownWords, nPlusOne, fields.word/audio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, and isKiku.fieldGrouping update live while SubMiner is running.
|
||||||
// Shared AI provider transport settings are read from top-level ai and typically require restart.
|
// Shared AI provider transport settings are read from top-level ai and typically require restart.
|
||||||
// Most other AnkiConnect settings still require restart.
|
// Most other AnkiConnect settings still require restart.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"ankiConnect": {
|
"ankiConnect": {
|
||||||
"enabled": true, // Enable AnkiConnect integration. Values: true | false
|
"enabled": true, // Enable AnkiConnect integration. Values: true | false
|
||||||
"url": "http://127.0.0.1:8765", // Url setting.
|
"url": "http://127.0.0.1:8765", // Base URL of the AnkiConnect HTTP server.
|
||||||
"pollingRate": 3000, // Polling interval in milliseconds.
|
"pollingRate": 3000, // Polling interval in milliseconds.
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
||||||
@@ -467,13 +496,14 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"SubMiner"
|
"SubMiner"
|
||||||
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||||
|
"deck": "", // Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to search all decks.
|
||||||
"fields": {
|
"fields": {
|
||||||
"word": "Expression", // Card field for the mined word or expression text.
|
"word": "Expression", // Card field for the mined word or expression text.
|
||||||
"audio": "ExpressionAudio", // Audio setting.
|
"audio": "ExpressionAudio", // Card field that receives generated sentence audio.
|
||||||
"image": "Picture", // Image setting.
|
"image": "Picture", // Card field that receives the captured screenshot or animated image.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"sentence": "Sentence", // Card field that receives the source sentence text.
|
||||||
"miscInfo": "MiscInfo", // Misc info setting.
|
"miscInfo": "MiscInfo", // Card field that receives the miscellaneous info pattern (see ankiConnect.metadata.pattern).
|
||||||
"translation": "SelectionText" // Translation setting.
|
"translation": "SelectionText" // Card field that receives the current selection or translated text.
|
||||||
}, // Fields setting.
|
}, // Fields setting.
|
||||||
"ai": {
|
"ai": {
|
||||||
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
|
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
|
||||||
@@ -481,59 +511,64 @@
|
|||||||
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
|
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
|
||||||
}, // Ai setting.
|
}, // Ai setting.
|
||||||
"media": {
|
"media": {
|
||||||
"generateAudio": true, // Generate audio setting. Values: true | false
|
"generateAudio": true, // Generate sentence audio for mined cards. Values: true | false
|
||||||
"generateImage": true, // Generate image setting. Values: true | false
|
"generateImage": true, // Generate screenshot or animated image for mined cards. Values: true | false
|
||||||
"imageType": "static", // Image type setting.
|
"imageType": "static", // Image capture type: "static" for a single still frame, "avif" for an animated AVIF. Values: static | avif
|
||||||
"imageFormat": "jpg", // Image format setting.
|
"imageFormat": "jpg", // Encoding format used when imageType is "static". Values: jpg | png | webp
|
||||||
"imageQuality": 92, // Image quality setting.
|
"imageQuality": 92, // Quality (0-100) used for lossy static image encoders.
|
||||||
"animatedFps": 10, // Animated fps setting.
|
"imageMaxWidth": 0, // Maximum width for static images, in pixels. Set to 0 to preserve the source resolution.
|
||||||
"animatedMaxWidth": 640, // Animated max width setting.
|
"imageMaxHeight": 0, // Maximum height for static images, in pixels. Set to 0 to preserve the source resolution.
|
||||||
"animatedCrf": 35, // Animated crf setting.
|
"animatedFps": 10, // Target frame rate for animated AVIF captures.
|
||||||
|
"animatedMaxWidth": 640, // Maximum width applied to animated AVIF captures.
|
||||||
|
"animatedMaxHeight": 0, // Maximum height for animated AVIF captures, in pixels. Set to 0 to preserve aspect ratio.
|
||||||
|
"animatedCrf": 35, // Animated AVIF CRF quality target. Lower values produce larger, higher-quality files.
|
||||||
"syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
|
"syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
|
||||||
"audioPadding": 0.5, // Audio padding setting.
|
"audioPadding": 0, // Seconds of padding appended to both ends of generated sentence audio and animated AVIF clips.
|
||||||
"fallbackDuration": 3, // Fallback duration setting.
|
"fallbackDuration": 3, // Fallback clip duration in seconds when subtitle timing data is unavailable.
|
||||||
"maxMediaDuration": 30 // Max media duration setting.
|
"maxMediaDuration": 30 // Maximum allowed media clip duration in seconds.
|
||||||
}, // Media setting.
|
}, // Media setting.
|
||||||
"knownWords": {
|
"knownWords": {
|
||||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||||
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
|
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
|
||||||
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false
|
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false
|
||||||
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Values: headword | surface
|
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Cache matches always receive known-word highlighting even when POS filters suppress other annotation types. Values: headword | surface
|
||||||
"decks": {}, // Decks and fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word", "Word Reading"] }.
|
"decks": {} // Decks and expression/word fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word"] }.
|
||||||
"color": "#a6da95" // Color used for known-word highlights.
|
|
||||||
}, // Known words setting.
|
}, // Known words setting.
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
"overwriteAudio": true, // When updating an existing card, overwrite the audio field instead of skipping it. Values: true | false
|
||||||
"overwriteImage": true, // Overwrite image setting. Values: true | false
|
"overwriteImage": true, // When updating an existing card, overwrite the image field instead of skipping it. Values: true | false
|
||||||
"mediaInsertMode": "append", // Media insert mode setting.
|
"mediaInsertMode": "append", // Whether new media is appended after or prepended before existing field contents on update. Values: append | prepend
|
||||||
"highlightWord": true, // Highlight word setting. Values: true | false
|
"highlightWord": true, // Bold the mined word inside the sentence field on the saved Anki card. Values: true | false
|
||||||
"notificationType": "osd", // Notification type setting.
|
"notificationType": "osd", // Notification surface used to announce mining and update outcomes. Values: osd | system | both | none
|
||||||
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
||||||
}, // Behavior setting.
|
}, // Behavior setting.
|
||||||
"nPlusOne": {
|
"nPlusOne": {
|
||||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
"enabled": false, // Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Requires known-word cache data. Values: true | false
|
||||||
"nPlusOne": "#c6a0f6" // Color used for the single N+1 target token highlight.
|
"minSentenceWords": 3 // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||||
}, // N plus one setting.
|
}, // N plus one setting.
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
|
"pattern": "[SubMiner] %f (%t)" // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
|
||||||
}, // Metadata setting.
|
}, // Metadata setting.
|
||||||
"isLapis": {
|
"isLapis": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enable Lapis-specific mining behaviors and sentence card model targeting. Values: true | false
|
||||||
"sentenceCardModel": "Japanese sentences" // Sentence card model setting.
|
"sentenceCardModel": "Lapis" // Note type name used by Lapis sentence cards.
|
||||||
}, // Is lapis setting.
|
}, // Is lapis setting.
|
||||||
"isKiku": {
|
"isKiku": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enable Kiku-specific mining behaviors (duplicate handling, field grouping). Values: true | false
|
||||||
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
||||||
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
|
"deleteDuplicateInAuto": true // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
|
||||||
} // Is kiku setting.
|
} // Is kiku setting.
|
||||||
}, // Automatic Anki updates and media generation options.
|
}, // Automatic Anki updates and media generation options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Jimaku
|
// Jimaku
|
||||||
// Jimaku API configuration and defaults.
|
// Jimaku API configuration and defaults.
|
||||||
|
// Hot-reload: Jimaku changes apply to the next Jimaku request.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"jimaku": {
|
"jimaku": {
|
||||||
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
"apiBaseUrl": "https://jimaku.cc", // Base URL of the Jimaku subtitle search API.
|
||||||
|
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
|
||||||
|
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
|
||||||
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
||||||
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
||||||
}, // Jimaku API configuration and defaults.
|
}, // Jimaku API configuration and defaults.
|
||||||
@@ -541,6 +576,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// YouTube Playback Settings
|
// YouTube Playback Settings
|
||||||
// Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
// Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
||||||
|
// Hot-reload: primarySubLanguages applies to the next YouTube subtitle load.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"youtube": {
|
"youtube": {
|
||||||
"primarySubLanguages": [
|
"primarySubLanguages": [
|
||||||
@@ -559,11 +595,8 @@
|
|||||||
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
||||||
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
||||||
"characterDictionary": {
|
"characterDictionary": {
|
||||||
"enabled": false, // Enable automatic Yomitan character dictionary sync for currently watched AniList media. Values: true | false
|
|
||||||
"refreshTtlHours": 168, // Legacy setting; merged character dictionary retention is now usage-based and this value is ignored.
|
|
||||||
"maxLoaded": 3, // Maximum number of most-recently-used anime snapshots included in the merged Yomitan character dictionary.
|
"maxLoaded": 3, // Maximum number of most-recently-used anime snapshots included in the merged Yomitan character dictionary.
|
||||||
"evictionPolicy": "delete", // Legacy setting; merged character dictionary eviction is usage-based and this value is ignored. Values: disable | delete
|
"profileScope": "all", // Yomitan profile scope for character dictionary settings updates. Values: all | active
|
||||||
"profileScope": "all", // Yomitan profile scope for dictionary enable/disable updates. Values: all | active
|
|
||||||
"collapsibleSections": {
|
"collapsibleSections": {
|
||||||
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
|
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
|
||||||
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
|
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
|
||||||
@@ -585,14 +618,25 @@
|
|||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// MPV Launcher
|
// MPV Launcher
|
||||||
// Optional mpv.exe override for Windows playback entry points.
|
// SubMiner-managed mpv launch and bundled plugin options.
|
||||||
|
// Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin.
|
||||||
|
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
|
||||||
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.
|
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.
|
||||||
|
// Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none.
|
||||||
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"mpv": {
|
"mpv": {
|
||||||
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||||
"launchMode": "normal" // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
"launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
||||||
}, // Optional mpv.exe override for Windows playback entry points.
|
"profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
|
||||||
|
"socketPath": "\\\\.\\pipe\\subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
|
||||||
|
"backend": "auto", // Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform. Values: auto | hyprland | sway | x11 | macos | windows
|
||||||
|
"autoStartSubMiner": true, // Start SubMiner in the background when SubMiner-managed mpv loads a file. Values: true | false
|
||||||
|
"pauseUntilOverlayReady": true, // Pause mpv on visible-overlay auto-start until SubMiner signals subtitle tokenization readiness. Values: true | false
|
||||||
|
"subminerBinaryPath": "", // Optional SubMiner app binary path passed to the bundled mpv plugin. Leave empty to use the launcher-detected app path.
|
||||||
|
"aniskipEnabled": true, // Enable AniSkip intro detection and skip markers in the bundled mpv plugin. Values: true | false
|
||||||
|
"aniskipButtonKey": "TAB" // mpv key used to trigger the AniSkip button while the skip marker is visible.
|
||||||
|
}, // SubMiner-managed mpv launch and bundled plugin options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Jellyfin
|
// Jellyfin
|
||||||
@@ -605,14 +649,10 @@
|
|||||||
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
|
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
|
||||||
"recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup.
|
"recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup.
|
||||||
"username": "", // Default Jellyfin username used during CLI login.
|
"username": "", // Default Jellyfin username used during CLI login.
|
||||||
"deviceId": "subminer", // Device id setting.
|
|
||||||
"clientName": "SubMiner", // Client name setting.
|
|
||||||
"clientVersion": "0.1.0", // Client version setting.
|
|
||||||
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
|
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
|
||||||
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
|
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
|
||||||
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
|
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
|
||||||
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
|
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
|
||||||
"remoteControlDeviceName": "SubMiner", // Device name reported for Jellyfin remote control sessions.
|
|
||||||
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
||||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
||||||
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
||||||
@@ -635,7 +675,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"discordPresence": {
|
"discordPresence": {
|
||||||
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
|
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||||
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal".
|
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal". Values: default | meme | japanese | minimal
|
||||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||||
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||||
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||||
|
|||||||
+385
-66
@@ -1,27 +1,398 @@
|
|||||||
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
||||||
|
import { extname, join, posix, resolve, sep } from 'node:path';
|
||||||
|
import type { DefaultTheme, HeadConfig, TransformContext, UserConfig } from 'vitepress';
|
||||||
|
|
||||||
function pageToCanonicalHref(page: string): string | null {
|
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
|
||||||
|
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
|
||||||
|
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
|
||||||
|
const PLAUSIBLE_ENDPOINT = `${PLAUSIBLE_PROXY_HOSTNAME}/api/event`;
|
||||||
|
const PLAUSIBLE_INIT_SCRIPT = [
|
||||||
|
'window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};',
|
||||||
|
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
type DocsChannel = 'stable-root' | 'stable-archive' | 'main';
|
||||||
|
|
||||||
|
type VersionManifest = {
|
||||||
|
latestStable: string;
|
||||||
|
channels: Array<{ label: string; path: string }>;
|
||||||
|
versions: Array<{ version: string; path: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function optionalEnv(value: string | undefined): string | undefined {
|
||||||
|
return value && value !== 'undefined' ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = normalizeBase(optionalEnv(process.env.SUBMINER_DOCS_BASE) ?? '/');
|
||||||
|
const outDir = optionalEnv(process.env.SUBMINER_DOCS_OUT_DIR);
|
||||||
|
const docsSourceDir = optionalEnv(process.env.SUBMINER_DOCS_SOURCE_DIR) ?? process.cwd();
|
||||||
|
const channel = normalizeChannel(optionalEnv(process.env.SUBMINER_DOCS_CHANNEL));
|
||||||
|
const docsVersion = optionalEnv(process.env.SUBMINER_DOCS_VERSION);
|
||||||
|
const latestStable = optionalEnv(process.env.SUBMINER_DOCS_LATEST_STABLE) ?? 'v0.14.0';
|
||||||
|
const versionManifest = parseVersionManifest(process.env.SUBMINER_DOCS_VERSION_MANIFEST);
|
||||||
|
const versionLinkOrigin =
|
||||||
|
optionalEnv(process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN) ?? 'production';
|
||||||
|
|
||||||
|
function getLocalArchiveDir(): string {
|
||||||
|
return resolve(
|
||||||
|
optionalEnv(process.env.SUBMINER_DOCS_LOCAL_ARCHIVE_DIR) ??
|
||||||
|
join(docsSourceDir, '..', '.tmp/docs-versioned-site'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBase(value: string): string {
|
||||||
|
if (!value || value === '/') return '/';
|
||||||
|
return `/${value.replace(/^\/+|\/+$/g, '')}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeChannel(value: string | undefined): DocsChannel {
|
||||||
|
if (value === 'main' || value === 'stable-archive') return value;
|
||||||
|
return 'stable-root';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVersionManifest(value: string | undefined): VersionManifest {
|
||||||
|
if (!value || value === 'undefined') {
|
||||||
|
return {
|
||||||
|
latestStable,
|
||||||
|
channels: [
|
||||||
|
{ label: 'Latest stable', path: '/' },
|
||||||
|
{ label: 'main', path: '/main/' },
|
||||||
|
],
|
||||||
|
versions: [{ version: latestStable, path: `/v/${latestStable.replace(/^v/, '')}/` }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(value) as VersionManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withDocsBase(path: string): string {
|
||||||
|
if (/^[a-z]+:\/\//i.test(path)) return path;
|
||||||
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
if (base === '/') return normalizedPath;
|
||||||
|
return `${base.replace(/\/$/, '')}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageToRoute(page: string): string | null {
|
||||||
if (page === '404.md') return null;
|
if (page === '404.md') return null;
|
||||||
|
|
||||||
const route = page
|
const route = page
|
||||||
.replace(/(^|\/)index\.md$/, '')
|
.replace(/(^|\/)index\.md$/, '')
|
||||||
.replace(/\.md$/, '')
|
.replace(/\.md$/, '')
|
||||||
.replace(/\/$/, '');
|
.replace(/\/$/, '');
|
||||||
return route ? `${DOCS_HOSTNAME}/${route}` : `${DOCS_HOSTNAME}/`;
|
return route ? `/${route}` : '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
function pageToCanonicalHref(page: string): string | null {
|
||||||
|
const route = pageToRoute(page);
|
||||||
|
if (!route) return null;
|
||||||
|
|
||||||
|
if (channel === 'main') {
|
||||||
|
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel === 'stable-archive' && docsVersion !== latestStable) {
|
||||||
|
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return route === '/' ? `${DOCS_HOSTNAME}/` : `${DOCS_HOSTNAME}${route}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalRouteWithBase(route: string): string {
|
||||||
|
const routeWithBase = withDocsBase(route);
|
||||||
|
return route === '/' ? routeWithBase : routeWithBase.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformPageHead({ page }: TransformContext): HeadConfig[] {
|
||||||
|
const href = pageToCanonicalHref(page);
|
||||||
|
const head: HeadConfig[] = href ? [['link', { rel: 'canonical', href }]] : [];
|
||||||
|
|
||||||
|
if (channel === 'main') {
|
||||||
|
head.push(['meta', { name: 'robots', content: 'noindex,follow' }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkToPagePath(link: string): string | null {
|
||||||
|
if (!link.startsWith('/') || link.startsWith('/v/') || link.startsWith('/main/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutHash = link.split('#')[0] ?? '/';
|
||||||
|
const withoutQuery = withoutHash.split('?')[0] ?? '/';
|
||||||
|
const route = withoutQuery.replace(/^\/+|\/+$/g, '');
|
||||||
|
return route ? `${route}.md` : 'index.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPageForLink(link: string): boolean {
|
||||||
|
const pagePath = linkToPagePath(link);
|
||||||
|
if (!pagePath) return true;
|
||||||
|
return existsSync(join(docsSourceDir, pagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterNav(items: DefaultTheme.NavItem[]): DefaultTheme.NavItem[] {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
if ('items' in item && item.items) {
|
||||||
|
return { ...item, items: filterNav(item.items as DefaultTheme.NavItem[]) };
|
||||||
|
}
|
||||||
|
if ('link' in item && item.link && !hasPageForLink(item.link)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
.filter((item): item is DefaultTheme.NavItem => Boolean(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSidebar(items: DefaultTheme.SidebarItem[]): DefaultTheme.SidebarItem[] {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
const filteredChildren = item.items ? filterSidebar(item.items) : undefined;
|
||||||
|
if (item.link && !hasPageForLink(item.link)) return null;
|
||||||
|
if (item.items && filteredChildren?.length === 0 && !item.link) return null;
|
||||||
|
return { ...item, items: filteredChildren };
|
||||||
|
})
|
||||||
|
.filter((item): item is DefaultTheme.SidebarItem => Boolean(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function versionSwitchLink(path: string): string {
|
||||||
|
if (/^[a-z]+:\/\//i.test(path)) return path;
|
||||||
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
if (versionLinkOrigin === 'local') return localVersionSwitchLink(normalizedPath);
|
||||||
|
return `${DOCS_HOSTNAME}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function localVersionSwitchLink(path: string): string {
|
||||||
|
if (base === '/') return path;
|
||||||
|
|
||||||
|
const basePath = base.replace(/\/$/, '');
|
||||||
|
const targetPath = path === '/' ? '/' : path.replace(/\/$/, '');
|
||||||
|
const relativePath = posix.relative(basePath, targetPath) || '.';
|
||||||
|
|
||||||
|
return path.endsWith('/') ? `${relativePath}/` : relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldHandleLocalVersionRoute(pathname: string): boolean {
|
||||||
|
if (base !== '/' || channel !== 'stable-root') return false;
|
||||||
|
return /^\/main(?:\/|$)/.test(pathname) || /^\/v\/[^/]+(?:\/|$)/.test(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentTypeForPath(path: string): string {
|
||||||
|
switch (extname(path)) {
|
||||||
|
case '.css':
|
||||||
|
return 'text/css; charset=utf-8';
|
||||||
|
case '.gif':
|
||||||
|
return 'image/gif';
|
||||||
|
case '.ico':
|
||||||
|
return 'image/x-icon';
|
||||||
|
case '.jpg':
|
||||||
|
case '.jpeg':
|
||||||
|
return 'image/jpeg';
|
||||||
|
case '.js':
|
||||||
|
case '.mjs':
|
||||||
|
return 'text/javascript; charset=utf-8';
|
||||||
|
case '.json':
|
||||||
|
case '.jsonc':
|
||||||
|
return 'application/json; charset=utf-8';
|
||||||
|
case '.mp4':
|
||||||
|
return 'video/mp4';
|
||||||
|
case '.png':
|
||||||
|
return 'image/png';
|
||||||
|
case '.svg':
|
||||||
|
return 'image/svg+xml';
|
||||||
|
case '.ttf':
|
||||||
|
return 'font/ttf';
|
||||||
|
case '.webm':
|
||||||
|
return 'video/webm';
|
||||||
|
case '.woff':
|
||||||
|
return 'font/woff';
|
||||||
|
case '.woff2':
|
||||||
|
return 'font/woff2';
|
||||||
|
case '.xml':
|
||||||
|
return 'application/xml; charset=utf-8';
|
||||||
|
default:
|
||||||
|
return 'text/html; charset=utf-8';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFile(path: string): boolean {
|
||||||
|
try {
|
||||||
|
return statSync(path).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function archiveFileForPathname(pathname: string): string | null {
|
||||||
|
if (!shouldHandleLocalVersionRoute(pathname)) return null;
|
||||||
|
|
||||||
|
const localArchiveDir = getLocalArchiveDir();
|
||||||
|
const routePath = decodeURIComponent(pathname).replace(/^\/+/, '');
|
||||||
|
const filePath = resolve(localArchiveDir, routePath);
|
||||||
|
if (filePath !== localArchiveDir && !filePath.startsWith(`${localArchiveDir}${sep}`)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = pathname.endsWith('/')
|
||||||
|
? [join(filePath, 'index.html')]
|
||||||
|
: extname(filePath)
|
||||||
|
? [filePath]
|
||||||
|
: [`${filePath}.html`, join(filePath, 'index.html')];
|
||||||
|
|
||||||
|
return candidates.find(isFile) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveLocalArchiveRoute(pathname: string, response: DevServerResponse): boolean {
|
||||||
|
if (
|
||||||
|
(optionalEnv(process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN) ?? versionLinkOrigin) !== 'local'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = archiveFileForPathname(pathname);
|
||||||
|
if (!filePath) return false;
|
||||||
|
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.setHeader('Content-Type', contentTypeForPath(filePath));
|
||||||
|
response.end(readFileSync(filePath));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DevServerResponse = {
|
||||||
|
statusCode: number;
|
||||||
|
setHeader(name: string, value: string): void;
|
||||||
|
end(chunk?: string | Uint8Array): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionItems = [
|
||||||
|
{
|
||||||
|
text: `Latest stable (${versionManifest.latestStable})`,
|
||||||
|
link: versionSwitchLink('/'),
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
},
|
||||||
|
...versionManifest.channels
|
||||||
|
.filter((entry) => entry.label !== 'Latest stable')
|
||||||
|
.map((entry) => ({
|
||||||
|
text: entry.label,
|
||||||
|
link: versionSwitchLink(entry.path),
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
})),
|
||||||
|
...versionManifest.versions.map((entry) => ({
|
||||||
|
text: entry.version,
|
||||||
|
link: versionSwitchLink(entry.path),
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const nav: DefaultTheme.NavItem[] = [
|
||||||
|
{ text: 'Home', link: '/' },
|
||||||
|
{ text: 'Get Started', link: '/installation' },
|
||||||
|
{ text: 'Mining', link: '/mining-workflow' },
|
||||||
|
{ text: 'Configuration', link: '/configuration' },
|
||||||
|
{ text: 'Changelog', link: '/changelog' },
|
||||||
|
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||||
|
{ text: docsVersion ?? (channel === 'main' ? 'main' : latestStable), items: versionItems },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sidebar: DefaultTheme.SidebarItem[] = [
|
||||||
|
{
|
||||||
|
text: 'Getting Started',
|
||||||
|
items: [
|
||||||
|
{ text: 'Overview', link: '/' },
|
||||||
|
{ text: 'Installation', link: '/installation' },
|
||||||
|
{ text: 'Usage', link: '/usage' },
|
||||||
|
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
||||||
|
{ text: 'Launcher Script', link: '/launcher-script' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Reference',
|
||||||
|
items: [
|
||||||
|
{ text: 'Configuration', link: '/configuration' },
|
||||||
|
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
|
||||||
|
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
||||||
|
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
|
||||||
|
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
||||||
|
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Integrations',
|
||||||
|
items: [
|
||||||
|
{ text: 'MPV Plugin', link: '/mpv-plugin' },
|
||||||
|
{ text: 'Anki', link: '/anki-integration' },
|
||||||
|
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
||||||
|
{ text: 'YouTube', link: '/youtube-integration' },
|
||||||
|
{ text: 'Jimaku', link: '/jimaku-integration' },
|
||||||
|
{ text: 'AniList', link: '/anilist-integration' },
|
||||||
|
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Development',
|
||||||
|
items: [
|
||||||
|
{ text: 'Building & Testing', link: '/development' },
|
||||||
|
{ text: 'Architecture', link: '/architecture' },
|
||||||
|
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
||||||
|
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
|
||||||
|
{ text: 'Changelog', link: '/changelog' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const config: UserConfig = {
|
||||||
title: 'SubMiner Docs',
|
title: 'SubMiner Docs',
|
||||||
description:
|
description:
|
||||||
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
|
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
|
||||||
|
base,
|
||||||
|
...(outDir ? { outDir } : {}),
|
||||||
|
vite: {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'subminer-docs-local-version-redirects',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use((request, response, next) => {
|
||||||
|
const requestUrl = new URL(request.url ?? '/', 'http://localhost');
|
||||||
|
if (serveLocalArchiveRoute(requestUrl.pathname, response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldHandleLocalVersionRoute(requestUrl.pathname)) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.statusCode = 302;
|
||||||
|
response.setHeader(
|
||||||
|
'Location',
|
||||||
|
`${DOCS_HOSTNAME}${requestUrl.pathname}${requestUrl.search}`,
|
||||||
|
);
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
head: [
|
head: [
|
||||||
['link', { rel: 'icon', href: '/favicon.ico', sizes: 'any' }],
|
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
|
||||||
|
[
|
||||||
|
'script',
|
||||||
|
{
|
||||||
|
async: '',
|
||||||
|
src: `${PLAUSIBLE_PROXY_HOSTNAME}${PLAUSIBLE_SITE_SCRIPT_PATH}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['script', {}, PLAUSIBLE_INIT_SCRIPT],
|
||||||
|
['link', { rel: 'icon', href: withDocsBase('/favicon.ico'), sizes: 'any' }],
|
||||||
[
|
[
|
||||||
'link',
|
'link',
|
||||||
{
|
{
|
||||||
rel: 'icon',
|
rel: 'icon',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
href: '/favicon-32x32.png',
|
href: withDocsBase('/favicon-32x32.png'),
|
||||||
sizes: '32x32',
|
sizes: '32x32',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -30,7 +401,7 @@ export default {
|
|||||||
{
|
{
|
||||||
rel: 'icon',
|
rel: 'icon',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
href: '/favicon-16x16.png',
|
href: withDocsBase('/favicon-16x16.png'),
|
||||||
sizes: '16x16',
|
sizes: '16x16',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -38,7 +409,7 @@ export default {
|
|||||||
'link',
|
'link',
|
||||||
{
|
{
|
||||||
rel: 'apple-touch-icon',
|
rel: 'apple-touch-icon',
|
||||||
href: '/apple-touch-icon.png',
|
href: withDocsBase('/apple-touch-icon.png'),
|
||||||
sizes: '180x180',
|
sizes: '180x180',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -54,12 +425,9 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
transformHead({ page }) {
|
transformHead: transformPageHead,
|
||||||
const href = pageToCanonicalHref(page);
|
|
||||||
return href ? [['link', { rel: 'canonical', href }]] : [];
|
|
||||||
},
|
|
||||||
lastUpdated: true,
|
lastUpdated: true,
|
||||||
srcExclude: ['subagents/**'],
|
srcExclude: ['subagents/**', 'README.md'],
|
||||||
markdown: {
|
markdown: {
|
||||||
theme: {
|
theme: {
|
||||||
light: 'catppuccin-latte',
|
light: 'catppuccin-latte',
|
||||||
@@ -72,59 +440,8 @@ export default {
|
|||||||
dark: '/assets/SubMiner.png',
|
dark: '/assets/SubMiner.png',
|
||||||
},
|
},
|
||||||
siteTitle: 'SubMiner Docs',
|
siteTitle: 'SubMiner Docs',
|
||||||
nav: [
|
nav: filterNav(nav),
|
||||||
{ text: 'Home', link: '/' },
|
sidebar: filterSidebar(sidebar),
|
||||||
{ text: 'Get Started', link: '/installation' },
|
|
||||||
{ text: 'Mining', link: '/mining-workflow' },
|
|
||||||
{ text: 'Configuration', link: '/configuration' },
|
|
||||||
{ text: 'Changelog', link: '/changelog' },
|
|
||||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
|
||||||
],
|
|
||||||
sidebar: [
|
|
||||||
{
|
|
||||||
text: 'Getting Started',
|
|
||||||
items: [
|
|
||||||
{ text: 'Overview', link: '/' },
|
|
||||||
{ text: 'Installation', link: '/installation' },
|
|
||||||
{ text: 'Usage', link: '/usage' },
|
|
||||||
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
|
||||||
{ text: 'Launcher Script', link: '/launcher-script' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Reference',
|
|
||||||
items: [
|
|
||||||
{ text: 'Configuration', link: '/configuration' },
|
|
||||||
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
|
|
||||||
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
|
||||||
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
|
|
||||||
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
|
||||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Integrations',
|
|
||||||
items: [
|
|
||||||
{ text: 'MPV Plugin', link: '/mpv-plugin' },
|
|
||||||
{ text: 'Anki', link: '/anki-integration' },
|
|
||||||
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
|
||||||
{ text: 'YouTube', link: '/youtube-integration' },
|
|
||||||
{ text: 'Jimaku', link: '/jimaku-integration' },
|
|
||||||
{ text: 'AniList', link: '/anilist-integration' },
|
|
||||||
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Development',
|
|
||||||
items: [
|
|
||||||
{ text: 'Building & Testing', link: '/development' },
|
|
||||||
{ text: 'Architecture', link: '/architecture' },
|
|
||||||
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
|
||||||
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
|
|
||||||
{ text: 'Changelog', link: '/changelog' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
search: {
|
search: {
|
||||||
provider: 'local',
|
provider: 'local',
|
||||||
},
|
},
|
||||||
@@ -143,3 +460,5 @@ export default {
|
|||||||
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
|
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useRoute, useData } from 'vitepress';
|
import { useRoute, useData } from 'vitepress';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { formatStatusLineFilePath } from '../status-line';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { page, frontmatter } = useData();
|
const { page, frontmatter } = useData();
|
||||||
@@ -12,8 +13,7 @@ const mode = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const filePath = computed(() => {
|
const filePath = computed(() => {
|
||||||
const path = route.path;
|
return formatStatusLineFilePath(route.path);
|
||||||
return path === '/' ? 'index.md' : `${path.replace(/^\//, '')}.md`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const section = computed(() => {
|
const section = computed(() => {
|
||||||
|
|||||||
@@ -7,32 +7,7 @@ import './mermaid-modal.css';
|
|||||||
import TuiLayout from './TuiLayout.vue';
|
import TuiLayout from './TuiLayout.vue';
|
||||||
|
|
||||||
let mermaidLoader: Promise<any> | null = null;
|
let mermaidLoader: Promise<any> | null = null;
|
||||||
let plausibleTrackerInitialized = false;
|
|
||||||
const MERMAID_MODAL_ID = 'mermaid-diagram-modal';
|
const MERMAID_MODAL_ID = 'mermaid-diagram-modal';
|
||||||
const PLAUSIBLE_DOMAIN = 'subminer.moe';
|
|
||||||
const PLAUSIBLE_ENABLED_HOSTNAMES = new Set(['docs.subminer.moe']);
|
|
||||||
const PLAUSIBLE_ENDPOINT = 'https://worker.subminer.moe/api/capture';
|
|
||||||
|
|
||||||
async function initPlausibleTracker() {
|
|
||||||
if (typeof window === 'undefined' || plausibleTrackerInitialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PLAUSIBLE_ENABLED_HOSTNAMES.has(window.location.hostname)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { init } = await import('@plausible-analytics/tracker');
|
|
||||||
init({
|
|
||||||
domain: PLAUSIBLE_DOMAIN,
|
|
||||||
endpoint: PLAUSIBLE_ENDPOINT,
|
|
||||||
outboundLinks: true,
|
|
||||||
fileDownloads: true,
|
|
||||||
formSubmissions: true,
|
|
||||||
captureOnLocalhost: false,
|
|
||||||
});
|
|
||||||
plausibleTrackerInitialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeMermaidModal() {
|
function closeMermaidModal() {
|
||||||
if (typeof document === 'undefined') {
|
if (typeof document === 'undefined') {
|
||||||
@@ -222,9 +197,6 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initPlausibleTracker().catch((error) => {
|
|
||||||
console.error('Failed to initialize Plausible tracker:', error);
|
|
||||||
});
|
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
watch(() => route.path, render);
|
watch(() => route.path, render);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { expect, test } from 'bun:test';
|
||||||
|
import { formatStatusLineFilePath } from './status-line';
|
||||||
|
|
||||||
|
test('status line file path formats root home as index markdown', () => {
|
||||||
|
expect(formatStatusLineFilePath('/')).toBe('index.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('status line file path formats version archive home without trailing slash', () => {
|
||||||
|
expect(formatStatusLineFilePath('/v/0.12.0/')).toBe('v/0.12.0.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('status line file path keeps normal docs routes as markdown files', () => {
|
||||||
|
expect(formatStatusLineFilePath('/v/0.12.0/configuration')).toBe(
|
||||||
|
'v/0.12.0/configuration.md',
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export function formatStatusLineFilePath(routePath: string): string {
|
||||||
|
if (routePath === '/') return 'index.md';
|
||||||
|
return `${routePath.replace(/^\/|\/$/g, '')}.md`;
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'M PLUS 1';
|
font-family: 'M PLUS 1';
|
||||||
src: url('/assets/fonts/Mplus1-Medium.ttf') format('truetype');
|
src: url('../../public/assets/fonts/Mplus1-Medium.ttf') format('truetype');
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Manrope Default';
|
font-family: 'Manrope Default';
|
||||||
src: url('/assets/fonts/manrope-latin-600-normal.ttf') format('truetype');
|
src: url('../../public/assets/fonts/manrope-latin-600-normal.ttf') format('truetype');
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
|
|||||||
+12
-5
@@ -30,9 +30,16 @@ bun run docs:dev
|
|||||||
## Cloudflare Pages
|
## Cloudflare Pages
|
||||||
|
|
||||||
- Git repo: `ksyasuda/SubMiner`
|
- Git repo: `ksyasuda/SubMiner`
|
||||||
- Root directory: `docs-site`
|
- Production branch: `main`
|
||||||
- Build command: `bun run docs:build`
|
- Automatic production and preview deployments: disabled
|
||||||
- Build output directory: `.vitepress/dist`
|
- Custom domain: `docs.subminer.moe` attached to Production
|
||||||
- Build watch paths: `docs-site/*`
|
- Deployment path: GitHub Actions direct upload with Wrangler
|
||||||
|
|
||||||
Cloudflare Pages watch paths use a single `*` wildcard for monorepo subdirectories. `docs-site/*` matches nested files under the docs site; `docs-site/**` can cause docs-only pushes to be skipped.
|
The public docs root is stable-only:
|
||||||
|
|
||||||
|
- `/` serves the latest stable release docs.
|
||||||
|
- `/main/` serves development docs from `main` and is marked `noindex,follow`.
|
||||||
|
- `/v/<version>/` serves stable release archives.
|
||||||
|
- Prerelease tags do not update the docs site.
|
||||||
|
|
||||||
|
Keep Cloudflare Git auto-deploy disabled. The production deploy is `.github/workflows/docs-pages.yml`, which uploads `.tmp/docs-versioned-site` with `--branch main` so tag-triggered runs update Production instead of creating preview deployments.
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ SubMiner can sync your watch progress to [AniList](https://anilist.co) automatic
|
|||||||
|
|
||||||
AniList data also powers two additional features: [cover art](#cover-art) for the stats dashboard and the [Character Dictionary](/character-dictionary) for in-overlay name lookup.
|
AniList data also powers two additional features: [cover art](#cover-art) for the stats dashboard and the [Character Dictionary](/character-dictionary) for in-overlay name lookup.
|
||||||
|
|
||||||
|
[AniList](https://anilist.co) is a free website for tracking which anime you have watched. An **access token** is a private key SubMiner stores so it can update your list on your behalf — you approve it once during setup, and you never paste a password into SubMiner.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
AniList integration is opt-in. To enable it:
|
AniList integration is opt-in. To enable it:
|
||||||
@@ -17,8 +19,8 @@ AniList integration is opt-in. To enable it:
|
|||||||
{
|
{
|
||||||
"anilist": {
|
"anilist": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"accessToken": ""
|
"accessToken": "",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -37,20 +39,20 @@ SubMiner monitors playback and triggers an AniList progress update when an episo
|
|||||||
The update flow:
|
The update flow:
|
||||||
|
|
||||||
1. **Title detection** -- SubMiner extracts the anime title, season, and episode number from the media filename. It tries [`guessit`](https://github.com/guessit-io/guessit) first for accurate parsing, then falls back to an internal filename parser if guessit is unavailable.
|
1. **Title detection** -- SubMiner extracts the anime title, season, and episode number from the media filename. It tries [`guessit`](https://github.com/guessit-io/guessit) first for accurate parsing, then falls back to an internal filename parser if guessit is unavailable.
|
||||||
2. **AniList search** -- The detected title is searched against the AniList GraphQL API. SubMiner picks the best match by comparing titles (romaji, English, native) and filtering by episode count.
|
2. **AniList search** -- The detected title is searched against the AniList GraphQL API. For season 2 and later files, SubMiner searches the season-specific title first, then falls back to the base title. SubMiner picks the best match by comparing titles (romaji, English, native) and filtering by episode count.
|
||||||
3. **Progress check** -- SubMiner fetches your current list entry for the matched media. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
|
3. **Progress check** -- SubMiner fetches your current list entry for the matched media. The media must already be in Planning or Watching; otherwise SubMiner shows an MPV message explaining that the update is not possible. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
|
||||||
4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`.
|
4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`.
|
||||||
|
|
||||||
## Update Queue and Retry
|
## Update Queue and Retry
|
||||||
|
|
||||||
Failed AniList updates are persisted to a retry queue on disk and retried with exponential backoff.
|
Failed AniList updates are persisted to a retry queue on disk and retried with exponential backoff.
|
||||||
|
|
||||||
| Parameter | Value |
|
| Parameter | Value |
|
||||||
| --- | --- |
|
| ---------------- | ---------- |
|
||||||
| Initial backoff | 30 seconds |
|
| Initial backoff | 30 seconds |
|
||||||
| Maximum backoff | 6 hours |
|
| Maximum backoff | 6 hours |
|
||||||
| Maximum attempts | 8 |
|
| Maximum attempts | 8 |
|
||||||
| Queue capacity | 500 items |
|
| Queue capacity | 500 items |
|
||||||
|
|
||||||
After 8 failed attempts, the update is moved to a dead-letter queue and no longer retried automatically. The queue is persisted across restarts so no updates are lost if SubMiner exits before a retry succeeds.
|
After 8 failed attempts, the update is moved to a dead-letter queue and no longer retried automatically. The queue is persisted across restarts so no updates are lost if SubMiner exits before a retry succeeds.
|
||||||
|
|
||||||
@@ -85,36 +87,36 @@ All AniList API calls go through a shared rate limiter that enforces a sliding w
|
|||||||
"collapsibleSections": {
|
"collapsibleSections": {
|
||||||
"description": false,
|
"description": false,
|
||||||
"characterInformation": false,
|
"characterInformation": false,
|
||||||
"voicedBy": false
|
"voicedBy": false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Values | Description |
|
| Option | Values | Description |
|
||||||
| --- | --- | --- |
|
| ------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
| `enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
|
| `enabled` | `true`, `false` | Enable AniList post-watch progress updates (default: `false`) |
|
||||||
| `accessToken` | string | Explicit AniList access token override; when blank, SubMiner uses the stored encrypted token (default: `""`) |
|
| `accessToken` | string | Explicit AniList access token override; when blank, SubMiner uses the stored encrypted token (default: `""`) |
|
||||||
| `characterDictionary.enabled` | `true`, `false` | Enable auto-sync of the merged character dictionary from AniList (default: `false`) |
|
| `characterDictionary.maxLoaded` | number | Number of recent media snapshots kept in the merged dictionary (default: `3`) |
|
||||||
| `characterDictionary.maxLoaded` | number | Number of recent media snapshots kept in the merged dictionary (default: `3`) |
|
| `characterDictionary.profileScope` | `"all"`, `"active"` | Apply dictionary to all Yomitan profiles or only the active one |
|
||||||
| `characterDictionary.profileScope` | `"all"`, `"active"` | Apply dictionary to all Yomitan profiles or only the active one |
|
| `characterDictionary.collapsibleSections.*` | `true`, `false` | Control which dictionary entry sections start expanded |
|
||||||
| `characterDictionary.collapsibleSections.*` | `true`, `false` | Control which dictionary entry sections start expanded |
|
|
||||||
|
|
||||||
See the [Character Dictionary](/character-dictionary) page for full details on the character dictionary feature, including name generation, matching, auto-sync lifecycle, and dictionary entry format.
|
See the [Character Dictionary](/character-dictionary) page for full details on the character dictionary feature, including name generation, matching, auto-sync lifecycle, and dictionary entry format. Character dictionary sync follows `subtitleStyle.nameMatchEnabled`.
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| --- | --- |
|
| ----------------------- | ------------------------------------------------------------- |
|
||||||
| `--anilist-setup` | Open AniList setup/auth flow helper window |
|
| `--anilist-setup` | Open AniList setup/auth flow helper window |
|
||||||
| `--anilist-status` | Print current token resolution state and retry queue counters |
|
| `--anilist-status` | Print current token resolution state and retry queue counters |
|
||||||
| `--anilist-logout` | Clear stored AniList token from local persisted state |
|
| `--anilist-logout` | Clear stored AniList token from local persisted state |
|
||||||
| `--anilist-retry-queue` | Process one ready retry queue item immediately |
|
| `--anilist-retry-queue` | Process one ready retry queue item immediately |
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- **Updates not triggering:** Confirm `anilist.enabled` is `true`. SubMiner requires at least 85% of the episode watched and a minimum of 10 minutes. Short episodes or partial watches will not trigger an update.
|
- **Updates not triggering:** Confirm `anilist.enabled` is `true`. SubMiner requires at least 85% of the episode watched and a minimum of 10 minutes. Short episodes or partial watches will not trigger an update.
|
||||||
|
- **Update not possible:** Add the season to your AniList Planning or Watching list first. SubMiner will not create new AniList list entries automatically.
|
||||||
- **Wrong episode or title matched:** Detection quality is best when `guessit` is installed and on your `PATH`. Without it, SubMiner falls back to internal filename parsing which can be less accurate with unusual naming conventions.
|
- **Wrong episode or title matched:** Detection quality is best when `guessit` is installed and on your `PATH`. Without it, SubMiner falls back to internal filename parsing which can be less accurate with unusual naming conventions.
|
||||||
- **Token issues:** Run `--anilist-status` to check token state. If the token is invalid or expired, run `--anilist-setup` or `--anilist-logout` and re-authenticate.
|
- **Token issues:** Run `--anilist-status` to check token state. If the token is invalid or expired, run `--anilist-setup` or `--anilist-logout` and re-authenticate.
|
||||||
- **Updates failing repeatedly:** Run `--anilist-status` to see retry queue counters. Items that fail 8 times are moved to the dead-letter queue. Check network connectivity and AniList API status.
|
- **Updates failing repeatedly:** Run `--anilist-status` to see retry queue counters. Items that fail 8 times are moved to the dead-letter queue. Check network connectivity and AniList API status.
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
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.
|
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.
|
||||||
|
|
||||||
|
::: tip New to these terms?
|
||||||
|
- **Anki** is the flashcard app where your study cards live.
|
||||||
|
- **AnkiConnect** is a free add-on that lets other programs (like SubMiner) talk to Anki over a local connection. SubMiner needs it installed to add or edit cards.
|
||||||
|
- A **note type** (also called a "model") is the template that defines what a card looks like — for example the Kiku or Lapis templates many Japanese learners use.
|
||||||
|
- A **field** is one labeled slot in that template, such as `Sentence`, `Expression`, or `Picture`. SubMiner fills these fields when it mines a card.
|
||||||
|
:::
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
1. Install [Anki](https://apps.ankiweb.net/).
|
1. Install [Anki](https://apps.ankiweb.net/).
|
||||||
@@ -15,9 +22,9 @@ AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the po
|
|||||||
|
|
||||||
When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, image, and translation fields automatically. Two detection methods are available:
|
When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, image, and translation fields automatically. Two detection methods are available:
|
||||||
|
|
||||||
**Proxy mode** — SubMiner runs a local AnkiConnect-compatible proxy and intercepts card creation instantly. Recommended when possible.
|
**Proxy mode** (default) — SubMiner runs a local *proxy*: a small middleman server that sits between Yomitan and Anki. Yomitan sends new cards to SubMiner, SubMiner enriches them, then passes them along to Anki. This makes enrichment instant.
|
||||||
|
|
||||||
**Polling mode** (default) — SubMiner polls AnkiConnect every few seconds for newly added cards. Simpler setup, but with a short delay (~3 seconds).
|
**Polling mode** (fallback, when the proxy is disabled) — SubMiner asks AnkiConnect every few seconds whether any new cards were added, then enriches them. Simpler setup, but with a short delay (~3 seconds).
|
||||||
|
|
||||||
Use proxy mode if you want immediate enrichment. Use polling mode if your Yomitan instance is external (browser-based) or you prefer minimal configuration.
|
Use proxy mode if you want immediate enrichment. Use polling mode if your Yomitan instance is external (browser-based) or you prefer minimal configuration.
|
||||||
|
|
||||||
@@ -30,7 +37,7 @@ In both modes, the enrichment workflow is the same:
|
|||||||
5. Writes metadata to the miscInfo field.
|
5. Writes metadata to the miscInfo field.
|
||||||
|
|
||||||
Polling mode uses the query `"deck:<ankiConnect.deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks.
|
Polling mode uses the query `"deck:<ankiConnect.deck>" added:1` to find recently added cards. If no deck is configured, it searches all decks.
|
||||||
Known-word sync scope is controlled by `ankiConnect.knownWords.decks` (object map), with `ankiConnect.deck` used as legacy fallback.
|
Known-word sync scope is controlled by `ankiConnect.knownWords.decks`.
|
||||||
|
|
||||||
### Proxy Mode Setup (Yomitan / Texthooker)
|
### Proxy Mode Setup (Yomitan / Texthooker)
|
||||||
|
|
||||||
@@ -147,13 +154,13 @@ SubMiner uses FFmpeg to generate audio and image media from the video. FFmpeg mu
|
|||||||
|
|
||||||
### Audio
|
### Audio
|
||||||
|
|
||||||
Audio is extracted from the video file using the subtitle's start and end timestamps, with configurable padding added before and after.
|
Audio is extracted from the video file using the subtitle's start and end timestamps. Padding is opt-in; keep it at `0` when you want sentence audio to start exactly at the mined sentence.
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
"ankiConnect": {
|
"ankiConnect": {
|
||||||
"media": {
|
"media": {
|
||||||
"generateAudio": true,
|
"generateAudio": true,
|
||||||
"audioPadding": 0.5, // seconds before and after subtitle timing
|
"audioPadding": 0, // optional seconds before and after subtitle timing
|
||||||
"maxMediaDuration": 30 // cap total duration in seconds
|
"maxMediaDuration": 30 // cap total duration in seconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,6 +329,7 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
|
|||||||
"upstreamUrl": "http://127.0.0.1:8765",
|
"upstreamUrl": "http://127.0.0.1:8765",
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"word": "Expression",
|
||||||
"audio": "ExpressionAudio",
|
"audio": "ExpressionAudio",
|
||||||
"image": "Picture",
|
"image": "Picture",
|
||||||
"sentence": "Sentence",
|
"sentence": "Sentence",
|
||||||
@@ -334,7 +342,7 @@ When you mine the same word multiple times, SubMiner can merge the cards instead
|
|||||||
"imageType": "static",
|
"imageType": "static",
|
||||||
"imageFormat": "jpg",
|
"imageFormat": "jpg",
|
||||||
"imageQuality": 92,
|
"imageQuality": 92,
|
||||||
"audioPadding": 0.5,
|
"audioPadding": 0,
|
||||||
"maxMediaDuration": 30,
|
"maxMediaDuration": 30,
|
||||||
},
|
},
|
||||||
"behavior": {
|
"behavior": {
|
||||||
|
|||||||
@@ -269,11 +269,48 @@ For domains migrated to reducer-style transitions (for example AniList token/que
|
|||||||
- Reducer boundary: when a domain has transition helpers in `src/main/state.ts`, new callsites should route updates through those helpers instead of ad-hoc object mutation in `main.ts` or composers.
|
- Reducer boundary: when a domain has transition helpers in `src/main/state.ts`, new callsites should route updates through those helpers instead of ad-hoc object mutation in `main.ts` or composers.
|
||||||
- Tests for migrated domains should assert both the intended field changes and non-targeted field invariants.
|
- Tests for migrated domains should assert both the intended field changes and non-targeted field invariants.
|
||||||
|
|
||||||
|
## Playback Startup Flow
|
||||||
|
|
||||||
|
Before the app boots, something has to launch mpv, inject the plugin, and bring the overlay up. SubMiner-managed launches own this step — the `subminer` launcher, the app's own playback, and the packaged Windows shortcut all follow the same path. The launcher reads `config.jsonc`, spawns mpv with the IPC socket and the bundled plugin, and passes runtime settings as `--script-opts`. The plugin never reads a config file: the shipped `subminer.conf` is intentionally empty so command-line opts always win.
|
||||||
|
|
||||||
|
Once mpv is up, exactly one of two triggers brings up the overlay. On a first launch the plugin's `file-loaded` hook self-starts the app once the socket is ready (because the launcher injected `auto_start=yes`). When the app is already running — or for explicit `--start-overlay` and YouTube flows — the launcher instead attaches over the control socket and suppresses the plugin's auto-start, so the two never fire together. Both converge on the same app bring-up, which then runs the Program Lifecycle below.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
|
||||||
|
classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef proc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef app fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef overlay fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
|
||||||
|
Entry["Managed launch<br/>subminer CLI · app · Windows shortcut"]:::entry
|
||||||
|
Entry --> Cfg["Launcher reads config.jsonc<br/>→ plugin runtime config"]:::extrt
|
||||||
|
Cfg --> Spawn["Spawn mpv<br/>--input-ipc-server=/tmp/subminer-socket<br/>--script=plugin/subminer/main.lua<br/>--script-opts=subminer-… (auto_start, backend, …)"]:::proc
|
||||||
|
Spawn --> Boot["Plugin boot · read_options('subminer')<br/>empty subminer.conf; CLI opts win"]:::extrt
|
||||||
|
Boot --> Sock["mpv IPC socket ready"]:::proc
|
||||||
|
Sock --> Who{"Overlay trigger"}:::decision
|
||||||
|
|
||||||
|
Who -->|"app already running, or<br/>--start-overlay / YouTube"| Attach["Launcher startOverlay()<br/>attach via control socket<br/>plugin auto-start suppressed"]:::proc
|
||||||
|
Who -->|"first launch, auto_start=yes"| Self["Plugin file-loaded hook<br/>polls socket → process.start_overlay()"]:::extrt
|
||||||
|
|
||||||
|
Attach --> AppUp
|
||||||
|
Self --> AppUp
|
||||||
|
|
||||||
|
AppUp["Spawn / attach SubMiner app<br/>--start --managed-playback --socket … --backend …"]:::app
|
||||||
|
AppUp --> Ctrl["App control server up<br/>/tmp/subminer-control-* dedupes a 2nd launch"]:::app
|
||||||
|
Ctrl --> Life["app.whenReady → Program Lifecycle (below)"]:::app
|
||||||
|
Life --> Conn["MpvIpcClient connects to /tmp/subminer-socket"]:::overlay
|
||||||
|
Conn --> Show["Transparent overlay over mpv<br/>Yomitan lookup · mine"]:::overlay
|
||||||
|
```
|
||||||
|
|
||||||
|
The runtime sockets in this flow are detailed in [IPC + Runtime Contracts](./ipc-contracts#runtime-sockets).
|
||||||
|
|
||||||
## Program Lifecycle
|
## Program Lifecycle
|
||||||
|
|
||||||
- **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 mpv subtitle/playback properties via `observe_property`), 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 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.
|
||||||
- **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 (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).
|
||||||
- **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 are sent to the main overlay renderer and modal surfaces.
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"@catppuccin/vitepress": "^0.1.2",
|
"@catppuccin/vitepress": "^0.1.2",
|
||||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
"@fontsource/manrope": "^5.2.8",
|
"@fontsource/manrope": "^5.2.8",
|
||||||
"@plausible-analytics/tracker": "^0.4.4",
|
|
||||||
"mermaid": "^11.12.3",
|
"mermaid": "^11.12.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -143,8 +142,6 @@
|
|||||||
|
|
||||||
"@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="],
|
"@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="],
|
||||||
|
|
||||||
"@plausible-analytics/tracker": ["@plausible-analytics/tracker@0.4.4", "", {}, "sha512-fz0NOYUEYXtg1TBaPEEvtcBq3FfmLFuTe1VZw4M8sTWX129br5dguu3M15+plOQnc181ShYe67RfwhKgK89VnA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- **Character Dictionary:** Loaded entries are now scoped to the current AniList media for subtitle name matching and inline portraits. Added a character dictionary manager at `Ctrl/Cmd+D`; AniList overrides now live inside that manager instead of using a separate default shortcut.
|
||||||
|
|
||||||
## v0.14.0 (2026-05-12)
|
## v0.14.0 (2026-05-12)
|
||||||
|
|
||||||
|
SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin is injected at runtime only when SubMiner launches mpv — through the `subminer` launcher, the app's managed launch, or the packaged Windows SubMiner mpv shortcut. When you open mpv on its own, SubMiner is not involved and the plugin is never loaded. If you have a legacy global SubMiner plugin under mpv's `scripts` directory, first-run setup detects it and prompts you to remove it before playback starts.
|
||||||
|
|
||||||
**Added**
|
**Added**
|
||||||
|
|
||||||
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches — open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
|
- **Character Dictionary:** Added AniList-based character dictionary selection for resolving title mismatches — open it in-app with the new `Ctrl+Alt+A` shortcut or from the CLI with `subminer dictionary --candidates` / `--select`. Series-scoped overrides replace stale entries in the merged dictionary.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Character Dictionary
|
# Character Dictionary
|
||||||
|
|
||||||
SubMiner can build a Yomitan-compatible character dictionary from AniList metadata so that character names in subtitles are recognized, highlighted, and enrichable with context — portraits, roles, voice actors, and biographical detail — without leaving the overlay.
|
SubMiner can build a Yomitan-compatible character dictionary from [AniList](https://anilist.co) metadata so that character names in subtitles are recognized, highlighted, and enrichable with context — portraits, roles, voice actors, and biographical detail — without leaving the overlay. (AniList is an online anime/manga database; SubMiner pulls each show's character list from it.)
|
||||||
|
|
||||||
|
This is helpful because proper names rarely appear in normal dictionaries, so character names would otherwise be flagged as "unknown" words and clutter your mining. Recognizing them keeps your N+1 highlighting focused on real vocabulary.
|
||||||
|
|
||||||
The dictionary is generated per-media, merged across your recently-watched titles, and auto-imported into Yomitan. When a character name appears in a subtitle line, it gets highlighted and becomes available for hover-driven Yomitan profile lookup.
|
The dictionary is generated per-media, merged across your recently-watched titles, and auto-imported into Yomitan. When a character name appears in a subtitle line, it gets highlighted and becomes available for hover-driven Yomitan profile lookup.
|
||||||
|
|
||||||
@@ -12,14 +14,14 @@ The feature has three stages: **snapshot**, **merge**, and **match**.
|
|||||||
|
|
||||||
2. **Merge** — SubMiner maintains a most-recently-used list of media IDs (default: 3). Snapshots from those titles are merged into a single Yomitan ZIP — `character-dictionaries/merged.zip` — which is always named "SubMiner Character Dictionary" so Yomitan treats it as a single stable dictionary across rebuilds.
|
2. **Merge** — SubMiner maintains a most-recently-used list of media IDs (default: 3). Snapshots from those titles are merged into a single Yomitan ZIP — `character-dictionaries/merged.zip` — which is always named "SubMiner Character Dictionary" so Yomitan treats it as a single stable dictionary across rebuilds.
|
||||||
|
|
||||||
3. **Match** — During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. Tokens that match a character entry are flagged with `isNameMatch` and highlighted in the overlay with a distinct color.
|
3. **Match** — During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. SubMiner only accepts character entries for the current AniList media when that media ID is known, then flags matching tokens with `isNameMatch` and highlights them in the overlay with a distinct color.
|
||||||
|
|
||||||
## Enabling the Feature
|
## Enabling the Feature
|
||||||
|
|
||||||
Character dictionary sync is disabled by default. To turn it on:
|
Character dictionary sync is disabled by default. To turn it on:
|
||||||
|
|
||||||
1. Authenticate with AniList (see [AniList Integration](/anilist-integration#setup)).
|
1. Authenticate with AniList (see [AniList Integration](/anilist-integration#setup)).
|
||||||
2. Set `anilist.characterDictionary.enabled` to `true` in your config.
|
2. Set `subtitleStyle.nameMatchEnabled` to `true` in your config or enable **Name Match Enabled** in Settings.
|
||||||
3. Start watching — SubMiner will generate a snapshot for the current media and import the merged dictionary into Yomitan automatically.
|
3. Start watching — SubMiner will generate a snapshot for the current media and import the merged dictionary into Yomitan automatically.
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
@@ -27,15 +29,15 @@ Character dictionary sync is disabled by default. To turn it on:
|
|||||||
"anilist": {
|
"anilist": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"accessToken": "your-token",
|
"accessToken": "your-token",
|
||||||
"characterDictionary": {
|
},
|
||||||
"enabled": true,
|
"subtitleStyle": {
|
||||||
},
|
"nameMatchEnabled": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
::: tip
|
::: tip
|
||||||
The first sync for a media title takes a few seconds while character data and portraits are fetched from AniList. Subsequent launches reuse the cached snapshot.
|
The first sync for a media title takes a few seconds while character data and portraits are fetched from AniList. Subsequent launches reuse the cached media match and snapshot without a fresh AniList lookup.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
@@ -87,23 +89,29 @@ Name matching runs inside Yomitan's scanning pipeline during subtitle tokenizati
|
|||||||
|
|
||||||
1. Yomitan receives subtitle text and scans for dictionary matches.
|
1. Yomitan receives subtitle text and scans for dictionary matches.
|
||||||
2. Entries from "SubMiner Character Dictionary" are checked with exact primary-source matching — the token must match the entry's `originalText` with `isPrimary: true` and `matchType: 'exact'`.
|
2. Entries from "SubMiner Character Dictionary" are checked with exact primary-source matching — the token must match the entry's `originalText` with `isPrimary: true` and `matchType: 'exact'`.
|
||||||
3. Matched tokens are flagged `isNameMatch: true` and forwarded to the renderer.
|
3. When the current AniList media ID is known, entries whose embedded media ID belongs to a different title are ignored for name matching and inline portraits.
|
||||||
4. The renderer applies the name-match highlight color (default: `#f5bde6`).
|
4. Matched tokens are flagged `isNameMatch: true` and forwarded to the renderer.
|
||||||
|
5. If `subtitleStyle.nameMatchEnabled` is enabled, the renderer applies the name-match highlight color (default: `#f5bde6`).
|
||||||
|
6. If `subtitleStyle.nameMatchImagesEnabled` is enabled, the renderer also injects a small circular AniList portrait from the cached snapshot image data.
|
||||||
|
|
||||||
|
Older snapshot schema versions are regenerated automatically. Current-version snapshots are normally reused, but when `subtitleStyle.nameMatchImagesEnabled` is enabled SubMiner also checks whether the cached snapshot contains usable character portrait data. If it does not, the snapshot is refreshed so the merged dictionary can include images.
|
||||||
|
|
||||||
Name matches are visually distinct from [N+1 targeting, frequency highlighting, and JLPT tags](/subtitle-annotations) so you can tell at a glance whether a highlighted word is a character name or a vocabulary target.
|
Name matches are visually distinct from [N+1 targeting, frequency highlighting, and JLPT tags](/subtitle-annotations) so you can tell at a glance whether a highlighted word is a character name or a vocabulary target.
|
||||||
|
|
||||||
**Key settings:**
|
**Key settings:**
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| -------------------------------- | --------- | ---------------------------------- |
|
| -------------------------------------- | --------- | ----------------------------------------- |
|
||||||
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting |
|
| `subtitleStyle.nameMatchEnabled` | `false` | Enable dictionary sync and highlighting |
|
||||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
|
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits beside names |
|
||||||
|
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
|
||||||
|
|
||||||
## Dictionary Entries
|
## Dictionary Entries
|
||||||
|
|
||||||
Each character entry in the Yomitan dictionary includes structured content:
|
Each character entry in the Yomitan dictionary includes structured content:
|
||||||
|
|
||||||
- **Name** — native (Japanese) and romanized forms
|
- **Name** — the matched Japanese name form
|
||||||
|
- **Known names** — generated non-honorific Japanese aliases for that character, excluding raw romanized/English aliases from lookup results
|
||||||
- **Role badge** — color-coded by role: main (score 100), supporting (90), side (80), background (70)
|
- **Role badge** — color-coded by role: main (score 100), supporting (90), side (80), background (70)
|
||||||
- **Portrait** — character image from AniList, embedded in the ZIP
|
- **Portrait** — character image from AniList, embedded in the ZIP
|
||||||
- **Description** — biography text from AniList (collapsible)
|
- **Description** — biography text from AniList (collapsible)
|
||||||
@@ -128,7 +136,7 @@ The three collapsible sections can be configured to start open or closed:
|
|||||||
|
|
||||||
## Auto-Sync Lifecycle
|
## Auto-Sync Lifecycle
|
||||||
|
|
||||||
When `characterDictionary.enabled` is `true`, SubMiner runs an auto-sync routine whenever the active media changes.
|
When `subtitleStyle.nameMatchEnabled` is `true`, SubMiner runs an auto-sync routine whenever the active media changes.
|
||||||
|
|
||||||
**Phases:**
|
**Phases:**
|
||||||
|
|
||||||
@@ -139,7 +147,7 @@ When `characterDictionary.enabled` is `true`, SubMiner runs an auto-sync routine
|
|||||||
5. **importing** — Push the ZIP into Yomitan. Waits for Yomitan mutation readiness (7-second timeout per operation).
|
5. **importing** — Push the ZIP into Yomitan. Waits for Yomitan mutation readiness (7-second timeout per operation).
|
||||||
6. **ready** — Dictionary is live. Character names will match on the next subtitle line.
|
6. **ready** — Dictionary is live. Character names will match on the next subtitle line.
|
||||||
|
|
||||||
**State tracking** is persisted in `character-dictionaries/auto-sync-state.json`:
|
**State tracking** is persisted in `character-dictionaries/auto-sync-state.json`. AniList media matches are cached separately in `character-dictionaries/anilist-resolution-cache.json` so snapshot hits do not need another AniList search.
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
@@ -167,10 +175,13 @@ This creates a standalone dictionary ZIP for the target media and saves it along
|
|||||||
|
|
||||||
## Correcting AniList Matches
|
## Correcting AniList Matches
|
||||||
|
|
||||||
SubMiner uses `guessit` to infer the anime title from the active filename, then searches AniList. Some filenames can still resolve to the wrong title. For example, `Re - ZERO, Starting Life in Another World (2016)` can be misread as a different `Re...` series.
|
SubMiner uses `guessit` to infer the anime title from the active filename before searching AniList. Some filenames can still resolve to the wrong title. For example, `Re - ZERO, Starting Life in Another World (2016)` can be misread as a different `Re...` series.
|
||||||
|
|
||||||
Use the in-app selector or CLI to pin the correct AniList media for the whole series:
|
Use the in-app selector or CLI to pin the correct AniList media for the whole series:
|
||||||
|
|
||||||
|
- In-app: open the manager with `Ctrl/Cmd+D`, use the **Override** tab/button, edit the prefilled title if needed, then search and choose the correct result.
|
||||||
|
- CLI: `--dictionary-candidates` still lists matches for the current filename guess.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List candidate AniList matches for a file
|
# List candidate AniList matches for a file
|
||||||
subminer dictionary --candidates "/path/to/episode.mkv"
|
subminer dictionary --candidates "/path/to/episode.mkv"
|
||||||
@@ -183,10 +194,20 @@ SubMiner.AppImage --dictionary-candidates --dictionary-target "/path/to/episode.
|
|||||||
SubMiner.AppImage --dictionary-select --dictionary-anilist-id 21355 --dictionary-target "/path/to/episode.mkv"
|
SubMiner.AppImage --dictionary-select --dictionary-anilist-id 21355 --dictionary-target "/path/to/episode.mkv"
|
||||||
|
|
||||||
# Open the in-app selector from the running app
|
# Open the in-app selector from the running app
|
||||||
subminer app --open-character-dictionary
|
subminer app --session-action '{"actionId":"openCharacterDictionaryManager"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Manual selections are stored in `character-dictionaries/anilist-overrides.json` using a series key derived from the filename guess. Later episodes with the same series key use the selected AniList ID automatically. When the override replaces a previous wrong match, SubMiner removes that stale media ID from the merged dictionary's active set and rebuilds/imports the merged character dictionary.
|
Manual selections are stored in `character-dictionaries/anilist-overrides.json` using a series key derived from the episode's parent directory plus the filename guess. Later episodes in the same directory use the selected AniList ID automatically, while separate season directories can keep separate overrides and character dictionaries. When the override replaces a previous wrong match, SubMiner removes that stale media ID from the merged dictionary's active set and rebuilds/imports the merged character dictionary.
|
||||||
|
|
||||||
|
## Managing Loaded Entries
|
||||||
|
|
||||||
|
Open the manager with `Ctrl/Cmd+D` (`shortcuts.openCharacterDictionaryManager`). The manager shows the merged dictionary's active MRU entries, marks the current anime, and lets you adjust eviction priority for the other loaded entries.
|
||||||
|
|
||||||
|
- **Remove** drops a non-current entry from the active merged dictionary and rebuilds/imports once.
|
||||||
|
- **Up/Down** changes MRU order for future eviction without rebuilding.
|
||||||
|
- **Override** opens the AniList selector for that entry's title so you can replace a saved loaded entry.
|
||||||
|
|
||||||
|
The current anime cannot be removed while you are watching it; it stays loaded until playback changes.
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
@@ -205,7 +226,7 @@ character-dictionaries/
|
|||||||
m170942-va67890.jpg # Voice actor portrait
|
m170942-va67890.jpg # Voice actor portrait
|
||||||
```
|
```
|
||||||
|
|
||||||
**Snapshot format** (v15): each snapshot contains the media ID, title, entry count, timestamp, an array of Yomitan term entries, and base64-encoded images.
|
**Snapshot format** (v17): each snapshot contains the media ID, title, entry count, timestamp, an array of Yomitan term entries, and base64-encoded images.
|
||||||
|
|
||||||
**ZIP structure** follows the Yomitan dictionary format:
|
**ZIP structure** follows the Yomitan dictionary format:
|
||||||
|
|
||||||
@@ -222,13 +243,13 @@ merged.zip
|
|||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| ---------------------------------------------------------------------- | --------- | --------------------------------------------------------------- |
|
| ---------------------------------------------------------------------- | --------- | --------------------------------------------------------------- |
|
||||||
| `anilist.characterDictionary.enabled` | `false` | Enable auto-sync of character dictionary from AniList |
|
|
||||||
| `anilist.characterDictionary.maxLoaded` | `3` | Number of recent media snapshots kept in the merged dictionary |
|
| `anilist.characterDictionary.maxLoaded` | `3` | Number of recent media snapshots kept in the merged dictionary |
|
||||||
| `anilist.characterDictionary.profileScope` | `"all"` | Apply dictionary to `"all"` Yomitan profiles or `"active"` only |
|
| `anilist.characterDictionary.profileScope` | `"all"` | Apply dictionary to `"all"` Yomitan profiles or `"active"` only |
|
||||||
| `anilist.characterDictionary.collapsibleSections.description` | `false` | Start Description section expanded |
|
| `anilist.characterDictionary.collapsibleSections.description` | `false` | Start Description section expanded |
|
||||||
| `anilist.characterDictionary.collapsibleSections.characterInformation` | `false` | Start Character Information section expanded |
|
| `anilist.characterDictionary.collapsibleSections.characterInformation` | `false` | Start Character Information section expanded |
|
||||||
| `anilist.characterDictionary.collapsibleSections.voicedBy` | `false` | Start Voiced By section expanded |
|
| `anilist.characterDictionary.collapsibleSections.voicedBy` | `false` | Start Voiced By section expanded |
|
||||||
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting in subtitles |
|
| `subtitleStyle.nameMatchEnabled` | `false` | Enable character-dictionary sync and name highlighting |
|
||||||
|
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits beside matched names |
|
||||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for character-name matches |
|
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for character-name matches |
|
||||||
|
|
||||||
## Reference Implementation
|
## Reference Implementation
|
||||||
@@ -250,9 +271,10 @@ If you work with visual novels or want a standalone dictionary generator indepen
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- **Names not highlighting:** Confirm `anilist.characterDictionary.enabled` is `true` and `subtitleStyle.nameMatchEnabled` is `true`. Check that the current media has an AniList entry — SubMiner needs a media ID to fetch characters.
|
- **Names not highlighting:** Confirm `subtitleStyle.nameMatchEnabled` is `true`. Check that the current media has an AniList entry — SubMiner needs a media ID to fetch characters.
|
||||||
|
- **Inline portraits missing:** Confirm `subtitleStyle.nameMatchImagesEnabled` is `true`. On the next character dictionary sync, SubMiner refreshes current-version snapshots that do not contain usable cached character portrait data. Portraits still require AniList to return an image and the image download to succeed.
|
||||||
- **Sync seems stuck:** The auto-sync debounces for 800ms after media changes and throttles image downloads at 250ms per image. Large casts (50+ characters) take longer. Check the status bar for the current sync phase.
|
- **Sync seems stuck:** The auto-sync debounces for 800ms after media changes and throttles image downloads at 250ms per image. Large casts (50+ characters) take longer. Check the status bar for the current sync phase.
|
||||||
- **Wrong characters showing:** Open the in-app character dictionary selector (`--open-character-dictionary`) or run `--dictionary-candidates`, then save the correct media with `--dictionary-select --dictionary-anilist-id <id>`. This replaces stale wrong-title entries for that series. If names are only from an older unrelated show, they'll rotate out once you watch enough new titles to push it past `maxLoaded`.
|
- **Wrong characters showing:** Open the in-app character dictionary manager (`Ctrl/Cmd+D`) to remove/reorder loaded titles, then use **Override** to correct the active AniList match. You can also run `--dictionary-candidates`, then save the correct media with `--dictionary-select --dictionary-anilist-id <id>`. SubMiner ignores character entries from other loaded titles for subtitle name matching and inline portraits once the current media ID is known.
|
||||||
- **Yomitan import fails:** SubMiner waits up to 7 seconds for Yomitan to be ready for mutations. If Yomitan is still loading dictionaries or performing another import, the operation may time out. Restarting the overlay typically resolves this.
|
- **Yomitan import fails:** SubMiner waits up to 7 seconds for Yomitan to be ready for mutations. If Yomitan is still loading dictionaries or performing another import, the operation may time out. Restarting the overlay typically resolves this.
|
||||||
- **Portraits missing:** Images are downloaded from AniList CDN during snapshot generation. If the network was unavailable during the initial sync, delete the snapshot file from `character-dictionaries/snapshots/` and let it regenerate.
|
- **Portraits missing:** Images are downloaded from AniList CDN during snapshot generation. If the network was unavailable during the initial sync, delete the snapshot file from `character-dictionaries/snapshots/` and let it regenerate.
|
||||||
|
|
||||||
|
|||||||
+408
-315
File diff suppressed because it is too large
Load Diff
+18
-16
@@ -1,8 +1,10 @@
|
|||||||
# Feature Demos
|
# Feature Demos
|
||||||
|
|
||||||
Short recordings of SubMiner's key features and integrations from real playback sessions.
|
Short recordings of SubMiner's key features and integrations from real playback sessions. A few terms you'll see below: _Yomitan_ is the pop-up dictionary used for word lookups, _Jimaku_ is a community subtitle database, _alass_ and _ffsubsync_ are tools that retime subtitles to match the audio, _Jellyfin_ is a self-hosted media server, and a _texthooker_ is a web page that mirrors the current subtitle as selectable text for browser-based tools.
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { withBase } from 'vitepress';
|
||||||
|
|
||||||
const v = '20260301-1';
|
const v = '20260301-1';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -10,11 +12,11 @@ const v = '20260301-1';
|
|||||||
|
|
||||||
Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner automatically attaches the sentence, a timing-accurate audio clip, a screenshot, and a translation.
|
Mine 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}`">
|
<video controls playsinline preload="metadata" :poster="withBase(`/assets/minecard-poster.jpg?v=${v}`)">
|
||||||
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
|
<source :src="withBase(`/assets/minecard.webm?v=${v}`)" type="video/webm" />
|
||||||
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
|
<source :src="withBase(`/assets/minecard.mp4?v=${v}`)" type="video/mp4" />
|
||||||
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
|
<a :href="withBase(`/assets/minecard.webm?v=${v}`)" target="_blank" rel="noreferrer">
|
||||||
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
<img :src="withBase(`/assets/minecard.webp?v=${v}`)" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
||||||
</a>
|
</a>
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
@@ -23,11 +25,11 @@ Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner aut
|
|||||||
|
|
||||||
## Subtitle Download & Sync
|
## Subtitle Download & Sync
|
||||||
|
|
||||||
Search and download subtitles from Jimaku, then automatically synchronize them with alass or ffsubsync — all from within SubMiner.
|
Search and download subtitles from Jimaku, then retime them with alass or ffsubsync — all from within SubMiner.
|
||||||
|
|
||||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/subtitle-sync-poster.jpg?v=${v}`">
|
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/subtitle-sync-poster.jpg?v=${v}`)">
|
||||||
<source :src="`/assets/demos/subtitle-sync.webm?v=${v}`" type="video/webm" />
|
<source :src="withBase(`/assets/demos/subtitle-sync.webm?v=${v}`)" type="video/webm" />
|
||||||
<source :src="`/assets/demos/subtitle-sync.mp4?v=${v}`" type="video/mp4" />
|
<source :src="withBase(`/assets/demos/subtitle-sync.mp4?v=${v}`)" type="video/mp4" />
|
||||||
</video> -->
|
</video> -->
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
::: info VIDEO COMING SOON
|
||||||
@@ -37,9 +39,9 @@ Search and download subtitles from Jimaku, then automatically synchronize them w
|
|||||||
|
|
||||||
Browse your Jellyfin library, cast to devices, and launch playback directly from SubMiner. Watch progress syncs back to your Jellyfin server.
|
Browse your Jellyfin library, 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}`">
|
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/jellyfin-poster.jpg?v=${v}`)">
|
||||||
<source :src="`/assets/demos/jellyfin.webm?v=${v}`" type="video/webm" />
|
<source :src="withBase(`/assets/demos/jellyfin.webm?v=${v}`)" type="video/webm" />
|
||||||
<source :src="`/assets/demos/jellyfin.mp4?v=${v}`" type="video/mp4" />
|
<source :src="withBase(`/assets/demos/jellyfin.mp4?v=${v}`)" type="video/mp4" />
|
||||||
</video> -->
|
</video> -->
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
::: info VIDEO COMING SOON
|
||||||
@@ -49,9 +51,9 @@ Browse your Jellyfin library, cast to devices, and launch playback directly from
|
|||||||
|
|
||||||
Open subtitles in an external texthooker page for use with browser-based tools and extensions alongside the overlay.
|
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}`">
|
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/texthooker-poster.jpg?v=${v}`)">
|
||||||
<source :src="`/assets/demos/texthooker.webm?v=${v}`" type="video/webm" />
|
<source :src="withBase(`/assets/demos/texthooker.webm?v=${v}`)" type="video/webm" />
|
||||||
<source :src="`/assets/demos/texthooker.mp4?v=${v}`" type="video/mp4" />
|
<source :src="withBase(`/assets/demos/texthooker.mp4?v=${v}`)" type="video/mp4" />
|
||||||
</video> -->
|
</video> -->
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
::: info VIDEO COMING SOON
|
||||||
|
|||||||
+28
-14
@@ -68,10 +68,15 @@ make dev-watch-macos # same as dev-watch, forcing --bac
|
|||||||
```
|
```
|
||||||
|
|
||||||
For mpv-plugin-driven testing without exporting `SUBMINER_BINARY_PATH` each run, set a one-time
|
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`:
|
dev binary path with `mpv.subminerBinaryPath` in your SubMiner config. The launcher injects it into
|
||||||
|
the mpv plugin at runtime:
|
||||||
|
|
||||||
```ini
|
```json
|
||||||
binary_path=/absolute/path/to/SubMiner/scripts/subminer-dev.sh
|
{
|
||||||
|
"mpv": {
|
||||||
|
"subminerBinaryPath": "/absolute/path/to/SubMiner/scripts/subminer-dev.sh"
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
@@ -113,6 +118,14 @@ bun run docs:test
|
|||||||
bun run docs:build
|
bun run docs:build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For production docs routing, run the versioned build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run docs:build:versioned
|
||||||
|
```
|
||||||
|
|
||||||
|
The versioned build writes `.tmp/docs-versioned-site` with latest stable docs at `/`, development docs at `/main/`, and stable archives under `/v/<version>/`. Prerelease tags are skipped. Public assets from `docs-site/public/assets` are shared from root `/assets/` so large demo media is not duplicated into every version archive; generated VitePress CSS and JS assets stay under each version route. Stale `.tmp/docs-versioned-archive-cache` generations are pruned after a successful build, and intermediate `.tmp/docs-versioned-build` workspaces are removed.
|
||||||
|
|
||||||
Focused commands:
|
Focused commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -154,6 +167,7 @@ bun run format:check:src
|
|||||||
- `make pretty` runs the maintained Prettier allowlist only (`format:src`).
|
- `make pretty` runs the maintained Prettier allowlist only (`format:src`).
|
||||||
- `bun run format:check:src` checks the same scoped set without writing changes.
|
- `bun run format:check:src` checks the same scoped set without writing changes.
|
||||||
- `bun run format` remains the broad repo-wide Prettier command; use it intentionally.
|
- `bun run format` remains the broad repo-wide Prettier command; use it intentionally.
|
||||||
|
|
||||||
## Config Generation
|
## Config Generation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -197,17 +211,17 @@ Use Cloudflare's single `*` wildcard syntax for watch paths. `docs-site/*` cover
|
|||||||
|
|
||||||
Run `make help` for a full list of targets. Key ones:
|
Run `make help` for a full list of targets. Key ones:
|
||||||
|
|
||||||
| Target | Description |
|
| Target | Description |
|
||||||
| ---------------------- | ---------------------------------------------------------------- |
|
| --------------------------- | ----------------------------------------------------------------- |
|
||||||
| `make build` | Build platform package for detected OS |
|
| `make build` | Build platform package for detected OS |
|
||||||
| `make build-launcher` | Generate Bun launcher wrapper at `dist/launcher/subminer` |
|
| `make build-launcher` | Generate Bun launcher wrapper at `dist/launcher/subminer` |
|
||||||
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
|
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
|
||||||
| `make deps` | Install JS dependencies (root + stats + texthooker-ui) |
|
| `make deps` | Install JS dependencies (root + stats + texthooker-ui) |
|
||||||
| `make pretty` | Run scoped Prettier formatting for maintained source/config files |
|
| `make pretty` | Run scoped Prettier formatting for maintained source/config files |
|
||||||
| `make generate-config` | Generate default config from centralized registry |
|
| `make generate-config` | Generate default config from centralized registry |
|
||||||
| `make build-linux` | Convenience wrapper for Linux packaging |
|
| `make build-linux` | Convenience wrapper for Linux packaging |
|
||||||
| `make build-macos` | Convenience wrapper for signed macOS packaging |
|
| `make build-macos` | Convenience wrapper for signed macOS packaging |
|
||||||
| `make build-macos-unsigned` | Convenience wrapper for unsigned macOS packaging |
|
| `make build-macos-unsigned` | Convenience wrapper for unsigned macOS packaging |
|
||||||
|
|
||||||
## Contributor Notes
|
## Contributor Notes
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const installationContents = readFileSync(new URL('./installation.md', import.me
|
|||||||
const mpvPluginContents = readFileSync(new URL('./mpv-plugin.md', import.meta.url), 'utf8');
|
const mpvPluginContents = readFileSync(new URL('./mpv-plugin.md', import.meta.url), 'utf8');
|
||||||
const developmentContents = readFileSync(new URL('./development.md', import.meta.url), 'utf8');
|
const developmentContents = readFileSync(new URL('./development.md', import.meta.url), 'utf8');
|
||||||
const changelogContents = readFileSync(new URL('./changelog.md', import.meta.url), 'utf8');
|
const changelogContents = readFileSync(new URL('./changelog.md', import.meta.url), 'utf8');
|
||||||
|
const docsPackageContents = readFileSync(new URL('./package.json', import.meta.url), 'utf8');
|
||||||
const ankiIntegrationContents = readFileSync(
|
const ankiIntegrationContents = readFileSync(
|
||||||
new URL('./anki-integration.md', import.meta.url),
|
new URL('./anki-integration.md', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
@@ -37,13 +38,13 @@ test('docs reflect current launcher and release surfaces', () => {
|
|||||||
|
|
||||||
expect(mpvPluginContents).toContain('\\\\.\\pipe\\subminer-socket');
|
expect(mpvPluginContents).toContain('\\\\.\\pipe\\subminer-socket');
|
||||||
|
|
||||||
expect(readmeContents).toContain('Root directory: `docs-site`');
|
expect(readmeContents).toContain('Automatic production and preview deployments: disabled');
|
||||||
expect(readmeContents).toContain('Build output directory: `.vitepress/dist`');
|
expect(readmeContents).toContain('/main/');
|
||||||
expect(readmeContents).toContain('Build watch paths: `docs-site/*`');
|
expect(readmeContents).toContain('GitHub Actions direct upload with Wrangler');
|
||||||
expect(developmentContents).not.toContain('../subminer-docs');
|
expect(developmentContents).not.toContain('../subminer-docs');
|
||||||
expect(developmentContents).toContain('bun run docs:build');
|
expect(developmentContents).toContain('bun run docs:build');
|
||||||
expect(developmentContents).toContain('bun run docs:test');
|
expect(developmentContents).toContain('bun run docs:test');
|
||||||
expect(developmentContents).toContain('Build watch paths: `docs-site/*`');
|
expect(developmentContents).toContain('bun run docs:build:versioned');
|
||||||
expect(developmentContents).not.toContain('test:subtitle:dist');
|
expect(developmentContents).not.toContain('test:subtitle:dist');
|
||||||
expect(developmentContents).toContain('bun run build:win');
|
expect(developmentContents).toContain('bun run build:win');
|
||||||
|
|
||||||
@@ -57,6 +58,15 @@ test('docs reflect current launcher and release surfaces', () => {
|
|||||||
expect(changelogContents).toContain('v0.5.1 (2026-03-09)');
|
expect(changelogContents).toContain('v0.5.1 (2026-03-09)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('docs dev server links version navigation to local dev routes', () => {
|
||||||
|
expect(docsPackageContents).toContain('scripts/build-versioned-docs.ts');
|
||||||
|
expect(docsPackageContents).toContain(
|
||||||
|
'SUBMINER_DOCS_VERSION_LINK_ORIGIN=local bun run ../scripts/build-versioned-docs.ts',
|
||||||
|
);
|
||||||
|
expect(docsPackageContents).toContain('SUBMINER_DOCS_VERSION_LINK_ORIGIN=local');
|
||||||
|
expect(docsPackageContents).toContain('SUBMINER_DOCS_VERSION_MANIFEST');
|
||||||
|
});
|
||||||
|
|
||||||
test('docs changelog keeps the current minor release headings aligned with the root changelog', () => {
|
test('docs changelog keeps the current minor release headings aligned with the root changelog', () => {
|
||||||
const docsHeadings = extractCurrentMinorHeadings(changelogContents);
|
const docsHeadings = extractCurrentMinorHeadings(changelogContents);
|
||||||
expect(docsHeadings.length).toBeGreaterThan(0);
|
expect(docsHeadings.length).toBeGreaterThan(0);
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
SubMiner can log your watching and mining activity to a local SQLite database, then surface it in the built-in stats dashboard. Tracking is enabled by default and can be turned off if you do not want local analytics.
|
SubMiner can log your watching and mining activity to a local SQLite database, then surface it in the built-in stats dashboard. Tracking is enabled by default and can be turned off if you do not want local analytics.
|
||||||
|
|
||||||
|
"Immersion" here means time spent watching and reading native Japanese content. **All data stays on your computer** — nothing is uploaded anywhere. (SQLite is just a single-file database; you do not need to install or manage anything.)
|
||||||
|
|
||||||
When enabled, SubMiner records per-session statistics (watch time, subtitle lines seen, words encountered, cards mined) and maintains exact lifetime summary tables plus daily/monthly rollups. You can view that data in SubMiner's stats UI or query the database directly with any SQLite tool.
|
When enabled, SubMiner records per-session statistics (watch time, subtitle lines seen, words encountered, cards mined) and maintains exact lifetime summary tables plus daily/monthly rollups. You can view that data in SubMiner's stats UI or query the database directly with any SQLite tool.
|
||||||
|
|
||||||
|
::: tip For most users
|
||||||
|
Just leave tracking on and use the built-in [Stats Dashboard](#stats-dashboard). The retention, performance, SQL, and schema sections further down are reference material for advanced users who want to inspect or tune the database — you can safely skip them.
|
||||||
|
:::
|
||||||
|
|
||||||
Episode completion for local `watched` state uses the shared `DEFAULT_MIN_WATCH_RATIO` (`85%`) value from `src/shared/watch-threshold.ts`.
|
Episode completion for local `watched` state uses the shared `DEFAULT_MIN_WATCH_RATIO` (`85%`) value from `src/shared/watch-threshold.ts`.
|
||||||
|
|
||||||
## Enabling
|
## Enabling
|
||||||
|
|||||||
@@ -7,18 +7,18 @@ const docsIndexContents = readFileSync(docsIndexPath, 'utf8');
|
|||||||
test('docs demo media uses shared cache-busting asset version token', () => {
|
test('docs demo media uses shared cache-busting asset version token', () => {
|
||||||
expect(docsIndexContents).toMatch(/const demoAssetVersion = ['"][^'"]+['"]/);
|
expect(docsIndexContents).toMatch(/const demoAssetVersion = ['"][^'"]+['"]/);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
':poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`"',
|
':poster="withBase(`/assets/minecard-poster.jpg?v=${demoAssetVersion}`)"',
|
||||||
);
|
);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
'<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />',
|
'<source :src="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" type="video/webm" />',
|
||||||
);
|
);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
'<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />',
|
'<source :src="withBase(`/assets/minecard.mp4?v=${demoAssetVersion}`)" type="video/mp4" />',
|
||||||
);
|
);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
'<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">',
|
'<a :href="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" target="_blank" rel="noreferrer">',
|
||||||
);
|
);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
'<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />',
|
'<img :src="withBase(`/assets/minecard.webp?v=${demoAssetVersion}`)" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+8
-6
@@ -66,7 +66,7 @@ features:
|
|||||||
src: /assets/subtitle-download.svg
|
src: /assets/subtitle-download.svg
|
||||||
alt: Subtitle download icon
|
alt: Subtitle download icon
|
||||||
title: Subtitle Download & Sync
|
title: Subtitle Download & Sync
|
||||||
details: Search and pull subtitles from Jimaku, then auto-sync timing with alass or ffsubsync — all from the overlay.
|
details: Search and pull subtitles from Jimaku, then retime subtitles with alass or ffsubsync — all from the overlay.
|
||||||
link: /jimaku-integration
|
link: /jimaku-integration
|
||||||
linkText: Jimaku integration
|
linkText: Jimaku integration
|
||||||
- icon:
|
- icon:
|
||||||
@@ -86,6 +86,8 @@ features:
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { withBase } from 'vitepress';
|
||||||
|
|
||||||
const demoAssetVersion = '20260223-2';
|
const demoAssetVersion = '20260223-2';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -135,11 +137,11 @@ const demoAssetVersion = '20260223-2';
|
|||||||
<span class="demo-window__dot"></span>
|
<span class="demo-window__dot"></span>
|
||||||
<span class="demo-window__title">subminer -- playback</span>
|
<span class="demo-window__title">subminer -- playback</span>
|
||||||
</div>
|
</div>
|
||||||
<video controls playsinline preload="metadata" :poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`">
|
<video controls playsinline preload="metadata" :poster="withBase(`/assets/minecard-poster.jpg?v=${demoAssetVersion}`)">
|
||||||
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
|
<source :src="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" type="video/webm" />
|
||||||
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
|
<source :src="withBase(`/assets/minecard.mp4?v=${demoAssetVersion}`)" type="video/mp4" />
|
||||||
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
|
<a :href="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" target="_blank" rel="noreferrer">
|
||||||
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
<img :src="withBase(`/assets/minecard.webp?v=${demoAssetVersion}`)" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
||||||
</a>
|
</a>
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+196
-286
@@ -1,34 +1,35 @@
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
## How the Pieces Fit Together
|
SubMiner is a desktop app that draws an interactive layer — an **overlay** — on top of the [mpv](https://mpv.io) video player. As you watch native Japanese media, you can click or hover any word in the subtitles to look it up, then turn it into an Anki flashcard without pausing to switch apps. Building flashcards from real content you're watching is called **sentence mining**, and it's what SubMiner is built for. It bundles its own copy of **Yomitan** (a pop-up dictionary) and talks to **AnkiConnect** (an add-on that lets other programs add cards to Anki) so cards get filled in automatically.
|
||||||
|
|
||||||
SubMiner is an overlay that sits on top of mpv. It connects to mpv through an IPC socket, renders subtitles as interactive text using a bundled Yomitan dictionary engine, and optionally creates Anki flashcards via AnkiConnect.
|
Three steps to get started:
|
||||||
|
|
||||||
To get a working setup you need:
|
1. **Install requirements** — mpv and a few optional extras
|
||||||
|
2. **Install SubMiner** — from the AUR, or download from GitHub Releases
|
||||||
|
3. **Launch the app** — first-run setup walks you through dictionaries, the launcher, and everything else
|
||||||
|
|
||||||
1. **mpv** launched with an IPC socket so SubMiner can read subtitle data
|
## 1. Install Requirements
|
||||||
2. **SubMiner** (the Electron overlay app)
|
|
||||||
3. **Dictionaries** imported into the bundled Yomitan instance (lookups won't work without at least one)
|
|
||||||
4. **Anki + AnkiConnect** _(optional but recommended)_ for card creation and enrichment
|
|
||||||
|
|
||||||
The `subminer` launcher script handles step 1 automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or the equivalent named pipe on Windows) — without it the overlay will start but subtitles will never appear.
|
Only **mpv** is strictly required to run SubMiner. Everything else enhances the experience but is optional.
|
||||||
|
|
||||||
## Requirements
|
| Dependency | Status | What it does |
|
||||||
|
| -------------------- | ----------- | --------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| mpv | Required | The video player SubMiner overlays on. Must support `--input-ipc-server`. |
|
||||||
|
| ffmpeg | Recommended | Audio extraction and screenshots for Anki cards. Without it SubMiner still runs, but media fields will be empty. |
|
||||||
|
| MeCab + mecab-ipadic | Recommended | Part-of-speech filtering for more precise N+1, JLPT, and frequency annotations. Without it annotations still render, but POS-based filtering is less accurate. |
|
||||||
|
| yt-dlp | Optional | YouTube playback and subtitle extraction. |
|
||||||
|
| fzf | Optional | Terminal-based video picker in the launcher. |
|
||||||
|
| rofi | Optional | GUI-based video picker (Linux). |
|
||||||
|
| chafa | Optional | Thumbnail previews in fzf. |
|
||||||
|
| ffmpegthumbnailer | Optional | Video thumbnail generation for the picker. |
|
||||||
|
| guessit | Optional | Better AniSkip title/season/episode parsing. |
|
||||||
|
| alass | Optional | Subtitle sync engine (preferred). Disabled without alass or ffsubsync. |
|
||||||
|
| ffsubsync | Optional | Audio-based subtitle sync engine. Disabled without alass or ffsubsync. |
|
||||||
|
| fuse2 | Linux only | Required to run the AppImage. |
|
||||||
|
|
||||||
### System Dependencies
|
### Linux
|
||||||
|
|
||||||
| Dependency | Required | Notes |
|
**Window backend** — you need one of these depending on your compositor:
|
||||||
| -------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| mpv | Yes | Must support IPC sockets (`--input-ipc-server`) |
|
|
||||||
| Bun | For wrapper | Required for `subminer` CLI wrapper and source builds. Pre-built releases (AppImage, DMG, installer) work without it — only the `subminer` wrapper script needs Bun on `PATH`. |
|
|
||||||
| ffmpeg | Recommended | Audio extraction and screenshot generation. Without it SubMiner still runs, but audio and image fields on Anki cards will be empty. |
|
|
||||||
| MeCab + mecab-ipadic | No | Adds part-of-speech data used to filter particles out of N+1, JLPT, and frequency annotations. Without it annotations still render, but POS-based filtering is less precise. |
|
|
||||||
| fuse2 | Linux only | Required for AppImage |
|
|
||||||
| yt-dlp | No | Recommended for YouTube playback and subtitle extraction |
|
|
||||||
|
|
||||||
### Platform-Specific
|
|
||||||
|
|
||||||
**Linux** — one of the following window backends:
|
|
||||||
|
|
||||||
- **Hyprland** — native Wayland support (uses `hyprctl`)
|
- **Hyprland** — native Wayland support (uses `hyprctl`)
|
||||||
- **Sway** — native Wayland support (uses `swaymsg`)
|
- **Sway** — native Wayland support (uses `swaymsg`)
|
||||||
@@ -43,8 +44,10 @@ Wayland has no universal API for window positioning — each compositor exposes
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S --needed mpv ffmpeg
|
sudo pacman -S --needed mpv ffmpeg
|
||||||
|
# Recommended
|
||||||
|
sudo pacman -S --needed mecab mecab-ipadic
|
||||||
# Optional
|
# Optional
|
||||||
sudo pacman -S --needed mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
|
sudo pacman -S --needed yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||||
# Optional: subtitle sync (at least one needed for subtitle syncing)
|
# Optional: subtitle sync (at least one needed for subtitle syncing)
|
||||||
paru -S --needed alass python-ffsubsync
|
paru -S --needed alass python-ffsubsync
|
||||||
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
||||||
@@ -58,8 +61,10 @@ sudo pacman -S --needed xdotool xorg-xwininfo
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install mpv ffmpeg
|
sudo apt install mpv ffmpeg
|
||||||
|
# Recommended
|
||||||
|
sudo apt install mecab libmecab-dev mecab-ipadic-utf8
|
||||||
# Optional
|
# Optional
|
||||||
sudo apt install mecab libmecab-dev mecab-ipadic-utf8 fzf rofi chafa ffmpegthumbnailer yt-dlp
|
sudo apt install yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||||
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
||||||
sudo apt install xdotool x11-utils
|
sudo apt install xdotool x11-utils
|
||||||
# Optional: subtitle sync
|
# Optional: subtitle sync
|
||||||
@@ -74,8 +79,10 @@ pip install ffsubsync
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dnf install mpv ffmpeg
|
sudo dnf install mpv ffmpeg
|
||||||
|
# Recommended
|
||||||
|
sudo dnf install mecab mecab-ipadic
|
||||||
# Optional
|
# Optional
|
||||||
sudo dnf install mecab mecab-ipadic fzf rofi chafa ffmpegthumbnailer yt-dlp
|
sudo dnf install yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||||
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
||||||
sudo dnf install xdotool xorg-x11-utils
|
sudo dnf install xdotool xorg-x11-utils
|
||||||
# Optional: subtitle sync
|
# Optional: subtitle sync
|
||||||
@@ -85,38 +92,32 @@ pip install ffsubsync
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
**macOS** — macOS 10.13 or later. Accessibility permission required for window tracking.
|
### macOS
|
||||||
|
|
||||||
|
macOS 11 (Big Sur) or later. Accessibility permission — the macOS setting that lets one app observe and position another app's windows — is required so the overlay can follow the mpv window (see [step 2](#macos-dmg)).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install mpv ffmpeg
|
brew install mpv ffmpeg
|
||||||
# Optional but recommended for annotations
|
# Recommended
|
||||||
brew install mecab mecab-ipadic
|
brew install mecab mecab-ipadic
|
||||||
# Optional
|
# Optional
|
||||||
brew install yt-dlp fzf rofi chafa ffmpegthumbnailer
|
brew install yt-dlp fzf chafa ffmpegthumbnailer
|
||||||
# Optional: subtitle sync
|
# Optional: subtitle sync
|
||||||
brew install alass
|
brew install alass
|
||||||
pip install ffsubsync
|
pip install ffsubsync
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows** — Windows 10 or later. Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on `PATH`. Keep `mpv.exe` on `PATH` for auto-discovery or set `mpv.executablePath` in config if it lives elsewhere. SubMiner's packaged build handles window tracking directly. Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary.
|
### Windows
|
||||||
|
|
||||||
### Optional Tools
|
Windows 10 or later. Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on `PATH`. Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary.
|
||||||
|
|
||||||
| Tool | Purpose |
|
No compositor tools or window helpers are needed — native window tracking is built in.
|
||||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| fzf | Terminal-based video picker (default) |
|
|
||||||
| rofi | GUI-based video picker |
|
|
||||||
| chafa | Thumbnail previews in fzf |
|
|
||||||
| ffmpegthumbnailer | Generate video thumbnails for picker |
|
|
||||||
| guessit | Better AniSkip title/season/episode parsing for file playback |
|
|
||||||
| alass | Subtitle sync engine (preferred) — must be on `PATH` or set `subsync.alass_path` in config; subtitle syncing is disabled without it or ffsubsync |
|
|
||||||
| ffsubsync | Subtitle sync engine (fallback) — must be on `PATH` or set `subsync.ffsubsync_path` in config; subtitle syncing is disabled without it or alass |
|
|
||||||
|
|
||||||
## Linux
|
## 2. Install SubMiner
|
||||||
|
|
||||||
### Arch Linux (AUR)
|
### Arch Linux (AUR) {#arch-aur}
|
||||||
|
|
||||||
Install [`subminer-bin`](https://aur.archlinux.org/packages/subminer-bin) from the AUR if you want the packaged Linux release managed by pacman. The package installs the official SubMiner AppImage plus the `subminer` wrapper.
|
Install [`subminer-bin`](https://aur.archlinux.org/packages/subminer-bin) from the AUR. The package includes the SubMiner AppImage and the `subminer` launcher.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
paru -S subminer-bin
|
paru -S subminer-bin
|
||||||
@@ -130,99 +131,73 @@ cd subminer-bin
|
|||||||
makepkg -si
|
makepkg -si
|
||||||
```
|
```
|
||||||
|
|
||||||
### AppImage (Recommended)
|
### Linux (AppImage) {#linux-appimage}
|
||||||
|
|
||||||
Download the latest AppImage and the `subminer` launcher from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest).
|
Download the latest AppImage from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
||||||
|
|
||||||
**Step 1 — Install Bun** (required for the launcher):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://bun.sh/install | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
The `subminer` launcher uses a Bun shebang. The AppImage itself does **not** need Bun — only the launcher does. If you skip the launcher and run the AppImage directly (for example `SubMiner.AppImage --start`), you can skip this step, but you will need to configure `mpv.conf` with `input-ipc-server=/tmp/subminer-socket` manually.
|
|
||||||
|
|
||||||
**Step 2 — Download and install:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.local/bin
|
mkdir -p ~/.local/bin
|
||||||
|
|
||||||
# Download and install AppImage
|
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage
|
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage
|
||||||
chmod +x ~/.local/bin/SubMiner.AppImage
|
chmod +x ~/.local/bin/SubMiner.AppImage
|
||||||
|
|
||||||
# Download and install the subminer launcher (recommended)
|
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer
|
|
||||||
chmod +x ~/.local/bin/subminer
|
|
||||||
|
|
||||||
# Download launcher support assets used for bundled runtime plugin injection
|
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
|
||||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
|
||||||
mkdir -p ~/.local/share/SubMiner/plugin/subminer
|
|
||||||
cp -R /tmp/plugin/subminer/. ~/.local/share/SubMiner/plugin/subminer/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `subminer` launcher is the recommended way to use SubMiner on Linux. It ensures mpv is launched with the correct IPC socket, SubMiner defaults, and the bundled runtime plugin so you don't need to configure `mpv.conf` or install a global mpv plugin.
|
::: tip Launcher install is optional
|
||||||
|
First-run setup can install [Bun](https://bun.sh) and the `subminer` command-line launcher for you automatically. You don't need to download the launcher separately.
|
||||||
|
|
||||||
|
If you prefer to install it manually, see [manual launcher install](#manual-launcher-install-linux).
|
||||||
|
:::
|
||||||
|
|
||||||
|
### macOS (DMG) {#macos-dmg}
|
||||||
|
|
||||||
|
Download the DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest), open it, and drag `SubMiner.app` into `/Applications`. A ZIP artifact is also available as a fallback.
|
||||||
|
|
||||||
|
**Gatekeeper:** If macOS blocks SubMiner on first launch, right-click the app and select **Open** to bypass the warning. Alternatively:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xattr -d com.apple.quarantine /Applications/SubMiner.app
|
||||||
|
```
|
||||||
|
|
||||||
|
**Accessibility permission:** Grant accessibility permission so the overlay can track the mpv window:
|
||||||
|
|
||||||
|
1. Open **System Settings** → **Privacy & Security** → **Accessibility**
|
||||||
|
2. Enable SubMiner in the list (add it if it does not appear)
|
||||||
|
|
||||||
|
::: tip Launcher install is optional
|
||||||
|
First-run setup can install [Bun](https://bun.sh) and the `subminer` command-line launcher for you automatically. You don't need to download the launcher separately.
|
||||||
|
|
||||||
|
If you prefer to install it manually, see [manual launcher install](#manual-launcher-install-macos).
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Windows (Installer) {#windows-installer}
|
||||||
|
|
||||||
|
Download the latest installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
||||||
|
|
||||||
|
- `SubMiner-<version>.exe` — installer (recommended)
|
||||||
|
- `SubMiner-<version>-win.zip` — portable fallback
|
||||||
|
|
||||||
|
Make sure `mpv.exe` is on your `PATH`, or set `mpv.executablePath` in the config during first-run setup.
|
||||||
|
|
||||||
### From Source
|
### From Source
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Linux</b></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
|
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
|
||||||
cd SubMiner
|
cd SubMiner
|
||||||
# if you cloned without --recurse-submodules:
|
|
||||||
git submodule update --init --recursive
|
|
||||||
|
|
||||||
bun install
|
bun install
|
||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
# Optional packaged Linux artifact
|
# Optional: build AppImage
|
||||||
bun run build:appimage
|
bun run build:appimage
|
||||||
```
|
```
|
||||||
|
|
||||||
Bundled Yomitan is built during `bun run build`.
|
Bundled Yomitan is built during `bun run build`.
|
||||||
If you prefer Make wrappers for local install flows, `make build-launcher` still generates `dist/launcher/subminer` and `make install` still installs the wrapper/theme/AppImage when those artifacts exist.
|
|
||||||
|
|
||||||
`make build` also builds the bundled Yomitan Chrome extension from the `vendor/subminer-yomitan` submodule into `build/yomitan` using Bun.
|
</details>
|
||||||
|
|
||||||
## macOS
|
<details>
|
||||||
|
<summary><b>macOS</b></summary>
|
||||||
### DMG (Recommended)
|
|
||||||
|
|
||||||
Download the **DMG** artifact from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Open it and drag `SubMiner.app` into `/Applications`.
|
|
||||||
|
|
||||||
A **ZIP** artifact is also available as a fallback — unzip and drag `SubMiner.app` into `/Applications`.
|
|
||||||
|
|
||||||
Install dependencies using Homebrew:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install mpv ffmpeg
|
|
||||||
# Optional but recommended if you use N+1, JLPT, or frequency annotations
|
|
||||||
brew install mecab mecab-ipadic
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Install the `subminer` launcher (recommended)
|
|
||||||
|
|
||||||
The `subminer` launcher is the recommended way to use SubMiner on macOS. It launches mpv with the correct IPC socket and SubMiner defaults so you don't need to set up an `mpv.conf` profile manually.
|
|
||||||
|
|
||||||
Download it from the same [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) page:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O /usr/local/bin/subminer
|
|
||||||
sudo chmod +x /usr/local/bin/subminer
|
|
||||||
```
|
|
||||||
|
|
||||||
Or with curl:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer
|
|
||||||
sudo chmod +x /usr/local/bin/subminer
|
|
||||||
```
|
|
||||||
|
|
||||||
::: warning Bun required for the launcher
|
|
||||||
The `subminer` launcher uses a Bun shebang (`#!/usr/bin/env bun`), so [Bun](https://bun.sh) must be installed and available on `PATH`. Install Bun if you haven't already: `curl -fsSL https://bun.sh/install | bash`.
|
|
||||||
:::
|
|
||||||
|
|
||||||
### From Source (macOS)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
|
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
|
||||||
@@ -231,122 +206,19 @@ git submodule update --init --recursive
|
|||||||
make build-macos
|
make build-macos
|
||||||
```
|
```
|
||||||
|
|
||||||
The built app will be available in the `release` directory (`.dmg` and `.zip`).
|
The built app will be in the `release` directory (`.dmg` and `.zip`). For unsigned local builds: `bun run build:mac:unsigned`.
|
||||||
|
|
||||||
For unsigned local builds:
|
</details>
|
||||||
|
|
||||||
```bash
|
<details>
|
||||||
bun run build:mac:unsigned
|
<summary><b>Windows</b></summary>
|
||||||
```
|
|
||||||
|
|
||||||
Build and install the launcher alongside the app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make install-macos
|
|
||||||
```
|
|
||||||
|
|
||||||
This builds the `subminer` launcher into `dist/launcher/subminer` and installs it to `~/.local/bin/subminer` along with the app bundle and rofi theme. To install to `/usr/local/bin` instead (already on the default macOS `PATH`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo make install-macos PREFIX=/usr/local
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gatekeeper
|
|
||||||
|
|
||||||
If macOS blocks SubMiner on first launch, right-click the app and select **Open** to bypass the warning. Alternatively, remove the quarantine attribute:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
xattr -d com.apple.quarantine /Applications/SubMiner.app
|
|
||||||
```
|
|
||||||
|
|
||||||
### Accessibility Permission
|
|
||||||
|
|
||||||
After launching SubMiner for the first time, grant accessibility permission:
|
|
||||||
|
|
||||||
1. Open **System Settings** → **Privacy & Security** → **Accessibility**
|
|
||||||
2. Enable SubMiner in the list (add it if it does not appear)
|
|
||||||
|
|
||||||
Without this permission, window tracking will not work and the overlay won't follow the mpv window.
|
|
||||||
|
|
||||||
### macOS Usage Notes
|
|
||||||
|
|
||||||
**Launching with the `subminer` launcher (recommended):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer video.mkv
|
|
||||||
```
|
|
||||||
|
|
||||||
The launcher handles the IPC socket and SubMiner defaults automatically. If you prefer to launch mpv manually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mpv --input-ipc-server=/tmp/subminer-socket video.mkv
|
|
||||||
```
|
|
||||||
|
|
||||||
**Config location:** `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`).
|
|
||||||
|
|
||||||
**MeCab paths (Homebrew):**
|
|
||||||
|
|
||||||
- Apple Silicon (M1/M2): `/opt/homebrew/bin/mecab`
|
|
||||||
- Intel: `/usr/local/bin/mecab`
|
|
||||||
|
|
||||||
Ensure `mecab` is available on your PATH when launching SubMiner.
|
|
||||||
|
|
||||||
**Fullscreen:** The overlay should appear correctly in fullscreen. If you encounter issues, check that accessibility permissions are granted.
|
|
||||||
|
|
||||||
**mpv plugin binary path:**
|
|
||||||
|
|
||||||
```ini
|
|
||||||
binary_path=/Applications/SubMiner.app/Contents/MacOS/subminer
|
|
||||||
```
|
|
||||||
|
|
||||||
## Windows
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> **Windows support is experimental.** Core features — mining, annotations, and dictionary lookups — work, but some functionality may be missing or unstable. Bug reports welcome.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
1. Install [`mpv`](https://mpv.io/installation/) and ensure `mpv.exe` is on `PATH`. If mpv is installed elsewhere, you can set `mpv.executablePath` in `config.jsonc` or use the first-run setup field to point at the executable.
|
|
||||||
2. Install [`ffmpeg`](https://ffmpeg.org/download.html) and add it to `PATH` — recommended for audio/screenshot extraction (without it, media fields on Anki cards will be empty).
|
|
||||||
3. _(Optional)_ Install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for annotation POS filtering.
|
|
||||||
|
|
||||||
No compositor tools or window helpers are needed — native window tracking is built in on Windows.
|
|
||||||
|
|
||||||
### Installer (Recommended)
|
|
||||||
|
|
||||||
Download the latest Windows installer from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
|
||||||
|
|
||||||
- `SubMiner-<version>.exe` installs the app, Start menu shortcut, and default files under `Program Files`
|
|
||||||
- `SubMiner-<version>.zip` is available as a portable fallback
|
|
||||||
|
|
||||||
### Getting Started on Windows
|
|
||||||
|
|
||||||
1. **Run `SubMiner.exe` once** — first-run setup creates `%APPDATA%\SubMiner\config.jsonc` and opens Yomitan settings for dictionary import. The global mpv plugin install is optional for compatibility; the SubMiner mpv shortcut injects the bundled runtime plugin.
|
|
||||||
2. **Create the SubMiner mpv shortcut** _(recommended)_ — the setup popup offers to create a `SubMiner mpv` Start Menu and/or Desktop shortcut. This is the recommended way to launch playback on Windows.
|
|
||||||
3. **Play a video** — double-click the shortcut, drag a video file onto it, or run from a terminal:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
& "C:\Program Files\SubMiner\SubMiner.exe" --launch-mpv "C:\Videos\episode 01.mkv"
|
|
||||||
```
|
|
||||||
|
|
||||||
The shortcut and `--launch-mpv` pass SubMiner's default IPC socket, subtitle args, and bundled runtime plugin directly — no `mpv.conf` profile or global mpv plugin install is needed.
|
|
||||||
|
|
||||||
### Windows-Specific Notes
|
|
||||||
|
|
||||||
- The `subminer` launcher script requires [Bun](https://bun.sh) and must be invoked with `bun run subminer` on Windows since the shebang is not supported. The **SubMiner mpv** shortcut or `SubMiner.exe --launch-mpv` is the simpler alternative.
|
|
||||||
- First-run plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is in a non-standard location.
|
|
||||||
- Plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket` — do not keep `/tmp/subminer-socket` on Windows.
|
|
||||||
- Config is stored at `%APPDATA%\SubMiner\config.jsonc`.
|
|
||||||
|
|
||||||
### From Source (Windows)
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git clone https://github.com/ksyasuda/SubMiner.git
|
git clone https://github.com/ksyasuda/SubMiner.git
|
||||||
cd SubMiner
|
cd SubMiner
|
||||||
bun install
|
bun install
|
||||||
|
|
||||||
# Windows requires building the texthooker-ui submodule manually before
|
# Windows requires building texthooker-ui manually before the main build
|
||||||
# the main build (Linux/macOS handle this automatically during `bun run build`).
|
|
||||||
Set-Location vendor/texthooker-ui
|
Set-Location vendor/texthooker-ui
|
||||||
bun install --frozen-lockfile
|
bun install --frozen-lockfile
|
||||||
bun run build
|
bun run build
|
||||||
@@ -355,85 +227,52 @@ Set-Location ../..
|
|||||||
bun run build:win
|
bun run build:win
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows installer builds already get the required NSIS `WinShell` helper through electron-builder's cached `nsis-resources` bundle.
|
</details>
|
||||||
No extra repo-local WinShell plugin install step is required.
|
|
||||||
|
|
||||||
## MPV Plugin
|
## 3. Launch & First-Run Setup
|
||||||
|
|
||||||
SubMiner-managed playback loads the bundled mpv plugin at runtime. No separate global mpv plugin install is required when launching from the app, the launcher, or the packaged Windows SubMiner mpv shortcut.
|
Launch SubMiner and the setup wizard will open automatically:
|
||||||
|
|
||||||
::: warning Important
|
|
||||||
If first-run setup detects an older global SubMiner mpv plugin under mpv's `scripts` directory, use **Remove legacy mpv plugin** so regular mpv playback stops loading SubMiner.
|
|
||||||
:::
|
|
||||||
|
|
||||||
See [MPV Plugin](/mpv-plugin) for the keybindings, script messages, and runtime configuration reference.
|
|
||||||
|
|
||||||
## Anki Setup (Recommended)
|
|
||||||
|
|
||||||
If you plan to mine Anki cards (the primary use case for most users):
|
|
||||||
|
|
||||||
1. Install [Anki](https://apps.ankiweb.net/).
|
|
||||||
2. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on — open Anki, go to **Tools → Add-ons → Get Add-ons**, enter code `2055492159`.
|
|
||||||
3. Restart Anki and keep it running while using SubMiner.
|
|
||||||
|
|
||||||
AnkiConnect listens on `http://127.0.0.1:8765` by default. SubMiner will connect to it automatically with no extra config needed for basic card creation.
|
|
||||||
|
|
||||||
For enrichment configuration (sentence, audio, screenshot fields), see [Anki Integration](/anki-integration).
|
|
||||||
|
|
||||||
## First-Run Setup
|
|
||||||
|
|
||||||
Run the setup wizard to create a default config and finish initial configuration. You do **not** need to create the config manually — SubMiner handles it.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Linux (AUR install)
|
||||||
|
subminer app --setup
|
||||||
|
|
||||||
|
# Linux (AppImage directly)
|
||||||
|
~/.local/bin/SubMiner.AppImage --setup
|
||||||
|
|
||||||
|
# macOS — launch SubMiner.app from /Applications, or:
|
||||||
subminer app --setup
|
subminer app --setup
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
On **Windows**, just run `SubMiner.exe` — the setup wizard opens automatically on first launch.
|
||||||
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
|
|
||||||
|
|
||||||
The setup popup walks you through:
|
The setup wizard walks you through:
|
||||||
|
|
||||||
- **Config file**: auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
|
- **Config file** — auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
|
||||||
- **mpv plugin**: install the bundled Lua plugin for in-player keybindings
|
- **Yomitan dictionaries** — import at least one dictionary so word lookups work
|
||||||
- **Yomitan dictionaries**: import at least one dictionary so lookups work
|
- **Bun + `subminer` launcher** _(optional)_ — installs the command-line launcher into a writable PATH directory
|
||||||
- **Windows shortcut** _(Windows only)_: optionally create a `SubMiner mpv` Start Menu/Desktop shortcut
|
- **Windows shortcut** _(Windows only)_ — create a `SubMiner mpv` Start Menu/Desktop shortcut
|
||||||
|
|
||||||
The `Finish setup` button stays disabled until the plugin is installed and at least one dictionary is imported. Once you finish, SubMiner will not show the popup again.
|
The `Finish setup` button requires a config file and at least one Yomitan dictionary. Bun and the launcher are optional and never block setup completion.
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> You can re-open the setup popup at any time with `subminer app --setup` or `SubMiner.AppImage --setup`.
|
> You can re-open the setup wizard at any time with `subminer app --setup` or `SubMiner.AppImage --setup`.
|
||||||
|
|
||||||
Once setup is complete, play a video to verify everything works:
|
### Play a Video
|
||||||
|
|
||||||
|
Once setup is complete:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer video.mkv
|
subminer video.mkv
|
||||||
```
|
```
|
||||||
|
|
||||||
You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay.
|
You should see the overlay appear over mpv. If subtitles are loaded, they will appear as interactive text in the overlay.
|
||||||
|
|
||||||
<details>
|
On **Windows**, the recommended way to play video is with the **SubMiner mpv** shortcut created during setup — double-click it, or drag a video file onto it.
|
||||||
<summary><b>More launch examples</b></summary>
|
|
||||||
|
|
||||||
```bash
|
### Verify Setup
|
||||||
# Optional explicit overlay start for setups with plugin auto_start=no
|
|
||||||
subminer --start video.mkv
|
|
||||||
|
|
||||||
# Useful launch modes for troubleshooting
|
Run the built-in diagnostic to confirm everything is working:
|
||||||
subminer --log-level debug video.mkv
|
|
||||||
SubMiner.AppImage --start --log-level debug
|
|
||||||
|
|
||||||
# Or with direct AppImage control
|
|
||||||
SubMiner.AppImage --background # Background tray service mode
|
|
||||||
SubMiner.AppImage --start
|
|
||||||
SubMiner.AppImage --start --dev
|
|
||||||
SubMiner.AppImage --help # Show all CLI options
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Verify Setup
|
|
||||||
|
|
||||||
After completing first-run setup, run the built-in diagnostic to confirm everything is in place:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer doctor
|
subminer doctor
|
||||||
@@ -441,19 +280,90 @@ subminer doctor
|
|||||||
|
|
||||||
This checks for the app binary, mpv, ffmpeg, config file, and socket path. Fix any failures before continuing.
|
This checks for the app binary, mpv, ffmpeg, config file, and socket path. Fix any failures before continuing.
|
||||||
|
|
||||||
> [!NOTE]
|
## Anki Setup (Recommended)
|
||||||
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly. Replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below.
|
|
||||||
|
If you plan to mine Anki cards:
|
||||||
|
|
||||||
|
1. Install [Anki](https://apps.ankiweb.net/)
|
||||||
|
2. Install [AnkiConnect](https://ankiweb.net/shared/info/2055492159) — open Anki → **Tools → Add-ons → Get Add-ons** → enter code `2055492159`
|
||||||
|
3. Restart Anki and keep it running while using SubMiner
|
||||||
|
|
||||||
|
AnkiConnect listens on `http://127.0.0.1:8765` by default. SubMiner connects automatically with no extra config needed.
|
||||||
|
|
||||||
|
For enrichment configuration (sentence, audio, screenshot fields), see [Anki Integration](/anki-integration).
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer -u
|
||||||
|
# or
|
||||||
|
subminer --update
|
||||||
|
```
|
||||||
|
|
||||||
|
SubMiner verifies AppImage, launcher, and rofi theme downloads against `SHA256SUMS.txt`. If the binary is in a protected path, SubMiner shows the exact command to run rather than elevating itself.
|
||||||
|
|
||||||
|
The tray "Check for Updates" entry installs the new app automatically on Linux, macOS, and Windows. On Linux it replaces the running `.AppImage` in place via `electron-updater`; AppImages managed by a system package (for example the AUR `/opt/SubMiner/SubMiner.AppImage`) are skipped so the package manager stays in charge.
|
||||||
|
|
||||||
|
`subminer -u` also performs the AppImage update directly from the launcher process, which is useful when SubMiner is not currently running.
|
||||||
|
|
||||||
|
## How It All Fits Together
|
||||||
|
|
||||||
|
SubMiner is an overlay that sits on top of mpv. It connects to mpv through an IPC socket, renders subtitles as interactive text using a bundled Yomitan dictionary engine, and optionally creates Anki flashcards via AnkiConnect.
|
||||||
|
|
||||||
|
The `subminer` launcher handles mpv IPC socket setup automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or `\\.\pipe\subminer-socket` on Windows) — without it the overlay starts but subtitles won't appear.
|
||||||
|
|
||||||
|
The bundled mpv plugin is injected at runtime automatically — you don't need to install it separately. It provides in-player keybindings (the `y` chord) for controlling the overlay from within mpv. See [MPV Plugin](/mpv-plugin) for the full keybinding and configuration reference.
|
||||||
|
|
||||||
|
## Platform Notes
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
**MeCab paths (Homebrew):**
|
||||||
|
- Apple Silicon (M1/M2): `/opt/homebrew/bin/mecab`
|
||||||
|
- Intel: `/usr/local/bin/mecab`
|
||||||
|
|
||||||
|
Ensure `mecab` is available on your PATH when launching SubMiner.
|
||||||
|
|
||||||
|
**Fullscreen:** The overlay should appear correctly in fullscreen. If you encounter issues, check that accessibility permissions are granted.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
- The **SubMiner mpv** shortcut is the recommended way to launch playback. It starts `mpv.exe` with the right IPC socket and subtitle defaults.
|
||||||
|
- First-run setup adds only `%LOCALAPPDATA%\SubMiner\bin` to the HKCU user PATH. It does not add `SubMiner.exe` to PATH.
|
||||||
|
- IPC socket on Windows is `\\.\pipe\subminer-socket` — do not use `/tmp/subminer-socket`.
|
||||||
|
- Config is stored at `%APPDATA%\SubMiner\config.jsonc`.
|
||||||
|
|
||||||
|
## Manual Launcher Install
|
||||||
|
|
||||||
|
The `subminer` launcher uses a [Bun](https://bun.sh) shebang, so Bun must be installed. First-run setup can handle this automatically, but if you prefer to do it yourself:
|
||||||
|
|
||||||
|
### Linux {#manual-launcher-install-linux}
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Bun
|
||||||
|
curl -fsSL https://bun.sh/install | bash
|
||||||
|
|
||||||
|
# Download the launcher
|
||||||
|
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer
|
||||||
|
chmod +x ~/.local/bin/subminer
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS {#manual-launcher-install-macos}
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Bun
|
||||||
|
curl -fsSL https://bun.sh/install | bash
|
||||||
|
|
||||||
|
# Download the launcher
|
||||||
|
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer
|
||||||
|
sudo chmod +x /usr/local/bin/subminer
|
||||||
|
```
|
||||||
|
|
||||||
## Optional Extras
|
## Optional Extras
|
||||||
|
|
||||||
### Rofi Theme (Linux Only)
|
### Rofi Theme (Linux Only)
|
||||||
|
|
||||||
SubMiner ships a custom rofi theme bundled in the release assets tarball.
|
SubMiner ships a custom rofi theme in the release assets:
|
||||||
|
|
||||||
Install path (default auto-detected by `subminer`):
|
|
||||||
|
|
||||||
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
|
|
||||||
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
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
|
||||||
|
|||||||
@@ -36,6 +36,37 @@ flowchart TB
|
|||||||
style E fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
style E fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Runtime Sockets
|
||||||
|
|
||||||
|
The renderer↔main bridge above lives *inside* the Electron app. A separate set of OS sockets connects the app to the other runtimes — mpv and the launcher/plugin. These carry no renderer payloads and bypass the contract/validator layer; they are command and property channels between processes.
|
||||||
|
|
||||||
|
- **mpv IPC socket** (`/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows): the `MpvIpcClient` in the main process connects here to send JSON commands and subscribe to playback/subtitle properties via `observe_property`. Created by mpv's `--input-ipc-server`.
|
||||||
|
- **App control socket** (`/tmp/subminer-control-<uid>-<hash>.sock`, or a named pipe on Windows): the launcher and the mpv plugin send CLI-style commands (`--start`, `--show-visible-overlay`, `--texthooker`) to a running app here. It also dedupes a second `subminer` invocation into the existing instance instead of launching twice.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef app fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef ext fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
|
||||||
|
subgraph MpvProc["mpv process"]
|
||||||
|
direction TB
|
||||||
|
Mpv["mpv core"]:::ext
|
||||||
|
Plugin["SubMiner plugin (Lua)"]:::extrt
|
||||||
|
end
|
||||||
|
|
||||||
|
Launcher["Launcher CLI"]:::extrt
|
||||||
|
App["SubMiner app (Electron main)"]:::app
|
||||||
|
|
||||||
|
App <-->|"mpv IPC socket · /tmp/subminer-socket<br/>JSON commands + property observe"| Mpv
|
||||||
|
Launcher -->|"app control socket · /tmp/subminer-control-*<br/>--start, --show-visible-overlay, …"| App
|
||||||
|
Plugin -->|"app control socket<br/>spawn / attach"| App
|
||||||
|
|
||||||
|
style MpvProc fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
|
```
|
||||||
|
|
||||||
|
How these sockets are established during launch is covered in [Playback Startup Flow](./architecture#playback-startup-flow).
|
||||||
|
|
||||||
## Core Surfaces
|
## Core Surfaces
|
||||||
|
|
||||||
| File | Role |
|
| File | Role |
|
||||||
|
|||||||
@@ -1,185 +1,118 @@
|
|||||||
# Jellyfin Integration
|
# Jellyfin Integration
|
||||||
|
|
||||||
SubMiner includes an optional Jellyfin CLI integration for:
|
[Jellyfin](https://jellyfin.org) is a free, self-hosted media server — think of it as your own private streaming service for video you own. If you keep your anime on a Jellyfin server, SubMiner can play episodes through mpv with the full mining overlay.
|
||||||
|
|
||||||
- authenticating against a server
|
::: tip Who needs this?
|
||||||
- listing libraries and media items
|
This page is only relevant if you already run (or have access to) a Jellyfin server. If you watch local files or YouTube, you can skip it. The in-app setup window (`subminer jellyfin`) is the easiest starting point.
|
||||||
- launching item playback in the connected mpv instance
|
:::
|
||||||
- receiving Jellyfin remote cast-to-device playback events in-app
|
|
||||||
- opening an in-app setup window for server selection and authentication
|
SubMiner can act as a **cast-to-device target** for Jellyfin (similar to jellyfin-mpv-shim). Sign in once, turn on discovery, and SubMiner shows up in the "Play on…" / cast menu of any Jellyfin app — web, phone, or TV. Pick an episode, cast it to SubMiner, and it plays in SubMiner's mpv window with the full overlay and Yomitan click-to-lookup.
|
||||||
- toggling Jellyfin cast discovery from the tray once configured
|
|
||||||
|
This is the recommended way to use Jellyfin with SubMiner. A terminal-only option is covered in [Launcher playback](#launcher-playback) at the end.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Jellyfin server URL and user credentials
|
- A Jellyfin server plus your username and password
|
||||||
- For `--jellyfin-play`: connected mpv IPC socket (`--start` or existing mpv plugin workflow)
|
- SubMiner installed and running (see [Installation](/installation))
|
||||||
- On Linux, token encryption defaults to `gnome-libsecret`; pass `--password-store=<backend>` to override.
|
- On Linux, the session token is stored with `gnome-libsecret` by default
|
||||||
|
|
||||||
## Setup
|
## Quick start
|
||||||
|
|
||||||
1. Set base config values (`config.jsonc`):
|
### 1. Start SubMiner
|
||||||
|
|
||||||
|
Launch SubMiner so it's running in the system tray.
|
||||||
|
|
||||||
|
### 2. Sign in to your server
|
||||||
|
|
||||||
|
Open the tray menu and click **Configure Jellyfin**. In the window that opens, enter your **Server URL** (for example `http://127.0.0.1:8096`), **Username**, and **Password**, then click **Login**.
|
||||||
|
|
||||||
|
On success, SubMiner:
|
||||||
|
|
||||||
|
- saves an encrypted session token — your password is never stored,
|
||||||
|
- turns the Jellyfin integration on, and
|
||||||
|
- remembers the server and username for next time.
|
||||||
|
|
||||||
|
Reopen this window any time to switch servers or **Logout**.
|
||||||
|
|
||||||
|
### 3. Turn on discovery
|
||||||
|
|
||||||
|
Discovery is what makes SubMiner appear as a cast target. Two ways to enable it:
|
||||||
|
|
||||||
|
- **For the current session** — open the tray menu and tick **Jellyfin Discovery**. (This item appears once you've signed in.)
|
||||||
|
- **Automatically on every launch** — already on by default. After your first sign-in, SubMiner auto-connects to Jellyfin at startup, so the cast target is ready without touching the tray. You can change this under [Settings](#settings).
|
||||||
|
|
||||||
|
### 4. Cast from any Jellyfin app
|
||||||
|
|
||||||
|
In the Jellyfin web UI or mobile app, start playing something, open the **cast / "Play on"** menu, and pick your device — SubMiner appears there named after your computer's hostname. Playback opens in SubMiner.
|
||||||
|
|
||||||
|
From then on, pause / resume / seek / stop and audio or subtitle track changes you make in the Jellyfin app are mirrored in SubMiner, and your watch progress syncs back to Jellyfin (now-playing and resume position).
|
||||||
|
|
||||||
|
## What happens during playback
|
||||||
|
|
||||||
|
- **mpv launches automatically.** If mpv isn't already running when you cast, SubMiner starts it with SubMiner defaults and the bundled mpv plugin, so keybindings work right away.
|
||||||
|
- **The overlay is managed by SubMiner,** so your configured `subtitleStyle` controls how subtitles look. Use the [overlay-toggle shortcut](/shortcuts) to hide it for a session.
|
||||||
|
- **Resume works.** If Jellyfin has a saved position for the item, SubMiner seeks there on load.
|
||||||
|
- **Direct play first.** When the source allows it and the container is in your direct-play allowlist, SubMiner streams the original file; otherwise it requests a transcoded stream from Jellyfin.
|
||||||
|
- **Japanese subtitles are auto-selected,** preferring Jellyfin's default and embedded tracks over external sidecar files when several match.
|
||||||
|
- **Subtitle timing is corrected when possible.** SubMiner removes Jellyfin's server-selected subtitle stream from the mpv load URL, suppresses the mpv plugin's one-shot subtitle auto-selection and overlay auto-start for managed Jellyfin loads, stages downloaded subtitle tracks without letting mpv auto-switch between tracks, then selects the Japanese track once after applying any saved or inferred timing delay. When Jellyfin provides both Japanese and English subtitle files, SubMiner compares their cue timelines and applies a global delay if one track is clearly offset. Manual delay shifts you make with SubMiner's adjacent-cue controls are saved per item and subtitle track, then restored the next time you select that track.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
All Jellyfin options live under **Settings → Integrations → Jellyfin** (open settings from the tray's **Open SubMiner Settings**). The ones that matter for casting:
|
||||||
|
|
||||||
|
| Setting | Default | What it does |
|
||||||
|
| ------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **Enabled** | Off | Turns the Jellyfin integration on. Switched on for you when you sign in. |
|
||||||
|
| **Server Url** | — | Your Jellyfin server. Filled in when you sign in. |
|
||||||
|
| **Remote Control Enabled** | On | Lets SubMiner act as a cast target. |
|
||||||
|
| **Remote Control Auto Connect** | On | Connects to Jellyfin at startup so discovery is automatic. Turn off if you'd rather start it from the tray each time. |
|
||||||
|
| **Auto Announce** | Off | Re-broadcasts visibility on connect. Enable if your device is slow to appear in the cast menu. |
|
||||||
|
|
||||||
|
Prefer editing the config file? The same keys live under `jellyfin` in `config.jsonc`:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"serverUrl": "http://127.0.0.1:8096",
|
"serverUrl": "http://127.0.0.1:8096",
|
||||||
"recentServers": ["http://127.0.0.1:8096"],
|
|
||||||
"username": "your-user",
|
|
||||||
"remoteControlEnabled": true,
|
"remoteControlEnabled": true,
|
||||||
"remoteControlAutoConnect": true,
|
"remoteControlAutoConnect": true,
|
||||||
"autoAnnounce": false,
|
|
||||||
"remoteControlDeviceName": "SubMiner",
|
|
||||||
"defaultLibraryId": "",
|
|
||||||
"pullPictures": false,
|
|
||||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons",
|
|
||||||
"directPlayPreferred": true,
|
|
||||||
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"],
|
|
||||||
"transcodeVideoCodec": "h264",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Authenticate:
|
See [Configuration](/configuration) for the full list (transcode codec, direct-play containers, default library, and more).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**SubMiner doesn't appear in the cast menu**
|
||||||
|
|
||||||
|
- Make sure SubMiner is running.
|
||||||
|
- Make sure you're signed in — reopen **Configure Jellyfin** and log in again if your token expired.
|
||||||
|
- Make sure discovery is on (tray **Jellyfin Discovery**, or **Remote Control Auto Connect** in settings).
|
||||||
|
- Make sure SubMiner and the Jellyfin client point at the same server.
|
||||||
|
|
||||||
|
**Casting starts but nothing plays**
|
||||||
|
|
||||||
|
- Confirm the item plays normally in another Jellyfin client.
|
||||||
|
- If mpv was closed, give it a moment — SubMiner launches it on demand and retries.
|
||||||
|
|
||||||
|
**SubMiner keeps disconnecting**
|
||||||
|
|
||||||
|
- Check server/network stability and whether the session token has expired.
|
||||||
|
|
||||||
|
## Security notes
|
||||||
|
|
||||||
|
- The Jellyfin session (access token + user ID) is kept in SubMiner's local encrypted token storage. Your password is used only to log in and is never saved.
|
||||||
|
- Treat the token storage and your `config.jsonc` as secrets — don't commit them.
|
||||||
|
- Advanced/headless: the `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID` environment variables can supply a session without the sign-in window.
|
||||||
|
|
||||||
|
## Launcher playback
|
||||||
|
|
||||||
|
If you'd rather stay in the terminal, the `subminer` launcher can browse and play Jellyfin media directly, without casting from a Jellyfin app:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer jellyfin
|
subminer jellyfin -p # alias: subminer jf -p
|
||||||
subminer jellyfin -l \
|
|
||||||
--server http://127.0.0.1:8096 \
|
|
||||||
--username your-user \
|
|
||||||
--password 'your-password'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`subminer jellyfin` opens the setup window. It offers the configured server, recent servers, and a manual server URL field. Successful login keeps the window open, stores the Jellyfin session token in encrypted storage, updates the configured server/username/client metadata, and refreshes recent servers. Passwords are never stored.
|
This opens an fzf picker (add `-R` for rofi) to browse your libraries and episodes, then plays the selected item in SubMiner's mpv with the same overlay, resume, and subtitle behavior described above. Sign in first (step 2) so the launcher can reach your server. See [Launcher Script](/launcher-script) for the rest of the launcher's features.
|
||||||
|
|
||||||
3. List libraries:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SubMiner.AppImage --jellyfin-libraries
|
|
||||||
```
|
|
||||||
|
|
||||||
Launcher wrapper equivalent for interactive playback flow:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer jellyfin -p
|
|
||||||
```
|
|
||||||
|
|
||||||
Launcher wrapper for Jellyfin cast discovery mode (background app + tray):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer jellyfin -d
|
|
||||||
```
|
|
||||||
|
|
||||||
After Jellyfin is enabled with a server URL and SubMiner is already running, the tray menu shows `Jellyfin Discovery`. Use that checkbox to start or stop discovery for the current runtime session without changing config. If the stored login session is missing or expired, starting discovery shows a warning and setup remains the path to refresh credentials. It does not survive app restart.
|
|
||||||
|
|
||||||
Stop discovery session/app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer app --stop
|
|
||||||
```
|
|
||||||
|
|
||||||
`subminer jf ...` is an alias for `subminer jellyfin ...`.
|
|
||||||
|
|
||||||
To clear saved session credentials:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer jellyfin --logout
|
|
||||||
```
|
|
||||||
|
|
||||||
4. List items in a library:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SubMiner.AppImage --jellyfin-items --jellyfin-library-id LIBRARY_ID --jellyfin-search term
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional listing controls:
|
|
||||||
|
|
||||||
- `--jellyfin-recursive=true|false` (default: true)
|
|
||||||
- `--jellyfin-include-item-types=Series,Season,Folder,CollectionFolder,Movie,...`
|
|
||||||
|
|
||||||
These are used by the launcher picker flow to:
|
|
||||||
|
|
||||||
- keep root search focused on shows/folders/movies (exclude episode rows)
|
|
||||||
- browse selected anime/show directories as folder-or-file lists
|
|
||||||
- recurse for playable files only after selecting a folder
|
|
||||||
|
|
||||||
5. Start playback:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SubMiner.AppImage --start
|
|
||||||
SubMiner.AppImage --jellyfin-play --jellyfin-item-id ITEM_ID
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional stream overrides:
|
|
||||||
|
|
||||||
- `--jellyfin-audio-stream-index N`
|
|
||||||
- `--jellyfin-subtitle-stream-index N`
|
|
||||||
|
|
||||||
## Playback Behavior
|
|
||||||
|
|
||||||
- Direct play is attempted first when:
|
|
||||||
- `jellyfin.directPlayPreferred=true`
|
|
||||||
- media source supports direct stream
|
|
||||||
- source container matches `jellyfin.directPlayContainers`
|
|
||||||
- If direct play is not selected/available, SubMiner requests a Jellyfin transcoded stream (`master.m3u8`) using `jellyfin.transcodeVideoCodec`.
|
|
||||||
- Resume position (`PlaybackPositionTicks`) is applied via mpv seek.
|
|
||||||
- Media title is set in mpv as `[Jellyfin/<mode>] <title>`.
|
|
||||||
|
|
||||||
## Cast To Device Mode (jellyfin-mpv-shim style)
|
|
||||||
|
|
||||||
When SubMiner is running with a valid Jellyfin session, it can appear as a
|
|
||||||
remote playback target in Jellyfin's cast-to-device menu.
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- `jellyfin.enabled=true`
|
|
||||||
- valid `jellyfin.serverUrl` and Jellyfin auth session (env override or stored login session)
|
|
||||||
- `jellyfin.remoteControlEnabled=true` (default)
|
|
||||||
- `jellyfin.remoteControlAutoConnect=true` (default) for startup auto-connect
|
|
||||||
- `jellyfin.autoAnnounce=false` by default (`true` enables auto announce/visibility check logs on connect)
|
|
||||||
|
|
||||||
### Behavior
|
|
||||||
|
|
||||||
- SubMiner connects to Jellyfin remote websocket and posts playback capabilities.
|
|
||||||
- Startup auto-connect still requires `remoteControlAutoConnect=true`; the tray `Jellyfin Discovery` checkbox can start discovery later even when startup auto-connect is disabled.
|
|
||||||
- `Play` events open media in mpv with the same defaults used by `--jellyfin-play`.
|
|
||||||
- If mpv IPC is not connected at cast time, SubMiner auto-launches mpv in idle mode with SubMiner defaults and retries playback.
|
|
||||||
- `Playstate` events map to mpv pause/resume/seek/stop controls.
|
|
||||||
- Stream selection commands (`SetAudioStreamIndex`, `SetSubtitleStreamIndex`) are mapped to mpv track selection.
|
|
||||||
- SubMiner reports start/progress/stop timeline updates back to Jellyfin so now-playing and resume state stay synchronized.
|
|
||||||
- `--jellyfin-remote-announce` forces an immediate capability re-broadcast and logs whether server sessions can see the device.
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
- Device not visible in Jellyfin cast menu:
|
|
||||||
- ensure SubMiner is running
|
|
||||||
- ensure session token is valid (`--jellyfin-login` again if needed)
|
|
||||||
- ensure `remoteControlEnabled` is true
|
|
||||||
- use tray `Jellyfin Discovery` or `subminer jellyfin -d` to start discovery
|
|
||||||
- Cast command received but playback does not start:
|
|
||||||
- verify mpv IPC can connect (`--start` flow)
|
|
||||||
- verify item is playable from normal `--jellyfin-play --jellyfin-item-id ...`
|
|
||||||
- Frequent reconnects:
|
|
||||||
- check Jellyfin server/network stability and token expiration
|
|
||||||
|
|
||||||
## Failure Handling
|
|
||||||
|
|
||||||
User-visible errors are shown through CLI logs and mpv OSD for:
|
|
||||||
|
|
||||||
- invalid credentials
|
|
||||||
- expired/invalid token
|
|
||||||
- server/network errors
|
|
||||||
- missing library/item identifiers
|
|
||||||
- no playable source
|
|
||||||
- mpv not connected for playback
|
|
||||||
|
|
||||||
## Security Notes and Limitations
|
|
||||||
|
|
||||||
- Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted token storage after login/setup.
|
|
||||||
- Launcher wrappers support `--password-store=<backend>` and forward it through to the app process.
|
|
||||||
- Optional environment overrides are supported: `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID`.
|
|
||||||
- Treat both token storage and config files as secrets and avoid committing them.
|
|
||||||
- Password is used only for login and is not stored.
|
|
||||||
- Optional setup UI is available via `--jellyfin`; all actions are also available via CLI flags.
|
|
||||||
- `subminer` wrapper uses Jellyfin subcommands (`subminer jellyfin ...`, alias `subminer jf ...`). Use `SubMiner.AppImage` for direct `--jellyfin-libraries` and `--jellyfin-items`.
|
|
||||||
- For direct app CLI usage (`SubMiner.AppImage ...`), `--jellyfin-server` can override server URL for login/play flows without editing config.
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# Jimaku Integration
|
# Jimaku Integration
|
||||||
|
|
||||||
[Jimaku](https://jimaku.cc) is a community-driven subtitle repository for anime. SubMiner integrates with the Jimaku API so you can search, browse, and download Japanese subtitle files directly from the overlay — no alt-tabbing or manual file management required. Downloaded subtitles are loaded into mpv immediately.
|
[Jimaku](https://jimaku.cc) is a community-driven subtitle repository for anime — a shared online library of subtitle files contributed by other learners. SubMiner integrates with the Jimaku API so you can search, browse, and download Japanese subtitle files directly from the overlay — no alt-tabbing or manual file management required. Downloaded subtitles are loaded into mpv immediately.
|
||||||
|
|
||||||
|
::: tip Prerequisite: a free API key
|
||||||
|
You need a Jimaku account and an API key (a personal access string) before this feature works. Create an account at [jimaku.cc](https://jimaku.cc), copy your key, and add it to your config as shown under [Configuration](#configuration) below. Without a key, the search modal will report "Jimaku API key not set."
|
||||||
|
:::
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
The `subminer` launcher is an all-in-one script that handles video selection, mpv startup, and overlay management. It is the recommended way to use SubMiner on Linux and macOS because it guarantees mpv is launched with the correct IPC socket and SubMiner defaults. It's a Bun script distributed as a release asset alongside the AppImage and DMG.
|
The `subminer` launcher is an all-in-one script that handles video selection, mpv startup, and overlay management. It is the recommended way to use SubMiner on Linux and macOS because it guarantees mpv is launched with the correct IPC socket and SubMiner defaults. It's a Bun script distributed as a release asset alongside the AppImage and DMG.
|
||||||
|
|
||||||
::: tip Windows users
|
::: tip Windows users
|
||||||
On Windows the `subminer` script cannot run directly via shebang — use `bun run subminer` instead (e.g. `bun run subminer video.mkv`). The recommended alternative is the **SubMiner mpv** shortcut created during first-run setup, or `SubMiner.exe --launch-mpv`. See [Windows mpv Shortcut](/usage#windows-mpv-shortcut) for details.
|
On Windows, the recommended way to launch playback is the **SubMiner mpv** shortcut created during first-run setup — double-click it, drag a file onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See [Windows mpv Shortcut](/usage#windows-mpv-shortcut) for details.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Video Picker
|
## Video Picker
|
||||||
@@ -63,6 +63,8 @@ SUBMINER_ROFI_THEME=/path/to/custom-theme.rasi subminer -R
|
|||||||
subminer video.mkv # play a specific file (default plugin config auto-starts visible overlay)
|
subminer video.mkv # play a specific file (default plugin config auto-starts visible overlay)
|
||||||
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
|
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
|
||||||
subminer --backend x11 video.mkv # Force x11 backend for a specific file
|
subminer --backend x11 video.mkv # Force x11 backend for a specific file
|
||||||
|
subminer -u # check for SubMiner updates
|
||||||
|
subminer logs -e # export sanitized log ZIP
|
||||||
subminer stats # open immersion dashboard
|
subminer stats # open immersion dashboard
|
||||||
subminer stats -b # start background stats daemon
|
subminer stats -b # start background stats daemon
|
||||||
```
|
```
|
||||||
@@ -76,6 +78,8 @@ subminer stats -b # start background stats daemon
|
|||||||
| `subminer stats -b` | Start or reuse background stats daemon (non-blocking) |
|
| `subminer stats -b` | Start or reuse background stats daemon (non-blocking) |
|
||||||
| `subminer stats cleanup` | Backfill vocabulary metadata and prune stale rows |
|
| `subminer stats cleanup` | Backfill vocabulary metadata and prune stale rows |
|
||||||
| `subminer doctor` | Dependency + config + socket diagnostics |
|
| `subminer doctor` | Dependency + config + socket diagnostics |
|
||||||
|
| `subminer settings` | Open the SubMiner settings window |
|
||||||
|
| `subminer logs -e` | Export a sanitized log ZIP and print its path |
|
||||||
| `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 |
|
||||||
@@ -98,6 +102,8 @@ Use `subminer <subcommand> -h` for command-specific help.
|
|||||||
| `-r, --recursive` | Search directories recursively |
|
| `-r, --recursive` | Search directories recursively |
|
||||||
| `-R, --rofi` | Use rofi instead of fzf |
|
| `-R, --rofi` | Use rofi instead of fzf |
|
||||||
| `--setup` | Open first-run setup popup manually |
|
| `--setup` | Open first-run setup popup manually |
|
||||||
|
| `-v, --version` | Print installed SubMiner version |
|
||||||
|
| `-u, --update` | Check for SubMiner updates and update the app/launcher when possible |
|
||||||
| `--start` | Explicitly start overlay after mpv launches |
|
| `--start` | Explicitly start overlay after mpv launches |
|
||||||
| `-S, --start-overlay` | Explicitly 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 |
|
||||||
@@ -107,6 +113,8 @@ Use `subminer <subcommand> -h` for command-specific help.
|
|||||||
| `--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) |
|
||||||
|
|
||||||
|
On Linux, `subminer -u` updates from the launcher process itself. It can check and replace the AppImage, launcher, and rofi theme even when SubMiner is already running in the tray.
|
||||||
|
|
||||||
With default plugin settings (`auto_start=yes`, `auto_start_visible_overlay=yes`, `auto_start_pause_until_ready=yes`), explicit start flags are usually unnecessary.
|
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
|
||||||
|
|||||||
+85
-116
@@ -4,74 +4,15 @@ This guide walks through the sentence mining loop — from watching a video to c
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
SubMiner runs as a transparent overlay on top of mpv. As subtitles play, the overlay displays them as interactive text. You hover a word, trigger Yomitan lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, audio clip, and screenshot.
|
*Sentence mining* means turning real sentences you encounter while watching native video into Anki flashcards, so you learn vocabulary in the context where you actually met it. SubMiner automates the tedious parts of that loop.
|
||||||
|
|
||||||
## Subtitle Delivery Path (Startup + Runtime)
|
SubMiner runs as a transparent overlay on top of mpv (the video player). As subtitles play, the overlay displays them as interactive text. You hover a word, trigger a Yomitan dictionary lookup with your configured lookup key/modifier, then create an Anki card with a single action. SubMiner automatically attaches the sentence, an audio clip, and a screenshot to that card — no manual copy-pasting or screen capturing.
|
||||||
|
|
||||||
SubMiner prioritizes subtitle responsiveness over heavy initialization:
|
> **Yomitan** is the popup dictionary that shows definitions when you hover or scan a word. **AnkiConnect** is the add-on that lets SubMiner talk to Anki. Both are set up during installation — see [Anki Integration](/anki-integration) if you have not configured them yet.
|
||||||
|
|
||||||
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.
|
|
||||||
3. Under rapid subtitle churn, SubMiner uses a **latest-only tokenization queue** so stale lines are dropped instead of building lag.
|
|
||||||
4. MeCab, Yomitan extension load, and dictionary prewarm run as background warmups after overlay initialization (configurable via `startupWarmups`, including low-power mode).
|
|
||||||
|
|
||||||
This keeps early playback snappy and avoids mpv-side sluggishness while startup work completes.
|
|
||||||
|
|
||||||
## Overlay Model
|
|
||||||
|
|
||||||
SubMiner uses one overlay window with modal surfaces.
|
|
||||||
|
|
||||||
### Primary Subtitle Layer
|
|
||||||
|
|
||||||
The visible overlay renders subtitles as tokenized hoverable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
|
|
||||||
|
|
||||||
- Word-level hover targets for Yomitan lookup
|
|
||||||
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
|
|
||||||
- Auto pause/resume while the Yomitan popup is open (enabled by default via `subtitleStyle.autoPauseVideoOnYomitanPopup`)
|
|
||||||
- Right-click to pause/resume
|
|
||||||
- Right-click + drag to reposition subtitles
|
|
||||||
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
|
|
||||||
- **Reading annotations** — known words, N+1 targets, character-name matches, JLPT levels, and frequency hits can all be visually highlighted
|
|
||||||
|
|
||||||
Toggle visibility with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
|
|
||||||
|
|
||||||
### Secondary Subtitle Bar
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
It is controlled by `secondarySub` configuration and shares lifecycle with the main overlay window.
|
|
||||||
|
|
||||||
### Modal Surfaces
|
|
||||||
|
|
||||||
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
|
|
||||||
|
|
||||||
## Looking Up Words
|
|
||||||
|
|
||||||
1. Hover over the subtitle area — the overlay activates pointer events.
|
|
||||||
2. Hover the word you want. SubMiner keeps per-token boundaries so Yomitan can target that token cleanly.
|
|
||||||
3. Trigger Yomitan lookup with your configured lookup key/modifier (for example `Shift` if that is how your Yomitan profile is set up).
|
|
||||||
4. Yomitan opens its lookup popup for the hovered token.
|
|
||||||
5. From the popup, add the word to Anki.
|
|
||||||
|
|
||||||
### Controller Workflow
|
|
||||||
|
|
||||||
With a gamepad connected and keyboard-only mode enabled, the full mining loop works without a mouse or keyboard:
|
|
||||||
|
|
||||||
1. **Navigate** — push the left stick left/right to move the token highlight across subtitle words.
|
|
||||||
2. **Look up** — press `A` to trigger Yomitan lookup on the highlighted word.
|
|
||||||
3. **Browse the popup** — push the left stick up/down to smooth-scroll through the Yomitan popup, or use the right stick for larger jumps.
|
|
||||||
4. **Cycle audio** — press `R1` to move to the next dictionary audio entry, `L1` to play the current one.
|
|
||||||
5. **Mine** — press `X` to create an Anki card for the current sentence (same as `Ctrl+S`).
|
|
||||||
6. **Close** — press `B` to dismiss the Yomitan popup and return to subtitle navigation.
|
|
||||||
7. **Pause/resume** — press `L3` (left stick click) to toggle mpv pause at any time.
|
|
||||||
|
|
||||||
The controller and keyboard can be used interchangeably — switching mid-session is seamless. Toggle keyboard-only mode on or off with `Y` on the controller.
|
|
||||||
|
|
||||||
See [Usage — Controller Support](/usage#controller-support) for setup details and [Configuration — Controller Support](/configuration#controller-support) for the full mapping and tuning options.
|
|
||||||
|
|
||||||
## Creating Anki Cards
|
## Creating Anki Cards
|
||||||
|
|
||||||
There are three ways to create cards, depending on your workflow.
|
There are four ways to create or enrich cards, depending on your workflow.
|
||||||
|
|
||||||
### 1. Auto-Update from Yomitan
|
### 1. Auto-Update from Yomitan
|
||||||
|
|
||||||
@@ -80,11 +21,11 @@ This is the most common flow. Yomitan creates a card in Anki, and SubMiner enric
|
|||||||
1. Hover a word, then trigger Yomitan lookup → Yomitan popup appears.
|
1. Hover a word, then trigger Yomitan lookup → 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 receives or detects the new card:
|
||||||
- **Proxy mode** (`ankiConnect.proxy.enabled: true`): immediate enrich after successful `addNote` / `addNotes`.
|
- **Proxy mode** (default, `ankiConnect.proxy.enabled: true`): immediate enrich after a successful `addNote` / `addNotes` is pushed through the local proxy.
|
||||||
- **Polling mode** (default): detects via AnkiConnect polling (`ankiConnect.pollingRate`, default 3 seconds).
|
- **Polling mode** (fallback, when the proxy is disabled): detects new cards 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 optional configured padding).
|
||||||
- **Image**: A screenshot or animated clip from the current playback position.
|
- **Image**: A screenshot or animated clip from the current playback position.
|
||||||
- **Translation**: From the secondary subtitle track, or generated via AI if configured.
|
- **Translation**: From the secondary subtitle track, or generated via AI if configured.
|
||||||
- **MiscInfo**: Metadata like filename and timestamp.
|
- **MiscInfo**: Metadata like filename and timestamp.
|
||||||
@@ -131,55 +72,88 @@ After adding a word via Yomitan, press the audio card shortcut to overwrite the
|
|||||||
Audio card marking requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration — Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
|
Audio card marking requires a [Lapis](https://github.com/donkuri/lapis) or [Kiku](https://github.com/youyoumu/kiku) compatible note type and `ankiConnect.isLapis.enabled: true` in your config. See [Anki Integration — Sentence Cards](/anki-integration#sentence-cards-lapis) for setup.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Secondary Subtitles
|
### Field Grouping (Kiku)
|
||||||
|
|
||||||
SubMiner can display a secondary subtitle track (typically English) alongside the primary Japanese subtitles. This is useful for:
|
|
||||||
|
|
||||||
- Quick comprehension checks without leaving the mining flow.
|
|
||||||
- Auto-populating the translation field on mined cards.
|
|
||||||
|
|
||||||
### Display Modes
|
|
||||||
|
|
||||||
Cycle through modes with the configured shortcut:
|
|
||||||
|
|
||||||
- **Hidden**: Secondary subtitle not shown.
|
|
||||||
- **Visible**: Always displayed below the primary subtitle.
|
|
||||||
- **Hover**: Only shown when you hover over the primary subtitle.
|
|
||||||
|
|
||||||
When a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
|
|
||||||
|
|
||||||
## Field Grouping (Kiku)
|
|
||||||
|
|
||||||
If you mine the same word from different sentences, SubMiner can merge the cards instead of creating duplicates. This feature is designed for use with [Kiku](https://github.com/youyoumu/kiku) and similar note types that support grouped fields.
|
If you mine the same word from different sentences, SubMiner can merge the cards instead of creating duplicates. This feature is designed for use with [Kiku](https://github.com/youyoumu/kiku) and similar note types that support grouped fields.
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
1. You add a word via Yomitan.
|
1. You add a word via Yomitan.
|
||||||
2. SubMiner detects the new card and checks if a card with the same expression already exists.
|
2. SubMiner detects the new card and checks if a card with the same expression already exists.
|
||||||
3. If a duplicate is found:
|
3. If a duplicate is found (this requires `ankiConnect.isKiku.fieldGrouping` to be set to `"auto"` or `"manual"`; it defaults to `"disabled"`):
|
||||||
- **Auto mode** (`fieldGrouping: "auto"`): Merges automatically. Both sentences, audio clips, and images are combined into the existing card. The duplicate is optionally deleted.
|
- **Auto mode** (`ankiConnect.isKiku.fieldGrouping: "auto"`): Merges automatically. Both sentences, audio clips, and images are combined into the existing card. The duplicate is optionally deleted.
|
||||||
- **Manual mode** (`fieldGrouping: "manual"`): A modal appears showing both cards side by side. You choose which card to keep and preview the merged result before confirming.
|
- **Manual mode** (`ankiConnect.isKiku.fieldGrouping: "manual"`): A modal appears showing both cards side by side. You choose which card to keep and preview the merged result before confirming.
|
||||||
|
|
||||||
See [Anki Integration — Field Grouping](/anki-integration#field-grouping-kiku) for configuration options, merge behavior, and modal keyboard shortcuts.
|
See [Anki Integration — Field Grouping](/anki-integration#field-grouping-kiku) for configuration options, merge behavior, and modal keyboard shortcuts.
|
||||||
|
|
||||||
## Jimaku Subtitle Search
|
## Overlay Model
|
||||||
|
|
||||||
SubMiner integrates with [Jimaku](https://jimaku.cc) to search and download subtitle files for anime directly from the overlay.
|
SubMiner uses one overlay window with modal surfaces. It carries two subtitle bars — a primary reading bar and a secondary translation/context bar — plus modal dialogs that open on top.
|
||||||
|
|
||||||
1. Open the Jimaku modal via the configured shortcut (`Ctrl+Shift+J` by default).
|
Toggle the entire overlay window with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
|
||||||
2. SubMiner auto-fills the search from the current video filename (title, season, episode).
|
|
||||||
3. Browse matching entries and select a subtitle file to download.
|
|
||||||
4. The subtitle is loaded into mpv as a new track.
|
|
||||||
|
|
||||||
Requires an internet connection. An API key (`jimaku.apiKey`) is optional but recommended for higher rate limits.
|
### Primary Subtitle Layer
|
||||||
|
|
||||||
## Texthooker
|
The primary bar renders subtitles as tokenized hoverable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
|
||||||
|
|
||||||
SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) that serves a texthooker UI. This allows external tools — such as a browser-based Yomitan instance — to receive subtitle text in real time.
|
- Word-level hover targets for Yomitan lookup
|
||||||
|
- Auto pause/resume on subtitle hover (enabled by default via `subtitleStyle.autoPauseVideoOnHover`)
|
||||||
|
- Auto pause/resume while the Yomitan popup is open (enabled by default via `subtitleStyle.autoPauseVideoOnYomitanPopup`)
|
||||||
|
- Right-click to pause/resume
|
||||||
|
- Right-click + drag to reposition subtitles
|
||||||
|
- **Reading annotations** — known words, N+1 targets, character-name matches, JLPT levels, and frequency hits can all be visually highlighted
|
||||||
|
|
||||||
The texthooker page displays the current subtitle and updates as new lines arrive. This is useful if you prefer to do lookups in a browser rather than through the overlay's built-in Yomitan.
|
### Secondary Subtitle Bar
|
||||||
|
|
||||||
If you want to build your own browser client, websocket consumer, or automation relay, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
|
The secondary bar is a compact top-strip region in the same overlay window. It shows a secondary subtitle track (typically English) for translation/context while keeping the primary reading flow below. It is useful for:
|
||||||
|
|
||||||
|
- Quick comprehension checks without leaving the mining flow.
|
||||||
|
- Auto-populating the translation field on mined cards — when a card is created, SubMiner uses the secondary subtitle text as the translation field value (unless AI translation is configured to override it).
|
||||||
|
|
||||||
|
It is controlled by `secondarySub` configuration and shares its lifecycle with the main overlay window. Cycle which track feeds it with `Shift+J`.
|
||||||
|
|
||||||
|
### Display Modes
|
||||||
|
|
||||||
|
Both the primary and secondary subtitle bars share the same three visibility modes, and each can be changed independently at runtime:
|
||||||
|
|
||||||
|
- **Hidden** — the bar is not shown.
|
||||||
|
- **Visible** — the bar is always shown.
|
||||||
|
- **Hover** — the bar is revealed only while you hover over the overlay.
|
||||||
|
|
||||||
|
By default the **primary** bar is `visible` (`subtitleStyle.primaryDefaultMode`) and the **secondary** bar is `hover` (`secondarySub.defaultMode`).
|
||||||
|
|
||||||
|
Cycle each bar's mode at runtime with its own shortcut:
|
||||||
|
|
||||||
|
| Shortcut | Action | Config key |
|
||||||
|
| -------------------- | -------------------------------------------------------- | ------------------------------ |
|
||||||
|
| `V` | Cycle primary subtitle mode (hidden → visible → hover) | overlay-local |
|
||||||
|
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
|
||||||
|
|
||||||
|
### Modal Surfaces
|
||||||
|
|
||||||
|
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
|
||||||
|
|
||||||
|
## Looking Up Words
|
||||||
|
|
||||||
|
1. Hover over the subtitle area — the overlay activates pointer events.
|
||||||
|
2. Hover the word you want. SubMiner keeps per-token boundaries so Yomitan can target that token cleanly.
|
||||||
|
3. Trigger Yomitan lookup with your configured lookup key/modifier (for example `Shift` if that is how your Yomitan profile is set up).
|
||||||
|
4. Yomitan opens its lookup popup for the hovered token.
|
||||||
|
5. From the popup, add the word to Anki.
|
||||||
|
|
||||||
|
### Controller Workflow
|
||||||
|
|
||||||
|
With a gamepad connected and keyboard-only mode enabled, the full mining loop works without a mouse or keyboard:
|
||||||
|
|
||||||
|
1. **Navigate** — push the left stick left/right to move the token highlight across subtitle words.
|
||||||
|
2. **Look up** — press `A` to trigger Yomitan lookup on the highlighted word.
|
||||||
|
3. **Browse the popup** — push the left stick up/down to smooth-scroll through the Yomitan popup, or use the right stick for larger jumps.
|
||||||
|
4. **Cycle audio** — press `R1` to move to the next dictionary audio entry, `L1` to play the current one.
|
||||||
|
5. **Mine** — press `X` to create an Anki card for the current sentence (same as `Ctrl+S`).
|
||||||
|
6. **Close** — press `B` to dismiss the Yomitan popup and return to subtitle navigation.
|
||||||
|
7. **Pause/resume** — press `L3` (left stick click) to toggle mpv pause at any time.
|
||||||
|
|
||||||
|
After controller support is enabled, the controller and keyboard can be used interchangeably — switching mid-session is seamless. Toggle keyboard-only mode on or off with `Y` on the controller.
|
||||||
|
|
||||||
|
See [Usage — Controller Support](/usage#controller-support) for setup details and [Configuration — Controller Support](/configuration#controller-support) for the full mapping and tuning options.
|
||||||
|
|
||||||
## Subtitle Sync (Subsync)
|
## Subtitle Sync (Subsync)
|
||||||
|
|
||||||
@@ -190,29 +164,24 @@ If your subtitle file is out of sync with the audio, SubMiner can resynchronize
|
|||||||
3. For alass, select a reference subtitle track from the video.
|
3. For alass, select a reference subtitle track from the video.
|
||||||
4. SubMiner runs the sync and reloads the corrected subtitle.
|
4. SubMiner runs the sync and reloads the corrected subtitle.
|
||||||
|
|
||||||
|
For remote streams, including Jellyfin playback, the modal only offers alass. Jellyfin subtitle URLs are cached as temporary subtitle files so alass can read them, but the video stream is not downloaded. ffsubsync needs direct access to the local media file and is unavailable for stream URLs.
|
||||||
|
|
||||||
Install the sync tools separately — see [Troubleshooting](/troubleshooting#subtitle-sync-subsync) if the tools are not found.
|
Install the sync tools separately — see [Troubleshooting](/troubleshooting#subtitle-sync-subsync) if the tools are not found.
|
||||||
|
|
||||||
## N+1 Word Highlighting
|
## Texthooker
|
||||||
|
|
||||||
When enabled, SubMiner cross-references your Anki decks to highlight known words in the overlay, making true N+1 sentences (exactly one unknown word) easy to spot during immersion.
|
SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port) that serves a texthooker UI. This allows external tools — such as a browser-based Yomitan instance — to receive subtitle text in real time.
|
||||||
|
|
||||||
See [Subtitle Annotations — N+1](/subtitle-annotations#n1-word-highlighting) for configuration options and color settings.
|
The texthooker page displays the current subtitle and updates as new lines arrive. This is useful if you prefer to do lookups in a browser rather than through the overlay's built-in Yomitan.
|
||||||
|
|
||||||
## Immersion Tracking
|
If you want to build your own browser client, websocket consumer, or automation relay, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
|
||||||
|
|
||||||
SubMiner can log your watching and mining activity to a local SQLite database and expose it in the built-in stats dashboard — session times, words seen, cards mined, and daily/monthly rollups.
|
## Related Features
|
||||||
|
|
||||||
Enable it in your config:
|
These features support the mining loop but have their own dedicated pages:
|
||||||
|
|
||||||
```jsonc
|
- **[Jimaku subtitle search](/jimaku-integration)** — search and download anime subtitle files directly from the overlay (`Ctrl+Shift+J` by default), then load them into mpv.
|
||||||
"immersionTracking": {
|
- **[N+1 word highlighting](/subtitle-annotations#n1-word-highlighting)** — cross-reference your Anki decks to highlight known words, making true N+1 sentences (exactly one unknown word) easy to spot during immersion.
|
||||||
"enabled": true,
|
- **[Immersion tracking](/immersion-tracking)** — log watching and mining activity to a local database and view session times, words seen, and cards mined in the built-in stats dashboard.
|
||||||
"dbPath": "" // leave empty to use the default location
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Open the dashboard in the overlay with `stats.toggleKey` (default: `` ` ``), launch it in a browser with `subminer stats`, keep a dedicated background server alive with `subminer stats -b`, stop that background server with `subminer stats -s`, or visit `http://127.0.0.1:6969` directly if the local stats server is already running. The dashboard covers overview totals, anime progress, session detail, and vocabulary drill-down from the same local immersion database.
|
|
||||||
|
|
||||||
See [Immersion Tracking](/immersion-tracking) for dashboard details, schema, and retention settings.
|
|
||||||
|
|
||||||
Next: [Anki Integration](/anki-integration) — field mapping, media generation, and card enrichment configuration.
|
Next: [Anki Integration](/anki-integration) — field mapping, media generation, and card enrichment configuration.
|
||||||
|
|||||||
+43
-101
@@ -1,6 +1,10 @@
|
|||||||
# MPV Plugin
|
# MPV Plugin
|
||||||
|
|
||||||
The SubMiner mpv plugin (`subminer/main.lua`) provides in-player keybindings to control the overlay without leaving mpv. SubMiner-managed launches inject the bundled runtime plugin, so users do not need to install it into mpv's global `scripts` directory.
|
**What this is:** mpv is the video player SubMiner overlays subtitles on. The SubMiner mpv plugin is a small Lua script that runs *inside* mpv and gives you in-player keybindings to control the SubMiner overlay (start/stop/toggle, skip intro, etc.) without leaving the player window.
|
||||||
|
|
||||||
|
**Who needs this page:** Most users never touch the plugin directly — SubMiner-managed launches (the app, the `subminer` launcher, or the Windows shortcut) inject the bundled plugin automatically for that session, so there is nothing to install into mpv's global `scripts` directory. Read on if you launch mpv from another tool and want SubMiner's in-player controls, or you want to script mpv against SubMiner.
|
||||||
|
|
||||||
|
The plugin ships as a modular Lua package under `plugin/subminer/` (entry point `init.lua`, which loads `main.lua` and sibling modules). Earlier releases shipped a single global `main.lua`; runtime loading replaces it.
|
||||||
|
|
||||||
## Runtime Loading
|
## Runtime Loading
|
||||||
|
|
||||||
@@ -23,22 +27,45 @@ input-ipc-server=\\.\pipe\subminer-socket
|
|||||||
|
|
||||||
## Keybindings
|
## Keybindings
|
||||||
|
|
||||||
All keybindings use a `y` chord prefix — press `y`, then the second key:
|
Most plugin actions use a `y` chord prefix — press `y`, then the second key (a "chord"):
|
||||||
|
|
||||||
| 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 |
|
||||||
| `v` | Toggle primary subtitle bar visibility |
|
| `y-o` | Open settings window |
|
||||||
| `y-o` | Open settings window |
|
| `y-r` | Restart overlay |
|
||||||
| `y-r` | Restart overlay |
|
| `y-c` | Check status |
|
||||||
| `y-c` | Check status |
|
| `y-h` | Open session help / keybinding modal |
|
||||||
| `y-k` | Skip intro (AniSkip) |
|
| `v` | Toggle primary subtitle bar visibility |
|
||||||
|
| `TAB` (default) | Skip intro (AniSkip) |
|
||||||
|
|
||||||
|
The AniSkip key is **not** a `y` chord. It defaults to `TAB` and is configurable via `mpv.aniskipButtonKey`. The legacy `y-k` chord still works as a fallback unless you remap the AniSkip key onto it.
|
||||||
|
|
||||||
The bare `v` binding is a forced mpv binding. It overrides mpv's default primary subtitle visibility toggle and routes the action to SubMiner's primary subtitle bar instead.
|
The bare `v` binding is a forced mpv binding. It overrides mpv's default primary subtitle visibility toggle and routes the action to SubMiner's primary subtitle bar instead.
|
||||||
|
|
||||||
|
## Shared Shortcuts (Session Bindings)
|
||||||
|
|
||||||
|
The `y-*` chords above are built into the plugin. Everything else you configure under [`shortcuts.*`](/shortcuts) — plus any custom [`keybindings`](/configuration) and the stats toggle/mark-watched keys — is **injected into mpv at runtime**, so the same shortcut works both inside mpv and in the SubMiner overlay. You do not edit any mpv config to enable them.
|
||||||
|
|
||||||
|
How it works:
|
||||||
|
|
||||||
|
1. The SubMiner app compiles your configured shortcuts, custom keybindings, and stats keys into a normalized list and writes it to `session-bindings.json` in the SubMiner config directory.
|
||||||
|
2. On load, the plugin reads that file and registers each entry as a forced mpv key binding, translating each accelerator into the matching mpv key name.
|
||||||
|
3. When a binding fires, the plugin either runs a SubMiner action (by invoking the SubMiner binary with the corresponding CLI flag, e.g. `--mine-sentence`) or runs a raw mpv command, depending on what the shortcut maps to.
|
||||||
|
|
||||||
|
Because the bindings come from the same configuration the overlay uses, you maintain one set of shortcuts for both surfaces.
|
||||||
|
|
||||||
|
Live updates: changing a shortcut in the app rewrites `session-bindings.json` and sends the plugin a `subminer-reload-session-bindings` script message, so mpv re-registers the bindings immediately — no mpv restart required.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Accelerators are normalized per platform — `CommandOrControl` resolves to `Cmd` on macOS and `Ctrl` elsewhere.
|
||||||
|
- Multi-line actions (`copySubtitleMultiple`, `mineSentenceMultiple`) register temporary `1`–`9` digit follow-up bindings after the trigger key, with `Esc` to cancel.
|
||||||
|
- If two shortcuts compile to the same key, or an accelerator can't be mapped to an mpv key, the app logs a warning and skips that binding instead of registering a broken one.
|
||||||
|
|
||||||
## Menu
|
## Menu
|
||||||
|
|
||||||
Press `y-y` to open an interactive menu in mpv's OSD:
|
Press `y-y` to open an interactive menu in mpv's OSD:
|
||||||
@@ -55,93 +82,6 @@ SubMiner:
|
|||||||
|
|
||||||
Select an item by pressing its number.
|
Select an item by pressing its number.
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
For advanced/manual runtime use, create or edit `~/.config/mpv/script-opts/subminer.conf`:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
# Path to SubMiner binary. Leave empty for auto-detection.
|
|
||||||
binary_path=
|
|
||||||
|
|
||||||
# MPV IPC socket path. Must match input-ipc-server in mpv.conf.
|
|
||||||
socket_path=/tmp/subminer-socket
|
|
||||||
|
|
||||||
# Enable the texthooker WebSocket server.
|
|
||||||
texthooker_enabled=yes
|
|
||||||
|
|
||||||
# Port for the texthooker server.
|
|
||||||
texthooker_port=5174
|
|
||||||
|
|
||||||
# Window manager backend: auto, hyprland, sway, x11, macos, windows.
|
|
||||||
backend=auto
|
|
||||||
|
|
||||||
# Start the overlay automatically when a file is loaded.
|
|
||||||
# Runs only when mpv input-ipc-server matches socket_path.
|
|
||||||
auto_start=yes
|
|
||||||
|
|
||||||
# Show the visible overlay on auto-start.
|
|
||||||
# Runs only when mpv input-ipc-server matches socket_path.
|
|
||||||
auto_start_visible_overlay=yes
|
|
||||||
|
|
||||||
# Pause mpv on visible auto-start until SubMiner signals overlay/tokenization readiness.
|
|
||||||
# Requires auto_start=yes and auto_start_visible_overlay=yes.
|
|
||||||
auto_start_pause_until_ready=yes
|
|
||||||
|
|
||||||
# Show OSD messages for overlay status changes.
|
|
||||||
osd_messages=yes
|
|
||||||
|
|
||||||
# Logging level: debug, info, warn, error.
|
|
||||||
log_level=info
|
|
||||||
|
|
||||||
# Enable AniSkip intro detection/markers.
|
|
||||||
aniskip_enabled=yes
|
|
||||||
|
|
||||||
# Optional title override (launcher fills from guessit when available).
|
|
||||||
aniskip_title=
|
|
||||||
|
|
||||||
# Optional season override (launcher fills from guessit when available).
|
|
||||||
aniskip_season=
|
|
||||||
|
|
||||||
# Optional MAL ID override. Leave blank to resolve from media title.
|
|
||||||
aniskip_mal_id=
|
|
||||||
|
|
||||||
# Optional episode override. Leave blank to detect from filename/title.
|
|
||||||
aniskip_episode=
|
|
||||||
|
|
||||||
# Show OSD skip button while inside intro range.
|
|
||||||
aniskip_show_button=yes
|
|
||||||
|
|
||||||
# OSD label + keybinding for intro skip action.
|
|
||||||
aniskip_button_text=You can skip by pressing %s
|
|
||||||
aniskip_button_key=y-k
|
|
||||||
aniskip_button_duration=3
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option Reference
|
|
||||||
|
|
||||||
| Option | Default | Values | Description |
|
|
||||||
| ------------------------------ | ----------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
||||||
| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary |
|
|
||||||
| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path |
|
|
||||||
| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server |
|
|
||||||
| `texthooker_port` | `5174` | 1–65535 | Texthooker server port |
|
|
||||||
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
|
|
||||||
| `auto_start` | `yes` | `yes` / `no` | Auto-start overlay on file load when mpv socket matches `socket_path` |
|
|
||||||
| `auto_start_visible_overlay` | `yes` | `yes` / `no` | Show visible layer on auto-start when mpv socket matches `socket_path` |
|
|
||||||
| `auto_start_pause_until_ready` | `yes` | `yes` / `no` | Pause mpv on visible auto-start; resume when SubMiner signals tokenization-ready |
|
|
||||||
| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
|
|
||||||
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
|
|
||||||
| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection |
|
|
||||||
| `aniskip_title` | `""` | string | Override title used for lookup |
|
|
||||||
| `aniskip_season` | `""` | numeric season | Optional season hint |
|
|
||||||
| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id |
|
|
||||||
| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed |
|
|
||||||
| `aniskip_payload` | `""` | JSON / base64-encoded JSON | Optional pre-fetched AniSkip payload for this media. When set, plugin skips network lookup |
|
|
||||||
| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt |
|
|
||||||
| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) |
|
|
||||||
| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) |
|
|
||||||
| `aniskip_button_duration` | `3` | float seconds | OSD hint duration |
|
|
||||||
|
|
||||||
## Binary Auto-Detection
|
## Binary Auto-Detection
|
||||||
|
|
||||||
When `binary_path` is empty, the plugin searches platform-specific locations:
|
When `binary_path` is empty, the plugin searches platform-specific locations:
|
||||||
@@ -218,11 +158,13 @@ script-message subminer-start backend=hyprland socket=/custom/path texthooker=no
|
|||||||
- If the payload is absent or invalid, lookup falls back to title/MAL-based async fetch.
|
- If the payload is absent or invalid, lookup falls back to title/MAL-based async fetch.
|
||||||
- Install `guessit` for best detection quality (`python3 -m pip install --user guessit`).
|
- 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.
|
||||||
- At intro start, plugin shows an OSD hint for the first 3 seconds (`You can skip by pressing y-k` by default).
|
- At intro start, plugin shows an OSD hint for the first 3 seconds (`You can skip by pressing TAB` by default; the key reflects `mpv.aniskipButtonKey`).
|
||||||
- Use `script-message subminer-aniskip-refresh` after changing media metadata/options to retry lookup.
|
- Use `script-message subminer-aniskip-refresh` after changing media metadata/options to retry lookup.
|
||||||
|
|
||||||
## Lifecycle
|
## Lifecycle
|
||||||
|
|
||||||
|
For how the plugin's auto-start fits into the full launch sequence — including when the launcher starts the overlay instead of the plugin — see [Playback Startup Flow](./architecture#playback-startup-flow).
|
||||||
|
|
||||||
- **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, then defers AniSkip lookup until after startup 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).
|
- **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).
|
- **Duplicate auto-start events**: Repeated `file-loaded` hooks while overlay is already running are ignored for auto-start triggers (prevents duplicate start attempts).
|
||||||
|
|||||||
@@ -5,16 +5,15 @@
|
|||||||
"description": "In-repo VitePress documentation site for SubMiner",
|
"description": "In-repo VitePress documentation site for SubMiner",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev --host 0.0.0.0 --port 5173 --strictPort",
|
"docs:dev": "SUBMINER_DOCS_VERSION_LINK_ORIGIN=local bun run ../scripts/build-versioned-docs.ts && SUBMINER_DOCS_VERSION_LINK_ORIGIN=local SUBMINER_DOCS_VERSION_MANIFEST=\"$(bun run ../scripts/print-docs-version-manifest.ts)\" VITE_EXTRA_EXTENSIONS=jsonc vitepress dev --host 0.0.0.0 --port 5173 --strictPort",
|
||||||
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build",
|
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build",
|
||||||
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview --host 0.0.0.0 --port 4173 --strictPort",
|
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview --host 0.0.0.0 --port 4173 --strictPort",
|
||||||
"test": "bun test plausible.test.ts index.assets.test.ts docs-sync.test.ts seo.test.ts"
|
"test": "bun test plausible.test.ts index.assets.test.ts docs-sync.test.ts seo.test.ts .vitepress/theme/status-line.test.ts ../scripts/docs-versioning.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@catppuccin/vitepress": "^0.1.2",
|
"@catppuccin/vitepress": "^0.1.2",
|
||||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
"@fontsource/manrope": "^5.2.8",
|
"@fontsource/manrope": "^5.2.8",
|
||||||
"@plausible-analytics/tracker": "^0.4.4",
|
|
||||||
"mermaid": "^11.12.3"
|
"mermaid": "^11.12.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+57
-16
@@ -3,25 +3,66 @@ import { readFileSync } from 'node:fs';
|
|||||||
|
|
||||||
const docsConfigPath = new URL('./.vitepress/config.ts', import.meta.url);
|
const docsConfigPath = new URL('./.vitepress/config.ts', import.meta.url);
|
||||||
const docsThemePath = new URL('./.vitepress/theme/index.ts', import.meta.url);
|
const docsThemePath = new URL('./.vitepress/theme/index.ts', import.meta.url);
|
||||||
|
const docsPackagePath = new URL('./package.json', import.meta.url);
|
||||||
|
const versionedBuildPath = new URL('../scripts/build-versioned-docs.ts', import.meta.url);
|
||||||
const docsConfigContents = readFileSync(docsConfigPath, 'utf8');
|
const docsConfigContents = readFileSync(docsConfigPath, 'utf8');
|
||||||
const docsThemeContents = readFileSync(docsThemePath, 'utf8');
|
const docsThemeContents = readFileSync(docsThemePath, 'utf8');
|
||||||
|
const docsPackageContents = readFileSync(docsPackagePath, 'utf8');
|
||||||
|
const versionedBuildContents = readFileSync(versionedBuildPath, 'utf8');
|
||||||
|
|
||||||
test('docs site keeps docs hostname while sending plausible events to subminer.moe via worker.subminer.moe capture endpoint', () => {
|
test('docs site loads the docs.subminer.moe Plausible script through the analytics proxy', () => {
|
||||||
expect(docsConfigContents).toContain("const DOCS_HOSTNAME = 'https://docs.subminer.moe'");
|
expect(docsConfigContents).toContain("const DOCS_HOSTNAME = 'https://docs.subminer.moe'");
|
||||||
expect(docsConfigContents).toContain('hostname: DOCS_HOSTNAME');
|
expect(docsConfigContents).toContain(
|
||||||
expect(docsThemeContents).toContain("const PLAUSIBLE_DOMAIN = 'subminer.moe'");
|
"const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com'",
|
||||||
expect(docsThemeContents).toContain('const PLAUSIBLE_ENABLED_HOSTNAMES = new Set([');
|
|
||||||
expect(docsThemeContents).toContain("'docs.subminer.moe'");
|
|
||||||
expect(docsThemeContents).toContain(
|
|
||||||
"const PLAUSIBLE_ENDPOINT = 'https://worker.subminer.moe/api/capture'",
|
|
||||||
);
|
);
|
||||||
expect(docsThemeContents).toContain('@plausible-analytics/tracker');
|
expect(docsConfigContents).toContain(
|
||||||
expect(docsThemeContents).toContain('const { init } = await import');
|
"const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js'",
|
||||||
expect(docsThemeContents).toContain('!PLAUSIBLE_ENABLED_HOSTNAMES.has(window.location.hostname)');
|
);
|
||||||
expect(docsThemeContents).toContain('domain: PLAUSIBLE_DOMAIN');
|
expect(docsConfigContents).toContain(
|
||||||
expect(docsThemeContents).toContain('endpoint: PLAUSIBLE_ENDPOINT');
|
'const PLAUSIBLE_ENDPOINT = `${PLAUSIBLE_PROXY_HOSTNAME}/api/event`',
|
||||||
expect(docsThemeContents).toContain('outboundLinks: true');
|
);
|
||||||
expect(docsThemeContents).toContain('fileDownloads: true');
|
expect(docsConfigContents).toContain('hostname: DOCS_HOSTNAME');
|
||||||
expect(docsThemeContents).toContain('formSubmissions: true');
|
expect(docsConfigContents).toContain("rel: 'preconnect'");
|
||||||
expect(docsThemeContents).toContain('captureOnLocalhost: false');
|
expect(docsConfigContents).toContain('href: PLAUSIBLE_PROXY_HOSTNAME');
|
||||||
|
expect(docsConfigContents).toContain("async: ''");
|
||||||
|
expect(docsConfigContents).toContain(
|
||||||
|
'src: `${PLAUSIBLE_PROXY_HOSTNAME}${PLAUSIBLE_SITE_SCRIPT_PATH}`',
|
||||||
|
);
|
||||||
|
expect(docsConfigContents).toContain('plausible.init({ endpoint:');
|
||||||
|
expect(docsConfigContents).toContain('PLAUSIBLE_ENDPOINT');
|
||||||
|
expect(docsConfigContents).not.toContain("'data-domain'");
|
||||||
|
expect(docsConfigContents).not.toContain("'data-api'");
|
||||||
|
expect(docsThemeContents).not.toContain('@plausible-analytics/tracker');
|
||||||
|
expect(docsThemeContents).not.toContain('initPlausibleTracker');
|
||||||
|
expect(docsPackageContents).not.toContain('@plausible-analytics/tracker');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('versioned docs reuse current VitePress internals for old page snapshots', () => {
|
||||||
|
expect(versionedBuildContents).toContain("cpSync(join(currentDocsSite, '.vitepress')");
|
||||||
|
expect(versionedBuildContents).toContain('overlayCurrentVitePress(snapshotDocsSite)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('versioned docs build reports archive cache hits and rebuilds', () => {
|
||||||
|
expect(versionedBuildContents).toContain(
|
||||||
|
'console.info(`[docs] archive cache key ${archiveCacheKey.slice(0, 12)}`)',
|
||||||
|
);
|
||||||
|
expect(versionedBuildContents).toContain('console.info(`[docs] cache hit ${version}`)');
|
||||||
|
expect(versionedBuildContents).toContain('console.info(`[docs] rebuilding archive ${version}`)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('versioned docs build deduplicates public assets and prunes stale workspaces', () => {
|
||||||
|
expect(versionedBuildContents).toContain('dedupeVersionedPublicAssets({');
|
||||||
|
expect(versionedBuildContents).toContain('pruneArchiveCacheGenerations({');
|
||||||
|
expect(versionedBuildContents).toContain('rmSync(buildRoot, { recursive: true, force: true });');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('versioned docs archive cache key ignores generated and test-only files', () => {
|
||||||
|
expect(versionedBuildContents).toContain('isSharedInternalsHashIgnoredPath(path)');
|
||||||
|
expect(versionedBuildContents).toContain('|| /\\.test\\.[cm]?[jt]s$/.test(path)');
|
||||||
|
expect(versionedBuildContents).toContain('process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN');
|
||||||
|
expect(versionedBuildContents).not.toContain('hash.update(String(stat.mode))');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs builds exclude the internal README from VitePress page entries', () => {
|
||||||
|
expect(docsConfigContents).toContain("srcExclude: ['subagents/**', 'README.md']");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Overlay Auto-Start
|
// Visible Overlay Auto-Start
|
||||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
// Show the visible subtitle overlay automatically after managed mpv playback starts SubMiner.
|
||||||
|
// SubMiner can still auto-start in the background when this is false.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
"auto_start_overlay": true, // Show the visible subtitle overlay automatically when the bundled mpv plugin starts SubMiner. Values: true | false
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Texthooker Server
|
// Texthooker Server
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"texthooker": {
|
"texthooker": {
|
||||||
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
||||||
"openBrowser": false // Open browser setting. Values: true | false
|
"openBrowser": false // Open the texthooker page in the default browser when the server starts. Values: true | false
|
||||||
}, // Configure texthooker startup launch and browser opening behavior.
|
}, // Configure texthooker startup launch and browser opening behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -45,9 +46,16 @@
|
|||||||
// Logging
|
// Logging
|
||||||
// Controls logging verbosity.
|
// Controls logging verbosity.
|
||||||
// Set to debug for full runtime diagnostics.
|
// Set to debug for full runtime diagnostics.
|
||||||
|
// Hot-reload: logging.level and logging.files apply live while SubMiner is running.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"logging": {
|
"logging": {
|
||||||
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
|
"level": "warn", // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||||
|
"rotation": 7, // Number of days of app, launcher, and mpv logs to retain.
|
||||||
|
"files": {
|
||||||
|
"app": true, // Write SubMiner app runtime logs. Values: true | false
|
||||||
|
"launcher": true, // Write launcher command logs. Values: true | false
|
||||||
|
"mpv": false // Write mpv player logs. Enable temporarily when debugging mpv/plugin startup. Values: true | false
|
||||||
|
} // Files setting.
|
||||||
}, // Controls logging verbosity.
|
}, // Controls logging verbosity.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -81,7 +89,7 @@
|
|||||||
"rightStickPress": 10, // Raw button index used for controller R3 input.
|
"rightStickPress": 10, // Raw button index used for controller R3 input.
|
||||||
"leftTrigger": 6, // Raw button index used for controller L2 input.
|
"leftTrigger": 6, // Raw button index used for controller L2 input.
|
||||||
"rightTrigger": 7 // Raw button index used for controller R2 input.
|
"rightTrigger": 7 // Raw button index used for controller R2 input.
|
||||||
}, // Semantic button-name reference mapping used for legacy configs and debug output. Updating it does not rewrite existing raw binding descriptors.
|
}, // Semantic button-name reference mapping used for debug output. Updating it does not rewrite existing raw binding descriptors.
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"toggleLookup": {
|
"toggleLookup": {
|
||||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
@@ -138,7 +146,8 @@
|
|||||||
"axisIndex": 4, // Raw axis index captured for this analog controller action.
|
"axisIndex": 4, // Raw axis index captured for this analog controller action.
|
||||||
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||||
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
|
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||||
} // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
|
}, // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
|
||||||
|
"profiles": {} // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
|
||||||
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -152,33 +161,45 @@
|
|||||||
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
|
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
|
||||||
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
|
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
|
||||||
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
|
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
|
||||||
"jellyfinRemoteSession": true // Warm up Jellyfin remote session at startup. Values: true | false
|
"jellyfinRemoteSession": false // Warm up Jellyfin remote session at startup. Values: true | false
|
||||||
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Updates
|
||||||
|
// Automatic update check behavior.
|
||||||
|
// Manual checks from the tray or launcher are always allowed.
|
||||||
|
// ==========================================
|
||||||
|
"updates": {
|
||||||
|
"enabled": true, // Run automatic update checks in the background. Values: true | false
|
||||||
|
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
|
||||||
|
"notificationType": "system", // How SubMiner announces available updates. Values: system | osd | both | none
|
||||||
|
"channel": "stable" // Release channel used for update checks. Values: stable | prerelease
|
||||||
|
}, // Automatic update check behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keyboard Shortcuts
|
// Keyboard Shortcuts
|
||||||
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
// Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
// Hot-reload: shortcut changes apply live and update the session help modal on reopen.
|
// Hot-reload: shortcut changes apply live and update the session help modal on reopen.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Global accelerator that toggles overlay visibility from anywhere on the system. Use null to disable.
|
||||||
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
"copySubtitle": "CommandOrControl+C", // Accelerator that copies the current subtitle line to the clipboard.
|
||||||
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Accelerator that copies consecutive subtitle lines while the multi-copy window stays open.
|
||||||
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
"updateLastCardFromClipboard": "CommandOrControl+V", // Accelerator that updates the last mined Anki card using the current clipboard contents.
|
||||||
"triggerFieldGrouping": "CommandOrControl+G", // Trigger field grouping setting.
|
"triggerFieldGrouping": "CommandOrControl+G", // Accelerator that triggers Kiku field grouping on duplicate cards.
|
||||||
"triggerSubsync": "Ctrl+Alt+S", // Trigger subsync setting.
|
"triggerSubsync": "Ctrl+Alt+S", // Accelerator that triggers subsync against the active subtitle file.
|
||||||
"mineSentence": "CommandOrControl+S", // Mine sentence setting.
|
"mineSentence": "CommandOrControl+S", // Accelerator that mines the current sentence as a new Anki card.
|
||||||
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Mine sentence multiple setting.
|
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Accelerator that mines consecutive sentences while the multi-mine window stays open.
|
||||||
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
|
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
|
||||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
"toggleSecondarySub": "CommandOrControl+Shift+V", // Accelerator that toggles the secondary subtitle bar visibility.
|
||||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
"markAudioCard": "CommandOrControl+Shift+A", // Accelerator that marks the last mined card as an audio card.
|
||||||
"openCharacterDictionary": "CommandOrControl+Alt+A", // Open character dictionary setting.
|
"openCharacterDictionaryManager": "CommandOrControl+D", // Accelerator that opens the character dictionary manager modal.
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
"openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
|
||||||
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
"openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
|
||||||
"openSessionHelp": "CommandOrControl+Slash", // Open session help setting.
|
"openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet.
|
||||||
"openControllerSelect": "Alt+C", // Open controller select setting.
|
"openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal.
|
||||||
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
|
"openControllerDebug": "Alt+Shift+C", // Accelerator that opens the controller debug modal with live axis/button readouts.
|
||||||
"toggleSubtitleSidebar": "Backslash" // Toggle subtitle sidebar setting.
|
"toggleSubtitleSidebar": "Backslash" // Accelerator that toggles the subtitle sidebar visibility.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -315,20 +336,20 @@
|
|||||||
// Hot-reload: defaultMode updates live while SubMiner is running.
|
// Hot-reload: defaultMode updates live while SubMiner is running.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"secondarySub": {
|
"secondarySub": {
|
||||||
"secondarySubLanguages": [], // Secondary sub languages setting.
|
"secondarySubLanguages": [], // Language code priority list used to auto-select a secondary subtitle track when available.
|
||||||
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
"autoLoadSecondarySub": false, // Automatically load a matching secondary subtitle when the primary subtitle loads. Values: true | false
|
||||||
"defaultMode": "hover" // Default mode setting.
|
"defaultMode": "hover" // Default visibility mode for the secondary subtitle bar. Values: hidden | visible | hover
|
||||||
}, // Dual subtitle track options.
|
}, // Dual subtitle track options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Auto Subtitle Sync
|
// Subtitle Sync
|
||||||
// Subsync engine and executable paths.
|
// Subsync engine and executable paths.
|
||||||
|
// Hot-reload: subsync changes apply to the next subtitle sync run.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subsync": {
|
"subsync": {
|
||||||
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
"alass_path": "", // Optional absolute path to the alass binary used by subsync. Leave empty to auto-discover from PATH.
|
||||||
"alass_path": "", // Alass path setting.
|
"ffsubsync_path": "", // Optional absolute path to the ffsubsync binary used by subsync. Leave empty to auto-discover from PATH.
|
||||||
"ffsubsync_path": "", // Ffsubsync path setting.
|
"ffmpeg_path": "", // Optional absolute path to the ffmpeg binary used by subsync. Leave empty to auto-discover from PATH.
|
||||||
"ffmpeg_path": "", // Ffmpeg path setting.
|
|
||||||
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
|
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
|
||||||
}, // Subsync engine and executable paths.
|
}, // Subsync engine and executable paths.
|
||||||
|
|
||||||
@@ -337,7 +358,7 @@
|
|||||||
// Initial vertical subtitle position from the bottom.
|
// Initial vertical subtitle position from the bottom.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitlePosition": {
|
"subtitlePosition": {
|
||||||
"yPercent": 10 // Y percent setting.
|
"yPercent": 10 // Vertical position of the subtitle overlay expressed as a percentage from the bottom of the screen.
|
||||||
}, // Initial vertical subtitle position from the bottom.
|
}, // Initial vertical subtitle position from the bottom.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -347,29 +368,33 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitleStyle": {
|
"subtitleStyle": {
|
||||||
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
|
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
|
||||||
|
"css": {
|
||||||
|
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
|
"color": "#cad3f5", // Color setting.
|
||||||
|
"background-color": "transparent", // Background color setting.
|
||||||
|
"font-size": "35px", // Font size setting.
|
||||||
|
"font-weight": "600", // Font weight setting.
|
||||||
|
"font-style": "normal", // Font style setting.
|
||||||
|
"line-height": "1.35", // Line height setting.
|
||||||
|
"letter-spacing": "-0.01em", // Letter spacing setting.
|
||||||
|
"word-spacing": "0", // Word spacing setting.
|
||||||
|
"font-kerning": "normal", // Font kerning setting.
|
||||||
|
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||||
|
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
||||||
|
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
|
||||||
|
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
|
||||||
|
"--subtitle-hover-token-background-color": "transparent" // Subtitle hover token background color setting.
|
||||||
|
}, // CSS declaration object applied to primary subtitles after normal subtitle style defaults.
|
||||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
||||||
"autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
"autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||||
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
"primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
|
||||||
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
"nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
|
||||||
"nameMatchEnabled": true, // Enable subtitle token coloring for matches from the SubMiner character dictionary. Values: true | false
|
"nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
|
||||||
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
|
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
|
||||||
"fontFamily": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
"nPlusOneColor": "#c6a0f6", // Color used for the single N+1 target token subtitle highlight.
|
||||||
"fontSize": 35, // Font size setting.
|
"knownWordColor": "#a6da95", // Color used for known-word subtitle highlights.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
|
||||||
"fontWeight": "600", // 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 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
|
||||||
"fontStyle": "normal", // Font style setting.
|
|
||||||
"backgroundColor": "transparent", // Background color setting.
|
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
|
||||||
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
|
|
||||||
"knownWordColor": "#a6da95", // Known word color setting.
|
|
||||||
"jlptColors": {
|
"jlptColors": {
|
||||||
"N1": "#ed8796", // N1 setting.
|
"N1": "#ed8796", // N1 setting.
|
||||||
"N2": "#f5a97f", // N2 setting.
|
"N2": "#f5a97f", // N2 setting.
|
||||||
@@ -393,19 +418,21 @@
|
|||||||
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||||
}, // Frequency dictionary setting.
|
}, // Frequency dictionary setting.
|
||||||
"secondary": {
|
"secondary": {
|
||||||
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
|
"css": {
|
||||||
"fontSize": 24, // Font size setting.
|
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
"color": "#cad3f5", // Color setting.
|
||||||
"lineHeight": 1.35, // Line height setting.
|
"background-color": "transparent", // Background color setting.
|
||||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
"font-size": "24px", // Font size setting.
|
||||||
"wordSpacing": 0, // Word spacing setting.
|
"font-weight": "600", // Font weight setting.
|
||||||
"fontKerning": "normal", // Font kerning setting.
|
"font-style": "normal", // Font style setting.
|
||||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
"line-height": "1.35", // Line height setting.
|
||||||
"textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
"letter-spacing": "-0.01em", // Letter spacing setting.
|
||||||
"backgroundColor": "transparent", // Background color setting.
|
"word-spacing": "0", // Word spacing setting.
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
"font-kerning": "normal", // Font kerning setting.
|
||||||
"fontWeight": "600", // Font weight setting.
|
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||||
"fontStyle": "normal" // Font style setting.
|
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
|
||||||
|
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
|
||||||
|
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
|
||||||
} // Secondary setting.
|
} // Secondary setting.
|
||||||
}, // Primary and secondary subtitle styling.
|
}, // Primary and secondary subtitle styling.
|
||||||
|
|
||||||
@@ -419,18 +446,20 @@
|
|||||||
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
|
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
|
||||||
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
|
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
|
||||||
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
|
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
|
||||||
"pauseVideoOnHover": false, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
|
"pauseVideoOnHover": true, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
|
||||||
"autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
|
"autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
|
||||||
"maxWidth": 420, // Maximum sidebar width in CSS pixels.
|
"css": {
|
||||||
"opacity": 0.95, // Base opacity applied to the sidebar shell.
|
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
"backgroundColor": "rgba(73, 77, 100, 0.9)", // Background color for the subtitle sidebar shell.
|
"color": "#cad3f5", // Color setting.
|
||||||
"textColor": "#cad3f5", // Default cue text color in the subtitle sidebar.
|
"background-color": "rgba(73, 77, 100, 0.9)", // Background color setting.
|
||||||
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif", // Font family used for subtitle sidebar cue text.
|
"font-size": "16px", // Font size setting.
|
||||||
"fontSize": 16, // Base font size for subtitle sidebar cue text in CSS pixels.
|
"opacity": "0.95", // Opacity setting.
|
||||||
"timestampColor": "#a5adcb", // Timestamp color in the subtitle sidebar.
|
"--subtitle-sidebar-max-width": "420px", // Subtitle sidebar max width setting.
|
||||||
"activeLineColor": "#f5bde6", // Text color for the active subtitle cue.
|
"--subtitle-sidebar-timestamp-color": "#a5adcb", // Subtitle sidebar timestamp color setting.
|
||||||
"activeLineBackgroundColor": "rgba(138, 173, 244, 0.22)", // Background color for the active subtitle cue.
|
"--subtitle-sidebar-active-line-color": "#f5bde6", // Subtitle sidebar active line color setting.
|
||||||
"hoverLineBackgroundColor": "rgba(54, 58, 79, 0.84)" // Background color for hovered subtitle cues.
|
"--subtitle-sidebar-active-background-color": "rgba(138, 173, 244, 0.22)", // Subtitle sidebar active background color setting.
|
||||||
|
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)" // Subtitle sidebar hover background color setting.
|
||||||
|
} // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
|
||||||
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -441,22 +470,22 @@
|
|||||||
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
|
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
|
||||||
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
|
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
|
||||||
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
|
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
|
||||||
"model": "openai/gpt-4o-mini", // Model setting.
|
"model": "openai/gpt-4o-mini", // Default model identifier requested from the shared AI provider.
|
||||||
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
||||||
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // Default system prompt sent with shared AI provider requests.
|
||||||
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
||||||
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// AnkiConnect Integration
|
// AnkiConnect Integration
|
||||||
// Automatic Anki updates and media generation options.
|
// Automatic Anki updates and media generation options.
|
||||||
// Hot-reload: ankiConnect.ai.enabled updates live while SubMiner is running.
|
// Hot-reload: ankiConnect.ai.enabled, knownWords, nPlusOne, fields.word/audio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, and isKiku.fieldGrouping update live while SubMiner is running.
|
||||||
// Shared AI provider transport settings are read from top-level ai and typically require restart.
|
// Shared AI provider transport settings are read from top-level ai and typically require restart.
|
||||||
// Most other AnkiConnect settings still require restart.
|
// Most other AnkiConnect settings still require restart.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"ankiConnect": {
|
"ankiConnect": {
|
||||||
"enabled": true, // Enable AnkiConnect integration. Values: true | false
|
"enabled": true, // Enable AnkiConnect integration. Values: true | false
|
||||||
"url": "http://127.0.0.1:8765", // Url setting.
|
"url": "http://127.0.0.1:8765", // Base URL of the AnkiConnect HTTP server.
|
||||||
"pollingRate": 3000, // Polling interval in milliseconds.
|
"pollingRate": 3000, // Polling interval in milliseconds.
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
||||||
@@ -467,13 +496,14 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"SubMiner"
|
"SubMiner"
|
||||||
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||||
|
"deck": "", // Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to search all decks.
|
||||||
"fields": {
|
"fields": {
|
||||||
"word": "Expression", // Card field for the mined word or expression text.
|
"word": "Expression", // Card field for the mined word or expression text.
|
||||||
"audio": "ExpressionAudio", // Audio setting.
|
"audio": "ExpressionAudio", // Card field that receives generated sentence audio.
|
||||||
"image": "Picture", // Image setting.
|
"image": "Picture", // Card field that receives the captured screenshot or animated image.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"sentence": "Sentence", // Card field that receives the source sentence text.
|
||||||
"miscInfo": "MiscInfo", // Misc info setting.
|
"miscInfo": "MiscInfo", // Card field that receives the miscellaneous info pattern (see ankiConnect.metadata.pattern).
|
||||||
"translation": "SelectionText" // Translation setting.
|
"translation": "SelectionText" // Card field that receives the current selection or translated text.
|
||||||
}, // Fields setting.
|
}, // Fields setting.
|
||||||
"ai": {
|
"ai": {
|
||||||
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
|
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
|
||||||
@@ -481,59 +511,64 @@
|
|||||||
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
|
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
|
||||||
}, // Ai setting.
|
}, // Ai setting.
|
||||||
"media": {
|
"media": {
|
||||||
"generateAudio": true, // Generate audio setting. Values: true | false
|
"generateAudio": true, // Generate sentence audio for mined cards. Values: true | false
|
||||||
"generateImage": true, // Generate image setting. Values: true | false
|
"generateImage": true, // Generate screenshot or animated image for mined cards. Values: true | false
|
||||||
"imageType": "static", // Image type setting.
|
"imageType": "static", // Image capture type: "static" for a single still frame, "avif" for an animated AVIF. Values: static | avif
|
||||||
"imageFormat": "jpg", // Image format setting.
|
"imageFormat": "jpg", // Encoding format used when imageType is "static". Values: jpg | png | webp
|
||||||
"imageQuality": 92, // Image quality setting.
|
"imageQuality": 92, // Quality (0-100) used for lossy static image encoders.
|
||||||
"animatedFps": 10, // Animated fps setting.
|
"imageMaxWidth": 0, // Maximum width for static images, in pixels. Set to 0 to preserve the source resolution.
|
||||||
"animatedMaxWidth": 640, // Animated max width setting.
|
"imageMaxHeight": 0, // Maximum height for static images, in pixels. Set to 0 to preserve the source resolution.
|
||||||
"animatedCrf": 35, // Animated crf setting.
|
"animatedFps": 10, // Target frame rate for animated AVIF captures.
|
||||||
|
"animatedMaxWidth": 640, // Maximum width applied to animated AVIF captures.
|
||||||
|
"animatedMaxHeight": 0, // Maximum height for animated AVIF captures, in pixels. Set to 0 to preserve aspect ratio.
|
||||||
|
"animatedCrf": 35, // Animated AVIF CRF quality target. Lower values produce larger, higher-quality files.
|
||||||
"syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
|
"syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
|
||||||
"audioPadding": 0.5, // Audio padding setting.
|
"audioPadding": 0, // Seconds of padding appended to both ends of generated sentence audio and animated AVIF clips.
|
||||||
"fallbackDuration": 3, // Fallback duration setting.
|
"fallbackDuration": 3, // Fallback clip duration in seconds when subtitle timing data is unavailable.
|
||||||
"maxMediaDuration": 30 // Max media duration setting.
|
"maxMediaDuration": 30 // Maximum allowed media clip duration in seconds.
|
||||||
}, // Media setting.
|
}, // Media setting.
|
||||||
"knownWords": {
|
"knownWords": {
|
||||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||||
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
|
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
|
||||||
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false
|
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false
|
||||||
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Values: headword | surface
|
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Cache matches always receive known-word highlighting even when POS filters suppress other annotation types. Values: headword | surface
|
||||||
"decks": {}, // Decks and fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word", "Word Reading"] }.
|
"decks": {} // Decks and expression/word fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word"] }.
|
||||||
"color": "#a6da95" // Color used for known-word highlights.
|
|
||||||
}, // Known words setting.
|
}, // Known words setting.
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
"overwriteAudio": true, // When updating an existing card, overwrite the audio field instead of skipping it. Values: true | false
|
||||||
"overwriteImage": true, // Overwrite image setting. Values: true | false
|
"overwriteImage": true, // When updating an existing card, overwrite the image field instead of skipping it. Values: true | false
|
||||||
"mediaInsertMode": "append", // Media insert mode setting.
|
"mediaInsertMode": "append", // Whether new media is appended after or prepended before existing field contents on update. Values: append | prepend
|
||||||
"highlightWord": true, // Highlight word setting. Values: true | false
|
"highlightWord": true, // Bold the mined word inside the sentence field on the saved Anki card. Values: true | false
|
||||||
"notificationType": "osd", // Notification type setting.
|
"notificationType": "osd", // Notification surface used to announce mining and update outcomes. Values: osd | system | both | none
|
||||||
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
||||||
}, // Behavior setting.
|
}, // Behavior setting.
|
||||||
"nPlusOne": {
|
"nPlusOne": {
|
||||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
"enabled": false, // Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Requires known-word cache data. Values: true | false
|
||||||
"nPlusOne": "#c6a0f6" // Color used for the single N+1 target token highlight.
|
"minSentenceWords": 3 // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||||
}, // N plus one setting.
|
}, // N plus one setting.
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
|
"pattern": "[SubMiner] %f (%t)" // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
|
||||||
}, // Metadata setting.
|
}, // Metadata setting.
|
||||||
"isLapis": {
|
"isLapis": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enable Lapis-specific mining behaviors and sentence card model targeting. Values: true | false
|
||||||
"sentenceCardModel": "Japanese sentences" // Sentence card model setting.
|
"sentenceCardModel": "Lapis" // Note type name used by Lapis sentence cards.
|
||||||
}, // Is lapis setting.
|
}, // Is lapis setting.
|
||||||
"isKiku": {
|
"isKiku": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enable Kiku-specific mining behaviors (duplicate handling, field grouping). Values: true | false
|
||||||
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
||||||
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
|
"deleteDuplicateInAuto": true // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
|
||||||
} // Is kiku setting.
|
} // Is kiku setting.
|
||||||
}, // Automatic Anki updates and media generation options.
|
}, // Automatic Anki updates and media generation options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Jimaku
|
// Jimaku
|
||||||
// Jimaku API configuration and defaults.
|
// Jimaku API configuration and defaults.
|
||||||
|
// Hot-reload: Jimaku changes apply to the next Jimaku request.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"jimaku": {
|
"jimaku": {
|
||||||
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
"apiBaseUrl": "https://jimaku.cc", // Base URL of the Jimaku subtitle search API.
|
||||||
|
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
|
||||||
|
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
|
||||||
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
||||||
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
||||||
}, // Jimaku API configuration and defaults.
|
}, // Jimaku API configuration and defaults.
|
||||||
@@ -541,6 +576,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// YouTube Playback Settings
|
// YouTube Playback Settings
|
||||||
// Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
// Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
||||||
|
// Hot-reload: primarySubLanguages applies to the next YouTube subtitle load.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"youtube": {
|
"youtube": {
|
||||||
"primarySubLanguages": [
|
"primarySubLanguages": [
|
||||||
@@ -559,11 +595,8 @@
|
|||||||
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
||||||
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
||||||
"characterDictionary": {
|
"characterDictionary": {
|
||||||
"enabled": false, // Enable automatic Yomitan character dictionary sync for currently watched AniList media. Values: true | false
|
|
||||||
"refreshTtlHours": 168, // Legacy setting; merged character dictionary retention is now usage-based and this value is ignored.
|
|
||||||
"maxLoaded": 3, // Maximum number of most-recently-used anime snapshots included in the merged Yomitan character dictionary.
|
"maxLoaded": 3, // Maximum number of most-recently-used anime snapshots included in the merged Yomitan character dictionary.
|
||||||
"evictionPolicy": "delete", // Legacy setting; merged character dictionary eviction is usage-based and this value is ignored. Values: disable | delete
|
"profileScope": "all", // Yomitan profile scope for character dictionary settings updates. Values: all | active
|
||||||
"profileScope": "all", // Yomitan profile scope for dictionary enable/disable updates. Values: all | active
|
|
||||||
"collapsibleSections": {
|
"collapsibleSections": {
|
||||||
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
|
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
|
||||||
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
|
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
|
||||||
@@ -585,14 +618,25 @@
|
|||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// MPV Launcher
|
// MPV Launcher
|
||||||
// Optional mpv.exe override for Windows playback entry points.
|
// SubMiner-managed mpv launch and bundled plugin options.
|
||||||
|
// Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin.
|
||||||
|
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
|
||||||
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.
|
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.
|
||||||
|
// Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none.
|
||||||
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"mpv": {
|
"mpv": {
|
||||||
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||||
"launchMode": "normal" // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
"launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
||||||
}, // Optional mpv.exe override for Windows playback entry points.
|
"profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
|
||||||
|
"socketPath": "\\\\.\\pipe\\subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
|
||||||
|
"backend": "auto", // Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform. Values: auto | hyprland | sway | x11 | macos | windows
|
||||||
|
"autoStartSubMiner": true, // Start SubMiner in the background when SubMiner-managed mpv loads a file. Values: true | false
|
||||||
|
"pauseUntilOverlayReady": true, // Pause mpv on visible-overlay auto-start until SubMiner signals subtitle tokenization readiness. Values: true | false
|
||||||
|
"subminerBinaryPath": "", // Optional SubMiner app binary path passed to the bundled mpv plugin. Leave empty to use the launcher-detected app path.
|
||||||
|
"aniskipEnabled": true, // Enable AniSkip intro detection and skip markers in the bundled mpv plugin. Values: true | false
|
||||||
|
"aniskipButtonKey": "TAB" // mpv key used to trigger the AniSkip button while the skip marker is visible.
|
||||||
|
}, // SubMiner-managed mpv launch and bundled plugin options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Jellyfin
|
// Jellyfin
|
||||||
@@ -605,14 +649,10 @@
|
|||||||
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
|
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
|
||||||
"recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup.
|
"recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup.
|
||||||
"username": "", // Default Jellyfin username used during CLI login.
|
"username": "", // Default Jellyfin username used during CLI login.
|
||||||
"deviceId": "subminer", // Device id setting.
|
|
||||||
"clientName": "SubMiner", // Client name setting.
|
|
||||||
"clientVersion": "0.1.0", // Client version setting.
|
|
||||||
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
|
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
|
||||||
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
|
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
|
||||||
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
|
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
|
||||||
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
|
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
|
||||||
"remoteControlDeviceName": "SubMiner", // Device name reported for Jellyfin remote control sessions.
|
|
||||||
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
||||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
||||||
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
||||||
@@ -635,7 +675,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"discordPresence": {
|
"discordPresence": {
|
||||||
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
|
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||||
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal".
|
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal". Values: default | meme | japanese | minimal
|
||||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||||
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||||
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { expect, test } from 'bun:test';
|
import { expect, test } from 'bun:test';
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import type { TransformContext } from 'vitepress';
|
import type { TransformContext } from 'vitepress';
|
||||||
import docsConfig from './.vitepress/config';
|
import docsConfig from './.vitepress/config';
|
||||||
|
|
||||||
|
const docsSiteDir = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
function makeTransformContext(page: string): TransformContext {
|
function makeTransformContext(page: string): TransformContext {
|
||||||
return {
|
return {
|
||||||
page,
|
page,
|
||||||
@@ -31,6 +37,393 @@ test('docs pages emit stable self-referential canonical URLs', async () => {
|
|||||||
expect(JSON.stringify(rootHead).toLowerCase()).not.toContain('noindex');
|
expect(JSON.stringify(rootHead).toLowerCase()).not.toContain('noindex');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('main docs canonical uses /main/ and emits noindex', async () => {
|
||||||
|
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
|
||||||
|
const previousBase = process.env.SUBMINER_DOCS_BASE;
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = 'main';
|
||||||
|
process.env.SUBMINER_DOCS_BASE = '/main/';
|
||||||
|
const { default: mainDocsConfig } = await import('./.vitepress/config?main-docs');
|
||||||
|
|
||||||
|
const head = await mainDocsConfig.transformHead?.(makeTransformContext('usage.md'));
|
||||||
|
const rootHead = await mainDocsConfig.transformHead?.(makeTransformContext('index.md'));
|
||||||
|
|
||||||
|
expect(head).toContainEqual([
|
||||||
|
'link',
|
||||||
|
{ rel: 'canonical', href: 'https://docs.subminer.moe/main/usage' },
|
||||||
|
]);
|
||||||
|
expect(rootHead).toContainEqual([
|
||||||
|
'link',
|
||||||
|
{ rel: 'canonical', href: 'https://docs.subminer.moe/main/' },
|
||||||
|
]);
|
||||||
|
expect(head).toContainEqual(['meta', { name: 'robots', content: 'noindex,follow' }]);
|
||||||
|
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
|
||||||
|
process.env.SUBMINER_DOCS_BASE = previousBase;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('latest stable archive canonical points to root equivalent', async () => {
|
||||||
|
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
|
||||||
|
const previousBase = process.env.SUBMINER_DOCS_BASE;
|
||||||
|
const previousVersion = process.env.SUBMINER_DOCS_VERSION;
|
||||||
|
const previousLatest = process.env.SUBMINER_DOCS_LATEST_STABLE;
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = 'stable-archive';
|
||||||
|
process.env.SUBMINER_DOCS_BASE = '/v/0.14.0/';
|
||||||
|
process.env.SUBMINER_DOCS_VERSION = 'v0.14.0';
|
||||||
|
process.env.SUBMINER_DOCS_LATEST_STABLE = 'v0.14.0';
|
||||||
|
const { default: latestArchiveConfig } = await import('./.vitepress/config?latest-archive');
|
||||||
|
|
||||||
|
const head = await latestArchiveConfig.transformHead?.(makeTransformContext('usage.md'));
|
||||||
|
|
||||||
|
expect(head).toContainEqual([
|
||||||
|
'link',
|
||||||
|
{ rel: 'canonical', href: 'https://docs.subminer.moe/usage' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
|
||||||
|
process.env.SUBMINER_DOCS_BASE = previousBase;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION = previousVersion;
|
||||||
|
process.env.SUBMINER_DOCS_LATEST_STABLE = previousLatest;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stable archive theme links stay on the selected version', async () => {
|
||||||
|
const previousCwd = process.cwd();
|
||||||
|
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
|
||||||
|
const previousBase = process.env.SUBMINER_DOCS_BASE;
|
||||||
|
const previousVersion = process.env.SUBMINER_DOCS_VERSION;
|
||||||
|
const previousLatest = process.env.SUBMINER_DOCS_LATEST_STABLE;
|
||||||
|
const previousManifest = process.env.SUBMINER_DOCS_VERSION_MANIFEST;
|
||||||
|
const previousVersionLinkOrigin = process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN;
|
||||||
|
process.chdir(docsSiteDir);
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = 'stable-archive';
|
||||||
|
process.env.SUBMINER_DOCS_BASE = '/v/0.12.0/';
|
||||||
|
process.env.SUBMINER_DOCS_VERSION = 'v0.12.0';
|
||||||
|
process.env.SUBMINER_DOCS_LATEST_STABLE = 'v0.14.0';
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN = 'production';
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_MANIFEST = JSON.stringify({
|
||||||
|
latestStable: 'v0.14.0',
|
||||||
|
channels: [
|
||||||
|
{ label: 'Latest stable', path: '/' },
|
||||||
|
{ label: 'main', path: '/main/' },
|
||||||
|
],
|
||||||
|
versions: [
|
||||||
|
{ version: 'v0.14.0', path: '/v/0.14.0/' },
|
||||||
|
{ version: 'v0.12.0', path: '/v/0.12.0/' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { default: archiveConfig } = await import('./.vitepress/config?stable-archive-links');
|
||||||
|
|
||||||
|
const nav = archiveConfig.themeConfig?.nav as Array<{
|
||||||
|
text: string;
|
||||||
|
link?: string;
|
||||||
|
items?: Array<{ text: string; link: string }>;
|
||||||
|
}>;
|
||||||
|
const sidebar = archiveConfig.themeConfig?.sidebar as Array<{
|
||||||
|
text: string;
|
||||||
|
items?: Array<{ text: string; link: string }>;
|
||||||
|
}>;
|
||||||
|
const configurationNav = nav.find((item) => item.text === 'Configuration');
|
||||||
|
const versionNav = nav.find((item) => item.text === 'v0.12.0');
|
||||||
|
const referenceSidebar = sidebar.find((item) => item.text === 'Reference');
|
||||||
|
const configurationSidebar = referenceSidebar?.items?.find(
|
||||||
|
(item) => item.text === 'Configuration',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(configurationNav?.link).toBe('/configuration');
|
||||||
|
expect(configurationSidebar?.link).toBe('/configuration');
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'Latest stable (v0.14.0)',
|
||||||
|
link: 'https://docs.subminer.moe/',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'main',
|
||||||
|
link: 'https://docs.subminer.moe/main/',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'v0.14.0',
|
||||||
|
link: 'https://docs.subminer.moe/v/0.14.0/',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'v0.12.0',
|
||||||
|
link: 'https://docs.subminer.moe/v/0.12.0/',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(archiveConfig.themeConfig?.logo).toEqual({
|
||||||
|
light: '/assets/SubMiner.png',
|
||||||
|
dark: '/assets/SubMiner.png',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
process.chdir(previousCwd);
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
|
||||||
|
process.env.SUBMINER_DOCS_BASE = previousBase;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION = previousVersion;
|
||||||
|
process.env.SUBMINER_DOCS_LATEST_STABLE = previousLatest;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_MANIFEST = previousManifest;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN = previousVersionLinkOrigin;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('local stable archive version links stay on the dev server', async () => {
|
||||||
|
const previousCwd = process.cwd();
|
||||||
|
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
|
||||||
|
const previousBase = process.env.SUBMINER_DOCS_BASE;
|
||||||
|
const previousVersion = process.env.SUBMINER_DOCS_VERSION;
|
||||||
|
const previousLatest = process.env.SUBMINER_DOCS_LATEST_STABLE;
|
||||||
|
const previousManifest = process.env.SUBMINER_DOCS_VERSION_MANIFEST;
|
||||||
|
const previousVersionLinkOrigin = process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN;
|
||||||
|
process.chdir(docsSiteDir);
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = 'stable-archive';
|
||||||
|
process.env.SUBMINER_DOCS_BASE = '/v/0.10.0/';
|
||||||
|
process.env.SUBMINER_DOCS_VERSION = 'v0.10.0';
|
||||||
|
process.env.SUBMINER_DOCS_LATEST_STABLE = 'v0.14.0';
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN = 'local';
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_MANIFEST = JSON.stringify({
|
||||||
|
latestStable: 'v0.14.0',
|
||||||
|
channels: [
|
||||||
|
{ label: 'Latest stable', path: '/' },
|
||||||
|
{ label: 'main', path: '/main/' },
|
||||||
|
],
|
||||||
|
versions: [
|
||||||
|
{ version: 'v0.14.0', path: '/v/0.14.0/' },
|
||||||
|
{ version: 'v0.10.0', path: '/v/0.10.0/' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { default: archiveConfig } = await import('./.vitepress/config?local-archive-links');
|
||||||
|
|
||||||
|
const nav = archiveConfig.themeConfig?.nav as Array<{
|
||||||
|
text: string;
|
||||||
|
items?: Array<{ text: string; link: string }>;
|
||||||
|
}>;
|
||||||
|
const versionNav = nav.find((item) => item.text === 'v0.10.0');
|
||||||
|
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'Latest stable (v0.14.0)',
|
||||||
|
link: '../../',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'main',
|
||||||
|
link: '../../main/',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'v0.14.0',
|
||||||
|
link: '../0.14.0/',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'v0.10.0',
|
||||||
|
link: './',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
process.chdir(previousCwd);
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
|
||||||
|
process.env.SUBMINER_DOCS_BASE = previousBase;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION = previousVersion;
|
||||||
|
process.env.SUBMINER_DOCS_LATEST_STABLE = previousLatest;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_MANIFEST = previousManifest;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN = previousVersionLinkOrigin;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dev docs version links use local targets for version route testing', async () => {
|
||||||
|
const previousCwd = process.cwd();
|
||||||
|
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
|
||||||
|
const previousBase = process.env.SUBMINER_DOCS_BASE;
|
||||||
|
const previousVersion = process.env.SUBMINER_DOCS_VERSION;
|
||||||
|
const previousLatest = process.env.SUBMINER_DOCS_LATEST_STABLE;
|
||||||
|
const previousManifest = process.env.SUBMINER_DOCS_VERSION_MANIFEST;
|
||||||
|
const previousVersionLinkOrigin = process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN;
|
||||||
|
process.chdir(docsSiteDir);
|
||||||
|
delete process.env.SUBMINER_DOCS_CHANNEL;
|
||||||
|
delete process.env.SUBMINER_DOCS_BASE;
|
||||||
|
delete process.env.SUBMINER_DOCS_VERSION;
|
||||||
|
delete process.env.SUBMINER_DOCS_LATEST_STABLE;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN = 'local';
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_MANIFEST = JSON.stringify({
|
||||||
|
latestStable: 'v0.14.0',
|
||||||
|
channels: [
|
||||||
|
{ label: 'Latest stable', path: '/' },
|
||||||
|
{ label: 'main', path: '/main/' },
|
||||||
|
],
|
||||||
|
versions: [
|
||||||
|
{ version: 'v0.14.0', path: '/v/0.14.0/' },
|
||||||
|
{ version: 'v0.12.0', path: '/v/0.12.0/' },
|
||||||
|
{ version: 'v0.11.2', path: '/v/0.11.2/' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const { default: devConfig } = await import('./.vitepress/config?dev-version-links');
|
||||||
|
|
||||||
|
const nav = devConfig.themeConfig?.nav as Array<{
|
||||||
|
text: string;
|
||||||
|
items?: Array<{ text: string; link: string }>;
|
||||||
|
}>;
|
||||||
|
const versionNav = nav.find((item) => item.text === 'v0.14.0');
|
||||||
|
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'Latest stable (v0.14.0)',
|
||||||
|
link: '/',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'main',
|
||||||
|
link: '/main/',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(versionNav?.items).toContainEqual({
|
||||||
|
text: 'v0.12.0',
|
||||||
|
link: '/v/0.12.0/',
|
||||||
|
target: '_self',
|
||||||
|
noIcon: true,
|
||||||
|
});
|
||||||
|
expect(versionNav?.items?.map((item) => item.text)).toEqual([
|
||||||
|
'Latest stable (v0.14.0)',
|
||||||
|
'main',
|
||||||
|
'v0.14.0',
|
||||||
|
'v0.12.0',
|
||||||
|
'v0.11.2',
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
process.chdir(previousCwd);
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
|
||||||
|
process.env.SUBMINER_DOCS_BASE = previousBase;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION = previousVersion;
|
||||||
|
process.env.SUBMINER_DOCS_LATEST_STABLE = previousLatest;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_MANIFEST = previousManifest;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN = previousVersionLinkOrigin;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dev server redirects unserved version routes to production docs', () => {
|
||||||
|
let routeHandler:
|
||||||
|
| ((req: { url?: string }, res: DevRedirectResponse, next: () => void) => void)
|
||||||
|
| undefined;
|
||||||
|
const fakeServer = {
|
||||||
|
middlewares: {
|
||||||
|
use(handler: typeof routeHandler) {
|
||||||
|
routeHandler = handler;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const plugins = Array.isArray(docsConfig.vite?.plugins)
|
||||||
|
? docsConfig.vite.plugins
|
||||||
|
: [docsConfig.vite?.plugins].filter(Boolean);
|
||||||
|
const redirectPlugin = plugins.find(
|
||||||
|
(plugin): plugin is { name: string; configureServer: (server: never) => void } =>
|
||||||
|
Boolean(plugin) &&
|
||||||
|
typeof plugin === 'object' &&
|
||||||
|
'name' in plugin &&
|
||||||
|
plugin.name === 'subminer-docs-local-version-redirects' &&
|
||||||
|
'configureServer' in plugin,
|
||||||
|
);
|
||||||
|
expect(redirectPlugin).toBeDefined();
|
||||||
|
redirectPlugin?.configureServer(fakeServer as never);
|
||||||
|
|
||||||
|
const response = new DevRedirectResponse();
|
||||||
|
let nextCalled = false;
|
||||||
|
routeHandler?.({ url: '/v/0.14.0/?from=dev' }, response, () => {
|
||||||
|
nextCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextCalled).toBe(false);
|
||||||
|
expect(response.statusCode).toBe(302);
|
||||||
|
expect(response.headers.location).toBe('https://docs.subminer.moe/v/0.14.0/?from=dev');
|
||||||
|
|
||||||
|
const rootResponse = new DevRedirectResponse();
|
||||||
|
routeHandler?.({ url: '/configuration' }, rootResponse, () => {
|
||||||
|
nextCalled = true;
|
||||||
|
});
|
||||||
|
expect(rootResponse.ended).toBe(false);
|
||||||
|
expect(nextCalled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dev server serves local archive files for local version links', async () => {
|
||||||
|
const previousVersionLinkOrigin = process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN;
|
||||||
|
const previousArchiveDir = process.env.SUBMINER_DOCS_LOCAL_ARCHIVE_DIR;
|
||||||
|
const archiveDir = mkdtempSync(join(tmpdir(), 'subminer-docs-archive-'));
|
||||||
|
mkdirSync(join(archiveDir, 'v/0.14.0'), { recursive: true });
|
||||||
|
writeFileSync(join(archiveDir, 'v/0.14.0/index.html'), '<h1>local archive</h1>');
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN = 'local';
|
||||||
|
process.env.SUBMINER_DOCS_LOCAL_ARCHIVE_DIR = archiveDir;
|
||||||
|
try {
|
||||||
|
const { default: localDevConfig } = await import(
|
||||||
|
`./.vitepress/config?local-dev-redirects-${Date.now()}`
|
||||||
|
);
|
||||||
|
let routeHandler:
|
||||||
|
| ((req: { url?: string }, res: DevRedirectResponse, next: () => void) => void)
|
||||||
|
| undefined;
|
||||||
|
const fakeServer = {
|
||||||
|
middlewares: {
|
||||||
|
use(handler: typeof routeHandler) {
|
||||||
|
routeHandler = handler;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const plugins = Array.isArray(localDevConfig.vite?.plugins)
|
||||||
|
? localDevConfig.vite.plugins
|
||||||
|
: [localDevConfig.vite?.plugins].filter(Boolean);
|
||||||
|
const redirectPlugin = plugins.find(
|
||||||
|
(plugin): plugin is { name: string; configureServer: (server: never) => void } =>
|
||||||
|
Boolean(plugin) &&
|
||||||
|
typeof plugin === 'object' &&
|
||||||
|
'name' in plugin &&
|
||||||
|
plugin.name === 'subminer-docs-local-version-redirects' &&
|
||||||
|
'configureServer' in plugin,
|
||||||
|
);
|
||||||
|
redirectPlugin?.configureServer(fakeServer as never);
|
||||||
|
|
||||||
|
const response = new DevRedirectResponse();
|
||||||
|
let nextCalled = false;
|
||||||
|
routeHandler?.({ url: '/v/0.14.0/?from=dev' }, response, () => {
|
||||||
|
nextCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextCalled).toBe(false);
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.headers['content-type']).toBe('text/html; charset=utf-8');
|
||||||
|
expect(response.headers.location).toBeUndefined();
|
||||||
|
expect(response.body).toBe('<h1>local archive</h1>');
|
||||||
|
} finally {
|
||||||
|
process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN = previousVersionLinkOrigin;
|
||||||
|
process.env.SUBMINER_DOCS_LOCAL_ARCHIVE_DIR = previousArchiveDir;
|
||||||
|
rmSync(archiveDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class DevRedirectResponse {
|
||||||
|
statusCode = 200;
|
||||||
|
headers: Record<string, string> = {};
|
||||||
|
ended = false;
|
||||||
|
body = '';
|
||||||
|
|
||||||
|
setHeader(name: string, value: string) {
|
||||||
|
this.headers[name.toLowerCase()] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
end(chunk?: string | Uint8Array) {
|
||||||
|
if (chunk) {
|
||||||
|
this.body = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
|
||||||
|
}
|
||||||
|
this.ended = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test('docs sitemap excludes duplicate README page from indexable URLs', async () => {
|
test('docs sitemap excludes duplicate README page from indexable URLs', async () => {
|
||||||
const items = [{ url: '' }, { url: 'README' }, { url: 'usage' }];
|
const items = [{ url: '' }, { url: 'README' }, { url: 'usage' }];
|
||||||
|
|
||||||
|
|||||||
+36
-26
@@ -1,5 +1,13 @@
|
|||||||
# Keyboard Shortcuts
|
# Keyboard Shortcuts
|
||||||
|
|
||||||
|
This page is the complete reference for every keystroke SubMiner responds to. If you are just getting started, focus on the **Mining Shortcuts** and **Overlay Controls** sections — those cover the day-to-day mining loop. The rest can wait until you need them.
|
||||||
|
|
||||||
|
A few terms used throughout:
|
||||||
|
|
||||||
|
- **Overlay** — the transparent SubMiner window that sits on top of mpv and shows the interactive subtitles. Most shortcuts only work while this window has focus (click the video once if a shortcut seems to do nothing).
|
||||||
|
- **`Ctrl/Cmd`** — use `Ctrl` on Windows/Linux and `Cmd` (⌘) on macOS. In the config file this is written as `CommandOrControl`.
|
||||||
|
- **Accelerator** — Electron's name for a shortcut string like `Alt+Shift+O`.
|
||||||
|
|
||||||
All shortcuts are configurable in `config.jsonc` under `shortcuts` and `keybindings`. Set any shortcut to `null` to disable it.
|
All shortcuts are configurable in `config.jsonc` under `shortcuts` and `keybindings`. Set any shortcut to `null` to disable it.
|
||||||
|
|
||||||
## Global Shortcuts
|
## Global Shortcuts
|
||||||
@@ -29,7 +37,7 @@ These work when the overlay window has focus.
|
|||||||
| `Ctrl/Cmd+G` | Trigger field grouping (Kiku merge check) | `shortcuts.triggerFieldGrouping` |
|
| `Ctrl/Cmd+G` | Trigger field grouping (Kiku merge check) | `shortcuts.triggerFieldGrouping` |
|
||||||
| `Ctrl/Cmd+Shift+A` | Mark last card as audio card | `shortcuts.markAudioCard` |
|
| `Ctrl/Cmd+Shift+A` | Mark last card as audio card | `shortcuts.markAudioCard` |
|
||||||
|
|
||||||
The multi-line shortcuts open a digit selector with a 3-second timeout (`shortcuts.multiCopyTimeoutMs`). Press `1`–`9` to select how many recent subtitle lines to combine.
|
The multi-line shortcuts open a digit selector with a 3-second timeout (`shortcuts.multiCopyTimeoutMs`). Press `1`–`9` to select how many recent subtitle lines to combine. When the shortcut starts from mpv, SubMiner focuses the visible overlay for that selector instead of reserving the number keys in the mpv plugin.
|
||||||
|
|
||||||
## Overlay Controls
|
## Overlay Controls
|
||||||
|
|
||||||
@@ -39,7 +47,7 @@ These control playback and subtitle display. They require overlay window focus.
|
|||||||
| -------------------- | --------------------------------------------------- |
|
| -------------------- | --------------------------------------------------- |
|
||||||
| `Space` | Toggle mpv pause |
|
| `Space` | Toggle mpv pause |
|
||||||
| `F` | Toggle fullscreen |
|
| `F` | Toggle fullscreen |
|
||||||
| `V` | Toggle primary subtitle bar visibility |
|
| `V` | Cycle primary subtitle bar mode (hidden → visible → hover) |
|
||||||
| `J` | Cycle primary subtitle track |
|
| `J` | Cycle primary subtitle track |
|
||||||
| `Shift+J` | Cycle secondary subtitle track |
|
| `Shift+J` | Cycle secondary subtitle track |
|
||||||
| `Ctrl+Alt+P` | Open playlist browser for current directory + queue |
|
| `Ctrl+Alt+P` | Open playlist browser for current directory + queue |
|
||||||
@@ -61,21 +69,23 @@ These control playback and subtitle display. They require overlay window focus.
|
|||||||
|
|
||||||
These keybindings can be overridden or disabled via the `keybindings` config array. The playlist browser opens a split overlay modal with sibling video files on the left and the live mpv playlist on the right.
|
These keybindings can be overridden or disabled via the `keybindings` config array. The playlist browser opens a split overlay modal with sibling video files on the left and the live mpv playlist on the right.
|
||||||
|
|
||||||
|
On macOS managed playback, SubMiner disables mpv's menu-bar shortcuts so configured SubMiner shortcuts like `Cmd+Shift+O` reach the mpv plugin instead of opening native mpv menu actions.
|
||||||
|
|
||||||
Mouse-hover playback behavior is configured separately from shortcuts: `subtitleStyle.autoPauseVideoOnHover` defaults to `true` (pause on subtitle hover, resume on leave).
|
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 |
|
||||||
| ------------------ | -------------------------------------------------------- | ----------------------------------- |
|
| ------------------ | -------------------------------------------------------- | ----------------------------------------------- |
|
||||||
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
|
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
|
||||||
| `Ctrl/Cmd+Alt+A` | Open character dictionary AniList selector | `shortcuts.openCharacterDictionary` |
|
| `Ctrl/Cmd+D` | Open loaded character dictionary manager | `shortcuts.openCharacterDictionaryManager` |
|
||||||
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
||||||
| `Ctrl/Cmd+/` | Open session help modal | `shortcuts.openSessionHelp` |
|
| `Ctrl/Cmd+/` | Open session help modal | `shortcuts.openSessionHelp` |
|
||||||
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
||||||
| `Ctrl+Alt+C` | Open the manual YouTube subtitle picker | `keybindings` |
|
| `Ctrl+Alt+C` | Open the manual YouTube subtitle picker | `keybindings` |
|
||||||
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
||||||
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` |
|
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` |
|
||||||
| `` ` `` | Toggle stats overlay | `stats.toggleKey` |
|
| `` ` `` | Toggle stats overlay | `stats.toggleKey` |
|
||||||
|
|
||||||
The stats toggle is handled inside the focused visible overlay window. It is configurable through the top-level `stats.toggleKey` setting and defaults to `Backquote`.
|
The stats toggle is handled inside the focused visible overlay window. It is configurable through the top-level `stats.toggleKey` setting and defaults to `Backquote`.
|
||||||
|
|
||||||
@@ -96,19 +106,19 @@ Controller input only drives the overlay while keyboard-only mode is enabled. Th
|
|||||||
|
|
||||||
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 |
|
||||||
| `v` | Toggle primary subtitle bar visibility |
|
| `v` | Cycle primary subtitle bar mode (hidden → visible → hover) |
|
||||||
| `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 |
|
||||||
| `y-h` | Open session help |
|
| `y-h` | Open session help |
|
||||||
|
|
||||||
The bare `v` plugin binding intentionally overrides mpv's native primary subtitle visibility toggle so the SubMiner primary subtitle bar is hidden or restored instead.
|
The bare `v` plugin binding intentionally overrides mpv's native primary subtitle visibility toggle so it cycles the SubMiner primary subtitle bar (hidden → visible → hover) instead.
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
@@ -121,7 +131,7 @@ When the overlay has focus, press `y` then `d` to toggle DevTools (debugging hel
|
|||||||
|
|
||||||
## Customizing Shortcuts
|
## Customizing Shortcuts
|
||||||
|
|
||||||
All `shortcuts.*` keys accept [Electron accelerator strings](https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts), for example `"CommandOrControl+Alt+A"`. Use `null` to disable a shortcut.
|
All `shortcuts.*` keys accept [Electron accelerator strings](https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts), for example `"CommandOrControl+D"`. Use `null` to disable a shortcut.
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,23 +12,26 @@ N+1 highlighting identifies sentences where you know every word except one, maki
|
|||||||
|
|
||||||
**How it works:**
|
**How it works:**
|
||||||
|
|
||||||
1. SubMiner queries your Anki decks for existing `Expression` / `Word` field values.
|
1. SubMiner queries your configured Anki decks for expression/word fields such as `Expression` or `Word`.
|
||||||
2. The results are cached locally (`known-words-cache.json`) and refreshed on a configurable interval.
|
2. The results are cached locally (`known-words-cache.json`) and refreshed on a configurable interval.
|
||||||
3. When a subtitle line appears, each token is checked against the cache.
|
3. When a subtitle line appears, each token is checked against the cache.
|
||||||
4. If exactly one unknown word remains in the sentence, it is highlighted with `nPlusOneColor` (default: `#c6a0f6`).
|
4. If exactly one unknown word remains in the sentence, it is highlighted with `subtitleStyle.nPlusOneColor` (default: `#c6a0f6`).
|
||||||
5. Already-known tokens can optionally display in `knownWordColor` (default: `#a6da95`).
|
5. Already-known tokens can optionally display in `subtitleStyle.knownWordColor` (default: `#a6da95`).
|
||||||
|
|
||||||
**Key settings:**
|
**Key settings:**
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| --- | --- | --- |
|
| ----------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `ankiConnect.knownWords.highlightEnabled` | `false` | Enable known-word cache lookups used by N+1 highlighting |
|
| `ankiConnect.knownWords.highlightEnabled` | `false` | Enable known-word cache lookups used by N+1 highlighting |
|
||||||
| `ankiConnect.knownWords.refreshMinutes` | `1440` | Minutes between Anki cache refreshes |
|
| `ankiConnect.knownWords.refreshMinutes` | `1440` | Minutes between Anki cache refreshes |
|
||||||
| `ankiConnect.knownWords.decks` | `{}` | Deck→fields map for known-word cache queries (legacy fallback: `ankiConnect.deck`) |
|
| `ankiConnect.knownWords.decks` | `{}` | Deck→fields map for known-word cache queries |
|
||||||
| `ankiConnect.knownWords.matchMode` | `"headword"` | `"headword"` (dictionary form) or `"surface"` (raw text) |
|
| `ankiConnect.knownWords.matchMode` | `"headword"` | `"headword"` (dictionary form) or `"surface"` (raw text) |
|
||||||
| `ankiConnect.nPlusOne.minSentenceWords` | `3` | Minimum tokens in a sentence for N+1 to trigger |
|
| `ankiConnect.nPlusOne.enabled` | `false` | Enable N+1 target highlighting |
|
||||||
| `ankiConnect.nPlusOne.nPlusOne` | `#c6a0f6` | Color for the single unknown target word |
|
| `ankiConnect.nPlusOne.minSentenceWords` | `3` | Minimum tokens in a sentence for N+1 to trigger |
|
||||||
| `ankiConnect.knownWords.color` | `#a6da95` | Color for already-known tokens |
|
| `subtitleStyle.nPlusOneColor` | `#c6a0f6` | Color for the single unknown target word |
|
||||||
|
| `subtitleStyle.knownWordColor` | `#a6da95` | Color for already-known tokens |
|
||||||
|
|
||||||
|
Prefer expression/word fields for `ankiConnect.knownWords.decks`. Reading-only fields can mark unrelated homophones as known, so only include them when that tradeoff is intentional.
|
||||||
|
|
||||||
::: tip
|
::: tip
|
||||||
Set `refreshMinutes` to `1440` (24 hours) for daily sync if your Anki collection is large.
|
Set `refreshMinutes` to `1440` (24 hours) for daily sync if your Anki collection is large.
|
||||||
@@ -36,20 +39,22 @@ Set `refreshMinutes` to `1440` (24 hours) for daily sync if your Anki collection
|
|||||||
|
|
||||||
## Character-Name Highlighting
|
## Character-Name Highlighting
|
||||||
|
|
||||||
Character-name matches are built from the active merged SubMiner character dictionary, which auto-syncs character data from AniList for your recently-watched titles. Matching names are highlighted in subtitles and become available for hover-driven Yomitan character profiles — portraits, roles, voice actors, and biographical detail.
|
Character-name matches are built from the active merged SubMiner character dictionary, which auto-syncs character data from AniList for your recently-watched titles. When the current AniList media ID is known, SubMiner ignores loaded entries from other titles for subtitle name matching and inline portraits. Matching names are highlighted in subtitles and become available for hover-driven Yomitan character profiles — portraits, roles, voice actors, and biographical detail.
|
||||||
|
|
||||||
**How it works:**
|
**How it works:**
|
||||||
|
|
||||||
1. Subtitles are tokenized, then candidate name tokens are matched against the character dictionary via Yomitan's scanning pipeline.
|
1. Subtitles are tokenized, then candidate name tokens are matched against the character dictionary via Yomitan's scanning pipeline.
|
||||||
2. Matching tokens receive a dedicated style distinct from N+1 and frequency layers.
|
2. Matching tokens receive a dedicated style distinct from N+1 and frequency layers.
|
||||||
3. This layer can be independently toggled with `subtitleStyle.nameMatchEnabled`.
|
3. This layer can be independently toggled with `subtitleStyle.nameMatchEnabled`.
|
||||||
|
4. When `subtitleStyle.nameMatchImagesEnabled` is also enabled, SubMiner shows the cached AniList portrait beside matched names.
|
||||||
|
|
||||||
**Key settings:**
|
**Key settings:**
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| --- | --- | --- |
|
| -------------------------------------- | --------- | ------------------------------------------------ |
|
||||||
| `subtitleStyle.nameMatchEnabled` | `true` | Enable character-name token highlighting |
|
| `subtitleStyle.nameMatchEnabled` | `false` | Enable character-name token highlighting |
|
||||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Color used for character-name matches |
|
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small AniList portraits next to name tokens |
|
||||||
|
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Color used for character-name matches |
|
||||||
|
|
||||||
For full details on dictionary generation, name variant expansion, auto-sync lifecycle, and configuration, see the dedicated [Character Dictionary](/character-dictionary) page.
|
For full details on dictionary generation, name variant expansion, auto-sync lifecycle, and configuration, see the dedicated [Character Dictionary](/character-dictionary) page.
|
||||||
|
|
||||||
@@ -66,15 +71,17 @@ SubMiner looks up each token's `frequencyRank` from `term_meta_bank_*.json` file
|
|||||||
|
|
||||||
**Key settings:**
|
**Key settings:**
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| --- | --- | --- |
|
| ------------------------------------------------ | ------------ | ---------------------------------------------------------------- |
|
||||||
| `subtitleStyle.frequencyDictionary.enabled` | `false` | Enable frequency highlighting |
|
| `subtitleStyle.frequencyDictionary.enabled` | `false` | Enable frequency highlighting |
|
||||||
| `subtitleStyle.frequencyDictionary.topX` | `1000` | Max frequency rank to highlight |
|
| `subtitleStyle.frequencyDictionary.topX` | `1000` | Max frequency rank to highlight |
|
||||||
| `subtitleStyle.frequencyDictionary.mode` | `"single"` | `"single"` or `"banded"` |
|
| `subtitleStyle.frequencyDictionary.mode` | `"single"` | `"single"` or `"banded"` |
|
||||||
| `subtitleStyle.frequencyDictionary.matchMode` | `"headword"` | `"headword"` or `"surface"` |
|
| `subtitleStyle.frequencyDictionary.matchMode` | `"headword"` | `"headword"` or `"surface"` |
|
||||||
| `subtitleStyle.frequencyDictionary.singleColor` | — | Color for single mode |
|
| `subtitleStyle.frequencyDictionary.singleColor` | `#f5a97f` | Color for single mode |
|
||||||
| `subtitleStyle.frequencyDictionary.bandedColors` | — | Array of five hex colors for banded mode |
|
| `subtitleStyle.frequencyDictionary.bandedColors` | 5 colors[^1] | Array of five hex colors for banded mode |
|
||||||
| `subtitleStyle.frequencyDictionary.sourcePath` | — | Custom path to frequency dictionary root |
|
| `subtitleStyle.frequencyDictionary.sourcePath` | `""` | Custom path to frequency dictionary root (empty = auto-discover) |
|
||||||
|
|
||||||
|
[^1]: Default banded palette (most common → least common): `#ed8796`, `#f5a97f`, `#f9e2af`, `#8bd5ca`, `#8aadf4`.
|
||||||
|
|
||||||
When `sourcePath` is omitted, SubMiner searches default install/runtime locations for `frequency-dictionary` directories automatically.
|
When `sourcePath` is omitted, SubMiner searches default install/runtime locations for `frequency-dictionary` directories automatically.
|
||||||
|
|
||||||
@@ -96,22 +103,22 @@ SubMiner loads offline `term_meta_bank_*.json` files from `vendor/yomitan-jlpt-v
|
|||||||
|
|
||||||
**Default colors:**
|
**Default colors:**
|
||||||
|
|
||||||
| Level | Color | Preview |
|
| Level | Color | Preview |
|
||||||
| --- | --- | --- |
|
| ----- | --------- | ------- |
|
||||||
| N1 | `#ed8796` | Red |
|
| N1 | `#ed8796` | Red |
|
||||||
| N2 | `#f5a97f` | Peach |
|
| N2 | `#f5a97f` | Peach |
|
||||||
| N3 | `#f9e2af` | Yellow |
|
| N3 | `#f9e2af` | Yellow |
|
||||||
| N4 | `#a6e3a1` | Green |
|
| N4 | `#8bd5ca` | Teal |
|
||||||
| N5 | `#8aadf4` | Blue |
|
| N5 | `#8aadf4` | Blue |
|
||||||
|
|
||||||
All colors are customizable via the `subtitleStyle.jlptColors` object.
|
All colors are customizable via the `subtitleStyle.jlptColors` object.
|
||||||
|
|
||||||
**Key settings:**
|
**Key settings:**
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| --- | --- | --- |
|
| ---------------------------------- | --------- | ----------------------------- |
|
||||||
| `subtitleStyle.enableJlpt` | `false` | Enable JLPT underline styling |
|
| `subtitleStyle.enableJlpt` | `false` | Enable JLPT underline styling |
|
||||||
| `subtitleStyle.jlptColors.N1`–`N5` | see above | Per-level underline colors |
|
| `subtitleStyle.jlptColors.N1`–`N5` | see above | Per-level underline colors |
|
||||||
|
|
||||||
## Runtime Toggles
|
## Runtime Toggles
|
||||||
|
|
||||||
@@ -119,6 +126,7 @@ All annotation layers can be toggled at runtime via the mpv command menu without
|
|||||||
|
|
||||||
- `ankiConnect.knownWords.highlightEnabled` (`On` / `Off`)
|
- `ankiConnect.knownWords.highlightEnabled` (`On` / `Off`)
|
||||||
- `subtitleStyle.nameMatchEnabled` (`On` / `Off`)
|
- `subtitleStyle.nameMatchEnabled` (`On` / `Off`)
|
||||||
|
- `subtitleStyle.nameMatchImagesEnabled` (`On` / `Off`)
|
||||||
- `subtitleStyle.enableJlpt` (`On` / `Off`)
|
- `subtitleStyle.enableJlpt` (`On` / `Off`)
|
||||||
- `subtitleStyle.frequencyDictionary.enabled` (`On` / `Off`)
|
- `subtitleStyle.frequencyDictionary.enabled` (`On` / `Off`)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Enable and configure the sidebar under `subtitleSidebar` in your config file:
|
|||||||
"autoOpen": false,
|
"autoOpen": false,
|
||||||
"layout": "overlay",
|
"layout": "overlay",
|
||||||
"toggleKey": "Backslash",
|
"toggleKey": "Backslash",
|
||||||
"pauseVideoOnHover": false,
|
"pauseVideoOnHover": true,
|
||||||
"autoScroll": true,
|
"autoScroll": true,
|
||||||
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif",
|
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif",
|
||||||
"fontSize": 16
|
"fontSize": 16
|
||||||
@@ -47,18 +47,18 @@ Enable and configure the sidebar under `subtitleSidebar` in your config file:
|
|||||||
| `autoOpen` | boolean | `false` | Open the sidebar automatically on overlay startup |
|
| `autoOpen` | boolean | `false` | Open the sidebar automatically on overlay startup |
|
||||||
| `layout` | string | `"overlay"` | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space |
|
| `layout` | string | `"overlay"` | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space |
|
||||||
| `toggleKey` | string | `"Backslash"` | `KeyboardEvent.code` for the toggle shortcut |
|
| `toggleKey` | string | `"Backslash"` | `KeyboardEvent.code` for the toggle shortcut |
|
||||||
| `pauseVideoOnHover` | boolean | `false` | Pause playback while hovering the cue list |
|
| `pauseVideoOnHover` | boolean | `true` | Pause playback while hovering the cue list |
|
||||||
| `autoScroll` | boolean | `true` | Keep the active cue in view during playback |
|
| `autoScroll` | boolean | `true` | Keep the active cue in view during playback |
|
||||||
| `maxWidth` | number | `420` | Maximum sidebar width in CSS pixels |
|
| `maxWidth` | number | `420` | Maximum sidebar width in CSS pixels |
|
||||||
| `opacity` | number | `0.95` | Sidebar opacity between `0` and `1` |
|
| `opacity` | number | `0.95` | Sidebar opacity between `0` and `1` |
|
||||||
| `backgroundColor` | string | — | Sidebar shell background color |
|
| `backgroundColor` | string | `rgba(73, 77, 100, 0.9)` | Sidebar shell background color |
|
||||||
| `textColor` | string | — | Default cue text color |
|
| `textColor` | string | `#cad3f5` | Default cue text color |
|
||||||
| `fontFamily` | string | — | CSS `font-family` applied to cue text |
|
| `fontFamily` | string | `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP` | CSS `font-family` applied to cue text |
|
||||||
| `fontSize` | number | `16` | Base cue font size in CSS pixels |
|
| `fontSize` | number | `16` | Base cue font size in CSS pixels |
|
||||||
| `timestampColor` | string | — | Cue timestamp color |
|
| `timestampColor` | string | `#a5adcb` | Cue timestamp color |
|
||||||
| `activeLineColor` | string | — | Active cue text color |
|
| `activeLineColor` | string | `#f5bde6` | Active cue text color |
|
||||||
| `activeLineBackgroundColor` | string | — | Active cue background color |
|
| `activeLineBackgroundColor` | string | `rgba(138, 173, 244, 0.22)` | Active cue background color |
|
||||||
| `hoverLineBackgroundColor` | string | — | Hovered cue background color |
|
| `hoverLineBackgroundColor` | string | `rgba(54, 58, 79, 0.84)` | Hovered cue background color |
|
||||||
|
|
||||||
## Keyboard Shortcut
|
## Keyboard Shortcut
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Troubleshooting
|
# Troubleshooting
|
||||||
|
|
||||||
Common issues and how to resolve them.
|
Common issues and how to resolve them. Most problems fall into one of a few buckets — the overlay shows but subtitles don't (see [MPV Connection](#mpv-connection)), cards aren't being created or come out empty (see [AnkiConnect](#ankiconnect)), or word lookups don't appear (see [Yomitan](#yomitan)). If an error message popped up on screen, search this page for the exact text — most headings below are quoted error strings.
|
||||||
|
|
||||||
## MPV Connection
|
## MPV Connection
|
||||||
|
|
||||||
@@ -9,11 +9,13 @@ Common issues and how to resolve them.
|
|||||||
SubMiner connects to mpv via a Unix socket (or named pipe on Windows). If the socket does not exist or the path does not match, the overlay will appear but subtitles will never arrive.
|
SubMiner connects to mpv via a Unix socket (or named pipe on Windows). If the socket does not exist or the path does not match, the overlay will appear but subtitles will never arrive.
|
||||||
|
|
||||||
- Ensure mpv is running with `--input-ipc-server=/tmp/subminer-socket`.
|
- Ensure mpv is running with `--input-ipc-server=/tmp/subminer-socket`.
|
||||||
- If you use a custom socket path, set it in both your mpv config and SubMiner config (`mpvSocketPath`).
|
- If you use a custom socket path, set it in both your mpv config and SubMiner config (`mpv.socketPath`).
|
||||||
- The `subminer` wrapper script sets the socket automatically when it launches mpv. If you launch mpv yourself, the `--input-ipc-server` flag is required.
|
- The `subminer` wrapper script sets the socket automatically when it launches mpv. If you launch mpv yourself, the `--input-ipc-server` flag is required.
|
||||||
|
|
||||||
SubMiner retries the connection automatically with increasing delays (200 ms, 500 ms, 1 s, 2 s on first connect; 1 s, 2 s, 5 s, 10 s on reconnect). If mpv exits and restarts, the overlay reconnects without needing a restart.
|
SubMiner retries the connection automatically with increasing delays (200 ms, 500 ms, 1 s, 2 s on first connect; 1 s, 2 s, 5 s, 10 s on reconnect). If mpv exits and restarts, the overlay reconnects without needing a restart.
|
||||||
|
|
||||||
|
If the overlay never appears at all, see [Playback Startup Flow](./architecture#playback-startup-flow) for how a managed launch starts mpv and brings up the overlay.
|
||||||
|
|
||||||
## Logging and App Mode
|
## Logging and App Mode
|
||||||
|
|
||||||
- Default log output is `info`.
|
- Default log output is `info`.
|
||||||
@@ -100,6 +102,30 @@ SubMiner retries the connection automatically with increasing delays (200 ms, 50
|
|||||||
|
|
||||||
Logged when a malformed JSON line arrives from the mpv socket. Usually harmless — SubMiner skips the bad line and continues. If it happens constantly, check that nothing else is writing to the same socket path.
|
Logged when a malformed JSON line arrives from the mpv socket. Usually harmless — SubMiner skips the bad line and continues. If it happens constantly, check that nothing else is writing to the same socket path.
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
**"Update check failed"**
|
||||||
|
|
||||||
|
Manual update checks show this when GitHub Releases or updater metadata cannot be reached. Check your network connection, then try again from the tray menu or:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer -u
|
||||||
|
```
|
||||||
|
|
||||||
|
Automatic checks log failures quietly so playback is not interrupted.
|
||||||
|
|
||||||
|
**"SubMiner is up to date" but a prerelease exists**
|
||||||
|
|
||||||
|
SubMiner uses the configured release channel for update checks. Set `updates.channel` to `"prerelease"` in `config.jsonc` when you want update checks to include beta and RC releases.
|
||||||
|
|
||||||
|
**Launcher update shows a sudo command**
|
||||||
|
|
||||||
|
The detected launcher is installed in a protected path such as `/usr/local/bin/subminer` or `/usr/bin/subminer`. SubMiner does not elevate itself. Run the command shown in the popup to replace the launcher after checksum verification.
|
||||||
|
|
||||||
|
**OSD update notification did not appear**
|
||||||
|
|
||||||
|
`updates.notificationType: "osd"` uses the existing mpv/overlay notification path. If mpv is disconnected, SubMiner logs the update and does not force-start the overlay. Use `"system"` or `"both"` if you want OS notifications outside playback.
|
||||||
|
|
||||||
## AnkiConnect
|
## AnkiConnect
|
||||||
|
|
||||||
**"AnkiConnect: unable to connect"**
|
**"AnkiConnect: unable to connect"**
|
||||||
@@ -181,7 +207,7 @@ If you installed from the AppImage and see this error, the package may be incomp
|
|||||||
**Yomitan lookup popup does not appear when hovering words or triggering lookup**
|
**Yomitan lookup popup does not appear when hovering words or triggering lookup**
|
||||||
|
|
||||||
- Verify Yomitan loaded successfully — check the terminal output for "Loaded Yomitan extension".
|
- Verify Yomitan loaded successfully — check the terminal output for "Loaded Yomitan extension".
|
||||||
- Yomitan requires dictionaries to be installed. Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --settings`) and confirm at least one dictionary is imported.
|
- Yomitan requires dictionaries to be installed. Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --yomitan`) and confirm at least one dictionary is imported.
|
||||||
- If `yomitan.externalProfilePath` is set, import/check dictionaries in the external app/profile instead. SubMiner treats that profile as read-only and does not open its own Yomitan settings window.
|
- If `yomitan.externalProfilePath` is set, import/check dictionaries in the external app/profile instead. SubMiner treats that profile as read-only and does not open its own Yomitan settings window.
|
||||||
- If the overlay shows subtitles but hover lookup never resolves on tokens, the tokenizer may have failed. See the MeCab section below.
|
- If the overlay shows subtitles but hover lookup never resolves on tokens, the tokenizer may have failed. See the MeCab section below.
|
||||||
|
|
||||||
@@ -272,7 +298,7 @@ Install ffsubsync or configure the path:
|
|||||||
|
|
||||||
**"Subtitle synchronization failed"**
|
**"Subtitle synchronization failed"**
|
||||||
|
|
||||||
SubMiner tries alass first, then falls back to ffsubsync. If both fail:
|
If subtitle sync fails:
|
||||||
|
|
||||||
- Ensure the reference subtitle track exists in the video (alass requires a source track).
|
- Ensure the reference subtitle track exists in the video (alass requires a source track).
|
||||||
- Check that `ffmpeg` is available (used to extract the internal subtitle track).
|
- Check that `ffmpeg` is available (used to extract the internal subtitle track).
|
||||||
@@ -291,6 +317,7 @@ The Jimaku API has rate limits. If you see 429 errors, wait for the retry durati
|
|||||||
|
|
||||||
- **Wayland (Hyprland/Sway only)**: Native Wayland support is limited to Hyprland and Sway. Window tracking uses compositor-specific commands (`hyprctl` / `swaymsg`). If these are not on `PATH`, tracking will fail silently. Other Wayland compositors are not supported — both mpv and SubMiner must run under X11 or Xwayland instead.
|
- **Wayland (Hyprland/Sway only)**: Native Wayland support is limited to Hyprland and Sway. Window tracking uses compositor-specific commands (`hyprctl` / `swaymsg`). If these are not on `PATH`, tracking will fail silently. Other Wayland compositors are not supported — both mpv and SubMiner must run under X11 or Xwayland instead.
|
||||||
- **X11 / Xwayland**: Requires `xdotool` and `xwininfo`. If missing, the overlay cannot track the mpv window position. This is the required backend for any Wayland compositor other than Hyprland or Sway — both mpv and SubMiner must be running under X11/Xwayland for window tracking to work.
|
- **X11 / Xwayland**: Requires `xdotool` and `xwininfo`. If missing, the overlay cannot track the mpv window position. This is the required backend for any Wayland compositor other than Hyprland or Sway — both mpv and SubMiner must be running under X11/Xwayland for window tracking to work.
|
||||||
|
- **Tray icon missing**: SubMiner creates an Electron tray icon in `--background` mode, but Linux trays require a StatusNotifier/AppIndicator host. Hyprland does not provide one by itself; enable a tray in Waybar, Hyprpanel, or another panel. If Electron cannot register the tray, SubMiner logs a warning that mentions the missing tray host.
|
||||||
- **Mouse passthrough**: On Linux, Electron's mouse passthrough is unreliable. SubMiner keeps pointer events enabled, meaning you may need to toggle the overlay off to interact with mpv controls underneath.
|
- **Mouse passthrough**: On Linux, Electron's mouse passthrough is unreliable. SubMiner keeps pointer events enabled, meaning you may need to toggle the overlay off to interact with mpv controls underneath.
|
||||||
|
|
||||||
### Hyprland
|
### Hyprland
|
||||||
|
|||||||
+56
-39
@@ -1,11 +1,23 @@
|
|||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Play a video with SubMiner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer video.mkv
|
||||||
|
```
|
||||||
|
|
||||||
|
On **Windows**, use the **SubMiner mpv** shortcut created during first-run setup — double-click it, or drag a video file onto it.
|
||||||
|
|
||||||
|
That's the simplest way to get started. The `subminer` launcher handles mpv, the IPC socket, and the overlay automatically.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> SubMiner requires the bundled Yomitan instance to have at least one dictionary imported for lookups to work.
|
> SubMiner requires the bundled Yomitan instance to have at least one dictionary imported for lookups to work.
|
||||||
> See [Yomitan setup](#yomitan-setup) for details.
|
> See [Yomitan setup](#yomitan-setup) for details.
|
||||||
|
|
||||||
::: tip Just finished first-run setup?
|
::: tip Anki card enrichment
|
||||||
If you want Anki card enrichment (sentence, audio, screenshot), the only config you need is `ankiConnect` with your deck name and field names. Here is a minimal working example:
|
If you want sentence, audio, and screenshot fields on your Anki cards, add this to your config:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
@@ -26,26 +38,23 @@ Field names must match your Anki note type exactly (case-sensitive). See [Anki I
|
|||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
|
When you launch SubMiner, it wires up mpv and the overlay for you:
|
||||||
|
|
||||||
1. SubMiner starts the overlay app in the background
|
1. SubMiner starts the overlay app in the background
|
||||||
2. MPV runs with an IPC socket at `/tmp/subminer-socket`
|
2. mpv runs with an **IPC socket** at `/tmp/subminer-socket` — a small local channel two programs use to talk to each other, so the overlay can ask mpv what subtitle is on screen right now
|
||||||
3. The overlay connects and subscribes to subtitle changes
|
3. The overlay connects and subscribes to subtitle changes
|
||||||
4. Subtitles are tokenized with Yomitan's internal parser
|
|
||||||
5. Words are displayed as interactive spans in the overlay
|
|
||||||
6. Hover a word, then trigger Yomitan lookup with your configured lookup key/modifier to open the Yomitan popup
|
|
||||||
7. Optional [subtitle annotations](/subtitle-annotations) (N+1, character-name, frequency, JLPT) highlight useful cues in real time
|
|
||||||
|
|
||||||
There are several ways to use SubMiner:
|
From there, subtitles render as interactive, hoverable word spans and you mine cards directly from the overlay. For the overlay anatomy and the full mining loop — word lookup, card creation, annotations — see [Mining Workflow](/mining-workflow).
|
||||||
|
|
||||||
> [!TIP]
|
### Ways to Launch
|
||||||
> **New users: start with the `subminer` wrapper script** (or the **SubMiner mpv** shortcut on Windows). It handles mpv launch, IPC socket setup, and overlay lifecycle automatically so you don't need to configure anything in `mpv.conf`.
|
|
||||||
|
|
||||||
| Approach | Use when | How |
|
| Approach | Use when | How |
|
||||||
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||||
| **`subminer` script** | You want SubMiner to handle everything — launch mpv, set up the socket, start the overlay. **The simplest path and recommended starting point.** | `subminer video.mkv` |
|
| **`subminer` launcher** | You want SubMiner to handle everything — launch mpv, set up the socket, start the overlay. **Recommended for most users.** | `subminer video.mkv` |
|
||||||
| **SubMiner mpv shortcut** (Windows) | The recommended Windows entry point. Created during first-run setup, launches mpv with SubMiner's defaults directly. | Double-click, drag a file onto it, or run `SubMiner.exe --launch-mpv` |
|
| **SubMiner mpv shortcut** (Windows) | The recommended Windows entry point. Created during first-run setup, launches mpv with SubMiner's defaults. | Double-click, drag a file onto it, or run `SubMiner.exe --launch-mpv` |
|
||||||
| **MPV plugin** (all platforms) | You launch mpv yourself or from another tool (file manager, Jellyfin, etc.). Requires `--input-ipc-server=/tmp/subminer-socket` in your mpv config. | Use `y` chord keybindings inside mpv |
|
| **mpv plugin** (all platforms) | Bundled and injected at runtime. Provides `y` chord keybindings for controlling the overlay from within mpv. No manual install needed. | Automatic when using the launcher or shortcut |
|
||||||
|
|
||||||
You can use both — the plugin provides in-player controls, while the `subminer` script (or the Windows shortcut) is convenient for direct playback. The `subminer` script runs directly via shebang on Linux and macOS (no `bun run` needed); on Windows it must be invoked with `bun run subminer` since the shebang is not supported.
|
The mpv plugin is always available — it's bundled with SubMiner and injected at runtime. If you launch mpv yourself (without the launcher), pass `--input-ipc-server=/tmp/subminer-socket` in your mpv config for the overlay to connect.
|
||||||
|
|
||||||
## Live Config Reload
|
## Live Config Reload
|
||||||
|
|
||||||
@@ -72,12 +81,14 @@ 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 (overlay auto-starts)
|
||||||
subminer --start video.mkv # Optional explicit overlay start (use when plugin auto_start=no)
|
subminer --start video.mkv # Explicit overlay start (use when auto_start=no in config)
|
||||||
subminer -S video.mkv # Same as above via --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 --setup # Open first-run setup popup
|
subminer --setup # Open first-run setup popup
|
||||||
|
subminer --version # Print installed SubMiner version
|
||||||
|
subminer -v # Same as above
|
||||||
subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging
|
subminer --log-level debug video.mkv # Enable verbose logs for launch/debugging
|
||||||
subminer --log-level warn video.mkv # Set logging level explicitly
|
subminer --log-level warn video.mkv # Set logging level explicitly
|
||||||
subminer --args '--fs=opengl-hq --ytdl-format=bestvideo*+bestaudio/best' video.mkv # Pass extra mpv args
|
subminer --args '--fs=opengl-hq --ytdl-format=bestvideo*+bestaudio/best' video.mkv # Pass extra mpv args
|
||||||
@@ -94,6 +105,7 @@ subminer jellyfin -p # Interactive Jellyfin library/item picker + p
|
|||||||
subminer jellyfin -d # Jellyfin cast-discovery mode (background tray app)
|
subminer jellyfin -d # Jellyfin cast-discovery mode (background tray app)
|
||||||
subminer app --stop # Stop background app (including Jellyfin cast broadcast)
|
subminer app --stop # Stop background app (including Jellyfin cast broadcast)
|
||||||
subminer doctor # Dependency + config + socket diagnostics
|
subminer doctor # Dependency + config + socket diagnostics
|
||||||
|
subminer logs -e # Export a sanitized log ZIP and print its path
|
||||||
subminer config path # Print active config path
|
subminer config path # Print active config path
|
||||||
subminer config show # Print active config contents
|
subminer config show # Print active config contents
|
||||||
subminer mpv socket # Print active mpv socket path
|
subminer mpv socket # Print active mpv socket path
|
||||||
@@ -120,21 +132,23 @@ SubMiner.AppImage --toggle-primary-subtitle-bar # Toggle primary subtitle
|
|||||||
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
|
||||||
SubMiner.AppImage --settings # Open Yomitan settings
|
SubMiner.AppImage --yomitan # Open Yomitan settings
|
||||||
|
SubMiner.AppImage --settings # Open SubMiner settings window
|
||||||
SubMiner.AppImage --jellyfin # Open Jellyfin setup window
|
SubMiner.AppImage --jellyfin # Open Jellyfin setup window
|
||||||
SubMiner.AppImage --jellyfin-login --jellyfin-server http://127.0.0.1:8096 --jellyfin-username me --jellyfin-password 'secret'
|
SubMiner.AppImage --jellyfin-login --jellyfin-server http://127.0.0.1:8096 --jellyfin-username me --jellyfin-password 'secret'
|
||||||
SubMiner.AppImage --jellyfin-logout # Clear stored Jellyfin token/session data
|
SubMiner.AppImage --jellyfin-logout # Clear stored Jellyfin token/session data
|
||||||
SubMiner.AppImage --jellyfin-libraries
|
SubMiner.AppImage --jellyfin-libraries
|
||||||
SubMiner.AppImage --jellyfin-items --jellyfin-library-id LIBRARY_ID --jellyfin-search anime --jellyfin-limit 20
|
SubMiner.AppImage --jellyfin-items --jellyfin-library-id LIBRARY_ID --jellyfin-search anime --jellyfin-limit 20
|
||||||
SubMiner.AppImage --jellyfin-play --jellyfin-item-id ITEM_ID --jellyfin-audio-stream-index 1 --jellyfin-subtitle-stream-index 2 # Requires connected mpv IPC (--start or plugin workflow)
|
SubMiner.AppImage --jellyfin-play --jellyfin-item-id ITEM_ID --jellyfin-audio-stream-index 1 --jellyfin-subtitle-stream-index 2 # Requires connected mpv IPC (--start)
|
||||||
SubMiner.AppImage --jellyfin-remote-announce # Force cast-target capability announce + visibility check
|
SubMiner.AppImage --jellyfin-remote-announce # Force cast-target capability announce + visibility check
|
||||||
SubMiner.AppImage --dictionary # Generate character dictionary ZIP for current anime
|
SubMiner.AppImage --dictionary # Generate character dictionary ZIP for current anime
|
||||||
SubMiner.AppImage --dictionary-candidates # List AniList candidates for current character dictionary series
|
SubMiner.AppImage --dictionary-candidates # List AniList candidates for current character dictionary series
|
||||||
SubMiner.AppImage --dictionary-select --dictionary-anilist-id 21355 # Pin correct AniList media for series
|
SubMiner.AppImage --dictionary-select --dictionary-anilist-id 21355 # Pin correct AniList media for series
|
||||||
SubMiner.AppImage --open-character-dictionary # Open in-app AniList selector
|
|
||||||
SubMiner.AppImage --help # Show all options
|
SubMiner.AppImage --help # Show all options
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The tray menu includes `Export Logs`, which creates the same sanitized log ZIP as `subminer logs -e` and shows the archive path when complete.
|
||||||
|
|
||||||
Once Jellyfin is configured, the tray menu includes `Jellyfin Discovery` for starting or stopping cast discovery in the current app session without changing config.
|
Once Jellyfin is configured, the tray menu includes `Jellyfin Discovery` for starting or stopping cast discovery in the current app session without changing config.
|
||||||
|
|
||||||
### Logging and App Mode
|
### Logging and App Mode
|
||||||
@@ -144,6 +158,7 @@ Once Jellyfin is configured, the tray menu includes `Jellyfin Discovery` for sta
|
|||||||
- `--background` defaults to quieter logging (`warn`) unless `--log-level` is set.
|
- `--background` defaults to quieter logging (`warn`) unless `--log-level` is set.
|
||||||
- `--background` launched from a terminal detaches and returns the prompt; stop it with tray Quit or `SubMiner.AppImage --stop` (`SubMiner.exe --stop` on Windows).
|
- `--background` launched from a terminal detaches and returns the prompt; stop it with tray Quit or `SubMiner.AppImage --stop` (`SubMiner.exe --stop` on Windows).
|
||||||
- Linux desktop launcher starts SubMiner with `--background` by default (via electron-builder `linux.executableArgs`).
|
- Linux desktop launcher starts SubMiner with `--background` by default (via electron-builder `linux.executableArgs`).
|
||||||
|
- On Hyprland and other Wayland compositors, the tray icon appears only when your panel provides a StatusNotifier/AppIndicator tray host.
|
||||||
- On Linux, the app now defaults `safeStorage` to `gnome-libsecret` for encrypted token persistence.
|
- On Linux, the app now defaults `safeStorage` to `gnome-libsecret` for encrypted token persistence.
|
||||||
Launcher pass-through commands also support `--password-store=<backend>` and forward it to the app when present.
|
Launcher pass-through commands also support `--password-store=<backend>` and forward it to the app when present.
|
||||||
Override with e.g. `--password-store=basic_text`.
|
Override with e.g. `--password-store=basic_text`.
|
||||||
@@ -151,7 +166,7 @@ Once Jellyfin is configured, the tray menu includes `Jellyfin Discovery` for sta
|
|||||||
|
|
||||||
### Windows mpv Shortcut
|
### Windows mpv Shortcut
|
||||||
|
|
||||||
First-run setup creates the config file, then requires Yomitan dictionaries before it can finish. The global mpv plugin install is optional because SubMiner-managed mpv launches inject the bundled runtime plugin.
|
First-run setup creates the config file, then requires Yomitan dictionaries before it can finish.
|
||||||
|
|
||||||
If you enabled the optional Windows shortcut during install, SubMiner creates a `SubMiner mpv` shortcut in the Start menu and/or on the desktop. On Windows, that shortcut is the recommended way to launch local files with SubMiner because it starts `mpv.exe` with the right defaults directly.
|
If you enabled the optional Windows shortcut during install, SubMiner creates a `SubMiner mpv` shortcut in the Start menu and/or on the desktop. On Windows, that shortcut is the recommended way to launch local files with SubMiner because it starts `mpv.exe` with the right defaults directly.
|
||||||
After setup completes, the shortcut is the normal Windows playback entry point.
|
After setup completes, the shortcut is the normal Windows playback entry point.
|
||||||
@@ -173,11 +188,13 @@ This flow requires `mpv.exe` to be discoverable. Leave `mpv.executablePath` blan
|
|||||||
|
|
||||||
- `subminer jellyfin` / `subminer jf`: Jellyfin-focused workflow aliases.
|
- `subminer jellyfin` / `subminer jf`: Jellyfin-focused workflow aliases.
|
||||||
- `subminer doctor`: health checks for core dependencies and runtime paths.
|
- `subminer doctor`: health checks for core dependencies and runtime paths.
|
||||||
- `subminer config`: config helpers (`path`, `show`).
|
- `subminer settings`: open the SubMiner settings window (also `subminer --settings`).
|
||||||
|
- `subminer logs -e`: export a sanitized ZIP of today's logs, or the most recent logs when no current-day log exists.
|
||||||
|
- `subminer config`: config file helpers (`path`, `show`).
|
||||||
- `subminer mpv`: mpv helpers (`status`, `socket`, `idle`).
|
- `subminer mpv`: mpv helpers (`status`, `socket`, `idle`).
|
||||||
- `subminer dictionary <path>`: generates a Yomitan-importable character dictionary ZIP from a file/directory target.
|
- `subminer dictionary <path>`: generates a Yomitan-importable character dictionary ZIP from a file/directory target.
|
||||||
- Use `subminer dictionary --candidates <path>` and `subminer dictionary --select <id> <path>` to correct AniList character-dictionary matches for a whole series.
|
- Use `subminer dictionary --candidates <path>` and `subminer dictionary --select <id> <path>` to correct AniList character-dictionary matches for a whole series.
|
||||||
- `subminer texthooker`: texthooker-only shortcut (same behavior as `--texthooker`).
|
- `subminer texthooker`: texthooker-only shortcut (same behavior as `--texthooker`). A _texthooker_ is a web page that displays the current subtitle line as selectable text, so browser-based dictionary extensions and other tools can read along with playback.
|
||||||
- `subminer app` / `subminer bin`: direct passthrough to the SubMiner binary/AppImage.
|
- `subminer app` / `subminer bin`: direct passthrough to the SubMiner binary/AppImage.
|
||||||
- Subcommand help pages are available (for example `subminer jellyfin -h`).
|
- Subcommand help pages are available (for example `subminer jellyfin -h`).
|
||||||
|
|
||||||
@@ -195,19 +212,18 @@ SubMiner.AppImage --setup
|
|||||||
Setup flow:
|
Setup flow:
|
||||||
|
|
||||||
- config file: create the default config directory and prefer `config.jsonc`
|
- config file: create the default config directory and prefer `config.jsonc`
|
||||||
- plugin compatibility: optionally install the legacy global mpv plugin; managed launches use the bundled runtime plugin without it
|
- legacy plugin cleanup: remove detected older global SubMiner mpv plugin files if present (the bundled plugin is injected at runtime automatically)
|
||||||
- legacy plugin cleanup: remove detected global SubMiner mpv plugin files from mpv script directories via the OS trash when you do not want regular mpv to load SubMiner
|
|
||||||
- Yomitan shortcut: open bundled Yomitan settings directly from the setup window
|
- Yomitan shortcut: open bundled Yomitan settings directly from the setup window
|
||||||
- dictionary check: ensure at least one bundled Yomitan dictionary is available, unless an external Yomitan profile is configured
|
- dictionary check: ensure at least one bundled Yomitan dictionary is available, unless an external Yomitan profile is configured
|
||||||
- Windows: optionally create or remove `SubMiner mpv` Start Menu/Desktop shortcuts (`SubMiner.exe --launch-mpv`)
|
- Windows: optionally create or remove `SubMiner mpv` Start Menu/Desktop shortcuts (`SubMiner.exe --launch-mpv`)
|
||||||
- Windows: optionally set `mpv.executablePath` if `mpv.exe` is not on `PATH`
|
- Windows: optionally set `mpv.executablePath` if `mpv.exe` is not on `PATH`
|
||||||
- refresh: re-check plugin + dictionary state without restarting
|
- refresh: re-check dictionary state without restarting
|
||||||
- `Finish setup` stays disabled until the config and dictionary gates are satisfied
|
- `Finish setup` stays disabled until the config and dictionary gates are satisfied
|
||||||
- finish action writes setup completion state and suppresses future auto-open prompts
|
- finish action writes setup completion state and suppresses future auto-open prompts
|
||||||
|
|
||||||
AniList character dictionary auto-sync (optional):
|
AniList character dictionary auto-sync (optional):
|
||||||
|
|
||||||
- Enable with `anilist.characterDictionary.enabled=true` in config.
|
- Enable with `subtitleStyle.nameMatchEnabled=true` in config or **Name Match Enabled** in Settings.
|
||||||
- SubMiner syncs the currently watched AniList media into a per-media snapshot, then rebuilds one merged `SubMiner Character Dictionary` from the most recently used snapshots.
|
- SubMiner syncs the currently watched AniList media into a per-media snapshot, then rebuilds one merged `SubMiner Character Dictionary` from the most recently used snapshots.
|
||||||
- Rotation limit defaults to 3 recent media snapshots in that merged dictionary (`maxLoaded`).
|
- Rotation limit defaults to 3 recent media snapshots in that merged dictionary (`maxLoaded`).
|
||||||
|
|
||||||
@@ -229,7 +245,7 @@ Top-level launcher flags like `--jellyfin-*` are intentionally rejected.
|
|||||||
|
|
||||||
You can append additional MPV arguments with launcher `-a/--args`, for example `--args "--ao=alsa --volume=80"`.
|
You can append additional MPV arguments with launcher `-a/--args`, for example `--args "--ao=alsa --volume=80"`.
|
||||||
|
|
||||||
You can define a matching profile in `~/.config/mpv/mpv.conf` for consistency when launching `mpv` manually or from other tools. The Windows `SubMiner.exe --launch-mpv` shortcut path uses equivalent args directly, but skips the extra current-directory subtitle scan to avoid duplicate sidecar detection when you drag a video onto the shortcut; the optional profile remains useful for manual mpv launches and the `subminer` wrapper defaults to `--profile=subminer` (or override with `subminer -p <profile> ...`):
|
You can define a matching profile in `~/.config/mpv/mpv.conf` for consistency when launching `mpv` manually or from other tools. The Windows `SubMiner.exe --launch-mpv` shortcut path uses equivalent args directly, but skips the extra current-directory subtitle scan to avoid duplicate sidecar detection when you drag a video onto the shortcut; the optional profile remains useful for manual mpv launches. The `subminer` wrapper passes no mpv profile by default; set one with `subminer -p <profile> ...` or with `mpv.profile` in your config (for example `"profile": "subminer"` to use the `[subminer]` profile below):
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[subminer]
|
[subminer]
|
||||||
@@ -254,7 +270,7 @@ secondary-sub-visibility=no
|
|||||||
|
|
||||||
SubMiner includes a bundled Yomitan extension for overlay word lookup. This bundled extension is separate from any Yomitan browser extension you may have installed.
|
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.
|
For SubMiner overlay lookups to work, open Yomitan settings (`subminer app --yomitan` or `SubMiner.AppImage --yomitan`) 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.
|
If you also use Yomitan in a browser, configure that browser profile separately; it does not inherit dictionaries or settings from the bundled instance.
|
||||||
|
|
||||||
@@ -271,7 +287,7 @@ Notes:
|
|||||||
- For YouTube URLs, `subminer` probes available YouTube subtitle tracks, reuses existing authoritative tracks when available, and downloads only missing sides.
|
- For YouTube URLs, `subminer` probes available YouTube subtitle tracks, reuses existing authoritative tracks when available, and downloads only missing sides.
|
||||||
- Native mpv secondary subtitle rendering stays hidden so the overlay remains the visible secondary subtitle surface.
|
- Native mpv secondary subtitle rendering stays hidden so the overlay remains the visible secondary subtitle surface.
|
||||||
- Primary subtitle target languages come from `youtube.primarySubLanguages` (defaults to `["ja","jpn"]`).
|
- Primary subtitle target languages come from `youtube.primarySubLanguages` (defaults to `["ja","jpn"]`).
|
||||||
- Secondary target languages come from `secondarySub.secondarySubLanguages` (defaults to English if unset).
|
- Secondary target languages come from `secondarySub.secondarySubLanguages` (empty by default; when empty, no language-based secondary track is auto-selected, though mpv's `--slang` list above still prefers English variants).
|
||||||
- Configure defaults in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`) under `youtube` and `secondarySub`.
|
- Configure defaults in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`) under `youtube` and `secondarySub`.
|
||||||
|
|
||||||
For local video files, SubMiner uses the same config-driven language priorities to auto-select the primary and secondary subtitle tracks from internal and external subtitle sources.
|
For local video files, SubMiner uses the same config-driven language priorities to auto-select the primary and secondary subtitle tracks from internal and external subtitle sources.
|
||||||
@@ -283,13 +299,14 @@ SubMiner supports gamepad/controller input for couch-friendly usage via the Chro
|
|||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
1. Connect a controller before or after launching SubMiner.
|
1. Connect a controller before or after launching SubMiner.
|
||||||
2. Enable keyboard-only mode — press `Y` on the controller (default binding) or use the overlay keybinding.
|
2. Set `controller.enabled` to `true` in your config.
|
||||||
3. Press `Alt+C` in the overlay by default to pick the controller you want to save and remap any action inline.
|
3. Press `Alt+C` in the overlay by default to pick the controller you want to save and remap any action inline.
|
||||||
4. Click `Learn` on the overlay action you want, then press the matching button, trigger, or stick direction on the controller.
|
4. Enable keyboard-only mode — press `Y` on the controller (default binding) or use the overlay keybinding.
|
||||||
5. Use the left stick to navigate subtitle tokens and scroll the popup; use the right stick vertically for popup page jumps.
|
5. Click the binding badge, edit pencil, or `Learn` on the overlay action you want, then press the matching button, trigger, or stick direction on the controller.
|
||||||
6. Press `A` to look up the selected word, `X` to mine a card, `B` to close the popup.
|
6. Use the left stick to navigate subtitle tokens and scroll the popup; use the right stick vertically for popup page jumps.
|
||||||
|
7. Press `A` to look up the selected word, `X` to mine a card, `B` to close the popup.
|
||||||
|
|
||||||
By default SubMiner uses the first connected controller. `Alt+C` opens the controller config modal, where you can save the preferred controller and remap bindings inline, and `Alt+Shift+C` opens the live debug modal with raw axes/button values for non-standard pads. Both shortcuts can be changed through `shortcuts.openControllerSelect` and `shortcuts.openControllerDebug`.
|
By default SubMiner uses the first connected controller after controller support is enabled. `Alt+C` opens the controller config modal, where you can save the preferred controller and remap bindings inline per controller. The reset button beside each edit pencil restores that binding to its built-in default for the selected controller. `Alt+Shift+C` opens the live debug modal with raw axes/button values for non-standard pads. Both modals stay closed while `controller.enabled` is false, and both shortcuts can be changed through `shortcuts.openControllerSelect` and `shortcuts.openControllerDebug`.
|
||||||
|
|
||||||
### Default Button Mapping
|
### Default Button Mapping
|
||||||
|
|
||||||
@@ -316,7 +333,7 @@ By default SubMiner uses the first connected controller. `Alt+C` opens the contr
|
|||||||
|
|
||||||
Learn mode ignores already-held inputs and waits for the next fresh button press or axis direction, which avoids accidental captures when you open the modal mid-input.
|
Learn mode ignores already-held inputs and waits for the next fresh button press or axis direction, which avoids accidental captures when you open the modal mid-input.
|
||||||
|
|
||||||
All button and axis mappings are configurable under the `controller` config block. See [Configuration — Controller Support](/configuration#controller-support) for the full options.
|
All button and axis mappings are configurable under the `controller` config block. Learned remaps are saved under `controller.profiles` for the selected controller id. See [Configuration — Controller Support](/configuration#controller-support) for the full options.
|
||||||
|
|
||||||
## Keybindings
|
## Keybindings
|
||||||
|
|
||||||
@@ -333,9 +350,9 @@ See [Keyboard Shortcuts](/shortcuts) for the full reference, including mining sh
|
|||||||
|
|
||||||
Useful overlay-local default keybinding: `Ctrl+Alt+P` opens the playlist browser for the current video's parent directory and the live mpv queue so you can append, reorder, remove, or jump between episodes without leaving playback.
|
Useful overlay-local default keybinding: `Ctrl+Alt+P` opens the playlist browser for the current video's parent directory and the live mpv queue so you can append, reorder, remove, or jump between episodes without leaving playback.
|
||||||
|
|
||||||
Press `V` to hide or restore the primary SubMiner subtitle bar. The mpv plugin also binds bare `v` to the same action, overriding mpv's native primary subtitle visibility toggle.
|
Press `V` to hide or restore the primary SubMiner subtitle bar. The bundled mpv plugin also binds bare `v` to the same action (injected at runtime).
|
||||||
|
|
||||||
`Ctrl/Cmd+/` opens the session help modal with the current overlay and mpv keybindings. If you use the mpv plugin, the same help view is also available through the `y-h` chord.
|
`Ctrl/Cmd+/` opens the session help modal with the current overlay and mpv keybindings. The same help view is also available through the `y-h` chord in mpv.
|
||||||
|
|
||||||
Hovering over subtitle text pauses mpv by default; leaving resumes it. Yomitan popups also pause playback by default. Set `subtitleStyle.autoPauseVideoOnHover: false` or `subtitleStyle.autoPauseVideoOnYomitanPopup: false` to disable either behavior.
|
Hovering over subtitle text pauses mpv by default; leaving resumes it. Yomitan popups also pause playback by default. Set `subtitleStyle.autoPauseVideoOnHover: false` or `subtitleStyle.autoPauseVideoOnYomitanPopup: false` to disable either behavior.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# WebSocket / Texthooker API & Integration
|
# WebSocket / Texthooker API & Integration
|
||||||
|
|
||||||
|
**Who this page is for:** developers and tinkerers who want to consume SubMiner's live subtitle stream from their own tools — a browser tab, an automation script, or another mpv plugin. If you just want subtitles in a browser tab for Yomitan, skip to [Texthooker Integration Guide](#texthooker-integration-guide); the rest is reference for building custom clients.
|
||||||
|
|
||||||
|
A *texthooker* is a page/tool that receives the text currently on screen so a dictionary extension (like Yomitan) can look words up. SubMiner ships its own texthooker UI and also broadcasts subtitle text over local WebSockets that any client can connect to.
|
||||||
|
|
||||||
SubMiner exposes a small set of local integration surfaces for browser tools, automation helpers, and mpv-driven workflows:
|
SubMiner exposes a small set of local integration surfaces for browser tools, automation helpers, and mpv-driven workflows:
|
||||||
|
|
||||||
- **Subtitle WebSocket** at `ws://127.0.0.1:6677` by default for plain subtitle pushes.
|
- **Subtitle WebSocket** at `ws://127.0.0.1:6677` by default for plain subtitle pushes.
|
||||||
@@ -46,13 +50,13 @@ SubMiner's integration ports are configured in `config.jsonc`.
|
|||||||
- `texthooker.launchAtStartup` starts the local HTTP UI automatically.
|
- `texthooker.launchAtStartup` starts the local HTTP UI automatically.
|
||||||
- `texthooker.openBrowser` controls whether SubMiner opens the texthooker page in your browser when it starts.
|
- `texthooker.openBrowser` controls whether SubMiner opens the texthooker page in your browser when it starts.
|
||||||
|
|
||||||
If you use the [mpv plugin](/mpv-plugin), it can also start a texthooker-only helper process and override the texthooker port in `subminer.conf`.
|
If you use the [mpv plugin](/mpv-plugin), it can also start a texthooker-only helper process. The launcher derives the plugin's texthooker setting from your SubMiner config (`texthooker.launchAtStartup`) and injects it at runtime — there is no plugin config file to edit.
|
||||||
|
|
||||||
## Developer API Documentation
|
## Developer API Documentation
|
||||||
|
|
||||||
### 1. Subtitle WebSocket
|
### 1. Subtitle WebSocket
|
||||||
|
|
||||||
Use the basic subtitle websocket when you only need the current subtitle line and a ready-to-render HTML sentence string.
|
Use the basic subtitle websocket when you only need the current subtitle line as plain text.
|
||||||
|
|
||||||
- **Default URL:** `ws://127.0.0.1:6677`
|
- **Default URL:** `ws://127.0.0.1:6677`
|
||||||
- **Transport:** local WebSocket server bound to `127.0.0.1`
|
- **Transport:** local WebSocket server bound to `127.0.0.1`
|
||||||
@@ -64,6 +68,36 @@ When a client connects, SubMiner immediately sends the latest subtitle payload i
|
|||||||
|
|
||||||
#### Message shape
|
#### Message shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"text": "無事",
|
||||||
|
"sentence": "無事",
|
||||||
|
"tokens": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Field reference
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `version` | number | Current websocket payload version. Today this is `1`. |
|
||||||
|
| `text` | string | Raw subtitle text. |
|
||||||
|
| `sentence` | string | Plain subtitle text with line breaks represented as `<br>`. No annotation spans or attributes. |
|
||||||
|
| `tokens` | array | Always empty on the basic subtitle websocket. |
|
||||||
|
|
||||||
|
### 2. Annotation WebSocket
|
||||||
|
|
||||||
|
Use the annotation websocket for custom clients that want the same structured token payload the bundled texthooker UI consumes.
|
||||||
|
|
||||||
|
- **Default URL:** `ws://127.0.0.1:6678`
|
||||||
|
- **Payload shape:** JSON payload with `text`, rendered `sentence` HTML, and token metadata
|
||||||
|
- **Primary difference:** this stream is intended to stay on even when the basic websocket auto-disables because `mpv_websocket` is installed
|
||||||
|
|
||||||
|
In practice, if you are building a new client, prefer `annotationWebsocket` unless you specifically need compatibility with an existing `websocket` consumer.
|
||||||
|
|
||||||
|
#### Message shape
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
@@ -91,16 +125,7 @@ When a client connects, SubMiner immediately sends the latest subtitle payload i
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Field reference
|
Each annotation token may include:
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `version` | number | Current websocket payload version. Today this is `1`. |
|
|
||||||
| `text` | string | Raw subtitle text. |
|
|
||||||
| `sentence` | string | HTML string with `<span>` wrappers and `data-*` attributes for client rendering. |
|
|
||||||
| `tokens` | array | Token metadata; empty when the subtitle is not tokenized yet. |
|
|
||||||
|
|
||||||
Each token may include:
|
|
||||||
|
|
||||||
| Token field | Type | Notes |
|
| Token field | Type | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -119,16 +144,6 @@ Each token may include:
|
|||||||
| `frequencyRankLabel` | string or `null` | Preformatted rank label for UIs |
|
| `frequencyRankLabel` | string or `null` | Preformatted rank label for UIs |
|
||||||
| `jlptLevelLabel` | string or `null` | Preformatted JLPT label for UIs |
|
| `jlptLevelLabel` | string or `null` | Preformatted JLPT label for UIs |
|
||||||
|
|
||||||
### 2. Annotation WebSocket
|
|
||||||
|
|
||||||
Use the annotation websocket for custom clients that want the same structured token payload the bundled texthooker UI consumes.
|
|
||||||
|
|
||||||
- **Default URL:** `ws://127.0.0.1:6678`
|
|
||||||
- **Payload shape:** same JSON contract as the basic subtitle websocket
|
|
||||||
- **Primary difference:** this stream is intended to stay on even when the basic websocket auto-disables because `mpv_websocket` is installed
|
|
||||||
|
|
||||||
In practice, if you are building a new client, prefer `annotationWebsocket` unless you specifically need compatibility with an existing `websocket` consumer.
|
|
||||||
|
|
||||||
### 3. HTML markup conventions
|
### 3. HTML markup conventions
|
||||||
|
|
||||||
The `sentence` field is pre-rendered HTML generated by SubMiner. Depending on token state, it can include classes such as:
|
The `sentence` field is pre-rendered HTML generated by SubMiner. Depending on token state, it can include classes such as:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ SubMiner auto-loads Japanese subtitles when you play a YouTube URL, giving you t
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- **yt-dlp** must be installed and on `PATH` (or set `SUBMINER_YTDLP_BIN` to its path)
|
- **[yt-dlp](https://github.com/yt-dlp/yt-dlp)** must be installed and on your `PATH`. yt-dlp is a free command-line tool that reads YouTube video and subtitle info; SubMiner calls it behind the scenes. (`PATH` is the list of folders your system searches for programs — most installers add yt-dlp to it automatically. If yours did not, set `SUBMINER_YTDLP_BIN` to the full path of the yt-dlp binary.)
|
||||||
- mpv with `--input-ipc-server` configured (handled automatically by the `subminer` launcher)
|
- mpv with `--input-ipc-server` configured (handled automatically when you launch playback through the `subminer` launcher — no manual setup needed).
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
@@ -111,8 +111,8 @@ Secondary track selection uses the shared `secondarySub` config:
|
|||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"secondarySub": {
|
"secondarySub": {
|
||||||
"secondarySubLanguages": ["eng", "en"],
|
"secondarySubLanguages": [],
|
||||||
"autoLoadSecondarySub": true,
|
"autoLoadSecondarySub": false,
|
||||||
"defaultMode": "hover"
|
"defaultMode": "hover"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,8 +120,8 @@ Secondary track selection uses the shared `secondarySub` config:
|
|||||||
|
|
||||||
| Option | Type | Description |
|
| Option | Type | Description |
|
||||||
| ------ | ---- | ----------- |
|
| ------ | ---- | ----------- |
|
||||||
| `secondarySubLanguages` | `string[]` | Language codes for secondary subtitle auto-loading (default: English) |
|
| `secondarySubLanguages` | `string[]` | Extra language codes (e.g. `["eng", "en"]`) used when auto-selecting a secondary track. Default is empty (`[]`). For YouTube, SubMiner always tries an English track first regardless of this list. |
|
||||||
| `autoLoadSecondarySub` | `boolean` | Auto-detect and load matching secondary track |
|
| `autoLoadSecondarySub` | `boolean` | Auto-detect and load a matching secondary track (default: `false`) |
|
||||||
| `defaultMode` | `"hidden"` / `"visible"` / `"hover"` | Initial display mode for secondary subtitles (default: `"hover"`) |
|
| `defaultMode` | `"hidden"` / `"visible"` / `"hover"` | Initial display mode for secondary subtitles (default: `"hover"`) |
|
||||||
|
|
||||||
Precedence: CLI flag > environment variable > `config.jsonc` > built-in default.
|
Precedence: CLI flag > environment variable > `config.jsonc` > built-in default.
|
||||||
|
|||||||
+1
-2
@@ -3,7 +3,7 @@
|
|||||||
# SubMiner Internal Docs
|
# SubMiner Internal Docs
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-13
|
Last verified: 2026-05-23
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: you need internal architecture, workflow, verification, or release guidance
|
Read when: you need internal architecture, workflow, verification, or release guidance
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ Read when: you need internal architecture, workflow, verification, or release gu
|
|||||||
- [Workflow](./workflow/README.md) - planning, execution, verification expectations
|
- [Workflow](./workflow/README.md) - planning, execution, verification expectations
|
||||||
- [Knowledge Base](./knowledge-base/README.md) - how docs are structured, maintained, and audited
|
- [Knowledge Base](./knowledge-base/README.md) - how docs are structured, maintained, and audited
|
||||||
- [Release Guide](./RELEASING.md) - tagged release checklist
|
- [Release Guide](./RELEASING.md) - tagged release checklist
|
||||||
- [Plans](./plans/) - active design and implementation artifacts
|
|
||||||
|
|
||||||
## Fast Paths
|
## Fast Paths
|
||||||
|
|
||||||
|
|||||||
+27
-3
@@ -31,9 +31,13 @@
|
|||||||
`bun run test:fast`
|
`bun run test:fast`
|
||||||
`bun run test:env`
|
`bun run test:env`
|
||||||
`bun run build`
|
`bun run build`
|
||||||
|
When validating auto-update metadata, also run the relevant platform package
|
||||||
|
build and confirm `release/` contains the generated updater metadata
|
||||||
|
(`*.yml`) and blockmaps (`*.blockmap`).
|
||||||
8. If `docs-site/` changed, also run:
|
8. If `docs-site/` changed, also run:
|
||||||
`bun run docs:test`
|
`bun run docs:test`
|
||||||
`bun run docs:build`
|
`bun run docs:build`
|
||||||
|
`bun run docs:build:versioned`
|
||||||
9. Commit release prep.
|
9. Commit release prep.
|
||||||
10. Tag the commit: `git tag v<version>`.
|
10. Tag the commit: `git tag v<version>`.
|
||||||
11. Push commit + tag.
|
11. Push commit + tag.
|
||||||
@@ -50,11 +54,20 @@
|
|||||||
`bun run test:fast`
|
`bun run test:fast`
|
||||||
`bun run test:env`
|
`bun run test:env`
|
||||||
`bun run build`
|
`bun run build`
|
||||||
5. Commit the prerelease prep. Do not run `bun run changelog:build`.
|
When validating packaged updater output, confirm the platform build writes
|
||||||
|
`*.yml` and `*.blockmap` files under `release/`.
|
||||||
|
5. Commit the prerelease prep (package.json version bump + the generated
|
||||||
|
`release/prerelease-notes.md`). CI does not regenerate notes — it uses the
|
||||||
|
committed file — so review it before committing. If you add more
|
||||||
|
`changes/*.md` fragments for a later beta/RC, rerun
|
||||||
|
`bun run changelog:prerelease-notes --version <version>`; the generator uses
|
||||||
|
the existing prerelease notes as the baseline and asks Claude to merge only
|
||||||
|
the new fragment material. Do not run `bun run changelog:build`.
|
||||||
6. Tag the commit: `git tag v<version>`.
|
6. Tag the commit: `git tag v<version>`.
|
||||||
7. Push commit + tag.
|
7. Push commit + tag.
|
||||||
|
|
||||||
Prerelease tags publish a GitHub prerelease only. They do not update `CHANGELOG.md`, `docs-site/changelog.md`, or the AUR package, and they do not consume `changes/*.md` fragments. The final stable release is still the point where `bun run changelog:build` consumes fragments into `CHANGELOG.md` and regenerates stable release notes.
|
Prerelease tags publish a GitHub prerelease only. They do not update `CHANGELOG.md`, `docs-site/changelog.md`, or the AUR package, and they do not consume `changes/*.md` fragments. The final stable release is still the point where `bun run changelog:build` consumes fragments into `CHANGELOG.md` and regenerates stable release notes.
|
||||||
|
Prerelease tags also do not update `https://docs.subminer.moe/`.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
@@ -62,13 +75,24 @@ Notes:
|
|||||||
- Supported prerelease channels are `beta` and `rc`, with versions like `0.11.3-beta.1` and `0.11.3-rc.1`.
|
- Supported prerelease channels are `beta` and `rc`, with versions like `0.11.3-beta.1` and `0.11.3-rc.1`.
|
||||||
- Pass `--date` explicitly when you want the release stamped with the local cut date; otherwise the generator uses the current ISO date, which can roll over to the next UTC day late at night.
|
- Pass `--date` explicitly when you want the release stamped with the local cut date; otherwise the generator uses the current ISO date, which can roll over to the next UTC day late at night.
|
||||||
- `changelog:check` now rejects tag/package version mismatches.
|
- `changelog:check` now rejects tag/package version mismatches.
|
||||||
- `changelog:prerelease-notes` also rejects tag/package version mismatches and writes `release/prerelease-notes.md` without mutating tracked changelog files.
|
- `changelog:prerelease-notes` also rejects tag/package version mismatches and writes `release/prerelease-notes.md` without mutating tracked changelog files. When that file already exists, the generator includes it in the Claude prompt so later beta/RC notes reuse the reviewed text instead of starting over.
|
||||||
- `changelog:build` generates `CHANGELOG.md` + `release/release-notes.md` (both polished by `claude -p`) and removes the released `changes/*.md` fragments. The CHANGELOG keeps internal notes inside a `<details><summary>Internal changes</summary>` collapse; the release notes drop them entirely.
|
- `changelog:build` generates `CHANGELOG.md` + `release/release-notes.md` (both polished by `claude -p`) and removes the released `changes/*.md` fragments. The CHANGELOG keeps internal notes inside a `<details><summary>Internal changes</summary>` collapse; the release notes drop them entirely.
|
||||||
- The release workflow no longer auto-runs `changelog:build`. If pending `changes/*.md` fragments are present on a tag-based run, CI exits with a clear `::error::` pointing at the local fix. Run `bun run changelog:build --version <version>` locally, commit the polished output, then tag.
|
- The release workflow no longer auto-runs `changelog:build`. If pending `changes/*.md` fragments are present on a tag-based run, CI exits with a clear `::error::` pointing at the local fix. Run `bun run changelog:build --version <version>` locally, commit the polished output, then tag.
|
||||||
- Do not tag while `changes/*.md` fragments still exist.
|
- Do not tag while `changes/*.md` fragments still exist.
|
||||||
- Prerelease tags intentionally keep `changes/*.md` fragments in place so multiple prereleases can reuse the same cumulative pending notes until the final stable cut.
|
- Prerelease tags intentionally keep `changes/*.md` fragments in place so multiple prereleases can reuse the same cumulative pending notes until the final stable cut. `make clean` preserves `release/prerelease-notes.md` while deleting generated build artifacts.
|
||||||
- If you need to repair a published release body (for example, a prior version’s section was omitted), regenerate notes from `CHANGELOG.md` and re-edit the release with `gh release edit --notes-file`.
|
- If you need to repair a published release body (for example, a prior version’s section was omitted), regenerate notes from `CHANGELOG.md` and re-edit the release with `gh release edit --notes-file`.
|
||||||
- Prerelease tags are handled by `.github/workflows/prerelease.yml`, which always publishes a GitHub prerelease with all current release platforms and never runs the AUR sync job.
|
- Prerelease tags are handled by `.github/workflows/prerelease.yml`, which always publishes a GitHub prerelease with all current release platforms and never runs the AUR sync job.
|
||||||
- Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication.
|
- Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication.
|
||||||
|
- Stable release tags update `https://docs.subminer.moe/` and `https://docs.subminer.moe/v/<version>/` through `.github/workflows/docs-pages.yml`; `/main/` continues to show development docs from `main`.
|
||||||
|
- Keep Cloudflare Pages Git auto-deploy disabled for `docs.subminer.moe`. Production docs are direct-uploaded by Wrangler from GitHub Actions with `--branch main`.
|
||||||
- AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed.
|
- AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed.
|
||||||
- Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation.
|
- Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation.
|
||||||
|
- Release and prerelease workflows upload updater metadata (`*.yml`) and blockmaps (`*.blockmap`) alongside platform artifacts. Do not remove those files while `electron-updater` is enabled.
|
||||||
|
- macOS tray app updates use the standard `electron-updater`/Squirrel path. Keep `latest-mac.yml`, the macOS `SubMiner-<version>-mac.zip`, and ZIP blockmap published; Squirrel uses the ZIP payload even when the DMG remains the user-facing installer.
|
||||||
|
- macOS update metadata and full ZIP downloads are routed through `/usr/bin/curl` before Squirrel installation to avoid Electron main-process network crashes on update checks.
|
||||||
|
- Windows tray app updates use the standard `electron-updater`/NSIS path. Keep `latest.yml`, the Windows NSIS installer, and installer blockmap published; updater HTTP is routed through main-process fetch to avoid Electron main-process network crashes during update checks.
|
||||||
|
- Build config emits distinct ZIP names: `SubMiner-<version>-mac.zip` for the macOS Squirrel updater payload and `SubMiner-<version>-win.zip` for the Windows portable fallback. The user-facing DMG and Windows installer keep the unqualified `SubMiner-<version>` basename.
|
||||||
|
- Linux GitHub release metadata and asset downloads also use `/usr/bin/curl` instead of Electron networking for the same reason.
|
||||||
|
- Local macOS build-output apps outside `/Applications` or `~/Applications` skip native update checks. Manual tray and launcher checks still use GitHub release metadata to report newer releases, but automatic notifications stay quiet when native app installation is unsupported. To validate auto-update end to end, install the signed and notarized app bundle into one of those Applications folders and point it at a published updater feed.
|
||||||
|
- The first updater-enabled release cannot update older installs automatically. Users need one manual install to get the updater code.
|
||||||
|
- Stable auto-update checks ignore beta/RC prereleases by default. Set `updates.channel` to `"prerelease"` on a test install when validating beta/RC updater behavior.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Architecture Map
|
# Architecture Map
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-26
|
Last verified: 2026-05-23
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: runtime ownership, composition boundaries, or layering questions
|
Read when: runtime ownership, composition boundaries, or layering questions
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Domain Ownership
|
# Domain Ownership
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-26
|
Last verified: 2026-05-23
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: you need to find the owner module for a behavior or test surface
|
Read when: you need to find the owner module for a behavior or test surface
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ Read when: you need to find the owner module for a behavior or test surface
|
|||||||
- Config system: `src/config/`
|
- Config system: `src/config/`
|
||||||
- Overlay/window state: `src/core/services/overlay-*`, `src/main/overlay-*.ts`
|
- Overlay/window state: `src/core/services/overlay-*`, `src/main/overlay-*.ts`
|
||||||
- MPV runtime and protocol: `src/core/services/mpv*.ts`
|
- MPV runtime and protocol: `src/core/services/mpv*.ts`
|
||||||
- Subtitle/token pipeline: `src/core/services/tokenizer*`, `src/subtitle/`, `src/tokenizers/`
|
- Subtitle/token pipeline: `src/core/services/subtitle-*.ts`, `src/core/services/tokenizer*`, `src/core/services/tokenizer/`, `src/subsync/`
|
||||||
- Anki workflow: `src/anki-integration/`, `src/core/services/anki-jimaku*.ts`
|
- Anki workflow: `src/anki-integration/`, `src/core/services/anki-jimaku*.ts`
|
||||||
- Immersion tracking: `src/core/services/immersion-tracker/`
|
- Immersion tracking: `src/core/services/immersion-tracker/`
|
||||||
Includes stats storage/query schema such as `imm_videos`, `imm_media_art`, and `imm_youtube_videos` for per-video and YouTube-specific library metadata.
|
Includes stats storage/query schema such as `imm_videos`, `imm_media_art`, and `imm_youtube_videos` for per-video and YouTube-specific library metadata.
|
||||||
@@ -37,6 +37,8 @@ Read when: you need to find the owner module for a behavior or test surface
|
|||||||
- Anki-specific contracts: `src/types/anki.ts`
|
- Anki-specific contracts: `src/types/anki.ts`
|
||||||
- External integration contracts: `src/types/integrations.ts`
|
- External integration contracts: `src/types/integrations.ts`
|
||||||
- Runtime-option contracts: `src/types/runtime-options.ts`
|
- Runtime-option contracts: `src/types/runtime-options.ts`
|
||||||
|
- Settings UI contracts: `src/types/settings.ts`
|
||||||
|
- Session-binding contracts: `src/types/session-bindings.ts`
|
||||||
- Compatibility-only barrel: `src/types.ts`
|
- Compatibility-only barrel: `src/types.ts`
|
||||||
|
|
||||||
## Ownership Heuristics
|
## Ownership Heuristics
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Layering Rules
|
# Layering Rules
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-13
|
Last verified: 2026-05-23
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: deciding whether a dependency direction is acceptable
|
Read when: deciding whether a dependency direction is acceptable
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Knowledge Base Rules
|
# Knowledge Base Rules
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-13
|
Last verified: 2026-05-23
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: maintaining the internal doc system itself
|
Read when: maintaining the internal doc system itself
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,24 @@
|
|||||||
# Documentation Catalog
|
# Documentation Catalog
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-13
|
Last verified: 2026-05-23
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: finding internal docs or checking verification status
|
Read when: finding internal docs or checking verification status
|
||||||
|
|
||||||
| Area | Path | Status | Last verified | Notes |
|
| Area | Path | Status | Last verified | Notes |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| KB home | `docs/README.md` | active | 2026-03-13 | internal entrypoint |
|
| KB home | `docs/README.md` | active | 2026-05-23 | internal entrypoint |
|
||||||
| Architecture index | `docs/architecture/README.md` | active | 2026-03-13 | top-level runtime map |
|
| Architecture index | `docs/architecture/README.md` | active | 2026-05-23 | top-level runtime map |
|
||||||
| Domain ownership | `docs/architecture/domains.md` | active | 2026-03-13 | runtime and feature ownership |
|
| Domain ownership | `docs/architecture/domains.md` | active | 2026-05-23 | runtime and feature ownership |
|
||||||
| Layering rules | `docs/architecture/layering.md` | active | 2026-03-13 | dependency direction and smells |
|
| Layering rules | `docs/architecture/layering.md` | active | 2026-05-23 | dependency direction and smells |
|
||||||
| KB rules | `docs/knowledge-base/README.md` | active | 2026-03-13 | maintenance policy |
|
| KB rules | `docs/knowledge-base/README.md` | active | 2026-05-23 | maintenance policy |
|
||||||
| Core beliefs | `docs/knowledge-base/core-beliefs.md` | active | 2026-03-13 | agent-first principles |
|
| Core beliefs | `docs/knowledge-base/core-beliefs.md` | active | 2026-03-13 | agent-first principles |
|
||||||
| Quality scorecard | `docs/knowledge-base/quality.md` | active | 2026-03-13 | quality grades and gaps |
|
| Quality scorecard | `docs/knowledge-base/quality.md` | active | 2026-03-13 | quality grades and gaps |
|
||||||
| Workflow index | `docs/workflow/README.md` | active | 2026-03-13 | execution map |
|
| Workflow index | `docs/workflow/README.md` | active | 2026-05-23 | execution map |
|
||||||
| Planning guide | `docs/workflow/planning.md` | active | 2026-03-13 | lightweight vs execution plans |
|
| Planning guide | `docs/workflow/planning.md` | active | 2026-05-23 | lightweight vs execution plans |
|
||||||
| Verification guide | `docs/workflow/verification.md` | active | 2026-03-13 | maintained verification lanes |
|
| Agent plugins | `docs/workflow/agent-plugins.md` | active | 2026-05-23 | repo-local agent workflow plugin ownership |
|
||||||
| Release guide | `docs/RELEASING.md` | active | 2026-03-13 | release checklist |
|
| Verification guide | `docs/workflow/verification.md` | active | 2026-05-23 | maintained verification lanes |
|
||||||
| Active plans | `docs/plans/` | active | 2026-03-13 | task-scoped design and implementation artifacts |
|
| Release guide | `docs/RELEASING.md` | active | 2026-05-23 | release checklist |
|
||||||
|
|
||||||
## Update Rules
|
## Update Rules
|
||||||
|
|
||||||
|
|||||||
@@ -37,4 +37,3 @@ Grades are directional, not ceremonial. The point is to keep gaps visible.
|
|||||||
|
|
||||||
- Some deep architecture detail still lives in `docs-site/architecture.md` and may merit later migration.
|
- Some deep architecture detail still lives in `docs-site/architecture.md` and may merit later migration.
|
||||||
- Quality grading is manual and should be refreshed when major refactors land.
|
- Quality grading is manual and should be refreshed when major refactors land.
|
||||||
- Active plans can accumulate without lifecycle cleanup if humans do not prune them.
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Workflow
|
# Workflow
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-13
|
Last verified: 2026-05-23
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: planning or executing nontrivial work in this repo
|
Read when: planning or executing nontrivial work in this repo
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Agent Plugins
|
# Agent Plugins
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-26
|
Last verified: 2026-05-23
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: packaging or migrating repo-local agent workflow skills into plugins
|
Read when: packaging or migrating repo-local agent workflow skills into plugins
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Planning
|
# Planning
|
||||||
|
|
||||||
Status: active
|
Status: active
|
||||||
Last verified: 2026-03-13
|
Last verified: 2026-05-23
|
||||||
Owner: Kyle Yasuda
|
Owner: Kyle Yasuda
|
||||||
Read when: the task spans multiple files, subsystems, or verification lanes
|
Read when: the task spans multiple files, subsystems, or verification lanes
|
||||||
|
|
||||||
@@ -28,9 +28,9 @@ Read when: the task spans multiple files, subsystems, or verification lanes
|
|||||||
|
|
||||||
## Plan Location
|
## Plan Location
|
||||||
|
|
||||||
- active design and implementation docs live in `docs/plans/`
|
- plans are task-scoped scratch artifacts; keep them with the work (worktree, branch, or PR description), not committed under `docs/`
|
||||||
- keep names date-prefixed and task-specific
|
- if a plan must be shared, keep names date-prefixed and task-specific
|
||||||
- remove or archive old plans deliberately; do not leave mystery artifacts
|
- delete plans once the work lands; do not leave mystery artifacts behind
|
||||||
|
|
||||||
## Plan Contents
|
## Plan Contents
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user