Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15cb9fe918
|
||
|
|
db45a70ec2
|
||
|
|
9e79fa93c2 | ||
|
|
9fadbc8cdd
|
||
|
|
ffbbe698fc | ||
|
|
7b72dd2cc4
|
||
|
|
416cd7e9ac
|
||
|
|
eb749f8c19
|
||
|
|
65ac5a7728 | ||
|
|
df675cfbfe
|
||
|
|
e49b5f6589
|
||
|
|
0bbd8617f4 | ||
|
|
4e43c5c6e1 | ||
|
|
953abdf42a
|
||
|
|
fdb0a31fff
|
||
|
|
04b878dd93
|
||
|
|
07b32626a6 | ||
|
|
c68b73c8c1 | ||
|
|
b24008237b | ||
|
|
63eb228e4f | ||
|
|
ba0155e429 | ||
|
|
23af9c6464
|
||
|
|
8825cca642
|
||
|
|
bc73a02623 | ||
|
|
5096ca217b
|
||
|
|
285b19ccc3
|
||
|
|
7fbc36dc41
|
||
|
|
e93b696e71
|
||
|
|
7c161723cd | ||
|
|
b0fb39a8f5 | ||
|
|
a7a302bdd0 | ||
|
|
6a1d1211ae
|
||
|
|
7585d430ae
|
||
|
|
122ff9a3c5
|
||
|
|
786bd5af81
|
||
|
|
42d1384c68 | ||
|
|
1508863dbb | ||
|
|
4dd30f44d1
|
||
|
|
ab48a5678e | ||
|
|
2f21582666 | ||
|
|
383aed8bad | ||
|
|
f9c4e892dc | ||
|
|
7959e1a4e5 | ||
|
|
917d52dd94 | ||
|
|
05425cc5db | ||
|
|
8097ed27e5 | ||
|
|
76e43d7e6d | ||
|
|
4f0762e840 | ||
|
|
026d495fac | ||
|
|
dd76782d30
|
||
|
|
a8c16147ad | ||
|
|
186d4a0640 | ||
|
|
2831e05124 | ||
|
|
6841a37a22 | ||
|
|
7c1eac03dc | ||
|
|
e6dc9dfec5
|
||
|
|
6d69a56574 | ||
|
|
0c37c665a2 | ||
|
|
8ae5bde6a4
|
||
|
|
016a43b804 | ||
|
|
fb2d1a512b | ||
|
|
614a8ca912 | ||
|
|
9eecb7358b
|
||
|
|
1e5d7747b4 | ||
|
|
c14c690875
|
||
|
|
84f718043a | ||
|
|
99266294b8 | ||
|
|
c055359be1
|
||
|
|
ec5a147095 | ||
|
|
a0635f4360 | ||
|
|
c0a78ef008 | ||
|
|
87b01155df | ||
|
|
fc5c49e365 | ||
|
|
a20269e9f5 | ||
|
|
2ad491e95c
|
||
|
|
6fffcc731f | ||
|
|
5cc21113fd | ||
|
|
051140f910 | ||
|
|
6e945f0872 | ||
|
|
c2c25c0da6 | ||
|
|
556de61756
|
||
|
|
a98fb0fddf
|
||
|
|
e816b3b371 | ||
|
|
ed7d3f4c3d | ||
|
|
509dc5bf7f | ||
|
|
0a0aa3ec98
|
||
|
|
b87cc3cfdd
|
||
|
|
8cb3c8c90a
|
||
|
|
03ea903927
|
||
|
|
3aea42e6f8
|
||
|
|
88bb3edfa4
|
||
|
|
9445aef004
|
||
|
|
c01bcd9d0f
|
||
|
|
695613b1e7
|
||
|
|
20797772f8 | ||
|
|
72c312810a
|
||
|
|
b61f050e78
|
||
|
|
2a77ba9dd4 | ||
|
|
4ed878270f
|
||
|
|
1d1b0c7bb6
|
||
|
|
db61ce358d
|
||
|
|
e9778a945a | ||
|
|
7de73e16a1 | ||
|
|
273652f781 | ||
|
|
14c0086520
|
||
|
|
ff0bc0af6d | ||
|
|
00b1b79bf4 | ||
|
|
e11a5fea0d
|
||
|
|
f73fe179d0
|
||
|
|
2938e7a32a | ||
|
|
82f6b4705a | ||
|
|
a02c33dac4 | ||
|
|
2174e689a2 | ||
|
|
7d729cb60c | ||
|
|
d963def1a9 | ||
|
|
6fb3d2eb13 | ||
|
|
c5a77ac067 | ||
|
|
8ddc151435
|
||
|
|
b98d4d65c7 | ||
|
|
046e74ea91
|
||
|
|
8bf847503d | ||
|
|
47b5903392
|
||
|
|
bf85554d1e | ||
|
|
d74c7e1235
|
||
|
|
57ddd19953 | ||
|
|
ee25536d90 | ||
|
|
7b0fbdf254 | ||
|
|
2fefc83e3f | ||
|
|
dbdf578c68 | ||
|
|
441ecf3c04 | ||
|
|
a0dde4ee3e
|
||
|
|
fe4dacc1e7 | ||
|
|
b08cd0db35 | ||
|
|
bffb1c5982 | ||
|
|
5b8848518a | ||
|
|
176edd67f1
|
||
|
|
4d65dec340 | ||
|
|
6607c333bc
|
||
|
|
b2bbf1ae12
|
||
|
|
b204d4dd6e | ||
|
|
89ed675935 | ||
|
|
1995200e76
|
||
|
|
e876e483d6
|
||
|
|
6d2a72e13b | ||
|
|
0d7084c8aa | ||
|
|
95e0abc7b7
|
||
|
|
d9155ceacb
|
||
|
|
9d0585423c
|
||
|
|
455cfff90a
|
||
|
|
987b325edb | ||
|
|
08c6807cb1
|
||
|
|
18c6410f24
|
||
|
|
e223cf9b71
|
||
|
|
c718bb8b34
|
||
|
|
177f7e5fd6
|
||
|
|
20b50ec4e1 | ||
|
|
deae61b211 | ||
|
|
44959ed282
|
||
|
|
2398f5c030 | ||
|
|
8711cf1a48
|
||
|
|
9f4888293b
|
||
|
|
29332b103e
|
||
|
|
8b2cee2c58 | ||
|
|
2e2ee3f028 | ||
|
|
49b926e08c | ||
|
|
66f8ca4f80 | ||
|
|
6fe1e0fee4 | ||
|
|
0ab840b362 | ||
|
|
21a06c12fb | ||
|
|
14070acceb
|
||
|
|
7d81342f0f
|
||
|
|
4b7f750919 | ||
|
|
6ab3d823a4 | ||
|
|
8797719a09
|
||
|
|
8acc78cc1c | ||
|
|
321461c50f | ||
|
|
beeab564b4
|
||
|
|
cab7975a2b
|
||
|
|
846292809c
|
||
|
|
8712780d08 | ||
|
|
7b1a005a65 | ||
|
|
84c75f50aa
|
||
|
|
8b838f2c7d
|
||
|
|
db4139ba0b
|
||
|
|
6c251502b3 | ||
|
|
ae40934d3a | ||
|
|
cdb1475a54
|
||
|
|
a2e49b369b
|
||
|
|
7f13aed50a | ||
|
|
8b21a2bca8
|
||
|
|
925413adfe
|
||
|
|
d0644ab2eb | ||
|
|
d253710c2e | ||
|
|
c3df510e4f
|
||
|
|
187f68e5b6 | ||
|
|
0e254cbbef
|
||
|
|
7b94adafbd
|
||
|
|
61f39d1e09 | ||
|
|
e7739de51c | ||
|
|
ad1d240f20 | ||
|
|
8b0ef662bc
|
||
|
|
0a58c20ad7 | ||
|
|
38ddb29aa0 | ||
|
|
8b9a70c5a6 | ||
|
|
48a084914a | ||
|
|
a042b04357 | ||
|
|
35ca2afc6f | ||
|
|
b14f977e33 | ||
|
|
eef4500599 | ||
|
|
73af1451b7
|
||
|
|
36a3704815 | ||
|
|
359cb0a301 | ||
|
|
4b10e85053
|
||
|
|
c942a2cf2d | ||
|
|
f65afa6046 | ||
|
|
389d8e06e0
|
||
|
|
0008b55b70
|
||
|
|
d16ae9c745
|
||
|
|
36f94151b8 | ||
|
|
5326ad32f5 | ||
|
|
d199376364
|
||
|
|
3aee88c150
|
||
|
|
70da3ee8bd | ||
|
|
aa8eb753f6
|
||
|
|
8d73de8731
|
||
|
|
5a98397efe
|
||
|
|
a117c5759c | ||
|
|
ae7e6f82a8 | ||
|
|
1158be5b39 | ||
|
|
33e767458f | ||
|
|
94a65416ae | ||
|
|
0a384a22c9 | ||
|
|
b3b45521b6
|
||
|
|
131b23efa9
|
||
|
|
e2afceb492 | ||
|
|
7be1843c41 | ||
|
|
c09d009a3e
|
||
|
|
2007e28be8 | ||
|
|
d5bfdcae7b | ||
|
|
311f1e8ee5 | ||
|
|
e6a16a069b | ||
|
|
af67c53dd6 | ||
|
|
ea79e331fa
|
||
|
|
ee89b0c8a9 | ||
|
|
f2fd58cd2b | ||
|
|
1280a30216
|
||
|
|
a80ed72b2d
|
||
|
|
4cc6c12dc7 | ||
|
|
425004879a | ||
|
|
76f99e6518 | ||
|
|
f1e260e996 | ||
|
|
54e90754ef
|
||
|
|
487143802a | ||
|
|
e6a004ab8b | ||
|
|
b510c54875 | ||
|
|
e1ea464bc9 | ||
|
|
b46b8dfa41 | ||
|
|
ca067a6ccf | ||
|
|
d719b346e0 | ||
|
|
a1da3dcdc8
|
||
|
|
9927ef1581
|
||
|
|
791c993870
|
||
|
|
38dbce517c
|
||
|
|
889dc9c009
|
||
|
|
097021072d
|
||
|
|
91c8eb8faf
|
||
|
|
eed0a6a243 | ||
|
|
d33009d4a3 | ||
|
|
8d0535f3ca | ||
|
|
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
|
No files matched your search
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "subminer-local",
|
|
||||||
"interface": {
|
|
||||||
"displayName": "SubMiner Local"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "subminer-workflow",
|
|
||||||
"source": {
|
|
||||||
"source": "local",
|
|
||||||
"path": "./plugins/subminer-workflow"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE",
|
|
||||||
"authentication": "ON_INSTALL"
|
|
||||||
},
|
|
||||||
"category": "Productivity"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,45 @@
|
|||||||
---
|
---
|
||||||
name: 'subminer-change-verification'
|
name: subminer-change-verification
|
||||||
description: 'Compatibility shim. Canonical SubMiner change verification workflow now lives in the repo-local subminer-workflow plugin.'
|
description: Verify SubMiner changes with repo-native cheap-first test lanes. Use after code, config, launcher, plugin, runtime, stats, documentation, or workflow changes; do not use for read-only questions.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Compatibility Shim
|
# SubMiner Change Verification
|
||||||
|
|
||||||
Canonical source:
|
Verify the behavior claimed by a change without running unrelated expensive checks by default.
|
||||||
|
|
||||||
- `plugins/subminer-workflow/skills/subminer-change-verification/SKILL.md`
|
## Workflow
|
||||||
|
|
||||||
Canonical helper scripts:
|
1. Inspect the requested scope and changed paths with `git status --short` and `git diff`.
|
||||||
|
2. Read `docs/workflow/verification.md` as the source of truth for maintained lanes.
|
||||||
|
3. Run the cheapest lane or lanes that cover the changed behavior.
|
||||||
|
4. Escalate to the full handoff gate only for substantial or cross-boundary changes.
|
||||||
|
5. Report exact commands, results, skipped checks, blockers, and remaining risk.
|
||||||
|
|
||||||
- `plugins/subminer-workflow/skills/subminer-change-verification/scripts/classify_subminer_diff.sh`
|
Do not use hidden wrapper commands. Verification commands are owned by `package.json` and the workflow documentation.
|
||||||
- `plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh`
|
|
||||||
|
|
||||||
When this shim is invoked:
|
## Lane Selection
|
||||||
|
|
||||||
1. Read the canonical plugin-owned skill.
|
- Internal docs, `AGENTS.md`, or `.agents/skills/**`: `bun run test:docs:kb`
|
||||||
2. Follow the plugin-owned skill as the source of truth.
|
- User-facing `docs-site/**`: `bun run docs:test`, then `bun run docs:build`
|
||||||
3. Use the wrapper scripts in this shim directory only for compatibility with existing commands and docs.
|
- Config/schema/defaults: `bun run test:config`
|
||||||
4. Do not duplicate workflow changes here; update the plugin-owned skill and scripts instead.
|
- If defaults or templates changed, also run `bun run generate:config-example` and `bun run verify:config-example`.
|
||||||
|
- General TypeScript source: `bun run typecheck`, then `bun run test:fast`
|
||||||
|
- Launcher or mpv plugin: `bun run test:launcher` or `bun run test:env`, based on the behavior changed
|
||||||
|
- Runtime compatibility or dist-sensitive wiring: `bun run test:runtime:compat`
|
||||||
|
- Stats dashboard: `bun run test:stats`
|
||||||
|
- Build/release scripts: `bun run test:scripts`
|
||||||
|
|
||||||
|
For substantial changes, use the full gate documented in `AGENTS.md` and `docs/workflow/verification.md`.
|
||||||
|
|
||||||
|
## Runtime Escalation
|
||||||
|
|
||||||
|
Real runtime checks are required when the claim depends on actual Electron, mpv, overlay, focus, window tracking, launch, or socket behavior. Run the relevant application flow when the environment supports it. Otherwise, report the missing runtime dependency and do not present cheaper checks as authoritative runtime validation.
|
||||||
|
|
||||||
|
## Pre-Handoff Checks
|
||||||
|
|
||||||
|
Before handoff, reconcile both questions:
|
||||||
|
|
||||||
|
1. Do behavior, defaults, flags, shortcuts, ports, APIs, architecture, or workflow changes require documentation updates?
|
||||||
|
2. Does the change require a current-outcome fragment under `changes/` according to `changes/README.md`?
|
||||||
|
|
||||||
|
Complete required updates before handoff or report the blocker.
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
REPO_ROOT=$(cd "$SCRIPT_DIR/../../../.." && pwd)
|
|
||||||
TARGET="$REPO_ROOT/plugins/subminer-workflow/skills/subminer-change-verification/scripts/classify_subminer_diff.sh"
|
|
||||||
|
|
||||||
if [[ ! -x "$TARGET" ]]; then
|
|
||||||
echo "Missing canonical script: $TARGET" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$TARGET" "$@"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
REPO_ROOT=$(cd "$SCRIPT_DIR/../../../.." && pwd)
|
|
||||||
TARGET="$REPO_ROOT/plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh"
|
|
||||||
|
|
||||||
if [[ ! -x "$TARGET" ]]; then
|
|
||||||
echo "Missing canonical script: $TARGET" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$TARGET" "$@"
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: subminer-release
|
||||||
|
description: Prepare, cut, publish, or repair SubMiner stable and prerelease releases. Use for hands-on release work; do not use for general release questions.
|
||||||
|
---
|
||||||
|
|
||||||
|
# SubMiner release
|
||||||
|
|
||||||
|
Carry out the requested release phase using the repository's current release process.
|
||||||
|
|
||||||
|
## Source of truth
|
||||||
|
|
||||||
|
Read `docs/RELEASING.md` completely before changing files or release state. Treat it as canonical. Read `changes/README.md` when the work touches change fragments or generated release notes.
|
||||||
|
|
||||||
|
Do not copy release commands or policy into this skill. If this skill disagrees with the release guide, follow the guide and reconcile the skill before handoff.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Identify whether the request is for a stable release, prerelease, release preparation, publication, or repair.
|
||||||
|
2. Inspect the current branch, worktree status, package version, pending change fragments, relevant tags, and latest CI state before making changes.
|
||||||
|
3. Follow the matching procedure in `docs/RELEASING.md` in order. Review generated changelog and release-note Markdown before it can be committed or published.
|
||||||
|
4. Run every required gate for the requested release phase. Do not treat a cheaper test lane as a substitute for the documented release gate.
|
||||||
|
5. Before a stable tag, confirm the package and tag versions match and no pending `changes/*.md` fragments remain. Preserve fragments for prereleases as documented.
|
||||||
|
6. Report the resulting version, completed checks, local commit and tag state, remote publication state, skipped platform checks, and any remaining manual work.
|
||||||
|
|
||||||
|
## Authorization boundaries
|
||||||
|
|
||||||
|
- A request to prepare a release stops before commit, tag, push, or remote publication unless the user also authorizes those actions.
|
||||||
|
- A clear request to cut or publish a release includes the documented commit, tag, and push steps. Ask before the first remote mutation when the wording is ambiguous.
|
||||||
|
- Do not edit an existing GitHub release, publish to the AUR, change secrets, or alter signing configuration unless the user explicitly requests that operation.
|
||||||
|
- Do not switch branches without consent.
|
||||||
|
|
||||||
|
## Stop conditions
|
||||||
|
|
||||||
|
Stop and report the blocker when required CI or a release gate fails, authentication is missing, versions disagree, required artifacts are absent, or the worktree contains unexpected changes that overlap the release. Do not tag or publish a partially verified release.
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
name: 'subminer-scrum-master'
|
|
||||||
description: 'Compatibility shim. Canonical SubMiner scrum-master workflow now lives in the repo-local subminer-workflow plugin.'
|
|
||||||
---
|
|
||||||
|
|
||||||
# Compatibility Shim
|
|
||||||
|
|
||||||
Canonical source:
|
|
||||||
|
|
||||||
- `plugins/subminer-workflow/skills/subminer-scrum-master/SKILL.md`
|
|
||||||
|
|
||||||
When this shim is invoked:
|
|
||||||
|
|
||||||
1. Read the canonical plugin-owned skill.
|
|
||||||
2. Follow the plugin-owned skill as the source of truth.
|
|
||||||
3. Do not duplicate workflow changes here; update the plugin-owned skill instead.
|
|
||||||
|
|
||||||
This shim exists so existing repo references and prompts keep resolving during the migration to the repo-local plugin workflow.
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [ksyasuda]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: sudacode
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report something that is broken or behaving incorrectly
|
||||||
|
title: '[Bug]: '
|
||||||
|
labels: ['bug']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to file a bug report! Please search [existing issues](https://github.com/ksyasuda/SubMiner/issues?q=is%3Aissue) first to avoid duplicates.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: A clear description of the bug, including what you expected to happen instead.
|
||||||
|
placeholder: When I open the Yomitan popup, the overlay freezes...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Minimal, ordered steps that reliably trigger the bug.
|
||||||
|
placeholder: |
|
||||||
|
1. Launch `subminer`
|
||||||
|
2. Play a video in MPV
|
||||||
|
3. Hover a word and press ...
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: area
|
||||||
|
attributes:
|
||||||
|
label: Affected area
|
||||||
|
description: Which part of SubMiner is affected?
|
||||||
|
options:
|
||||||
|
- Overlay / Yomitan popup
|
||||||
|
- Anki mining
|
||||||
|
- Subtitle annotations
|
||||||
|
- Subtitle sidebar
|
||||||
|
- Immersion tracking / stats
|
||||||
|
- Launcher / CLI
|
||||||
|
- MPV plugin
|
||||||
|
- Jellyfin integration
|
||||||
|
- Jimaku integration
|
||||||
|
- AniList integration
|
||||||
|
- YouTube integration
|
||||||
|
- Character dictionary
|
||||||
|
- WebSocket / texthooker API
|
||||||
|
- Configuration
|
||||||
|
- Documentation
|
||||||
|
- Other / not sure
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
options:
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: SubMiner version
|
||||||
|
description: Output of `subminer --version`, or the release tag / commit you are running.
|
||||||
|
placeholder: v0.15.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: compositor
|
||||||
|
attributes:
|
||||||
|
label: Compositor (Linux only)
|
||||||
|
description: SubMiner's overlay supports Hyprland and sway. Name yours (and version if known). Leave blank on macOS / Windows.
|
||||||
|
placeholder: Hyprland 0.55
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: mpv-version
|
||||||
|
attributes:
|
||||||
|
label: MPV version
|
||||||
|
description: Output of `mpv --version` (first line).
|
||||||
|
placeholder: mpv 0.38.0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Logs / console output
|
||||||
|
description: |
|
||||||
|
Relevant logs. For verbose output, run `electron . --dev --log-level debug`.
|
||||||
|
This will be rendered as code automatically — no backticks needed.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Documentation
|
||||||
|
url: https://docs.subminer.moe
|
||||||
|
about: Setup, configuration, and feature docs — check here before filing an issue.
|
||||||
|
- name: Troubleshooting guide
|
||||||
|
url: https://docs.subminer.moe/troubleshooting
|
||||||
|
about: Common problems and fixes (Hyprland rules, MPV detection, Anki connection, etc.).
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature or an improvement to an existing one
|
||||||
|
title: '[Feature]: '
|
||||||
|
labels: ['enhancement']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for the idea! Please search [existing issues](https://github.com/ksyasuda/SubMiner/issues?q=is%3Aissue) first to avoid duplicates.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem / motivation
|
||||||
|
description: What problem are you trying to solve? What is missing or frustrating today?
|
||||||
|
placeholder: When mining a card I have to manually switch to Anki because...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposal
|
||||||
|
attributes:
|
||||||
|
label: Proposed solution
|
||||||
|
description: Describe the feature or change you'd like to see.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: area
|
||||||
|
attributes:
|
||||||
|
label: Related area
|
||||||
|
description: Which part of SubMiner does this relate to?
|
||||||
|
options:
|
||||||
|
- Overlay / Yomitan popup
|
||||||
|
- Anki mining
|
||||||
|
- Subtitle annotations
|
||||||
|
- Subtitle sidebar
|
||||||
|
- Immersion tracking / stats
|
||||||
|
- Launcher / CLI
|
||||||
|
- MPV plugin
|
||||||
|
- Jellyfin integration
|
||||||
|
- Jimaku integration
|
||||||
|
- AniList integration
|
||||||
|
- YouTube integration
|
||||||
|
- Character dictionary
|
||||||
|
- WebSocket / texthooker API
|
||||||
|
- Configuration
|
||||||
|
- Documentation
|
||||||
|
- Other / new area
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives considered
|
||||||
|
description: Any workarounds you currently use or other approaches you've thought about.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -1,3 +1,36 @@
|
|||||||
|
<!--
|
||||||
|
Thanks for contributing to SubMiner! Fill out the sections below.
|
||||||
|
Keep it short — a couple of sentences per section is fine.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- What does this PR do and why? -->
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
<!-- Check all that apply. -->
|
||||||
|
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature
|
||||||
|
- [ ] Refactor / internal
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] Other
|
||||||
|
|
||||||
|
## Related issues
|
||||||
|
|
||||||
|
<!-- e.g. "Closes #123". Delete if none. -->
|
||||||
|
|
||||||
|
## How was this tested?
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe verification. The default handoff gate is:
|
||||||
|
bun run typecheck && bun run test:fast && bun run test:env && bun run build && bun run test:smoke:dist
|
||||||
|
If docs-site/ changed, also: bun run docs:test && bun run docs:build
|
||||||
|
-->
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] Added a changelog fragment in `changes/`, or this PR is labeled `skip-changelog`
|
- [ ] Reconciled current-outcome changelog fragment(s), or this PR is labeled `skip-changelog` (see [`changes/README.md`](../changes/README.md))
|
||||||
|
- [ ] Docs updated in the same PR if behavior, defaults, flags, shortcuts, ports, or APIs changed
|
||||||
|
- [ ] Relevant checks pass locally (typecheck, tests, build)
|
||||||
@@ -8,98 +8,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-test-audit:
|
build-test-audit:
|
||||||
runs-on: ubuntu-latest
|
uses: ./.github/workflows/quality-gate.yml
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Lint changelog fragments
|
|
||||||
run: bun run changelog:lint
|
|
||||||
|
|
||||||
- name: Lint stats (formatting)
|
|
||||||
run: bun run lint:stats
|
|
||||||
|
|
||||||
- name: Enforce pull request changelog fragments (`skip-changelog` label bypass)
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
run: bun run changelog:pr-check --base-ref "origin/${{ github.base_ref }}" --head-ref "HEAD" --labels "${{ join(github.event.pull_request.labels.*.name, ',') }}"
|
|
||||||
|
|
||||||
- name: Build (TypeScript check)
|
|
||||||
# Keep explicit typecheck for fast fail before full build/bundle.
|
|
||||||
run: bun run typecheck
|
|
||||||
|
|
||||||
- name: Verify generated config examples
|
|
||||||
run: bun run verify:config-example
|
|
||||||
|
|
||||||
- name: Internal docs knowledge-base checks
|
|
||||||
run: bun run test:docs:kb
|
|
||||||
|
|
||||||
- name: Test suite (source)
|
|
||||||
run: bun run test:fast
|
|
||||||
|
|
||||||
- name: Coverage suite (maintained source lane)
|
|
||||||
run: bun run test:coverage:src
|
|
||||||
|
|
||||||
- name: Upload coverage artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: coverage-test-src
|
|
||||||
path: coverage/test-src/lcov.info
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
|
||||||
run: bun run test:launcher:smoke:src
|
|
||||||
|
|
||||||
- name: Upload launcher smoke artifacts (on failure)
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: launcher-smoke
|
|
||||||
path: .tmp/launcher-smoke/**
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
- name: Build (bundle)
|
|
||||||
run: bun run build
|
|
||||||
|
|
||||||
- name: Immersion SQLite verification
|
|
||||||
run: bun run test:immersion:sqlite:dist
|
|
||||||
|
|
||||||
- name: Dist smoke suite
|
|
||||||
run: bun run test:smoke:dist
|
|
||||||
|
|
||||||
- name: Security audit
|
|
||||||
run: bun audit --audit-level high
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Build Bun subminer wrapper
|
|
||||||
run: make build-launcher
|
|
||||||
|
|
||||||
- name: Verify Bun subminer wrapper
|
|
||||||
run: dist/launcher/subminer --help >/dev/null
|
|
||||||
|
|
||||||
- name: Enforce generated launcher workflow
|
|
||||||
run: bash scripts/verify-generated-launcher.sh
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: Docs Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
rebuild_archives:
|
||||||
|
description: 'Stable tags to rebuild in R2 (comma-separated, or "all"); archives are otherwise built once'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
paths:
|
||||||
|
- 'docs-site/**'
|
||||||
|
- 'scripts/docs-versioning.ts'
|
||||||
|
- 'scripts/docs-versioned-assets.ts'
|
||||||
|
- 'scripts/docs-archive-store.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'
|
||||||
|
env:
|
||||||
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "::notice::Skipping non-stable docs tag $TAG_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: Test docs
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
run: bun run docs:test
|
||||||
|
|
||||||
|
# Builds only archives missing from R2 (plus any requested rebuilds), then the
|
||||||
|
# root and /main/ builds that make up the Pages deployment.
|
||||||
|
- name: Build versioned docs
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
DOCS_ARCHIVE_R2_ACCESS_KEY_ID: ${{ secrets.DOCS_ARCHIVE_R2_ACCESS_KEY_ID }}
|
||||||
|
DOCS_ARCHIVE_R2_SECRET_ACCESS_KEY: ${{ secrets.DOCS_ARCHIVE_R2_SECRET_ACCESS_KEY }}
|
||||||
|
DOCS_ARCHIVE_R2_BUCKET: ${{ vars.DOCS_ARCHIVE_R2_BUCKET }}
|
||||||
|
REBUILD_ARCHIVES: ${{ inputs.rebuild_archives }}
|
||||||
|
run: bun run scripts/build-versioned-docs.ts --require-archives "--rebuild-archives=${REBUILD_ARCHIVES}"
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
|
# Run from docs-site so Wrangler bundles docs-site/functions (the /v/* archive server).
|
||||||
|
workingDirectory: docs-site
|
||||||
|
command: pages deploy ../.tmp/docs-versioned-site --project-name "${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME }}" --branch main
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
name: Package release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
secrets:
|
||||||
|
CSC_LINK:
|
||||||
|
required: true
|
||||||
|
CSC_KEY_PASSWORD:
|
||||||
|
required: true
|
||||||
|
APPLE_ID:
|
||||||
|
required: true
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD:
|
||||||
|
required: true
|
||||||
|
APPLE_TEAM_ID:
|
||||||
|
required: true
|
||||||
|
# Project TMDB key baked into release artifacts; builds stay valid without it.
|
||||||
|
SUBMINER_TMDB_API_KEY:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/texthooker-ui/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build texthooker-ui
|
||||||
|
run: |
|
||||||
|
cd vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Build AppImage
|
||||||
|
run: bun run build:appimage
|
||||||
|
env:
|
||||||
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
|
|
||||||
|
- name: Build unversioned AppImage
|
||||||
|
run: |
|
||||||
|
shopt -s nullglob
|
||||||
|
appimages=(release/SubMiner-*.AppImage)
|
||||||
|
if [ "${#appimages[@]}" -eq 0 ]; then
|
||||||
|
echo "No versioned AppImage found to create unversioned artifact."
|
||||||
|
ls -la release
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp "${appimages[0]}" release/SubMiner.AppImage
|
||||||
|
|
||||||
|
- name: Smoke packaged runtime assets
|
||||||
|
shell: bash
|
||||||
|
run: xvfb-run -a bun run test:package "release/linux-unpacked/resources"
|
||||||
|
|
||||||
|
- name: Upload AppImage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: appimage
|
||||||
|
path: |
|
||||||
|
release/*.AppImage
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/texthooker-ui/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Validate macOS signing/notarization secrets
|
||||||
|
run: |
|
||||||
|
missing=0
|
||||||
|
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
|
||||||
|
if [ -z "${!name}" ]; then
|
||||||
|
echo "Missing required secret: $name"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$missing" -ne 0 ]; then
|
||||||
|
echo "Set all required macOS signing/notarization secrets and rerun."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build texthooker-ui
|
||||||
|
run: |
|
||||||
|
cd vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Build signed + notarized macOS artifacts
|
||||||
|
run: bun run build:mac
|
||||||
|
env:
|
||||||
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|
||||||
|
- name: Smoke packaged runtime assets
|
||||||
|
shell: bash
|
||||||
|
run: bun run test:package "release/mac-arm64/SubMiner.app/Contents/Resources"
|
||||||
|
|
||||||
|
- name: Upload macOS artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos
|
||||||
|
path: |
|
||||||
|
release/*.dmg
|
||||||
|
release/*.zip
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/texthooker-ui/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build texthooker-ui
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Set-Location vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Verify managed Windows launcher
|
||||||
|
run: bun test src/main/runtime/managed-launcher.test.ts
|
||||||
|
|
||||||
|
- name: Verify Windows launcher bootstrap
|
||||||
|
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts
|
||||||
|
|
||||||
|
- name: Build unsigned Windows artifacts
|
||||||
|
run: bun run build:win:unsigned
|
||||||
|
env:
|
||||||
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
|
|
||||||
|
- name: Smoke packaged runtime assets
|
||||||
|
shell: bash
|
||||||
|
run: bun run test:package "release/win-unpacked/resources"
|
||||||
|
|
||||||
|
- name: Upload Windows artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows
|
||||||
|
path: |
|
||||||
|
release/*.exe
|
||||||
|
release/*.zip
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
if-no-files-found: error
|
||||||
@@ -12,279 +12,25 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
quality-gate:
|
quality-gate:
|
||||||
runs-on: ubuntu-latest
|
permissions:
|
||||||
steps:
|
contents: read
|
||||||
- name: Checkout
|
uses: ./.github/workflows/quality-gate.yml
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
package:
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Lint stats (formatting)
|
|
||||||
run: bun run lint:stats
|
|
||||||
|
|
||||||
- name: Build (TypeScript check)
|
|
||||||
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)
|
|
||||||
run: bun run test:fast
|
|
||||||
|
|
||||||
- name: Environment suite
|
|
||||||
run: bun run test:env
|
|
||||||
|
|
||||||
- name: Coverage suite (maintained source lane)
|
|
||||||
run: bun run test:coverage:src
|
|
||||||
|
|
||||||
- name: Upload coverage artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: coverage-test-src
|
|
||||||
path: coverage/test-src/lcov.info
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
|
||||||
run: bun run test:launcher:smoke:src
|
|
||||||
|
|
||||||
- name: Upload launcher smoke artifacts (on failure)
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: launcher-smoke
|
|
||||||
path: .tmp/launcher-smoke/**
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
- name: Build (bundle)
|
|
||||||
run: bun run build
|
|
||||||
|
|
||||||
- name: Immersion SQLite verification
|
|
||||||
run: bun run test:immersion:sqlite:dist
|
|
||||||
|
|
||||||
- name: Dist smoke suite
|
|
||||||
run: bun run test:smoke:dist
|
|
||||||
|
|
||||||
build-linux:
|
|
||||||
needs: [quality-gate]
|
needs: [quality-gate]
|
||||||
runs-on: ubuntu-latest
|
permissions:
|
||||||
steps:
|
contents: read
|
||||||
- name: Checkout
|
uses: ./.github/workflows/package-release.yml
|
||||||
uses: actions/checkout@v4
|
secrets:
|
||||||
with:
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
submodules: true
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
- name: Setup Bun
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
uses: oven-sh/setup-bun@v2
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build AppImage
|
|
||||||
run: bun run build:appimage
|
|
||||||
|
|
||||||
- name: Build unversioned AppImage
|
|
||||||
run: |
|
|
||||||
shopt -s nullglob
|
|
||||||
appimages=(release/SubMiner-*.AppImage)
|
|
||||||
if [ "${#appimages[@]}" -eq 0 ]; then
|
|
||||||
echo "No versioned AppImage found to create unversioned artifact."
|
|
||||||
ls -la release
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
cp "${appimages[0]}" release/SubMiner.AppImage
|
|
||||||
|
|
||||||
- name: Upload AppImage artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: appimage
|
|
||||||
path: |
|
|
||||||
release/*.AppImage
|
|
||||||
release/*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build-macos:
|
|
||||||
needs: [quality-gate]
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-bun-
|
|
||||||
|
|
||||||
- name: Validate macOS signing/notarization secrets
|
|
||||||
run: |
|
|
||||||
missing=0
|
|
||||||
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
|
|
||||||
if [ -z "${!name}" ]; then
|
|
||||||
echo "Missing required secret: $name"
|
|
||||||
missing=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$missing" -ne 0 ]; then
|
|
||||||
echo "Set all required macOS signing/notarization secrets and rerun."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build signed + notarized macOS artifacts
|
|
||||||
run: bun run build:mac
|
|
||||||
env:
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Upload macOS artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: macos
|
|
||||||
path: |
|
|
||||||
release/*.dmg
|
|
||||||
release/*.zip
|
|
||||||
release/*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build-windows:
|
|
||||||
needs: [quality-gate]
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
Set-Location vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build unsigned Windows artifacts
|
|
||||||
run: bun run build:win:unsigned
|
|
||||||
|
|
||||||
- name: Upload Windows artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows
|
|
||||||
path: |
|
|
||||||
release/*.exe
|
|
||||||
release/*.zip
|
|
||||||
release/*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build-linux, build-macos, build-windows]
|
needs: [package]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -330,11 +76,11 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build Bun subminer wrapper
|
- name: Build launcher runtime artifacts
|
||||||
run: make build-launcher
|
run: make build-launcher
|
||||||
|
|
||||||
- name: Verify Bun subminer wrapper
|
- name: Smoke launcher bundle
|
||||||
run: dist/launcher/subminer --help >/dev/null
|
run: bun dist/launcher/subminer.js --help >/dev/null
|
||||||
|
|
||||||
- name: Enforce generated launcher workflow
|
- name: Enforce generated launcher workflow
|
||||||
run: bash scripts/verify-generated-launcher.sh
|
run: bash scripts/verify-generated-launcher.sh
|
||||||
@@ -348,12 +94,17 @@ jobs:
|
|||||||
config.example.jsonc \
|
config.example.jsonc \
|
||||||
plugin/subminer \
|
plugin/subminer \
|
||||||
plugin/subminer.conf \
|
plugin/subminer.conf \
|
||||||
assets/themes/subminer.rasi
|
assets/themes/subminer.rasi \
|
||||||
|
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer \
|
||||||
|
resources/bun/licenses
|
||||||
|
|
||||||
|
- name: Package Bun corresponding source
|
||||||
|
run: bun scripts/package-bun-source.mjs
|
||||||
|
|
||||||
- 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 release/*.yml release/*.blockmap dist/launcher/subminer)
|
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||||
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
|
||||||
@@ -370,15 +121,22 @@ jobs:
|
|||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Verify committed prerelease notes
|
- name: Verify committed prerelease notes
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
if [ ! -s release/prerelease-notes.md ]; then
|
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."
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if ! bun run changelog:check-prerelease-notes --version "$RELEASE_VERSION"; then
|
||||||
|
echo "::error::release/prerelease-notes.md was not generated for $RELEASE_VERSION. Rerun 'bun run changelog:prerelease-notes --version $RELEASE_VERSION' locally, commit, and retag."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Publish Prerelease
|
- name: Publish Prerelease
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -389,10 +147,12 @@ jobs:
|
|||||||
release/*.exe
|
release/*.exe
|
||||||
release/*.zip
|
release/*.zip
|
||||||
release/*.tar.gz
|
release/*.tar.gz
|
||||||
release/*.yml
|
release/*.tar.gz.sha256
|
||||||
|
release/latest*.yml
|
||||||
release/*.blockmap
|
release/*.blockmap
|
||||||
release/SHA256SUMS.txt
|
release/SHA256SUMS.txt
|
||||||
dist/launcher/subminer
|
dist/launcher/subminer
|
||||||
|
dist/launcher/subminer.cmd
|
||||||
)
|
)
|
||||||
|
|
||||||
if [ "${#artifacts[@]}" -eq 0 ]; then
|
if [ "${#artifacts[@]}" -eq 0 ]; then
|
||||||
@@ -400,27 +160,27 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if gh release view "${{ steps.version.outputs.VERSION }}" >/dev/null 2>&1; then
|
if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then
|
||||||
gh release edit "${{ steps.version.outputs.VERSION }}" \
|
gh release edit "$RELEASE_VERSION" \
|
||||||
--draft \
|
--draft \
|
||||||
--prerelease \
|
--prerelease \
|
||||||
--title "${{ steps.version.outputs.VERSION }}" \
|
--title "$RELEASE_VERSION" \
|
||||||
--notes-file release/prerelease-notes.md
|
--notes-file release/prerelease-notes.md
|
||||||
else
|
else
|
||||||
gh release create "${{ steps.version.outputs.VERSION }}" \
|
gh release create "$RELEASE_VERSION" \
|
||||||
--draft \
|
--draft \
|
||||||
--latest=false \
|
--latest=false \
|
||||||
--prerelease \
|
--prerelease \
|
||||||
--title "${{ steps.version.outputs.VERSION }}" \
|
--title "$RELEASE_VERSION" \
|
||||||
--notes-file release/prerelease-notes.md
|
--notes-file release/prerelease-notes.md
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for asset in "${artifacts[@]}"; do
|
for asset in "${artifacts[@]}"; do
|
||||||
gh release upload "${{ steps.version.outputs.VERSION }}" "$asset" --clobber
|
gh release upload "$RELEASE_VERSION" "$asset" --clobber
|
||||||
done
|
done
|
||||||
|
|
||||||
gh release edit "${{ steps.version.outputs.VERSION }}" \
|
gh release edit "$RELEASE_VERSION" \
|
||||||
--draft=false \
|
--draft=false \
|
||||||
--prerelease \
|
--prerelease \
|
||||||
--title "${{ steps.version.outputs.VERSION }}" \
|
--title "$RELEASE_VERSION" \
|
||||||
--notes-file release/prerelease-notes.md
|
--notes-file release/prerelease-notes.md
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
name: Quality Gate
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
launcher-runtime:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Verify native runtime staging
|
||||||
|
run: bun test src/main/runtime/managed-launcher.test.ts
|
||||||
|
|
||||||
|
- name: Verify Windows launcher bootstrap
|
||||||
|
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts
|
||||||
|
|
||||||
|
- name: Verify native mpv process launch
|
||||||
|
run: bun test src/main/runtime/mpv-process.test.ts
|
||||||
|
|
||||||
|
- name: Verify POSIX launcher bootstrap
|
||||||
|
run: bun test src/main/runtime/posix-launcher-bootstrap.test.ts
|
||||||
|
|
||||||
|
quality-gate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint changelog fragments
|
||||||
|
run: bun run changelog:lint
|
||||||
|
|
||||||
|
- name: Lint stats (formatting)
|
||||||
|
run: bun run lint:stats
|
||||||
|
|
||||||
|
- name: Enforce pull request changelog fragments (`skip-changelog` label bypass)
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
BASE_REF: ${{ github.base_ref }}
|
||||||
|
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
|
||||||
|
run: bun run changelog:pr-check --base-ref "origin/$BASE_REF" --head-ref "HEAD" --labels "$PR_LABELS"
|
||||||
|
|
||||||
|
- name: Build (TypeScript check)
|
||||||
|
run: bun run typecheck
|
||||||
|
|
||||||
|
- name: Verify generated config examples
|
||||||
|
run: bun run verify:config-example
|
||||||
|
|
||||||
|
- name: Install Lua
|
||||||
|
run: |
|
||||||
|
# Lua needs only Ubuntu sources; unrelated runner repositories can be unavailable.
|
||||||
|
test -f /etc/apt/sources.list.d/ubuntu.sources
|
||||||
|
apt_sources=(-o Dir::Etc::sourcelist=sources.list.d/ubuntu.sources -o Dir::Etc::sourceparts=-)
|
||||||
|
sudo apt-get "${apt_sources[@]}" update
|
||||||
|
sudo apt-get "${apt_sources[@]}" install -y lua5.4
|
||||||
|
sudo ln -sf /usr/bin/lua5.4 /usr/local/bin/lua
|
||||||
|
lua -v
|
||||||
|
|
||||||
|
- name: Launcher unit and script suites
|
||||||
|
run: bun run test:launcher:unit:src && bun run test:scripts
|
||||||
|
|
||||||
|
- name: Environment suite
|
||||||
|
run: bun run test:env
|
||||||
|
|
||||||
|
- name: Upload launcher smoke artifacts (on failure)
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: launcher-smoke
|
||||||
|
path: .tmp/launcher-smoke/**
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
- name: Coverage suite (maintained source lane)
|
||||||
|
run: bun run test:coverage:src
|
||||||
|
|
||||||
|
- name: Upload coverage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-test-src
|
||||||
|
path: coverage/test-src/lcov.info
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Stats UI tests
|
||||||
|
run: bun run test:stats
|
||||||
|
|
||||||
|
- name: Build (bundle)
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: Immersion SQLite verification
|
||||||
|
run: bun run test:immersion:sqlite:dist
|
||||||
|
|
||||||
|
- name: Dist smoke suite
|
||||||
|
run: bun run test:smoke:dist
|
||||||
|
|
||||||
|
- name: Security audit
|
||||||
|
run: bun audit --audit-level high
|
||||||
|
|
||||||
|
- name: Build launcher runtime artifacts
|
||||||
|
run: make build-launcher
|
||||||
|
|
||||||
|
- name: Smoke launcher bundle
|
||||||
|
run: bun dist/launcher/subminer.js --help >/dev/null
|
||||||
|
|
||||||
|
- name: Enforce generated launcher workflow
|
||||||
|
run: bash scripts/verify-generated-launcher.sh
|
||||||
@@ -13,267 +13,25 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
quality-gate:
|
quality-gate:
|
||||||
runs-on: ubuntu-latest
|
permissions:
|
||||||
steps:
|
contents: read
|
||||||
- name: Checkout
|
uses: ./.github/workflows/quality-gate.yml
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
package:
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Lint stats (formatting)
|
|
||||||
run: bun run lint:stats
|
|
||||||
|
|
||||||
- name: Build (TypeScript check)
|
|
||||||
run: bun run typecheck
|
|
||||||
|
|
||||||
- name: Test suite (source)
|
|
||||||
run: bun run test:fast
|
|
||||||
|
|
||||||
- name: Coverage suite (maintained source lane)
|
|
||||||
run: bun run test:coverage:src
|
|
||||||
|
|
||||||
- name: Upload coverage artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: coverage-test-src
|
|
||||||
path: coverage/test-src/lcov.info
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
|
||||||
run: bun run test:launcher:smoke:src
|
|
||||||
|
|
||||||
- name: Upload launcher smoke artifacts (on failure)
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: launcher-smoke
|
|
||||||
path: .tmp/launcher-smoke/**
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
- name: Build (bundle)
|
|
||||||
run: bun run build
|
|
||||||
|
|
||||||
- name: Immersion SQLite verification
|
|
||||||
run: bun run test:immersion:sqlite:dist
|
|
||||||
|
|
||||||
- name: Dist smoke suite
|
|
||||||
run: bun run test:smoke:dist
|
|
||||||
|
|
||||||
build-linux:
|
|
||||||
needs: [quality-gate]
|
needs: [quality-gate]
|
||||||
runs-on: ubuntu-latest
|
permissions:
|
||||||
steps:
|
contents: read
|
||||||
- name: Checkout
|
uses: ./.github/workflows/package-release.yml
|
||||||
uses: actions/checkout@v4
|
secrets:
|
||||||
with:
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
submodules: true
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
- name: Setup Bun
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
uses: oven-sh/setup-bun@v2
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build AppImage
|
|
||||||
run: bun run build:appimage
|
|
||||||
|
|
||||||
- name: Build unversioned AppImage
|
|
||||||
run: |
|
|
||||||
shopt -s nullglob
|
|
||||||
appimages=(release/SubMiner-*.AppImage)
|
|
||||||
if [ "${#appimages[@]}" -eq 0 ]; then
|
|
||||||
echo "No versioned AppImage found to create unversioned artifact."
|
|
||||||
ls -la release
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
cp "${appimages[0]}" release/SubMiner.AppImage
|
|
||||||
|
|
||||||
- name: Upload AppImage artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: appimage
|
|
||||||
path: |
|
|
||||||
release/*.AppImage
|
|
||||||
release/*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
|
|
||||||
build-macos:
|
|
||||||
needs: [quality-gate]
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Validate macOS signing/notarization secrets
|
|
||||||
run: |
|
|
||||||
missing=0
|
|
||||||
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
|
|
||||||
if [ -z "${!name}" ]; then
|
|
||||||
echo "Missing required secret: $name"
|
|
||||||
missing=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$missing" -ne 0 ]; then
|
|
||||||
echo "Set all required macOS signing/notarization secrets and rerun."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build signed + notarized macOS artifacts
|
|
||||||
run: bun run build:mac
|
|
||||||
env:
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Upload macOS artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: macos
|
|
||||||
path: |
|
|
||||||
release/*.dmg
|
|
||||||
release/*.zip
|
|
||||||
release/*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
|
|
||||||
build-windows:
|
|
||||||
needs: [quality-gate]
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
Set-Location vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build unsigned Windows artifacts
|
|
||||||
run: bun run build:win:unsigned
|
|
||||||
|
|
||||||
- name: Upload Windows artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows
|
|
||||||
path: |
|
|
||||||
release/*.exe
|
|
||||||
release/*.zip
|
|
||||||
release/*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build-linux, build-macos, build-windows]
|
needs: [package]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -319,11 +77,11 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build Bun subminer wrapper
|
- name: Build launcher runtime artifacts
|
||||||
run: make build-launcher
|
run: make build-launcher
|
||||||
|
|
||||||
- name: Verify Bun subminer wrapper
|
- name: Smoke launcher bundle
|
||||||
run: dist/launcher/subminer --help >/dev/null
|
run: bun dist/launcher/subminer.js --help >/dev/null
|
||||||
|
|
||||||
- name: Enforce generated launcher workflow
|
- name: Enforce generated launcher workflow
|
||||||
run: bash scripts/verify-generated-launcher.sh
|
run: bash scripts/verify-generated-launcher.sh
|
||||||
@@ -337,12 +95,17 @@ jobs:
|
|||||||
config.example.jsonc \
|
config.example.jsonc \
|
||||||
plugin/subminer \
|
plugin/subminer \
|
||||||
plugin/subminer.conf \
|
plugin/subminer.conf \
|
||||||
assets/themes/subminer.rasi
|
assets/themes/subminer.rasi \
|
||||||
|
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer \
|
||||||
|
resources/bun/licenses
|
||||||
|
|
||||||
|
- name: Package Bun corresponding source
|
||||||
|
run: bun scripts/package-bun-source.mjs
|
||||||
|
|
||||||
- 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 release/*.yml release/*.blockmap dist/launcher/subminer)
|
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||||
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
|
||||||
@@ -359,33 +122,40 @@ jobs:
|
|||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Guard against pending changelog fragments
|
- name: Guard against pending changelog fragments
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
if find changes -maxdepth 1 -name '*.md' -not -name README.md -print -quit | grep -q .; then
|
if find changes -maxdepth 1 -name '*.md' -not -name README.md -print -quit | grep -q .; then
|
||||||
echo "::error::Pending changelog fragments detected. Run 'bun run changelog:build --version ${{ steps.version.outputs.VERSION }}' locally and commit the polished CHANGELOG.md before tagging. CI no longer auto-builds the changelog because the polish step requires the local 'claude' CLI."
|
echo "::error::Pending changelog fragments detected. Run 'bun run changelog:build --version $RELEASE_VERSION' locally and commit the polished CHANGELOG.md before tagging. CI no longer auto-builds the changelog because the polish step requires the local 'claude' CLI."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Verify changelog is ready for tagged release
|
- name: Verify changelog is ready for tagged release
|
||||||
run: bun run changelog:check --version "${{ steps.version.outputs.VERSION }}"
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
run: bun run changelog:check --version "$RELEASE_VERSION"
|
||||||
|
|
||||||
- name: Generate release notes from changelog
|
- name: Generate release notes from changelog
|
||||||
run: bun run changelog:release-notes --version "${{ steps.version.outputs.VERSION }}"
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
run: bun run changelog:release-notes --version "$RELEASE_VERSION"
|
||||||
|
|
||||||
- name: Publish Release
|
- name: Publish Release
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
if gh release view "${{ steps.version.outputs.VERSION }}" >/dev/null 2>&1; then
|
if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then
|
||||||
# Do not pass the prerelease flag here; gh defaults to a normal release.
|
# Do not pass the prerelease flag here; gh defaults to a normal release.
|
||||||
gh release edit "${{ steps.version.outputs.VERSION }}" \
|
gh release edit "$RELEASE_VERSION" \
|
||||||
--draft=false \
|
--draft=false \
|
||||||
--title "${{ steps.version.outputs.VERSION }}" \
|
--title "$RELEASE_VERSION" \
|
||||||
--notes-file release/release-notes.md
|
--notes-file release/release-notes.md
|
||||||
else
|
else
|
||||||
gh release create "${{ steps.version.outputs.VERSION }}" \
|
gh release create "$RELEASE_VERSION" \
|
||||||
--title "${{ steps.version.outputs.VERSION }}" \
|
--title "$RELEASE_VERSION" \
|
||||||
--notes-file release/release-notes.md
|
--notes-file release/release-notes.md
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -396,10 +166,12 @@ jobs:
|
|||||||
release/*.exe
|
release/*.exe
|
||||||
release/*.zip
|
release/*.zip
|
||||||
release/*.tar.gz
|
release/*.tar.gz
|
||||||
release/*.yml
|
release/*.tar.gz.sha256
|
||||||
|
release/latest*.yml
|
||||||
release/*.blockmap
|
release/*.blockmap
|
||||||
release/SHA256SUMS.txt
|
release/SHA256SUMS.txt
|
||||||
dist/launcher/subminer
|
dist/launcher/subminer
|
||||||
|
dist/launcher/subminer.cmd
|
||||||
)
|
)
|
||||||
|
|
||||||
if [ "${#artifacts[@]}" -eq 0 ]; then
|
if [ "${#artifacts[@]}" -eq 0 ]; then
|
||||||
@@ -408,7 +180,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
for asset in "${artifacts[@]}"; do
|
for asset in "${artifacts[@]}"; do
|
||||||
gh release upload "${{ steps.version.outputs.VERSION }}" "$asset" --clobber
|
gh release upload "$RELEASE_VERSION" "$asset" --clobber
|
||||||
done
|
done
|
||||||
|
|
||||||
aur-publish:
|
aur-publish:
|
||||||
@@ -481,39 +253,52 @@ jobs:
|
|||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Download release assets for AUR
|
- name: Download release assets for AUR
|
||||||
|
id: aur_assets
|
||||||
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
version="${{ steps.version.outputs.VERSION }}"
|
version="$RELEASE_VERSION"
|
||||||
install -dm755 .tmp/aur-release-assets
|
install -dm755 .tmp/aur-release-assets
|
||||||
gh release download "$version" \
|
for asset in "SubMiner-${version#v}.AppImage" subminer subminer-assets.tar.gz; do
|
||||||
--dir .tmp/aur-release-assets \
|
destination=".tmp/aur-release-assets/$asset"
|
||||||
--pattern "SubMiner-${version#v}.AppImage" \
|
if ! curl --fail --silent --show-error --location \
|
||||||
--pattern "subminer" \
|
--retry 3 --retry-delay 1 --retry-all-errors \
|
||||||
--pattern "subminer-assets.tar.gz"
|
--connect-timeout 30 --max-time 600 \
|
||||||
|
--output "$destination.partial" \
|
||||||
|
"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$version/$asset"; then
|
||||||
|
echo "::warning::Unable to download $asset after retries; skipping automated AUR publish."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
mv "$destination.partial" "$destination"
|
||||||
|
done
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Update AUR packaging metadata
|
- name: Update AUR packaging metadata
|
||||||
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
version_no_v="${{ steps.version.outputs.VERSION }}"
|
version_no_v="$RELEASE_VERSION"
|
||||||
version_no_v="${version_no_v#v}"
|
version_no_v="${version_no_v#v}"
|
||||||
cp packaging/aur/subminer-bin/PKGBUILD aur-subminer-bin/PKGBUILD
|
cp packaging/aur/subminer-bin/PKGBUILD aur-subminer-bin/PKGBUILD
|
||||||
cp packaging/aur/subminer-bin/.SRCINFO aur-subminer-bin/.SRCINFO
|
cp packaging/aur/subminer-bin/.SRCINFO aur-subminer-bin/.SRCINFO
|
||||||
bash scripts/update-aur-package.sh \
|
bash scripts/update-aur-package.sh \
|
||||||
--pkg-dir aur-subminer-bin \
|
--pkg-dir aur-subminer-bin \
|
||||||
--version "${{ steps.version.outputs.VERSION }}" \
|
--version "$RELEASE_VERSION" \
|
||||||
--appimage ".tmp/aur-release-assets/SubMiner-${version_no_v}.AppImage" \
|
--appimage ".tmp/aur-release-assets/SubMiner-${version_no_v}.AppImage" \
|
||||||
--wrapper ".tmp/aur-release-assets/subminer" \
|
--wrapper ".tmp/aur-release-assets/subminer" \
|
||||||
--assets ".tmp/aur-release-assets/subminer-assets.tar.gz"
|
--assets ".tmp/aur-release-assets/subminer-assets.tar.gz"
|
||||||
|
|
||||||
- name: Commit and push AUR update
|
- name: Commit and push AUR update
|
||||||
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true'
|
||||||
working-directory: aur-subminer-bin
|
working-directory: aur-subminer-bin
|
||||||
env:
|
env:
|
||||||
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes
|
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if git diff --quiet -- PKGBUILD .SRCINFO; then
|
if git diff --quiet -- PKGBUILD .SRCINFO; then
|
||||||
@@ -523,7 +308,7 @@ jobs:
|
|||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git add PKGBUILD .SRCINFO
|
git add PKGBUILD .SRCINFO
|
||||||
git commit -m "Update to ${{ steps.version.outputs.VERSION }}"
|
git commit -m "Update to $RELEASE_VERSION"
|
||||||
|
|
||||||
attempts=3
|
attempts=3
|
||||||
for attempt in $(seq 1 "$attempts"); do
|
for attempt in $(seq 1 "$attempts"); do
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
/package-lock.json
|
||||||
|
|
||||||
# Superpowers brainstorming
|
# Superpowers brainstorming
|
||||||
.superpowers/
|
.superpowers/
|
||||||
@@ -16,6 +17,8 @@ coverage/
|
|||||||
|
|
||||||
# Launcher build artifact (produced by make build-launcher)
|
# Launcher build artifact (produced by make build-launcher)
|
||||||
/subminer
|
/subminer
|
||||||
|
/main-entry.js
|
||||||
|
/main-entry.js.map
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
@@ -46,6 +49,7 @@ tests/*
|
|||||||
!.agents/skills/
|
!.agents/skills/
|
||||||
.agents/skills/*
|
.agents/skills/*
|
||||||
!.agents/skills/subminer-change-verification/
|
!.agents/skills/subminer-change-verification/
|
||||||
|
!.agents/skills/subminer-release/
|
||||||
!.agents/skills/subminer-scrum-master/
|
!.agents/skills/subminer-scrum-master/
|
||||||
.agents/skills/subminer-change-verification/*
|
.agents/skills/subminer-change-verification/*
|
||||||
!.agents/skills/subminer-change-verification/SKILL.md
|
!.agents/skills/subminer-change-verification/SKILL.md
|
||||||
@@ -53,8 +57,13 @@ tests/*
|
|||||||
.agents/skills/subminer-change-verification/scripts/*
|
.agents/skills/subminer-change-verification/scripts/*
|
||||||
!.agents/skills/subminer-change-verification/scripts/classify_subminer_diff.sh
|
!.agents/skills/subminer-change-verification/scripts/classify_subminer_diff.sh
|
||||||
!.agents/skills/subminer-change-verification/scripts/verify_subminer_change.sh
|
!.agents/skills/subminer-change-verification/scripts/verify_subminer_change.sh
|
||||||
|
.agents/skills/subminer-release/*
|
||||||
|
!.agents/skills/subminer-release/SKILL.md
|
||||||
.agents/skills/subminer-scrum-master/*
|
.agents/skills/subminer-scrum-master/*
|
||||||
!.agents/skills/subminer-scrum-master/SKILL.md
|
!.agents/skills/subminer-scrum-master/SKILL.md
|
||||||
favicon.png
|
favicon.png
|
||||||
.claude/*
|
.claude/*
|
||||||
!stats/public/favicon.png
|
!stats/public/favicon.png
|
||||||
|
|
||||||
|
# Browser-automation session artifacts (page snapshots, console logs, downloads)
|
||||||
|
.playwright-mcp/
|
||||||
@@ -4,3 +4,4 @@ release
|
|||||||
coverage
|
coverage
|
||||||
vendor
|
vendor
|
||||||
*.log
|
*.log
|
||||||
|
src/core/services/tokenizer/__fixtures__/golden/*.json
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# AGENTS.MD
|
# AGENTS.md
|
||||||
|
|
||||||
## Internal Docs
|
## Internal Docs
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@ Start here, then leave this file.
|
|||||||
|
|
||||||
`docs-site/` is user-facing. Do not treat it as the canonical internal source of truth.
|
`docs-site/` is user-facing. Do not treat it as the canonical internal source of truth.
|
||||||
|
|
||||||
|
`CLAUDE.md` is a symlink to this file; there is one project instruction file, not two.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
- Init workspace: `git submodule update --init --recursive`
|
- Init workspace: `git submodule update --init --recursive`
|
||||||
@@ -23,8 +25,9 @@ Start here, then leave this file.
|
|||||||
|
|
||||||
## Build / Test
|
## Build / Test
|
||||||
|
|
||||||
- Runtime/package manager: Bun (`packageManager: bun@1.3.5`)
|
- Runtime/package manager: Bun; use the version pinned by `package.json`.
|
||||||
- Default handoff gate:
|
- Follow [`docs/workflow/verification.md`](./docs/workflow/verification.md) and start with the cheapest sufficient lane.
|
||||||
|
- Full handoff gate for substantial changes:
|
||||||
`bun run typecheck`
|
`bun run typecheck`
|
||||||
`bun run test:fast`
|
`bun run test:fast`
|
||||||
`bun run test:env`
|
`bun run test:env`
|
||||||
@@ -40,7 +43,26 @@ Start here, then leave this file.
|
|||||||
- Config/schema/defaults: `bun run test:config`; if template/defaults changed, `bun run generate:config-example`
|
- Config/schema/defaults: `bun run test:config`; if template/defaults changed, `bun run generate:config-example`
|
||||||
- Launcher/plugin: `bun run test:launcher` or `bun run test:env`
|
- Launcher/plugin: `bun run test:launcher` or `bun run test:env`
|
||||||
- Runtime-compat / dist-sensitive: `bun run test:runtime:compat`
|
- Runtime-compat / dist-sensitive: `bun run test:runtime:compat`
|
||||||
- Docs-only: `bun run docs:test`, then `bun run docs:build`
|
- Stats dashboard UI (`stats/`): `bun run test:stats`
|
||||||
|
- Build/release scripts (`scripts/**`): `bun run test:scripts`
|
||||||
|
- Internal docs, `AGENTS.md`, or repo skills: `bun run test:docs:kb`
|
||||||
|
- User-facing `docs-site/`: `bun run docs:test`, then `bun run docs:build`
|
||||||
|
- macOS mpv window helper: `bun test scripts/get-mpv-window-macos.test.ts`
|
||||||
|
- Test lanes are directory-discovered via `scripts/test-lanes.ts`; never hand-list test files in `package.json`
|
||||||
|
|
||||||
|
## Docs Upkeep
|
||||||
|
|
||||||
|
- Docs ship with the change, not after. If a change alters behavior, defaults, flags, shortcuts, ports, or APIs, update the matching docs in the same PR. Touching code without reconciling its docs is an incomplete change.
|
||||||
|
- Source of truth for config defaults is the generated `config.example.jsonc`. Never write a default value into prose you didn't read from it, and don't restate the same default across multiple docs; cite/link to one place so there's a single thing to update.
|
||||||
|
- Trigger map (touch left → update right):
|
||||||
|
- `src/config/definitions/**` (schema/defaults/template) → `bun run generate:config-example`, then reconcile `docs-site/configuration.md` + any feature doc that cites that default
|
||||||
|
- shortcuts/keybindings (`shortcuts.*`, `keybindings`, `stats.*Key`, `subtitleSidebar.toggleKey`, controller bindings) → `docs-site/shortcuts.md`
|
||||||
|
- CLI flags/subcommands (`src/cli/args.ts`, `launcher/**`) → `docs-site/usage.md` + relevant integration doc
|
||||||
|
- feature behavior (anki / jellyfin / jimaku / anilist / youtube / immersion / stats / websocket / sidebar / character-dictionary / annotations) → matching `docs-site/<feature>.md`
|
||||||
|
- architecture / IPC / workflow / internal process → internal `docs/` (system of record)
|
||||||
|
- feature set / requirements / install flow → `README.md`
|
||||||
|
- Removing or renaming a config key: grep `docs-site/` and `docs/` for the old key and any value it documented; legacy/hidden keys (`LEGACY_HIDDEN_CONFIG_PATHS`) should not appear in user docs as current settings.
|
||||||
|
- Verify after doc edits: `bun run verify:config-example` (if config touched), `bun run docs:test`, `bun run docs:build`.
|
||||||
|
|
||||||
## Sensitive Files
|
## Sensitive Files
|
||||||
|
|
||||||
@@ -52,15 +74,10 @@ Start here, then leave this file.
|
|||||||
|
|
||||||
## Release / PR Notes
|
## Release / PR Notes
|
||||||
|
|
||||||
- User-visible PRs need one fragment in `changes/*.md`
|
- User-visible PRs need reconciled current-outcome fragment(s) in `changes/*.md`. Format and rules live in [`changes/README.md`](./changes/README.md) (`type` + `area` keys required; inspect existing same-PR fragments, then update/remove stale bullets or add only genuinely separate outcomes; apply the `skip-changelog` label to opt out).
|
||||||
|
- User-visible docs changes get a `type: docs` fragment
|
||||||
- CI enforces `bun run changelog:lint` and `bun run changelog:pr-check`
|
- CI enforces `bun run changelog:lint` and `bun run changelog:pr-check`
|
||||||
- PR review helpers:
|
- PR review helpers:
|
||||||
- `gh pr view --json number,title,url --jq '"PR #\\(.number): \\(.title)\\n\\(.url)"'`
|
- `gh pr view --json number,title --jq '"PR #\\(.number): \\(.title)"'`
|
||||||
- `gh api repos/:owner/:repo/pulls/<num>/comments --paginate`
|
- `gh api repos/:owner/:repo/pulls/<num>/comments --paginate`
|
||||||
|
- For CI debugging, inspect runs with `gh run list/view`; rerun or fix only within the requested scope.
|
||||||
## Runtime Notes
|
|
||||||
|
|
||||||
- Use Codex background for long jobs; tmux only when persistence/interaction is required
|
|
||||||
- CI red: `gh run list/view`, rerun, fix, repeat until green
|
|
||||||
- TypeScript: keep files small; follow existing patterns
|
|
||||||
- Swift: use workspace helper/daemon; validate `swift build` + tests
|
|
||||||
@@ -1,10 +1,613 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.20.0 (2026-09-23)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Japanese Subtitle Generation**:
|
||||||
|
- Generate Japanese SRT subtitles locally with whisper.cpp. Start it from a new modal (Ctrl+Shift+G), from the generate button in an empty subtitle sidebar, or with `subminer generate-subs`.
|
||||||
|
- Generation shows progress, can be cancelled, and loads the finished subtitles into mpv automatically.
|
||||||
|
- Pick an official multilingual Whisper model, including quantized variants, with size and accuracy guidance. You can download it in the app or point Settings at a model you already have.
|
||||||
|
- `large-v3-turbo` is recommended when CUDA support is detected, and `small` otherwise.
|
||||||
|
- whisper-cli, ffmpeg, and ffprobe are found on PATH unless you override them. SubMiner names any missing tools before a download starts.
|
||||||
|
- An optional "Focus on spoken dialogue" mode uses a separately downloaded Silero VAD model. It keeps audible sections it is unsure about, so dialogue under music is not dropped, but songs may also be transcribed.
|
||||||
|
- Long passages are split near speech starts or quiet pauses to reduce subtitles that appear too early. When an eligible embedded or external subtitle track is loaded in mpv, it guides the split points.
|
||||||
|
- Each passage runs in a fresh Whisper process, which prevents repeated-character output.
|
||||||
|
- **Subtitle Selection Modal**:
|
||||||
|
- An optional modal for choosing primary and secondary mpv subtitle tracks.
|
||||||
|
- Turn it on in Settings under Behavior, then press g followed by s to open it. Turning it off restores mpv's own subtitle selection binding.
|
||||||
|
- Single-key actions take priority over configured key sequence prefixes.
|
||||||
|
- Conflicting sequences are disabled with a warning, and the existing y commands stay reserved.
|
||||||
|
- **Subtitle Sidebar Copy**:
|
||||||
|
- Select dialogue across several sidebar rows and copy it without timestamps using Ctrl/Cmd+C or the Copy button.
|
||||||
|
- Selecting text does not seek playback and does not require mining a card.
|
||||||
|
- **Media Timing Review Screenshot Picker**:
|
||||||
|
- Choose the still screenshot separately from the audio range, with a live preview and its own time slider.
|
||||||
|
- Step through decoded frames one at a time to get the exact frame you want.
|
||||||
|
- Works with local video and with seekable remote streams such as Jellyfin.
|
||||||
|
- **mpv Keybindings in the Overlay**:
|
||||||
|
- The overlay now picks up keyboard bindings from mpv defaults, `input.conf`, and loaded scripts when they do not conflict with SubMiner.
|
||||||
|
- SubMiner controls and bindings you explicitly disabled take precedence.
|
||||||
|
- These bindings apply only to the current session and are not listed in the help menu.
|
||||||
|
- **Jimaku Live Action Search**: The Jimaku modal has new Anime and Live action tabs, so you can search Jimaku's live action catalogue as well as anime. Use Arrow Left and Arrow Right to switch tabs.
|
||||||
|
- **TMDB Live-Action Library**:
|
||||||
|
- Live-action dramas and movies in the stats Library now get posters, synopses, and titles from TMDB.
|
||||||
|
- Release builds include a project key. Setting `tmdb.apiKey` or `tmdb.apiKeyCommand` overrides it, and one of them is required when running from source.
|
||||||
|
- Titles that AniList cannot match are looked up on TMDB automatically when the parsed filename exactly matches a Japanese live-action title. For everything else, use the new **Link to TMDB** action.
|
||||||
|
- Entries linked to the same TMDB title merge into one card, and the Library kind selector has a new Live Action option.
|
||||||
|
- If a replacement download fails during provider reassignment, the previous link and artwork are kept. Merges and sync keep AniList and TMDB identities separate, and the merge dialog explains mixed selections instead of failing.
|
||||||
|
- **YouTube Library Kind**:
|
||||||
|
- YouTube channels are now their own Library media kind. Existing channel entries migrate automatically, and viewing history and manual video assignments are unchanged.
|
||||||
|
- New All Titles, Anime, and YouTube Library filters.
|
||||||
|
- Channels are excluded from AniList matching, season repair, and duplicate recommendations.
|
||||||
|
- Merges and video moves can no longer combine an anime entry with a YouTube channel.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Launcher Uses Bundled Bun**:
|
||||||
|
- Every installed and downloadable launcher now runs on the Bun runtime that ships with SubMiner. A system Bun is no longer needed.
|
||||||
|
- Recognized legacy launchers migrate automatically.
|
||||||
|
- Windows gets a `subminer.cmd` launcher download.
|
||||||
|
- First-run setup is reduced to one optional launcher control. Runtime repair guidance appears only when it is needed.
|
||||||
|
- **Faster Sync Transfers**:
|
||||||
|
- Sync between compatible macOS and Linux machines now uses compressed, incremental rsync transfers.
|
||||||
|
- The last snapshot received from each peer is cached, which reduces traffic on later syncs.
|
||||||
|
- Machines without a compatible rsync, including Windows, fall back to compressed scp.
|
||||||
|
- Older peers still work without the upload cache.
|
||||||
|
- Transfers abort after 30 minutes.
|
||||||
|
- **Stats Server Request Safety**:
|
||||||
|
- The stats server now accepts loopback hosts only and rejects requests from browser origins other than its own.
|
||||||
|
- Requests that change data must send an `application/json` body. Scripts that POST to the server need to set a JSON content type.
|
||||||
|
- The in-app stats overlay now loads from the local server, so it gets the same protection.
|
||||||
|
- Dashboards served through a reverse proxy or Tailscale Serve are no longer supported.
|
||||||
|
- **Smaller Downloads**:
|
||||||
|
- Installers and unpacked apps are smaller. Demo media, source maps, TypeScript sources, test fixtures, and unused Koffi binaries are no longer packaged.
|
||||||
|
- All windows now share one Japanese UI font.
|
||||||
|
- Release builds publish package size reports that compare against the previous release.
|
||||||
|
- **Bundled Yomitan**: Updated with upstream Yomitan 26.9.8 changes, including historical Japanese kana transformations, Ukrainian language support, and improvements to Anki duplicate searches and audio retrieval.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Jellyfin 12 Compatibility**:
|
||||||
|
- Playback, subtitle, artwork, and remote-control requests now authenticate with the `ApiKey` query parameter, so the integration works on Jellyfin 12, where legacy authorization is off by default.
|
||||||
|
- "Play on SubMiner" stays available. The cast connection now answers keep-alive requests and reconnects when the server stops responding, instead of silently dying after about a minute.
|
||||||
|
- The Jellyfin "now playing" bar clears when you close or finish a cast video instead of running on to the end of the episode.
|
||||||
|
- Cards mined during Jellyfin playback get the episode title in the misc info field again instead of "Unknown media".
|
||||||
|
- **Jellyfin Privacy and Playback**:
|
||||||
|
- Jellyfin streams no longer leak titles taken from the stream URL, or stream URLs that contain credentials, into metadata lookups, Anki source fields, Discord presence, stats, or AniList retries.
|
||||||
|
- Previously cached metadata that contained credentials is cleaned up. Watch history and library assignments are not touched.
|
||||||
|
- Jellyfin playback and casting now use your configured mpv executable, so they work when mpv is installed outside PATH. Portable plugins next to that executable are detected.
|
||||||
|
- **Anki Mining**:
|
||||||
|
- New `ankiConnect.fields.wordAudio` setting reads word audio separately from the sentence audio field. This fixes animated images that started moving immediately when `fields.audio` pointed to `SentenceAudio`. Existing animated images need to be regenerated to pick up the fix.
|
||||||
|
- Sentence furigana on word cards stays in sync with the full stats-search context and with expanded timing review selections. Stale readings are cleared if regeneration fails.
|
||||||
|
- Closing the overlay while media timing review is still loading now cancels the review, resumes playback if the review paused it, and cleans up the hidden preview player.
|
||||||
|
- `ankiConnect.media.maxMediaDuration: 0` now means unlimited when mining from the stats dashboard, matching overlay mining.
|
||||||
|
- Invalid AnkiConnect, Kiku, and Senren settings are now rejected with a warning and fall back to defaults.
|
||||||
|
- **Stats Server Stability**:
|
||||||
|
- A port conflict is now reported in a status notification instead of crashing SubMiner.
|
||||||
|
- Simultaneous startup requests share one server start. Stopping the background server no longer disconnects dashboards open in the foreground.
|
||||||
|
- Shutdown waits only a limited time for active requests to finish.
|
||||||
|
- Malformed resource IDs, and ID lists with any invalid entries, are rejected before Library changes or cover backfills run.
|
||||||
|
- **Subtitle Sidebar**:
|
||||||
|
- Clicking a cue no longer leaves the row focused, and Space no longer seeks back to a focused cue. Enter still seeks to the focused cue, and Space keeps its configured playback action.
|
||||||
|
- The sidebar stays near the current playback position during gaps when the subtitle file has a cue that starts at zero.
|
||||||
|
- **Settings Save Feedback**:
|
||||||
|
- Settings marked LIVE no longer show false restart warnings, including for notifications and subtitle generation.
|
||||||
|
- When a save mixes live and restart-only changes, the live changes apply right away and only the changed sections that need a restart are listed.
|
||||||
|
- **Overlay Windows**:
|
||||||
|
- On Hyprland, recovery dialogs stay above SubMiner windows so overlay placement updates no longer cover their Wait and Close buttons.
|
||||||
|
- On Linux, a delayed close callback during teardown can no longer reopen the overlay.
|
||||||
|
- **First Launch on macOS**: SubMiner no longer exits on first launch when the config directory does not exist yet.
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
- **Launcher**: Documented the launcher install that uses the bundled runtime, migration from legacy launchers, package-managed updates, and the bundled Bun runtime's MIT and LGPL notices. The AUR package installs these notices under `/usr/share/licenses/subminer-bin`, and they are also included in `subminer-assets.tar.gz`.
|
||||||
|
- **Subtitle Generation**: Documented model choice, VAD behavior, splitting guided by a reference track, fallback behavior, and known limits.
|
||||||
|
- **Subtitle Selection**: Documented the subtitle selector setting, its shortcut override, and the primary and secondary track controls.
|
||||||
|
- **Settings**: Clarified save feedback for live settings, warnings for saves that mix live and restart-only changes, and how subtitle generation settings reload.
|
||||||
|
- **Jellyfin**:
|
||||||
|
- Clarified that Windows mpv playback and Jellyfin casting can use a configured executable path instead of PATH.
|
||||||
|
- Documented how Jellyfin media titles and stats identities keep stream credentials out of metadata.
|
||||||
|
- **Stats Library**:
|
||||||
|
- Documented TMDB linking, provider reassignment, merge compatibility, and caching of the credential command's output.
|
||||||
|
- Documented YouTube channel filtering and video statistics in the Library.
|
||||||
|
- **Mining**:
|
||||||
|
- Documented choosing the screenshot separately in media timing review.
|
||||||
|
- Documented the separate word audio field mapping, including that existing animated images need to be regenerated.
|
||||||
|
- **Sync**: Documented compressed transfers, where the incremental sync cache is stored, and compatibility with older peers.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Removed duplicate source and launcher smoke runs from the reusable CI quality gate. Every distinct test lane and failure artifact is kept.
|
||||||
|
- Replaced mislabeled dist source reruns with a small Electron-runtime smoke check. It covers compiled stats startup, the HTTP service, native SQLite, port conflicts, and cleanup.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v0.19.6 (2026-09-04)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Card Timing Review**:
|
||||||
|
- Optional pre-generation timing review for word, sentence, and audio cards, with a speech-weighted waveform that flattens background noise so dialogue edges stand out clearly.
|
||||||
|
- The clip end automatically snaps back to where the line's dialogue actually ends once the waveform loads, with drag and keyboard adjustments available.
|
||||||
|
- Audio preview includes a sweeping playhead that plays the clip to its true end, even on high-latency outputs like Bluetooth headphones.
|
||||||
|
- Previous and next subtitle lines can be pulled onto the card with `P`/`N` (or the Prev/Next steppers) and removed with Shift; the sentence preview and waveform markers update automatically.
|
||||||
|
- Cancelling lets you keep a card without media, and the review can be toggled on or off for the session.
|
||||||
|
- **Senren Field Grouping**:
|
||||||
|
- Enable `ankiConnect.isSenren` to merge duplicate mined cards using Senren's scene-switching markup, grouping sentence, furigana, audio, picture, and misc-info fields.
|
||||||
|
- Supports the same auto/manual/disabled modes as Kiku, including the manual merge modal; only one of Senren or Kiku can be enabled at a time.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Remote Stream Mining Performance**: Mining a card from a remote stream (Jellyfin and other HTTP sources) now downloads the clip window once and reuses it for the timing review waveform, audio preview, audio extraction, and screenshot, instead of re-fetching the stream at each step; the temporary file is cleaned up after ten minutes of inactivity or on exit.
|
||||||
|
- **TsukiHime Release Filtering**: The TsukiHime modal's Japanese and secondary-language tabs now filter the release list by the subtitle languages each release actually carries, and report when no release has subtitles for the active tab.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Subtitle & Mining Accuracy**:
|
||||||
|
- Broadcast-style captions that split one sentence across two on-screen rows (e.g. Crunchyroll Japanese subs) now merge into a single line for the sidebar and mined cards, while separate speakers, sound effects, and labeled turns still stay on their own lines.
|
||||||
|
- Mining from the overlay no longer pulls in a lingering row from the previous caption; the mined sentence and clip timing now match what's actually on screen.
|
||||||
|
- Multi-line copy and mining now select lines backward in timeline order after seeking, instead of in playback encounter order.
|
||||||
|
- Copying a subtitle, mining a sentence, or recording immersion stats no longer includes the separate furigana line that broadcast ASS captions place above a word.
|
||||||
|
- **Card Update Notifications**: Dismissed lingering overlay card-update progress when notification settings switch to OSD before an update finishes.
|
||||||
|
- **Overlay Stability on Hyprland**: Opening a modal window (timing review, Jimaku, session help, and others) while mpv is fullscreen no longer causes the overlay to flicker while the modal loads; the overlay now stays on screen untouched until the modal is ready.
|
||||||
|
- **Jellyfin Subtitle Sync**: Jellyfin subtitle files now load with zero mpv delay instead of inferring and saving an offset from Japanese and English cue timelines.
|
||||||
|
- **Secondary Subtitle Visibility**: Native mpv secondary subtitles stay hidden when switching secondary subtitle tracks during playback.
|
||||||
|
|
||||||
|
## v0.19.5 (2026-08-30)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Anki Card Update Progress**: The card-update spinner now stays visible until audio and image updates finish, instead of disappearing early.
|
||||||
|
- **Anki Word-Card Fields**: Word-card enrichment now writes sentence text and audio to the fields configured in AnkiConnect, while the dedicated sentence-card and audio-card actions keep their existing compatible field names.
|
||||||
|
- **Overlapping Subtitles**:
|
||||||
|
- Subtitle lines that start while another line is still on screen now appear alongside it, instead of staying hidden until a track switch or seek.
|
||||||
|
- Subtitles shown at the same time now stack by their authored screen position, with top signs and song lines above bottom dialogue.
|
||||||
|
- Half-size ASS furigana is no longer shown as if it were a dialogue line.
|
||||||
|
- **YouTube Auto Captions**:
|
||||||
|
- Auto-generated captions now follow their intended timing and two-row roll-up layout.
|
||||||
|
- Long speech is paged instead of covering the video with a wall of text.
|
||||||
|
- Explicitly timed sound cues like `[音楽]` no longer cover later dialogue.
|
||||||
|
|
||||||
|
## v0.19.4 (2026-08-25)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Library Merge & Move**: Duplicate library cards for the same show can now be combined. Select cards in the library grid and use "Merge Selected" to pick which entry to keep and move every episode onto it, preserving sessions, mined cards, and watch time. Episodes can also be reassigned individually via the "→" button, useful when a file lands under a stray title; manual assignments survive later filename parsing, Jellyfin refreshes, and season repair. Exact AniList title matches with compatible seasons now merge automatically, while fuzzy matches surface as dismissible "Possible duplicate" reviews instead of merging silently.
|
||||||
|
- **Duplicate Line Cleanup Tool**: The Vocabulary tab's new "Duplicates" button scans a chosen time window (7 days through all time) for old karaoke/typeset duplicate-line bursts, shows what it found, and collapses each run to one line once confirmed; `subminer stats cleanup --duplicate-lines` does the same from the terminal, with `--dry-run` and `--lookback-days <n>` options. Watch time and lines-seen totals are left unchanged.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Prerelease Release Notes**: Prerelease notes now open with a "Changes since" section listing only what changed versus the previous beta/RC of the same version, above the cumulative highlights, and CI rejects prerelease tags whose committed notes were generated for a different beta/RC.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Subtitle & Karaoke Duplication**:
|
||||||
|
- Karaoke and animated signs are reconstructed once from their authored text and shown only while actually sung, with original word spacing preserved, instead of flooding the overlay, subtitle sidebar, immersion history, mining, or stats with glyph fragments, per-frame color phases, and repeated animation events.
|
||||||
|
- Decorative layers (highlight sweeps, glow/shadow copies, symbol-font decoration, particle swarms, hidden or zero-scaled text) stay out of published text, while ordinary repeated dialogue, positioned signs, wrapped lyric rows, and multi-row CC-style blocks still display correctly.
|
||||||
|
- Embedded subtitle tracks on network-mounted (SMB/NFS) media are extracted and parsed again instead of falling back to live-text-only, restoring karaoke reconstruction, sidebar cues, and mining for releases that only ship subtitles inside the container.
|
||||||
|
- Secondary subtitles go through the same deduplication pipeline as primary subtitles and no longer clip display after about four lines.
|
||||||
|
- Event-heavy karaoke files that previously stalled subtitle loading for several seconds now parse in well under a second.
|
||||||
|
- **Character Dictionary Reliability**:
|
||||||
|
- Generation, merged rebuilds, and imports no longer freeze the app on large dictionaries; snapshot I/O, archive building, and image/name lookup caches moved off the UI's critical path.
|
||||||
|
- Dictionaries are reused instead of regenerated when MeCab finds no name splits.
|
||||||
|
- Cached portraits restore correctly after the portrait index finishes loading post-tokenization.
|
||||||
|
- Desktop progress notifications on Linux AppImage installs update in place instead of flickering, fixing a bug where the AppImage's bundled libraries broke the system notification helper.
|
||||||
|
- **Overlay Startup & Modals**:
|
||||||
|
- The macOS window-tracking helper targets macOS 12.0+ instead of requiring the build machine's exact macOS version, fixing crashes on older systems like Ventura that left the overlay stuck on "Overlay loading".
|
||||||
|
- mpv IPC connection attempts time out and retry, showing an actionable error if content still isn't ready after 30 seconds.
|
||||||
|
- Dedicated overlay modals are prewarmed on macOS and Windows so shortcuts open them promptly.
|
||||||
|
- On macOS, reused modals and the stats window open above fullscreen mpv on its current Space instead of jumping to another desktop.
|
||||||
|
- **Wayland File Drop**: Fixed native Wayland drag-and-drop from file managers such as Thunar, so subtitle and video files dropped on the visible overlay are resolved and forwarded to mpv.
|
||||||
|
- **Windows Mouse Lag**: Fixed system-wide mouse lag on Windows while SubMiner is running, caused by the overlay's global mouse hook for click-through forwarding and by the mpv window tracker blocking the app on repeated PowerShell lookups.
|
||||||
|
- **Sentence Mining Audio & Clips**: Sentence-audio generation no longer times out on slow network-mounted media with many subtitle/font streams (bounded FFmpeg probing, two-minute extraction budget, clearer error reporting), and mined audio/animated AVIF clips now capture the subtitle line that was actually mined by snapshotting the clip range at lookup time instead of reading live mpv state later.
|
||||||
|
- **Stats Performance & Reliability**: Immersion stats storage now sets its SQLite busy timeout before WAL setup, avoiding transient lock errors under concurrent writes. Deletes in the stats dashboard no longer freeze the UI, run proportional to what's deleted instead of rebuilding full lifetime summaries, retry safely if the delete worker crashes, and no longer rescan the whole library when deleting very common words; a new index also makes large session deletes drop from minutes to milliseconds. Library merges, video moves, and AniList reassignments got the same lifetime-summary fix.
|
||||||
|
- **Vocabulary Stats Accuracy**: Vocabulary totals and charts now count all tracked vocabulary instead of only the first page, new-word history uses corrected daily rollups (fixing legacy timestamp and time-zone issues), summary cards refresh automatically after edits to the exclusion list, and rapid exclusion edits no longer race each other.
|
||||||
|
- **Rofi MKV Thumbnails**: Fixed missing MKV thumbnails in the Linux rofi picker when system thumbnailer registrations only advertise legacy Matroska MIME aliases.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Docs Site Indexing: Excluded the `/main/` and `/v/<version>/` docs trees from search indexing (self-referential canonical, `noindex,follow`, matching `X-Robots-Tag`) so crawlers focus on current docs instead of ~30 archived copies of every page, and restored `<lastmod>` dates in the docs sitemap that were silently dropped by production builds.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v0.19.3 (2026-08-13)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Changelog Modal: Adds an in-app changelog you can open from the tray ("View Changelog") or the "What's New" button on the update notification, so the notification stays reachable while you read. It shows the newest published release notes (falling back to the bundled changelog if that fetch fails), folds older versions while keeping the current one expanded, and supports keyboard navigation (`J`/`K`/arrows, `Enter`, `R`, `Esc`).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Subtitle Tokenization Performance: Reworks subtitle dictionary lookups to cut per-line work roughly in half, cache repeated lookups across lines, and stop tokenization from competing with on-screen subtitle prefetching. Also fixes several accuracy issues along the way: dropped readings on trailing kana, character names being skipped after a dictionary sync, annotations not refreshing after mining a card, and halfwidth katakana character names losing their reading or being swallowed by other words.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Character Dictionary Large Imports: Large character dictionaries (e.g. One Piece) no longer fail to install from a fixed timeout budget; the import now scales its time budget to dictionary size and reports detailed progress (page/character counts, image download progress, elapsed time) instead of one static message.
|
||||||
|
- Stats Delete Responsiveness: Deleting sessions, episodes, or library entries no longer freezes the stats page or an active video player; deletes are now batched into a single transaction.
|
||||||
|
- Styled Subtitle Cue Parsing: Heavily typeset subtitles (karaoke, signs) no longer flood the subtitle sidebar with garbage; vector drawing commands are no longer shown as text, and duplicate/animation-burst cues now collapse into one.
|
||||||
|
- X11 mpv Renderer: Fixes an mpv crash on the first fullscreen toggle for X11/XWayland users with `gpu-next` shaders (e.g. ArtCNN), which was caused by X11 mode forcing the legacy OpenGL renderer.
|
||||||
|
- X11 Overlay Display Scaling: Fixes the overlay appearing oversized and offset from mpv on X11/XWayland under fractional or mixed-monitor display scaling.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Subtitle text is now decoded from ASS exactly once at ingest, so the renderer, timing tracker, and tokenizer all share one decoded value instead of each re-deriving it.
|
||||||
|
- Added per-stage debug timings (`scanMs`, `mecabMs`, `frequencyMs`, `annotateMs`) to the subtitle tokenization pipeline log.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v0.19.2 (2026-08-04)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Subsync: The sync modal now lets you choose both the reference subtitle (correct timing) and the out-of-sync subtitle to retime, for both alass and ffsubsync. alass can also use the loaded video's audio as a reference for local files. Retiming the secondary track now reloads the result into the secondary slot instead of overwriting the primary subtitle.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Streaming Subtitle Tokenization: Jellyfin streams now seed subtitle tokenization directly from the downloaded subtitle file instead of relying on an mpv event that could be missed, and prefetching now runs to the end of the file and clears between episodes. The tokenization cache was raised from 256 to 2500 lines, and parsed cues are no longer lost when the active subtitle track briefly can't be resolved (e.g. switching to an embedded track). Together these prevent episodes from falling back to slow, line-by-line tokenization during playback.
|
||||||
|
- Overlay: Subtitle lines now appear immediately at their cue time even on a tokenization cache miss, upgrading in place once tokens and annotations are ready, instead of waiting on a line still being processed. A failed tokenization is no longer cached as plain text, so repeated lines get another chance at annotations.
|
||||||
|
- Background Logging: Background startup now respects the configured logging level when no explicit log level is passed.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Patched three high-severity dependency advisories (`undici`, `brace-expansion`, `fast-uri`).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v0.19.1 (2026-08-01)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Word Card Type: Adds a setting (Settings > Mining/Anki > Kiku/Lapis Features > "Word Card Type") to choose which card-type flag SubMiner marks on Kiku/Lapis word cards — `word-and-sentence` (default), `click`, `sentence`, `audio`, or `none`. Click cards (`IsClickCard`) can now be flagged, and setting any card-type flag clears the others so a note can't claim two types at once.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Yomitan Popup: Fixes the macOS Yomitan popup going inert after mining a card — clicks outside the popup no longer pass through to mpv, and scrolling over the popup scrolls its definitions instead of seeking playback.
|
||||||
|
- YouTube Playlist Links: Fixes opening a video from a playlist URL (e.g. a Watch Later link with `list=`/`index=`) timing out while probing subtitles, metadata, or the playback URL.
|
||||||
|
|
||||||
|
## v0.19.0 (2026-07-29)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Anki Maturity Highlighting: Known-word subtitle highlights can now be colored by Anki card maturity (new, learning, young, mature), similar to asbplayer. Tier thresholds and colors are configurable, with a runtime toggle and an updated help legend.
|
||||||
|
- Post-Playback Menu: After a watch-history episode ends, the fzf/rofi launcher returns to that series with options to play the previous or next episode, rewatch, pick another episode, or quit. The pre-playback series menu now offers the previous episode too.
|
||||||
|
- Delete Library Entries: The stats Library detail view can now delete an entire title in one step (episodes, sessions, subtitle lines, rollups, cover art, and vocabulary counts). Delete progress is now shown app-wide via a progress bar and status toast instead of disappearing when you switch tabs.
|
||||||
|
- Cross-Machine Sync: Added SSH-based syncing of stats and watch history between machines, available from the tray ("Sync Stats & History") or `subminer sync`, with saved devices, per-host sync direction, background auto-sync, connection testing, manual snapshots, and support for Windows remotes.
|
||||||
|
- TsukiHime Subtitle Downloads: Added subtitle downloads for the current video via TsukiHime, loading Japanese as the primary track and your configured secondary language directly into mpv.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Clipboard-Video Shortcut: The "append clipboard video to queue" shortcut is now configurable.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- AniList Season Resolution: Season 2+ files now resolve to the correct AniList entry instead of silently falling back to season 1 (which mismatched character dictionaries and watch progress). Manual overrides now stay scoped per season, fix both the dictionary and progress tracking together, and also correct per-season cover art.
|
||||||
|
- Subtitle Annotation Accuracy: Fixed several annotation edge cases, including inconsistent POS exclusions on merged quote-particle tokens, dropped annotations on supplementary-plane kanji, katakana punctuation wrongly treated as noise, and certain kanji vocabulary losing N+1 highlighting eligibility.
|
||||||
|
- AnkiConnect Proxy Port Conflict: Video startup no longer crashes when another process already holds the configured AnkiConnect proxy port; a notification now explains how to resolve it.
|
||||||
|
- AppImage Quit Crash: Fixed a "Service Crash" desktop notification appearing after closing a video when running the Linux AppImage.
|
||||||
|
- Autoplay Pause Timing: Fixed playback resuming a few seconds before subtitle tokenization warmup finished, most noticeable when resuming mid-episode or when a cue starts within the first two seconds.
|
||||||
|
- Stats Known-Word Count: Fixed stats reporting 0 known words for every session after the known-word cache format changed.
|
||||||
|
- Stats Library Cover Art: Relinking a title to a different AniList entry now updates its cover in the Library grid immediately instead of leaving a stale, mismatched cover cached.
|
||||||
|
- Rofi Prompt Spacing: Rofi menu prompts now keep a space before the input field instead of running into the placeholder text.
|
||||||
|
- Stats Settings & Reliability: Hardened stats settings validation (nested/legacy AnkiConnect config now falls back safely instead of breaking) and stats routes against malformed requests and other edge cases.
|
||||||
|
- Stats Delete Performance: Deleting sessions, episodes, and library entries is now dramatically faster and no longer stalls playback (e.g. a 12-episode title dropped from about a minute to under a second on a large library); the Vocabulary tab also loads much faster. The first launch after upgrading runs a one-time database migration.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Added a golden-file regression test corpus for the tokenizer/annotation pipeline, plus scripts to record new fixtures and diff against stock Yomitan.
|
||||||
|
- Consolidated renderer modal state handling into a descriptor registry.
|
||||||
|
- Consolidated CI quality checks (PR, stable, and prerelease) into one reusable workflow with mpv plugin tests and dependency audits.
|
||||||
|
- Removed the unused stats IPC transport and unified stats dashboard HTTP types with the backend contract.
|
||||||
|
- Added a script to verify known-word highlight tiers against live Anki data outside of playback.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v0.18.0 (2026-07-10)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Sentence Audio Normalization: Generated sentence audio is now normalized to -23 LUFS by default, and clips mined from playback mirror mpv's software volume curve with a limiter to prevent clipping. Both behaviors are configurable independently.
|
||||||
|
- Watch History Command: Added `subminer -H` / `--history` to browse watch history, replay or continue episodes, or pick one via fzf or rofi, with cover art shown in the rofi picker.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Fzf Preview Layout: Moved fzf previews below launcher menus, giving long titles and metadata more room.
|
||||||
|
- Known-Word Highlighting: Now compares subtitle and Anki-card readings, preventing false matches between homographs and unrelated words that share a reading, while still supporting matching across kana and kanji spellings.
|
||||||
|
- Annotation Filtering: Standalone suffix tokens (e.g. さん, れる) are now excluded from JLPT/frequency/N+1 highlighting by default, matching how particles and interjections are treated; configurable via the pos2 exclusion setting.
|
||||||
|
- App Icon: Replaced the app icon with new pixel-art submarine artwork contributed by the community, used across the app icon, tray, notifications, README, docs site, and Stats page.
|
||||||
|
- Stats Trend Charts: Overhauled with persisted title visibility, per-chart title limits, "top" and "most recent" ranking modes, an option to show or hide empty days, calendar-aligned periods, and value-sorted tooltips.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Background Stats Server: `subminer app` background launches now auto-start the stats server when enabled, and skip startup if one is already running.
|
||||||
|
- Character Name Highlighting: Character dictionaries now split unspaced native names more reliably, and portraits, highlights, and hover lookup survive punctuation, unmatched text, and competing dictionary matches without incorrectly splitting longer words.
|
||||||
|
- Highlighting Coverage: Frequency/JLPT highlighting and vocabulary stats now include content adverbs (e.g. 確かに, やはり) and kanji nouns MeCab tags as non-independent (e.g. 日, 点, 以外), while still suppressing interjections, pronouns, and grammar fragments; lexicalized kana expressions like かといって keep their annotations.
|
||||||
|
- Cover Art Fetching: Stats now fetches AniList cover art as soon as a new series starts playing, and backfills missing art for existing series on the next Stats visit.
|
||||||
|
- Kiku Field Grouping: The manual field-grouping dialog now stays above fullscreen mpv, remains usable across repeated attempts, closes abandoned windows after timeouts, and reports a clear error when the original card can no longer be loaded.
|
||||||
|
- Karaoke Subtitle Collapse: Secondary subtitles no longer stack dozens of one-syllable lines during openings and endings; repeated events collapse into one line, capped to a strip at the top.
|
||||||
|
- Unparsed Token Hover: Subtitle text Yomitan can't parse (truncated inflections, elongation runs) remains hoverable, while staying excluded from highlighting, N+1 candidate math, and vocabulary stats.
|
||||||
|
- YouTube Streaming: Fixed direct YouTube stream extraction that could corrupt signed URLs and cause ffmpeg 403 errors.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Test lanes moved to `scripts/test-lanes.ts` with per-directory discovery and isolated per-file timeouts; CI now covers previously orphaned stats, scripts, plugin process-retry, and runtime-compat suites, plus a new stats lane in the change-verification workflow.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v0.17.2 (2026-06-28)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- YouTube Background Cache: Fixed Windows YouTube background media cache startup for YouTube URLs opened directly in mpv, including resolved stream URLs when mpv still exposes the original YouTube playlist entry, so queued Anki media updates can append audio and images after the cache finishes.
|
||||||
|
- YouTube Subtitle Picker: Manual subtitle picker requests now show an immediate configured notification while SubMiner probes tracks and opens the modal. Subtitle download progress is replaced with a transient success notification after tracks load.
|
||||||
|
|
||||||
|
## v0.17.1 (2026-06-27)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- YouTube Media Cache Mode: Adds `youtube.mediaCache.mode` with `direct` and `background` options. Background mode uses a yt-dlp cache download when direct stream extraction is unreliable — creates a text-only card immediately, queues media updates for mined notes, and fills audio/image fields once the download finishes. Progress is announced via overlay/OSD notifications. Downloads are capped at 720p by default (`youtube.mediaCache.maxHeight`). Switching back to direct mode cancels any in-flight background download.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Log Export: Fixed log filenames to use the local date so exports around UTC midnight include the current day's logs rather than stale prior-day files. Expanded export redaction to mask IPs, emails, auth and cookie headers, yt-dlp cookie arguments, URL credentials, token/key/password fields, and signed YouTube media URL parameters.
|
||||||
|
- YouTube Card Media: Improved media generation reliability by sending safer ffmpeg options for resolved streams and skipping stale stream maps (including cached YouTube files). Hardened background cache downloads with IPv4 and extractor retry flags; failed downloads now notify the user and clear queued media updates instead of leaving them silently pending. Stale background cache files are cleaned on startup and before each new download.
|
||||||
|
|
||||||
|
## v0.17.0 (2026-06-15)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Subtitle Delay Keybindings**: Updated default overlay subtitle delay and step bindings to match mpv conventions: `z`, `Z`, and `x` adjust `sub-delay`; `Ctrl+Shift+Left/Right` run native `sub-step` with OSD feedback. The previous SubMiner-only adjacent-cue delay action has been removed.
|
||||||
|
- **Update Notifications**: New installs now default update notifications to overlay-only instead of overlay + system notifications.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Anki – Highlight Word**: Fixed bolding of the mined word in Kiku sentence and sentence-furigana fields when the source Yomitan sentence did not already contain bold markup.
|
||||||
|
- **Anki – Lapis/Kiku Word Cards**: Fixed word-and-sentence marker missing from Lapis/Kiku word cards enriched through SubMiner, which could hide sentence context on the card front.
|
||||||
|
- **Anki – Windows Media Generation**: Fixed known-word cache refreshes when no deck is configured, and fixed audio/image generation after background launches by recreating missing FFmpeg temp directories before clipping.
|
||||||
|
- **Character Dictionary – Windows**: Fixed the Windows "SubMiner mpv" shortcut so character dictionary auto-sync can fall back to mpv's current video path when app media state is not yet ready.
|
||||||
|
- **Notifications**: Restored the SubMiner app icon on system notifications that do not supply a custom notification image.
|
||||||
|
- **Overlay – macOS Yomitan Popup**: Fixed Yomitan popup focus after card mining or popup reload; fixed popups staying open when clicking transparent overlay space — click-away now closes the popup and returns click passthrough to mpv without a hide/reappear cycle.
|
||||||
|
- **Overlay – Linux Auto-Pause Startup**: Fixed the visible overlay on Linux auto-paused startup to stay interactive during the initial measurement gap; startup subtitle cache misses now paint raw text before tokenization finishes, and temporarily empty `sub-text` refreshes are resolved before warm readiness resumes playback.
|
||||||
|
- **Overlay – Playlist Advance**: Fixed the visible overlay being dismissed when mpv advances to the next playlist item, including when the next episode loads after the warm transition delay.
|
||||||
|
- **Overlay – Windows Subtitle Bar**: Fixed shaky hover and click interaction on the Windows subtitle bar when a video attaches to an already-running background SubMiner instance.
|
||||||
|
- **Stats – AniList Linking**: Fixed manual AniList linking from the stats anime page so auto-searches strip the generated "Season N" suffix and query only the anime title.
|
||||||
|
- **Updates – Linux Support Assets**: Fixed Linux updates to correctly install and refresh the launcher runtime plugin copy and rofi theme alongside AppImage and launcher updates; unrelated SubMiner data directories are now left untouched and plugin copies are staged before replacing the live runtime. Fixed first-launch playback on fresh installs by auto-installing missing managed support assets from the bundled app before mpv starts.
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
- **Linux Update Flows**: Documented that Linux update flows manage the launcher runtime plugin copy and rofi theme from `subminer-assets.tar.gz`, and that normal launcher playback auto-installs those managed support assets on a fresh install if either is missing.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
|
||||||
|
- **Runtime Modules**: Split main-process runtime wiring into focused modules without changing user-facing behavior; hardened helpers against stale background stats daemon PIDs, stalled subtitle extraction, and dropped async errors.
|
||||||
|
- **Release CI**: Fixed GitHub release notes to preserve the `What's Changed` and `New Contributors` attribution sections when CI regenerates from the committed changelog; scoped prerelease note reuse to the same base version so a new beta line starts from current fragments.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v0.16.0 (2026-06-10)
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- **Notification Mode `both`**: `notificationType: "both"` now routes to overlay + system notifications. Users who previously used `"both"` for mpv OSD + system notifications should set `notificationType` to `"osd-system"` in `config.jsonc`. The `osd` and `osd-system` values remain valid as config-file entries but no longer appear in Settings.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Overlay Notifications**: New overlay notification stack (Catppuccin Macchiato theme) with configurable screen position (`notifications.overlayPosition`: top-left, top-center, top-right), 3-second transient dismissals, and persistent cards for long-running jobs like character dictionary sync.
|
||||||
|
- **Notification History Panel**: `Ctrl/Cmd+N` (configurable via `shortcuts.toggleNotificationHistory`) opens a session log of all notifications. Works whether the overlay or mpv has focus; slides in from the notification edge; entries can be individually removed or cleared.
|
||||||
|
- **Mined-Card Notification Actions**: Mined-card overlay notifications show generated card thumbnails and include an Open in Anki button in both live cards and history entries.
|
||||||
|
- **Update Notification Action**: Update-available overlay notifications include an Update button to start the app update flow directly from the notification.
|
||||||
|
- **Stats Search**: New Search tab in Stats for realtime subtitle sentence search with media context, headword matching, and mining actions for source-backed sentence cards or exact-match word/audio cards.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **AniSkip**: Moved intro detection from the mpv plugin to the SubMiner app. Lookups now cover every file loaded during a session including playlist advances, and `mpv.aniskipEnabled`/`mpv.aniskipButtonKey` hot-reload without restarting playback. The bundled plugin no longer makes network calls. Note: AniSkip now requires the SubMiner app to be connected; plugin-only mpv sessions will not fetch skip windows.
|
||||||
|
- **Stats Library**: Entries are now split by detected season (season folder first, filename parsing as fallback). Existing combined-series rows are automatically migrated to per-season entries on startup. Cover art and anime details refresh immediately after a manual AniList entry change.
|
||||||
|
- **Stats Vocabulary**: Remembers Hide Known/Hide Kana filters across sessions, applies Hide Kana filtering cross-title, collapses duplicate token variants in exclusions, and matches Related Seen Words by shared readings or kanji.
|
||||||
|
- **Stats Trends**: Reorganized into Activity, Cumulative Totals, Efficiency, Patterns, and Library sections; disambiguated per-period vs. cumulative charts; added Words/Min and Cards/Hour efficiency charts.
|
||||||
|
- **Stats Mining**: Sentence cards are created before slow media generation finishes; stored/requested secondary subtitles are preserved before falling back to sidecar or alass-retimed English subtitles; empty `ankiConnect.deck` falls back to Yomitan's mining deck; partial media failures are surfaced.
|
||||||
|
- **Stats Browsing**: Remembers library card size; retries stored cover art without extra AniList lookups; preserves PNG/WebP MIME types; honors custom AnkiConnect URLs for Browse; shows progress during session deletes.
|
||||||
|
- **Startup Notifications**: Tokenization, subtitle annotation, and character dictionary status now route through queued overlay notifications in `overlay`/`both` mode instead of falling back to mpv OSD while the overlay loads.
|
||||||
|
- **Notification Deduplication**: Cycling subtitle modes updates the active overlay card in place rather than stacking duplicates; repeated progress updates (e.g. subsync) tick in place without flickering.
|
||||||
|
- **Update Notification Default**: New installs default `notificationType` to `both` so update alerts appear in both overlay and system notifications.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **AniList Completion**: Entries are now marked completed when a post-watch update reaches the final known episode of the season.
|
||||||
|
- **AniSkip Markers**: Fixed intro markers disappearing after same-media mpv reloads; fixed metadata detection for intros that start at 0 seconds and common release-group filenames.
|
||||||
|
- **Jellyfin Session**: Remote session now restarts after setup login so the websocket reconnects with fresh credentials, and stops cleanly on logout.
|
||||||
|
- **Sentence Card Audio**: Mining a sentence card no longer fills the expression audio field; generated audio goes only to the configured sentence audio field.
|
||||||
|
- **Stats Mining Fields**: Sentence clips update `SentenceAudio` correctly; word audio uses configured Yomitan sources; English subtitle text is not written to word cards; secondary subtitle auto-selection prefers regular English tracks over Signs/Songs tracks.
|
||||||
|
- **Overlay Hover Readiness**: Subtitle bars are hoverable and clickable from the first subtitle line on visible overlay startup or resume, without waiting for the next subtitle event.
|
||||||
|
- **Startup Autoplay**: Playback is released after tokenization and overlay content are ready even when playback begins before the first subtitle line appears.
|
||||||
|
- **Overlay Startup Feedback**: Restored mpv OSD loading spinner that starts on connect, media open, or overlay request, and clears once the overlay is content-ready and visible.
|
||||||
|
- **Linux Overlay Input**: Notification close and action buttons remain clickable above subtitle bars on Linux.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- **Build**: `make deps` now initializes git submodules before installing dependencies on a fresh source checkout.
|
||||||
|
- **Release Tooling**: Release notes now credit contributors and first-time authors resolved from changelog fragments via git and the GitHub API.
|
||||||
|
- **Changelog Guidance**: PR fragment guidance updated to preserve separate-outcome fragments while directing contributors to consolidate same-PR follow-up notes before adding churn.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v0.15.2 (2026-06-02)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Yomitan: Updated the bundled Yomitan build to the latest vendored revision.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Anki - Animated AVIF: Clip timing no longer starts or ends early; word-audio lead-in and clip duration are now aligned to frame boundaries.
|
||||||
|
- Overlay (Hyprland): Fixed fullscreen overlay alignment - modal, stats, and sidebar content no longer shift below the mpv window.
|
||||||
|
- Overlay (macOS): Subtitle bars are now interactive immediately after autoplay starts with "wait for overlay to be ready" enabled, without requiring a manual click.
|
||||||
|
- Overlay (macOS): Fixed overlay, subtitles, and subtitle sidebar staying hidden after a modal closes until the user clicked the mpv window; focus is now restored to mpv when the last modal closes, so playback shortcuts and the overlay reappear correctly - including in native fullscreen.
|
||||||
|
|
||||||
|
## v0.15.1 (2026-05-31)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Linux Overlay Stacking**: Fixed the overlay intermittently dropping behind mpv on KDE Plasma and other non-Hyprland/Sway Wayland sessions; restored subtitle hover, pause-on-hover, and Yomitan lookups on X11/XWayland; the overlay now correctly layers above/below mpv based on fullscreen state, yields to foreground windows (Settings, Yomitan, AniList, etc.), and avoids startup flashes and fullscreen transition glitches.
|
||||||
|
- **Linux Overlay (Hyprland Lua)**: Fixed overlay placement on Hyprland 0.55+ when using a Lua-based config.
|
||||||
|
- **Manual Overlay Startup**: Fixed manual visible-overlay startup from mpv - now correctly attaches to playback, keeps the window bounds synced with mpv, and primes current subtitles before showing.
|
||||||
|
- **Playlist Transitions**: Reused the warm overlay when mpv advances to the next playlist item, avoiding a redundant tokenization pause and preserving visible subtitles across tracks.
|
||||||
|
- **macOS Overlay**: Fixed the visible subtitle overlay staying click-through after pause-until-ready releases playback; restored mpv focus after closing modal windows so subtitles and keybinds resume without clicking the player.
|
||||||
|
- **Mouse Keybindings**: Fixed keybinding capture and runtime handling for mouse buttons, including side buttons like `MBTN_BACK` and `MBTN_FORWARD`.
|
||||||
|
- **Windows mpv Shortcut**: Fixed the Windows `SubMiner mpv` shortcut so videos attach to an already-running background app instead of spawning a second process.
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
- **Troubleshooting**: Updated Hyprland overlay docs with current Lua (`hl.window_rule`) and legacy config syntax; added troubleshooting for KDE/Wayland and other non-Hyprland/Sway Wayland sessions; added a Character Dictionary troubleshooting section; added a "See Also" index linking each feature's troubleshooting page.
|
||||||
|
|
||||||
|
## v0.15.0 (2026-05-29)
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- **Subsync:**
|
||||||
|
- The `subsync.defaultMode` config option has been removed
|
||||||
|
- Subsync now always opens the manual subtitle picker regardless of any previously set default mode
|
||||||
|
- **N+1 Highlighting:**
|
||||||
|
- N+1 highlighting now has its own dedicated `ankiConnect.nPlusOne.enabled` option, separate from known-word highlighting
|
||||||
|
- It is no longer enabled automatically when known-word highlighting is on - enable it explicitly to keep N+1 annotations
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Auto-Updater:**
|
||||||
|
- Tray and `subminer -u` update checks with app update prompts
|
||||||
|
- Launcher and Linux rofi theme auto-updates
|
||||||
|
- Checksum verification and configurable notifications
|
||||||
|
- Opt-in prerelease channel via `updates.channel: "prerelease"`
|
||||||
|
- **Settings Window:**
|
||||||
|
- New dedicated Settings window via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections
|
||||||
|
- Click-to-learn keybinding controls
|
||||||
|
- AnkiConnect-backed deck, field, and note-type pickers that auto-fill from the configured Anki deck
|
||||||
|
- Cross-category search
|
||||||
|
- Live save for most options including subtitle CSS, stats keys, logging level, Jimaku, Subsync, and Anki mappings
|
||||||
|
- AI and translation settings remain config-file only
|
||||||
|
- **Inline Character Portraits:**
|
||||||
|
- Optional AniList character portraits appear inline for name-matched subtitle text
|
||||||
|
- Manual AniList overrides scoped per parent media directory so separate season folders maintain separate character dictionary selections
|
||||||
|
- **Character Dictionary Manager:** New `Ctrl/Cmd+D` manager modal to remove, reorder, or override loaded entries.
|
||||||
|
- **Log Export:** Sanitized log ZIP export from the tray menu and via `subminer logs -e`, with home-directory usernames redacted from exported contents.
|
||||||
|
- **Launcher CLI:**
|
||||||
|
- `subminer --version` / `subminer -v` prints the installed app version
|
||||||
|
- `mpv.profile` config and Settings support passes a named mpv profile to managed launches
|
||||||
|
- Bundled mpv plugin startup options are now configurable from SubMiner config
|
||||||
|
- **First-Run Setup:**
|
||||||
|
- Optional installer for Bun and the `subminer` CLI on Linux, macOS, and Windows
|
||||||
|
- Windows `subminer.cmd` PATH shim so `subminer` works without manually adding `SubMiner.exe` to PATH
|
||||||
|
- Setup recognizes existing Homebrew or user PATH installs and avoids writing into Homebrew-owned paths
|
||||||
|
- Includes an Open SubMiner Settings button
|
||||||
|
- Standalone setup app quits after completing, returning terminal control
|
||||||
|
- **Primary Subtitle Visibility on Yomitan Popup:** New `subtitleStyle.primaryVisibleOnYomitanPopup` option keeps hover-mode primary subtitles visible while a Yomitan popup is open.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Subtitle Appearance Config:**
|
||||||
|
- Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css`, `subtitleStyle.secondary.css`, and `subtitleSidebar.css`
|
||||||
|
- Known-word and N+1 annotation colors moved to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`
|
||||||
|
- Subtitle font defaults updated to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`
|
||||||
|
- Existing configs migrate automatically; legacy Anki color keys still accepted with deprecation warnings
|
||||||
|
- **Subtitle Style Defaults:**
|
||||||
|
- Stronger outline-style text shadow
|
||||||
|
- Thicker JLPT underlines
|
||||||
|
- Frequency `topX` default raised to `10000`
|
||||||
|
- **Character Dictionary:**
|
||||||
|
- Entries scoped to the current AniList media for name matching and inline portraits
|
||||||
|
- Generates Japanese-only name aliases so raw romanized/English aliases no longer surface as separate results
|
||||||
|
- In-app AniList selector waits for an explicit search with the box prefilled from the current filename
|
||||||
|
- `subtitleStyle.nameMatchEnabled` is now the sole switch for dictionary sync and builds
|
||||||
|
- **Electron Runtime:** Updated from 39.8.6 to 42.2.0, returning SubMiner to a supported Electron release line.
|
||||||
|
- **Jellyfin Setup:**
|
||||||
|
- Removed the server presets dropdown
|
||||||
|
- Setup now shows a single editable server URL field
|
||||||
|
- **Jellyfin Cast Identity:**
|
||||||
|
- Device identity now derived from the OS hostname and always reported as SubMiner
|
||||||
|
- Previously configurable identity fields are ignored, preventing multiple installs from sharing a remote-session identity
|
||||||
|
- **Startup Defaults:** Jellyfin remote-session startup warmup and character-name subtitle highlighting now default to off.
|
||||||
|
- **Setup Appearance:** Removed the bundled mpv runtime plugin readiness card from the setup flow.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **AniList Progress:**
|
||||||
|
- Progress updates fire correctly when playback reaches or skips past the watched threshold, using fresh mpv timing events
|
||||||
|
- Season-specific results preferred for multi-season files, with a clear message when the matched season is not in Planning or Watching
|
||||||
|
- Repeated missing-token checks no longer exhaust retry attempts or duplicate dead-letter entries
|
||||||
|
- **Anki Mining:**
|
||||||
|
- Sentence-audio padding is opt-in by default
|
||||||
|
- Animated AVIF freeze-frame duration aligned to word audio length without double-counting
|
||||||
|
- Multi-line sentence alignment fixed for repeated subtitle text
|
||||||
|
- Kiku duplicate-card detection, auto-merge, modal acknowledgment race, and field/tag ordering corrected
|
||||||
|
- YouTube playback cards use mpv's resolved stream URLs
|
||||||
|
- Sentence cards refresh the secondary subtitle before saving
|
||||||
|
- **Jellyfin Discovery:**
|
||||||
|
- Startup, subtitle track selection, and duplicate ready-signal handling all fixed
|
||||||
|
- Paused mpv no longer misreported as playing
|
||||||
|
- Resume corrected when a remote play command sends `StartPositionTicks: 0` despite saved progress
|
||||||
|
- **Jellyfin Remote:**
|
||||||
|
- Tray checkbox stays in sync on Linux after tray, CLI, or startup changes
|
||||||
|
- Remote controller visibility and progress sync fixed for seeks, stops, startup path changes, and Linux websocket reconnect windows
|
||||||
|
- Play and Resume now behave correctly (Play from beginning, Resume from saved position)
|
||||||
|
- Final progress reports reuse SubMiner's last known position when mpv resets on stop
|
||||||
|
- Windows setup login flow fixed with an IPC bridge, immediate feedback, and a timeout with inline error for unreachable servers
|
||||||
|
- **Overlay (macOS):**
|
||||||
|
- Overlay hides when mpv loses focus, is minimized, or is no longer the foreground app
|
||||||
|
- Stays stable through transient window geometry disappearances from macOS APIs and when clicking from the overlay back into mpv
|
||||||
|
- Stats overlay opened inactive so it appears over fullscreen mpv without switching Spaces
|
||||||
|
- Passthrough fixed so mpv controls stay clickable before hovering a subtitle bar
|
||||||
|
- **Yomitan Sidebar:**
|
||||||
|
- Playback stays paused for sidebar-opened Yomitan popups when auto-pause is enabled
|
||||||
|
- Popups now open when startup races the Yomitan extension load
|
||||||
|
- Sidebar mining cards use audio and images from the clicked sidebar line instead of the current primary subtitle
|
||||||
|
- **Launcher:**
|
||||||
|
- `subminer app` on Linux returns terminal control immediately
|
||||||
|
- `subminer app --setup` opens the setup flow when SubMiner is already running in the background
|
||||||
|
- **YouTube Playback:**
|
||||||
|
- Selected subtitles downloaded to local temp files so the primary bar and sidebar read the same source, with cleanup on reload and quit
|
||||||
|
- False load-failure notifications suppressed
|
||||||
|
- Tray icon created on launcher-managed playback that attaches to an already-running process
|
||||||
|
- **Shortcuts:**
|
||||||
|
- Native mpv menu shortcuts disabled during managed macOS playback so configured SubMiner shortcuts work while mpv has focus
|
||||||
|
- Custom session shortcuts including `stats.markWatchedKey` wired through mpv
|
||||||
|
- Multi-line copy/mine overlay correctly focused so number keys choose the line count on macOS and Windows
|
||||||
|
- **Controller Bindings:**
|
||||||
|
- Controller config and debug shortcuts stay closed while controller support is disabled
|
||||||
|
- Binding learn mode starts from the edit pencil
|
||||||
|
- Remaps saved per controller profile
|
||||||
|
- Binding badges also start learn mode
|
||||||
|
- Row reset buttons restore individual bindings to defaults
|
||||||
|
- **Logging:**
|
||||||
|
- `logging.level` forwarded to launcher-started and Windows shortcut-started mpv sessions, covering mpv log verbosity, plugin logging, and plugin-launched app logging
|
||||||
|
- `logging.rotation` (default 7 days) and per-component `logging.files` toggles added, with mpv logs disabled by default
|
||||||
|
- Repeated IPC socket warning spam suppressed while waiting for mpv to recreate the socket
|
||||||
|
- Windows mpv IPC, subtitle track, and Yomitan diagnostics added
|
||||||
|
- **In-Player Stats:**
|
||||||
|
- Layering fixed so delete confirmations, overlay modals, and update-check dialogs appear above the stats window
|
||||||
|
- Jellyfin playback stats grouped by item metadata so watched episodes merge with matching local library titles and keep clean display names
|
||||||
|
- **WebSocket Annotations:**
|
||||||
|
- Annotation spans and token metadata stay on the annotation WebSocket
|
||||||
|
- The regular subtitle WebSocket is plain-text only
|
||||||
|
- **Subtitle Annotation Prefetching:** Cached colored annotations and character images ready sooner for live subtitle changes without delaying raw subtitle display.
|
||||||
|
- **Windows Startup Errors:** Fatal startup failures now show a native error dialog and write details to the app log instead of exiting silently.
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
- **Documentation Site:**
|
||||||
|
- Published stable docs at the site root with current development docs under `/main/`
|
||||||
|
- Fixed versioned docs navigation, archived page link handling, and local dev version routing
|
||||||
|
- Documented all previously undocumented config options including `subtitleStyle.primaryDefaultMode`, `stats.markWatchedKey`, `immersionTracking.lifetimeSummaries.*`, and all seven `mpv.*` launcher options
|
||||||
|
- Added Playback Startup Flow and Runtime Sockets diagrams to the architecture docs with cross-reference pointers in the MPV Plugin and Troubleshooting pages
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
|
||||||
|
- **Release Tooling:**
|
||||||
|
- Release-note polishing treats pending fragments and reviewed prerelease notes as a cumulative final outcome, collapsing prerelease-only fixes into the final user-facing change
|
||||||
|
- Prerelease generation reuses existing reviewed notes and merges only new fragment material
|
||||||
|
- `make clean` preserves `release/prerelease-notes.md`
|
||||||
|
- **Tests:** Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## v0.14.0 (2026-05-12)
|
## v0.14.0 (2026-05-12)
|
||||||
|
|
||||||
### 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.
|
||||||
- **Primary Subtitle Bar:** Added a `V` shortcut and mpv plugin binding to toggle the primary subtitle bar without affecting mpv's native subtitle visibility.
|
- **Primary Subtitle Bar:** Added a `V` shortcut and mpv plugin binding to toggle the primary subtitle bar without affecting mpv's native subtitle visibility.
|
||||||
- **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
|
- **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
|
||||||
|
|
||||||
@@ -58,7 +661,7 @@
|
|||||||
### Internal
|
### Internal
|
||||||
|
|
||||||
- Replaced the changelog renderer with an AI polish pass that merges related fragments and writes user-facing release notes. `CHANGELOG.md` keeps internal items in a collapsed `<details>` block; GitHub release notes omit them entirely.
|
- Replaced the changelog renderer with an AI polish pass that merges related fragments and writes user-facing release notes. `CHANGELOG.md` keeps internal items in a collapsed `<details>` block; GitHub release notes omit them entirely.
|
||||||
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain — run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
|
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain - run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -77,8 +680,8 @@
|
|||||||
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity.
|
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity.
|
||||||
- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility.
|
- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility.
|
||||||
- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard.
|
- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard.
|
||||||
- Stats: Session timeline no longer plots seek-forward/seek-backward markers — they were too noisy on sessions with lots of rewinds.
|
- Stats: Session timeline no longer plots seek-forward/seek-backward markers - they were too noisy on sessions with lots of rewinds.
|
||||||
- Stats: Replaced the "Library — Per Day" section on the Stats → Trends page with a "Library — Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
|
- Stats: Replaced the "Library - Per Day" section on the Stats → Trends page with a "Library - Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
.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 submodules 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
|
||||||
|
THUMBNAILER_SOURCE := assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer
|
||||||
LAUNCHER_OUT := dist/launcher/$(APP_NAME)
|
LAUNCHER_OUT := dist/launcher/$(APP_NAME)
|
||||||
THEME_FILE := subminer.rasi
|
THEME_FILE := subminer.rasi
|
||||||
|
THUMBNAILER_FILE := subminer-ffmpegthumbnailer.thumbnailer
|
||||||
|
|
||||||
# Default install prefix for the wrapper script.
|
# Default install prefix for the wrapper script.
|
||||||
PREFIX ?= $(HOME)/.local
|
PREFIX ?= $(HOME)/.local
|
||||||
@@ -23,6 +25,7 @@ MACOS_APP_DEST ?= $(MACOS_APP_DIR)/SubMiner.app
|
|||||||
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,13 +64,18 @@ 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" \
|
||||||
" generate-config Generate ~/.config/SubMiner/config.jsonc from centralized defaults" \
|
" generate-config Generate ~/.config/SubMiner/config.jsonc from centralized defaults" \
|
||||||
"" \
|
"" \
|
||||||
"Other targets:" \
|
"Other targets:" \
|
||||||
" deps Install JS dependencies (root + stats + texthooker-ui)" \
|
" submodules Initialize/update git submodules" \
|
||||||
|
" deps Initialize submodules and install JS dependencies (root + stats + texthooker-ui)" \
|
||||||
" uninstall-linux Remove Linux install artifacts" \
|
" uninstall-linux Remove Linux install artifacts" \
|
||||||
" uninstall-macos Remove macOS install artifacts" \
|
" uninstall-macos Remove macOS install artifacts" \
|
||||||
" uninstall-windows Remove Windows mpv plugin artifacts" \
|
" uninstall-windows Remove Windows mpv plugin artifacts" \
|
||||||
@@ -100,8 +108,10 @@ print-dirs:
|
|||||||
"MACOS_APP_SRC=$(MACOS_APP_SRC)" \
|
"MACOS_APP_SRC=$(MACOS_APP_SRC)" \
|
||||||
"MACOS_ZIP_SRC=$(MACOS_ZIP_SRC)"
|
"MACOS_ZIP_SRC=$(MACOS_ZIP_SRC)"
|
||||||
|
|
||||||
deps:
|
submodules:
|
||||||
@$(MAKE) --no-print-directory ensure-bun
|
@git submodule update --init --recursive
|
||||||
|
|
||||||
|
deps: submodules ensure-bun
|
||||||
@bun install
|
@bun install
|
||||||
@cd stats && bun install --frozen-lockfile
|
@cd stats && bun install --frozen-lockfile
|
||||||
@cd vendor/texthooker-ui && bun install --frozen-lockfile
|
@cd vendor/texthooker-ui && bun install --frozen-lockfile
|
||||||
@@ -150,18 +160,20 @@ build-macos-unsigned: deps
|
|||||||
@bun run build:mac:unsigned
|
@bun run build:mac:unsigned
|
||||||
|
|
||||||
build-launcher:
|
build-launcher:
|
||||||
@printf '%s\n' "[INFO] Bundling launcher script"
|
@printf '%s\n' "[INFO] Building launcher runtime artifacts"
|
||||||
@install -d "$(dir $(LAUNCHER_OUT))"
|
@bun run build:launcher
|
||||||
@bun build ./launcher/main.ts --target=bun --packages=bundle --outfile="$(LAUNCHER_OUT)"
|
|
||||||
@if ! head -1 "$(LAUNCHER_OUT)" | grep -q '^#!/usr/bin/env bun'; then \
|
|
||||||
{ printf '#!/usr/bin/env bun\n'; cat "$(LAUNCHER_OUT)"; } > "$(LAUNCHER_OUT).tmp" && mv "$(LAUNCHER_OUT).tmp" "$(LAUNCHER_OUT)"; \
|
|
||||||
fi
|
|
||||||
@chmod +x "$(LAUNCHER_OUT)"
|
|
||||||
@printf '%s\n' "[INFO] Launcher artifact: $(LAUNCHER_OUT)"
|
|
||||||
|
|
||||||
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,13 +203,27 @@ 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/support artifacts"
|
||||||
@install -d "$(BINDIR)"
|
@install -d "$(BINDIR)"
|
||||||
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
|
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
|
||||||
@install -d "$(LINUX_DATA_DIR)/themes"
|
@install -d "$(LINUX_DATA_DIR)/themes"
|
||||||
@install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
|
@install -d "$(LINUX_DATA_DIR)/thumbnailers"
|
||||||
|
@install -m 0644 "./$(THUMBNAILER_SOURCE)" "$(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||||
@install -d "$(LINUX_DATA_DIR)/plugin/subminer"
|
@install -d "$(LINUX_DATA_DIR)/plugin/subminer"
|
||||||
@cp -R ./plugin/subminer/. "$(LINUX_DATA_DIR)/plugin/subminer/"
|
@cp -R ./plugin/subminer/. "$(LINUX_DATA_DIR)/plugin/subminer/"
|
||||||
@if [ -n "$(APPIMAGE_SRC)" ]; then \
|
@if [ -n "$(APPIMAGE_SRC)" ]; then \
|
||||||
@@ -206,7 +232,7 @@ install-linux: build-launcher
|
|||||||
printf '%s\n' "[WARN] No release/SubMiner-*.AppImage found; skipping AppImage install"; \
|
printf '%s\n' "[WARN] No release/SubMiner-*.AppImage found; skipping AppImage install"; \
|
||||||
printf '%s\n' " Build one with: make build"; \
|
printf '%s\n' " Build one with: make build"; \
|
||||||
fi
|
fi
|
||||||
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||||
|
|
||||||
install-macos: build-launcher
|
install-macos: build-launcher
|
||||||
@printf '%s\n' "[INFO] Installing macOS wrapper/theme/app artifacts"
|
@printf '%s\n' "[INFO] Installing macOS wrapper/theme/app artifacts"
|
||||||
@@ -247,8 +273,9 @@ uninstall:
|
|||||||
uninstall-linux:
|
uninstall-linux:
|
||||||
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
||||||
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
|
@rm -f "$(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||||
@rm -rf "$(LINUX_DATA_DIR)/plugin/subminer"
|
@rm -rf "$(LINUX_DATA_DIR)/plugin/subminer"
|
||||||
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/plugin/subminer"
|
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)" " $(LINUX_DATA_DIR)/plugin/subminer"
|
||||||
|
|
||||||
uninstall-macos:
|
uninstall-macos:
|
||||||
@rm -f "$(BINDIR)/subminer"
|
@rm -f "$(BINDIR)/subminer"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
# SubMiner
|
# SubMiner
|
||||||
|
|
||||||
Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv.
|
Look up words with Yomitan, mine them to Anki, and track your immersion without leaving mpv
|
||||||
|
|
||||||
[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)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ Look up words with Yomitan, export to Anki in one key, track your immersion —
|
|||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://www.typescriptlang.org)
|
[](https://www.typescriptlang.org)
|
||||||
|
|
||||||
[](https://github.com/user-attachments/assets/89e61895-e2b7-4b47-8d50-a35afe4132b2)
|
[](https://github.com/user-attachments/assets/7abab8a9-4e4e-4f06-9f3c-9783e15a3807)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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 in the subtitles to open the full Yomitan popup with definitions, pitch accent, and frequency data. SubMiner bundles its own Yomitan, separate from any browser install.
|
||||||
|
|
||||||
<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">
|
||||||
@@ -33,7 +33,7 @@ Yomitan runs inside the overlay. Trigger a lookup on any word for full dictionar
|
|||||||
|
|
||||||
### Instant Anki Mining
|
### Instant Anki Mining
|
||||||
|
|
||||||
Create an Anki card with the sentence, audio clip, screenshot, and machine translation from the exact playback moment with one key press, click, or controller input.
|
Create an Anki card from the exact playback moment with one key press, click, or controller input. SubMiner fills in the sentence, an audio clip, and a screenshot or animated image.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="docs-site/public/screenshots/one-key-mining.png" width="800" alt="Anki card created from SubMiner with sentence, audio, and screenshot">
|
<img src="docs-site/public/screenshots/one-key-mining.png" width="800" alt="Anki card created from SubMiner with sentence, audio, and screenshot">
|
||||||
@@ -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.
|
Subtitles are annotated as they play with frequency highlighting, JLPT tags, N+1 targeting, and character names from a generated dictionary. Particles and grammar-only tokens stay plain so the words worth learning stand out.
|
||||||
|
|
||||||
<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.
|
A stats dashboard tracks watch time, vocabulary growth, mining throughput, session history, and trends. Everything stays on your machine, with 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">
|
||||||
@@ -61,22 +61,12 @@ Local stats dashboard — watch time, anime library, vocabulary growth, mining t
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
### Playlist Browser
|
|
||||||
|
|
||||||
Browse sibling episode files and the active mpv queue in one overlay modal. Open it with `Ctrl+Alt+P` to append episodes from the current directory, jump to queued items, remove entries, or reorder the playlist without leaving playback.
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="docs-site/public/screenshots/playlist-browser.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### Integrations
|
### Integrations
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>YouTube</b></td>
|
<td><b>YouTube</b></td>
|
||||||
<td>Auto-loaded yt-dlp subtitle tracks at startup with config-driven primary/secondary language priorities and a manual overlay picker on demand (<code>Ctrl+Alt+C</code>)</td>
|
<td>Play YouTube URLs with subtitle tracks picked by your language priorities, or choose tracks yourself in the overlay picker (<code>Ctrl+Alt+C</code>). Requires <code>yt-dlp</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>AniList</b></td>
|
<td><b>AniList</b></td>
|
||||||
@@ -84,91 +74,93 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>Jellyfin</b></td>
|
<td><b>Jellyfin</b></td>
|
||||||
<td>Browse, launch, and cast media from your Jellyfin server with setup and discovery controls in the app tray</td>
|
<td>Browse your Jellyfin library, or cast to SubMiner from any Jellyfin client. Setup and discovery live in the tray menu</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>Jimaku</b></td>
|
<td><b>Jimaku</b></td>
|
||||||
<td>Search and download Japanese subtitles</td>
|
<td>Search and download Japanese subtitles. Requires a free Jimaku API key</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>TsukiHime</b></td>
|
||||||
|
<td>Search and download subtitles extracted from anime releases, with Japanese and secondary-language tabs (<code>Ctrl+Shift+T</code>). Requires <code>xz</code> on your <code>PATH</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Subtitle generation</b></td>
|
||||||
|
<td>Transcribe a video's audio into Japanese subtitles locally from the generation modal (<code>Ctrl+Shift+G</code>), the subtitle sidebar, or the launcher. SubMiner can download models for you; optional Silero speech detection helps focus on dialogue. Requires whisper.cpp and FFmpeg. <a href="https://docs.subminer.moe/subtitle-generation">Setup guide</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>AniSkip</b></td>
|
||||||
|
<td>Automatic intro detection with chapter markers and a one-key skip (<code>TAB</code> by default)</td>
|
||||||
</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>Retime a subtitle against the audio or another subtitle track (<code>Ctrl+Alt+S</code>). Requires <code>alass</code> or <code>ffsubsync</code>; set <code>subsync.alass_path</code> or <code>subsync.ffsubsync_path</code> if they are not in <code>/usr/bin</code></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>
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="docs-site/public/screenshots/texthooker.png" width="800" alt="Texthooker page receiving annotated subtitle lines via WebSocket">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
| | Required | Recommended | Optional |
|
SubMiner runs on Linux, macOS 11+, and Windows 10+. Only **mpv** is required to run it (plus `fuse2` for the Linux AppImage). Mining cards also needs Anki with the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on. Everything else is optional.
|
||||||
| -------------- | --------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — | — |
|
|
||||||
| **Processing** | — | `ffmpeg` (audio clips & screenshots) | `mecab` + `mecab-ipadic` (annotation POS filtering), `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
|
|
||||||
| **Media** | — | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
|
|
||||||
| **Selection** | — | — | `fzf` / `rofi` |
|
|
||||||
|
|
||||||
> [!TIP]
|
| 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 draws over |
|
||||||
> [!NOTE]
|
| fuse2 | Required (Linux) | Running the AppImage |
|
||||||
> [`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.
|
| Anki + AnkiConnect | Required to mine | Card creation from the Yomitan popup |
|
||||||
|
| ffmpeg | Recommended | Audio clips and screenshots on cards |
|
||||||
**Platform-specific:**
|
| MeCab + mecab-ipadic | Recommended | More accurate N+1, JLPT, and frequency highlighting |
|
||||||
|
| xdotool + xwininfo | Required (X11) | Window tracking on desktops other than Hyprland or Sway |
|
||||||
| Linux | macOS | Windows |
|
| yt-dlp | Optional | YouTube playback |
|
||||||
| ------------------------------------------------------------ | ------------------------ | ------------- |
|
| xz | Optional | TsukiHime subtitle downloads (most Linux distros already have it) |
|
||||||
| Hyprland (`hyprctl`) · X11/Xwayland (`xdotool` + `xwininfo`) | Accessibility permission | No extra deps |
|
| alass / ffsubsync | Optional | Subtitle sync |
|
||||||
|
| whisper.cpp | Optional | [Subtitle generation](https://docs.subminer.moe/subtitle-generation) |
|
||||||
> [!NOTE]
|
| guessit | Optional | Better title, season, and episode detection for AniSkip and AniList |
|
||||||
> **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.
|
| fzf / rofi | Optional | Video picker in the `subminer` launcher (rofi is Linux only) |
|
||||||
|
| chafa, ffmpegthumbnailer | Optional | Thumbnail previews in the launcher pickers |
|
||||||
|
|
||||||
<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
|
||||||
# Optional
|
paru -S --needed mecab-git mecab-ipadic # MeCab is only in the AUR
|
||||||
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>
|
On desktops other than Hyprland or Sway, also install `xdotool` and `xorg-xwininfo`.
|
||||||
|
|
||||||
<details>
|
**macOS:**
|
||||||
<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:**
|
||||||
|
|
||||||
</details>
|
```powershell
|
||||||
|
winget install shinchiro.mpv
|
||||||
|
winget install Gyan.FFmpeg
|
||||||
|
```
|
||||||
|
|
||||||
<details>
|
Then reopen your terminal and check `mpv --version` and `ffmpeg -version`. ffmpeg must be on `PATH`; mpv does not have to be. If `mpv` is not found, either add its folder (usually `%LOCALAPPDATA%\Programs\mpv`) to `PATH` or enter the full path to `mpv.exe` during first-run setup.
|
||||||
<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`.
|
[Scoop](https://scoop.sh) is the alternative if you want one package manager for everything. It is the only one that also carries `xz`:
|
||||||
|
|
||||||
Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for additional metadata enrichment.
|
```powershell
|
||||||
|
scoop bucket add extras
|
||||||
|
scoop install extras/mpv main/ffmpeg main/yt-dlp main/xz
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [installation guide](https://docs.subminer.moe/installation#_1-install-requirements) for Ubuntu, Debian, and Fedora commands and the full optional package lists.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -176,7 +168,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,11 +177,7 @@ Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download
|
|||||||
paru -S subminer-bin
|
paru -S subminer-bin
|
||||||
```
|
```
|
||||||
|
|
||||||
Or manually:
|
Includes the AppImage and the `subminer` command.
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://aur.archlinux.org/subminer-bin.git && cd subminer-bin && makepkg -si
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -199,40 +187,26 @@ 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 \
|
|
||||||
&& chmod +x ~/.local/bin/subminer
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
The AppImage is all you need. First-run setup can install the optional `subminer` command, which runs on a copy of Bun bundled with the app, so you do not need Bun installed.
|
||||||
> The `subminer` wrapper uses a [Bun](https://bun.sh) shebang. First-run setup can optionally install Bun and the launcher into an existing writable PATH directory.
|
|
||||||
|
|
||||||
</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):
|
Then enable SubMiner under **System Settings > Privacy & Security > Accessibility**, or the overlay cannot follow the mpv window. If macOS blocks the first launch, right-click the app and choose **Open**.
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/.local/bin
|
|
||||||
curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o ~/.local/bin/subminer \
|
|
||||||
&& chmod +x ~/.local/bin/subminer
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The `subminer` launcher uses a [Bun](https://bun.sh) shebang. First-run setup can optionally install Bun and the launcher into an existing writable PATH directory. Make sure `~/.local/bin` is on your PATH before installing there.
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Windows</b></summary>
|
<summary><b>Windows</b></summary>
|
||||||
|
|
||||||
Download the latest installer (`.exe`) [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`.
|
Download and run the latest installer (`SubMiner-<version>.exe`) from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). A portable `.zip` is also available.
|
||||||
|
|
||||||
**Note:** On Windows the recommended way to run playback is with the **SubMiner mpv** shortcut created during first-run setup. First-run setup can also optionally install Bun and a `subminer.cmd` command shim to your user PATH, so new terminals can run `subminer` without adding `SubMiner.exe` to PATH.
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -243,28 +217,32 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### 2. First Launch
|
### 2. Launch & Set Up
|
||||||
|
|
||||||
|
Start SubMiner and the setup window opens on first launch. It creates your config file, imports Yomitan dictionaries (you need at least one for lookups), and can install the `subminer` command. On Windows it also creates a **SubMiner mpv** shortcut.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer app --setup # launch the first-run setup wizard
|
subminer app --setup # AUR
|
||||||
|
~/.local/bin/SubMiner.AppImage --setup # AppImage
|
||||||
```
|
```
|
||||||
|
|
||||||
SubMiner creates a default config, starts in the system tray, and opens a setup popup that walks you through installing Yomitan dictionaries. The setup popup can also optionally install Bun and the `subminer` command-line launcher; those choices do not block setup completion.
|
On **macOS**, open `SubMiner.app` from `/Applications`. On **Windows**, run SubMiner from the Start menu. To reopen setup later, run `subminer app --setup`.
|
||||||
|
|
||||||
> [!NOTE]
|
For card creation, keep Anki open with AnkiConnect installed. SubMiner connects to it at its default address with no extra setup.
|
||||||
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
|
|
||||||
|
|
||||||
### 3. Mine
|
### 3. Mine
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer video.mkv # play video with overlay
|
subminer video.mkv # play a video with SubMiner
|
||||||
subminer --start video.mkv # explicit overlay start
|
subminer /path/to/dir # pick a file with fzf
|
||||||
subminer stats # open immersion dashboard
|
subminer -R /path/to/dir # pick a file with rofi (Linux only)
|
||||||
subminer stats -b # stats daemon in background
|
subminer -H # watch history: replay, next, or previous episode
|
||||||
subminer stats -s # stop background stats daemon
|
subminer doctor # check your setup
|
||||||
```
|
```
|
||||||
|
|
||||||
On **Windows**, use the **SubMiner mpv** shortcut created during first-run setup — double-click it to open mpv, or drag a video file onto it. You can also run `SubMiner.exe --launch-mpv` from a terminal.
|
On **Windows**, double-click the **SubMiner mpv** shortcut or drag a video file onto it.
|
||||||
|
|
||||||
|
Starting mpv some other way? See [Launching mpv yourself](https://docs.subminer.moe/installation#launching-mpv-yourself) for the IPC socket option the overlay needs.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -278,9 +256,11 @@ SubMiner builds on the work of these open-source projects:
|
|||||||
|
|
||||||
| Project | Role |
|
| Project | Role |
|
||||||
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||||
|
| [ani-skip](https://github.com/synacktraa/ani-skip) | AniSkip API client for anime intro/outro skip timestamps |
|
||||||
| [Anacreon-Script](https://github.com/friedrich-de/Anacreon-Script) | Inspiration for the mining workflow |
|
| [Anacreon-Script](https://github.com/friedrich-de/Anacreon-Script) | Inspiration for the mining workflow |
|
||||||
| [asbplayer](https://github.com/killergerbah/asbplayer) | Inspiration for subtitle sidebar and logic for YouTube subtitle parsing |
|
| [asbplayer](https://github.com/killergerbah/asbplayer) | Inspiration for subtitle sidebar and logic for YouTube subtitle parsing |
|
||||||
| [Bee's Character Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) | Character name recognition in subtitles |
|
| [Bee's Character Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) | Character name recognition in subtitles |
|
||||||
|
| [Bun](https://github.com/oven-sh/bun) | Bundled runtime for the `subminer` command-line launcher |
|
||||||
| [GameSentenceMiner](https://github.com/bpwhelan/GameSentenceMiner) | Inspiration for Electron overlay with Yomitan integration |
|
| [GameSentenceMiner](https://github.com/bpwhelan/GameSentenceMiner) | Inspiration for Electron overlay with Yomitan integration |
|
||||||
| [jellyfin-mpv-shim](https://github.com/jellyfin/jellyfin-mpv-shim) | Jellyfin integration |
|
| [jellyfin-mpv-shim](https://github.com/jellyfin/jellyfin-mpv-shim) | Jellyfin integration |
|
||||||
| [Jimaku.cc](https://jimaku.cc) | Japanese subtitle search and downloads |
|
| [Jimaku.cc](https://jimaku.cc) | Japanese subtitle search and downloads |
|
||||||
@@ -290,4 +270,6 @@ SubMiner builds on the work of these open-source projects:
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[GNU General Public License v3.0](LICENSE)
|
SubMiner is released under the [GNU General Public License v3.0](LICENSE).
|
||||||
|
|
||||||
|
Release packages also bundle an unmodified copy of [Bun](https://github.com/oven-sh/bun), which is MIT licensed and statically links JavaScriptCore (LGPL 2.0) and TinyCC (LGPL 2.1). Its license texts and third-party notices ship inside the app under `resources/bun/licenses`, and each release publishes `bun-v1.3.5-source.tar.gz` with the corresponding source. See [Bundled Bun runtime](https://docs.subminer.moe/installation#bundled-bun-runtime).
|
||||||
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 253 B |
|
Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 366 B |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 23 MiB |
|
Before Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.8 MiB |
@@ -0,0 +1,4 @@
|
|||||||
|
[Thumbnailer Entry]
|
||||||
|
TryExec=ffmpegthumbnailer
|
||||||
|
Exec=ffmpegthumbnailer -i %i -o %o -s %s -f
|
||||||
|
MimeType=video/matroska;video/matroska-3d;video/x-matroska;video/x-matroska-3d;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"version": "1.3.5",
|
||||||
|
"bunRevision": "1e86cebd74a5723e818b5c0555276b646bcf0e4c",
|
||||||
|
"releaseTagCommit": "fa5a5bbe556a4bda5bde77b4013aa6c3bb4ec9ab",
|
||||||
|
"artifacts": {
|
||||||
|
"darwin-arm64": {
|
||||||
|
"file": "bun-darwin-aarch64.zip",
|
||||||
|
"sha256": "db17588a4aea8804856825d4bead3f05e1f37276ca606f37e369b4f72f35d3fb"
|
||||||
|
},
|
||||||
|
"darwin-x64": {
|
||||||
|
"file": "bun-darwin-x64-baseline.zip",
|
||||||
|
"sha256": "34b9a56b851058dafa1bc9d61233f2c383aa996889bba30b3180f5ccc2cff1b2"
|
||||||
|
},
|
||||||
|
"linux-arm64": {
|
||||||
|
"file": "bun-linux-aarch64.zip",
|
||||||
|
"sha256": "ed01000f85bd97785228ad2845dc92a1860b8054856826d7317690ac8f8ee74b"
|
||||||
|
},
|
||||||
|
"linux-x64": {
|
||||||
|
"file": "bun-linux-x64-baseline.zip",
|
||||||
|
"sha256": "6bddacd6a65855698b9816f2d74871eda4dd0b7fa921140c6445248f94a742fd"
|
||||||
|
},
|
||||||
|
"win32-x64": {
|
||||||
|
"file": "bun-windows-x64-baseline.zip",
|
||||||
|
"sha256": "bf447dcc3b06aba9b9706a9db46fcd65e06a4d47d31439922313825d06eb47ca"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"licenseInventoryStatus": "source-and-notices-pinned-to-binary-revision",
|
||||||
|
"sourceManifest": "build/bun-source-manifest.json",
|
||||||
|
"correspondingSourceAsset": "bun-v1.3.5-source.tar.gz"
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"version": "1.3.5",
|
||||||
|
"releaseTag": "bun-v1.3.5",
|
||||||
|
"releaseTagCommit": "fa5a5bbe556a4bda5bde77b4013aa6c3bb4ec9ab",
|
||||||
|
"bunRevision": "1e86cebd74a5723e818b5c0555276b646bcf0e4c",
|
||||||
|
"archiveName": "bun-v1.3.5-source.tar.gz",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "bun",
|
||||||
|
"repository": "oven-sh/bun",
|
||||||
|
"revision": "1e86cebd74a5723e818b5c0555276b646bcf0e4c",
|
||||||
|
"destination": "bun",
|
||||||
|
"sha256": "3a766087902a62a4920e5ac5452dbc7143ff97ea19721f945146629fc9f2d82c",
|
||||||
|
"licensePaths": ["LICENSE.md"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WebKit",
|
||||||
|
"repository": "oven-sh/WebKit",
|
||||||
|
"revision": "6d0f3aac0b817cc01a846b3754b21271adedac12",
|
||||||
|
"destination": "bun/vendor/WebKit",
|
||||||
|
"transport": "git-sparse",
|
||||||
|
"exclude": ["JSTests", "LayoutTests", "ManualTests", "PerformanceTests", "WebDriverTests"],
|
||||||
|
"licensePaths": ["Source/JavaScriptCore/COPYING.LIB"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "boringssl",
|
||||||
|
"repository": "oven-sh/boringssl",
|
||||||
|
"revision": "f1ffd9e83d4f5c28a9c70d73f9a4e6fcf310062f",
|
||||||
|
"destination": "bun/vendor/boringssl",
|
||||||
|
"sha256": "af8fd325793bb261c70114d09a24f36092fbe252be8ec040f0054ec9455f0ea3",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "brotli",
|
||||||
|
"repository": "google/brotli",
|
||||||
|
"revision": "ed738e842d2fbdf2d6459e39267a633c4a9b2f5d",
|
||||||
|
"upstreamReference": "v1.1.0",
|
||||||
|
"destination": "bun/vendor/brotli",
|
||||||
|
"sha256": "aaa739962a45b508b2e783b915e6b2b57ed3b12bd4b0feac73acfb144dffa54f",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cares",
|
||||||
|
"repository": "c-ares/c-ares",
|
||||||
|
"revision": "3ac47ee46edd8ea40370222f91613fc16c434853",
|
||||||
|
"destination": "bun/vendor/cares",
|
||||||
|
"sha256": "8c94116cb366ae4a44e487da4d9f7e736287d329efa6f88fdf077cd2d0a2e4b8",
|
||||||
|
"licensePaths": ["LICENSE.md"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hdrhistogram",
|
||||||
|
"repository": "HdrHistogram/HdrHistogram_c",
|
||||||
|
"revision": "be60a9987ee48d0abf0d7b6a175bad8d6c1585d1",
|
||||||
|
"destination": "bun/vendor/hdrhistogram",
|
||||||
|
"sha256": "811c5e5ae5303a75ade50688880af6aad5d2f951ec5785f68186bd18635cdfc9",
|
||||||
|
"licensePaths": ["COPYING.txt", "LICENSE.txt"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "highway",
|
||||||
|
"repository": "google/highway",
|
||||||
|
"revision": "ac0d5d297b13ab1b89f48484fc7911082d76a93f",
|
||||||
|
"destination": "bun/vendor/highway",
|
||||||
|
"sha256": "a7a816f4b62a0414ff0d39c0a8875847468dd6f7c9ad71781a24469a756cdea7",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "libarchive",
|
||||||
|
"repository": "libarchive/libarchive",
|
||||||
|
"revision": "9525f90ca4bd14c7b335e2f8c84a4607b0af6bdf",
|
||||||
|
"destination": "bun/vendor/libarchive",
|
||||||
|
"sha256": "944db9ab58a3cbdb5d947db4f04a3cc15f83b3147fcf7830ada592ba8d4a102c",
|
||||||
|
"licensePaths": ["COPYING"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "libdeflate",
|
||||||
|
"repository": "ebiggers/libdeflate",
|
||||||
|
"revision": "c8c56a20f8f621e6a966b716b31f1dedab6a41e3",
|
||||||
|
"destination": "bun/vendor/libdeflate",
|
||||||
|
"sha256": "1e5cc06bdbf3e1245d8b89c9e3588f507e3c8bc53fe8b8229770a9e8661dea81",
|
||||||
|
"licensePaths": ["COPYING"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "libuv",
|
||||||
|
"repository": "libuv/libuv",
|
||||||
|
"revision": "f3ce527ea940d926c40878ba5de219640c362811",
|
||||||
|
"destination": "bun/vendor/libuv",
|
||||||
|
"sha256": "46040d51e8aa86a7c84e377e224d427c35022ec6e3ed8ed399493b0d8574d0cf",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lolhtml",
|
||||||
|
"repository": "cloudflare/lol-html",
|
||||||
|
"revision": "d64457d9ff0143deef025d5df7e8586092b9afb7",
|
||||||
|
"destination": "bun/vendor/lolhtml",
|
||||||
|
"sha256": "893b77b460f4c4f4634c973f72cf35e38ebe30646d14a92db3f50ee7e586c0ad",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lshpack",
|
||||||
|
"repository": "litespeedtech/ls-hpack",
|
||||||
|
"revision": "8905c024b6d052f083a3d11d0a169b3c2735c8a1",
|
||||||
|
"destination": "bun/vendor/lshpack",
|
||||||
|
"sha256": "07d8bf901bb1b15543f38eabd23938519e1210eebadb52f3d651d6ef130ef973",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mimalloc",
|
||||||
|
"repository": "oven-sh/mimalloc",
|
||||||
|
"revision": "1beadf9651a7bfdec6b5367c380ecc3fe1c40d1a",
|
||||||
|
"destination": "bun/vendor/mimalloc",
|
||||||
|
"sha256": "317ec2a83462ece78c344c4955c6fee103b412a8e6a0d6ddf9ec8963ed9c0881",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "picohttpparser",
|
||||||
|
"repository": "h2o/picohttpparser",
|
||||||
|
"revision": "066d2b1e9ab820703db0837a7255d92d30f0c9f5",
|
||||||
|
"destination": "bun/vendor/picohttpparser",
|
||||||
|
"sha256": "637ff2ab6f5c7f7e05a5b5dc393d5cf2fea8d4754fcaceaaf935ffff5c1323ee",
|
||||||
|
"licensePaths": ["picohttpparser.c", "picohttpparser.h"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tinycc",
|
||||||
|
"repository": "oven-sh/tinycc",
|
||||||
|
"revision": "29985a3b59898861442fa3b43f663fc1af2591d7",
|
||||||
|
"destination": "bun/vendor/tinycc",
|
||||||
|
"sha256": "813cc09aafd6cea9c1ae6b745781e6b27576a0c103f49b63b5c8a732bbe3d290",
|
||||||
|
"licensePaths": ["COPYING"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zlib",
|
||||||
|
"repository": "cloudflare/zlib",
|
||||||
|
"revision": "886098f3f339617b4243b286f5ed364b9989e245",
|
||||||
|
"destination": "bun/vendor/zlib",
|
||||||
|
"sha256": "14bf449df8308696af52f87de88f54c9eb9c91ec5e280587f9d37f7a3f6ed9bb",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zstd",
|
||||||
|
"repository": "facebook/zstd",
|
||||||
|
"revision": "f8745da6ff1ad1e7bab384bd1f9d742439278e99",
|
||||||
|
"destination": "bun/vendor/zstd",
|
||||||
|
"sha256": "4b0bd1f0cfb25e61b9103c35f27395530ff5b4c0d2513a00fd745849e85ea52c",
|
||||||
|
"licensePaths": ["COPYING", "LICENSE"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -7,58 +7,69 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
"@fontsource-variable/geist-mono": "^5.2.7",
|
"@fontsource-variable/geist-mono": "^5.2.7",
|
||||||
"@xhayper/discord-rpc": "^1.3.3",
|
"@xhayper/discord-rpc": "^1.3.4",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.20.0",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"electron-updater": "^6.8.3",
|
"electron-updater": "^6.8.9",
|
||||||
"hono": "^4.12.7",
|
"hono": "^4.12.28",
|
||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"koffi": "^2.15.6",
|
"koffi": "^2.15.6",
|
||||||
"libsql": "^0.5.22",
|
"libsql": "^0.5.22",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.21.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.3.0",
|
"@electron/asar": "3.4.1",
|
||||||
|
"@types/node": "^24.10.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"electron": "39.8.6",
|
"electron": "42.10.0",
|
||||||
"electron-builder": "26.8.2",
|
"electron-builder": "26.16.1",
|
||||||
"esbuild": "^0.25.12",
|
"esbuild": "^0.25.12",
|
||||||
|
"eslint": "^10.8.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"undici": "7.29.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"patchedDependencies": {
|
||||||
|
"@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch",
|
||||||
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@xmldom/xmldom": "0.8.12",
|
"@xmldom/xmldom": "0.8.15",
|
||||||
"app-builder-lib": "26.8.2",
|
"app-builder-lib": "26.16.1",
|
||||||
"electron-builder-squirrel-windows": "26.8.2",
|
"brace-expansion": "5.0.11",
|
||||||
|
"electron-builder-squirrel-windows": "26.16.1",
|
||||||
|
"fast-uri": "3.1.7",
|
||||||
|
"form-data": "4.0.6",
|
||||||
|
"ip-address": "10.2.0",
|
||||||
|
"js-yaml": "4.3.2",
|
||||||
"lodash": "4.18.0",
|
"lodash": "4.18.0",
|
||||||
"minimatch": "10.2.3",
|
"minimatch": "10.2.5",
|
||||||
"picomatch": "4.0.4",
|
"picomatch": "4.0.4",
|
||||||
"tar": "7.5.11",
|
"tar": "7.5.21",
|
||||||
|
"tmp": "0.2.7",
|
||||||
|
"undici": "7.29.1",
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="],
|
|
||||||
|
|
||||||
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
|
|
||||||
|
|
||||||
"@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
"@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
||||||
|
|
||||||
"@discordjs/rest": ["@discordjs/rest@2.6.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg=="],
|
"@discordjs/rest": ["@discordjs/rest@2.6.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.27.0" } }, "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg=="],
|
||||||
|
|
||||||
"@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
|
"@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
|
||||||
|
|
||||||
|
"@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.4", "", {}, "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg=="],
|
||||||
|
|
||||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
"@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="],
|
"@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="],
|
||||||
|
|
||||||
"@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="],
|
"@electron/rebuild": ["@electron/rebuild@4.2.0", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ=="],
|
||||||
|
|
||||||
"@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="],
|
"@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="],
|
||||||
|
|
||||||
@@ -116,11 +127,33 @@
|
|||||||
|
|
||||||
"@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.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
|
||||||
|
|
||||||
|
"@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.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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/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=="],
|
||||||
|
|
||||||
@@ -148,11 +181,15 @@
|
|||||||
|
|
||||||
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
|
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
|
||||||
|
|
||||||
"@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="],
|
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||||
|
|
||||||
"@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="],
|
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="],
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
|
||||||
|
|
||||||
|
"@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="],
|
||||||
|
|
||||||
|
"@peculiar/webcrypto": ["@peculiar/webcrypto@1.7.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "tslib": "^2.8.1", "webcrypto-core": "^1.9.2" } }, "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ=="],
|
||||||
|
|
||||||
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
|
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
|
||||||
|
|
||||||
@@ -166,53 +203,51 @@
|
|||||||
|
|
||||||
"@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/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
|
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
|
||||||
|
|
||||||
"@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="],
|
|
||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
|
||||||
|
|
||||||
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
|
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
|
||||||
|
|
||||||
"@xhayper/discord-rpc": ["@xhayper/discord-rpc@1.3.3", "", { "dependencies": { "@discordjs/rest": "^2.6.1", "@vladfrangu/async_event_emitter": "^2.4.7", "discord-api-types": "^0.38.42", "ws": "^8.20.0" } }, "sha512-Ih48GHiua7TtZgKO+f0uZPhCeQqb84fY2qUys/oMh8UbUfiUkUJLVCmd/v2AK0/pV33euh0aqSXo7+9LiPSwGw=="],
|
"@xhayper/discord-rpc": ["@xhayper/discord-rpc@1.3.4", "", { "dependencies": { "@discordjs/rest": "^2.6.1", "@vladfrangu/async_event_emitter": "^2.4.7", "discord-api-types": "^0.38.47", "ws": "^8.20.0" } }, "sha512-ff0uEXuibh9wi+l4vOj7xInLUjtlTaQBje/SCyQkeXZ0j2V0y+Zge5PQIQFRHH9TjjGaYJkTofEcQhncM2q7/w=="],
|
||||||
|
|
||||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],
|
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.15", "", {}, "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA=="],
|
||||||
|
|
||||||
"abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
|
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
|
||||||
|
|
||||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
"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@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
|
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="],
|
"app-builder-lib": ["app-builder-lib@26.16.1", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.4", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^1.8.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.16.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.16.1", "electron-builder-squirrel-windows": "26.16.1" } }, "sha512-FhaO6YOup01ZfQW0Z6gt3AyukJjv1gW4uFK47jTgwcHZKqyN/fSlK2LqPf9tAeZYLP2bRJLDzeOkRImsw2X4Pg=="],
|
||||||
|
|
||||||
"app-builder-lib": ["app-builder-lib@26.8.2", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.2", "electron-builder-squirrel-windows": "26.8.2" } }, "sha512-z3ptLzJwNl35fyR0wxv4qWOfZuU36VysYHnbs8PDtf8S0QzIl2OWimdDFVmCxYMkIV1k/RT9CeTgcP7oUznFOw=="],
|
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
|
"asn1js": ["asn1js@3.0.10", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.5", "tslib": "^2.8.1" } }, "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg=="],
|
||||||
|
|
||||||
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
|
|
||||||
|
|
||||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
@@ -222,29 +257,27 @@
|
|||||||
|
|
||||||
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
|
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
|
||||||
|
|
||||||
"axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
|
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
|
||||||
|
|
||||||
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
|
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
"brace-expansion": ["brace-expansion@5.0.11", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-awigjhi6cLTh90bdw6+QJ9CtmJmyYhEIi70iCbc8Rozn04Fw9FeQIBjv/E22FFGuCGx1bLJyUfB64x/szUSXUg=="],
|
||||||
|
|
||||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
|
||||||
|
|
||||||
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
|
||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="],
|
"builder-util": ["builder-util@26.16.0", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-RLyJhB7Si3YkzKR9ubQslWuXW3Vhs3CGe1i+SeixBZ0qTd1mk3XBmssvY22TlB6CS5blyko8Gu1JzpYk8UkYAg=="],
|
||||||
|
|
||||||
"builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="],
|
"builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
|
||||||
|
|
||||||
"cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="],
|
"bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="],
|
||||||
|
|
||||||
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
|
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
|
||||||
|
|
||||||
@@ -260,16 +293,8 @@
|
|||||||
|
|
||||||
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
|
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
|
||||||
|
|
||||||
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
|
|
||||||
|
|
||||||
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
|
||||||
|
|
||||||
"cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
|
|
||||||
|
|
||||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
|
||||||
|
|
||||||
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
|
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
@@ -284,8 +309,6 @@
|
|||||||
|
|
||||||
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||||
|
|
||||||
"crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="],
|
|
||||||
|
|
||||||
"cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="],
|
"cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
@@ -294,7 +317,7 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
||||||
|
|
||||||
@@ -310,11 +333,9 @@
|
|||||||
|
|
||||||
"dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="],
|
"dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="],
|
||||||
|
|
||||||
"discord-api-types": ["discord-api-types@0.38.43", "", {}, "sha512-sSoBf/nK6m7BGtw65mi+QBuvEWaHE8MMziFLqWL+gT6ME/BLg34dRSVKS3Husx40uU06bvxUc3/X+D9Y6/zAbw=="],
|
"discord-api-types": ["discord-api-types@0.38.49", "", {}, "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg=="],
|
||||||
|
|
||||||
"dmg-builder": ["dmg-builder@26.8.2", "", { "dependencies": { "app-builder-lib": "26.8.2", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-DaWI+p4DOqiFVZFMovdGYammBOyJAiHHFWUTQ0Z7gNc0twfdIN0LvyJ+vFsgZEDR1fjgbpCj690IVtbYIsZObQ=="],
|
"dmg-builder": ["dmg-builder@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-pnI/3Qb24Uk+rMTgIUrsVUKosVgwmBUdF8Zeb8TexOSbpq8MWc7v6l+n+FrEqVkjNZwzBN+XpDS9ENgZ/rkWAw=="],
|
||||||
|
|
||||||
"dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="],
|
|
||||||
|
|
||||||
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
|
|
||||||
@@ -322,29 +343,27 @@
|
|||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
"duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="],
|
||||||
|
|
||||||
"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.10.0", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-yBWnZO8tKId0ADvGa4+imPmm3+8Kfs8MtkUPhQQJz1AjTnUUDwCTWkQEU0CrArVVL97Jo4Q/0mVcLHciicBDGQ=="],
|
||||||
|
|
||||||
"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.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.16.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-LrLK65QX5PUYYODXqp23FKrV7CILTtVY7mrJckNknO9jLNSMiqFkKbSMiDRw4CjOADMPVDdWLxY4mezOZWswxg=="],
|
||||||
|
|
||||||
"electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.2", "", { "dependencies": { "app-builder-lib": "26.8.2", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-kXhajX6DzdIQcTlctVTKoG1oO39JhWcTG0lH7ZEJ4FzPaKJy7KFNfNJUd5BoEmLjv5GlrRZpEOYnniD+LcwNJA=="],
|
"electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "electron-winstaller": "5.4.0" } }, "sha512-w0y44wSaT1l6R7CAGmeHn4nHPfvzDyCAU1xJyi1w9SbPYJpYn76SmHDzqHf8Y7l91cPWTdPYBpGQtB2T5mJ08A=="],
|
||||||
|
|
||||||
"electron-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.16.0", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-Vt3KzQIiw9BImvNOYtndg9Mjki+tl4+1sQiC/+G5j8khWaENOJFWodiB+sUl6yyHwtd37avehskdtPw7f8y/+Q=="],
|
||||||
|
|
||||||
"electron-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-updater": ["electron-updater@6.8.9", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig=="],
|
||||||
|
|
||||||
"electron-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=="],
|
||||||
|
|
||||||
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
|
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -364,32 +383,50 @@
|
|||||||
|
|
||||||
"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.8.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.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@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.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ=="],
|
||||||
|
|
||||||
|
"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=="],
|
|
||||||
|
|
||||||
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
|
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
|
|
||||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
|
||||||
|
|
||||||
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
||||||
|
|
||||||
"fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
|
|
||||||
|
|
||||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
@@ -404,6 +441,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=="],
|
||||||
@@ -422,9 +461,9 @@
|
|||||||
|
|
||||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||||
|
|
||||||
"hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="],
|
"hono": ["hono@4.12.28", "", {}, "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA=="],
|
||||||
|
|
||||||
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
||||||
|
|
||||||
@@ -434,13 +473,9 @@
|
|||||||
|
|
||||||
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
|
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
|
||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||||
|
|
||||||
"iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
|
||||||
|
|
||||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
|
||||||
|
|
||||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
|
|
||||||
@@ -448,30 +483,30 @@
|
|||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
"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-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||||
|
|
||||||
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
|
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
|
||||||
|
|
||||||
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||||
|
|
||||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
|
||||||
|
|
||||||
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
|
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
|
||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
"js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="],
|
||||||
|
|
||||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -486,24 +521,24 @@
|
|||||||
|
|
||||||
"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.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
|
||||||
|
|
||||||
"lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
|
"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=="],
|
|
||||||
|
|
||||||
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
|
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||||
|
|
||||||
"magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="],
|
"magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="],
|
||||||
|
|
||||||
"make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="],
|
|
||||||
|
|
||||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
@@ -514,43 +549,31 @@
|
|||||||
|
|
||||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
|
||||||
|
|
||||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@10.2.3", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg=="],
|
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||||
|
|
||||||
"minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="],
|
|
||||||
|
|
||||||
"minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="],
|
|
||||||
|
|
||||||
"minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="],
|
|
||||||
|
|
||||||
"minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
|
|
||||||
|
|
||||||
"minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="],
|
|
||||||
|
|
||||||
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||||
|
|
||||||
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
|
|
||||||
|
|
||||||
"node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="],
|
"node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="],
|
||||||
|
|
||||||
"node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="],
|
"node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="],
|
||||||
|
|
||||||
"nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
|
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
|
||||||
|
|
||||||
|
"nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
|
||||||
|
|
||||||
"normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
|
"normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
|
||||||
|
|
||||||
@@ -558,39 +581,39 @@
|
|||||||
|
|
||||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
"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=="],
|
|
||||||
|
|
||||||
"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-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
|
||||||
|
|
||||||
"pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="],
|
"pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="],
|
||||||
|
|
||||||
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||||
|
|
||||||
|
"pkijs": ["pkijs@3.4.0", "", { "dependencies": { "@noble/hashes": "1.4.0", "asn1js": "^3.0.6", "bytestreamjs": "^2.0.1", "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw=="],
|
||||||
|
|
||||||
"plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
|
"plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
|
||||||
|
|
||||||
"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@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
|
||||||
|
|
||||||
|
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||||
|
|
||||||
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
||||||
|
|
||||||
@@ -598,37 +621,39 @@
|
|||||||
|
|
||||||
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
|
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
|
||||||
|
|
||||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
|
||||||
|
|
||||||
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||||
|
|
||||||
|
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
|
||||||
|
|
||||||
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
|
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
|
||||||
|
|
||||||
"read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
|
"read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
|
||||||
|
|
||||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="],
|
"resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="],
|
||||||
|
|
||||||
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
|
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
|
||||||
|
|
||||||
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
||||||
|
|
||||||
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
|
|
||||||
|
|
||||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||||
|
|
||||||
"rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
|
"rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
|
||||||
|
|
||||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
||||||
|
|
||||||
"sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="],
|
"sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="],
|
||||||
|
|
||||||
@@ -648,39 +673,25 @@
|
|||||||
|
|
||||||
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
|
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
|
||||||
|
|
||||||
"slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
|
|
||||||
|
|
||||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
|
||||||
|
|
||||||
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
|
|
||||||
|
|
||||||
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
|
|
||||||
|
|
||||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||||
|
|
||||||
"ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="],
|
|
||||||
|
|
||||||
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
|
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
|
||||||
|
|
||||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
|
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
"tar": ["tar@7.5.11", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ=="],
|
"tar": ["tar@7.5.21", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA=="],
|
||||||
|
|
||||||
"temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="],
|
"temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="],
|
||||||
|
|
||||||
@@ -692,7 +703,7 @@
|
|||||||
|
|
||||||
"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.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="],
|
||||||
|
|
||||||
"tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
|
"tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
|
||||||
|
|
||||||
@@ -700,39 +711,37 @@
|
|||||||
|
|
||||||
"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.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="],
|
||||||
|
|
||||||
"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-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="],
|
|
||||||
|
|
||||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||||
|
|
||||||
|
"unzipper": ["unzipper@0.12.5", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "11.3.1", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A=="],
|
||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
|
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
"webcrypto-core": ["webcrypto-core@1.9.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q=="],
|
||||||
|
|
||||||
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
|
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"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=="],
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
"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": ["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=="],
|
||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
"ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
|
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||||
|
|
||||||
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||||
|
|
||||||
@@ -744,18 +753,16 @@
|
|||||||
|
|
||||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
|
||||||
|
|
||||||
"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/discord-api-types": ["discord-api-types@0.38.43", "", {}, "sha512-sSoBf/nK6m7BGtw65mi+QBuvEWaHE8MMziFLqWL+gT6ME/BLg34dRSVKS3Husx40uU06bvxUc3/X+D9Y6/zAbw=="],
|
||||||
|
|
||||||
|
"@discordjs/util/discord-api-types": ["discord-api-types@0.38.43", "", {}, "sha512-sSoBf/nK6m7BGtw65mi+QBuvEWaHE8MMziFLqWL+gT6ME/BLg34dRSVKS3Husx40uU06bvxUc3/X+D9Y6/zAbw=="],
|
||||||
|
|
||||||
"@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/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
|
||||||
|
|
||||||
"@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=="],
|
||||||
@@ -764,75 +771,81 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
|
||||||
|
|
||||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
|
||||||
|
|
||||||
"@malept/flatpak-bundler/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=="],
|
"@malept/flatpak-bundler/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=="],
|
||||||
|
|
||||||
"@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"@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/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=="],
|
||||||
|
|
||||||
"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/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"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=="],
|
"builder-util/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
"cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
|
||||||
|
|
||||||
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
|
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
|
||||||
|
|
||||||
"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=="],
|
"es-set-tostringtag/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"get-intrinsic/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
"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=="],
|
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
|
||||||
|
|
||||||
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"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=="],
|
"unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="],
|
||||||
|
|
||||||
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
"@types/cacheable-request/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"@types/fs-extra/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
"@types/keyv/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
"@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=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"app-builder-lib/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"builder-util/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
"electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
"electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||||
|
|
||||||
"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=="],
|
"node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
|
||||||
|
|
||||||
"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-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
"app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,25 @@ Rules:
|
|||||||
- `README.md` is ignored by the generator
|
- `README.md` is ignored by the generator
|
||||||
- if a PR should not produce release notes, apply the `skip-changelog` label instead of adding a fragment
|
- if a PR should not produce release notes, apply the `skip-changelog` label instead of adding a fragment
|
||||||
|
|
||||||
|
PR branch workflow:
|
||||||
|
|
||||||
|
- Before adding a fragment or bullet, inspect the `changes/*.md` files already changed in the PR
|
||||||
|
- If the new work fixes, modifies, renames, or supersedes behavior introduced or referenced by that fragment, edit or remove the stale bullet instead of adding follow-up churn
|
||||||
|
- Add a new bullet only when it describes a truly separate user-visible outcome
|
||||||
|
- Multiple fragment files are allowed when one PR has genuinely separate release-note outcomes, but keep them minimized and current
|
||||||
|
|
||||||
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.
|
||||||
|
- `CHANGELOG.md`, GitHub release notes, and prerelease notes all use short top-level items with one nested bullet per distinct change, instead of packing a release's worth of detail into a single paragraph bullet. An item with only one thing to say stays inline on the top-level bullet. Release notes and prerelease notes additionally cover user benefit and any useful action note in their nested bullets.
|
||||||
- `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`
|
||||||
|
- from the second prerelease of a base version onward, the notes also open with a `## Changes since <previous tag>` section generated from the fragment diff against the previous beta/RC tag; keep fragment edits meaningful. Editorial-only rewording is filtered out of that section, while genuinely changed behavior and deleted fragments (reverted changes) are reported
|
||||||
|
- 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,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: linux
|
||||||
|
|
||||||
|
- Fixed the AppImage exiting without opening a window when launched inside a sandbox that mounts the image itself, such as `firejail --appimage` (used by the AppImage catalog test). The detached background app now runs straight from the sandbox's squashfs mount instead of trying to re-mount the AppImage over FUSE.
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: added
|
|
||||||
area: updater
|
|
||||||
|
|
||||||
- Added tray and `subminer -u` update checks for SubMiner releases, including app update prompts, launcher updates, Linux rofi theme updates, checksum verification, configurable update notifications, and an opt-in prerelease update channel for beta/RC testing.
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: app
|
||||||
|
|
||||||
|
- SubMiner no longer crashes with "write EPIPE" when the terminal command that started it is stopped with Ctrl+C.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: docs
|
||||||
|
area: docs
|
||||||
|
|
||||||
|
- Rewrote the docs site to be shorter and easier to scan: pages lead with setup and use, reference material lives in compact tables, and internal detail was cut from user pages.
|
||||||
|
- The configuration reference now has a short explanation and a key/default table for each config block.
|
||||||
|
- Fixed docs that no longer matched current behavior.
|
||||||
|
- Fixed the Arch Linux install command for MeCab, which is only in the AUR (`mecab-git`), and brought the README requirements and quick start in line with the installation guide.
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
type: added
|
|
||||||
area: setup
|
|
||||||
|
|
||||||
- Added optional first-run setup controls to install Bun and the `subminer` command-line launcher on Linux, macOS, and Windows.
|
|
||||||
- Added a Windows `subminer.cmd` user PATH shim so users can type `subminer` without adding `SubMiner.exe` to PATH.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: overlay
|
|
||||||
|
|
||||||
- Fixed macOS overlay tracking so transient mpv window misses no longer hide the overlay; minimizing mpv still hides it.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: overlay
|
|
||||||
|
|
||||||
- Fixed macOS overlay passthrough so mpv controls remain clickable before hovering subtitle bars.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: overlay
|
|
||||||
|
|
||||||
- Fixed subtitle sync modal opens so macOS no longer flashes and hides the first modal attempt or leaves stale modal state after syncing.
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Keep the subtitle overlay mapped during fullscreen transitions on Hyprland, avoiding focus changes from repeated hide/show calls that can cancel mpv fullscreen entry. Preserve the hide/show refresh on Sway.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Fixed the overlay (subtitles and sidebar) becoming unclickable after mpv goes fullscreen on Hyprland with `mpv.backend: x11`. The launcher no longer strips `HYPRLAND_INSTANCE_SIGNATURE` from the X11 environment, so the overlay can lift Hyprland's fullscreen input block again.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: jellyfin
|
||||||
|
|
||||||
|
- Jellyfin playback now selects the Japanese subtitle track, and starts subtitle annotations, as soon as that track downloads instead of waiting for every other subtitle track. Tracks now download in parallel, so a slow embedded track Jellyfin has to extract no longer delays the primary subtitles.
|
||||||
|
- Jellyfin episodes with image-based embedded subtitles (PGS, DVD, DVB) now auto-select the Japanese and English tracks. SubMiner no longer requests those tracks as text, and a subtitle track that fails to download no longer cancels selection of the others.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: jellyfin
|
||||||
|
|
||||||
|
- Timing review previews and mined card audio now use the audio track you are playing on Jellyfin direct-play streams, instead of the track Jellyfin marks as default.
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: updater
|
|
||||||
|
|
||||||
- Made Linux `subminer -u` perform release updates from the launcher, independent of any running tray app instance, while reporting `up to date` without downloading assets when the latest release is not newer.
|
|
||||||
- Limited support asset updates to the Linux rofi theme.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: launcher
|
|
||||||
|
|
||||||
- Fixed Linux first-run launcher installs by building the packaged launcher with a valid Bun shebang.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: updater
|
|
||||||
|
|
||||||
- Stopped Linux tray update checks from invoking the native Electron updater, using GitHub release metadata/assets instead so checks do not crash the tray app.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: setup
|
|
||||||
|
|
||||||
- First-run setup now recognizes installed macOS launchers in Homebrew or user PATH dirs, while manual setup installs avoid Homebrew-owned directories.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: updater
|
|
||||||
|
|
||||||
- Bring macOS update dialogs to the front when `subminer --update` is run from the launcher.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: build
|
|
||||||
|
|
||||||
- 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: overlay
|
||||||
|
|
||||||
|
- Fixed mpv key bindings (input.conf and mpv defaults, e.g. `9`/`0` volume) doing nothing while the overlay had focus when the overlay loaded before SubMiner connected to mpv, common in `mpv.backend: x11` mode. The overlay now reloads mpv's bindings once mpv connects.
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: updates
|
|
||||||
|
|
||||||
- Avoided native `electron-updater` checks where they are unsafe, so tray and background update checks continue through GitHub release metadata without crashing the app.
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: added
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Added scroll wheel keys (`WHEEL_UP`, `WHEEL_DOWN`, `WHEEL_LEFT`, `WHEEL_RIGHT`, with modifiers) to `keybindings`, including capture from the settings key editor.
|
||||||
|
- Mouse button and scroll wheel bindings from mpv (`input.conf` and mpv defaults, e.g. double-click fullscreen, wheel volume, back/forward for playlist) now work while the cursor is over the overlay. On Hyprland, where the overlay always receives input, they previously did nothing. SubMiner bindings and the right-click pause still take priority.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: dependencies
|
||||||
|
|
||||||
|
- Updated Electron to 42.10.0 and undici to 7.29.1 to resolve the security advisories blocking CI.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: changed
|
||||||
|
area: release
|
||||||
|
|
||||||
|
- Removed package-size JSON reports from future releases and their CI size comparisons. Package-content validation remains enabled.
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: internal
|
|
||||||
area: tests
|
|
||||||
|
|
||||||
- Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: added
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Session help can run the highlighted command during playback: press `Enter` or double-click a row. Hovering a row with the mouse makes it the `Enter` target. Help opened from the tray with no video loaded stays read-only.
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: launcher
|
|
||||||
|
|
||||||
- Fixed `subminer app --setup` so it opens the setup flow when SubMiner is already running in the background.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: setup
|
|
||||||
|
|
||||||
- Quit standalone setup app launches after first-run setup finishes, returning the terminal instead of leaving the app process open.
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- On macOS with Bluetooth headphones such as AirPods, the timing review preview cursor now moves smoothly across the selection instead of stuttering and jumping to the end partway through. This affected Opus audio, including Jellyfin streams.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Media timing review trim handles are now thin lines centered on the clip edge, so it's clear where the clip starts and ends and less of the waveform is covered.
|
||||||
|
- Original subtitle boundaries use orange bars without text tags, leaving the waveform unobstructed.
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: tray
|
|
||||||
|
|
||||||
- Kept the tray app running when closing tray-launched Yomitan settings.
|
|
||||||
- Kept tray-launched Yomitan settings loading from blocking other tray actions.
|
|
||||||
- Replaced the default native Yomitan settings menu with a close-only menu so closing settings does not quit the tray app.
|
|
||||||
- Added an in-page close button for Yomitan settings on Hyprland, where native window controls are not available.
|
|
||||||
- Disabled Yomitan's embedded popup preview in the tray-launched settings window to avoid renderer hangs during normal sidebar navigation.
|
|
||||||
- Serialized copied Yomitan extension refreshes so startup cannot race itself and leave extension loading in an error state.
|
|
||||||
- Fixed tray-launched session help focus handling so the modal can close without mpv running.
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: added
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- Added a YouTube browser window (`subminer youtube`/`subminer yt`, or tray **Browse YouTube**) that keeps your YouTube login across restarts.
|
||||||
|
- Clicking a video plays it in mpv instead of the page; middle-click, Shift/Ctrl-click, or the right-click menu queues it in mpv's playlist.
|
||||||
|
- Queued YouTube videos get the same Japanese subtitle setup when mpv reaches them.
|
||||||
|
- Closing the YouTube window quits a SubMiner started by `subminer youtube` (after mpv closes, if a video is still playing).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- YouTube Japanese subtitles no longer fail with "HTTP 429" on videos where YouTube also lists a machine-translated Japanese track; SubMiner now picks the real track and falls back to yt-dlp if a direct subtitle download is refused.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: added
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- Added `youtube.subtitleSource`: set it to `whisper` to transcribe YouTube videos with Whisper instead of downloading YouTube's captions. The default stays `youtube`.
|
||||||
|
- Whisper mode keeps the video paused while it transcribes, with progress in the subtitle generation modal; close the modal to keep watching, or cancel to continue without subtitles.
|
||||||
|
- The subtitle generation modal (`Ctrl+Shift+G`) now also works on YouTube videos, whatever `youtube.subtitleSource` is set to.
|
||||||
|
- Whisper mode downloads a small audio-only stream and deletes it once generation ends, or when you switch videos, close mpv, or quit.
|
||||||
@@ -7,10 +7,37 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Overlay Auto-Start
|
// Subtitle Selection
|
||||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
// Select primary and secondary mpv subtitle tracks from the overlay.
|
||||||
|
// Hot-reload: enabling or disabling updates the session shortcut immediately.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
"subtitleSelection": {
|
||||||
|
"enabled": false // Use the SubMiner modal to select primary and secondary subtitle tracks. When enabled, its shortcut overrides mpv subtitle selection. Values: true | false
|
||||||
|
}, // Select primary and secondary mpv subtitle tracks from the overlay.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Japanese Subtitle Generation
|
||||||
|
// Generate timed Japanese subtitles from local audio using whisper.cpp.
|
||||||
|
// Configure an existing GGML model path or explicitly download a SubMiner-managed model.
|
||||||
|
// Hot-reload: settings apply to the next generation or model download.
|
||||||
|
// ==========================================
|
||||||
|
"subtitleGeneration": {
|
||||||
|
"whisperPath": "", // Optional path override for whisper.cpp. Leave empty to find whisper-cli on PATH.
|
||||||
|
"modelPath": "", // Path to an existing multilingual whisper.cpp GGML model. Leave empty to use a SubMiner-managed model. A configured path always takes precedence.
|
||||||
|
"managedModel": "small", // Multilingual whisper.cpp model to use when modelPath is empty. Download it explicitly from the generation modal or launcher. Values: tiny | tiny-q5_1 | tiny-q8_0 | base | base-q5_1 | base-q8_0 | small | small-q5_1 | small-q8_0 | medium | medium-q5_0 | medium-q8_0 | large-v1 | large-v2 | large-v2-q5_0 | large-v2-q8_0 | large-v3 | large-v3-q5_0 | large-v3-turbo | large-v3-turbo-q5_0 | large-v3-turbo-q8_0
|
||||||
|
"threads": 4, // Positive integer CPU thread count for whisper.cpp Japanese transcription.
|
||||||
|
"ffmpegPath": "", // Optional FFmpeg path override for audio extraction. Leave empty to find ffmpeg on PATH.
|
||||||
|
"ffprobePath": "", // Optional FFprobe path override for audio tracks and timing. Leave empty to find ffprobe on PATH.
|
||||||
|
"vadModelPath": "", // Path to a whisper.cpp Silero VAD model. Enables dialogue-focused generation while retaining uncertain audible sections, which may include songs. Leave empty to transcribe the full audio.
|
||||||
|
"vadPath": "" // Optional speech detector executable override. With vadModelPath configured, leave empty to find whisper-vad-speech-segments or vad-speech-segments on PATH.
|
||||||
|
}, // Generate timed Japanese subtitles from local audio using whisper.cpp.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Visible Overlay Auto-Start
|
||||||
|
// 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": true, // Show the visible subtitle overlay automatically when the bundled mpv plugin starts SubMiner. Values: true | false
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Texthooker Server
|
// Texthooker Server
|
||||||
@@ -18,7 +45,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 +72,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 +115,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 +172,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,7 +187,7 @@
|
|||||||
"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.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -163,34 +198,48 @@
|
|||||||
"updates": {
|
"updates": {
|
||||||
"enabled": true, // Run automatic update checks in the background. Values: true | false
|
"enabled": true, // Run automatic update checks in the background. Values: true | false
|
||||||
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
|
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
|
||||||
"notificationType": "system", // How SubMiner announces available updates. Values: system | osd | both | none
|
"notificationType": "overlay", // How SubMiner announces available updates. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system
|
||||||
"channel": "stable" // Release channel used for update checks. Values: stable | prerelease
|
"channel": "stable" // Release channel used for update checks. Values: stable | prerelease
|
||||||
}, // Automatic update check behavior.
|
}, // Automatic update check behavior.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Notifications
|
||||||
|
// Overlay notification display behavior.
|
||||||
|
// Hot-reload: position changes apply to the next overlay notification.
|
||||||
|
// ==========================================
|
||||||
|
"notifications": {
|
||||||
|
"overlayPosition": "top-right" // Position for in-overlay notification cards. Values: top-left | top | top-right
|
||||||
|
}, // Overlay notification display 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.
|
"openTsukihime": "Ctrl+Shift+T", // Accelerator that opens the TsukiHime subtitle search modal (configured secondary/Japanese primary tabs).
|
||||||
"openControllerSelect": "Alt+C", // Open controller select setting.
|
"openSubtitleSelection": "g-s", // Open subtitle selection when enabled. Use g-s to press g then s. Set null to unbind.
|
||||||
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
|
"openSubtitleGeneration": "Ctrl+Shift+G", // Accelerator that opens the standalone Japanese subtitle generation modal.
|
||||||
"toggleSubtitleSidebar": "Backslash" // Toggle subtitle sidebar setting.
|
"openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet.
|
||||||
|
"openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal.
|
||||||
|
"openControllerDebug": "Alt+Shift+C", // Accelerator that opens the controller debug modal with live axis/button readouts.
|
||||||
|
"toggleSubtitleSidebar": "Backslash", // Accelerator that toggles the subtitle sidebar visibility.
|
||||||
|
"toggleNotificationHistory": "CommandOrControl+N", // Accelerator that toggles the overlay notification history panel.
|
||||||
|
"appendClipboardVideoToQueue": "CommandOrControl+A" // Accelerator that appends a video path from the clipboard to the mpv playlist.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -271,15 +320,41 @@
|
|||||||
] // Command setting.
|
] // Command setting.
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "Shift+BracketRight", // Key setting.
|
"key": "Ctrl+Shift+ArrowLeft", // Key setting.
|
||||||
"command": [
|
"command": [
|
||||||
"__sub-delay-next-line"
|
"sub-step",
|
||||||
|
-1
|
||||||
] // Command setting.
|
] // Command setting.
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "Shift+BracketLeft", // Key setting.
|
"key": "Ctrl+Shift+ArrowRight", // Key setting.
|
||||||
"command": [
|
"command": [
|
||||||
"__sub-delay-prev-line"
|
"sub-step",
|
||||||
|
1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KeyZ", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"add",
|
||||||
|
"sub-delay",
|
||||||
|
-0.1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Shift+KeyZ", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"add",
|
||||||
|
"sub-delay",
|
||||||
|
0.1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KeyX", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"add",
|
||||||
|
"sub-delay",
|
||||||
|
0.1
|
||||||
] // Command setting.
|
] // Command setting.
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -327,20 +402,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.
|
||||||
|
|
||||||
@@ -349,7 +424,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.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -359,29 +434,39 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"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": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // 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.
|
"knownWordMaturityColors": {
|
||||||
"fontWeight": "600", // Font weight setting.
|
"new": "#ee99a0", // Color for known words whose Anki cards are new (never reviewed), when maturity highlighting is enabled.
|
||||||
"lineHeight": 1.35, // Line height setting.
|
"learning": "#b7bdf8", // Color for known words whose Anki cards are in (re)learning, when maturity highlighting is enabled.
|
||||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
"young": "#91d7e3", // Color for known words whose Anki cards are in review below the mature threshold, when maturity highlighting is enabled.
|
||||||
"wordSpacing": 0, // Word spacing setting.
|
"mature": "#a6da95" // Color for known words whose Anki cards are at or above the mature interval threshold, when maturity highlighting is enabled.
|
||||||
"fontKerning": "normal", // Font kerning setting.
|
}, // Known word maturity colors 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.
|
||||||
@@ -392,7 +477,7 @@
|
|||||||
"frequencyDictionary": {
|
"frequencyDictionary": {
|
||||||
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
||||||
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
|
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
|
||||||
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
|
"topX": 10000, // Only color tokens with frequency rank <= topX (default: 10000).
|
||||||
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
||||||
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
|
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
|
||||||
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
||||||
@@ -405,19 +490,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": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // 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.
|
||||||
|
|
||||||
@@ -431,18 +518,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.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -453,22 +542,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, media.normalizeAudio/mirrorMpvVolume/reviewTiming, knownWords, nPlusOne, fields.word/audio/wordAudio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, isKiku.fieldGrouping, isSenren.fieldGrouping, and lapisKiku.wordCardKind 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
|
||||||
@@ -479,13 +568,15 @@
|
|||||||
"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 use the Yomitan mining deck when available.
|
||||||
"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.
|
"wordAudio": "ExpressionAudio", // Existing word-audio field read to time the frozen first frame of animated images. This mapping is only used for synchronization.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"image": "Picture", // Card field that receives the captured screenshot or animated image.
|
||||||
"miscInfo": "MiscInfo", // Misc info setting.
|
"sentence": "Sentence", // Card field that receives the source sentence text.
|
||||||
"translation": "SelectionText" // Translation setting.
|
"miscInfo": "MiscInfo", // Card field that receives the miscellaneous info pattern (see ankiConnect.metadata.pattern).
|
||||||
|
"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
|
||||||
@@ -493,72 +584,116 @@
|
|||||||
"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.
|
"normalizeAudio": true, // Normalize generated sentence audio loudness during media extraction. Changes apply live. Values: true | false
|
||||||
"fallbackDuration": 3, // Fallback duration setting.
|
"mirrorMpvVolume": true, // Apply mpv's current software volume curve to generated sentence audio. Changes apply live. Values: true | false
|
||||||
"maxMediaDuration": 30 // Max media duration setting.
|
"reviewTiming": false, // Review and preview subtitle media timing before SubMiner creates or enriches a mined card. Values: true | false
|
||||||
|
"audioPadding": 0, // Seconds of padding appended to both ends of generated sentence audio and animated AVIF clips.
|
||||||
|
"fallbackDuration": 3, // Fallback clip duration in seconds when subtitle timing data is unavailable.
|
||||||
|
"maxMediaDuration": 30 // Maximum allowed media clip duration in seconds. 0 disables the cap.
|
||||||
}, // 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
|
||||||
|
"maturityEnabled": false, // Color known-word highlights by Anki card maturity (new, learning, young, mature) instead of a single color. Requires known-word highlighting. Values: true | false
|
||||||
|
"matureThresholdDays": 21, // Card interval in days at which a known word counts as mature (Anki convention: 21).
|
||||||
"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"] }. Reading fields (Reading, Word Reading, ExpressionReading) are always probed so cached words match only in the reading their note teaches; words from notes without readings match in any reading.
|
||||||
"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": "overlay", // Notification surface used to announce mining and update outcomes. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system
|
||||||
"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.
|
||||||
|
"isSenren": {
|
||||||
|
"enabled": false, // Enable Senren-specific duplicate handling (scene-switching field grouping, including miscInfo grouping). Mutually exclusive with isKiku.enabled. Values: true | false
|
||||||
|
"fieldGrouping": "auto", // Senren duplicate-card field grouping mode (scene switching). Values: auto | manual | disabled
|
||||||
|
"deleteDuplicateInAuto": true // When Senren field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
|
||||||
|
}, // Is senren setting.
|
||||||
|
"lapisKiku": {
|
||||||
|
"wordCardKind": "word-and-sentence" // Card-type flag SubMiner marks on Kiku/Lapis word cards. Only one flag is set at a time; the others are cleared. Requires isKiku.enabled or isLapis.enabled. Values: word-and-sentence | click | sentence | audio | none
|
||||||
|
} // Lapis 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.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TsukiHime
|
||||||
|
// TsukiHime subtitle search configuration for Japanese primary and configured secondary subtitles. No API key required.
|
||||||
|
// Hot-reload: TsukiHime changes apply to the next TsukiHime request.
|
||||||
|
// ==========================================
|
||||||
|
"tsukihime": {
|
||||||
|
"apiBaseUrl": "https://api.tsukihime.org/v1", // Base URL of the TsukiHime API (Animetosho successor). No API key required.
|
||||||
|
"maxSearchResults": 10 // Maximum TsukiHime search results returned.
|
||||||
|
}, // TsukiHime subtitle search configuration for Japanese primary and configured secondary subtitles. No API key required.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TMDB
|
||||||
|
// TMDB (The Movie Database) metadata for live-action dramas and movies in the stats Library: posters, synopses, and grouping by show.
|
||||||
|
// Hot-reload: TMDB changes apply to the next TMDB request.
|
||||||
|
// ==========================================
|
||||||
|
"tmdb": {
|
||||||
|
"apiKey": "", // Your own TMDB API key or read access token for live-action posters and synopses in the stats Library. Release builds bundle a project key, so set this only to use your own quota or when running from source (free under Settings > API on themoviedb.org).
|
||||||
|
"apiKeyCommand": "" // Shell command that prints the TMDB API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
|
||||||
|
}, // TMDB (The Movie Database) metadata for live-action dramas and movies in the stats Library: posters, synopses, and grouping by show.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 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 and subtitleSource apply to the next YouTube subtitle load.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"youtube": {
|
"youtube": {
|
||||||
"primarySubLanguages": [
|
"primarySubLanguages": [
|
||||||
"ja",
|
"ja",
|
||||||
"jpn"
|
"jpn"
|
||||||
] // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
|
], // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
|
||||||
|
"subtitleSource": "youtube", // Where primary YouTube subtitles come from. Whisper transcribes the audio locally using the subtitleGeneration settings. Values: youtube | whisper
|
||||||
|
"mediaCache": {
|
||||||
|
"mode": "direct", // How YouTube card audio/images are extracted. Values: direct | background
|
||||||
|
"maxHeight": 720 // Maximum video height downloaded for the YouTube background media cache. Set to 0 for unlimited.
|
||||||
|
} // Media cache setting.
|
||||||
}, // Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
}, // Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -571,11 +706,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
|
||||||
@@ -597,14 +729,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": "/tmp/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, chapter markers, and the skip-intro key. Values: true | false
|
||||||
|
"aniskipButtonKey": "TAB" // mpv key used to skip the detected intro while the skip prompt is visible.
|
||||||
|
}, // SubMiner-managed mpv launch and bundled plugin options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Jellyfin
|
// Jellyfin
|
||||||
@@ -617,14 +760,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
|
||||||
@@ -647,7 +786,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,3 +1,8 @@
|
|||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import type { DefaultTheme, HeadConfig, TransformContext, UserConfig } from 'vitepress';
|
||||||
|
|
||||||
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
|
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
|
||||||
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
|
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
|
||||||
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
|
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
|
||||||
@@ -7,20 +12,235 @@ const PLAUSIBLE_INIT_SCRIPT = [
|
|||||||
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
|
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
function pageToCanonicalHref(page: string): string | null {
|
type DocsChannel = 'stable-root' | 'stable-archive' | 'main';
|
||||||
|
|
||||||
|
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();
|
||||||
|
// The tracked `docs-site/` checkout, which stays a git working tree even when
|
||||||
|
// `docsSourceDir` points at an untracked release snapshot. Used for git lookups only.
|
||||||
|
const repoDocsDir = optionalEnv(process.env.SUBMINER_DOCS_REPO_DIR) ?? process.cwd();
|
||||||
|
const channel = normalizeChannel(optionalEnv(process.env.SUBMINER_DOCS_CHANNEL));
|
||||||
|
const docsVersion = optionalEnv(process.env.SUBMINER_DOCS_VERSION);
|
||||||
|
|
||||||
|
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 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 {
|
// Only the root channel is indexable. `main` and every /v/<version>/ archive are
|
||||||
|
// near-verbatim copies of it, so they own their URL via a self-referential canonical
|
||||||
|
// and are excluded from the index instead of being consolidated onto root. Uniform
|
||||||
|
// self-canonical plus noindex avoids mixing noindex with a cross-page canonical,
|
||||||
|
// which Google treats as a conflicting signal.
|
||||||
|
const isIndexableChannel = channel === 'stable-root';
|
||||||
|
|
||||||
|
function pageToCanonicalHref(page: string): string | null {
|
||||||
|
const route = pageToRoute(page);
|
||||||
|
if (!route) return null;
|
||||||
|
|
||||||
|
if (!isIndexableChannel) {
|
||||||
|
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 }]] : [];
|
||||||
|
|
||||||
|
// Crawlable so links still pass through, but out of the index: ~30 archived copies
|
||||||
|
// of every page otherwise soak up the crawl budget the current docs need.
|
||||||
|
if (!isIndexableChannel) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version navigation targets other builds (root, `/main/`, `/versions`), so it links to
|
||||||
|
// production by absolute URL: base-relative links would stay inside this build, and
|
||||||
|
// `target: '_self'` makes the VitePress router do a full page load. The list is
|
||||||
|
// deliberately static; the full release list lives on the root-only `/versions` page
|
||||||
|
// so frozen `/v/<version>/` archives never need a rebuild when a new tag ships.
|
||||||
|
const versionItems: DefaultTheme.NavItemWithLink[] = [
|
||||||
|
{ text: 'Latest stable', link: `${DOCS_HOSTNAME}/` },
|
||||||
|
{ text: 'main', link: `${DOCS_HOSTNAME}/main/` },
|
||||||
|
{ text: 'All versions', link: `${DOCS_HOSTNAME}/versions` },
|
||||||
|
].map((item) => ({ ...item, target: '_self', noIcon: true }));
|
||||||
|
|
||||||
|
function sitemapUrlToPage(url: string): string {
|
||||||
|
const route = url.replace(/\.html$/, '').replace(/^\/+|\/+$/g, '');
|
||||||
|
return route ? `${route}.md` : 'index.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
// VitePress derives <lastmod> by running `git log` inside its source dir. Production
|
||||||
|
// builds point that at an untracked snapshot of the release tag, so the lookup comes
|
||||||
|
// back empty and the sitemap ships with no dates at all. Resolve it from the tracked
|
||||||
|
// checkout at the ref being built instead.
|
||||||
|
function lastModifiedFor(url: string): string | undefined {
|
||||||
|
const ref = docsVersion && docsVersion !== 'main' ? docsVersion : 'HEAD';
|
||||||
|
const result = spawnSync('git', ['log', '-1', '--format=%cI', ref, '--', sitemapUrlToPage(url)], {
|
||||||
|
cwd: repoDocsDir,
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (result.status === 0 && result.stdout.trim()) || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the root channel publishes a sitemap. Archived and `main` builds would emit
|
||||||
|
// their own copies listing the same canonical URLs, which just advertises the
|
||||||
|
// duplicate trees we are trying to keep out of the index.
|
||||||
|
const sitemap: UserConfig['sitemap'] = isIndexableChannel
|
||||||
|
? {
|
||||||
|
hostname: DOCS_HOSTNAME,
|
||||||
|
transformItems(items) {
|
||||||
|
return items
|
||||||
|
.filter((item) => item.url !== 'README' && item.url !== `${DOCS_HOSTNAME}/README`)
|
||||||
|
.map((item) => ({ ...item, lastmod: item.lastmod ?? lastModifiedFor(item.url) }));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
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 ?? 'main', 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: 'Subtitle Generation', link: '/subtitle-generation' },
|
||||||
|
{ text: 'TsukiHime', link: '/tsukihime-integration' },
|
||||||
|
{ text: 'AniList', link: '/anilist-integration' },
|
||||||
|
{ text: 'AniSkip', link: '/aniskip-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 } : {}),
|
||||||
head: [
|
head: [
|
||||||
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
|
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
|
||||||
[
|
[
|
||||||
@@ -31,13 +251,13 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
['script', {}, PLAUSIBLE_INIT_SCRIPT],
|
['script', {}, PLAUSIBLE_INIT_SCRIPT],
|
||||||
['link', { rel: 'icon', href: '/favicon.ico', sizes: 'any' }],
|
['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',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -46,7 +266,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',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -54,7 +274,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',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -62,20 +282,10 @@ export default {
|
|||||||
appearance: 'dark',
|
appearance: 'dark',
|
||||||
cleanUrls: true,
|
cleanUrls: true,
|
||||||
metaChunk: true,
|
metaChunk: true,
|
||||||
sitemap: {
|
sitemap,
|
||||||
hostname: DOCS_HOSTNAME,
|
transformHead: transformPageHead,
|
||||||
transformItems(items) {
|
|
||||||
return items.filter(
|
|
||||||
(item) => item.url !== 'README' && item.url !== `${DOCS_HOSTNAME}/README`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transformHead({ page }) {
|
|
||||||
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',
|
||||||
@@ -88,59 +298,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',
|
||||||
},
|
},
|
||||||
@@ -159,3 +318,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,9 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useRoute, useData } from 'vitepress';
|
import { useRoute, useData } from 'vitepress';
|
||||||
import { computed } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { formatStatusLineDate, formatStatusLineFilePath } from '../status-line';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { page, frontmatter } = useData();
|
const { frontmatter } = useData();
|
||||||
|
|
||||||
const mode = computed(() => {
|
const mode = computed(() => {
|
||||||
const layout = frontmatter.value.layout;
|
const layout = frontmatter.value.layout;
|
||||||
@@ -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(() => {
|
||||||
@@ -23,10 +23,10 @@ const section = computed(() => {
|
|||||||
return parts[0] || 'root';
|
return parts[0] || 'root';
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastUpdated = computed(() => {
|
// Set on the client only, so the prerendered HTML never bakes in the build date.
|
||||||
if (!page.value.lastUpdated) return '';
|
const today = ref('');
|
||||||
const date = new Date(page.value.lastUpdated);
|
onMounted(() => {
|
||||||
return date.toISOString().slice(0, 10);
|
today.value = formatStatusLineDate(new Date());
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@ const lastUpdated = computed(() => {
|
|||||||
<div class="tui-statusline__right">
|
<div class="tui-statusline__right">
|
||||||
<span class="tui-statusline__section">{{ section }}</span>
|
<span class="tui-statusline__section">{{ section }}</span>
|
||||||
<span class="tui-statusline__sep"></span>
|
<span class="tui-statusline__sep"></span>
|
||||||
<span v-if="lastUpdated" class="tui-statusline__date">{{ lastUpdated }}</span>
|
<span v-if="today" class="tui-statusline__date">{{ today }}</span>
|
||||||
<span v-if="lastUpdated" class="tui-statusline__sep"></span>
|
<span v-if="today" class="tui-statusline__sep"></span>
|
||||||
<span class="tui-statusline__branch">GPL-3.0</span>
|
<span class="tui-statusline__branch">GPL-3.0</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { expect, test } from 'bun:test';
|
||||||
|
import { formatStatusLineDate, 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('status line date uses the local calendar day, zero padded', () => {
|
||||||
|
expect(formatStatusLineDate(new Date(2026, 0, 5, 23, 59))).toBe('2026-01-05');
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export function formatStatusLineFilePath(routePath: string): string {
|
||||||
|
if (routePath === '/') return 'index.md';
|
||||||
|
return `${routePath.replace(/^\/|\/$/g, '')}.md`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local calendar date as YYYY-MM-DD (toISOString would give the UTC date).
|
||||||
|
export function formatStatusLineDate(date: Date): string {
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${date.getFullYear()}-${month}-${day}`;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SubMiner Docs
|
# SubMiner docs
|
||||||
|
|
||||||
In-repo VitePress documentation source for SubMiner.
|
In-repo VitePress documentation source for SubMiner.
|
||||||
|
|
||||||
@@ -30,9 +30,33 @@ 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`.
|
||||||
|
- `/v/<version>/` serves stable release archives.
|
||||||
|
- `/versions` (root build only) lists every published version.
|
||||||
|
- Prerelease tags do not update the docs site.
|
||||||
|
|
||||||
|
Only `/` is indexable. `/main/` and every `/v/<version>/` page carries a self-referential canonical plus `noindex,follow`, repeated as an `X-Robots-Tag` header (by the generated `_headers` file for `/main/`, by the archive function for `/v/`). They stay crawlable so their links still resolve, but ~30 archived copies of every page would otherwise consume the crawl budget the current docs need. Only the root build emits `sitemap.xml`, and its `<lastmod>` dates come from `git log` against the tracked checkout at the released tag, because the build renders from an untracked snapshot that VitePress cannot date itself.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Stable archives in R2
|
||||||
|
|
||||||
|
`/v/<version>/` archives are not part of the Pages deployment. Each one is built once, uploaded to an R2 bucket under `v/<version>/`, and served by the Pages Function in `functions/v/[[path]].ts`. Each deploy builds only archives the bucket is missing (an archive counts as present once its `_archive.json` marker exists), plus the root and `/main/` builds. Archive nav links to `/versions` instead of listing releases, so a new tag never invalidates old archives.
|
||||||
|
|
||||||
|
To re-render archives on purpose (theme change, docs fix), run the `Docs Pages` workflow manually with `rebuild_archives` set to comma-separated tags or `all`.
|
||||||
|
|
||||||
|
One-time setup:
|
||||||
|
|
||||||
|
- R2 bucket for archives; its name goes in the `DOCS_ARCHIVE_R2_BUCKET` repository variable.
|
||||||
|
- R2 API token with Object Read & Write on that bucket; its S3 credentials go in the `DOCS_ARCHIVE_R2_ACCESS_KEY_ID` and `DOCS_ARCHIVE_R2_SECRET_ACCESS_KEY` repository secrets.
|
||||||
|
- Pages project: Settings > Bindings > R2 bucket, variable name `DOCS_ARCHIVES`, pointing at the same bucket.
|
||||||
|
|
||||||
|
The first deploy after setup builds and uploads every stable archive; later deploys only add new tags.
|
||||||
@@ -1,128 +1,68 @@
|
|||||||
# AniList Integration
|
# AniList integration
|
||||||
|
|
||||||
SubMiner can sync your watch progress to [AniList](https://anilist.co) automatically. When you finish an episode, SubMiner detects the title and episode number from the filename, finds the matching AniList entry, and updates your progress via the GraphQL API. Failed updates are retried with exponential backoff in the background.
|
SubMiner updates your [AniList](https://anilist.co) watch progress when you finish an episode. The same connection supplies cover art for the stats dashboard and names for the [character dictionary](/character-dictionary).
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
AniList integration is opt-in. To enable it:
|
1. Set `anilist.enabled` to `true`:
|
||||||
|
|
||||||
1. Set `anilist.enabled` to `true` in your config.
|
```jsonc
|
||||||
2. Leave `anilist.accessToken` empty and restart SubMiner (or run `--anilist-setup`).
|
{
|
||||||
3. Approve access in the AniList authorization page.
|
"anilist": {
|
||||||
4. The callback returns to SubMiner via the `subminer://anilist-setup?...` protocol URL, and SubMiner stores the token automatically.
|
"enabled": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```jsonc
|
2. Restart SubMiner. With no token stored, it opens the AniList setup window. You can also open it from the tray (**Configure AniList**) or with `subminer app --anilist-setup`.
|
||||||
{
|
3. Approve access on the AniList page. SubMiner receives the token through a `subminer://` link and stores it encrypted.
|
||||||
"anilist": {
|
|
||||||
"enabled": true,
|
|
||||||
"accessToken": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The access token is encrypted at rest using Electron's `safeStorage` API. On Linux this defaults to `gnome-libsecret`; override the backend with `--password-store=<backend>` (for example `--password-store=basic_text`).
|
If the setup window does not render, SubMiner opens the authorization page in your browser instead. To skip the flow entirely, paste a token into `anilist.accessToken`.
|
||||||
|
|
||||||
If the embedded auth UI fails to render, SubMiner opens the authorize URL in your default browser and shows fallback instructions in-app.
|
On Linux, the token is stored with `gnome-libsecret` by default. If your keyring is unavailable, start it (gnome-keyring or KWallet) or launch SubMiner with `--password-store=basic_text`.
|
||||||
|
|
||||||
::: tip
|
## How updates work
|
||||||
You can also set `anilist.accessToken` directly in config to skip the setup flow entirely. When blank, SubMiner uses the locally stored encrypted token.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## How Tracking Works
|
An episode counts as watched after 85% of its length and at least 10 minutes of playback. SubMiner then:
|
||||||
|
|
||||||
SubMiner monitors playback and triggers an AniList progress update when an episode is considered "watched" -- at least 85% of the episode duration viewed and a minimum of 10 minutes watched.
|
1. Reads the title, season, and episode from the file name and folder. Install [guessit](https://github.com/guessit-io/guessit) for better parsing. A folder named `Season 2` is a strong season hint.
|
||||||
|
2. Finds the matching AniList entry. For season 2 and later, it follows the show's sequels.
|
||||||
|
3. Sets your progress to that episode and marks the entry Watching, or Completed on the final episode.
|
||||||
|
|
||||||
The update flow:
|
The show must already be on your Planning or Watching list. SubMiner does not add new entries, and it never lowers your progress.
|
||||||
|
|
||||||
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.
|
Failed updates are saved and retried in the background, up to 8 times with growing delays. The queue survives restarts.
|
||||||
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.
|
|
||||||
3. **Progress check** -- SubMiner fetches your current list entry for the matched media. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
|
|
||||||
4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`.
|
|
||||||
|
|
||||||
## Update Queue and Retry
|
## Fixing a wrong match
|
||||||
|
|
||||||
Failed AniList updates are persisted to a retry queue on disk and retried with exponential backoff.
|
If a cover or title in the stats Library is wrong, open the title and use **Change AniList Entry**.
|
||||||
|
|
||||||
| Parameter | Value |
|
If SubMiner cannot find a later season, it skips the update rather than writing progress to season 1. Pin the right entry with the character dictionary's AniList override. See [Character dictionary](/character-dictionary).
|
||||||
| --- | --- |
|
|
||||||
| Initial backoff | 30 seconds |
|
|
||||||
| Maximum backoff | 6 hours |
|
|
||||||
| Maximum attempts | 8 |
|
|
||||||
| 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.
|
## Commands
|
||||||
|
|
||||||
Use `--anilist-retry-queue` to manually process one ready item from the queue.
|
| Command | What it does |
|
||||||
|
| ------------------------------------ | --------------------------------------- |
|
||||||
|
| `subminer app --anilist-setup` | Open the AniList setup window |
|
||||||
|
| `subminer app --anilist-status` | Show token state and retry queue counts |
|
||||||
|
| `subminer app --anilist-logout` | Remove the stored token |
|
||||||
|
| `subminer app --anilist-retry-queue` | Retry one queued update now |
|
||||||
|
|
||||||
## Cover Art
|
## Options
|
||||||
|
|
||||||
SubMiner fetches cover art from AniList for display in the stats dashboard. When a new video starts playing, the cover art fetcher:
|
| Key | What it does |
|
||||||
|
| --------------------- | ------------------------------------------------------------- |
|
||||||
|
| `anilist.enabled` | Turns on progress updates. |
|
||||||
|
| `anilist.accessToken` | Token override. Leave empty to use the token stored by setup. |
|
||||||
|
|
||||||
1. Checks the local database for cached art.
|
Character dictionary settings live under `anilist.characterDictionary` and are covered on the [Character dictionary](/character-dictionary) page. See [Configuration](/configuration#anilist) for defaults.
|
||||||
2. If missing, parses the media title (guessit then fallback) and searches the AniList API.
|
|
||||||
3. Downloads the cover image from the AniList CDN and caches it locally (both URL and blob).
|
|
||||||
4. Stores AniList metadata (romaji/English titles, total episodes) alongside the cover for dashboard display.
|
|
||||||
|
|
||||||
A no-match result is cached for 5 minutes before SubMiner retries, preventing repeated API calls for unrecognized media.
|
|
||||||
|
|
||||||
## Rate Limiting
|
|
||||||
|
|
||||||
All AniList API calls go through a shared rate limiter that enforces a sliding window of 20 requests per minute. The limiter also reads AniList's `X-RateLimit-Remaining` and `Retry-After` response headers and pauses requests when the server signals throttling. This applies to both episode tracking and cover art fetching.
|
|
||||||
|
|
||||||
## Configuration Reference
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"anilist": {
|
|
||||||
"enabled": true,
|
|
||||||
"accessToken": "",
|
|
||||||
"characterDictionary": {
|
|
||||||
"enabled": false,
|
|
||||||
"maxLoaded": 3,
|
|
||||||
"profileScope": "all",
|
|
||||||
"collapsibleSections": {
|
|
||||||
"description": false,
|
|
||||||
"characterInformation": false,
|
|
||||||
"voicedBy": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Option | Values | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `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: `""`) |
|
|
||||||
| `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.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 |
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## CLI Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
| --- | --- |
|
|
||||||
| `--anilist-setup` | Open AniList setup/auth flow helper window |
|
|
||||||
| `--anilist-status` | Print current token resolution state and retry queue counters |
|
|
||||||
| `--anilist-logout` | Clear stored AniList token from local persisted state |
|
|
||||||
| `--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.
|
**No update after an episode.** Check that `anilist.enabled` is `true` and that you watched at least 85% of the episode.
|
||||||
- **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.
|
|
||||||
- **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.
|
|
||||||
- **Cover art missing:** Cover art is fetched on a best-effort basis using title matching. If the filename is hard to parse, the search may return no results. The fetcher retries after 5 minutes.
|
|
||||||
- **Encryption unavailable on Linux:** If you see warnings about safeStorage, try `--password-store=basic_text` as a workaround, or ensure your desktop keyring (gnome-keyring, KWallet) is running.
|
|
||||||
|
|
||||||
## Related
|
**"AniList update not possible."** Add the show to your Planning or Watching list, then mark the episode watched again.
|
||||||
|
|
||||||
- [Character Dictionary](/character-dictionary) -- AniList-powered character name dictionary for Yomitan
|
**Wrong show or episode.** Install guessit and make sure it is on your `PATH`. Unusual file names parse poorly without it.
|
||||||
- [Configuration Reference](/configuration) -- full config options
|
|
||||||
- [Jellyfin Integration](/jellyfin-integration) -- media server integration
|
**Token errors.** Run `subminer app --anilist-status`. If the token is invalid, run `--anilist-logout`, then `--anilist-setup`.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# AniSkip integration
|
||||||
|
|
||||||
|
SubMiner looks up opening timestamps on [AniSkip](https://aniskip.com) so you can skip an anime's intro with one key.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
AniSkip is on by default. To turn it off or change the key:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"mpv": {
|
||||||
|
"aniskipEnabled": true,
|
||||||
|
"aniskipButtonKey": "TAB",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both settings apply immediately, without restarting mpv. For better title and episode detection, install [guessit](https://github.com/guessit-io/guessit):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install --user guessit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
When a local file loads, SubMiner reads the title and episode from the file name, finds the show on MyAnimeList, and asks AniSkip for the intro's timestamps. Streams and URLs are skipped.
|
||||||
|
|
||||||
|
If AniSkip has an intro, SubMiner adds `AniSkip Intro Start` and `AniSkip Intro End` chapters. When the intro starts, mpv shows "You can skip by pressing TAB" (with your key) for 3 seconds. Press the key any time during the intro to jump to its end.
|
||||||
|
|
||||||
|
With a custom key other than `TAB` or `y-k`, `y-k` also skips.
|
||||||
|
|
||||||
|
## Triggering from mpv
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
| ----------------------------------------- | ------------------------------------------------------- |
|
||||||
|
| `script-message subminer-skip-intro` | Skip to the end of the intro |
|
||||||
|
| `script-message subminer-aniskip-refresh` | Look up the current file again, ignoring cached results |
|
||||||
|
|
||||||
|
Use `subminer-aniskip-refresh` after a lookup failed or matched the wrong show.
|
||||||
@@ -1,42 +1,34 @@
|
|||||||
# Anki Integration
|
# Anki integration
|
||||||
|
|
||||||
SubMiner uses the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on to create and update Anki cards with sentence context, audio, and screenshots.
|
SubMiner talks to Anki through the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on. It fills new cards with the sentence, an audio clip, and a screenshot, and can create sentence cards and merge duplicate words. It is built for the [Lapis](https://github.com/donkuri/lapis), [Kiku](https://kiku.youyoumu.my.id/), and [Senren](https://github.com/BrenoAqua/Senren) note types, but works with any note type once you map its fields.
|
||||||
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.
|
|
||||||
|
For the day-to-day flow, see [Mining workflow](/mining-workflow). Every key on this page, with its default, is listed in the [AnkiConnect config reference](/configuration#ankiconnect).
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
1. Install [Anki](https://apps.ankiweb.net/).
|
1. Install [Anki](https://apps.ankiweb.net/).
|
||||||
2. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on (code: `2055492159`).
|
2. Install AnkiConnect (add-on code `2055492159`).
|
||||||
3. Keep Anki running while using SubMiner.
|
3. Install FFmpeg and make sure it is on your `PATH`. SubMiner uses it for audio and images.
|
||||||
|
4. Keep Anki running while you mine.
|
||||||
|
|
||||||
AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the port in AnkiConnect's settings, update `ankiConnect.url` in your SubMiner config.
|
If you changed AnkiConnect's port, set `ankiConnect.url` to match.
|
||||||
|
|
||||||
## Auto-Enrichment Transport
|
## How cards get filled
|
||||||
|
|
||||||
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 Yomitan adds a note, SubMiner fills the sentence, audio, image, and MiscInfo fields. It finds new notes in one of two ways:
|
||||||
|
|
||||||
**Proxy mode** — SubMiner runs a local AnkiConnect-compatible proxy and intercepts card creation instantly. Recommended when possible.
|
- **Proxy (default).** SubMiner runs a local AnkiConnect-compatible server. Yomitan sends notes through it, and SubMiner fills each one right after Anki accepts it.
|
||||||
|
- **Polling.** With `ankiConnect.proxy.enabled` set to `false`, SubMiner asks AnkiConnect for recently added notes every `ankiConnect.pollingRate` milliseconds.
|
||||||
|
|
||||||
**Polling mode** (default) — SubMiner polls AnkiConnect every few seconds for newly added cards. Simpler setup, but with a short delay (~3 seconds).
|
Set `ankiConnect.behavior.autoUpdateNewCards` to `false` to stop automatic filling and update cards by hand with `Ctrl/Cmd+V` instead.
|
||||||
|
|
||||||
Use proxy mode if you want immediate enrichment. Use polling mode if your Yomitan instance is external (browser-based) or you prefer minimal configuration.
|
`ankiConnect.deck` limits enrichment and duplicate checks to one deck. If it is empty, SubMiner uses Yomitan's mining deck when it can read it, and otherwise searches all decks.
|
||||||
|
|
||||||
In both modes, the enrichment workflow is the same:
|
### Proxy mode setup (Yomitan / texthooker) {#proxy-mode-setup-yomitan-texthooker}
|
||||||
|
|
||||||
1. Checks if a duplicate expression already exists (for field grouping).
|
|
||||||
2. Updates the sentence field with the current subtitle.
|
|
||||||
3. Generates and uploads audio and image media.
|
|
||||||
4. Fills the translation field from the secondary subtitle or AI.
|
|
||||||
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.
|
|
||||||
Known-word sync scope is controlled by `ankiConnect.knownWords.decks` (object map), with `ankiConnect.deck` used as legacy fallback.
|
|
||||||
|
|
||||||
### Proxy Mode Setup (Yomitan / Texthooker)
|
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
"ankiConnect": {
|
"ankiConnect": {
|
||||||
"url": "http://127.0.0.1:8765", // real AnkiConnect
|
"url": "http://127.0.0.1:8765",
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
@@ -46,325 +38,185 @@ Known-word sync scope is controlled by `ankiConnect.knownWords.decks` (object ma
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then point Yomitan/clients to `http://127.0.0.1:8766` instead of `8765`.
|
Clients must send notes to the proxy (`http://127.0.0.1:8766` here), not to AnkiConnect directly.
|
||||||
|
|
||||||
When SubMiner loads the bundled Yomitan extension, it also attempts to update the **default Yomitan profile** (`profiles[0].options.anki.server`) to the active SubMiner endpoint:
|
- **Bundled Yomitan.** SubMiner sets the active Yomitan profile's Anki server for you. With the proxy on, it always points the profile at the proxy. With the proxy off, it sets `ankiConnect.url`, but only if the profile's server is blank or the stock `http://127.0.0.1:8765`.
|
||||||
|
- **Browser Yomitan or other clients.** Set the Anki server to the proxy URL yourself. To leave your main profile alone, create a separate Yomitan profile for SubMiner, set its Anki server (Settings, Anki) to the proxy URL, and make it active while you mine.
|
||||||
|
|
||||||
- proxy URL when `ankiConnect.proxy.enabled` is `true`
|
### Proxy troubleshooting
|
||||||
- direct `ankiConnect.url` when proxy mode is disabled
|
|
||||||
|
|
||||||
To avoid clobbering custom setups, this auto-update only changes the default profile when its current server is blank or the stock Yomitan default (`http://127.0.0.1:8765`).
|
If cards are not getting filled:
|
||||||
|
|
||||||
For browser-based Yomitan or other external clients (for example Texthooker in a normal browser profile), set their Anki server to the same proxy URL separately: `http://127.0.0.1:8766` (or your configured `proxy.host` + `proxy.port`).
|
1. Check that the proxy is listening while SubMiner runs:
|
||||||
|
|
||||||
### Browser/Yomitan external setup (separate profile)
|
```bash
|
||||||
|
ss -ltnp | grep 8766
|
||||||
|
```
|
||||||
|
|
||||||
If you want SubMiner to use proxy mode without touching your main/default Yomitan profile, create or select a separate Yomitan profile just for SubMiner and set its Anki server to the proxy URL.
|
2. Check that requests pass through to Anki:
|
||||||
|
|
||||||
That profile isolation gives you both benefits:
|
```bash
|
||||||
|
curl -sS http://127.0.0.1:8766 \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
-d '{"action":"version","version":2}'
|
||||||
|
```
|
||||||
|
|
||||||
- SubMiner can auto-enrich immediately via proxy.
|
3. Read the app log (`app-YYYY-MM-DD.log`) in the logs folder. See [Troubleshooting](/troubleshooting) for where logs live.
|
||||||
- Your default Yomitan profile keeps its existing Anki server setting.
|
|
||||||
|
|
||||||
In Yomitan, go to Settings → Profile and:
|
## Field mapping
|
||||||
|
|
||||||
1. Create a profile for SubMiner (or choose one dedicated profile).
|
`ankiConnect.fields` maps SubMiner's data to fields on your note type.
|
||||||
2. Open Anki settings for that profile.
|
|
||||||
3. Set server to `http://127.0.0.1:8766` (or your configured proxy URL).
|
|
||||||
4. Save and make that profile active when using SubMiner.
|
|
||||||
|
|
||||||
This is only for non-bundled, external/browser Yomitan or other clients. The bundled profile auto-update logic only targets `profiles[0]` when it is blank or still default.
|
| Key | Receives |
|
||||||
|
| ------------------ | ------------------------------------------------------------------------- |
|
||||||
### Proxy Troubleshooting (quick checks)
|
| `fields.word` | The mined word |
|
||||||
|
| `fields.audio` | Sentence audio cut from the video |
|
||||||
If auto-enrichment appears to do nothing:
|
| `fields.wordAudio` | Read only: Yomitan's word audio, used to time animated images (see below) |
|
||||||
|
| `fields.image` | Screenshot or animated clip |
|
||||||
1. Confirm proxy listener is running while SubMiner is active:
|
| `fields.sentence` | Subtitle text |
|
||||||
|
| `fields.miscInfo` | Text from `ankiConnect.metadata.pattern` |
|
||||||
```bash
|
|
||||||
ss -ltnp | rg 8766
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Confirm requests can pass through the proxy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sS http://127.0.0.1:8766 \
|
|
||||||
-H 'content-type: application/json' \
|
|
||||||
-d '{"action":"version","version":2}'
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Check both log sinks:
|
|
||||||
|
|
||||||
- Launcher/mpv-integrated log: `~/.cache/SubMiner/mp.log`
|
|
||||||
- App runtime log: `~/.config/SubMiner/logs/SubMiner-YYYY-MM-DD.log`
|
|
||||||
|
|
||||||
4. Ensure config JSONC is valid and logging shape is correct:
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
"logging": {
|
|
||||||
"level": "debug"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`"logging": "debug"` is invalid for current schema and can break reload/start behavior.
|
|
||||||
|
|
||||||
## Field Mapping
|
|
||||||
|
|
||||||
SubMiner maps its data to your Anki note fields. Configure these under `ankiConnect.fields`:
|
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
"ankiConnect": {
|
"ankiConnect": {
|
||||||
"fields": {
|
"fields": {
|
||||||
"word": "Expression", // mined word / expression text
|
"audio": "SentenceAudio",
|
||||||
"audio": "ExpressionAudio", // audio clip from the video
|
"sentence": "Sentence"
|
||||||
"image": "Picture", // screenshot or animated clip
|
|
||||||
"sentence": "Sentence", // subtitle text
|
|
||||||
"miscInfo": "MiscInfo", // metadata (filename, timestamp)
|
|
||||||
"translation": "SelectionText" // secondary sub or AI translation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Field names must match your Anki note type exactly (case-sensitive). If a configured field does not exist on the note type, SubMiner skips it without error.
|
Field names are matched case-insensitively. A mapped field that is missing from the note type is skipped.
|
||||||
|
|
||||||
### Minimal Config
|
`fields.audio` gets sentence audio, not word audio. Yomitan writes its own dictionary audio into your note, so point `fields.audio` at a separate field such as `SentenceAudio`. The default, `ExpressionAudio`, is the field many note types use for Yomitan's word audio, so leaving it would overwrite that audio.
|
||||||
|
|
||||||
If you only want sentence and audio on your cards:
|
`ankiConnect.tags` adds tags to every mined or updated card. Set it to `[]` to add none.
|
||||||
|
|
||||||
```jsonc
|
`ankiConnect.metadata.pattern` builds the MiscInfo text. Tokens: `%f` file name, `%F` file name with extension, `%t` timestamp, `%T` timestamp with milliseconds, `<br>` line break.
|
||||||
"ankiConnect": {
|
|
||||||
"enabled": true,
|
|
||||||
"fields": {
|
|
||||||
"sentence": "Sentence",
|
|
||||||
"audio": "ExpressionAudio"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Media Generation
|
## Media
|
||||||
|
|
||||||
SubMiner uses FFmpeg to generate audio and image media from the video. FFmpeg must be installed and on `PATH`.
|
| Key | What it does |
|
||||||
|
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `media.generateAudio` | Cut sentence audio (MP3) from the subtitle's start and end time |
|
||||||
|
| `media.audioPadding` | Seconds added before and after the clip |
|
||||||
|
| `media.fallbackDuration` | Clip length when the subtitle has no timing |
|
||||||
|
| `media.maxMediaDuration` | Longest allowed clip, in seconds (`0` removes the cap) |
|
||||||
|
| `media.normalizeAudio` | Normalize clip loudness |
|
||||||
|
| `media.mirrorMpvVolume` | Scale the clip by mpv's current volume, so quiet playback gives quiet clips |
|
||||||
|
| `media.generateImage` | Capture an image |
|
||||||
|
| `media.imageType` | `static` for one frame, `avif` for an animated clip of the line |
|
||||||
|
| `media.imageFormat` | Static format: `jpg`, `png`, or `webp` |
|
||||||
|
| `media.imageQuality` | Static image quality |
|
||||||
|
| `media.imageMaxWidth` / `Height` | Static size limit (`0` keeps source size) |
|
||||||
|
| `media.animatedFps` | Animated clip frame rate |
|
||||||
|
| `media.animatedMaxWidth` / `Height` | Animated size limit (`0` keeps aspect ratio) |
|
||||||
|
| `media.animatedCrf` | Animated quality, `0` to `63`, lower is better |
|
||||||
|
| `media.syncAnimatedImageToWordAudio` | Hold the first frame for the length of the word audio in `fields.wordAudio`, so the motion starts with the sentence audio |
|
||||||
|
| `media.reviewTiming` | Pause and let you adjust the clip before media is made (see below) |
|
||||||
|
|
||||||
### Audio
|
Animated AVIF needs an FFmpeg build with an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`).
|
||||||
|
|
||||||
Audio is extracted from the video file using the subtitle's start and end timestamps, with configurable padding added before and after.
|
Media settings apply to the next card without a restart.
|
||||||
|
|
||||||
```jsonc
|
### Review media timing
|
||||||
"ankiConnect": {
|
|
||||||
"media": {
|
|
||||||
"generateAudio": true,
|
|
||||||
"audioPadding": 0.5, // seconds before and after subtitle timing
|
|
||||||
"maxMediaDuration": 30 // cap total duration in seconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Output format: MP3 at 44100 Hz. If the video has multiple audio streams, SubMiner uses the active stream.
|
With `media.reviewTiming` on, SubMiner pauses before making media for word, sentence, and audio cards and opens a review dialog. You can also toggle it for the current session with **Review Media Timing** in the runtime options palette (`Ctrl/Cmd+Shift+O`). Clipboard updates and stats-dashboard mining skip the review.
|
||||||
|
|
||||||
The audio is uploaded to Anki's media folder and inserted as `[sound:audio_<timestamp>.mp3]`.
|
The dialog shows the clip over a speech waveform. When the waveform loads, an untouched clip end moves back to just after the last speech in the line. The orange bars still mark the subtitle's original start and end.
|
||||||
|
|
||||||
### Screenshots (Static)
|
During preview, the yellow cursor follows the audio player's position through the selected range and stays at the end when playback finishes. Buffering pauses the cursor too. If the player stops reporting progress for 15 seconds, the preview stops so you can retry.
|
||||||
|
|
||||||
A single frame is captured at the current playback position.
|
| Action | How |
|
||||||
|
| ------------------------ | --------------------------------------------------------------------- |
|
||||||
|
| Trim | Drag either edge, or click the waveform to move the nearer edge there |
|
||||||
|
| Nudge an edge | Arrow keys on a focused edge (100 ms, `Shift` for 500 ms) |
|
||||||
|
| Slide the clip | Drag the middle |
|
||||||
|
| Show more timeline | Earlier / Later |
|
||||||
|
| Pick the screenshot | Screenshot slider or Frame buttons (static images only) |
|
||||||
|
| Add previous / next line | `P` / `N` (`Shift+P` / `Shift+N` removes) |
|
||||||
|
| Preview | `Space` |
|
||||||
|
| Confirm | `Enter` |
|
||||||
|
| Cancel | `Escape` |
|
||||||
|
|
||||||
```jsonc
|
The confirmed range is used as is, with no extra padding. Added lines go into the sentence field. Reset restores the original timing and removes added lines.
|
||||||
"ankiConnect": {
|
|
||||||
"media": {
|
|
||||||
"generateImage": true,
|
|
||||||
"imageType": "static",
|
|
||||||
"imageFormat": "jpg", // "jpg", "png", or "webp"
|
|
||||||
"imageQuality": 92, // 1–100
|
|
||||||
"imageMaxWidth": null, // optional, preserves aspect ratio
|
|
||||||
"imageMaxHeight": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Animated Clips (AVIF)
|
When you cancel, you can go back to editing, keep the original timing, create the card without media, or discard it. Discard deletes the Yomitan note or audio card, and skips creation for a sentence card.
|
||||||
|
|
||||||
Instead of a static screenshot, SubMiner can generate an animated AVIF covering the subtitle duration.
|
### Update behavior
|
||||||
|
|
||||||
```jsonc
|
| Key | What it does |
|
||||||
"ankiConnect": {
|
| ----------------------------- | ---------------------------------------------------- |
|
||||||
"media": {
|
| `behavior.overwriteAudio` | Replace existing audio instead of adding to it |
|
||||||
"generateImage": true,
|
| `behavior.overwriteImage` | Replace the existing image instead of adding to it |
|
||||||
"imageType": "avif",
|
| `behavior.mediaInsertMode` | `append` or `prepend` new media when not overwriting |
|
||||||
"animatedFps": 10,
|
| `behavior.autoUpdateNewCards` | Fill new Yomitan notes automatically |
|
||||||
"animatedMaxWidth": 640,
|
| `behavior.highlightWord` | Bold the mined word in the sentence field |
|
||||||
"animatedMaxHeight": null,
|
| `behavior.notificationType` | `overlay`, `system`, `both`, or `none` |
|
||||||
"animatedCrf": 35 // 0–63, lower = better quality
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Animated AVIF requires an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`) in your FFmpeg build. Generation timeout is 60 seconds.
|
Manual clipboard updates (`Ctrl/Cmd+V`) always replace the sentence audio, whatever `overwriteAudio` says.
|
||||||
|
|
||||||
### Behavior Options
|
## Sentence cards (Lapis) {#sentence-cards-lapis}
|
||||||
|
|
||||||
```jsonc
|
`Ctrl/Cmd+S` creates a standalone sentence card from the current line, and `Ctrl/Cmd+Shift+S` then a digit combines several lines. The card uses the note type named in `ankiConnect.isLapis.sentenceCardModel`, which must exist in Anki. If it is empty, no card is created.
|
||||||
"ankiConnect": {
|
|
||||||
"behavior": {
|
|
||||||
"overwriteAudio": true, // replace existing audio, or append
|
|
||||||
"overwriteImage": true, // replace existing image, or append
|
|
||||||
"mediaInsertMode": "append", // "append" or "prepend" to field content
|
|
||||||
"autoUpdateNewCards": true, // auto-update when new card detected
|
|
||||||
"notificationType": "osd" // "osd", "system", "both", or "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`overwriteAudio` applies to automatic card updates and duplicate-card enrichment. Manual clipboard subtitle updates (`Ctrl/Cmd+C`, then `Ctrl/Cmd+V`) always replace generated sentence audio, while leaving the word audio field unchanged.
|
|
||||||
|
|
||||||
## AI Translation
|
|
||||||
|
|
||||||
SubMiner can auto-translate the mined sentence and fill the translation field.
|
|
||||||
Secondary subtitle text still wins when present. AI translation is only attempted when `ankiConnect.ai.enabled` is `true` and no secondary subtitle exists.
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
"ai": {
|
|
||||||
"enabled": true,
|
|
||||||
"apiKey": "sk-...",
|
|
||||||
"apiKeyCommand": "",
|
|
||||||
"baseUrl": "https://openrouter.ai/api",
|
|
||||||
"requestTimeoutMs": 15000
|
|
||||||
},
|
|
||||||
"ankiConnect": {
|
|
||||||
"ai": {
|
|
||||||
"enabled": true,
|
|
||||||
"model": "openai/gpt-4o-mini",
|
|
||||||
"systemPrompt": "Translate mined sentence text only."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`ankiConnect.ai` controls feature-local enablement plus optional `model` / `systemPrompt` overrides.
|
|
||||||
Provider credentials and request transport settings live in top-level `ai`.
|
|
||||||
|
|
||||||
Translation priority:
|
|
||||||
|
|
||||||
1. If a secondary subtitle is available, use it as the translation.
|
|
||||||
2. If `ankiConnect.ai.enabled` is `true` and top-level `ai.enabled` is `true`, call the shared AI provider.
|
|
||||||
3. If AI translation fails and no secondary subtitle exists, fall back to the original sentence text.
|
|
||||||
|
|
||||||
The built-in translation request asks for English output by default. Customize that behavior through `ankiConnect.ai.systemPrompt`.
|
|
||||||
|
|
||||||
## Sentence Cards (Lapis)
|
|
||||||
|
|
||||||
SubMiner can create standalone sentence cards (without a word/expression) using a separate note type. This is designed for use with [Lapis](https://github.com/donkuri/Lapis) and similar sentence-focused note types.
|
|
||||||
|
|
||||||
::: warning Required config
|
|
||||||
Sentence card creation and audio card marking both require `ankiConnect.isLapis.enabled: true` and a valid `sentenceCardModel` pointing to your Lapis/Kiku note type. Without this, the `Ctrl/Cmd+S` and `Ctrl/Cmd+Shift+A` shortcuts will not create cards.
|
|
||||||
:::
|
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
"ankiConnect": {
|
"ankiConnect": {
|
||||||
"isLapis": {
|
"isLapis": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sentenceCardModel": "Japanese sentences"
|
"sentenceCardModel": "Lapis"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Trigger with the mine sentence shortcut (`Ctrl/Cmd+S` by default). The card is created directly via AnkiConnect with the sentence, audio, and image filled in.
|
Sentence cards and audio cards (`Ctrl/Cmd+Shift+A`) always write to the `Sentence` and `SentenceAudio` fields. Normal word cards keep using your `ankiConnect.fields` mapping.
|
||||||
|
|
||||||
To mine multiple subtitle lines as one sentence card, use `Ctrl/Cmd+Shift+S` followed by a digit (1–9) to select how many recent lines to combine.
|
## Word card type (Kiku/Lapis)
|
||||||
|
|
||||||
## Field Grouping (Kiku)
|
When `isKiku` or `isLapis` is enabled, SubMiner sets a card-type flag on word cards it fills. Choose the flag with `ankiConnect.lapisKiku.wordCardKind`:
|
||||||
|
|
||||||
When you mine the same word multiple times, SubMiner can merge the cards instead of creating duplicates. This is designed for note types like [Kiku](https://github.com/youyoumu/kiku) that support grouped sentence/audio/image fields.
|
| Value | Flag |
|
||||||
|
| ------------------- | ----------------------- |
|
||||||
|
| `word-and-sentence` | `IsWordAndSentenceCard` |
|
||||||
|
| `click` | `IsClickCard` |
|
||||||
|
| `sentence` | `IsSentenceCard` |
|
||||||
|
| `audio` | `IsAudioCard` |
|
||||||
|
| `none` | Leaves flags alone |
|
||||||
|
|
||||||
|
The other card-type flags are cleared. Sentence cards and audio cards keep their own flag.
|
||||||
|
|
||||||
|
## Field grouping (Kiku/Senren) {#field-grouping-kiku-senren}
|
||||||
|
|
||||||
|
When you mine a word that already has a card, SubMiner can merge the new card into the old one. The sentence, audio, image, and MiscInfo from both cards are kept as grouped entries, and the template lets you switch between them. This works with [Kiku](https://github.com/youyoumu/kiku) and [Senren](https://github.com/BrenoAqua/Senren) (which calls it [scene switching](https://github.com/BrenoAqua/Senren/blob/main/docs/scene_switching.md)).
|
||||||
|
|
||||||
|
Enable one of them. They write different markup to the same fields, so only one can be on. If both are enabled, Kiku is used and SubMiner logs a config warning.
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
"ankiConnect": {
|
"ankiConnect": {
|
||||||
"isKiku": {
|
"isKiku": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"fieldGrouping": "manual", // "auto", "manual", or "disabled"
|
"fieldGrouping": "manual",
|
||||||
"deleteDuplicateInAuto": true // delete new card after auto-merge
|
"deleteDuplicateInAuto": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Modes
|
For Senren, use the same keys under `isSenren`.
|
||||||
|
|
||||||
**Disabled** (`"disabled"`): No duplicate detection. Each card is independent.
|
| `fieldGrouping` | Behavior |
|
||||||
|
| --------------- | ------------------------------------------------------------------------------- |
|
||||||
|
| `disabled` | No duplicate check |
|
||||||
|
| `auto` | Merge into the existing card. With `deleteDuplicateInAuto`, delete the new card |
|
||||||
|
| `manual` | Show both cards, let you choose which to keep and preview the merge |
|
||||||
|
|
||||||
**Auto** (`"auto"`): When a duplicate expression is found, SubMiner merges the new card into the existing one automatically. Both sentences, audio clips, and images are preserved, and exact duplicate values are collapsed to one entry. If `deleteDuplicateInAuto` is true, the new card is deleted after merging.
|
The manual dialog cancels itself after 90 seconds. Identical entries are not deduplicated. Press `Ctrl/Cmd+G` to run the duplicate check on the last card yourself.
|
||||||
|
|
||||||
**Manual** (`"manual"`): A modal appears in the overlay showing both cards. You choose which card to keep, preview the merge result, then confirm. The modal has a 90-second timeout, after which it cancels automatically.
|
| Key | Action |
|
||||||
|
| ----------- | ------------------------------------- |
|
||||||
|
| `1` / `2` | Keep card 1 or card 2 |
|
||||||
|
| `Enter` | Confirm |
|
||||||
|
| `Backspace` | Back from the merge preview |
|
||||||
|
| `Esc` | Cancel and leave both cards unchanged |
|
||||||
|
|
||||||
### What Gets Merged
|
## Config validation
|
||||||
|
|
||||||
| Field | Merge behavior |
|
Invalid `ankiConnect` values produce a warning and fall back to the default. Use JSON booleans (`true`, not `"true"`) and a positive number for `pollingRate`.
|
||||||
| -------- | --------------------------------------------------------------- |
|
|
||||||
| Sentence | Both sentences preserved (exact duplicate text is deduplicated) |
|
|
||||||
| Audio | Both `[sound:...]` entries kept (exact duplicates deduplicated) |
|
|
||||||
| Image | Both images kept (exact duplicates deduplicated) |
|
|
||||||
|
|
||||||
### Keyboard Shortcuts in the Modal
|
|
||||||
|
|
||||||
| Key | Action |
|
|
||||||
| --------- | ---------------------------------- |
|
|
||||||
| `1` / `2` | Select card 1 or card 2 to keep |
|
|
||||||
| `Enter` | Confirm selection |
|
|
||||||
| `Esc` | Cancel (keep both cards unchanged) |
|
|
||||||
|
|
||||||
## Full Config Example
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"ankiConnect": {
|
|
||||||
"enabled": true,
|
|
||||||
"url": "http://127.0.0.1:8765",
|
|
||||||
"pollingRate": 3000,
|
|
||||||
"proxy": {
|
|
||||||
"enabled": false,
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 8766,
|
|
||||||
"upstreamUrl": "http://127.0.0.1:8765",
|
|
||||||
},
|
|
||||||
"fields": {
|
|
||||||
"audio": "ExpressionAudio",
|
|
||||||
"image": "Picture",
|
|
||||||
"sentence": "Sentence",
|
|
||||||
"miscInfo": "MiscInfo",
|
|
||||||
"translation": "SelectionText",
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"generateAudio": true,
|
|
||||||
"generateImage": true,
|
|
||||||
"imageType": "static",
|
|
||||||
"imageFormat": "jpg",
|
|
||||||
"imageQuality": 92,
|
|
||||||
"audioPadding": 0.5,
|
|
||||||
"maxMediaDuration": 30,
|
|
||||||
},
|
|
||||||
"behavior": {
|
|
||||||
"overwriteAudio": true,
|
|
||||||
"overwriteImage": true,
|
|
||||||
"mediaInsertMode": "append",
|
|
||||||
"autoUpdateNewCards": true,
|
|
||||||
"notificationType": "osd",
|
|
||||||
},
|
|
||||||
"ai": {
|
|
||||||
"enabled": false,
|
|
||||||
"model": "openai/gpt-4o-mini",
|
|
||||||
"systemPrompt": "Translate mined sentence text only.",
|
|
||||||
},
|
|
||||||
"isKiku": {
|
|
||||||
"enabled": false,
|
|
||||||
"fieldGrouping": "disabled",
|
|
||||||
"deleteDuplicateInAuto": true,
|
|
||||||
},
|
|
||||||
"isLapis": {
|
|
||||||
"enabled": false,
|
|
||||||
"sentenceCardModel": "Japanese sentences",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ai": {
|
|
||||||
"enabled": false,
|
|
||||||
"apiKey": "",
|
|
||||||
"apiKeyCommand": "",
|
|
||||||
"baseUrl": "https://openrouter.ai/api",
|
|
||||||
"requestTimeoutMs": 15000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,140 +1,55 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
This page is a contributor-facing architecture summary. Canonical internal architecture guidance lives in `docs/architecture/README.md` at the repo root.
|
A contributor-facing map of how SubMiner is put together. The canonical internal guidance, including domain ownership and layering rules, is [`docs/architecture/README.md`](https://github.com/ksyasuda/SubMiner/blob/main/docs/architecture/README.md) in the repo.
|
||||||
|
|
||||||
SubMiner is split into three cooperating runtimes:
|
SubMiner runs as three cooperating runtimes:
|
||||||
|
|
||||||
- Electron desktop app (`src/`) for overlay/UI/runtime orchestration.
|
- the Electron desktop app (`src/`): overlay, UI, and runtime orchestration
|
||||||
- Launcher CLI (`launcher/`) for mpv/app command workflows.
|
- the launcher CLI (`launcher/`): mpv and app command workflows
|
||||||
- mpv Lua plugin (`plugin/subminer/init.lua` + module files) for player-side controls and IPC handoff.
|
- the mpv Lua plugin (`plugin/subminer/`): player-side controls and handoff to the app
|
||||||
|
|
||||||
Within the desktop app, `src/main.ts` is a composition root that wires small runtime/domain modules plus core services.
|
Inside the app, `src/main.ts` is a composition root. It owns wiring and state, and delegates behavior to small runtime and domain modules that can be tested without Electron or mpv.
|
||||||
|
|
||||||
## Goals
|
## Project layout
|
||||||
|
|
||||||
- Keep behavior stable while reducing coupling.
|
|
||||||
- Prefer small, single-purpose units that can be tested in isolation.
|
|
||||||
- Keep `main.ts` focused on wiring and state ownership, not implementation detail.
|
|
||||||
- Follow Unix-style composability:
|
|
||||||
- each service does one job
|
|
||||||
- services compose through explicit inputs/outputs
|
|
||||||
- orchestration is separate from implementation
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
launcher/ # Standalone CLI launcher wrapper and mpv helpers
|
launcher/
|
||||||
commands/ # Command modules (doctor/config/mpv/jellyfin/playback/app passthrough)
|
main.ts # entrypoint and command dispatch
|
||||||
config/ # Launcher config parsers + CLI parser builder
|
commands/ # one module per subcommand (playback, jellyfin, stats, sync, ...)
|
||||||
main.ts # Launcher entrypoint and command dispatch
|
config/ # launcher config readers and CLI parser
|
||||||
plugin/
|
plugin/subminer/ # mpv plugin; main.lua loads init.lua, which boots the other modules
|
||||||
subminer/ # Modular mpv plugin (init · main · bootstrap · lifecycle · process
|
|
||||||
# state · messages · hover · ui · options · environment · log
|
|
||||||
# binary · aniskip · aniskip_match)
|
|
||||||
src/
|
src/
|
||||||
ai/ # AI translation provider utilities (client, config)
|
main-entry.ts # bootstrap wrapper that runs before main.js
|
||||||
main-entry.ts # Background-mode bootstrap wrapper before loading main.js
|
main.ts # composition root
|
||||||
main.ts # Entry point — delegates to runtime composers/domain modules
|
preload*.ts # preload bridges (overlay, settings, stats, sync, Jellyfin setup)
|
||||||
preload.ts # Electron preload bridge
|
main/ # main-process runtime modules and IPC/CLI wiring
|
||||||
types.ts # Shared type definitions
|
boot/ # pre-ready boot helpers
|
||||||
main/ # Main-process composition/runtime adapters
|
runtime/composers/ # larger runtime clusters assembled for main.ts
|
||||||
app-lifecycle.ts # App lifecycle + app-ready runtime runner factories
|
runtime/domains/ # domain barrels (startup, overlay, mpv, ipc, shortcuts, anilist, jellyfin, mining)
|
||||||
character-dictionary-runtime.ts # Character-dictionary orchestration/public runtime API
|
core/services/ # focused services: mpv client, overlay, tokenizer, mining, integrations, stats
|
||||||
cli-runtime.ts # CLI command runtime service adapters
|
core/utils/ # pure helpers
|
||||||
config-validation.ts # Startup/hot-reload config error formatting and fail-fast helpers
|
shared/ipc/ # IPC channel constants and payload validators
|
||||||
dependencies.ts # Shared dependency builders for IPC/runtime services
|
renderer/ # overlay renderer: subtitle rendering, input handlers, modals
|
||||||
ipc-runtime.ts # IPC runtime registration wrappers
|
config/ # definitions/ (defaults + option registries) and resolve/ (resolution pipeline)
|
||||||
overlay-runtime.ts # Overlay modal routing + active-window selection
|
cli/ # app CLI parsing and help output
|
||||||
overlay-shortcuts-runtime.ts # Overlay keyboard shortcut handling
|
settings/, syncui/ # settings and sync windows
|
||||||
overlay-visibility-runtime.ts # Overlay visibility + tracker-driven bounds service
|
window-trackers/ # Hyprland, Sway, X11, macOS, and Windows trackers
|
||||||
frequency-dictionary-runtime.ts # Frequency dictionary runtime adapter
|
anki-integration/ # AnkiConnect proxy and note-update workflow
|
||||||
jlpt-runtime.ts # JLPT dictionary runtime adapter
|
jimaku/, subsync/, tsukihime/ # integration helpers
|
||||||
media-runtime.ts # Media path/title/subtitle-position runtime service
|
types/ # shared domain types
|
||||||
startup.ts # Startup bootstrap dependency builder
|
stats/ # stats dashboard UI (Vite)
|
||||||
startup-lifecycle.ts # Lifecycle runtime runner adapter
|
vendor/ # Yomitan fork, texthooker-ui, JLPT vocab
|
||||||
state.ts # Application runtime state container + reducer transitions
|
|
||||||
subsync-runtime.ts # Subsync command runtime adapter
|
|
||||||
character-dictionary-runtime/ # Character-dictionary fetch/build/cache modules + focused tests
|
|
||||||
runtime/
|
|
||||||
composers/ # High-level composition clusters used by main.ts
|
|
||||||
domains/ # Domain barrel exports (startup/overlay/mpv/jellyfin/...)
|
|
||||||
registry.ts # Domain registry consumed by main.ts
|
|
||||||
core/
|
|
||||||
services/ # Focused runtime services (Electron adapters + pure logic)
|
|
||||||
anilist/ # AniList token store/update queue/update helpers
|
|
||||||
immersion-tracker/ # Immersion persistence/session/metadata modules
|
|
||||||
tokenizer/ # Tokenizer stage modules (selection/enrichment/annotation)
|
|
||||||
utils/ # Pure helpers and coercion/config utilities
|
|
||||||
cli/ # CLI parsing and help output
|
|
||||||
config/ # Config defaults/definitions, loading, parse, resolution pipeline
|
|
||||||
definitions/ # Domain-specific defaults + option registries
|
|
||||||
resolve/ # Domain-specific config resolution pipeline stages
|
|
||||||
shared/ipc/ # Cross-process IPC channel constants + payload validators
|
|
||||||
renderer/ # Overlay renderer (modularized UI/runtime)
|
|
||||||
handlers/ # Keyboard/mouse interaction modules
|
|
||||||
modals/ # Jimaku/Kiku/subsync/runtime-options/session-help modals
|
|
||||||
positioning/ # Subtitle position controller (drag-to-reposition)
|
|
||||||
window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS, Windows)
|
|
||||||
jimaku/ # Jimaku API integration helpers
|
|
||||||
subsync/ # Subtitle sync (alass/ffsubsync) helpers
|
|
||||||
subtitle/ # Subtitle processing utilities
|
|
||||||
tokenizers/ # Tokenizer implementations
|
|
||||||
anki-integration/ # AnkiConnect proxy server + note-update enrichment workflow
|
|
||||||
token-mergers/ # Token merge strategies
|
|
||||||
translators/ # AI translation providers
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service Layer (`src/core/services/`)
|
A few ownership notes that are hard to guess from file names:
|
||||||
|
|
||||||
- **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts`
|
- mpv access is split into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), and property modules under `src/core/services/`.
|
||||||
- **Shortcuts/input:** `shortcut.ts`, `overlay-shortcut.ts`, `overlay-shortcut-handler.ts`, `shortcut-fallback.ts`, `numeric-shortcut.ts`
|
- The renderer keeps `renderer.ts` to orchestration. Keyboard, mouse, and gamepad input live in `renderer/handlers/`, and each modal flow has its own file in `renderer/modals/`.
|
||||||
- **MPV runtime:** `mpv.ts`, `mpv-transport.ts`, `mpv-protocol.ts`, `mpv-properties.ts`, `mpv-render-metrics.ts`
|
- AniSkip intro detection runs in the app (`src/main/runtime/aniskip-runtime.ts`), which drives mpv chapters and the skip key over the mpv IPC socket. The plugin does not handle it.
|
||||||
- **Mining + Anki/Jimaku runtime:** `mining.ts`, `field-grouping.ts`, `field-grouping-overlay.ts`, `anki-jimaku.ts`, `anki-jimaku-ipc.ts`
|
|
||||||
- **Subtitle/token pipeline:** `subtitle-processing-controller.ts`, `subtitle-position.ts`, `subtitle-ws.ts`, `tokenizer.ts` + `tokenizer/*` stage modules (including `parser-enrichment-worker-runtime.ts` for async MeCab enrichment and `yomitan-parser-runtime.ts`)
|
|
||||||
- **Integrations:** `jimaku.ts`, `subsync.ts`, `subsync-runner.ts`, `texthooker.ts`, `jellyfin.ts`, `jellyfin-remote.ts`, `discord-presence.ts`, `yomitan-extension-loader.ts`, `yomitan-settings.ts`
|
|
||||||
- **Anki integration:** `anki-integration.ts`, `anki-integration/anki-connect-proxy.ts` (local proxy for push-based auto-enrichment), `anki-integration/note-update-workflow.ts`
|
|
||||||
- **Config/runtime controls:** `config-hot-reload.ts`, `runtime-options-ipc.ts`, `cli-command.ts`, `startup.ts`
|
|
||||||
- **Domain submodules:** `anilist/*` (token/update queue/updater), `immersion-tracker/*` (storage/session/metadata/query/reducer)
|
|
||||||
|
|
||||||
### Renderer Layer (`src/renderer/`)
|
## Component diagram
|
||||||
|
|
||||||
The renderer keeps `renderer.ts` focused on orchestration. UI behavior is delegated to per-concern modules.
|
The main process drives one primary overlay window plus modal surfaces. Primary and secondary subtitle layers render in the same overlay renderer, connected to the main process through `preload.ts`. The launcher and mpv plugin run as separate processes and talk to the app through sockets or CLI passthrough.
|
||||||
|
|
||||||
```text
|
|
||||||
src/renderer/
|
|
||||||
renderer.ts # Entrypoint/orchestration only
|
|
||||||
context.ts # Shared runtime context contract
|
|
||||||
state.ts # Centralized renderer mutable state (visible overlay only)
|
|
||||||
error-recovery.ts # Global renderer error boundary + recovery actions
|
|
||||||
overlay-content-measurement.ts # Reports rendered bounds to main process
|
|
||||||
subtitle-render.ts # Primary/secondary subtitle rendering + style application
|
|
||||||
positioning.ts # Facade export for positioning controller
|
|
||||||
yomitan-popup.ts # Yomitan popup iframe detection utilities
|
|
||||||
positioning/
|
|
||||||
controller.ts # Subtitle drag-position controller
|
|
||||||
position-state.ts # Position state helpers (yPercent)
|
|
||||||
handlers/
|
|
||||||
keyboard.ts # Keybindings, chord handling, modal key routing
|
|
||||||
mouse.ts # Hover/drag behavior, selection + observer wiring
|
|
||||||
modals/
|
|
||||||
jimaku.ts # Jimaku modal flow
|
|
||||||
kiku.ts # Kiku field-grouping modal flow
|
|
||||||
runtime-options.ts # Runtime options modal flow
|
|
||||||
session-help.ts # Keyboard shortcuts/help modal flow
|
|
||||||
subsync.ts # Manual subsync modal flow
|
|
||||||
utils/
|
|
||||||
dom.ts # Required DOM lookups + typed handles
|
|
||||||
platform.ts # Layer/platform capability detection
|
|
||||||
```
|
|
||||||
|
|
||||||
### Launcher + Plugin Runtimes
|
|
||||||
|
|
||||||
- `launcher/main.ts` dispatches commands through `launcher/commands/*` and shared config readers in `launcher/config/*`. It handles mpv startup, app passthrough, Jellyfin helper commands, and playback handoff.
|
|
||||||
- `plugin/subminer/init.lua` runs inside mpv and loads modular Lua files: `main.lua` (orchestration), `bootstrap.lua` (startup), `lifecycle.lua` (connect/disconnect), `process.lua` (process management), `state.lua` (shared state), `messages.lua` (IPC), `hover.lua` (hover-token highlight rendering), `ui.lua` (OSD rendering), `options.lua` (config), `environment.lua` (detection), `log.lua` (logging), `binary.lua` (path resolution), `aniskip.lua` + `aniskip_match.lua` (intro-skip UX).
|
|
||||||
|
|
||||||
## Flow Diagram
|
|
||||||
|
|
||||||
The main process orchestrates a single primary overlay window plus modal surfaces: `main.ts` delegates to composition modules that wire together domain services. Subtitle layers (primary + secondary bar) are rendered in the same overlay renderer process, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough.
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
@@ -215,69 +130,82 @@ flowchart TB
|
|||||||
style ExtRt fill:#363a4f,stroke:#494d64,color:#cad3f5
|
style ExtRt fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
```
|
```
|
||||||
|
|
||||||
## Composition Pattern
|
## Composition pattern
|
||||||
|
|
||||||
Most runtime code follows a dependency-injection pattern:
|
Runtime code uses dependency injection:
|
||||||
|
|
||||||
1. Define a service interface in `src/core/services/*`.
|
1. Put the logic in a service under `src/core/services/`, as pure or side-effect-bounded functions.
|
||||||
2. Keep core logic in pure or side-effect-bounded functions.
|
2. Build its runtime dependencies in a `src/main/` module. Pass simple dependencies inline; extract an adapter only when it adds behavior or gets reused.
|
||||||
3. Build runtime deps in `src/main/` composition modules; extract an adapter/helper only when it adds meaningful behavior or reuse.
|
3. Call the service from lifecycle or command wiring.
|
||||||
4. Call the service from lifecycle/command wiring points.
|
|
||||||
|
|
||||||
The composition root (`src/main.ts`) delegates to focused modules in `src/main/` and `src/main/runtime/composers/`:
|
`main.ts` gets domain handlers through `createMainRuntimeRegistry()` (`src/main/runtime/registry.ts`), which exposes the barrels in `src/main/runtime/domains/`. Larger clusters, such as app-ready startup, mpv, Jellyfin, AniList tracking, shortcuts, and IPC, are assembled by composers in `src/main/runtime/composers/`. Many handlers take a `*MainDeps` object built by a `createBuild*MainDepsHandler` builder, which keeps side effects out of the unit under test.
|
||||||
|
|
||||||
- `startup.ts` — argv/env processing and bootstrap flow
|
Composers declare their inputs with `ComposerInputs<T>` and results with `ComposerOutputs<T>` from `src/main/runtime/composers/contracts.ts`. A missing dependency then fails at compile time.
|
||||||
- `app-lifecycle.ts` — Electron lifecycle event registration
|
|
||||||
- `startup-lifecycle.ts` — app-ready initialization sequence
|
|
||||||
- `state.ts` — centralized application runtime state container
|
|
||||||
- `ipc-runtime.ts` — IPC channel registration and handler wiring
|
|
||||||
- `cli-runtime.ts` — CLI command parsing and dispatch
|
|
||||||
- `overlay-runtime.ts` — overlay window selection and modal state management
|
|
||||||
- `subsync-runtime.ts` — subsync command orchestration
|
|
||||||
- `runtime/composers/anilist-tracking-composer.ts` — AniList media tracking/probe/retry wiring
|
|
||||||
- `runtime/composers/jellyfin-runtime-composer.ts` — Jellyfin config/client/playback/command/setup composition wiring
|
|
||||||
- `runtime/composers/mpv-runtime-composer.ts` — MPV event/factory/tokenizer/warmup wiring
|
|
||||||
|
|
||||||
Composer modules share contract conventions via `src/main/runtime/composers/contracts.ts`:
|
### IPC boundary
|
||||||
|
|
||||||
- composer input surfaces are declared with `ComposerInputs<T>` so required dependencies cannot be omitted at compile time
|
Channel names live in `src/shared/ipc/contracts.ts` and payload validators in `src/shared/ipc/validators.ts`. Renderer payloads are validated at the IPC entry points (`src/core/services/ipc.ts`, `src/core/services/anki-jimaku-ipc.ts`) before any domain handler runs. See [IPC + runtime contracts](/ipc-contracts) for the full rules.
|
||||||
- composer outputs are declared with `ComposerOutputs<T>` to keep result contracts explicit and stable
|
|
||||||
- builder return payload extraction should use shared type helpers instead of inline ad-hoc inference
|
|
||||||
|
|
||||||
This keeps side effects explicit and makes behavior easy to unit-test with fakes.
|
### Runtime state ownership
|
||||||
|
|
||||||
Additional conventions in the current code:
|
Some domains, such as AniList token, queue, and media-guess state, use reducer-style transitions:
|
||||||
|
|
||||||
- `main.ts` uses `createMainRuntimeRegistry()` (`src/main/runtime/registry.ts`) to access domain handlers (`startup`, `overlay`, `mpv`, `ipc`, `shortcuts`, `anilist`, `jellyfin`, `mining`) without importing every runtime module directly.
|
- Composition modules own the mutable state and expose narrow `get*`/`set*` accessors.
|
||||||
- Domain barrels in `src/main/runtime/domains/*` re-export runtime handlers + main-deps builders, while composers in `src/main/runtime/composers/*` assemble larger runtime clusters.
|
- Handlers change another domain's state only through its transition helpers in `src/main/state.ts`, never by mutating the object directly.
|
||||||
- Many runtime handlers accept `*MainDeps` objects generated by `createBuild*MainDepsHandler` builders to isolate side effects and keep units testable.
|
- A transition may update derived counters or snapshots, but must leave metadata it does not own untouched.
|
||||||
|
- Tests for these domains check both the fields that should change and the ones that should not.
|
||||||
|
|
||||||
### IPC Contract + Validation Boundary
|
## Playback startup flow
|
||||||
|
|
||||||
- Central channel constants live in `src/shared/ipc/contracts.ts` and are consumed by both main (`ipcMain`) and renderer preload (`ipcRenderer`) wiring.
|
A SubMiner-managed launch (the `subminer` launcher, the app's own playback, or the packaged Windows shortcut) starts mpv, injects the plugin, and brings up the overlay. 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` has no settings, so command-line options always win.
|
||||||
- Runtime payload parsers/type guards live in `src/shared/ipc/validators.ts`.
|
|
||||||
- Rule: renderer-supplied payloads must be validated at IPC entry points (`src/core/services/ipc.ts`, `src/core/services/anki-jimaku-ipc.ts`) before calling domain handlers.
|
|
||||||
- Malformed invoke payloads return explicit structured errors (for example `{ ok: false, error: ... }`) and malformed fire-and-forget payloads are ignored safely.
|
|
||||||
|
|
||||||
### Runtime State Ownership (Migrated Domains)
|
Once mpv is up, exactly one of two triggers starts the overlay:
|
||||||
|
|
||||||
For domains migrated to reducer-style transitions (for example AniList token/queue/media-guess runtime state), follow these rules:
|
- On a first launch, the launcher sets `auto_start=yes` and the plugin's `file-loaded` hook starts the app once the socket is ready.
|
||||||
|
- When the app is already running, or for `--start-overlay` and YouTube flows, the launcher attaches over the app control socket and suppresses the plugin's auto-start.
|
||||||
|
|
||||||
- Composition/runtime modules own mutable state cells and expose narrow `get*`/`set*` accessors.
|
Both paths end in the same app bring-up, which then runs the program lifecycle below.
|
||||||
- Domain handlers do not mutate foreign state directly; they call explicit transition helpers that encode invariants.
|
|
||||||
- Transition helpers may sync derived counters/snapshots, but must preserve non-owned metadata unless the transition explicitly owns that metadata.
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Program Lifecycle
|
```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
|
||||||
|
|
||||||
- **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.
|
Entry["Managed launch<br/>subminer CLI · app · Windows shortcut"]:::entry
|
||||||
- **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.
|
Entry --> Cfg["Launcher reads config.jsonc<br/>→ plugin runtime config"]:::extrt
|
||||||
- **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`.
|
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
|
||||||
- **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.
|
Spawn --> Boot["Plugin boot · read_options('subminer')<br/>empty subminer.conf; CLI opts win"]:::extrt
|
||||||
- **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).
|
Boot --> Sock["mpv IPC socket ready"]:::proc
|
||||||
- **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.
|
Sock --> Who{"Overlay trigger"}:::decision
|
||||||
- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, stops the AnkiConnect proxy server, and cleans Anki/AniList state.
|
|
||||||
|
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 sockets in this flow are described in [IPC + runtime contracts](./ipc-contracts#runtime-sockets).
|
||||||
|
|
||||||
|
## Program lifecycle
|
||||||
|
|
||||||
|
1. **Module init.** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs services, and wires dependencies. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
|
||||||
|
2. **Startup.** `--generate-config` writes the template and exits. Otherwise `app-lifecycle.ts` takes the single-instance lock and registers Electron lifecycle hooks.
|
||||||
|
3. **App ready.** `composeAppReadyRuntime()` reloads config strictly, resolves keybindings, creates the `MpvIpcClient` (which connects and observes subtitle and playback properties), and starts the runtime options manager, subtitle timing tracker, and immersion tracker.
|
||||||
|
4. **Overlay.** `initializeOverlayRuntime()` creates the overlay window, registers global shortcuts, and tracks mpv's window bounds through the active window tracker. `src/main/runtime/overlay-mpv-sub-visibility.ts` hides mpv's own subtitles while the overlay shows them.
|
||||||
|
5. **Background warmups.** MeCab, Yomitan, JLPT and frequency dictionaries, the optional Jellyfin remote session, Discord presence, AniList token refresh, and the optional AnkiConnect proxy start asynchronously. `startupWarmups` controls which run; its low-power mode defers everything except Yomitan.
|
||||||
|
6. **Runtime.** Event-driven. mpv property changes, IPC messages, CLI commands, shortcuts, and config hot-reloads route through handlers and composers. Subtitle text goes through `SubtitleProcessingController` (normalize, tokenize, merge) and out to the overlay renderer and modals.
|
||||||
|
7. **Shutdown.** `onWillQuitCleanup` tears down the tray, config watcher, shortcuts, WebSocket and texthooker servers, mpv socket, window tracker, and Yomitan parser window. It flushes the immersion tracker to SQLite and stops Jellyfin, Discord, and the AnkiConnect proxy.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
@@ -341,11 +269,11 @@ flowchart TB
|
|||||||
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
|
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
```
|
```
|
||||||
|
|
||||||
## Subtitle Prefetch Pipeline
|
## Subtitle prefetch
|
||||||
|
|
||||||
SubMiner can pre-tokenize upcoming subtitle lines before they appear on screen. When an external subtitle file (SRT, VTT, or ASS) is detected on the active track, the `SubtitlePrefetchService` parses all cues via the `SubtitleCueParser`, identifies a priority window of upcoming lines based on the current playback position, and tokenizes them in the background through the same pipeline used for live subtitles. Results are stored directly into the `SubtitleProcessingController` cache, so when a subtitle actually appears during playback, it hits a warm cache and renders in ~30-50ms instead of ~200-320ms.
|
SubMiner tokenizes upcoming subtitle lines before they appear, so they render from a warm cache. `SubtitlePrefetchService` (`src/core/services/subtitle-prefetch.ts`) gets the cue list from the active track: an external subtitle file, or for local media an embedded text track extracted with ffmpeg. It parses the cues with `subtitle-cue-parser.ts`, picks a window of upcoming lines from the playback position, and tokenizes them through the live pipeline, storing results in the `SubtitleProcessingController` cache.
|
||||||
|
|
||||||
The prefetcher yields to live subtitle processing (which always takes priority over background work) and re-computes its priority window on seek. Cache invalidation events (e.g. marking a word as known) trigger re-prefetching of the current window to keep results fresh.
|
Live subtitle processing always takes priority; the prefetcher pauses while the on-screen line is being processed. It recomputes its window on seek and re-prefetches when the cache is invalidated, for example after a word is marked known.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
@@ -354,7 +282,7 @@ flowchart TB
|
|||||||
classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
|
||||||
SubFile["External Sub File"]:::init
|
SubFile["Subtitle Track"]:::init
|
||||||
Parse["Cue Parser"]:::phase
|
Parse["Cue Parser"]:::phase
|
||||||
Window["Upcoming Lines"]:::phase
|
Window["Upcoming Lines"]:::phase
|
||||||
Tokenize["Pre-tokenize"]:::warmup
|
Tokenize["Pre-tokenize"]:::warmup
|
||||||
@@ -371,22 +299,11 @@ flowchart TB
|
|||||||
style Render stroke-width:2px
|
style Render stroke-width:2px
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why This Design
|
## Extension rules
|
||||||
|
|
||||||
- **Smaller blast radius:** changing one feature usually touches one service.
|
- Add behavior to a service in `src/core/services/` or a focused module under `src/main/runtime/`. Keep new logic out of `main.ts`.
|
||||||
- **Better testability:** most behavior can be tested without Electron windows/mpv.
|
- For changes that span startup, overlay, mpv, or integration wiring, compose through `src/main/runtime/domains/` and `src/main/runtime/composers/` instead of wiring directly in `main.ts`.
|
||||||
- **Better reviewability:** PRs can be scoped to one subsystem.
|
- Add a cross-process channel in `src/shared/ipc/contracts.ts` first, validate it in `src/shared/ipc/validators.ts`, then wire the handler. See [IPC + runtime contracts](/ipc-contracts#add-a-new-ipc-action).
|
||||||
- **Backward compatibility:** CLI flags and IPC channels can remain stable while internals evolve.
|
- Config is split by domain under `src/config/definitions/` and `src/config/resolve/`. Keep config changes in the matching domain file.
|
||||||
- **Runtime registry + domain barrels:** `src/main/runtime/registry.ts` and `src/main/runtime/domains/*` reduce direct fan-in inside `main.ts` while keeping domain ownership explicit.
|
- Keep CLI flags and IPC channels stable unless a change is meant to break them.
|
||||||
- **Extracted composition root:** `main.ts` delegates to focused modules under `src/main/` and `src/main/runtime/composers/` for lifecycle, IPC, overlay, mpv, shortcut, and integration wiring.
|
- Add or update focused tests when a runtime boundary or contract changes, including malformed-payload tests for IPC.
|
||||||
- **Split MPV service layers:** MPV internals are separated into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), and properties/render metrics modules for maintainability.
|
|
||||||
- **Config by domain:** defaults, option registries, and resolution are split by domain under `src/config/definitions/*` and `src/config/resolve/*`, keeping config evolution localized.
|
|
||||||
|
|
||||||
## Extension Rules
|
|
||||||
|
|
||||||
- Add behavior to an existing service in `src/core/services/*` or create a focused runtime module under `src/main/runtime/*`; avoid ad-hoc logic in `main.ts`.
|
|
||||||
- Add new cross-process channels in `src/shared/ipc/contracts.ts` first, validate payloads in `src/shared/ipc/validators.ts`, then wire handlers in IPC runtime modules.
|
|
||||||
- See also the contributor IPC onboarding page: [IPC + Runtime Contracts](/ipc-contracts).
|
|
||||||
- If change spans startup/overlay/mpv/integration wiring, prefer composing through `src/main/runtime/domains/*` + `src/main/runtime/composers/*` rather than direct wiring in `main.ts`.
|
|
||||||
- Keep service APIs explicit and narrowly scoped, and preserve existing CLI flag / IPC channel behavior unless the change is intentionally breaking.
|
|
||||||
- Add or update focused tests (including malformed-payload IPC tests) when runtime boundaries or contracts change.
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { expect, test } from 'bun:test';
|
||||||
|
import { onRequest, resolveArchiveRoute, type ArchiveBucket } from './functions/v/[[path]]';
|
||||||
|
|
||||||
|
// In-memory stand-in for the R2 binding, including the range and precondition behavior
|
||||||
|
// the function relies on.
|
||||||
|
function fakeBucket(objects: Record<string, string>): ArchiveBucket {
|
||||||
|
return {
|
||||||
|
async get(key, options) {
|
||||||
|
const content = objects[key];
|
||||||
|
if (content === undefined) return null;
|
||||||
|
const bytes = new TextEncoder().encode(content);
|
||||||
|
const httpEtag = `"${key}"`;
|
||||||
|
|
||||||
|
if (options?.onlyIf?.get('if-none-match') === httpEtag) {
|
||||||
|
return { size: bytes.length, httpEtag };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeHeader = options?.range?.get('range');
|
||||||
|
const match = rangeHeader ? /^bytes=(\d+)-(\d*)$/.exec(rangeHeader) : null;
|
||||||
|
if (match) {
|
||||||
|
const offset = Number(match[1]);
|
||||||
|
const end = match[2] ? Number(match[2]) : bytes.length - 1;
|
||||||
|
const slice = bytes.slice(offset, end + 1);
|
||||||
|
return {
|
||||||
|
size: bytes.length,
|
||||||
|
httpEtag,
|
||||||
|
range: { offset, length: slice.length },
|
||||||
|
body: new Blob([slice]).stream(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { size: bytes.length, httpEtag, body: new Blob([bytes]).stream() };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = fakeBucket({
|
||||||
|
'v/0.19.6/index.html': '<h1>home</h1>',
|
||||||
|
'v/0.19.6/usage.html': '<h1>usage</h1>',
|
||||||
|
'v/0.19.6/404.html': '<h1>missing</h1>',
|
||||||
|
'v/0.19.6/assets/app.abc123.js': 'console.log(1)',
|
||||||
|
});
|
||||||
|
|
||||||
|
function request(path: string, init?: RequestInit) {
|
||||||
|
return onRequest({
|
||||||
|
request: new Request(`https://docs.subminer.moe${path}`, init),
|
||||||
|
env: { DOCS_ARCHIVES: bucket },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('archive routes follow the clean-URL layout of the built archives', () => {
|
||||||
|
expect(resolveArchiveRoute('/v/0.19.6/usage')).toEqual({
|
||||||
|
kind: 'lookup',
|
||||||
|
version: '0.19.6',
|
||||||
|
keys: ['v/0.19.6/usage.html', 'v/0.19.6/usage/index.html'],
|
||||||
|
});
|
||||||
|
expect(resolveArchiveRoute('/v/0.19.6/')).toEqual({
|
||||||
|
kind: 'lookup',
|
||||||
|
version: '0.19.6',
|
||||||
|
keys: ['v/0.19.6/index.html'],
|
||||||
|
});
|
||||||
|
expect(resolveArchiveRoute('/v/0.19.6', '?q=1')).toEqual({
|
||||||
|
kind: 'redirect',
|
||||||
|
location: '/v/0.19.6/?q=1',
|
||||||
|
status: 301,
|
||||||
|
});
|
||||||
|
expect(resolveArchiveRoute('/v/')).toEqual({
|
||||||
|
kind: 'redirect',
|
||||||
|
location: '/versions',
|
||||||
|
status: 302,
|
||||||
|
});
|
||||||
|
expect(resolveArchiveRoute('/v/0.19.6/%2e%2e/secret')).toEqual({ kind: 'not-found' });
|
||||||
|
expect(resolveArchiveRoute('/v/latest/')).toEqual({ kind: 'not-found' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serves archive pages and hashed assets with their cache policy', async () => {
|
||||||
|
const page = await request('/v/0.19.6/usage');
|
||||||
|
expect(page.status).toBe(200);
|
||||||
|
expect(page.headers.get('content-type')).toBe('text/html; charset=utf-8');
|
||||||
|
expect(page.headers.get('cache-control')).toBe('public, max-age=3600');
|
||||||
|
expect(page.headers.get('x-robots-tag')).toBe('noindex, follow');
|
||||||
|
expect(await page.text()).toBe('<h1>usage</h1>');
|
||||||
|
|
||||||
|
const asset = await request('/v/0.19.6/assets/app.abc123.js');
|
||||||
|
expect(asset.headers.get('content-type')).toBe('text/javascript; charset=utf-8');
|
||||||
|
expect(asset.headers.get('cache-control')).toContain('immutable');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing archive pages fall back to the archive 404 page', async () => {
|
||||||
|
const response = await request('/v/0.19.6/nope');
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(await response.text()).toBe('<h1>missing</h1>');
|
||||||
|
|
||||||
|
const unknownVersion = await request('/v/0.1.0/usage');
|
||||||
|
expect(unknownVersion.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports byte ranges, conditional requests, and HEAD', async () => {
|
||||||
|
const partial = await request('/v/0.19.6/usage', { headers: { Range: 'bytes=4-8' } });
|
||||||
|
expect(partial.status).toBe(206);
|
||||||
|
expect(partial.headers.get('content-range')).toBe('bytes 4-8/14');
|
||||||
|
expect(await partial.text()).toBe('usage');
|
||||||
|
|
||||||
|
const notModified = await request('/v/0.19.6/usage', {
|
||||||
|
headers: { 'If-None-Match': '"v/0.19.6/usage.html"' },
|
||||||
|
});
|
||||||
|
expect(notModified.status).toBe(304);
|
||||||
|
|
||||||
|
const head = await request('/v/0.19.6/usage', { method: 'HEAD' });
|
||||||
|
expect(head.status).toBe(200);
|
||||||
|
expect(head.headers.get('content-length')).toBe('14');
|
||||||
|
expect(await head.text()).toBe('');
|
||||||
|
|
||||||
|
const post = await request('/v/0.19.6/usage', { method: 'POST' });
|
||||||
|
expect(post.status).toBe(405);
|
||||||
|
});
|
||||||
@@ -1,12 +1,643 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v0.14.0 (2026-05-12)
|
## v0.20.0 (2026-09-23)
|
||||||
|
|
||||||
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**
|
||||||
|
- **Japanese Subtitle Generation**:
|
||||||
|
- Generate Japanese SRT subtitles locally with whisper.cpp. Start it from a new modal (Ctrl+Shift+G), from the generate button in an empty subtitle sidebar, or with `subminer generate-subs`.
|
||||||
|
- Generation shows progress, can be cancelled, and loads the finished subtitles into mpv automatically.
|
||||||
|
- Pick an official multilingual Whisper model, including quantized variants, with size and accuracy guidance. You can download it in the app or point Settings at a model you already have.
|
||||||
|
- `large-v3-turbo` is recommended when CUDA support is detected, and `small` otherwise.
|
||||||
|
- whisper-cli, ffmpeg, and ffprobe are found on PATH unless you override them. SubMiner names any missing tools before a download starts.
|
||||||
|
- An optional "Focus on spoken dialogue" mode uses a separately downloaded Silero VAD model. It keeps audible sections it is unsure about, so dialogue under music is not dropped, but songs may also be transcribed.
|
||||||
|
- Long passages are split near speech starts or quiet pauses to reduce subtitles that appear too early. When an eligible embedded or external subtitle track is loaded in mpv, it guides the split points.
|
||||||
|
- Each passage runs in a fresh Whisper process, which prevents repeated-character output.
|
||||||
|
- **Subtitle Selection Modal**:
|
||||||
|
- An optional modal for choosing primary and secondary mpv subtitle tracks.
|
||||||
|
- Turn it on in Settings under Behavior, then press g followed by s to open it. Turning it off restores mpv's own subtitle selection binding.
|
||||||
|
- Single-key actions take priority over configured key sequence prefixes.
|
||||||
|
- Conflicting sequences are disabled with a warning, and the existing y commands stay reserved.
|
||||||
|
- **Subtitle Sidebar Copy**:
|
||||||
|
- Select dialogue across several sidebar rows and copy it without timestamps using Ctrl/Cmd+C or the Copy button.
|
||||||
|
- Selecting text does not seek playback and does not require mining a card.
|
||||||
|
- **Media Timing Review Screenshot Picker**:
|
||||||
|
- Choose the still screenshot separately from the audio range, with a live preview and its own time slider.
|
||||||
|
- Step through decoded frames one at a time to get the exact frame you want.
|
||||||
|
- Works with local video and with seekable remote streams such as Jellyfin.
|
||||||
|
- **mpv Keybindings in the Overlay**:
|
||||||
|
- The overlay now picks up keyboard bindings from mpv defaults, `input.conf`, and loaded scripts when they do not conflict with SubMiner.
|
||||||
|
- SubMiner controls and bindings you explicitly disabled take precedence.
|
||||||
|
- These bindings apply only to the current session and are not listed in the help menu.
|
||||||
|
- **Jimaku Live Action Search**: The Jimaku modal has new Anime and Live action tabs, so you can search Jimaku's live action catalogue as well as anime. Use Arrow Left and Arrow Right to switch tabs.
|
||||||
|
- **TMDB Live-Action Library**:
|
||||||
|
- Live-action dramas and movies in the stats Library now get posters, synopses, and titles from TMDB.
|
||||||
|
- Release builds include a project key. Setting `tmdb.apiKey` or `tmdb.apiKeyCommand` overrides it, and one of them is required when running from source.
|
||||||
|
- Titles that AniList cannot match are looked up on TMDB automatically when the parsed filename exactly matches a Japanese live-action title. For everything else, use the new **Link to TMDB** action.
|
||||||
|
- Entries linked to the same TMDB title merge into one card, and the Library kind selector has a new Live Action option.
|
||||||
|
- If a replacement download fails during provider reassignment, the previous link and artwork are kept. Merges and sync keep AniList and TMDB identities separate, and the merge dialog explains mixed selections instead of failing.
|
||||||
|
- **YouTube Library Kind**:
|
||||||
|
- YouTube channels are now their own Library media kind. Existing channel entries migrate automatically, and viewing history and manual video assignments are unchanged.
|
||||||
|
- New All Titles, Anime, and YouTube Library filters.
|
||||||
|
- Channels are excluded from AniList matching, season repair, and duplicate recommendations.
|
||||||
|
- Merges and video moves can no longer combine an anime entry with a YouTube channel.
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
- **Launcher Uses Bundled Bun**:
|
||||||
|
- Every installed and downloadable launcher now runs on the Bun runtime that ships with SubMiner. A system Bun is no longer needed.
|
||||||
|
- Recognized legacy launchers migrate automatically.
|
||||||
|
- Windows gets a `subminer.cmd` launcher download.
|
||||||
|
- First-run setup is reduced to one optional launcher control. Runtime repair guidance appears only when it is needed.
|
||||||
|
- **Faster Sync Transfers**:
|
||||||
|
- Sync between compatible macOS and Linux machines now uses compressed, incremental rsync transfers.
|
||||||
|
- The last snapshot received from each peer is cached, which reduces traffic on later syncs.
|
||||||
|
- Machines without a compatible rsync, including Windows, fall back to compressed scp.
|
||||||
|
- Older peers still work without the upload cache.
|
||||||
|
- Transfers abort after 30 minutes.
|
||||||
|
- **Stats Server Request Safety**:
|
||||||
|
- The stats server now accepts loopback hosts only and rejects requests from browser origins other than its own.
|
||||||
|
- Requests that change data must send an `application/json` body. Scripts that POST to the server need to set a JSON content type.
|
||||||
|
- The in-app stats overlay now loads from the local server, so it gets the same protection.
|
||||||
|
- Dashboards served through a reverse proxy or Tailscale Serve are no longer supported.
|
||||||
|
- **Smaller Downloads**:
|
||||||
|
- Installers and unpacked apps are smaller. Demo media, source maps, TypeScript sources, test fixtures, and unused Koffi binaries are no longer packaged.
|
||||||
|
- All windows now share one Japanese UI font.
|
||||||
|
- Release builds publish package size reports that compare against the previous release.
|
||||||
|
- **Bundled Yomitan**: Updated with upstream Yomitan 26.9.8 changes, including historical Japanese kana transformations, Ukrainian language support, and improvements to Anki duplicate searches and audio retrieval.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- **Jellyfin 12 Compatibility**:
|
||||||
|
- Playback, subtitle, artwork, and remote-control requests now authenticate with the `ApiKey` query parameter, so the integration works on Jellyfin 12, where legacy authorization is off by default.
|
||||||
|
- "Play on SubMiner" stays available. The cast connection now answers keep-alive requests and reconnects when the server stops responding, instead of silently dying after about a minute.
|
||||||
|
- The Jellyfin "now playing" bar clears when you close or finish a cast video instead of running on to the end of the episode.
|
||||||
|
- Cards mined during Jellyfin playback get the episode title in the misc info field again instead of "Unknown media".
|
||||||
|
- **Jellyfin Privacy and Playback**:
|
||||||
|
- Jellyfin streams no longer leak titles taken from the stream URL, or stream URLs that contain credentials, into metadata lookups, Anki source fields, Discord presence, stats, or AniList retries.
|
||||||
|
- Previously cached metadata that contained credentials is cleaned up. Watch history and library assignments are not touched.
|
||||||
|
- Jellyfin playback and casting now use your configured mpv executable, so they work when mpv is installed outside PATH. Portable plugins next to that executable are detected.
|
||||||
|
- **Anki Mining**:
|
||||||
|
- New `ankiConnect.fields.wordAudio` setting reads word audio separately from the sentence audio field. This fixes animated images that started moving immediately when `fields.audio` pointed to `SentenceAudio`. Existing animated images need to be regenerated to pick up the fix.
|
||||||
|
- Sentence furigana on word cards stays in sync with the full stats-search context and with expanded timing review selections. Stale readings are cleared if regeneration fails.
|
||||||
|
- Closing the overlay while media timing review is still loading now cancels the review, resumes playback if the review paused it, and cleans up the hidden preview player.
|
||||||
|
- `ankiConnect.media.maxMediaDuration: 0` now means unlimited when mining from the stats dashboard, matching overlay mining.
|
||||||
|
- Invalid AnkiConnect, Kiku, and Senren settings are now rejected with a warning and fall back to defaults.
|
||||||
|
- **Stats Server Stability**:
|
||||||
|
- A port conflict is now reported in a status notification instead of crashing SubMiner.
|
||||||
|
- Simultaneous startup requests share one server start. Stopping the background server no longer disconnects dashboards open in the foreground.
|
||||||
|
- Shutdown waits only a limited time for active requests to finish.
|
||||||
|
- Malformed resource IDs, and ID lists with any invalid entries, are rejected before Library changes or cover backfills run.
|
||||||
|
- **Subtitle Sidebar**:
|
||||||
|
- Clicking a cue no longer leaves the row focused, and Space no longer seeks back to a focused cue. Enter still seeks to the focused cue, and Space keeps its configured playback action.
|
||||||
|
- The sidebar stays near the current playback position during gaps when the subtitle file has a cue that starts at zero.
|
||||||
|
- **Settings Save Feedback**:
|
||||||
|
- Settings marked LIVE no longer show false restart warnings, including for notifications and subtitle generation.
|
||||||
|
- When a save mixes live and restart-only changes, the live changes apply right away and only the changed sections that need a restart are listed.
|
||||||
|
- **Overlay Windows**:
|
||||||
|
- On Hyprland, recovery dialogs stay above SubMiner windows so overlay placement updates no longer cover their Wait and Close buttons.
|
||||||
|
- On Linux, a delayed close callback during teardown can no longer reopen the overlay.
|
||||||
|
- **First Launch on macOS**: SubMiner no longer exits on first launch when the config directory does not exist yet.
|
||||||
|
|
||||||
|
**Docs**
|
||||||
|
- **Launcher**: Documented the launcher install that uses the bundled runtime, migration from legacy launchers, package-managed updates, and the bundled Bun runtime's MIT and LGPL notices. The AUR package installs these notices under `/usr/share/licenses/subminer-bin`, and they are also included in `subminer-assets.tar.gz`.
|
||||||
|
- **Subtitle Generation**: Documented model choice, VAD behavior, splitting guided by a reference track, fallback behavior, and known limits.
|
||||||
|
- **Subtitle Selection**: Documented the subtitle selector setting, its shortcut override, and the primary and secondary track controls.
|
||||||
|
- **Settings**: Clarified save feedback for live settings, warnings for saves that mix live and restart-only changes, and how subtitle generation settings reload.
|
||||||
|
- **Jellyfin**:
|
||||||
|
- Clarified that Windows mpv playback and Jellyfin casting can use a configured executable path instead of PATH.
|
||||||
|
- Documented how Jellyfin media titles and stats identities keep stream credentials out of metadata.
|
||||||
|
- **Stats Library**:
|
||||||
|
- Documented TMDB linking, provider reassignment, merge compatibility, and caching of the credential command's output.
|
||||||
|
- Documented YouTube channel filtering and video statistics in the Library.
|
||||||
|
- **Mining**:
|
||||||
|
- Documented choosing the screenshot separately in media timing review.
|
||||||
|
- Documented the separate word audio field mapping, including that existing animated images need to be regenerated.
|
||||||
|
- **Sync**: Documented compressed transfers, where the incremental sync cache is stored, and compatibility with older peers.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
**Internal**
|
||||||
|
- Removed duplicate source and launcher smoke runs from the reusable CI quality gate. Every distinct test lane and failure artifact is kept.
|
||||||
|
- Replaced mislabeled dist source reruns with a small Electron-runtime smoke check. It covers compiled stats startup, the HTTP service, native SQLite, port conflicts, and cleanup.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Previous Versions
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>v0.19.x</summary>
|
||||||
|
|
||||||
|
<h2>v0.19.6 (2026-09-04)</h2>
|
||||||
|
|
||||||
**Added**
|
**Added**
|
||||||
|
|
||||||
- **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.
|
- **Card Timing Review**:
|
||||||
|
- Optional pre-generation timing review for word, sentence, and audio cards, with a speech-weighted waveform that flattens background noise so dialogue edges stand out clearly.
|
||||||
|
- The clip end automatically snaps back to where the line's dialogue actually ends once the waveform loads, with drag and keyboard adjustments available.
|
||||||
|
- Audio preview includes a sweeping playhead that plays the clip to its true end, even on high-latency outputs like Bluetooth headphones.
|
||||||
|
- Previous and next subtitle lines can be pulled onto the card with `P`/`N` (or the Prev/Next steppers) and removed with Shift; the sentence preview and waveform markers update automatically.
|
||||||
|
- Cancelling lets you keep a card without media, and the review can be toggled on or off for the session.
|
||||||
|
- **Senren Field Grouping**:
|
||||||
|
- Enable `ankiConnect.isSenren` to merge duplicate mined cards using Senren's scene-switching markup, grouping sentence, furigana, audio, picture, and misc-info fields.
|
||||||
|
- Supports the same auto/manual/disabled modes as Kiku, including the manual merge modal; only one of Senren or Kiku can be enabled at a time.
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- **Remote Stream Mining Performance**: Mining a card from a remote stream (Jellyfin and other HTTP sources) now downloads the clip window once and reuses it for the timing review waveform, audio preview, audio extraction, and screenshot, instead of re-fetching the stream at each step; the temporary file is cleaned up after ten minutes of inactivity or on exit.
|
||||||
|
- **TsukiHime Release Filtering**: The TsukiHime modal's Japanese and secondary-language tabs now filter the release list by the subtitle languages each release actually carries, and report when no release has subtitles for the active tab.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- **Subtitle & Mining Accuracy**:
|
||||||
|
- Broadcast-style captions that split one sentence across two on-screen rows (e.g. Crunchyroll Japanese subs) now merge into a single line for the sidebar and mined cards, while separate speakers, sound effects, and labeled turns still stay on their own lines.
|
||||||
|
- Mining from the overlay no longer pulls in a lingering row from the previous caption; the mined sentence and clip timing now match what's actually on screen.
|
||||||
|
- Multi-line copy and mining now select lines backward in timeline order after seeking, instead of in playback encounter order.
|
||||||
|
- Copying a subtitle, mining a sentence, or recording immersion stats no longer includes the separate furigana line that broadcast ASS captions place above a word.
|
||||||
|
- **Card Update Notifications**: Dismissed lingering overlay card-update progress when notification settings switch to OSD before an update finishes.
|
||||||
|
- **Overlay Stability on Hyprland**: Opening a modal window (timing review, Jimaku, session help, and others) while mpv is fullscreen no longer causes the overlay to flicker while the modal loads; the overlay now stays on screen untouched until the modal is ready.
|
||||||
|
- **Jellyfin Subtitle Sync**: Jellyfin subtitle files now load with zero mpv delay instead of inferring and saving an offset from Japanese and English cue timelines.
|
||||||
|
- **Secondary Subtitle Visibility**: Native mpv secondary subtitles stay hidden when switching secondary subtitle tracks during playback.
|
||||||
|
|
||||||
|
<h2>v0.19.5 (2026-08-30)</h2>
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- **Anki Card Update Progress**: The card-update spinner now stays visible until audio and image updates finish, instead of disappearing early.
|
||||||
|
- **Anki Word-Card Fields**: Word-card enrichment now writes sentence text and audio to the fields configured in AnkiConnect, while the dedicated sentence-card and audio-card actions keep their existing compatible field names.
|
||||||
|
- **Overlapping Subtitles**:
|
||||||
|
- Subtitle lines that start while another line is still on screen now appear alongside it, instead of staying hidden until a track switch or seek.
|
||||||
|
- Subtitles shown at the same time now stack by their authored screen position, with top signs and song lines above bottom dialogue.
|
||||||
|
- Half-size ASS furigana is no longer shown as if it were a dialogue line.
|
||||||
|
- **YouTube Auto Captions**:
|
||||||
|
- Auto-generated captions now follow their intended timing and two-row roll-up layout.
|
||||||
|
- Long speech is paged instead of covering the video with a wall of text.
|
||||||
|
- Explicitly timed sound cues like `[音楽]` no longer cover later dialogue.
|
||||||
|
|
||||||
|
<h2>v0.19.4 (2026-08-25)</h2>
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
- **Library Merge & Move**: Duplicate library cards for the same show can now be combined. Select cards in the library grid and use "Merge Selected" to pick which entry to keep and move every episode onto it, preserving sessions, mined cards, and watch time. Episodes can also be reassigned individually via the "→" button, useful when a file lands under a stray title; manual assignments survive later filename parsing, Jellyfin refreshes, and season repair. Exact AniList title matches with compatible seasons now merge automatically, while fuzzy matches surface as dismissible "Possible duplicate" reviews instead of merging silently.
|
||||||
|
- **Duplicate Line Cleanup Tool**: The Vocabulary tab's new "Duplicates" button scans a chosen time window (7 days through all time) for old karaoke/typeset duplicate-line bursts, shows what it found, and collapses each run to one line once confirmed; `subminer stats cleanup --duplicate-lines` does the same from the terminal, with `--dry-run` and `--lookback-days <n>` options. Watch time and lines-seen totals are left unchanged.
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
- **Prerelease Release Notes**: Prerelease notes now open with a "Changes since" section listing only what changed versus the previous beta/RC of the same version, above the cumulative highlights, and CI rejects prerelease tags whose committed notes were generated for a different beta/RC.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- **Subtitle & Karaoke Duplication**:
|
||||||
|
- Karaoke and animated signs are reconstructed once from their authored text and shown only while actually sung, with original word spacing preserved, instead of flooding the overlay, subtitle sidebar, immersion history, mining, or stats with glyph fragments, per-frame color phases, and repeated animation events.
|
||||||
|
- Decorative layers (highlight sweeps, glow/shadow copies, symbol-font decoration, particle swarms, hidden or zero-scaled text) stay out of published text, while ordinary repeated dialogue, positioned signs, wrapped lyric rows, and multi-row CC-style blocks still display correctly.
|
||||||
|
- Embedded subtitle tracks on network-mounted (SMB/NFS) media are extracted and parsed again instead of falling back to live-text-only, restoring karaoke reconstruction, sidebar cues, and mining for releases that only ship subtitles inside the container.
|
||||||
|
- Secondary subtitles go through the same deduplication pipeline as primary subtitles and no longer clip display after about four lines.
|
||||||
|
- Event-heavy karaoke files that previously stalled subtitle loading for several seconds now parse in well under a second.
|
||||||
|
- **Character Dictionary Reliability**:
|
||||||
|
- Generation, merged rebuilds, and imports no longer freeze the app on large dictionaries; snapshot I/O, archive building, and image/name lookup caches moved off the UI's critical path.
|
||||||
|
- Dictionaries are reused instead of regenerated when MeCab finds no name splits.
|
||||||
|
- Cached portraits restore correctly after the portrait index finishes loading post-tokenization.
|
||||||
|
- Desktop progress notifications on Linux AppImage installs update in place instead of flickering, fixing a bug where the AppImage's bundled libraries broke the system notification helper.
|
||||||
|
- **Overlay Startup & Modals**:
|
||||||
|
- The macOS window-tracking helper targets macOS 12.0+ instead of requiring the build machine's exact macOS version, fixing crashes on older systems like Ventura that left the overlay stuck on "Overlay loading".
|
||||||
|
- mpv IPC connection attempts time out and retry, showing an actionable error if content still isn't ready after 30 seconds.
|
||||||
|
- Dedicated overlay modals are prewarmed on macOS and Windows so shortcuts open them promptly.
|
||||||
|
- On macOS, reused modals and the stats window open above fullscreen mpv on its current Space instead of jumping to another desktop.
|
||||||
|
- **Wayland File Drop**: Fixed native Wayland drag-and-drop from file managers such as Thunar, so subtitle and video files dropped on the visible overlay are resolved and forwarded to mpv.
|
||||||
|
- **Windows Mouse Lag**: Fixed system-wide mouse lag on Windows while SubMiner is running, caused by the overlay's global mouse hook for click-through forwarding and by the mpv window tracker blocking the app on repeated PowerShell lookups.
|
||||||
|
- **Sentence Mining Audio & Clips**: Sentence-audio generation no longer times out on slow network-mounted media with many subtitle/font streams (bounded FFmpeg probing, two-minute extraction budget, clearer error reporting), and mined audio/animated AVIF clips now capture the subtitle line that was actually mined by snapshotting the clip range at lookup time instead of reading live mpv state later.
|
||||||
|
- **Stats Performance & Reliability**: Immersion stats storage now sets its SQLite busy timeout before WAL setup, avoiding transient lock errors under concurrent writes. Deletes in the stats dashboard no longer freeze the UI, run proportional to what's deleted instead of rebuilding full lifetime summaries, retry safely if the delete worker crashes, and no longer rescan the whole library when deleting very common words; a new index also makes large session deletes drop from minutes to milliseconds. Library merges, video moves, and AniList reassignments got the same lifetime-summary fix.
|
||||||
|
- **Vocabulary Stats Accuracy**: Vocabulary totals and charts now count all tracked vocabulary instead of only the first page, new-word history uses corrected daily rollups (fixing legacy timestamp and time-zone issues), summary cards refresh automatically after edits to the exclusion list, and rapid exclusion edits no longer race each other.
|
||||||
|
- **Rofi MKV Thumbnails**: Fixed missing MKV thumbnails in the Linux rofi picker when system thumbnailer registrations only advertise legacy Matroska MIME aliases.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
**Internal**
|
||||||
|
- Docs Site Indexing: Excluded the `/main/` and `/v/<version>/` docs trees from search indexing (self-referential canonical, `noindex,follow`, matching `X-Robots-Tag`) so crawlers focus on current docs instead of ~30 archived copies of every page, and restored `<lastmod>` dates in the docs sitemap that were silently dropped by production builds.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<h2>v0.19.3 (2026-08-13)</h2>
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
- Changelog Modal: Adds an in-app changelog you can open from the tray ("View Changelog") or the "What's New" button on the update notification, so the notification stays reachable while you read. It shows the newest published release notes (falling back to the bundled changelog if that fetch fails), folds older versions while keeping the current one expanded, and supports keyboard navigation (`J`/`K`/arrows, `Enter`, `R`, `Esc`).
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
- Subtitle Tokenization Performance: Reworks subtitle dictionary lookups to cut per-line work roughly in half, cache repeated lookups across lines, and stop tokenization from competing with on-screen subtitle prefetching. Also fixes several accuracy issues along the way: dropped readings on trailing kana, character names being skipped after a dictionary sync, annotations not refreshing after mining a card, and halfwidth katakana character names losing their reading or being swallowed by other words.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- Character Dictionary Large Imports: Large character dictionaries (e.g. One Piece) no longer fail to install from a fixed timeout budget; the import now scales its time budget to dictionary size and reports detailed progress (page/character counts, image download progress, elapsed time) instead of one static message.
|
||||||
|
- Stats Delete Responsiveness: Deleting sessions, episodes, or library entries no longer freezes the stats page or an active video player; deletes are now batched into a single transaction.
|
||||||
|
- Styled Subtitle Cue Parsing: Heavily typeset subtitles (karaoke, signs) no longer flood the subtitle sidebar with garbage; vector drawing commands are no longer shown as text, and duplicate/animation-burst cues now collapse into one.
|
||||||
|
- X11 mpv Renderer: Fixes an mpv crash on the first fullscreen toggle for X11/XWayland users with `gpu-next` shaders (e.g. ArtCNN), which was caused by X11 mode forcing the legacy OpenGL renderer.
|
||||||
|
- X11 Overlay Display Scaling: Fixes the overlay appearing oversized and offset from mpv on X11/XWayland under fractional or mixed-monitor display scaling.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
**Internal**
|
||||||
|
- Subtitle text is now decoded from ASS exactly once at ingest, so the renderer, timing tracker, and tokenizer all share one decoded value instead of each re-deriving it.
|
||||||
|
- Added per-stage debug timings (`scanMs`, `mecabMs`, `frequencyMs`, `annotateMs`) to the subtitle tokenization pipeline log.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<h2>v0.19.2 (2026-08-04)</h2>
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
- Subsync: The sync modal now lets you choose both the reference subtitle (correct timing) and the out-of-sync subtitle to retime, for both alass and ffsubsync. alass can also use the loaded video's audio as a reference for local files. Retiming the secondary track now reloads the result into the secondary slot instead of overwriting the primary subtitle.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- Streaming Subtitle Tokenization: Jellyfin streams now seed subtitle tokenization directly from the downloaded subtitle file instead of relying on an mpv event that could be missed, and prefetching now runs to the end of the file and clears between episodes. The tokenization cache was raised from 256 to 2500 lines, and parsed cues are no longer lost when the active subtitle track briefly can't be resolved (e.g. switching to an embedded track). Together these prevent episodes from falling back to slow, line-by-line tokenization during playback.
|
||||||
|
- Overlay: Subtitle lines now appear immediately at their cue time even on a tokenization cache miss, upgrading in place once tokens and annotations are ready, instead of waiting on a line still being processed. A failed tokenization is no longer cached as plain text, so repeated lines get another chance at annotations.
|
||||||
|
- Background Logging: Background startup now respects the configured logging level when no explicit log level is passed.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
**Internal**
|
||||||
|
- Patched three high-severity dependency advisories (`undici`, `brace-expansion`, `fast-uri`).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<h2>v0.19.1 (2026-08-01)</h2>
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
- Word Card Type: Adds a setting (Settings > Mining/Anki > Kiku/Lapis Features > "Word Card Type") to choose which card-type flag SubMiner marks on Kiku/Lapis word cards — `word-and-sentence` (default), `click`, `sentence`, `audio`, or `none`. Click cards (`IsClickCard`) can now be flagged, and setting any card-type flag clears the others so a note can't claim two types at once.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- Yomitan Popup: Fixes the macOS Yomitan popup going inert after mining a card — clicks outside the popup no longer pass through to mpv, and scrolling over the popup scrolls its definitions instead of seeking playback.
|
||||||
|
- YouTube Playlist Links: Fixes opening a video from a playlist URL (e.g. a Watch Later link with `list=`/`index=`) timing out while probing subtitles, metadata, or the playback URL.
|
||||||
|
|
||||||
|
<h2>v0.19.0 (2026-07-29)</h2>
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
- Anki Maturity Highlighting: Known-word subtitle highlights can now be colored by Anki card maturity (new, learning, young, mature), similar to asbplayer. Tier thresholds and colors are configurable, with a runtime toggle and an updated help legend.
|
||||||
|
- Post-Playback Menu: After a watch-history episode ends, the fzf/rofi launcher returns to that series with options to play the previous or next episode, rewatch, pick another episode, or quit. The pre-playback series menu now offers the previous episode too.
|
||||||
|
- Delete Library Entries: The stats Library detail view can now delete an entire title in one step (episodes, sessions, subtitle lines, rollups, cover art, and vocabulary counts). Delete progress is now shown app-wide via a progress bar and status toast instead of disappearing when you switch tabs.
|
||||||
|
- Cross-Machine Sync: Added SSH-based syncing of stats and watch history between machines, available from the tray ("Sync Stats & History") or `subminer sync`, with saved devices, per-host sync direction, background auto-sync, connection testing, manual snapshots, and support for Windows remotes.
|
||||||
|
- TsukiHime Subtitle Downloads: Added subtitle downloads for the current video via TsukiHime, loading Japanese as the primary track and your configured secondary language directly into mpv.
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
- Clipboard-Video Shortcut: The "append clipboard video to queue" shortcut is now configurable.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- AniList Season Resolution: Season 2+ files now resolve to the correct AniList entry instead of silently falling back to season 1 (which mismatched character dictionaries and watch progress). Manual overrides now stay scoped per season, fix both the dictionary and progress tracking together, and also correct per-season cover art.
|
||||||
|
- Subtitle Annotation Accuracy: Fixed several annotation edge cases, including inconsistent POS exclusions on merged quote-particle tokens, dropped annotations on supplementary-plane kanji, katakana punctuation wrongly treated as noise, and certain kanji vocabulary losing N+1 highlighting eligibility.
|
||||||
|
- AnkiConnect Proxy Port Conflict: Video startup no longer crashes when another process already holds the configured AnkiConnect proxy port; a notification now explains how to resolve it.
|
||||||
|
- AppImage Quit Crash: Fixed a "Service Crash" desktop notification appearing after closing a video when running the Linux AppImage.
|
||||||
|
- Autoplay Pause Timing: Fixed playback resuming a few seconds before subtitle tokenization warmup finished, most noticeable when resuming mid-episode or when a cue starts within the first two seconds.
|
||||||
|
- Stats Known-Word Count: Fixed stats reporting 0 known words for every session after the known-word cache format changed.
|
||||||
|
- Stats Library Cover Art: Relinking a title to a different AniList entry now updates its cover in the Library grid immediately instead of leaving a stale, mismatched cover cached.
|
||||||
|
- Rofi Prompt Spacing: Rofi menu prompts now keep a space before the input field instead of running into the placeholder text.
|
||||||
|
- Stats Settings & Reliability: Hardened stats settings validation (nested/legacy AnkiConnect config now falls back safely instead of breaking) and stats routes against malformed requests and other edge cases.
|
||||||
|
- Stats Delete Performance: Deleting sessions, episodes, and library entries is now dramatically faster and no longer stalls playback (e.g. a 12-episode title dropped from about a minute to under a second on a large library); the Vocabulary tab also loads much faster. The first launch after upgrading runs a one-time database migration.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
**Internal**
|
||||||
|
- Added a golden-file regression test corpus for the tokenizer/annotation pipeline, plus scripts to record new fixtures and diff against stock Yomitan.
|
||||||
|
- Consolidated renderer modal state handling into a descriptor registry.
|
||||||
|
- Consolidated CI quality checks (PR, stable, and prerelease) into one reusable workflow with mpv plugin tests and dependency audits.
|
||||||
|
- Removed the unused stats IPC transport and unified stats dashboard HTTP types with the backend contract.
|
||||||
|
- Added a script to verify known-word highlight tiers against live Anki data outside of playback.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>v0.18.x</summary>
|
||||||
|
|
||||||
|
<h2>v0.18.0 (2026-07-10)</h2>
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
- Sentence Audio Normalization: Generated sentence audio is now normalized to -23 LUFS by default, and clips mined from playback mirror mpv's software volume curve with a limiter to prevent clipping. Both behaviors are configurable independently.
|
||||||
|
- Watch History Command: Added `subminer -H` / `--history` to browse watch history, replay or continue episodes, or pick one via fzf or rofi, with cover art shown in the rofi picker.
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
- Fzf Preview Layout: Moved fzf previews below launcher menus, giving long titles and metadata more room.
|
||||||
|
- Known-Word Highlighting: Now compares subtitle and Anki-card readings, preventing false matches between homographs and unrelated words that share a reading, while still supporting matching across kana and kanji spellings.
|
||||||
|
- Annotation Filtering: Standalone suffix tokens (e.g. さん, れる) are now excluded from JLPT/frequency/N+1 highlighting by default, matching how particles and interjections are treated; configurable via the pos2 exclusion setting.
|
||||||
|
- App Icon: Replaced the app icon with new pixel-art submarine artwork contributed by the community, used across the app icon, tray, notifications, README, docs site, and Stats page.
|
||||||
|
- Stats Trend Charts: Overhauled with persisted title visibility, per-chart title limits, "top" and "most recent" ranking modes, an option to show or hide empty days, calendar-aligned periods, and value-sorted tooltips.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- Background Stats Server: `subminer app` background launches now auto-start the stats server when enabled, and skip startup if one is already running.
|
||||||
|
- Character Name Highlighting: Character dictionaries now split unspaced native names more reliably, and portraits, highlights, and hover lookup survive punctuation, unmatched text, and competing dictionary matches without incorrectly splitting longer words.
|
||||||
|
- Highlighting Coverage: Frequency/JLPT highlighting and vocabulary stats now include content adverbs (e.g. 確かに, やはり) and kanji nouns MeCab tags as non-independent (e.g. 日, 点, 以外), while still suppressing interjections, pronouns, and grammar fragments; lexicalized kana expressions like かといって keep their annotations.
|
||||||
|
- Cover Art Fetching: Stats now fetches AniList cover art as soon as a new series starts playing, and backfills missing art for existing series on the next Stats visit.
|
||||||
|
- Kiku Field Grouping: The manual field-grouping dialog now stays above fullscreen mpv, remains usable across repeated attempts, closes abandoned windows after timeouts, and reports a clear error when the original card can no longer be loaded.
|
||||||
|
- Karaoke Subtitle Collapse: Secondary subtitles no longer stack dozens of one-syllable lines during openings and endings; repeated events collapse into one line, capped to a strip at the top.
|
||||||
|
- Unparsed Token Hover: Subtitle text Yomitan can't parse (truncated inflections, elongation runs) remains hoverable, while staying excluded from highlighting, N+1 candidate math, and vocabulary stats.
|
||||||
|
- YouTube Streaming: Fixed direct YouTube stream extraction that could corrupt signed URLs and cause ffmpeg 403 errors.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
**Internal**
|
||||||
|
- Test lanes moved to `scripts/test-lanes.ts` with per-directory discovery and isolated per-file timeouts; CI now covers previously orphaned stats, scripts, plugin process-retry, and runtime-compat suites, plus a new stats lane in the change-verification workflow.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>v0.17.x</summary>
|
||||||
|
|
||||||
|
<h2>v0.17.2 (2026-06-28)</h2>
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- YouTube Background Cache: Fixed Windows YouTube background media cache startup for YouTube URLs opened directly in mpv, including resolved stream URLs when mpv still exposes the original YouTube playlist entry, so queued Anki media updates can append audio and images after the cache finishes.
|
||||||
|
- YouTube Subtitle Picker: Manual subtitle picker requests now show an immediate configured notification while SubMiner probes tracks and opens the modal. Subtitle download progress is replaced with a transient success notification after tracks load.
|
||||||
|
|
||||||
|
<h2>v0.17.1 (2026-06-27)</h2>
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
- YouTube Media Cache Mode: Adds `youtube.mediaCache.mode` with `direct` and `background` options. Background mode uses a yt-dlp cache download when direct stream extraction is unreliable — creates a text-only card immediately, queues media updates for mined notes, and fills audio/image fields once the download finishes. Progress is announced via overlay/OSD notifications. Downloads are capped at 720p by default (`youtube.mediaCache.maxHeight`). Switching back to direct mode cancels any in-flight background download.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- Log Export: Fixed log filenames to use the local date so exports around UTC midnight include the current day's logs rather than stale prior-day files. Expanded export redaction to mask IPs, emails, auth and cookie headers, yt-dlp cookie arguments, URL credentials, token/key/password fields, and signed YouTube media URL parameters.
|
||||||
|
- YouTube Card Media: Improved media generation reliability by sending safer ffmpeg options for resolved streams and skipping stale stream maps (including cached YouTube files). Hardened background cache downloads with IPv4 and extractor retry flags; failed downloads now notify the user and clear queued media updates instead of leaving them silently pending. Stale background cache files are cleaned on startup and before each new download.
|
||||||
|
|
||||||
|
<h2>v0.17.0 (2026-06-15)</h2>
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- **Subtitle Delay Keybindings**: Updated default overlay subtitle delay and step bindings to match mpv conventions: `z`, `Z`, and `x` adjust `sub-delay`; `Ctrl+Shift+Left/Right` run native `sub-step` with OSD feedback. The previous SubMiner-only adjacent-cue delay action has been removed.
|
||||||
|
- **Update Notifications**: New installs now default update notifications to overlay-only instead of overlay + system notifications.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- **Anki – Highlight Word**: Fixed bolding of the mined word in Kiku sentence and sentence-furigana fields when the source Yomitan sentence did not already contain bold markup.
|
||||||
|
- **Anki – Lapis/Kiku Word Cards**: Fixed word-and-sentence marker missing from Lapis/Kiku word cards enriched through SubMiner, which could hide sentence context on the card front.
|
||||||
|
- **Anki – Windows Media Generation**: Fixed known-word cache refreshes when no deck is configured, and fixed audio/image generation after background launches by recreating missing FFmpeg temp directories before clipping.
|
||||||
|
- **Character Dictionary – Windows**: Fixed the Windows "SubMiner mpv" shortcut so character dictionary auto-sync can fall back to mpv's current video path when app media state is not yet ready.
|
||||||
|
- **Notifications**: Restored the SubMiner app icon on system notifications that do not supply a custom notification image.
|
||||||
|
- **Overlay – macOS Yomitan Popup**: Fixed Yomitan popup focus after card mining or popup reload; fixed popups staying open when clicking transparent overlay space — click-away now closes the popup and returns click passthrough to mpv without a hide/reappear cycle.
|
||||||
|
- **Overlay – Linux Auto-Pause Startup**: Fixed the visible overlay on Linux auto-paused startup to stay interactive during the initial measurement gap; startup subtitle cache misses now paint raw text before tokenization finishes, and temporarily empty `sub-text` refreshes are resolved before warm readiness resumes playback.
|
||||||
|
- **Overlay – Playlist Advance**: Fixed the visible overlay being dismissed when mpv advances to the next playlist item, including when the next episode loads after the warm transition delay.
|
||||||
|
- **Overlay – Windows Subtitle Bar**: Fixed shaky hover and click interaction on the Windows subtitle bar when a video attaches to an already-running background SubMiner instance.
|
||||||
|
- **Stats – AniList Linking**: Fixed manual AniList linking from the stats anime page so auto-searches strip the generated "Season N" suffix and query only the anime title.
|
||||||
|
- **Updates – Linux Support Assets**: Fixed Linux updates to correctly install and refresh the launcher runtime plugin copy and rofi theme alongside AppImage and launcher updates; unrelated SubMiner data directories are now left untouched and plugin copies are staged before replacing the live runtime. Fixed first-launch playback on fresh installs by auto-installing missing managed support assets from the bundled app before mpv starts.
|
||||||
|
|
||||||
|
**Docs**
|
||||||
|
|
||||||
|
- **Linux Update Flows**: Documented that Linux update flows manage the launcher runtime plugin copy and rofi theme from `subminer-assets.tar.gz`, and that normal launcher playback auto-installs those managed support assets on a fresh install if either is missing.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
**Internal**
|
||||||
|
|
||||||
|
- **Runtime Modules**: Split main-process runtime wiring into focused modules without changing user-facing behavior; hardened helpers against stale background stats daemon PIDs, stalled subtitle extraction, and dropped async errors.
|
||||||
|
- **Release CI**: Fixed GitHub release notes to preserve the `What's Changed` and `New Contributors` attribution sections when CI regenerates from the committed changelog; scoped prerelease note reuse to the same base version so a new beta line starts from current fragments.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>v0.16.x</summary>
|
||||||
|
|
||||||
|
<h2>v0.16.0 (2026-06-10)</h2>
|
||||||
|
|
||||||
|
**Breaking Changes**
|
||||||
|
|
||||||
|
- **Notification Mode `both`**: `notificationType: "both"` now routes to overlay + system notifications. Users who previously used `"both"` for mpv OSD + system notifications should set `notificationType` to `"osd-system"` in `config.jsonc`. The `osd` and `osd-system` values remain valid as config-file entries but no longer appear in Settings.
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- **Overlay Notifications**: New overlay notification stack (Catppuccin Macchiato theme) with configurable screen position (`notifications.overlayPosition`: top-left, top-center, top-right), 3-second transient dismissals, and persistent cards for long-running jobs like character dictionary sync.
|
||||||
|
- **Notification History Panel**: `Ctrl/Cmd+N` (configurable via `shortcuts.toggleNotificationHistory`) opens a session log of all notifications. Works whether the overlay or mpv has focus; slides in from the notification edge; entries can be individually removed or cleared.
|
||||||
|
- **Mined-Card Notification Actions**: Mined-card overlay notifications show generated card thumbnails and include an Open in Anki button in both live cards and history entries.
|
||||||
|
- **Update Notification Action**: Update-available overlay notifications include an Update button to start the app update flow directly from the notification.
|
||||||
|
- **Stats Search**: New Search tab in Stats for realtime subtitle sentence search with media context, headword matching, and mining actions for source-backed sentence cards or exact-match word/audio cards.
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- **AniSkip**: Moved intro detection from the mpv plugin to the SubMiner app. Lookups now cover every file loaded during a session including playlist advances, and `mpv.aniskipEnabled`/`mpv.aniskipButtonKey` hot-reload without restarting playback. The bundled plugin no longer makes network calls. Note: AniSkip now requires the SubMiner app to be connected; plugin-only mpv sessions will not fetch skip windows.
|
||||||
|
- **Stats Library**: Entries are now split by detected season (season folder first, filename parsing as fallback). Existing combined-series rows are automatically migrated to per-season entries on startup. Cover art and anime details refresh immediately after a manual AniList entry change.
|
||||||
|
- **Stats Vocabulary**: Remembers Hide Known/Hide Kana filters across sessions, applies Hide Kana filtering cross-title, collapses duplicate token variants in exclusions, and matches Related Seen Words by shared readings or kanji.
|
||||||
|
- **Stats Trends**: Reorganized into Activity, Cumulative Totals, Efficiency, Patterns, and Library sections; disambiguated per-period vs. cumulative charts; added Words/Min and Cards/Hour efficiency charts.
|
||||||
|
- **Stats Mining**: Sentence cards are created before slow media generation finishes; stored/requested secondary subtitles are preserved before falling back to sidecar or alass-retimed English subtitles; empty `ankiConnect.deck` falls back to Yomitan's mining deck; partial media failures are surfaced.
|
||||||
|
- **Stats Browsing**: Remembers library card size; retries stored cover art without extra AniList lookups; preserves PNG/WebP MIME types; honors custom AnkiConnect URLs for Browse; shows progress during session deletes.
|
||||||
|
- **Startup Notifications**: Tokenization, subtitle annotation, and character dictionary status now route through queued overlay notifications in `overlay`/`both` mode instead of falling back to mpv OSD while the overlay loads.
|
||||||
|
- **Notification Deduplication**: Cycling subtitle modes updates the active overlay card in place rather than stacking duplicates; repeated progress updates (e.g. subsync) tick in place without flickering.
|
||||||
|
- **Update Notification Default**: New installs default `notificationType` to `both` so update alerts appear in both overlay and system notifications.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- **AniList Completion**: Entries are now marked completed when a post-watch update reaches the final known episode of the season.
|
||||||
|
- **AniSkip Markers**: Fixed intro markers disappearing after same-media mpv reloads; fixed metadata detection for intros that start at 0 seconds and common release-group filenames.
|
||||||
|
- **Jellyfin Session**: Remote session now restarts after setup login so the websocket reconnects with fresh credentials, and stops cleanly on logout.
|
||||||
|
- **Sentence Card Audio**: Mining a sentence card no longer fills the expression audio field; generated audio goes only to the configured sentence audio field.
|
||||||
|
- **Stats Mining Fields**: Sentence clips update `SentenceAudio` correctly; word audio uses configured Yomitan sources; English subtitle text is not written to word cards; secondary subtitle auto-selection prefers regular English tracks over Signs/Songs tracks.
|
||||||
|
- **Overlay Hover Readiness**: Subtitle bars are hoverable and clickable from the first subtitle line on visible overlay startup or resume, without waiting for the next subtitle event.
|
||||||
|
- **Startup Autoplay**: Playback is released after tokenization and overlay content are ready even when playback begins before the first subtitle line appears.
|
||||||
|
- **Overlay Startup Feedback**: Restored mpv OSD loading spinner that starts on connect, media open, or overlay request, and clears once the overlay is content-ready and visible.
|
||||||
|
- **Linux Overlay Input**: Notification close and action buttons remain clickable above subtitle bars on Linux.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
**Internal**
|
||||||
|
- **Build**: `make deps` now initializes git submodules before installing dependencies on a fresh source checkout.
|
||||||
|
- **Release Tooling**: Release notes now credit contributors and first-time authors resolved from changelog fragments via git and the GitHub API.
|
||||||
|
- **Changelog Guidance**: PR fragment guidance updated to preserve separate-outcome fragments while directing contributors to consolidate same-PR follow-up notes before adding churn.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>v0.15.x</summary>
|
||||||
|
|
||||||
|
<h2>v0.15.2 (2026-06-02)</h2>
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
- Yomitan: Updated the bundled Yomitan build to the latest vendored revision.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- Anki - Animated AVIF: Clip timing no longer starts or ends early; word-audio lead-in and clip duration are now aligned to frame boundaries.
|
||||||
|
- Overlay (Hyprland): Fixed fullscreen overlay alignment - modal, stats, and sidebar content no longer shift below the mpv window.
|
||||||
|
- Overlay (macOS): Subtitle bars are now interactive immediately after autoplay starts with "wait for overlay to be ready" enabled, without requiring a manual click.
|
||||||
|
- Overlay (macOS): Fixed overlay, subtitles, and subtitle sidebar staying hidden after a modal closes until the user clicked the mpv window; focus is now restored to mpv when the last modal closes, so playback shortcuts and the overlay reappear correctly - including in native fullscreen.
|
||||||
|
|
||||||
|
<h2>v0.15.1 (2026-05-31)</h2>
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- **Linux Overlay Stacking**: Fixed the overlay intermittently dropping behind mpv on KDE Plasma and other non-Hyprland/Sway Wayland sessions; restored subtitle hover, pause-on-hover, and Yomitan lookups on X11/XWayland; the overlay now correctly layers above/below mpv based on fullscreen state, yields to foreground windows (Settings, Yomitan, AniList, etc.), and avoids startup flashes and fullscreen transition glitches.
|
||||||
|
- **Linux Overlay (Hyprland Lua)**: Fixed overlay placement on Hyprland 0.55+ when using a Lua-based config.
|
||||||
|
- **Manual Overlay Startup**: Fixed manual visible-overlay startup from mpv - now correctly attaches to playback, keeps the window bounds synced with mpv, and primes current subtitles before showing.
|
||||||
|
- **Playlist Transitions**: Reused the warm overlay when mpv advances to the next playlist item, avoiding a redundant tokenization pause and preserving visible subtitles across tracks.
|
||||||
|
- **macOS Overlay**: Fixed the visible subtitle overlay staying click-through after pause-until-ready releases playback; restored mpv focus after closing modal windows so subtitles and keybinds resume without clicking the player.
|
||||||
|
- **Mouse Keybindings**: Fixed keybinding capture and runtime handling for mouse buttons, including side buttons like `MBTN_BACK` and `MBTN_FORWARD`.
|
||||||
|
- **Windows mpv Shortcut**: Fixed the Windows `SubMiner mpv` shortcut so videos attach to an already-running background app instead of spawning a second process.
|
||||||
|
|
||||||
|
**Docs**
|
||||||
|
|
||||||
|
- **Troubleshooting**: Updated Hyprland overlay docs with current Lua (`hl.window_rule`) and legacy config syntax; added troubleshooting for KDE/Wayland and other non-Hyprland/Sway Wayland sessions; added a Character Dictionary troubleshooting section; added a "See Also" index linking each feature's troubleshooting page.
|
||||||
|
|
||||||
|
<h2>v0.15.0 (2026-05-29)</h2>
|
||||||
|
|
||||||
|
**Breaking Changes**
|
||||||
|
|
||||||
|
- **Subsync:**
|
||||||
|
- The `subsync.defaultMode` config option has been removed
|
||||||
|
- Subsync now always opens the manual subtitle picker regardless of any previously set default mode
|
||||||
|
- **N+1 Highlighting:**
|
||||||
|
- N+1 highlighting now has its own dedicated `ankiConnect.nPlusOne.enabled` option, separate from known-word highlighting
|
||||||
|
- It is no longer enabled automatically when known-word highlighting is on - enable it explicitly to keep N+1 annotations
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- **Auto-Updater:**
|
||||||
|
- Tray and `subminer -u` update checks with app update prompts
|
||||||
|
- Launcher and Linux rofi theme auto-updates
|
||||||
|
- Checksum verification and configurable notifications
|
||||||
|
- Opt-in prerelease channel via `updates.channel: "prerelease"`
|
||||||
|
- **Settings Window:**
|
||||||
|
- New dedicated Settings window via `subminer --settings` or `subminer settings`, organized into Appearance, Behavior, Anki, Input, and Integration sections
|
||||||
|
- Click-to-learn keybinding controls
|
||||||
|
- AnkiConnect-backed deck, field, and note-type pickers that auto-fill from the configured Anki deck
|
||||||
|
- Cross-category search
|
||||||
|
- Live save for most options including subtitle CSS, stats keys, logging level, Jimaku, Subsync, and Anki mappings
|
||||||
|
- AI and translation settings remain config-file only
|
||||||
|
- **Inline Character Portraits:**
|
||||||
|
- Optional AniList character portraits appear inline for name-matched subtitle text
|
||||||
|
- Manual AniList overrides scoped per parent media directory so separate season folders maintain separate character dictionary selections
|
||||||
|
- **Character Dictionary Manager:** New `Ctrl/Cmd+D` manager modal to remove, reorder, or override loaded entries.
|
||||||
|
- **Log Export:** Sanitized log ZIP export from the tray menu and via `subminer logs -e`, with home-directory usernames redacted from exported contents.
|
||||||
|
- **Launcher CLI:**
|
||||||
|
- `subminer --version` / `subminer -v` prints the installed app version
|
||||||
|
- `mpv.profile` config and Settings support passes a named mpv profile to managed launches
|
||||||
|
- Bundled mpv plugin startup options are now configurable from SubMiner config
|
||||||
|
- **First-Run Setup:**
|
||||||
|
- Optional installer for Bun and the `subminer` CLI on Linux, macOS, and Windows
|
||||||
|
- Windows `subminer.cmd` PATH shim so `subminer` works without manually adding `SubMiner.exe` to PATH
|
||||||
|
- Setup recognizes existing Homebrew or user PATH installs and avoids writing into Homebrew-owned paths
|
||||||
|
- Includes an Open SubMiner Settings button
|
||||||
|
- Standalone setup app quits after completing, returning terminal control
|
||||||
|
- **Primary Subtitle Visibility on Yomitan Popup:** New `subtitleStyle.primaryVisibleOnYomitanPopup` option keeps hover-mode primary subtitles visible while a Yomitan popup is open.
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- **Subtitle Appearance Config:**
|
||||||
|
- Primary and secondary subtitle appearance now use color controls plus CSS declaration editors, saved as `subtitleStyle.css`, `subtitleStyle.secondary.css`, and `subtitleSidebar.css`
|
||||||
|
- Known-word and N+1 annotation colors moved to `subtitleStyle.knownWordColor` and `subtitleStyle.nPlusOneColor`
|
||||||
|
- Subtitle font defaults updated to `Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP`
|
||||||
|
- Existing configs migrate automatically; legacy Anki color keys still accepted with deprecation warnings
|
||||||
|
- **Subtitle Style Defaults:**
|
||||||
|
- Stronger outline-style text shadow
|
||||||
|
- Thicker JLPT underlines
|
||||||
|
- Frequency `topX` default raised to `10000`
|
||||||
|
- **Character Dictionary:**
|
||||||
|
- Entries scoped to the current AniList media for name matching and inline portraits
|
||||||
|
- Generates Japanese-only name aliases so raw romanized/English aliases no longer surface as separate results
|
||||||
|
- In-app AniList selector waits for an explicit search with the box prefilled from the current filename
|
||||||
|
- `subtitleStyle.nameMatchEnabled` is now the sole switch for dictionary sync and builds
|
||||||
|
- **Electron Runtime:** Updated from 39.8.6 to 42.2.0, returning SubMiner to a supported Electron release line.
|
||||||
|
- **Jellyfin Setup:**
|
||||||
|
- Removed the server presets dropdown
|
||||||
|
- Setup now shows a single editable server URL field
|
||||||
|
- **Jellyfin Cast Identity:**
|
||||||
|
- Device identity now derived from the OS hostname and always reported as SubMiner
|
||||||
|
- Previously configurable identity fields are ignored, preventing multiple installs from sharing a remote-session identity
|
||||||
|
- **Startup Defaults:** Jellyfin remote-session startup warmup and character-name subtitle highlighting now default to off.
|
||||||
|
- **Setup Appearance:** Removed the bundled mpv runtime plugin readiness card from the setup flow.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
|
||||||
|
- **AniList Progress:**
|
||||||
|
- Progress updates fire correctly when playback reaches or skips past the watched threshold, using fresh mpv timing events
|
||||||
|
- Season-specific results preferred for multi-season files, with a clear message when the matched season is not in Planning or Watching
|
||||||
|
- Repeated missing-token checks no longer exhaust retry attempts or duplicate dead-letter entries
|
||||||
|
- **Anki Mining:**
|
||||||
|
- Sentence-audio padding is opt-in by default
|
||||||
|
- Animated AVIF freeze-frame duration aligned to word audio length without double-counting
|
||||||
|
- Multi-line sentence alignment fixed for repeated subtitle text
|
||||||
|
- Kiku duplicate-card detection, auto-merge, modal acknowledgment race, and field/tag ordering corrected
|
||||||
|
- YouTube playback cards use mpv's resolved stream URLs
|
||||||
|
- Sentence cards refresh the secondary subtitle before saving
|
||||||
|
- **Jellyfin Discovery:**
|
||||||
|
- Startup, subtitle track selection, and duplicate ready-signal handling all fixed
|
||||||
|
- Paused mpv no longer misreported as playing
|
||||||
|
- Resume corrected when a remote play command sends `StartPositionTicks: 0` despite saved progress
|
||||||
|
- **Jellyfin Remote:**
|
||||||
|
- Tray checkbox stays in sync on Linux after tray, CLI, or startup changes
|
||||||
|
- Remote controller visibility and progress sync fixed for seeks, stops, startup path changes, and Linux websocket reconnect windows
|
||||||
|
- Play and Resume now behave correctly (Play from beginning, Resume from saved position)
|
||||||
|
- Final progress reports reuse SubMiner's last known position when mpv resets on stop
|
||||||
|
- Windows setup login flow fixed with an IPC bridge, immediate feedback, and a timeout with inline error for unreachable servers
|
||||||
|
- **Overlay (macOS):**
|
||||||
|
- Overlay hides when mpv loses focus, is minimized, or is no longer the foreground app
|
||||||
|
- Stays stable through transient window geometry disappearances from macOS APIs and when clicking from the overlay back into mpv
|
||||||
|
- Stats overlay opened inactive so it appears over fullscreen mpv without switching Spaces
|
||||||
|
- Passthrough fixed so mpv controls stay clickable before hovering a subtitle bar
|
||||||
|
- **Yomitan Sidebar:**
|
||||||
|
- Playback stays paused for sidebar-opened Yomitan popups when auto-pause is enabled
|
||||||
|
- Popups now open when startup races the Yomitan extension load
|
||||||
|
- Sidebar mining cards use audio and images from the clicked sidebar line instead of the current primary subtitle
|
||||||
|
- **Launcher:**
|
||||||
|
- `subminer app` on Linux returns terminal control immediately
|
||||||
|
- `subminer app --setup` opens the setup flow when SubMiner is already running in the background
|
||||||
|
- **YouTube Playback:**
|
||||||
|
- Selected subtitles downloaded to local temp files so the primary bar and sidebar read the same source, with cleanup on reload and quit
|
||||||
|
- False load-failure notifications suppressed
|
||||||
|
- Tray icon created on launcher-managed playback that attaches to an already-running process
|
||||||
|
- **Shortcuts:**
|
||||||
|
- Native mpv menu shortcuts disabled during managed macOS playback so configured SubMiner shortcuts work while mpv has focus
|
||||||
|
- Custom session shortcuts including `stats.markWatchedKey` wired through mpv
|
||||||
|
- Multi-line copy/mine overlay correctly focused so number keys choose the line count on macOS and Windows
|
||||||
|
- **Controller Bindings:**
|
||||||
|
- Controller config and debug shortcuts stay closed while controller support is disabled
|
||||||
|
- Binding learn mode starts from the edit pencil
|
||||||
|
- Remaps saved per controller profile
|
||||||
|
- Binding badges also start learn mode
|
||||||
|
- Row reset buttons restore individual bindings to defaults
|
||||||
|
- **Logging:**
|
||||||
|
- `logging.level` forwarded to launcher-started and Windows shortcut-started mpv sessions, covering mpv log verbosity, plugin logging, and plugin-launched app logging
|
||||||
|
- `logging.rotation` (default 7 days) and per-component `logging.files` toggles added, with mpv logs disabled by default
|
||||||
|
- Repeated IPC socket warning spam suppressed while waiting for mpv to recreate the socket
|
||||||
|
- Windows mpv IPC, subtitle track, and Yomitan diagnostics added
|
||||||
|
- **In-Player Stats:**
|
||||||
|
- Layering fixed so delete confirmations, overlay modals, and update-check dialogs appear above the stats window
|
||||||
|
- Jellyfin playback stats grouped by item metadata so watched episodes merge with matching local library titles and keep clean display names
|
||||||
|
- **WebSocket Annotations:**
|
||||||
|
- Annotation spans and token metadata stay on the annotation WebSocket
|
||||||
|
- The regular subtitle WebSocket is plain-text only
|
||||||
|
- **Subtitle Annotation Prefetching:** Cached colored annotations and character images ready sooner for live subtitle changes without delaying raw subtitle display.
|
||||||
|
- **Windows Startup Errors:** Fatal startup failures now show a native error dialog and write details to the app log instead of exiting silently.
|
||||||
|
|
||||||
|
**Docs**
|
||||||
|
|
||||||
|
- **Documentation Site:**
|
||||||
|
- Published stable docs at the site root with current development docs under `/main/`
|
||||||
|
- Fixed versioned docs navigation, archived page link handling, and local dev version routing
|
||||||
|
- Documented all previously undocumented config options including `subtitleStyle.primaryDefaultMode`, `stats.markWatchedKey`, `immersionTracking.lifetimeSummaries.*`, and all seven `mpv.*` launcher options
|
||||||
|
- Added Playback Startup Flow and Runtime Sockets diagrams to the architecture docs with cross-reference pointers in the MPV Plugin and Troubleshooting pages
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Internal changes</summary>
|
||||||
|
|
||||||
|
**Internal**
|
||||||
|
|
||||||
|
- **Release Tooling:**
|
||||||
|
- Release-note polishing treats pending fragments and reviewed prerelease notes as a cumulative final outcome, collapsing prerelease-only fixes into the final user-facing change
|
||||||
|
- Prerelease generation reuses existing reviewed notes and merges only new fragment material
|
||||||
|
- `make clean` preserves `release/prerelease-notes.md`
|
||||||
|
- **Tests:** Removed stale Yomitan vendor source-inspection assertions for changes that were not shipped.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>v0.14.x</summary>
|
||||||
|
|
||||||
|
<h2>v0.14.0 (2026-05-12)</h2>
|
||||||
|
|
||||||
|
**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.
|
||||||
- **Primary Subtitle Bar:** Added a `V` shortcut and mpv plugin binding to toggle the primary subtitle bar without affecting mpv's native subtitle visibility.
|
- **Primary Subtitle Bar:** Added a `V` shortcut and mpv plugin binding to toggle the primary subtitle bar without affecting mpv's native subtitle visibility.
|
||||||
- **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
|
- **Texthooker:** Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
|
||||||
|
|
||||||
@@ -60,11 +691,11 @@ SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin
|
|||||||
**Internal**
|
**Internal**
|
||||||
|
|
||||||
- Replaced the changelog renderer with an AI polish pass that merges related fragments and writes user-facing release notes. `CHANGELOG.md` keeps internal items in a collapsed `<details>` block; GitHub release notes omit them entirely.
|
- Replaced the changelog renderer with an AI polish pass that merges related fragments and writes user-facing release notes. `CHANGELOG.md` keeps internal items in a collapsed `<details>` block; GitHub release notes omit them entirely.
|
||||||
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain — run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
|
- Release CI no longer auto-builds pending `changes/*.md` fragments on tag. Tagging now fails fast if fragments remain - run `bun run changelog:build` (requires the `claude` CLI) and commit before tagging.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Previous Versions
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>v0.12.x</summary>
|
<summary>v0.12.x</summary>
|
||||||
@@ -84,8 +715,8 @@ SubMiner no longer requires a globally-installed mpv plugin. The bundled plugin
|
|||||||
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity.
|
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity.
|
||||||
- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility.
|
- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility.
|
||||||
- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard.
|
- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard.
|
||||||
- Stats: Session timeline no longer plots seek-forward/seek-backward markers — they were too noisy on sessions with lots of rewinds.
|
- Stats: Session timeline no longer plots seek-forward/seek-backward markers - they were too noisy on sessions with lots of rewinds.
|
||||||
- Stats: Replaced the "Library — Per Day" section on the Stats → Trends page with a "Library — Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
|
- Stats: Replaced the "Library - Per Day" section on the Stats → Trends page with a "Library - Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
|
||||||
|
|
||||||
**Fixed**
|
**Fixed**
|
||||||
|
|
||||||
|
|||||||
@@ -1,263 +1,104 @@
|
|||||||
# 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 builds a Yomitan dictionary of the characters in the show you are watching, using data from [AniList](https://anilist.co). Character names in subtitles get their own color, and hovering one shows the character's portrait, role, voice actor, and description.
|
||||||
|
|
||||||
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.
|
Ordinary dictionaries rarely contain character names, so without this every name counts as an unknown word and throws off [N+1 highlighting](/subtitle-annotations#n-1-word-highlighting).
|
||||||
|
|
||||||
## How It Works
|
## Turning it on
|
||||||
|
|
||||||
The feature has three stages: **snapshot**, **merge**, and **match**.
|
1. Set `subtitleStyle.nameMatchEnabled` to `true`, or turn it on in the Settings window under Annotation Display, Character Names.
|
||||||
|
2. Optionally set `subtitleStyle.nameMatchImagesEnabled` to `true` to show a small portrait next to each name in the subtitle line.
|
||||||
1. **Snapshot** — When you start watching a new title, SubMiner queries the AniList GraphQL API for the media's character list. Each character's names, reading, role, description, birthday, voice actors, and portrait are fetched and saved as a local JSON snapshot in `character-dictionaries/snapshots/anilist-{mediaId}.json`. Images are downloaded and base64-encoded into the snapshot.
|
3. Play an episode.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Enabling the Feature
|
|
||||||
|
|
||||||
Character dictionary sync is disabled by default. To turn it on:
|
|
||||||
|
|
||||||
1. Authenticate with AniList (see [AniList Integration](/anilist-integration#setup)).
|
|
||||||
2. Set `anilist.characterDictionary.enabled` to `true` in your config.
|
|
||||||
3. Start watching — SubMiner will generate a snapshot for the current media and import the merged dictionary into Yomitan automatically.
|
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"anilist": {
|
"subtitleStyle": {
|
||||||
"enabled": true,
|
"nameMatchEnabled": true,
|
||||||
"accessToken": "your-token",
|
"nameMatchImagesEnabled": true,
|
||||||
"characterDictionary": {
|
|
||||||
"enabled": true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
::: tip
|
No AniList account is needed. Logging in to AniList is only for [watch progress sync](/anilist-integration).
|
||||||
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.
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: warning
|
The character dictionary does not work when `yomitan.externalProfilePath` is set, because SubMiner then uses another app's Yomitan profile read-only.
|
||||||
If `yomitan.externalProfilePath` is set, SubMiner switches to read-only external-profile mode. In that mode SubMiner can reuse another app's installed Yomitan dictionaries/settings, but SubMiner's own character-dictionary features are fully disabled.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Name Generation
|
## What happens when you play something
|
||||||
|
|
||||||
A single character produces many searchable terms so that names are recognized regardless of how they appear in dialogue. SubMiner generates variants for:
|
When a new show starts, SubMiner:
|
||||||
|
|
||||||
**Spacing and combination:**
|
1. Guesses the title from the filename and finds it on AniList.
|
||||||
|
2. Downloads the cast list and portraits.
|
||||||
|
3. Builds the dictionary and imports it into SubMiner's Yomitan.
|
||||||
|
|
||||||
- Full name with space: 須々木 心一
|
A notification shows each step. Once it says the dictionary is ready, names match from the next subtitle line.
|
||||||
- Combined form: 須々木心一
|
|
||||||
- Family name alone: 須々木
|
|
||||||
- Given name alone: 心一
|
|
||||||
|
|
||||||
**Middle-dot removal** (common in katakana foreign names):
|
Each character gets entries for the full name, family name, given name, and common honorifics (`さん`, `君`, `ちゃん`, `先生`, and others), so `太郎さん` matches as well as `太郎`.
|
||||||
|
|
||||||
- ア・リ・ス → アリス (combined), plus individual segments
|
SubMiner keeps your most recent shows loaded in one merged dictionary. `anilist.characterDictionary.maxLoaded` sets how many. Starting another show drops the oldest one. Only the current show's characters are highlighted.
|
||||||
|
|
||||||
**Honorific suffixes** — each base name is expanded with 15 common suffixes:
|
### How long it takes
|
||||||
|
|
||||||
| Honorific | Reading |
|
The first time you watch a show, most of the time goes into downloading portraits, one at a time. A typical cast takes seconds to a minute. A very large cast takes much longer: One Piece has over a thousand characters and takes around 10 minutes. After that the show is cached, and later episodes load it from disk.
|
||||||
| --------- | ---------- |
|
|
||||||
| さん | さん |
|
|
||||||
| 様 | さま |
|
|
||||||
| 先生 | せんせい |
|
|
||||||
| 先輩 | せんぱい |
|
|
||||||
| 後輩 | こうはい |
|
|
||||||
| 氏 | し |
|
|
||||||
| 君 | くん |
|
|
||||||
| くん | くん |
|
|
||||||
| ちゃん | ちゃん |
|
|
||||||
| たん | たん |
|
|
||||||
| 坊 | ぼう |
|
|
||||||
| 殿 | どの |
|
|
||||||
| 博士 | はかせ |
|
|
||||||
| 社長 | しゃちょう |
|
|
||||||
| 部長 | ぶちょう |
|
|
||||||
|
|
||||||
**Romanized names** — names stored in romaji on AniList are converted to kana aliases so they can match against Japanese subtitle text.
|
## Correcting AniList matches
|
||||||
|
|
||||||
This means a character like "太郎" generates entries for 太郎, 太郎さん, 太郎先生, 太郎君, 太郎ちゃん, and so on — all with correct readings.
|
SubMiner can match the wrong show when a filename is ambiguous, for example `Re - ZERO, Starting Life in Another World (2016)` matching a different `Re...` series. To fix it:
|
||||||
|
|
||||||
## Name Matching
|
1. Press `Ctrl/Cmd+D` to open the character dictionary manager.
|
||||||
|
2. Click **Override**, edit the title if needed, search, and pick the right result.
|
||||||
|
|
||||||
Name matching runs inside Yomitan's scanning pipeline during subtitle tokenization.
|
From the command line:
|
||||||
|
|
||||||
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'`.
|
|
||||||
3. Matched tokens are flagged `isNameMatch: true` and forwarded to the renderer.
|
|
||||||
4. The renderer applies the name-match highlight color (default: `#f5bde6`).
|
|
||||||
|
|
||||||
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:**
|
|
||||||
|
|
||||||
| Option | Default | Description |
|
|
||||||
| -------------------------------- | --------- | ---------------------------------- |
|
|
||||||
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting |
|
|
||||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for matched names |
|
|
||||||
|
|
||||||
## Dictionary Entries
|
|
||||||
|
|
||||||
Each character entry in the Yomitan dictionary includes structured content:
|
|
||||||
|
|
||||||
- **Name** — native (Japanese) and romanized forms
|
|
||||||
- **Role badge** — color-coded by role: main (score 100), supporting (90), side (80), background (70)
|
|
||||||
- **Portrait** — character image from AniList, embedded in the ZIP
|
|
||||||
- **Description** — biography text from AniList (collapsible)
|
|
||||||
- **Character information** — age, birthday, gender, blood type (collapsible)
|
|
||||||
- **Voiced by** — voice actor name and portrait (collapsible)
|
|
||||||
|
|
||||||
The three collapsible sections can be configured to start open or closed:
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"anilist": {
|
|
||||||
"characterDictionary": {
|
|
||||||
"collapsibleSections": {
|
|
||||||
"description": false,
|
|
||||||
"characterInformation": false,
|
|
||||||
"voicedBy": false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Auto-Sync Lifecycle
|
|
||||||
|
|
||||||
When `characterDictionary.enabled` is `true`, SubMiner runs an auto-sync routine whenever the active media changes.
|
|
||||||
|
|
||||||
**Phases:**
|
|
||||||
|
|
||||||
1. **checking** — Is there already a cached snapshot for this media ID?
|
|
||||||
2. **generating** — No cache hit: fetch characters from AniList GraphQL, download portraits (250ms throttle between image requests), save snapshot JSON.
|
|
||||||
3. **syncing** — Add the media ID to the most-recently-used list. Evict old entries beyond `maxLoaded`.
|
|
||||||
4. **building** — Merge active snapshots into a single Yomitan ZIP. A SHA-1 revision hash is computed from the media set — if it matches the previously imported revision, the import is skipped.
|
|
||||||
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.
|
|
||||||
|
|
||||||
**State tracking** is persisted in `character-dictionaries/auto-sync-state.json`:
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{
|
|
||||||
"activeMediaIds": [170942, 163134, 154587],
|
|
||||||
"mergedRevision": "a1b2c3d4e5f6",
|
|
||||||
"mergedDictionaryTitle": "SubMiner Character Dictionary",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `maxLoaded` setting (default: 3) controls how many media snapshots stay in the active set. When you start a 4th title, the oldest is evicted and the merged dictionary is rebuilt without it.
|
|
||||||
|
|
||||||
## Manual Generation
|
|
||||||
|
|
||||||
You can generate a character dictionary from the command line without auto-sync:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate for a file or directory
|
# List AniList matches for a file
|
||||||
subminer dictionary /path/to/media
|
|
||||||
|
|
||||||
# Generate for current anime (AppImage)
|
|
||||||
SubMiner.AppImage --dictionary
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates a standalone dictionary ZIP for the target media and saves it alongside the snapshots.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
Use the in-app selector or CLI to pin the correct AniList media for the whole series:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List candidate AniList matches for a file
|
|
||||||
subminer dictionary --candidates "/path/to/episode.mkv"
|
subminer dictionary --candidates "/path/to/episode.mkv"
|
||||||
|
|
||||||
# Save the correct AniList media ID for that series
|
# Save the correct AniList ID for that series
|
||||||
subminer dictionary --select 21355 "/path/to/episode.mkv"
|
subminer dictionary --select 21355 "/path/to/episode.mkv"
|
||||||
|
|
||||||
# Equivalent direct app flags
|
|
||||||
SubMiner.AppImage --dictionary-candidates --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
|
|
||||||
subminer app --open-character-dictionary
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
The override applies to every episode of that season in the same folder. Other seasons are not affected, even when they share the folder. The override also sets which entry [AniList watch progress](/anilist-integration) updates, so one fix covers both.
|
||||||
|
|
||||||
## File Structure
|
## Managing loaded shows
|
||||||
|
|
||||||
All character dictionary data lives under `{userData}/character-dictionaries/`:
|
The manager (`Ctrl/Cmd+D`) lists the shows in the merged dictionary and marks the current one.
|
||||||
|
|
||||||
```text
|
- **Remove** drops a show from the dictionary. You cannot remove the show you are watching.
|
||||||
character-dictionaries/
|
- **Up/Down** changes which show gets dropped first when a new one is added.
|
||||||
snapshots/
|
- **Override** replaces a show's AniList match.
|
||||||
anilist-170942.json # Per-media character snapshot
|
|
||||||
anilist-163134.json
|
## Generating from the command line
|
||||||
merged.zip # Active merged dictionary (imported into Yomitan)
|
|
||||||
auto-sync-state.json # Tracks active media IDs and revision
|
```bash
|
||||||
anilist-overrides.json # Manual series-to-AniList overrides
|
subminer dictionary /path/to/media
|
||||||
img/
|
|
||||||
m170942-c12345.jpg # Character 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.
|
This builds a standalone dictionary file for that file or folder without playing it. With the AppImage directly, use `SubMiner.AppImage --dictionary`.
|
||||||
|
|
||||||
**ZIP structure** follows the Yomitan dictionary format:
|
## Configuration
|
||||||
|
|
||||||
```text
|
Defaults are in the [configuration reference](/configuration).
|
||||||
merged.zip
|
|
||||||
index.json # { title, revision, format: 3, author: "SubMiner" }
|
|
||||||
tag_bank_1.json # Tag definitions
|
|
||||||
term_bank_1.json # Up to 10,000 terms per bank
|
|
||||||
term_bank_2.json
|
|
||||||
img/ # Embedded character and VA portraits
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Reference
|
| Key | What it does |
|
||||||
|
| ---------------------------------------------------------------------- | ------------------------------------------------ |
|
||||||
| Option | Default | Description |
|
| `subtitleStyle.nameMatchEnabled` | Build the dictionary and color character names |
|
||||||
| ---------------------------------------------------------------------- | --------- | --------------------------------------------------------------- |
|
| `subtitleStyle.nameMatchImagesEnabled` | Show a portrait next to matched names |
|
||||||
| `anilist.characterDictionary.enabled` | `false` | Enable auto-sync of character dictionary from AniList |
|
| `subtitleStyle.nameMatchColor` | Color for character names |
|
||||||
| `anilist.characterDictionary.maxLoaded` | `3` | Number of recent media snapshots kept in the merged dictionary |
|
| `anilist.characterDictionary.maxLoaded` | Number of recent shows kept in the dictionary |
|
||||||
| `anilist.characterDictionary.profileScope` | `"all"` | Apply dictionary to `"all"` Yomitan profiles or `"active"` only |
|
| `anilist.characterDictionary.collapsibleSections.description` | Show the description expanded in the popup |
|
||||||
| `anilist.characterDictionary.collapsibleSections.description` | `false` | Start Description section expanded |
|
| `anilist.characterDictionary.collapsibleSections.characterInformation` | Show age, birthday, and similar details expanded |
|
||||||
| `anilist.characterDictionary.collapsibleSections.characterInformation` | `false` | Start Character Information section expanded |
|
| `anilist.characterDictionary.collapsibleSections.voicedBy` | Show the voice actor section expanded |
|
||||||
| `anilist.characterDictionary.collapsibleSections.voicedBy` | `false` | Start Voiced By section expanded |
|
| `shortcuts.openCharacterDictionaryManager` | Shortcut for the manager |
|
||||||
| `subtitleStyle.nameMatchEnabled` | `true` | Toggle character-name highlighting in subtitles |
|
|
||||||
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Highlight color for character-name matches |
|
|
||||||
|
|
||||||
## Reference Implementation
|
|
||||||
|
|
||||||
SubMiner's character dictionary builder is inspired by the [Japanese Character Name Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) project — a standalone Rust web service that generates Yomitan character dictionaries from AniList and VNDB data.
|
|
||||||
|
|
||||||
The reference implementation covers similar ground — name variant generation, honorific expansion, structured Yomitan content, portrait embedding — and additionally supports VNDB as a data source for visual novel characters. Key differences:
|
|
||||||
|
|
||||||
| | SubMiner | Reference Implementation |
|
|
||||||
| ---------------------- | -------------------------------------------- | ------------------------------------- |
|
|
||||||
| **Runtime** | TypeScript, runs inside Electron | Rust, standalone web service |
|
|
||||||
| **Data sources** | AniList only | AniList + VNDB |
|
|
||||||
| **Delivery** | Auto-synced into bundled Yomitan | ZIP download via web UI |
|
|
||||||
| **Honorific strategy** | Eager generation at build time | Lazy generation during ZIP export |
|
|
||||||
| **Caching** | File-based snapshots | Multi-tier (memory + disk + SQLite) |
|
|
||||||
| **Updates** | Revision-hashed; skips reimport if unchanged | URL-encoded settings for auto-refresh |
|
|
||||||
|
|
||||||
If you work with visual novels or want a standalone dictionary generator independent of SubMiner, the reference implementation is worth checking out.
|
|
||||||
|
|
||||||
## 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.
|
**It seems stuck.** Check the notification. While generating it shows counts (`image 120/400`), an estimate of time left, and an elapsed clock. If the clock moves, it is still working, and large casts are slow (see [how long it takes](#how-long-it-takes)). If you missed the notification, open the notification history with `Ctrl/Cmd+N`. For errors, check the app log ([log locations](/troubleshooting)).
|
||||||
- **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`.
|
|
||||||
- **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.
|
|
||||||
|
|
||||||
## Related
|
**Import failed or timed out.** Yomitan may have been busy importing another dictionary. Play the next episode or restart SubMiner. The import may have finished anyway, so check for the character popup before retrying.
|
||||||
|
|
||||||
- [Subtitle Annotations](/subtitle-annotations) — how name matches interact with N+1, frequency, and JLPT layers
|
**Names are not highlighted.** Check that `subtitleStyle.nameMatchEnabled` is `true`, that `yomitan.externalProfilePath` is empty, and that the show was found on AniList. The wrong show's cast means a wrong match. See [correcting AniList matches](#correcting-anilist-matches).
|
||||||
- [AniList Integration](/anilist-integration) — authentication, episode tracking, and AniList settings
|
|
||||||
- [Configuration Reference](/configuration) — full config options
|
**Portraits are missing.** Portraits need AniList to have an image and the download to succeed. If you were offline during the first sync, delete that show's file from `character-dictionaries/snapshots/` in the SubMiner config directory and replay it.
|
||||||
|
|
||||||
|
SubMiner's generator is based on the [Japanese Character Name Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) project, which also supports VNDB and works without SubMiner.
|
||||||
@@ -1,45 +1,46 @@
|
|||||||
# Feature Demos
|
# Feature demos
|
||||||
|
|
||||||
Short recordings of SubMiner's key features and integrations from real playback sessions.
|
Short recordings from real playback sessions.
|
||||||
|
|
||||||
|
_Yomitan_ is the pop-up dictionary. _Jimaku_ is a community subtitle database. _alass_ and _ffsubsync_ retime subtitles against the audio. _Jellyfin_ is a self-hosted media server. A _texthooker_ is a web page that mirrors the current subtitle as selectable text.
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const v = '20260301-1';
|
import { withBase } from 'vitepress';
|
||||||
|
|
||||||
|
const v = '20260819-1';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## Anki Card Mining & Enrichment
|
## Anki card mining and enrichment
|
||||||
|
|
||||||
Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner automatically attaches the sentence, a timing-accurate audio clip, a screenshot, and a translation.
|
Mine a card from Yomitan or straight from a subtitle line. SubMiner attaches the sentence, an audio clip cut to the line timing, and a screenshot.
|
||||||
|
|
||||||
<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="Animated demo of mining a card" style="width: 100%; height: auto;" />
|
||||||
</a>
|
</a>
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
## Subtitle download and sync
|
||||||
:::
|
|
||||||
|
|
||||||
## Subtitle Download & Sync
|
Search Jimaku, download a track, then retime it with alass or ffsubsync without leaving SubMiner.
|
||||||
|
|
||||||
Search and download subtitles from Jimaku, then automatically synchronize them with alass or ffsubsync — all from within SubMiner.
|
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/subtitle-sync-poster.jpg?v=${v}`)">
|
||||||
|
<source :src="withBase(`/assets/demos/subtitle-sync.webm?v=${v}`)" type="video/webm" />
|
||||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/subtitle-sync-poster.jpg?v=${v}`">
|
<source :src="withBase(`/assets/demos/subtitle-sync.mp4?v=${v}`)" type="video/mp4" />
|
||||||
<source :src="`/assets/demos/subtitle-sync.webm?v=${v}`" type="video/webm" />
|
|
||||||
<source :src="`/assets/demos/subtitle-sync.mp4?v=${v}`" type="video/mp4" />
|
|
||||||
</video> -->
|
</video> -->
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
::: info VIDEO COMING SOON
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Jellyfin Integration
|
## Jellyfin integration
|
||||||
|
|
||||||
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 and play from SubMiner, or cast to SubMiner from another Jellyfin client. Watch progress syncs back to the 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
|
||||||
@@ -47,11 +48,11 @@ Browse your Jellyfin library, cast to devices, and launch playback directly from
|
|||||||
|
|
||||||
## Texthooker
|
## Texthooker
|
||||||
|
|
||||||
Open subtitles in an external texthooker page for use with browser-based tools and extensions alongside the overlay.
|
Mirror subtitles to an external texthooker page so browser extensions can read them while the overlay runs.
|
||||||
|
|
||||||
<!-- <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
|
||||||
|
|||||||
@@ -1,54 +1,35 @@
|
|||||||
# Building & Testing
|
# Building and testing
|
||||||
|
|
||||||
For internal architecture/workflow guidance, use `docs/README.md` at the repo root. This page stays focused on contributor-facing build and test commands.
|
Build, run, and test SubMiner from source. Architecture and workflow rules live in the repo's internal docs, starting at [`docs/README.md`](https://github.com/ksyasuda/SubMiner/blob/main/docs/README.md). Module boundaries and layering rules are in [`docs/architecture/README.md`](https://github.com/ksyasuda/SubMiner/blob/main/docs/architecture/README.md). The lane-by-lane test guide is [`docs/workflow/verification.md`](https://github.com/ksyasuda/SubMiner/blob/main/docs/workflow/verification.md).
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- [Bun](https://bun.sh)
|
- [Bun](https://bun.sh), at the version pinned in `package.json`
|
||||||
|
- A system `lua` interpreter for the mpv plugin tests (`bun run test:launcher`, `bun run test:env`)
|
||||||
|
- macOS only: `bun run build` compiles a Swift window helper. Set `SUBMINER_SKIP_MACOS_HELPER_BUILD=1` to skip it.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```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:
|
make deps
|
||||||
git submodule update --init --recursive
|
|
||||||
|
|
||||||
bun install
|
|
||||||
(cd stats && bun install --frozen-lockfile)
|
|
||||||
(cd vendor/texthooker-ui && bun install --frozen-lockfile)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`make deps` is still available as a convenience wrapper around the same dependency install flow.
|
`make deps` initializes submodules and installs dependencies for the root, `stats/`, and `vendor/texthooker-ui`. The Yomitan submodule installs its own dependencies during `bun run build`.
|
||||||
|
|
||||||
## Building
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Main app build
|
bun run build # app build, including bundled Yomitan from vendor/subminer-yomitan
|
||||||
bun run build
|
|
||||||
|
|
||||||
# Platform packages
|
|
||||||
bun run build:appimage # Linux AppImage
|
bun run build:appimage # Linux AppImage
|
||||||
bun run build:mac # macOS DMG + ZIP (signed)
|
bun run build:mac # macOS DMG + ZIP (signed)
|
||||||
bun run build:mac:unsigned # macOS DMG + ZIP (unsigned)
|
bun run build:mac:unsigned # macOS DMG + ZIP (unsigned)
|
||||||
bun run build:win # Windows NSIS installer + ZIP
|
bun run build:win # Windows NSIS installer + ZIP
|
||||||
|
make build-launcher # launcher only, output: dist/launcher/subminer
|
||||||
# Optional launcher artifact only
|
|
||||||
make build-launcher
|
|
||||||
# output: dist/launcher/subminer
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`bun run build` includes the Yomitan build step. It builds the bundled Chrome extension directly from the `vendor/subminer-yomitan` submodule into `build/yomitan` using Bun.
|
The launcher source is `launcher/*.ts`. `dist/launcher/subminer` is generated, so never edit it by hand. The repo-root `./subminer` is a stale path and verification rejects it. `make install-linux` and `make install-macos` copy from `dist/launcher/subminer`. To check the launcher build:
|
||||||
|
|
||||||
## Launcher Artifact Workflow
|
|
||||||
|
|
||||||
- Source of truth: `launcher/*.ts`
|
|
||||||
- Generated output: `dist/launcher/subminer`
|
|
||||||
- Do not hand-edit generated launcher output.
|
|
||||||
- Repo-root `./subminer` is a stale artifact path and is rejected by verification checks.
|
|
||||||
- Install targets (`make install-linux`, `make install-macos`) copy from `dist/launcher/subminer`.
|
|
||||||
|
|
||||||
Verify the workflow:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make build-launcher
|
make build-launcher
|
||||||
@@ -56,47 +37,29 @@ dist/launcher/subminer --help >/dev/null
|
|||||||
bash scripts/verify-generated-launcher.sh
|
bash scripts/verify-generated-launcher.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running Locally
|
## Run locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run dev # builds + launches with --start --dev
|
bun run dev # build, then launch with --start --dev
|
||||||
electron . --start --dev --log-level debug # equivalent Electron launch with verbose logging
|
make dev-watch # watch TS + renderer and relaunch Electron
|
||||||
electron . --background # tray/background mode, minimal default logging
|
make dev-watch-macos # same, forcing --backend macos
|
||||||
make dev-start # build + launch via Makefile
|
electron . --start --dev --log-level debug # verbose launch of an existing build
|
||||||
make dev-watch # watch TS + renderer and launch Electron (faster edit loop)
|
electron . --background # tray/background mode
|
||||||
make dev-watch-macos # same as dev-watch, forcing --backend macos
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For mpv-plugin-driven testing without exporting `SUBMINER_BINARY_PATH` each run, set a one-time
|
To test through the mpv plugin without exporting `SUBMINER_BINARY_PATH` each time, point `mpv.subminerBinaryPath` in your config at the dev script. The launcher passes it to the plugin at runtime:
|
||||||
dev binary path in `~/.config/mpv/script-opts/subminer.conf`:
|
|
||||||
|
|
||||||
```ini
|
```json
|
||||||
binary_path=/absolute/path/to/SubMiner/scripts/subminer-dev.sh
|
{
|
||||||
|
"mpv": {
|
||||||
|
"subminerBinaryPath": "/absolute/path/to/SubMiner/scripts/subminer-dev.sh"
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Test
|
||||||
|
|
||||||
Default lanes:
|
Run the handoff gate before submitting substantial changes:
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run test # alias for test:fast
|
|
||||||
bun run test:fast # default fast lane
|
|
||||||
bun run test:full # maintained source + launcher-unit + runtime compat surface
|
|
||||||
bun run test:runtime:compat # compiled/runtime compatibility slice only
|
|
||||||
bun run test:env # launcher/plugin + env-sensitive verification
|
|
||||||
bun run test:immersion:sqlite # SQLite persistence lane
|
|
||||||
bun run test:subtitle # maintained alass/ffsubsync subtitle surface
|
|
||||||
```
|
|
||||||
|
|
||||||
- `bun run test` and `bun run test:fast` cover config/core suites plus representative entry/runtime, Anki integration, release-workflow coverage, typecheck, and runtime-registry checks.
|
|
||||||
- `bun run test:full` is the maintained full surface: Bun-compatible `src/**` discovery, Bun-compatible launcher unit discovery, and the compiled/runtime compatibility lane for suites routed through `dist/**`.
|
|
||||||
- `bun run test:runtime:compat` covers the compiled/runtime slice directly: `ipc`, `anki-jimaku-ipc`, `overlay-manager`, `config-validation`, `startup-config`, and `registry`.
|
|
||||||
- `bun run test:env` covers environment-sensitive checks: launcher smoke/plugin verification plus the Bun source SQLite lane.
|
|
||||||
- `bun run test:immersion:sqlite` is the reproducible persistence lane when you need real DB-backed SQLite coverage under Bun.
|
|
||||||
|
|
||||||
The Bun-managed discovery lanes intentionally exclude a small compiled/runtime-focused set: `src/core/services/ipc.test.ts`, `src/core/services/anki-jimaku-ipc.test.ts`, `src/core/services/overlay-manager.test.ts`, `src/main/config-validation.test.ts`, `src/main/runtime/startup-config.test.ts`, and `src/main/runtime/registry.test.ts`. `bun run test:runtime:compat` keeps them in the standard workflow via `dist/**`.
|
|
||||||
|
|
||||||
Suggested local gate before handoff:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run typecheck
|
bun run typecheck
|
||||||
@@ -106,133 +69,111 @@ bun run build
|
|||||||
bun run test:smoke:dist
|
bun run test:smoke:dist
|
||||||
```
|
```
|
||||||
|
|
||||||
If you changed docs in `docs-site/`, also run:
|
For smaller changes, start with the cheapest lane that covers what you touched:
|
||||||
|
|
||||||
|
| Command | Covers |
|
||||||
|
| ------------------------------- | ------------------------------------------------------------------------ |
|
||||||
|
| `bun run test` / `test:fast` | All `src/**` tests, launcher unit tests, and `scripts/**` tests |
|
||||||
|
| `bun run test:config` | Config schema, defaults, and `config.example.jsonc` generation |
|
||||||
|
| `bun run test:launcher` | Launcher tests plus the Lua plugin tests |
|
||||||
|
| `bun run test:env` | Launcher e2e smoke, Lua plugin tests, SQLite immersion tests from source |
|
||||||
|
| `bun run test:scripts` | Build and release scripts under `scripts/**` |
|
||||||
|
| `bun run test:stats` | Stats dashboard UI under `stats/src/**` |
|
||||||
|
| `bun run test:runtime:compat` | Compiled-runtime smoke against `dist/` (run `bun run build` first) |
|
||||||
|
| `bun run test:immersion:sqlite` | Compiles, then runs the SQLite-backed immersion tracker tests |
|
||||||
|
| `bun run test:subtitle` | alass/ffsubsync subtitle sync |
|
||||||
|
| `bun run test:docs:kb` | Internal docs, `AGENTS.md`, and repo skills |
|
||||||
|
|
||||||
|
Lane membership is defined in `scripts/test-lanes.ts` and discovered by directory, so a new test file joins its lane automatically. Do not hand-list test files in `package.json`. `scripts/run-test-lane.mjs` runs each file in its own `bun test` process, so a hanging test cannot take down the rest of the lane. Pass `--jobs N` to parallelize or `--single-process` to share one process while debugging.
|
||||||
|
|
||||||
|
Launcher smoke artifacts go to `.tmp/launcher-smoke`. CI uploads them when the smoke step fails.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run docs:test
|
make pretty # format the maintained source and stats files
|
||||||
bun run docs:build
|
bun run format:check:src # check the same set without writing
|
||||||
```
|
```
|
||||||
|
|
||||||
Focused commands:
|
`bun run format` runs Prettier over the whole repo. Use it only when you mean to.
|
||||||
|
|
||||||
|
## Config generation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run test:config # Source-level config schema/validation tests
|
bun run electron . --generate-config # write a default config to ~/.config/SubMiner/config.jsonc (%APPDATA%\SubMiner\config.jsonc on Windows)
|
||||||
bun run test:launcher # Launcher regression tests (config discovery + command routing)
|
bun run generate:config-example # regenerate config.example.jsonc from the defaults
|
||||||
bun run test:core # Source-level core regression tests (default lane)
|
|
||||||
bun run test:launcher:smoke:src # Launcher e2e smoke: launcher -> mpv IPC -> overlay start/stop wiring
|
|
||||||
bun run test:launcher:env:src # Launcher smoke + Lua plugin gate
|
|
||||||
bun run test:src # Bun-managed maintained src/** discovery lane
|
|
||||||
bun run test:launcher:unit:src # Bun-managed maintained launcher unit lane
|
|
||||||
bun run test:immersion:sqlite:src # Bun source lane
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Dist-level tests are now an explicit smoke lane used to validate compiled/runtime assumptions.
|
`make generate-config` and `make generate-example-config` wrap the same commands.
|
||||||
|
|
||||||
Launcher smoke artifacts are written to `.tmp/launcher-smoke` locally and uploaded by CI/release workflows when the smoke step fails.
|
Config definitions are split by domain under `src/config/definitions/`:
|
||||||
|
|
||||||
Smoke and optional deep dist commands:
|
- defaults: `defaults-*.ts`
|
||||||
|
- option metadata: `options-*.ts`
|
||||||
|
- generated template sections and comments: `template-sections.ts`
|
||||||
|
|
||||||
```bash
|
`src/config/definitions.ts` composes them into the public API (`DEFAULT_CONFIG`, registries, template export). A new key also needs a resolver entry under `src/config/resolve/`, or the resolved config keeps the default.
|
||||||
bun run build # compile dist artifacts
|
|
||||||
bun run test:immersion:sqlite # compile + run SQLite-backed immersion tests under Bun
|
|
||||||
bun run test:smoke:dist # explicit smoke scope for compiled runtime
|
|
||||||
bun run test:config:dist # optional full dist config suite
|
|
||||||
bun run test:core:dist # optional full dist core suite
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `bun run test:immersion:sqlite` when you need real DB-backed coverage for the immersion tracker.
|
## Documentation site
|
||||||
|
|
||||||
## Formatting
|
The user docs live in `docs-site/` (VitePress).
|
||||||
|
|
||||||
Use the scoped formatter for normal app-repo work:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make pretty
|
|
||||||
bun run format:check: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` remains the broad repo-wide Prettier command; use it intentionally.
|
|
||||||
## Config Generation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate default config to ~/.config/SubMiner/config.jsonc (or %APPDATA%\SubMiner\config.jsonc on Windows)
|
|
||||||
bun run electron . --generate-config
|
|
||||||
|
|
||||||
# Regenerate the repo's config.example.jsonc from centralized defaults
|
|
||||||
bun run generate:config-example
|
|
||||||
```
|
|
||||||
|
|
||||||
Convenience wrappers still exist:
|
|
||||||
|
|
||||||
- `make generate-config`
|
|
||||||
- `make generate-example-config`
|
|
||||||
|
|
||||||
## Documentation Site
|
|
||||||
|
|
||||||
The docs site now lives in `docs-site/` inside the main repo.
|
|
||||||
|
|
||||||
From the SubMiner app repo:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun --cwd docs-site install
|
bun --cwd docs-site install
|
||||||
bun run docs:dev # Dev server at http://localhost:5173
|
bun run docs:dev # dev server at http://localhost:5173
|
||||||
bun run docs:build # Production build into docs-site/.vitepress/dist
|
bun run docs:test # docs regression tests (links, pinned strings)
|
||||||
bun run docs:preview # Preview built site at http://localhost:4173
|
bun run docs:build # production build into docs-site/.vitepress/dist
|
||||||
bun run docs:test # Docs regression tests
|
bun run docs:preview # preview the build at http://localhost:4173
|
||||||
```
|
```
|
||||||
|
|
||||||
Cloudflare Pages deploy settings:
|
Run `bun run docs:test` and `bun run docs:build` whenever you change `docs-site/`.
|
||||||
|
|
||||||
- Git repo: `ksyasuda/SubMiner`
|
Production uses the versioned build:
|
||||||
- Root directory: `docs-site`
|
|
||||||
- Build command: `bun run docs:build`
|
|
||||||
- Build output directory: `.vitepress/dist`
|
|
||||||
- Build watch paths: `docs-site/*`
|
|
||||||
|
|
||||||
Use Cloudflare's single `*` wildcard syntax for watch paths. `docs-site/*` covers nested docs-site changes in the repo; `docs-site/**` is not the correct Pages pattern and may skip docs-only pushes.
|
```bash
|
||||||
|
bun run docs:build:versioned
|
||||||
|
```
|
||||||
|
|
||||||
## Makefile Reference
|
It writes `.tmp/docs-versioned-site`: the latest stable docs at `/` with a generated `/versions` page, and development docs at `/main/`. Prerelease tags are skipped. Stable archives under `/v/<version>/` are built once and stored in R2. Without R2 credentials the build skips archive sync, so a local run only produces `/` and `/main/`.
|
||||||
|
|
||||||
Run `make help` for a full list of targets. Key ones:
|
The `docs-pages` GitHub Actions workflow uploads that output to Cloudflare Pages with Wrangler. Cloudflare's Git-integration builds are disabled on purpose, so do not re-enable them in the dashboard. `docs-site/README.md` has the full deployment setup.
|
||||||
|
|
||||||
| Target | Description |
|
## Makefile targets
|
||||||
| ---------------------- | ---------------------------------------------------------------- |
|
|
||||||
| `make build` | Build platform package for detected OS |
|
|
||||||
| `make build-launcher` | Generate Bun launcher wrapper at `dist/launcher/subminer` |
|
|
||||||
| `make install` | Install platform artifacts (wrapper, theme, AppImage/app bundle) |
|
|
||||||
| `make deps` | Install JS dependencies (root + stats + texthooker-ui) |
|
|
||||||
| `make pretty` | Run scoped Prettier formatting for maintained source/config files |
|
|
||||||
| `make generate-config` | Generate default config from centralized registry |
|
|
||||||
| `make build-linux` | Convenience wrapper for Linux packaging |
|
|
||||||
| `make build-macos` | Convenience wrapper for signed macOS packaging |
|
|
||||||
| `make build-macos-unsigned` | Convenience wrapper for unsigned macOS packaging |
|
|
||||||
|
|
||||||
## Contributor Notes
|
Run `make help` for the full list.
|
||||||
|
|
||||||
- To add/change a config default, edit the matching domain file in `src/config/definitions/defaults-*.ts`.
|
| Target | Description |
|
||||||
- To add/change config option metadata, edit the matching domain file in `src/config/definitions/options-*.ts`.
|
| --------------------------- | ------------------------------------------------------------ |
|
||||||
- To add/change generated config template blocks/comments, update `src/config/definitions/template-sections.ts`.
|
| `make deps` | Init submodules and install root, stats, and texthooker deps |
|
||||||
- Keep `src/config/definitions.ts` as the composed public API (`DEFAULT_CONFIG`, registries, template export) that wires domain modules together.
|
| `make build` | Build the platform package for the current OS |
|
||||||
- Overlay window/visibility state is owned by `src/core/services/overlay-manager.ts`.
|
| `make build-linux` | Build the Linux package |
|
||||||
- Runtime architecture/module-boundary conventions are summarized in [Architecture](/architecture), with canonical internal guidance in `docs/architecture/README.md` at the repo root.
|
| `make build-macos` | Build the signed macOS package |
|
||||||
- Linux packaged desktop launches pass `--background` using electron-builder `build.linux.executableArgs` in `package.json`.
|
| `make build-macos-unsigned` | Build the unsigned macOS package |
|
||||||
- Prefer direct inline deps objects in `src/main/` modules for simple pass-through wiring.
|
| `make build-launcher` | Generate the launcher in `dist/launcher/` |
|
||||||
- Add a helper/adapter service only when it performs meaningful adaptation, validation, or reuse (not identity mapping).
|
| `make install` | Install platform artifacts (wrapper, theme, AppImage or app) |
|
||||||
|
| `make pretty` | Run scoped Prettier formatting |
|
||||||
|
| `make generate-config` | Generate a default config |
|
||||||
|
|
||||||
## Environment Variables
|
## Contributor notes
|
||||||
|
|
||||||
| Variable | Description |
|
- See [Architecture](/architecture) for module boundaries and [IPC + runtime contracts](/ipc-contracts) before adding IPC channels.
|
||||||
| ---------------------------------- | ------------------------------------------------------------------------------ |
|
- `src/core/services/overlay-manager.ts` owns overlay window and visibility state.
|
||||||
| `SUBMINER_APPIMAGE_PATH` | Override SubMiner app binary path for launcher playback commands |
|
- In `src/main/` modules, pass simple dependencies as inline objects. Add a helper or adapter only when it adapts, validates, or gets reused.
|
||||||
| `SUBMINER_BINARY_PATH` | Alias for `SUBMINER_APPIMAGE_PATH` |
|
- Packaged Linux desktop launches pass `--background` through `build.linux.executableArgs` in `package.json`.
|
||||||
| `SUBMINER_ROFI_THEME` | Override rofi theme path for launcher picker |
|
|
||||||
| `SUBMINER_LOG_LEVEL` | Override app logger level (`debug`, `info`, `warn`, `error`) |
|
## Environment variables
|
||||||
| `SUBMINER_MPV_LOG` | Override mpv/app shared log file path |
|
|
||||||
| `SUBMINER_JIMAKU_API_KEY` | Override Jimaku API key for launcher subtitle downloads |
|
| Variable | Description |
|
||||||
| `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command used to resolve Jimaku API key at runtime |
|
| ---------------------------------- | ---------------------------------------------------------------- |
|
||||||
| `SUBMINER_JIMAKU_API_BASE_URL` | Override Jimaku API base URL |
|
| `SUBMINER_APPIMAGE_PATH` | SubMiner app binary the launcher uses for playback |
|
||||||
| `SUBMINER_JELLYFIN_ACCESS_TOKEN` | Override Jellyfin access token (used before stored encrypted session fallback) |
|
| `SUBMINER_BINARY_PATH` | Alias for `SUBMINER_APPIMAGE_PATH` |
|
||||||
| `SUBMINER_JELLYFIN_USER_ID` | Optional Jellyfin user ID override |
|
| `SUBMINER_ROFI_THEME` | rofi theme for the launcher picker |
|
||||||
| `SUBMINER_SKIP_MACOS_HELPER_BUILD` | Set to `1` to skip building the macOS helper binary during `bun run build` |
|
| `SUBMINER_MPV_PLUGIN_PATH` | mpv plugin directory the launcher injects |
|
||||||
|
| `SUBMINER_LOG_LEVEL` | App log level (`debug`, `info`, `warn`, `error`) |
|
||||||
|
| `SUBMINER_MPV_LOG` | Shared mpv/app log file path |
|
||||||
|
| `SUBMINER_JIMAKU_API_KEY` | Jimaku API key for launcher subtitle downloads |
|
||||||
|
| `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command that prints the Jimaku API key |
|
||||||
|
| `SUBMINER_JIMAKU_API_BASE_URL` | Jimaku API base URL |
|
||||||
|
| `SUBMINER_JELLYFIN_ACCESS_TOKEN` | Jellyfin access token, used before the stored encrypted session |
|
||||||
|
| `SUBMINER_JELLYFIN_USER_ID` | Jellyfin user ID |
|
||||||
|
| `SUBMINER_SKIP_MACOS_HELPER_BUILD` | Set to `1` to skip the macOS helper build during `bun run build` |
|
||||||
@@ -13,6 +13,10 @@ const ankiIntegrationContents = readFileSync(
|
|||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
const configurationContents = readFileSync(new URL('./configuration.md', import.meta.url), 'utf8');
|
const configurationContents = readFileSync(new URL('./configuration.md', import.meta.url), 'utf8');
|
||||||
|
const troubleshootingContents = readFileSync(
|
||||||
|
new URL('./troubleshooting.md', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
function extractReleaseHeadings(content: string, count: number): string[] {
|
function extractReleaseHeadings(content: string, count: number): string[] {
|
||||||
return Array.from(content.matchAll(/^## v[^\n]+$/gm))
|
return Array.from(content.matchAll(/^## v[^\n]+$/gm))
|
||||||
@@ -37,13 +41,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');
|
||||||
|
|
||||||
@@ -52,11 +56,50 @@ test('docs reflect current launcher and release surfaces', () => {
|
|||||||
expect(configurationContents).not.toContain('youtubeSubgen": {\n "mode"');
|
expect(configurationContents).not.toContain('youtubeSubgen": {\n "mode"');
|
||||||
expect(configurationContents).not.toContain('youtubeSubgen.primarySubLanguages');
|
expect(configurationContents).not.toContain('youtubeSubgen.primarySubLanguages');
|
||||||
expect(configurationContents).toContain('youtube.primarySubLanguages');
|
expect(configurationContents).toContain('youtube.primarySubLanguages');
|
||||||
expect(configurationContents).toContain('### Shared AI Provider');
|
// The AI provider still exists in src/ai and ankiConnect.ai, but it is not
|
||||||
|
// exposed in the Settings window and is not documented for users. Keep the
|
||||||
|
// user-facing docs free of it so nobody configures a hidden surface.
|
||||||
|
expect(configurationContents).not.toContain('Shared AI Provider');
|
||||||
|
expect(configurationContents).not.toContain('ankiConnect.ai');
|
||||||
|
expect(ankiIntegrationContents).not.toContain('AI Translation');
|
||||||
|
// ankiConnect.fields.translation is a LEGACY_HIDDEN_CONFIG_PATHS key, so it
|
||||||
|
// must not be documented as a current setting.
|
||||||
|
expect(configurationContents).not.toContain('fields.translation');
|
||||||
|
expect(ankiIntegrationContents).not.toContain('SelectionText');
|
||||||
|
// fields.audio holds SubMiner's generated sentence audio; examples should not
|
||||||
|
// point it at the field Yomitan uses for word audio.
|
||||||
|
expect(ankiIntegrationContents).not.toContain('"audio": "ExpressionAudio"');
|
||||||
|
|
||||||
expect(changelogContents).toContain('v0.5.1 (2026-03-09)');
|
expect(changelogContents).toContain('v0.5.1 (2026-03-09)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('docs document config surfaces that are easy to miss when they ship', () => {
|
||||||
|
// Anki maturity-based known-word highlighting (#172) landed in
|
||||||
|
// subtitle-annotations.md but was missing from the config reference.
|
||||||
|
expect(configurationContents).toContain('ankiConnect.knownWords.maturityEnabled');
|
||||||
|
expect(configurationContents).toContain('ankiConnect.knownWords.matureThresholdDays');
|
||||||
|
|
||||||
|
// Every top-level config block should be reachable from the config reference.
|
||||||
|
expect(configurationContents).toContain('### TsukiHime');
|
||||||
|
expect(configurationContents).toContain('tsukihime.maxSearchResults');
|
||||||
|
|
||||||
|
// xz is a hard runtime dependency of the TsukiHime download path.
|
||||||
|
expect(installationContents).toContain('xz');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs state the real secondary-subtitle and Anki field-matching behavior', () => {
|
||||||
|
// secondarySub auto-load is off by default; the config example previously
|
||||||
|
// implied otherwise while youtube-integration.md documented it correctly.
|
||||||
|
expect(configurationContents).toContain('Secondary subtitles do **not** auto-load by default');
|
||||||
|
expect(configurationContents).toContain('default: `false`');
|
||||||
|
|
||||||
|
// Anki field names are matched case-insensitively (src/anki-integration.ts
|
||||||
|
// resolveFieldName: exact match first, then a lowercase comparison).
|
||||||
|
expect(usageContents).not.toContain('exactly (case-sensitive)');
|
||||||
|
expect(troubleshootingContents).not.toContain('exactly (case-sensitive)');
|
||||||
|
expect(ankiIntegrationContents).toContain('case-insensitively');
|
||||||
|
});
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
// Cloudflare Pages Function serving frozen `/v/<version>/` doc archives from R2.
|
||||||
|
// Archives are uploaded by scripts/build-versioned-docs.ts and never ship in the Pages
|
||||||
|
// deployment itself, so they do not count toward the Pages file limit. Requires an R2
|
||||||
|
// binding named DOCS_ARCHIVES on the Pages project (see docs-site/README.md).
|
||||||
|
|
||||||
|
// Minimal slice of the Workers R2 API used here; avoids a workers-types dependency.
|
||||||
|
type R2Range = { offset: number; length?: number } | { suffix: number };
|
||||||
|
|
||||||
|
type R2ObjectMeta = {
|
||||||
|
size: number;
|
||||||
|
httpEtag: string;
|
||||||
|
range?: R2Range;
|
||||||
|
};
|
||||||
|
|
||||||
|
type R2ObjectBody = R2ObjectMeta & { body: ReadableStream };
|
||||||
|
|
||||||
|
type R2GetOptions = { range?: Headers; onlyIf?: Headers };
|
||||||
|
|
||||||
|
export type ArchiveBucket = {
|
||||||
|
get(key: string, options?: R2GetOptions): Promise<R2ObjectMeta | R2ObjectBody | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ArchiveContext = {
|
||||||
|
request: Request;
|
||||||
|
env: { DOCS_ARCHIVES: ArchiveBucket };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArchiveRoute =
|
||||||
|
| { kind: 'redirect'; location: string; status: 301 | 302 }
|
||||||
|
| { kind: 'lookup'; version: string; keys: string[] }
|
||||||
|
| { kind: 'not-found' };
|
||||||
|
|
||||||
|
const CONTENT_TYPES: Record<string, string> = {
|
||||||
|
css: 'text/css; charset=utf-8',
|
||||||
|
gif: 'image/gif',
|
||||||
|
html: 'text/html; charset=utf-8',
|
||||||
|
ico: 'image/x-icon',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
jpg: 'image/jpeg',
|
||||||
|
js: 'text/javascript; charset=utf-8',
|
||||||
|
json: 'application/json; charset=utf-8',
|
||||||
|
jsonc: 'application/json; charset=utf-8',
|
||||||
|
mjs: 'text/javascript; charset=utf-8',
|
||||||
|
mkv: 'video/x-matroska',
|
||||||
|
mp4: 'video/mp4',
|
||||||
|
png: 'image/png',
|
||||||
|
svg: 'image/svg+xml',
|
||||||
|
ttf: 'font/ttf',
|
||||||
|
txt: 'text/plain; charset=utf-8',
|
||||||
|
webm: 'video/webm',
|
||||||
|
webp: 'image/webp',
|
||||||
|
woff: 'font/woff',
|
||||||
|
woff2: 'font/woff2',
|
||||||
|
xml: 'application/xml; charset=utf-8',
|
||||||
|
};
|
||||||
|
|
||||||
|
function extensionOf(path: string): string | null {
|
||||||
|
const name = path.slice(path.lastIndexOf('/') + 1);
|
||||||
|
const dot = name.lastIndexOf('.');
|
||||||
|
return dot > 0 ? name.slice(dot + 1).toLowerCase() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentTypeFor(key: string): string {
|
||||||
|
return CONTENT_TYPES[extensionOf(key) ?? ''] ?? 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps a request path onto candidate R2 keys, mirroring the Pages clean-URL rules the
|
||||||
|
// archives were built for (`cleanUrls: true`).
|
||||||
|
export function resolveArchiveRoute(pathname: string, search = ''): ArchiveRoute {
|
||||||
|
if (pathname === '/v' || pathname === '/v/') {
|
||||||
|
return { kind: 'redirect', location: '/versions', status: 302 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = /^\/v\/(\d+\.\d+\.\d+)(\/.*)?$/.exec(pathname);
|
||||||
|
if (!match) return { kind: 'not-found' };
|
||||||
|
|
||||||
|
const version = match[1]!;
|
||||||
|
const rest = match[2];
|
||||||
|
if (!rest) {
|
||||||
|
return { kind: 'redirect', location: `/v/${version}/${search}`, status: 301 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded: string;
|
||||||
|
try {
|
||||||
|
decoded = decodeURIComponent(rest);
|
||||||
|
} catch {
|
||||||
|
return { kind: 'not-found' };
|
||||||
|
}
|
||||||
|
if (decoded.split('/').some((segment) => segment === '..' || segment === '.')) {
|
||||||
|
return { kind: 'not-found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = `v/${version}`;
|
||||||
|
const path = `${prefix}${decoded}`;
|
||||||
|
const keys = decoded.endsWith('/')
|
||||||
|
? [`${path}index.html`]
|
||||||
|
: extensionOf(decoded)
|
||||||
|
? [path]
|
||||||
|
: [`${path}.html`, `${path}/index.html`];
|
||||||
|
|
||||||
|
return { kind: 'lookup', version, keys };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheControlFor(key: string): string {
|
||||||
|
// VitePress content-hashes everything it emits under assets/.
|
||||||
|
if (/\/assets\//.test(key) && extensionOf(key) !== 'html') {
|
||||||
|
return 'public, max-age=31536000, immutable';
|
||||||
|
}
|
||||||
|
return 'public, max-age=3600';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBody(object: R2ObjectMeta | R2ObjectBody): object is R2ObjectBody {
|
||||||
|
return 'body' in object && object.body !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentRange(range: R2Range, size: number): { start: number; end: number } {
|
||||||
|
if ('suffix' in range) {
|
||||||
|
const length = Math.min(range.suffix, size);
|
||||||
|
return { start: size - length, end: size - 1 };
|
||||||
|
}
|
||||||
|
const length = range.length ?? size - range.offset;
|
||||||
|
return { start: range.offset, end: range.offset + length - 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function respondWithObject(options: {
|
||||||
|
request: Request;
|
||||||
|
bucket: ArchiveBucket;
|
||||||
|
key: string;
|
||||||
|
status: number;
|
||||||
|
}): Promise<Response | null> {
|
||||||
|
const { request, bucket, key } = options;
|
||||||
|
// Ranges and conditional requests only make sense for the page that was asked for,
|
||||||
|
// not the 404 fallback.
|
||||||
|
const isRequestedObject = options.status === 200;
|
||||||
|
const wantsRange = isRequestedObject && request.headers.has('range');
|
||||||
|
|
||||||
|
let object: R2ObjectMeta | R2ObjectBody | null;
|
||||||
|
try {
|
||||||
|
object = await bucket.get(key, {
|
||||||
|
range: wantsRange ? request.headers : undefined,
|
||||||
|
onlyIf: isRequestedObject ? request.headers : undefined,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// R2 rejects unsatisfiable ranges.
|
||||||
|
return new Response(null, { status: 416 });
|
||||||
|
}
|
||||||
|
if (!object) return null;
|
||||||
|
|
||||||
|
const headers = new Headers({
|
||||||
|
'Content-Type': contentTypeFor(key),
|
||||||
|
'Cache-Control': cacheControlFor(key),
|
||||||
|
ETag: object.httpEtag,
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'X-Robots-Tag': 'noindex, follow',
|
||||||
|
});
|
||||||
|
|
||||||
|
// R2 returns the object without a body when an If-None-Match/If-Modified-Since
|
||||||
|
// precondition matched.
|
||||||
|
if (!hasBody(object)) {
|
||||||
|
return new Response(null, { status: 304, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = options.status;
|
||||||
|
if (wantsRange && object.range) {
|
||||||
|
const { start, end } = contentRange(object.range, object.size);
|
||||||
|
status = 206;
|
||||||
|
headers.set('Content-Range', `bytes ${start}-${end}/${object.size}`);
|
||||||
|
headers.set('Content-Length', String(end - start + 1));
|
||||||
|
} else {
|
||||||
|
headers.set('Content-Length', String(object.size));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(request.method === 'HEAD' ? null : object.body, { status, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function onRequest({ request, env }: ArchiveContext): Promise<Response> {
|
||||||
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||||
|
return new Response('Method Not Allowed', { status: 405, headers: { Allow: 'GET, HEAD' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const route = resolveArchiveRoute(url.pathname, url.search);
|
||||||
|
|
||||||
|
if (route.kind === 'redirect') {
|
||||||
|
return Response.redirect(new URL(route.location, url).toString(), route.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.kind === 'lookup') {
|
||||||
|
for (const key of route.keys) {
|
||||||
|
const response = await respondWithObject({
|
||||||
|
request,
|
||||||
|
bucket: env.DOCS_ARCHIVES,
|
||||||
|
key,
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
if (response) return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notFoundPage = await respondWithObject({
|
||||||
|
request,
|
||||||
|
bucket: env.DOCS_ARCHIVES,
|
||||||
|
key: `v/${route.version}/404.html`,
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
if (notFoundPage) return notFoundPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Not Found', {
|
||||||
|
status: 404,
|
||||||
|
headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Robots-Tag': 'noindex, follow' },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,304 +1,125 @@
|
|||||||
# Immersion Tracking
|
# Immersion tracking
|
||||||
|
|
||||||
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 records what you watch and mine in a local SQLite database and shows it in a stats dashboard. Tracking is on by default. Nothing leaves your machine.
|
||||||
|
|
||||||
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.
|
## What gets tracked
|
||||||
|
|
||||||
Episode completion for local `watched` state uses the shared `DEFAULT_MIN_WATCH_RATIO` (`85%`) value from `src/shared/watch-threshold.ts`.
|
- Watch sessions: time watched, subtitle lines seen, words seen, cards mined, pauses and seeks.
|
||||||
|
- Every primary subtitle line you see, with its timing, so you can search and mine from it later.
|
||||||
|
- Vocabulary and kanji you encounter, with how often and where.
|
||||||
|
- Library entries per show and episode, with cover art from AniList (or TMDB for live action) and YouTube channel metadata.
|
||||||
|
|
||||||
## Enabling
|
An episode counts as watched once you reach 85% of it.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Tracking needs no setup. To turn it off or move the database:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"immersionTracking": {
|
"immersionTracking": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"dbPath": ""
|
"dbPath": "",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Leave `dbPath` empty to use the default location (`immersion.sqlite` in SubMiner's app-data directory).
|
An empty `dbPath` stores `immersion.sqlite` in SubMiner's config directory (`~/.config/SubMiner/` on Linux). Set a path to keep it elsewhere.
|
||||||
- Set an explicit path to move the database (useful for backups, cloud syncing, or external tools).
|
|
||||||
|
|
||||||
## Stats Dashboard
|
To share stats and watch history between machines, use [`subminer sync <host>`](/launcher-script#sync-between-machines). It merges both databases. Copying the file with a cloud sync tool makes one side overwrite the other.
|
||||||
|
|
||||||
The same immersion data powers the stats dashboard.
|
## Open the dashboard
|
||||||
|
|
||||||
- In-app overlay: focus the visible overlay, then press the key from `stats.toggleKey` (default: `` ` `` / `Backquote`).
|
- In the overlay: focus it and press the `stats.toggleKey` key (Backquote by default).
|
||||||
- Launcher command: run `subminer stats` to start the local stats server on demand and open the dashboard in your browser.
|
- In a browser: run `subminer stats`, then open `http://127.0.0.1:6969` (or your `stats.serverPort`). Set `stats.autoOpenBrowser` to open it automatically.
|
||||||
- Background server: run `subminer stats -b` to start or reuse a dedicated background stats daemon without keeping the launcher attached, and `subminer stats -s` to stop that daemon.
|
- Background server: `subminer stats -b` starts a stats server that keeps running without the launcher attached. `subminer stats -s` stops it. You can still start SubMiner for playback while it runs.
|
||||||
- Maintenance command: run `subminer stats cleanup` or `subminer stats cleanup -v` to backfill/repair vocabulary metadata (`headword`, `reading`, POS) and purge stale or excluded rows from `imm_words` on demand.
|
|
||||||
- Browser page: open `http://127.0.0.1:6969` directly if the local stats server is already running.
|
|
||||||
|
|
||||||
### Dashboard Tabs
|
`subminer stats` fails if `immersionTracking.enabled` is `false`. The server only answers on localhost, so reverse proxies and Tailscale Serve URLs do not work.
|
||||||
|
|
||||||
#### Overview
|
## Stats dashboard
|
||||||
|
|
||||||
Recent sessions, streak calendar, watch-time history, and a tracking snapshot with completed episodes/anime totals.
|
### Overview
|
||||||
|
|
||||||
|
Recent sessions, a streak calendar, watch-time history, and totals for completed episodes and shows.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Library
|
### Library
|
||||||
|
|
||||||
Cover-art library with search and sorting, per-series progress, episode drill-down, and direct links into mined cards.
|
Your shows as cover-art cards with search, sorting, per-series progress, and an episode list linking to mined cards. The **All Titles** / **Anime** / **Live Action** / **YouTube** selector filters the grid. YouTube videos are grouped by channel.
|
||||||
|
|
||||||
When YouTube channel metadata is available, the Library tab groups videos by creator/channel and treats each tracked video as an episode-like entry inside that channel section.
|
Seasons get separate cards when a season number is detected. Live-action titles that AniList cannot match are looked up on [TMDB](/configuration#tmdb). If a title gets no match, open it and use **Link to TMDB** to pick one by hand.
|
||||||
|
|
||||||
|
The same show can end up on several cards when release names disagree. To fix that:
|
||||||
|
|
||||||
|
- Merge: click **Select**, tick the duplicate cards, choose **Merge Selected**, and pick the entry to keep. Sessions, cards, and watch time move over, and future episodes with those names join the kept entry.
|
||||||
|
- Move one episode: hover its row in the episode list and click **→** to assign it to another entry. SubMiner remembers the correction.
|
||||||
|
- Suggested merges appear as **Possible duplicate** above the grid. Choose **Review merge** or **Not duplicates**.
|
||||||
|
|
||||||
|
**Delete Entry** in a title's header removes the show with all its episodes, sessions, and lines. You cannot delete the title that is currently playing.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Trends
|
### Trends
|
||||||
|
|
||||||
Watch time, sessions, words seen, and per-anime progress/pattern charts with configurable date ranges and grouping.
|
Charts for watch time, cards, words, and sessions per day or month, running totals, efficiency (words per minute, cards per hour), and viewing patterns by weekday and hour. Each chart has its own date range and grouping.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Sessions
|
### Sessions
|
||||||
|
|
||||||
Expandable session history with new-word activity, cumulative totals, and pause/seek/card markers. Each session row exposes a hover-revealed ↗ button that navigates to the anime media-detail view for that session; pressing the back button there returns to the Sessions tab.
|
Session history with new-word activity and pause, seek, and card markers. The **↗** button on a row opens that show's detail view.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Vocabulary
|
### Vocabulary
|
||||||
|
|
||||||
Top repeated words (click a bar to open the word), new-word timeline, frequency rank table with full readings, kanji breakdown, word exclusion list, and click-through occurrence drilldown with Mine Word / Mine Sentence / Mine Audio buttons.
|
Unique words and kanji you have seen, new words per day, frequency rank tables with Hide Known and Hide Kana filters, and a kanji breakdown. Click a word to see every line it appeared in.
|
||||||
|
|
||||||
|
- **Exclusions** hides words from every vocabulary view. You can restore them from the same dialog.
|
||||||
|
- **Duplicates** cleans up lines repeated by karaoke openings and animated signs (see [Repeated lines](#repeated-lines)).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Stats server config lives under `stats`:
|
### Search
|
||||||
|
|
||||||
```jsonc
|
Searches the primary subtitle lines and titles in your history. **Search by headword** is on by default, so `知らない` also finds inflected forms. Turn it off for exact text matching. Secondary subtitles are not searched.
|
||||||
{
|
|
||||||
"stats": {
|
## Mining from the dashboard
|
||||||
"toggleKey": "Backquote",
|
|
||||||
"serverPort": 6969,
|
Search results and the Vocabulary word panel can create cards from past lines, as long as the source video file is still available:
|
||||||
"autoStartServer": true,
|
|
||||||
"autoOpenBrowser": false
|
- **Mine Word**: full Yomitan lookup for the word, plus sentence, audio, and image.
|
||||||
}
|
- **Mine Sentence**: a sentence card with `IsSentenceCard` set, for Lapis and Kiku note types.
|
||||||
}
|
- **Mine Audio**: an audio card with `IsAudioCard` set.
|
||||||
|
|
||||||
|
Word and audio mining appear only when the word occurs in the sentence. All three use your `ankiConnect` deck, note type, fields, and media settings. Anki must be running, and Mine Word needs Yomitan dictionaries.
|
||||||
|
|
||||||
|
## Repeated lines
|
||||||
|
|
||||||
|
Karaoke openings and animated signs repeat the same text once per frame. SubMiner collapses these as it records, so one lyric is stored once. Stats recorded before that can hold hundreds of copies and skew Top Repeated Words.
|
||||||
|
|
||||||
|
To clean them, use **Duplicates** in the Vocabulary tab: pick a time window, **Scan** to preview, then **Clean Up**. Or from the terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer stats cleanup --duplicate-lines --dry-run --lookback-days 30
|
||||||
|
subminer stats cleanup --duplicate-lines --lookback-days 30
|
||||||
```
|
```
|
||||||
|
|
||||||
- `toggleKey` is overlay-local, not a system-wide shortcut.
|
Leave out `--lookback-days` to scan all history. Word and kanji counts are corrected. Watch time and session totals are not changed.
|
||||||
- `serverPort` controls the localhost dashboard URL.
|
|
||||||
- `autoStartServer` starts the local stats HTTP server on launch once immersion tracking is active, or reuses the dedicated background stats server when one is already running.
|
|
||||||
- `autoOpenBrowser` controls whether `subminer stats` launches the dashboard URL in your browser after ensuring the server is running.
|
|
||||||
- `subminer stats` forces the dashboard server to start even when `autoStartServer` is `false`.
|
|
||||||
- `subminer stats -b` starts or reuses the dedicated background stats daemon and exits after startup acknowledgement.
|
|
||||||
- The background stats daemon is separate from the normal SubMiner overlay app, so you can leave it running and still launch SubMiner later to watch or mine from video.
|
|
||||||
- `subminer stats -s` stops the dedicated background stats daemon without closing any browser tabs.
|
|
||||||
- `subminer stats` fails with an error when `immersionTracking.enabled` is `false`.
|
|
||||||
- `subminer stats cleanup` defaults to vocabulary cleanup, repairs stale `headword`, `reading`, and `part_of_speech` values, attempts best-effort MeCab backfill for legacy rows, and removes rows that still fail vocab filtering.
|
|
||||||
|
|
||||||
## Mining Cards from the Stats Page
|
## Maintenance commands
|
||||||
|
|
||||||
The Vocabulary tab's word detail panel shows example lines from your viewing history. Each example line with a valid source file offers three mining buttons:
|
| Command | What it does |
|
||||||
|
| ------------------------------------------ | ------------------------------------------------------------------------- |
|
||||||
|
| `subminer stats cleanup` | Repair word readings and part of speech, drop words that fail the filters |
|
||||||
|
| `subminer stats cleanup -l` | Recompute lifetime totals from episode history, keeping old totals |
|
||||||
|
| `subminer stats cleanup --duplicate-lines` | Collapse repeated karaoke and sign lines (see above) |
|
||||||
|
|
||||||
- **Mine Word** — performs a full Yomitan dictionary lookup for the word (definition, reading, pitch accent, etc.) via a short-lived hidden helper, then enriches the card with sentence audio, a screenshot or animated AVIF clip, the highlighted sentence, and metadata extracted from the source video file. Requires Anki and Yomitan dictionaries to be loaded.
|
`subminer stats rebuild` and `subminer stats backfill` run the same lifetime repair as `cleanup -l`.
|
||||||
- **Mine Sentence** — creates a sentence card directly with the `IsSentenceCard` flag set (for Lapis/Kiku workflows), along with audio, image, and translation from the secondary subtitle if available.
|
|
||||||
- **Mine Audio** — creates an audio-only card with the `IsAudioCard` flag, attaching only the sentence audio clip.
|
|
||||||
|
|
||||||
All three modes respect your `ankiConnect` config: deck, model, field mappings, media settings (static vs AVIF, quality, dimensions), audio padding, metadata pattern, and tags. Media generation runs in parallel for faster card creation.
|
## Retention
|
||||||
|
|
||||||
Secondary subtitle text (typically English translations) is stored alongside primary subtitles during playback and used as the translation field when mining from the stats page.
|
By default SubMiner keeps everything. To limit history, set `immersionTracking.retentionPreset` to `minimal`, `balanced`, or `deep-history`, or set the `immersionTracking.retention.*Days` values yourself (`0` keeps all). Lifetime totals and vocabulary counts are stored separately and stay exact when old sessions are pruned.
|
||||||
|
|
||||||
### Word Exclusion List
|
See [Immersion tracking](/configuration#immersion-tracking) and [Stats dashboard](/configuration#stats-dashboard) in the config reference for every option and default.
|
||||||
|
|
||||||
The Vocabulary tab toolbar includes an **Exclusions** button for hiding words from all vocabulary views. Excluded words are stored in the immersion database, with older browser localStorage exclusions imported on first load after upgrade. They can be managed (restored or cleared) from the exclusion modal. Exclusions affect stat cards, charts, the frequency rank table, and the word list.
|
|
||||||
|
|
||||||
## Retention Defaults
|
|
||||||
|
|
||||||
By default, SubMiner keeps all retention tables and raw data (`0` means keep all) while continuing daily/monthly rollup maintenance:
|
|
||||||
|
|
||||||
| Data type | Retention |
|
|
||||||
| -------------- | --------- |
|
|
||||||
| Raw events | 0 (keep all) |
|
|
||||||
| Telemetry | 0 (keep all) |
|
|
||||||
| Sessions | 0 (keep all) |
|
|
||||||
| Daily rollups | 0 (keep all) |
|
|
||||||
| Monthly rollups | 0 (keep all) |
|
|
||||||
|
|
||||||
Maintenance runs on startup and every 24 hours. Vacuum runs only when `retention.vacuumIntervalDays` is non-zero.
|
|
||||||
|
|
||||||
In practice:
|
|
||||||
|
|
||||||
- Overview totals read from lifetime summary tables, so all-time watch time/cards/words stay exact even if raw query paths evolve.
|
|
||||||
- Anime and episode pages keep lifetime totals from summary tables while session drill-down still reads retained sessions directly. With the current defaults, both are kept forever.
|
|
||||||
- Trends can read the full available history because daily/monthly rollups are also kept forever by default.
|
|
||||||
- Vocabulary and kanji totals are cumulative and not bounded by the raw session retention knobs.
|
|
||||||
|
|
||||||
## Storage / Performance Model
|
|
||||||
|
|
||||||
The tracker is optimized for "keep everything" defaults:
|
|
||||||
|
|
||||||
- Exact all-time totals live in dedicated lifetime summary tables (`imm_lifetime_global`, `imm_lifetime_anime`, `imm_lifetime_media`).
|
|
||||||
- Ended-session totals are persisted onto `imm_sessions`, so most dashboard reads do not need to rescan raw telemetry.
|
|
||||||
- Daily and monthly rollups remain available for chart queries and coarse trend views.
|
|
||||||
- Subtitle text is stored once in `imm_subtitle_lines`; subtitle-line event payloads keep compact metadata only.
|
|
||||||
- Cover-art binaries are deduplicated through a shared blob store so episodes in the same series do not each carry duplicate image bytes.
|
|
||||||
- Hot tables have dedicated indexes for session time ranges, telemetry sample windows, frequency-ranked vocabulary, and cover-art lookup keys.
|
|
||||||
|
|
||||||
## Configurable Knobs
|
|
||||||
|
|
||||||
All policy options live under `immersionTracking` in your config:
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
| ------ | ----------- |
|
|
||||||
| `batchSize` | Writes per flush batch |
|
|
||||||
| `flushIntervalMs` | Max delay between flushes (default: 500ms) |
|
|
||||||
| `queueCap` | Max queued writes before oldest are dropped |
|
|
||||||
| `payloadCapBytes` | Max payload size per write |
|
|
||||||
| `maintenanceIntervalMs` | How often maintenance runs |
|
|
||||||
| `retention.eventsDays` | Raw event retention |
|
|
||||||
| `retention.telemetryDays` | Telemetry retention |
|
|
||||||
| `retention.sessionsDays` | Session retention |
|
|
||||||
| `retention.dailyRollupsDays` | Daily rollup retention |
|
|
||||||
| `retention.monthlyRollupsDays` | Monthly rollup retention |
|
|
||||||
| `retention.vacuumIntervalDays` | Minimum spacing between vacuums |
|
|
||||||
| `retentionMode` | `preset` or `advanced` |
|
|
||||||
| `retentionPreset` | `minimal`, `balanced`, or `deep-history` (used by `retentionMode`) |
|
|
||||||
| `lifetimeSummaries.global` | Maintain global lifetime totals |
|
|
||||||
| `lifetimeSummaries.anime` | Maintain per-anime lifetime totals |
|
|
||||||
| `lifetimeSummaries.media` | Maintain per-media lifetime totals |
|
|
||||||
|
|
||||||
## Query Templates
|
|
||||||
|
|
||||||
### Session timeline
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
sample_ms,
|
|
||||||
total_watched_ms,
|
|
||||||
active_watched_ms,
|
|
||||||
lines_seen,
|
|
||||||
words_seen,
|
|
||||||
tokens_seen,
|
|
||||||
cards_mined
|
|
||||||
FROM imm_session_telemetry
|
|
||||||
WHERE session_id = ?
|
|
||||||
ORDER BY sample_ms DESC, telemetry_id DESC
|
|
||||||
LIMIT ?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Session throughput summary
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
s.session_id,
|
|
||||||
s.video_id,
|
|
||||||
s.started_at_ms,
|
|
||||||
s.ended_at_ms,
|
|
||||||
COALESCE(s.active_watched_ms, 0) AS active_watched_ms,
|
|
||||||
COALESCE(s.words_seen, 0) AS words_seen,
|
|
||||||
COALESCE(s.cards_mined, 0) AS cards_mined,
|
|
||||||
CASE
|
|
||||||
WHEN COALESCE(s.active_watched_ms, 0) > 0
|
|
||||||
THEN COALESCE(s.words_seen, 0) / (COALESCE(s.active_watched_ms, 0) / 60000.0)
|
|
||||||
ELSE NULL
|
|
||||||
END AS words_per_min,
|
|
||||||
CASE
|
|
||||||
WHEN COALESCE(s.active_watched_ms, 0) > 0
|
|
||||||
THEN (COALESCE(s.cards_mined, 0) * 60.0) / (COALESCE(s.active_watched_ms, 0) / 60000.0)
|
|
||||||
ELSE NULL
|
|
||||||
END AS cards_per_hour
|
|
||||||
FROM imm_sessions s
|
|
||||||
ORDER BY s.started_at_ms DESC
|
|
||||||
LIMIT ?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lifetime anime totals
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
a.anime_id,
|
|
||||||
a.canonical_title,
|
|
||||||
la.total_sessions,
|
|
||||||
la.total_active_ms,
|
|
||||||
la.total_cards,
|
|
||||||
la.total_words_seen,
|
|
||||||
la.total_lines_seen,
|
|
||||||
la.first_watched_ms,
|
|
||||||
la.last_watched_ms
|
|
||||||
FROM imm_lifetime_anime la
|
|
||||||
JOIN imm_anime a ON a.anime_id = la.anime_id
|
|
||||||
ORDER BY la.last_watched_ms DESC
|
|
||||||
LIMIT ?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Daily rollups
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
rollup_day,
|
|
||||||
video_id,
|
|
||||||
total_sessions,
|
|
||||||
total_active_min,
|
|
||||||
total_lines_seen,
|
|
||||||
total_words_seen,
|
|
||||||
total_tokens_seen,
|
|
||||||
total_cards,
|
|
||||||
cards_per_hour,
|
|
||||||
words_per_min,
|
|
||||||
lookup_hit_rate
|
|
||||||
FROM imm_daily_rollups
|
|
||||||
ORDER BY rollup_day DESC, video_id DESC
|
|
||||||
LIMIT ?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Monthly rollups
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
rollup_month,
|
|
||||||
video_id,
|
|
||||||
total_sessions,
|
|
||||||
total_active_min,
|
|
||||||
total_lines_seen,
|
|
||||||
total_words_seen,
|
|
||||||
total_tokens_seen,
|
|
||||||
total_cards
|
|
||||||
FROM imm_monthly_rollups
|
|
||||||
ORDER BY rollup_month DESC, video_id DESC
|
|
||||||
LIMIT ?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
- Write path is asynchronous and queue-backed. Hot paths (subtitle parsing, render, token flows) enqueue telemetry and never await SQLite writes.
|
|
||||||
- Queue overflow policy: drop oldest queued writes, keep newest.
|
|
||||||
- SQLite tunings: `journal_mode=WAL`, `synchronous=NORMAL`, `foreign_keys=ON`, `busy_timeout=2500`, bounded WAL growth via `journal_size_limit`.
|
|
||||||
- Maintenance executes `PRAGMA optimize` after periodic cleanup.
|
|
||||||
- Rollups run incrementally from the last processed telemetry sample; startup performs a one-time bootstrap pass.
|
|
||||||
- Cover-art blobs are deduplicated into `imm_cover_art_blobs` and referenced from `imm_media_art`.
|
|
||||||
- Large-table reads are index-backed for `sample_ms`, session time windows, frequency-ranked words/kanji, and cover-art identity lookups.
|
|
||||||
- Workload-dependent tuning knobs remain at defaults unless you change them: `cache_size`, `mmap_size`, `temp_store`, `auto_vacuum`.
|
|
||||||
|
|
||||||
### Schema (v4)
|
|
||||||
|
|
||||||
Core tables:
|
|
||||||
|
|
||||||
- `imm_videos` — video key/title/source metadata
|
|
||||||
- `imm_sessions` — session UUID, video reference, timing/status, final denormalized totals
|
|
||||||
- `imm_session_telemetry` — high-frequency session aggregates over time
|
|
||||||
- `imm_session_events` — event stream with compact numeric event types
|
|
||||||
- `imm_subtitle_lines` — persisted subtitle text and timing per session/video
|
|
||||||
|
|
||||||
Lifetime summary tables:
|
|
||||||
|
|
||||||
- `imm_lifetime_global`
|
|
||||||
- `imm_lifetime_anime`
|
|
||||||
- `imm_lifetime_media`
|
|
||||||
- `imm_lifetime_applied_sessions`
|
|
||||||
|
|
||||||
Rollup tables:
|
|
||||||
|
|
||||||
- `imm_daily_rollups`
|
|
||||||
- `imm_monthly_rollups`
|
|
||||||
|
|
||||||
Vocabulary tables:
|
|
||||||
|
|
||||||
- `imm_words(id, headword, word, reading, first_seen, last_seen, frequency)`
|
|
||||||
- `imm_kanji(id, kanji, first_seen, last_seen, frequency)`
|
|
||||||
|
|
||||||
Media-art tables:
|
|
||||||
|
|
||||||
- `imm_media_art` — per-video cover metadata plus shared blob reference
|
|
||||||
- `imm_cover_art_blobs` — deduplicated image bytes keyed by blob hash
|
|
||||||
@@ -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;" />',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -7,7 +7,7 @@ titleTemplate: Immersion Mining Workflow for MPV
|
|||||||
hero:
|
hero:
|
||||||
name: SubMiner
|
name: SubMiner
|
||||||
text: Immersion Mining for MPV
|
text: Immersion Mining for MPV
|
||||||
tagline: Watch media, mine vocabulary, and craft anki cards without leaving the scene.
|
tagline: Watch, look up a word, and get an Anki card with audio and a screenshot. Without pausing your show.
|
||||||
image:
|
image:
|
||||||
src: /assets/SubMiner.png
|
src: /assets/SubMiner.png
|
||||||
alt: SubMiner logo
|
alt: SubMiner logo
|
||||||
@@ -24,69 +24,71 @@ features:
|
|||||||
src: /assets/mpv.svg
|
src: /assets/mpv.svg
|
||||||
alt: mpv icon
|
alt: mpv icon
|
||||||
title: Built for mpv
|
title: Built for mpv
|
||||||
details: Tracks subtitles via mpv IPC in real time. Launch with the wrapper script or the mpv plugin — no external bridge needed.
|
details: Reads subtitle state over mpv's IPC socket. Launch with the wrapper script or the mpv plugin. There is no separate bridge process to run.
|
||||||
link: /usage
|
link: /usage
|
||||||
linkText: How it works
|
linkText: How it works
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/yomitan-icon.svg
|
src: /assets/yomitan-icon.svg
|
||||||
alt: Yomitan logo
|
alt: Yomitan logo
|
||||||
title: Bundled Yomitan
|
title: Bundled Yomitan
|
||||||
details: Ships with a built-in Yomitan instance for instant word lookups and context-aware card creation directly from subtitle text.
|
details: A Yomitan instance is bundled and preconfigured. Hover a word in the subtitle overlay to look it up and mine it.
|
||||||
link: /mining-workflow
|
link: /mining-workflow
|
||||||
linkText: Mining workflow
|
linkText: Mining workflow
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/anki-card.svg
|
src: /assets/anki-card.svg
|
||||||
alt: Anki card icon
|
alt: Anki card icon
|
||||||
title: Anki Card Enrichment
|
title: Anki card enrichment
|
||||||
details: Auto-fills card fields with sentence, audio clip, screenshot, and translation so you can focus on learning.
|
details: New cards get the subtitle line, an audio clip cut to the line timing, and a screenshot from that moment.
|
||||||
link: /anki-integration
|
link: /anki-integration
|
||||||
linkText: Anki integration
|
linkText: Anki integration
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/highlight.svg
|
src: /assets/highlight.svg
|
||||||
alt: Highlight icon
|
alt: Highlight icon
|
||||||
title: Reading Annotations
|
title: Reading annotations
|
||||||
details: N+1 targeting, character-name matching, frequency highlighting, and JLPT tagging — all layered on subtitle text in real time.
|
details: N+1 targeting, character-name matching, frequency highlighting, and JLPT tagging, drawn onto the subtitle line as it plays.
|
||||||
link: /subtitle-annotations
|
link: /subtitle-annotations
|
||||||
linkText: Annotation details
|
linkText: Annotation details
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/video.svg
|
src: /assets/video.svg
|
||||||
alt: Video playback icon
|
alt: Video playback icon
|
||||||
title: YouTube Playback
|
title: YouTube playback
|
||||||
details: Play YouTube URLs or ytsearch targets directly — SubMiner automatically selects and loads subtitles for the video.
|
details: Pass a YouTube URL or a ytsearch target. SubMiner picks a subtitle track for the video and loads it.
|
||||||
link: /usage#youtube-playback
|
link: /usage#youtube-playback
|
||||||
linkText: YouTube playback
|
linkText: YouTube playback
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/jellyfin.svg
|
src: /assets/jellyfin.svg
|
||||||
alt: Jellyfin icon
|
alt: Jellyfin icon
|
||||||
title: Jellyfin Integration
|
title: Jellyfin integration
|
||||||
details: Browse your Jellyfin library, pick media interactively, and play through mpv with full subtitle and mining support.
|
details: Browse your Jellyfin library from the overlay and play a title through mpv. Subtitles and mining work the same as with local files.
|
||||||
link: /jellyfin-integration
|
link: /jellyfin-integration
|
||||||
linkText: Jellyfin setup
|
linkText: Jellyfin setup
|
||||||
- icon:
|
- icon:
|
||||||
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 and sync
|
||||||
details: Search and pull subtitles from Jimaku, then auto-sync timing with alass or ffsubsync — all from the overlay.
|
details: Search Jimaku or TsukiHime and download a track, then retime it with alass or ffsubsync. Both run from the overlay.
|
||||||
link: /jimaku-integration
|
link: /jimaku-integration
|
||||||
linkText: Jimaku integration
|
linkText: Jimaku integration
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/tokenization.svg
|
src: /assets/tokenization.svg
|
||||||
alt: Tracking chart icon
|
alt: Tracking chart icon
|
||||||
title: Stats Dashboard
|
title: Stats dashboard
|
||||||
details: Browse session history, streak calendars, vocabulary frequency, and per-series progress in a local dashboard — then mine cards straight from your viewing history.
|
details: A local dashboard with session history, streak calendars, word frequency, and per-series progress. You can mine cards from lines you already watched.
|
||||||
link: /immersion-tracking
|
link: /immersion-tracking
|
||||||
linkText: Dashboard & tracking
|
linkText: Dashboard & tracking
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/cross-platform.svg
|
src: /assets/cross-platform.svg
|
||||||
alt: Cross-platform icon
|
alt: Cross-platform icon
|
||||||
title: Cross-Platform
|
title: Cross-platform
|
||||||
details: Runs on Linux (Hyprland, Sway, X11), macOS, and Windows with compositor-aware window positioning and platform-native integration.
|
details: Runs on Linux (Hyprland, Sway, X11), macOS, and Windows. Overlay positioning is handled per compositor rather than assuming one window manager.
|
||||||
link: /installation
|
link: /installation
|
||||||
linkText: Platform setup
|
linkText: Platform setup
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const demoAssetVersion = '20260223-2';
|
import { withBase } from 'vitepress';
|
||||||
|
|
||||||
|
const demoAssetVersion = '20260819-1';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="landing-shell">
|
<div class="landing-shell">
|
||||||
@@ -96,38 +98,38 @@ const demoAssetVersion = '20260223-2';
|
|||||||
<div class="workflow-step" style="animation-delay: 0ms">
|
<div class="workflow-step" style="animation-delay: 0ms">
|
||||||
<div class="step-number">01</div>
|
<div class="step-number">01</div>
|
||||||
<div class="step-title">Start</div>
|
<div class="step-title">Start</div>
|
||||||
<div class="step-desc">Launch with the wrapper or existing mpv setup and keep subtitles in sync.</div>
|
<div class="step-desc">Launch through the wrapper, or from an mpv setup you already have.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-connector" aria-hidden="true"></div>
|
<div class="workflow-connector" aria-hidden="true"></div>
|
||||||
<div class="workflow-step" style="animation-delay: 60ms">
|
<div class="workflow-step" style="animation-delay: 60ms">
|
||||||
<div class="step-number">02</div>
|
<div class="step-number">02</div>
|
||||||
<div class="step-title">Lookup</div>
|
<div class="step-title">Lookup</div>
|
||||||
<div class="step-desc">Hover a token in the interactive overlay, then trigger Yomitan lookup to open context.</div>
|
<div class="step-desc">Hover a token in the overlay to open the Yomitan popup for that word.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-connector" aria-hidden="true"></div>
|
<div class="workflow-connector" aria-hidden="true"></div>
|
||||||
<div class="workflow-step" style="animation-delay: 120ms">
|
<div class="workflow-step" style="animation-delay: 120ms">
|
||||||
<div class="step-number">03</div>
|
<div class="step-number">03</div>
|
||||||
<div class="step-title">Mine</div>
|
<div class="step-title">Mine</div>
|
||||||
<div class="step-desc">Create cards from Yomitan or mine sentence cards directly from subtitle lines.</div>
|
<div class="step-desc">Add the word from Yomitan, or mine the whole line as a sentence card.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-connector" aria-hidden="true"></div>
|
<div class="workflow-connector" aria-hidden="true"></div>
|
||||||
<div class="workflow-step" style="animation-delay: 180ms">
|
<div class="workflow-step" style="animation-delay: 180ms">
|
||||||
<div class="step-number">04</div>
|
<div class="step-number">04</div>
|
||||||
<div class="step-title">Enrich</div>
|
<div class="step-title">Enrich</div>
|
||||||
<div class="step-desc">Automatically attach timing-accurate audio, sentence text, and visual evidence.</div>
|
<div class="step-desc">SubMiner fills in the audio clip, the sentence, and a screenshot from that moment.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-connector" aria-hidden="true"></div>
|
<div class="workflow-connector" aria-hidden="true"></div>
|
||||||
<div class="workflow-step" style="animation-delay: 240ms">
|
<div class="workflow-step" style="animation-delay: 240ms">
|
||||||
<div class="step-number">05</div>
|
<div class="step-number">05</div>
|
||||||
<div class="step-title">Track</div>
|
<div class="step-title">Track</div>
|
||||||
<div class="step-desc">Open the stats dashboard to review sessions, vocabulary trends, and mine cards from past viewing history.</div>
|
<div class="step-desc">Review past sessions and word trends, and mine anything you missed the first time.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="demo-section">
|
<section class="demo-section">
|
||||||
<h2>See it in action</h2>
|
<h2>See it in action</h2>
|
||||||
<p>Subtitles, lookup flow, and card enrichment from a real playback session.</p>
|
<p>Recorded from an actual playback session: subtitle hover, lookup, and the card that comes out the other end.</p>
|
||||||
<div class="demo-window">
|
<div class="demo-window">
|
||||||
<div class="demo-window__bar">
|
<div class="demo-window__bar">
|
||||||
<span class="demo-window__dot"></span>
|
<span class="demo-window__dot"></span>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,53 +1,54 @@
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
## How the Pieces Fit Together
|
SubMiner draws an interactive overlay on top of the [mpv](https://mpv.io) video player. While you watch Japanese media, you hover a word in the subtitles to look it up, then turn it into an Anki card without leaving the video.
|
||||||
|
|
||||||
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.
|
Building cards from what you watch is called **sentence mining**. SubMiner bundles its own copy of **Yomitan** (a pop-up dictionary) and talks to **AnkiConnect** (an Anki add-on that lets other programs create cards), so it can fill in the sentence, audio, and screenshot for you.
|
||||||
|
|
||||||
To get a working setup you need:
|
Getting started takes three steps:
|
||||||
|
|
||||||
1. **mpv** launched with an IPC socket so SubMiner can read subtitle data
|
1. Install mpv and the optional extras you want.
|
||||||
2. **SubMiner** (the Electron overlay app)
|
2. Install SubMiner.
|
||||||
3. **Dictionaries** imported into the bundled Yomitan instance (lookups won't work without at least one)
|
3. Launch it and follow the first-run setup.
|
||||||
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.
|
## 1. Install requirements
|
||||||
|
|
||||||
## Requirements
|
Only mpv is required. Install ffmpeg too unless you are fine with cards that have no audio or screenshot.
|
||||||
|
|
||||||
### System Dependencies
|
| Dependency | Needed for | Platforms |
|
||||||
|
| ------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------ |
|
||||||
|
| mpv | Required. The player SubMiner draws over. | All |
|
||||||
|
| fuse2 | Required to run the AppImage. | Linux |
|
||||||
|
| ffmpeg | Recommended. Audio clips and screenshots on cards. Without it those fields stay empty. | All |
|
||||||
|
| MeCab + mecab-ipadic | Recommended. More accurate N+1, JLPT, and frequency highlighting. | All |
|
||||||
|
| yt-dlp | YouTube playback. | All |
|
||||||
|
| xz | [TsukiHime](/tsukihime-integration) subtitle downloads. Most Linux distros already have it. | All |
|
||||||
|
| guessit | Better title, season, and episode detection for [AniSkip](/aniskip-integration) and [AniList](/anilist-integration). | All |
|
||||||
|
| alass or ffsubsync | Subtitle syncing. You need at least one to use it. | All |
|
||||||
|
| fzf, rofi | The file pickers in the `subminer` command (rofi is Linux only). | Linux, macOS |
|
||||||
|
| chafa, ffmpegthumbnailer | Thumbnail previews in the pickers. | Linux, macOS |
|
||||||
|
|
||||||
| Dependency | Required | Notes |
|
To generate Japanese subtitles from audio, you also need whisper.cpp. See [Subtitle generation](/subtitle-generation).
|
||||||
| -------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| 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
|
||||||
|
|
||||||
**Linux** — one of the following window backends:
|
SubMiner needs to track the mpv window, and how it does that depends on your desktop:
|
||||||
|
|
||||||
- **Hyprland** — native Wayland support (uses `hyprctl`)
|
- **Hyprland**: supported natively through `hyprctl`.
|
||||||
- **Sway** — native Wayland support (uses `swaymsg`)
|
- **Sway**: supported natively through `swaymsg`.
|
||||||
- **X11 / Xwayland** — for X11 sessions or any other Wayland compositor (uses `xdotool` and `xwininfo`)
|
- **Anything else** (X11, GNOME, KDE Plasma, other Wayland compositors): mpv and SubMiner must run under X11 or Xwayland. Install `xdotool` and `xwininfo`. The `subminer` command picks the X11 backend automatically, or you can force it with `--backend x11`.
|
||||||
|
|
||||||
::: warning Wayland support is compositor-specific
|
|
||||||
Wayland has no universal API for window positioning — each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Only Hyprland and Sway have native Wayland backends. If you run a different Wayland compositor (GNOME, KDE Plasma, river, etc.), both mpv **and** SubMiner must run under X11 or Xwayland. The `subminer` launcher handles this automatically when `--backend x11` is set or the X11 backend is auto-detected.
|
|
||||||
:::
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Arch Linux</b></summary>
|
<summary><b>Arch Linux</b></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S --needed mpv ffmpeg
|
sudo pacman -S --needed mpv ffmpeg
|
||||||
|
# Recommended (MeCab is only in the AUR)
|
||||||
|
paru -S --needed mecab-git 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 (install at least one)
|
||||||
paru -S --needed alass python-ffsubsync
|
paru -S --needed alass python-ffsubsync
|
||||||
# X11 / Xwayland (required for non-Hyprland/Sway compositors)
|
# Only for desktops other than Hyprland or Sway
|
||||||
sudo pacman -S --needed xdotool xorg-xwininfo
|
sudo pacman -S --needed xdotool xorg-xwininfo
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -58,13 +59,15 @@ 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)
|
# Only for desktops other than Hyprland or Sway
|
||||||
sudo apt install xdotool x11-utils
|
sudo apt install xdotool x11-utils
|
||||||
# Optional: subtitle sync
|
# Optional: subtitle sync
|
||||||
pip install ffsubsync
|
pip install ffsubsync
|
||||||
# alass is not in apt — install via cargo: cargo install alass-cli
|
cargo install alass-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -74,421 +77,269 @@ 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)
|
# Only for desktops other than Hyprland or Sway
|
||||||
sudo dnf install xdotool xorg-x11-utils
|
sudo dnf install xdotool xorg-x11-utils
|
||||||
# Optional: subtitle sync
|
# Optional: subtitle sync
|
||||||
pip install ffsubsync
|
pip install ffsubsync
|
||||||
# alass: cargo install alass-cli
|
cargo install alass-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
**macOS** — macOS 10.13 or later. Accessibility permission required for window tracking.
|
### macOS
|
||||||
|
|
||||||
|
You need macOS 11 (Big Sur) or later.
|
||||||
|
|
||||||
```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.
|
`mecab` must be on your `PATH` when SubMiner starts. Homebrew puts it in `/opt/homebrew/bin` on Apple Silicon and `/usr/local/bin` on Intel.
|
||||||
|
|
||||||
### Optional Tools
|
### Windows
|
||||||
|
|
||||||
| Tool | Purpose |
|
You need Windows 10 or later. Install mpv and ffmpeg with [winget](https://learn.microsoft.com/windows/package-manager/winget/), which ships with Windows 11 and current Windows 10. In PowerShell or Command Prompt:
|
||||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| 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
|
```powershell
|
||||||
|
winget install shinchiro.mpv
|
||||||
|
winget install Gyan.FFmpeg
|
||||||
|
winget install yt-dlp.yt-dlp # optional, for YouTube
|
||||||
|
```
|
||||||
|
|
||||||
### Arch Linux (AUR)
|
Close and reopen the terminal, then check both commands work:
|
||||||
|
|
||||||
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.
|
```powershell
|
||||||
|
mpv --version
|
||||||
|
ffmpeg -version
|
||||||
|
```
|
||||||
|
|
||||||
|
ffmpeg must be on `PATH`, because SubMiner runs it by name to make card audio and screenshots. mpv does not have to be. If `mpv --version` says `not recognized`, find `mpv.exe` (usually in `%LOCALAPPDATA%\Programs\mpv`) and either add that folder to `PATH` or enter the full path to `mpv.exe` during first-run setup (`mpv.executablePath`).
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Alternative: Scoop (no admin rights, includes xz)</b></summary>
|
||||||
|
|
||||||
|
[Scoop](https://scoop.sh) installs into your user profile and always adds commands to `PATH`. It is the only Windows package manager that also packages `xz`, which [TsukiHime](/tsukihime-integration) downloads need.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# One-time Scoop setup
|
||||||
|
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
|
||||||
|
|
||||||
|
scoop bucket add extras
|
||||||
|
scoop install extras/mpv main/ffmpeg
|
||||||
|
# Optional
|
||||||
|
scoop install main/yt-dlp main/xz
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Alternative: manual download</b></summary>
|
||||||
|
|
||||||
|
1. Download mpv from [mpv.io/installation](https://mpv.io/installation/) and ffmpeg from [ffmpeg.org/download.html](https://ffmpeg.org/download.html).
|
||||||
|
2. Unzip each into a permanent folder, for example `C:\Tools\mpv` and `C:\Tools\ffmpeg`. Find the folders that contain `mpv.exe` and `ffmpeg.exe` (for ffmpeg this is usually `bin`).
|
||||||
|
3. Press `Win`, search for **Edit the system environment variables**, and open it. Click **Environment Variables**, select **Path** under **User variables**, click **Edit**, and add both folders with **New**.
|
||||||
|
4. Open a new terminal and run `mpv --version` and `ffmpeg -version`. If either says `not recognized`, the folder you added does not contain the `.exe`.
|
||||||
|
|
||||||
|
For `xz` without Scoop, download [XZ Utils](https://tukaani.org/xz/) and add its folder to `PATH` the same way.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
For more accurate highlighting, install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary. The fzf and rofi pickers do not apply on Windows.
|
||||||
|
|
||||||
|
## 2. Install SubMiner
|
||||||
|
|
||||||
|
### Arch Linux (AUR) {#arch-aur}
|
||||||
|
|
||||||
|
Install [`subminer-bin`](https://aur.archlinux.org/packages/subminer-bin). It includes the AppImage and the `subminer` command.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
paru -S subminer-bin
|
paru -S subminer-bin
|
||||||
```
|
```
|
||||||
|
|
||||||
Or manually:
|
### Linux (AppImage) {#linux-appimage}
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://aur.archlinux.org/subminer-bin.git
|
|
||||||
cd subminer-bin
|
|
||||||
makepkg -si
|
|
||||||
```
|
|
||||||
|
|
||||||
### AppImage (Recommended)
|
|
||||||
|
|
||||||
Download the latest AppImage and the `subminer` launcher from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest).
|
|
||||||
|
|
||||||
**Step 1 — Install Bun** (required for the launcher):
|
|
||||||
|
|
||||||
```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 the optional Linux rofi theme
|
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
|
||||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
|
||||||
mkdir -p ~/.local/share/SubMiner/themes
|
|
||||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
First-run setup can install the `subminer` command for you.
|
||||||
|
|
||||||
The first-run setup window can also install Bun and the packaged `subminer` launcher into an existing writable PATH directory. Both steps are optional.
|
### macOS (DMG) {#macos-dmg}
|
||||||
|
|
||||||
To check for updates later:
|
1. Download the DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest), open it, and drag `SubMiner.app` into `/Applications`.
|
||||||
|
2. If macOS blocks the app on first launch, right-click it and choose **Open**, or run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer -u
|
xattr -d com.apple.quarantine /Applications/SubMiner.app
|
||||||
# or
|
```
|
||||||
subminer --update
|
|
||||||
```
|
|
||||||
|
|
||||||
SubMiner verifies launcher and Linux rofi theme downloads against `SHA256SUMS.txt`. If the launcher is installed in a protected path such as `/usr/local/bin/subminer`, SubMiner does not elevate itself; it shows the exact `sudo curl ... && sudo chmod +x ...` command to run instead.
|
3. Open **System Settings > Privacy & Security > Accessibility** and enable SubMiner (add it if it is missing). The overlay cannot follow the mpv window without this.
|
||||||
|
|
||||||
On Linux, `subminer -u` performs this update from the launcher process, so it does not need to start or IPC into the tray app.
|
First-run setup can install the `subminer` command for you.
|
||||||
|
|
||||||
### From Source
|
### Windows (installer) {#windows-installer}
|
||||||
|
|
||||||
|
Download from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest):
|
||||||
|
|
||||||
|
- `SubMiner-<version>.exe`: the installer. Use this one.
|
||||||
|
- `SubMiner-<version>-win.zip`: portable version.
|
||||||
|
- `subminer.cmd`: optional terminal command (setup can install it for you).
|
||||||
|
|
||||||
|
### 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:
|
make deps
|
||||||
git submodule update --init --recursive
|
|
||||||
|
|
||||||
bun install
|
|
||||||
bun run build
|
bun run build
|
||||||
|
bun run build:appimage # optional: package an AppImage
|
||||||
# Optional packaged Linux artifact
|
|
||||||
bun run build:appimage
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Bundled Yomitan is built during `bun run build`.
|
Building from source needs [Bun](https://bun.sh) installed.
|
||||||
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
|
```bash
|
||||||
brew install mpv ffmpeg
|
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
|
||||||
# Optional but recommended if you use N+1, JLPT, or frequency annotations
|
cd SubMiner
|
||||||
brew install mecab mecab-ipadic
|
make deps
|
||||||
|
make build-macos
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install the `subminer` launcher (recommended)
|
The `.dmg` and `.zip` land in `release/`. For an unsigned local build, run `bun run build:mac:unsigned`.
|
||||||
|
|
||||||
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.
|
</details>
|
||||||
|
|
||||||
First-run setup can install Bun and the packaged launcher into a writable directory that is already on PATH. It does not edit shell profiles.
|
<details>
|
||||||
|
<summary><b>Windows</b></summary>
|
||||||
|
|
||||||
Download it from the same [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) page:
|
```powershell
|
||||||
|
git clone https://github.com/ksyasuda/SubMiner.git
|
||||||
|
cd SubMiner
|
||||||
|
git submodule update --init --recursive
|
||||||
|
bun install
|
||||||
|
Set-Location stats
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
Set-Location ../vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
Set-Location ../..
|
||||||
|
bun run build:win
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 3. Launch and first-run setup
|
||||||
|
|
||||||
|
Start SubMiner. The setup window opens on first launch.
|
||||||
|
|
||||||
|
- **Linux (AUR)**: `subminer app --setup`
|
||||||
|
- **Linux (AppImage)**: `~/.local/bin/SubMiner.AppImage --setup`
|
||||||
|
- **macOS**: open `SubMiner.app` from `/Applications`
|
||||||
|
- **Windows**: run SubMiner from the Start menu
|
||||||
|
|
||||||
|
Setup walks you through:
|
||||||
|
|
||||||
|
1. **Config file.** Created at `~/.config/SubMiner/config.jsonc` (Linux and macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows).
|
||||||
|
2. **Yomitan dictionaries.** Import at least one dictionary, or lookups will not work. SubMiner's Yomitan is separate from any Yomitan in your browser.
|
||||||
|
3. **The `subminer` command** (optional). Setup installs it into a folder already on your `PATH`. If there is none on Linux or macOS, it uses `~/.local/bin` and shows the `export PATH=...` line to add to your shell config. On Windows it adds `%LOCALAPPDATA%\SubMiner\bin` to your user `PATH`.
|
||||||
|
4. **SubMiner mpv shortcut** (Windows only). A Start menu or desktop shortcut that opens mpv with SubMiner attached.
|
||||||
|
|
||||||
|
**Finish setup** unlocks once the config exists and at least one dictionary is imported. To reopen setup later, run `subminer app --setup`.
|
||||||
|
|
||||||
|
### Play a video
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O /usr/local/bin/subminer
|
subminer video.mkv
|
||||||
sudo chmod +x /usr/local/bin/subminer
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with curl:
|
On Windows, double-click the **SubMiner mpv** shortcut or drag a video onto it.
|
||||||
|
|
||||||
|
The overlay appears over mpv, and the subtitle text becomes hoverable. See [Usage](/usage) for everyday use.
|
||||||
|
|
||||||
|
### Check your setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
This checks for the SubMiner app, mpv, ffmpeg, yt-dlp, fzf, rofi, your config file, and the mpv socket path. Only a missing app or mpv counts as a failure. The rest are reported as optional.
|
||||||
|
|
||||||
|
## Anki setup
|
||||||
|
|
||||||
|
To create cards:
|
||||||
|
|
||||||
|
1. Install [Anki](https://apps.ankiweb.net/).
|
||||||
|
2. In Anki, open **Tools > Add-ons > Get Add-ons** and enter `2055492159` to install [AnkiConnect](https://ankiweb.net/shared/info/2055492159).
|
||||||
|
3. Restart Anki. Keep it open while you use SubMiner.
|
||||||
|
|
||||||
|
SubMiner connects to AnkiConnect at its default address with no extra setup. To choose your deck and card fields, see [Anki integration](/anki-integration).
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer -u
|
||||||
|
```
|
||||||
|
|
||||||
|
The tray menu's **Check for Updates** also installs updates on Linux, macOS, and Windows. If the AppImage sits in a folder you cannot write to, SubMiner prints the command to run instead of asking for admin rights.
|
||||||
|
|
||||||
|
If you installed from the AUR, update through your package manager instead.
|
||||||
|
|
||||||
|
## Launching mpv yourself
|
||||||
|
|
||||||
|
The `subminer` command and the Windows shortcut start mpv with the IPC socket SubMiner needs. If you start mpv another way, add this option or the overlay starts without subtitles:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--input-ipc-server=/tmp/subminer-socket # Linux and macOS
|
||||||
|
--input-ipc-server=\\.\pipe\subminer-socket # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
SubMiner loads its mpv plugin automatically, so there is nothing else to install. See [mpv plugin](/mpv-plugin) for the in-player keybindings.
|
||||||
|
|
||||||
|
## Manual launcher install
|
||||||
|
|
||||||
|
Use these if you skipped the launcher during setup. The launcher finds SubMiner in the usual install locations. For a custom location, set `SUBMINER_BINARY_PATH` to the app executable.
|
||||||
|
|
||||||
|
### Linux {#manual-launcher-install-linux}
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
```bash
|
||||||
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer
|
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer
|
||||||
sudo chmod +x /usr/local/bin/subminer
|
sudo chmod +x /usr/local/bin/subminer
|
||||||
```
|
```
|
||||||
|
|
||||||
To check for updates later:
|
### Windows {#manual-launcher-install-windows}
|
||||||
|
|
||||||
```bash
|
Download `subminer.cmd` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and put it in a folder on your user `PATH`.
|
||||||
subminer -u
|
|
||||||
# or
|
|
||||||
subminer --update
|
|
||||||
```
|
|
||||||
|
|
||||||
SubMiner verifies launcher downloads against `SHA256SUMS.txt`. If `/usr/local/bin/subminer` is protected, SubMiner shows the exact `sudo curl ... && sudo chmod +x ...` command to run instead of elevating itself.
|
## Bundled Bun runtime {#bundled-bun-runtime}
|
||||||
|
|
||||||
::: warning Bun required for the launcher
|
The `subminer` command runs on a copy of [Bun](https://bun.sh) 1.3.5 that ships inside the app, so you do not need to install Bun. Bun is MIT licensed and statically links JavaScriptCore (LGPL 2.0) and TinyCC (LGPL 2.1). License texts and a `SOURCE.md` ship in the app under `resources/bun/licenses`, and each GitHub release includes `bun-v1.3.5-source.tar.gz` with the matching sources.
|
||||||
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)
|
Next: [Usage](/usage).
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone --recurse-submodules https://github.com/ksyasuda/SubMiner.git
|
|
||||||
cd SubMiner
|
|
||||||
git submodule update --init --recursive
|
|
||||||
make build-macos
|
|
||||||
```
|
|
||||||
|
|
||||||
The built app will be available in the `release` directory (`.dmg` and `.zip`).
|
|
||||||
|
|
||||||
For unsigned local builds:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run build:mac:unsigned
|
|
||||||
```
|
|
||||||
|
|
||||||
Build and install the launcher alongside the app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make install-macos
|
|
||||||
```
|
|
||||||
|
|
||||||
This builds the `subminer` launcher into `dist/launcher/subminer` and installs it to `~/.local/bin/subminer` along with the app bundle. 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
|
|
||||||
|
|
||||||
### 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. **Optional: install the command-line launcher** — first-run setup can install Bun with winget/Scoop/the official installer and add `%LOCALAPPDATA%\SubMiner\bin\subminer.cmd` to your user PATH. Open a new terminal and type `subminer`.
|
|
||||||
4. **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 mpv** shortcut created during first-run setup is the recommended way to launch playback on Windows.
|
|
||||||
- The optional command-line launcher installs a `subminer.cmd` shim, but users type `subminer`; Windows resolves `.cmd` through `PATHEXT`.
|
|
||||||
- First-run setup adds only `%LOCALAPPDATA%\SubMiner\bin` to the HKCU user PATH. It does not add `SubMiner.exe` or the app install directory to PATH.
|
|
||||||
- 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
|
|
||||||
git clone https://github.com/ksyasuda/SubMiner.git
|
|
||||||
cd SubMiner
|
|
||||||
bun install
|
|
||||||
|
|
||||||
# Windows requires building the texthooker-ui submodule manually before
|
|
||||||
# the main build (Linux/macOS handle this automatically during `bun run build`).
|
|
||||||
Set-Location vendor/texthooker-ui
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
bun run build
|
|
||||||
Set-Location ../..
|
|
||||||
|
|
||||||
bun run build:win
|
|
||||||
```
|
|
||||||
|
|
||||||
Windows installer builds already get the required NSIS `WinShell` helper through electron-builder's cached `nsis-resources` bundle.
|
|
||||||
No extra repo-local WinShell plugin install step is required.
|
|
||||||
|
|
||||||
## MPV Plugin
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
::: 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
|
|
||||||
subminer app --setup
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
|
|
||||||
|
|
||||||
The setup popup walks you through:
|
|
||||||
|
|
||||||
- **Config file**: auto-created at `~/.config/SubMiner/config.jsonc` (Linux/macOS) or `%APPDATA%\SubMiner\config.jsonc` (Windows)
|
|
||||||
- **mpv plugin**: install the bundled Lua plugin for in-player keybindings
|
|
||||||
- **Yomitan dictionaries**: import at least one dictionary so lookups work
|
|
||||||
- **Windows shortcut** _(Windows only)_: optionally create a `SubMiner mpv` Start Menu/Desktop shortcut
|
|
||||||
- **Command line launcher**: optionally install Bun and the `subminer` launcher to your command-line PATH
|
|
||||||
|
|
||||||
The `Finish setup` button follows the normal config/Yomitan readiness checks. Bun and the command-line launcher are optional and never block setup completion.
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> You can re-open the setup popup at any time with `subminer app --setup` or `SubMiner.AppImage --setup`.
|
|
||||||
|
|
||||||
Once setup is complete, play a video to verify everything works:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer video.mkv
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see the overlay appear over mpv. If subtitles are loaded in the video, they will appear as interactive text in the overlay.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><b>More launch examples</b></summary>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Optional explicit overlay start for setups with plugin auto_start=no
|
|
||||||
subminer --start video.mkv
|
|
||||||
|
|
||||||
# Useful launch modes for troubleshooting
|
|
||||||
subminer --log-level debug video.mkv
|
|
||||||
SubMiner.AppImage --start --log-level debug
|
|
||||||
|
|
||||||
# Or with direct AppImage control
|
|
||||||
SubMiner.AppImage --background # Background tray service mode
|
|
||||||
SubMiner.AppImage --start
|
|
||||||
SubMiner.AppImage --start --dev
|
|
||||||
SubMiner.AppImage --help # Show all CLI options
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Verify Setup
|
|
||||||
|
|
||||||
After completing first-run setup, run the built-in diagnostic to confirm everything is in place:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer doctor
|
|
||||||
```
|
|
||||||
|
|
||||||
This checks for the app binary, mpv, ffmpeg, config file, and socket path. Fix any failures before continuing.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> On Windows, run `SubMiner.exe` directly. Replace `SubMiner.AppImage` with `SubMiner.exe` in the direct app commands below.
|
|
||||||
|
|
||||||
## Optional Extras
|
|
||||||
|
|
||||||
### Rofi Theme (Linux Only)
|
|
||||||
|
|
||||||
SubMiner ships a custom rofi theme bundled in the release assets tarball.
|
|
||||||
|
|
||||||
Install path (default auto-detected by `subminer`):
|
|
||||||
|
|
||||||
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
|
|
||||||
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
|
||||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
|
||||||
mkdir -p ~/.local/share/SubMiner/themes
|
|
||||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
|
||||||
```
|
|
||||||
|
|
||||||
Override with `SUBMINER_ROFI_THEME=/absolute/path/to/theme.rasi`.
|
|
||||||
|
|
||||||
Next: [Usage](/usage) — learn about the `subminer` wrapper, keybindings, and YouTube playback.
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
# IPC + Runtime Contracts
|
# IPC + runtime contracts
|
||||||
|
|
||||||
SubMiner's Electron app runs two isolated processes — main and renderer — that can only communicate through IPC channels. This boundary is intentional: the renderer is an untrusted surface (it loads Yomitan, renders user-controlled subtitle text, and runs in a Chromium sandbox), so every message crossing the bridge passes through a validation layer before it can reach domain logic.
|
The Electron main and renderer processes talk only through IPC channels. The renderer is an untrusted surface: it loads Yomitan and renders subtitle text SubMiner did not write. Every payload that crosses the bridge goes through a validator before domain code sees it.
|
||||||
|
|
||||||
The contract system enforces this by making channel names, payload shapes, and validators co-located and co-evolved. A change to any IPC surface touches the contract, the validator, the preload bridge, and the handler in the same commit — drift between any of those layers is treated as a bug.
|
Channel names, payload validators, the preload bridge, and the handler change together. When you touch an IPC surface, update all four in the same commit.
|
||||||
|
|
||||||
## Message Flow
|
## Message flow
|
||||||
|
|
||||||
Renderer-initiated calls (`invoke`) pass through four boundaries before reaching a service. Fire-and-forget messages (`send`) follow the same path but skip the response leg. Malformed payloads are caught at the validator and never reach domain code.
|
Renderer calls pass through the preload bridge, the main-process handler, and a validator before they reach a service. Malformed payloads stop at the validator.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
@@ -36,58 +36,79 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core Surfaces
|
`IPC_CHANNELS` in `src/shared/ipc/contracts.ts` groups channels by pattern:
|
||||||
|
|
||||||
| File | Role |
|
- `request`: invoke channels. The renderer awaits a result, for example lookups, config reads, and mining actions. Invalid payloads return a structured failure such as `{ ok: false, ... }` instead of throwing.
|
||||||
| --- | --- |
|
- `command`: fire-and-forget sends, for example focus events, UI state hints, and position updates. Invalid payloads are dropped.
|
||||||
| `src/shared/ipc/contracts.ts` | Canonical channel names and payload type contracts. Single source of truth for both processes. |
|
- `event`: messages pushed from main to the renderer.
|
||||||
| `src/shared/ipc/validators.ts` | Runtime payload parsers and type guards. Every `invoke` payload is validated here before the handler runs. |
|
|
||||||
| `src/preload.ts` | Renderer-side bridge. Exposes a typed API surface to the renderer — only approved channels are accessible. |
|
|
||||||
| `src/main/ipc-runtime.ts` | Main-process handler registration and routing. Wires validated channels to domain handlers. |
|
|
||||||
| `src/core/services/ipc.ts` | Service-level invoke handling. Applies guardrails (validation, error wrapping) before calling domain logic. |
|
|
||||||
| `src/core/services/anki-jimaku-ipc.ts` | Integration-specific IPC boundary for Anki and Jimaku operations. |
|
|
||||||
| `src/main/cli-runtime.ts` | CLI/runtime command boundary. Handles commands that originate from the launcher or mpv plugin rather than the renderer. |
|
|
||||||
|
|
||||||
## Contract Rules
|
## Runtime sockets
|
||||||
|
|
||||||
These rules exist to prevent a class of bugs where the renderer and main process silently disagree about message shapes — which surfaces as undefined fields, swallowed errors, or state corruption.
|
The bridge above lives inside the Electron app. Separate OS sockets connect the app to mpv and to the launcher and plugin. They carry no renderer payloads and do not go through the contract and validator layer.
|
||||||
|
|
||||||
- **Use shared constants.** Channel names come from `contracts.ts`, never ad-hoc literal strings. This makes channels greppable and refactor-safe.
|
- **mpv IPC socket**: `/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows. mpv creates it with `--input-ipc-server`. The app's `MpvIpcClient` sends JSON commands here and observes playback and subtitle properties.
|
||||||
- **Validate before handling.** Every `invoke` payload passes through `validators.ts` before reaching domain logic. This catches shape drift at the boundary instead of deep inside a service.
|
- **App control socket**: `subminer-control-<uid>-<hash>.sock` in the temp directory, or `\\.\pipe\subminer-control-<hash>` on Windows. The launcher and plugin send CLI-style commands (`--start`, `--show-visible-overlay`, `--texthooker`) to a running app here. It also routes a second `subminer` invocation into the existing instance.
|
||||||
- **Return structured failures.** Handlers return `{ ok: false, error: string }` on failure rather than throwing. The renderer can always distinguish success from failure without try/catch.
|
|
||||||
- **Keep payloads narrow.** Send only what the handler needs. Avoid passing entire state objects across the bridge — it couples the renderer to internal main-process structure.
|
|
||||||
- **Co-evolve all layers.** When a payload shape changes, update `contracts.ts`, `validators.ts`, `preload.ts`, and the handler in the same commit. Partial updates are treated as bugs.
|
|
||||||
|
|
||||||
## Two Message Patterns
|
```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
|
||||||
|
|
||||||
**Invoke (request/response):** The renderer calls a typed bridge method and awaits a result. The main process validates the payload, runs the handler, and returns a structured response. Used for operations where the renderer needs a result — lookups, config reads, mining actions.
|
subgraph MpvProc["mpv process"]
|
||||||
|
direction TB
|
||||||
|
Mpv["mpv core"]:::ext
|
||||||
|
Plugin["SubMiner plugin (Lua)"]:::extrt
|
||||||
|
end
|
||||||
|
|
||||||
**Fire-and-forget (send):** The renderer sends a message with no response. The main process validates and handles it silently. Malformed payloads are dropped. Used for notifications where the renderer doesn't need confirmation — UI state hints, focus events, position updates.
|
Launcher["Launcher CLI"]:::extrt
|
||||||
|
App["SubMiner app (Electron main)"]:::app
|
||||||
|
|
||||||
## Add a New IPC Action
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
[Playback startup flow](./architecture#playback-startup-flow) shows when each socket comes up during a launch.
|
||||||
|
|
||||||
|
## Core files
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| -------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||||
|
| `src/shared/ipc/contracts.ts` | Channel names and payload types, shared by both processes |
|
||||||
|
| `src/shared/ipc/validators.ts` | Runtime payload parsers and type guards |
|
||||||
|
| `src/preload.ts` | Typed renderer API; only approved channels are exposed |
|
||||||
|
| `src/core/services/ipc.ts` | Registers overlay handlers and validates payloads before calling domain logic |
|
||||||
|
| `src/core/services/anki-jimaku-ipc.ts` | Same boundary for Anki and Jimaku operations |
|
||||||
|
| `src/main/ipc-runtime.ts` | Builds handler dependencies (via `src/main/dependencies.ts`) and registers handlers |
|
||||||
|
| `src/main/cli-runtime.ts` | Handles commands from the launcher or mpv plugin, not the renderer |
|
||||||
|
|
||||||
|
## Contract rules
|
||||||
|
|
||||||
|
- **Use the shared constants.** Take channel names from `contracts.ts`, never string literals.
|
||||||
|
- **Validate before handling.** Every renderer payload goes through `validators.ts` before domain logic.
|
||||||
|
- **Return structured failures.** Invoke handlers return `{ ok: false, ... }` on failure instead of throwing, so the renderer can tell success from failure without try/catch.
|
||||||
|
- **Keep payloads narrow.** Send only what the handler needs, not whole state objects.
|
||||||
|
- **Keep handlers thin.** Validate, delegate to a service or composer, return. Route shared state changes through the transition helpers in `src/main/state.ts`.
|
||||||
|
|
||||||
|
## Add a new IPC action
|
||||||
|
|
||||||
1. Add the channel constant in `src/shared/ipc/contracts.ts`.
|
1. Add the channel constant in `src/shared/ipc/contracts.ts`.
|
||||||
2. Add or extend the payload validator in `src/shared/ipc/validators.ts`.
|
2. Add or extend the validator in `src/shared/ipc/validators.ts`.
|
||||||
3. Expose a typed bridge method in `src/preload.ts`.
|
3. Expose a typed bridge method in `src/preload.ts`.
|
||||||
4. Register the handler in `src/main/ipc-runtime.ts` (or the relevant domain runtime module).
|
4. Register the handler in `src/core/services/ipc.ts` (or `anki-jimaku-ipc.ts`), and supply any new dependency through `src/main/ipc-runtime.ts`.
|
||||||
5. Add tests for both valid and malformed payload cases in `src/core/services/*`.
|
5. Test valid and malformed payloads in `src/core/services/*`.
|
||||||
6. Update renderer tests when behavior or state transitions change.
|
6. Update renderer tests if behavior or state transitions change.
|
||||||
|
|
||||||
## Runtime State Notes
|
|
||||||
|
|
||||||
- Prefer runtime/domain composition via `src/main/runtime/composers/*` and `src/main/runtime/domains/*`. IPC handlers should delegate to composers rather than containing orchestration logic.
|
|
||||||
- Route shared mutable state updates through transition helpers in `src/main/state.ts` for migrated domains. Direct mutation from IPC handlers bypasses invariant checks.
|
|
||||||
- Keep IPC handlers thin — they validate, delegate, and return. Business logic belongs in services.
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- **Unknown payload in handler:** The validator is not being applied before the handler runs. Check that the channel is routed through `ipc-runtime.ts` with validation, not registered directly.
|
- **Handler receives an unexpected payload:** the validator is not applied. Check that the channel is registered through the IPC service with validation, not directly.
|
||||||
- **Renderer invoke fails:** Verify the preload bridge method exists and matches the channel constant. Check that the handler is registered and returning (not throwing).
|
- **Renderer invoke fails:** check that the preload method exists, uses the right channel constant, and that the handler is registered and returns instead of throwing.
|
||||||
- **Contract drift:** When invoke calls return unexpected shapes, compare the shared contract, validator, preload bridge, and main handler signatures side by side. One of them was updated without the others.
|
- **Invoke returns an unexpected shape:** compare the contract, validator, preload method, and handler side by side. One of them changed without the others.
|
||||||
|
|
||||||
## Related Docs
|
## Related docs
|
||||||
|
|
||||||
- [Architecture](/architecture)
|
- [Architecture](/architecture)
|
||||||
- [Development](/development)
|
- [Building and testing](/development)
|
||||||
- [Configuration](/configuration)
|
|
||||||
- [Troubleshooting](/troubleshooting)
|
|
||||||
@@ -1,185 +1,60 @@
|
|||||||
# Jellyfin Integration
|
# Jellyfin integration
|
||||||
|
|
||||||
SubMiner includes an optional Jellyfin CLI integration for:
|
If your anime lives on a [Jellyfin](https://jellyfin.org) server, SubMiner can appear as a cast target in any Jellyfin client. Cast an episode and it plays in SubMiner's mpv with the overlay and Yomitan lookup attached.
|
||||||
|
|
||||||
- authenticating against a server
|
|
||||||
- listing libraries and media items
|
|
||||||
- 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
|
|
||||||
- toggling Jellyfin cast discovery from the tray once configured
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Jellyfin server URL and user credentials
|
|
||||||
- For `--jellyfin-play`: connected mpv IPC socket (`--start` or existing mpv plugin workflow)
|
|
||||||
- On Linux, token encryption defaults to `gnome-libsecret`; pass `--password-store=<backend>` to override.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. Set base config values (`config.jsonc`):
|
You need a Jellyfin server (Jellyfin 12 is supported) and your username and password.
|
||||||
|
|
||||||
```jsonc
|
1. Start SubMiner and leave it in the system tray.
|
||||||
{
|
2. Open the tray menu and click **Configure Jellyfin**. You can also run `subminer jellyfin`.
|
||||||
"jellyfin": {
|
3. Enter the **Server URL** (for example `http://127.0.0.1:8096`), **Username**, and **Password**, then click **Login**.
|
||||||
"enabled": true,
|
|
||||||
"serverUrl": "http://127.0.0.1:8096",
|
|
||||||
"recentServers": ["http://127.0.0.1:8096"],
|
|
||||||
"username": "your-user",
|
|
||||||
"remoteControlEnabled": 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:
|
SubMiner stores an encrypted session token, not your password, and turns the integration on. Reopen the same window to switch servers or log out.
|
||||||
|
|
||||||
|
## Casting from Jellyfin
|
||||||
|
|
||||||
|
After you sign in, SubMiner connects to Jellyfin at startup and shows up in the cast ("Play on") menu under your computer's hostname. To connect for the current session only, tick **Jellyfin Discovery** in the tray menu.
|
||||||
|
|
||||||
|
1. In the Jellyfin web or mobile app, start playing an episode.
|
||||||
|
2. Open the cast menu and pick your computer.
|
||||||
|
|
||||||
|
SubMiner starts mpv if it is not already running. Pause, seek, stop, and track changes in the Jellyfin app are mirrored in mpv, and watch progress syncs back to Jellyfin. Playback resumes from Jellyfin's saved position.
|
||||||
|
|
||||||
|
SubMiner selects a Japanese subtitle track automatically and resets mpv's subtitle delay to zero. It direct-plays files when it can and asks Jellyfin to transcode the rest.
|
||||||
|
|
||||||
|
On Windows, casting finds mpv through `mpv.executablePath`, then `SUBMINER_MPV_PATH`, then `PATH`. An invalid `mpv.executablePath` stops mpv from starting.
|
||||||
|
|
||||||
|
## Playing from the terminal
|
||||||
|
|
||||||
|
The launcher can browse your libraries and play an item without a Jellyfin client:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer jellyfin
|
subminer jellyfin -p # fzf picker; `jf` is an alias for `jellyfin`
|
||||||
subminer jellyfin -l \
|
subminer -R jellyfin -p # rofi picker
|
||||||
--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.
|
Sign in first. See [Launcher script](/launcher-script) for the other `jellyfin` subcommands.
|
||||||
|
|
||||||
3. List libraries:
|
## Options
|
||||||
|
|
||||||
```bash
|
All options are under **Settings > Integrations > Jellyfin**, or `jellyfin` in `config.jsonc`. See [Configuration](/configuration#jellyfin) for the full list and defaults.
|
||||||
SubMiner.AppImage --jellyfin-libraries
|
|
||||||
```
|
|
||||||
|
|
||||||
Launcher wrapper equivalent for interactive playback flow:
|
| Key | What it does |
|
||||||
|
| -------------------------- | -------------------------------------------------------------------- |
|
||||||
|
| `enabled` | Turns the integration on. Set for you when you sign in. |
|
||||||
|
| `serverUrl` | Your Jellyfin server. Filled in when you sign in. |
|
||||||
|
| `remoteControlEnabled` | Lets SubMiner act as a cast target. |
|
||||||
|
| `remoteControlAutoConnect` | Connects at startup. Turn off to start discovery from the tray. |
|
||||||
|
| `autoAnnounce` | Re-announces the device on connect. Try it if SubMiner appears late. |
|
||||||
|
| `transcodeVideoCodec` | Video codec requested when Jellyfin transcodes. |
|
||||||
|
|
||||||
```bash
|
For headless setups, `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID` supply a session without the sign-in window. Treat the token store and `config.jsonc` as secrets.
|
||||||
subminer jellyfin -p
|
|
||||||
```
|
|
||||||
|
|
||||||
Launcher wrapper for Jellyfin cast discovery mode (background app + tray):
|
## Troubleshooting
|
||||||
|
|
||||||
```bash
|
**SubMiner is missing from the cast menu.** Check that SubMiner is running, that you are signed in (log in again if the token expired), and that discovery is on. The Jellyfin client and SubMiner must use the same server.
|
||||||
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.
|
**Casting starts but nothing plays.** Confirm the item plays in another Jellyfin client. If mpv was closed, give SubMiner a few seconds to start it.
|
||||||
|
|
||||||
Stop discovery session/app:
|
**Linux token storage fails.** SubMiner stores the token with `gnome-libsecret` by default. Start your keyring, or pass `--password-store=basic_text`.
|
||||||
|
|
||||||
```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,111 +1,62 @@
|
|||||||
# 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 archive of Japanese subtitles for anime and live action. SubMiner searches it from the overlay, downloads the file you pick, and loads it into mpv.
|
||||||
|
|
||||||
## How It Works
|
## Setup
|
||||||
|
|
||||||
The Jimaku integration runs through an in-overlay modal accessible via a keyboard shortcut (`Ctrl+Shift+J` by default).
|
1. Create a free account at [jimaku.cc](https://jimaku.cc) and copy your API key.
|
||||||
|
2. Add the key to `config.jsonc`, either directly or through a command that prints it:
|
||||||
When you open the modal, SubMiner parses the current video filename to extract a title, season, and episode number. Common naming conventions are supported — `S01E03`, `1x03`, `E03`, and dash-separated episode numbers all work. If the filename yields a high-confidence match (title + episode), SubMiner auto-searches immediately.
|
|
||||||
|
|
||||||
From there:
|
|
||||||
|
|
||||||
1. **Search** — SubMiner queries the Jimaku API with the parsed title. Results appear as a list of anime entries (Japanese and English names).
|
|
||||||
2. **Browse entries** — Select an entry to load its available subtitle files, filtered by episode if one was detected.
|
|
||||||
3. **Browse files** — Files show name, size, and last-modified date. If a language preference is configured, files are sorted accordingly (e.g., Japanese-tagged files first).
|
|
||||||
4. **Download** — Selecting a file downloads it to the same directory as the video (or a temp directory for remote/streamed media) and loads it into mpv as a new subtitle track.
|
|
||||||
|
|
||||||
If no files match the current episode filter, a "Show all files" button lets you broaden the search to all episodes for that entry.
|
|
||||||
|
|
||||||
### Modal Keyboard Shortcuts
|
|
||||||
|
|
||||||
| Key | Action |
|
|
||||||
| --- | --- |
|
|
||||||
| `Enter` (in text field) | Search |
|
|
||||||
| `Enter` (in list) | Select entry / download file |
|
|
||||||
| `Arrow Up` / `Arrow Down` | Navigate entries or files |
|
|
||||||
| `Escape` | Close modal |
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Add a `jimaku` section to your `config.jsonc`:
|
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"jimaku": {
|
"jimaku": {
|
||||||
"apiKey": "YOUR_API_KEY",
|
"apiKey": "YOUR_API_KEY",
|
||||||
"apiKeyCommand": "cat ~/.jimaku_key",
|
// or, to keep it out of the config file:
|
||||||
"apiBaseUrl": "https://jimaku.cc",
|
// "apiKeyCommand": "pass jimaku/api-key",
|
||||||
"languagePreference": "ja",
|
},
|
||||||
"maxEntryResults": 10
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
If both are set, `apiKey` wins. `apiKeyCommand` must print the key within 10 seconds. Without a key, the modal shows "Jimaku API key not set."
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `jimaku.apiKey` | `string` | — | Jimaku API key (plaintext). Mutually exclusive with `apiKeyCommand`. |
|
|
||||||
| `jimaku.apiKeyCommand` | `string` | — | Shell command that prints the API key to stdout. Useful for secret managers (e.g., `pass jimaku/api-key`). |
|
|
||||||
| `jimaku.apiBaseUrl` | `string` | `"https://jimaku.cc"` | Base URL for the Jimaku API. Only change this if using a mirror or local instance. |
|
|
||||||
| `jimaku.languagePreference` | `"ja"` \| `"en"` \| `"none"` | `"ja"` | Sort subtitle files by language tag. `"ja"` pushes Japanese-tagged files to the top; `"en"` does the same for English. `"none"` preserves the API order. |
|
|
||||||
| `jimaku.maxEntryResults` | `number` | `10` | Maximum number of anime entries returned per search. |
|
|
||||||
|
|
||||||
The keyboard shortcut is configured separately under `shortcuts`:
|
## Usage
|
||||||
|
|
||||||
```jsonc
|
1. Press `Ctrl+Shift+J` during playback.
|
||||||
{
|
2. SubMiner fills in the title, season, and episode from the file name. If it finds both a title and an episode, it searches right away. Otherwise, fix the fields and press `Enter`.
|
||||||
"shortcuts": {
|
3. Pick the **Anime** or **Live action** tab. Switching tabs repeats the search.
|
||||||
"openJimaku": "Ctrl+Shift+J"
|
4. Select an entry, then select a file. Files are filtered to the current episode. Click **Broaden search (all files)** to see every file in the entry.
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Key
|
The file is saved next to the video (or to a temp directory for streams) and loaded into mpv as a new subtitle track.
|
||||||
|
|
||||||
An API key is required to use the Jimaku integration. You can get one from [jimaku.cc](https://jimaku.cc). There are two ways to provide it:
|
| Key | Action |
|
||||||
|
| ---------------- | -------------------------------------- |
|
||||||
|
| `Enter` | Search, or select the highlighted item |
|
||||||
|
| `Up` / `Down` | Move through entries or files |
|
||||||
|
| `Left` / `Right` | Switch tabs |
|
||||||
|
| `Escape` | Close |
|
||||||
|
|
||||||
- **`apiKey`** — set the key directly in config. Simple, but the key is stored in plaintext.
|
You can also open the modal with `subminer app --open-jimaku`, or change the shortcut with `shortcuts.openJimaku`.
|
||||||
- **`apiKeyCommand`** — a shell command that outputs the key. Runs with a 10-second timeout. Preferred if you use a secret manager like `pass`, `gpg`, or a keychain tool.
|
|
||||||
|
|
||||||
If both are set, `apiKey` takes priority.
|
The file name parser understands `S01E03`, `1x03`, `E03`, `EP03`, and `Title - 03 -` patterns, and reads the season from a parent folder such as `Season 2`. It ignores bracket tags like `[SubGroup]` and year tags like `(2024)`.
|
||||||
|
|
||||||
## Filename Parsing
|
## Options
|
||||||
|
|
||||||
SubMiner extracts media info from the current video path to pre-fill the search fields. The parser handles:
|
| Key | What it does |
|
||||||
|
| --------------------------- | ------------------------------------------------------------------- |
|
||||||
|
| `jimaku.apiKey` | API key in plain text. |
|
||||||
|
| `jimaku.apiKeyCommand` | Shell command that prints the API key. |
|
||||||
|
| `jimaku.languagePreference` | Sorts files tagged with this language first: `ja`, `en`, or `none`. |
|
||||||
|
| `jimaku.maxEntryResults` | Maximum entries per search. |
|
||||||
|
| `jimaku.apiBaseUrl` | API address. Change only for a mirror. |
|
||||||
|
|
||||||
- **Season + episode patterns:** `S01E03`, `1x03`
|
See [Configuration](/configuration#jimaku) for defaults.
|
||||||
- **Episode-only patterns:** `E03`, `EP03`, or dash-separated numbers like `Title - 03 -`
|
|
||||||
- **Bracket tags:** `[SubGroup]`, `[1080p]`, `[HEVC]` — stripped before title extraction
|
|
||||||
- **Year tags:** `(2024)` — stripped
|
|
||||||
- **Dots and underscores:** treated as spaces
|
|
||||||
- **Remote/streamed URLs:** SubMiner checks URL query parameters (`title`, `name`, `q`) and path segments to extract a meaningful title
|
|
||||||
|
|
||||||
If the parser produces a high-confidence result (title + episode both detected), the search runs automatically when the modal opens. Otherwise, you can adjust the fields manually before searching.
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**"Jimaku API key not set"**
|
**"Jimaku API key not set."** Set `jimaku.apiKey` or `jimaku.apiKeyCommand`. Run the command in your shell to confirm it prints only the key.
|
||||||
|
|
||||||
Configure `jimaku.apiKey` or `jimaku.apiKeyCommand` in your config. If using `apiKeyCommand`, verify the command works in your shell: it should print the key and exit cleanly.
|
**HTTP 429.** You hit Jimaku's rate limit. Wait for the time shown in the message and retry.
|
||||||
|
|
||||||
**"Jimaku request failed" or HTTP 429**
|
**No entries found.** Search with just the show's name, without season or episode words. Jimaku matches against its own titles.
|
||||||
|
|
||||||
The Jimaku API has rate limits. If you see 429 errors, wait for the retry duration shown in the OSD message and try again. An API key provides higher rate limits.
|
**The subtitle downloads but does not load.** SubMiner loads it over the mpv socket. Make sure mpv is still running and connected.
|
||||||
|
|
||||||
**No entries found**
|
|
||||||
|
|
||||||
Try simplifying the title — remove season/episode qualifiers and search with just the anime name. Jimaku's search matches against its own database of anime titles, so the exact spelling matters.
|
|
||||||
|
|
||||||
**No files found for this episode**
|
|
||||||
|
|
||||||
The entry may not have per-episode files, or files may be named differently. Click "Show all files" to see everything available for the entry.
|
|
||||||
|
|
||||||
**Downloaded subtitle not loading**
|
|
||||||
|
|
||||||
Verify mpv is running and connected via IPC. SubMiner loads the subtitle by issuing a `sub-add` command over the mpv socket. If mpv is not connected, the download succeeds but the subtitle cannot be loaded.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- [Configuration Reference](/configuration#jimaku) — full config options
|
|
||||||
- [Mining Workflow](/mining-workflow#jimaku-subtitle-search) — how Jimaku fits into the sentence mining loop
|
|
||||||
- [Troubleshooting](/troubleshooting#jimaku) — additional error guidance
|
|
||||||
@@ -1,120 +1,196 @@
|
|||||||
# Launcher Script
|
# Launcher script
|
||||||
|
|
||||||
The `subminer` launcher is an all-in-one script that handles video selection, mpv startup, and overlay management. It is the recommended way to use SubMiner on Linux and macOS because it guarantees mpv is launched with the correct IPC socket and SubMiner defaults. It's a Bun script distributed as a release asset alongside the AppImage and DMG.
|
`subminer` is the command-line entry point for SubMiner. It starts mpv with the socket and options SubMiner needs, opens file pickers, and runs helper commands. This page is the reference for its subcommands and flags. For everyday use, start with [Usage](/usage).
|
||||||
|
|
||||||
::: tip Windows users
|
You do not need Bun or anything else installed to run it. It uses the runtime bundled with the app. On Windows, the **SubMiner mpv** shortcut is the simpler way to play files (see [Windows mpv shortcut](/usage#windows-mpv-shortcut)).
|
||||||
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
|
|
||||||
|
|
||||||
When you run `subminer` without specifying a file, it opens an interactive video picker. By default it uses **fzf** in the terminal; pass `-R` to use **rofi** instead.
|
|
||||||
|
|
||||||
### fzf (default)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer # pick from current directory
|
subminer [options] [file | directory | URL]
|
||||||
subminer -d ~/Videos # pick from a specific directory
|
subminer <subcommand> [options]
|
||||||
subminer -r -d ~/Anime # recursive search
|
|
||||||
```
|
```
|
||||||
|
|
||||||
fzf shows video files in a fuzzy-searchable list. If `chafa` is installed, you get thumbnail previews in the right pane. Thumbnails are sourced from the freedesktop thumbnail cache first, then generated on the fly with `ffmpegthumbnailer` or `ffmpeg` as fallback.
|
Run `subminer -h` or `subminer <subcommand> -h` for built-in help.
|
||||||
|
|
||||||
| Optional tool | Purpose |
|
|
||||||
| ------------------- | --------------------------------- |
|
|
||||||
| `chafa` | Render thumbnails in the terminal |
|
|
||||||
| `ffmpegthumbnailer` | Generate thumbnails on the fly |
|
|
||||||
|
|
||||||
### rofi
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer -R # rofi picker, current directory
|
|
||||||
subminer -R -d ~/Videos # rofi picker, specific directory
|
|
||||||
subminer -R -r -d ~/Anime # rofi picker, recursive
|
|
||||||
subminer -R /directory # rofi picker, directory shortcut
|
|
||||||
```
|
|
||||||
|
|
||||||
rofi shows a GUI menu with icon thumbnails when available. SubMiner ships a custom rofi theme bundled in the release assets tarball:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
|
||||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
|
||||||
mkdir -p ~/.local/share/SubMiner/themes
|
|
||||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
|
||||||
```
|
|
||||||
|
|
||||||
The theme is auto-detected from these paths (first match wins):
|
|
||||||
|
|
||||||
- `$SUBMINER_ROFI_THEME` environment variable (absolute path)
|
|
||||||
- `$XDG_DATA_HOME/SubMiner/themes/subminer.rasi` (default: `~/.local/share/SubMiner/themes/subminer.rasi`)
|
|
||||||
- `/usr/local/share/SubMiner/themes/subminer.rasi`
|
|
||||||
- `/usr/share/SubMiner/themes/subminer.rasi`
|
|
||||||
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
|
|
||||||
|
|
||||||
Override with the `SUBMINER_ROFI_THEME` environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SUBMINER_ROFI_THEME=/path/to/custom-theme.rasi subminer -R
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
subminer video.mkv # play a specific file (default plugin config auto-starts visible overlay)
|
|
||||||
subminer https://youtu.be/... # YouTube playback (requires yt-dlp)
|
|
||||||
subminer --backend x11 video.mkv # Force x11 backend for a specific file
|
|
||||||
subminer -u # check for SubMiner updates
|
|
||||||
subminer stats # open immersion dashboard
|
|
||||||
subminer stats -b # start background stats daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
## Subcommands
|
|
||||||
|
|
||||||
| Subcommand | Purpose |
|
|
||||||
| ------------------------------------------ | ------------------------------------------------------------------ |
|
|
||||||
| `subminer jellyfin` / `jf` | Jellyfin workflows (`-d` discovery, `-p` play, `-l` login) |
|
|
||||||
| `subminer stats` | Start stats server and open immersion dashboard in browser |
|
|
||||||
| `subminer stats -b` | Start or reuse background stats daemon (non-blocking) |
|
|
||||||
| `subminer stats cleanup` | Backfill vocabulary metadata and prune stale rows |
|
|
||||||
| `subminer doctor` | Dependency + config + socket diagnostics |
|
|
||||||
| `subminer config path` | Print active config file path |
|
|
||||||
| `subminer config show` | Print active config contents |
|
|
||||||
| `subminer mpv status` | Check mpv socket readiness |
|
|
||||||
| `subminer mpv socket` | Print active socket path |
|
|
||||||
| `subminer mpv idle` | Launch detached idle mpv instance |
|
|
||||||
| `subminer dictionary <path>` | Generate character dictionary ZIP from file/dir target |
|
|
||||||
| `subminer dictionary --candidates <path>` | List AniList candidate matches for character dictionary correction |
|
|
||||||
| `subminer dictionary --select <id> <path>` | Pin an AniList media ID for that target series |
|
|
||||||
| `subminer texthooker` | Launch texthooker-only mode |
|
|
||||||
| `subminer texthooker -o` | Launch texthooker and open it in the default browser |
|
|
||||||
| `subminer app` | Pass arguments directly to SubMiner binary |
|
|
||||||
|
|
||||||
Use `subminer <subcommand> -h` for command-specific help.
|
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
| --------------------- | -------------------------------------------------------------------- |
|
| --------------------- | ----------------------------------------------------------------------------------- |
|
||||||
| `-d, --directory` | Video search directory (default: cwd) |
|
| `-d, --directory` | Directory to browse (default: current directory) |
|
||||||
| `-r, --recursive` | Search directories recursively |
|
| `-r, --recursive` | Search subdirectories |
|
||||||
| `-R, --rofi` | Use rofi instead of fzf |
|
| `-R, --rofi` | Use rofi instead of fzf |
|
||||||
| `--setup` | Open first-run setup popup manually |
|
| `-H, --history` | Browse [watch history](#watch-history) |
|
||||||
| `-u, --update` | Check for SubMiner updates and update the app/launcher when possible |
|
| `-b, --backend` | Window backend: `auto`, `hyprland`, `sway`, `x11`, `macos`, `windows` |
|
||||||
| `--start` | Explicitly start overlay after mpv launches |
|
| `-p, --profile` | mpv profile to load |
|
||||||
| `-S, --start-overlay` | Explicitly start overlay after mpv launches |
|
| `-a, --args` | Extra mpv options as one quoted string, e.g. `--args "--volume=80"` |
|
||||||
| `-T, --no-texthooker` | Disable texthooker server |
|
| `--start` | Start the overlay after mpv launches. Only needed if `mpv.autoStartSubMiner` is off |
|
||||||
| `-p, --profile` | mpv profile name (no default; omitted unless set) |
|
| `-S, --start-overlay` | Show the overlay on start |
|
||||||
| `-a, --args` | Pass additional mpv arguments as a quoted string |
|
| `-T, --no-texthooker` | Do not start the texthooker server |
|
||||||
| `-b, --backend` | Force window backend (`hyprland`, `sway`, `x11`, `macos`, `windows`) |
|
| `--settings` | Open the settings window |
|
||||||
| `--log-level` | Logger verbosity (`debug`, `info`, `warn`, `error`) |
|
| `--log-level` | `debug`, `info`, `warn`, or `error` |
|
||||||
| `--dev`, `--debug` | Enable app dev-mode (not tied to log level) |
|
| `-u, --update` | Check for and install updates |
|
||||||
|
| `-v, --version` | Print the launcher's version |
|
||||||
|
|
||||||
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.
|
The target can be a video file, a directory (opens the picker there), a URL, or `ytsearch:"query"` for the first YouTube search result.
|
||||||
|
|
||||||
With default plugin settings (`auto_start=yes`, `auto_start_visible_overlay=yes`, `auto_start_pause_until_ready=yes`), explicit start flags are usually unnecessary.
|
App flags such as `--setup` and `--dev` are not launcher flags. Pass them through with `subminer app`, for example `subminer app --setup`.
|
||||||
|
|
||||||
|
## Subcommands
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `subminer stats` | Start the stats dashboard server. Opens your browser if `stats.autoOpenBrowser` is on |
|
||||||
|
| `subminer stats -b` / `-s` | Start (or reuse) the stats server in the background / stop it |
|
||||||
|
| `subminer stats cleanup` | Backfill vocabulary metadata and prune stale rows (same as `-v`) |
|
||||||
|
| `subminer stats cleanup -l` | Rebuild lifetime totals from the retained sessions |
|
||||||
|
| `subminer stats cleanup -d` | Collapse repeated lines from typeset subtitles. Add `--dry-run` to preview, `--lookback-days <n>` to limit the range |
|
||||||
|
| `subminer stats rebuild` / `backfill` | Same as `stats cleanup -l` |
|
||||||
|
| `subminer sync <host>` | Sync stats and watch history with another machine. See [below](#sync-between-machines) |
|
||||||
|
| `subminer doctor` | Check the app, mpv, ffmpeg, yt-dlp, pickers, config, and mpv socket |
|
||||||
|
| `subminer doctor --refresh-known-words` | Refresh the known-word cache from Anki |
|
||||||
|
| `subminer settings` | Open the settings window |
|
||||||
|
| `subminer youtube` / `yt` | Open the [YouTube browser](/youtube-integration#browse-youtube-in-subminer). Videos you pick play in mpv |
|
||||||
|
| `subminer generate-subs [video]` | Generate [Japanese subtitles](/subtitle-generation) with whisper.cpp |
|
||||||
|
| `subminer jellyfin` / `jf` | [Jellyfin](/jellyfin-integration) actions: `setup`, `login`, `logout`, `play`, `discovery` |
|
||||||
|
| `subminer dictionary <path>` / `dict` | Build a [character dictionary](/character-dictionary) for a file or directory |
|
||||||
|
| `subminer dictionary --candidates <path>` | List AniList matches for that target |
|
||||||
|
| `subminer dictionary --select <id> <path>` | Pin an AniList ID for that target |
|
||||||
|
| `subminer texthooker` | Run only the texthooker server. `-o` opens it in your browser |
|
||||||
|
| `subminer logs -e` | Export a sanitized log ZIP and print its path |
|
||||||
|
| `subminer config path` / `show` | Print the config file path or its contents |
|
||||||
|
| `subminer mpv status` | Exit 0 if the mpv socket is ready, 1 if not |
|
||||||
|
| `subminer mpv socket` | Print the mpv socket path |
|
||||||
|
| `subminer mpv idle` | Start an idle mpv in the background with SubMiner's options |
|
||||||
|
| `subminer app` / `bin` | Pass arguments to the SubMiner app, e.g. `subminer app --stop` |
|
||||||
|
|
||||||
|
`stats cleanup` runs one mode at a time. `--lookback-days` must be at least 1. Without it, cleanup scans all history.
|
||||||
|
|
||||||
|
`generate-subs` options: `--download-model`, `--model <name>`, `--model-path <file>`, `--output <file>`, and `--audio-stream <index>` (an ffprobe stream index). `--model-path` cannot be combined with `--model` or `--download-model`.
|
||||||
|
|
||||||
|
A texthooker is a web page that shows the current subtitle as plain text, so browser extensions and other tools can read along.
|
||||||
|
|
||||||
|
## Video picker
|
||||||
|
|
||||||
|
With no file argument, `subminer` opens a picker for the current directory, or for `-d <dir>`. Add `-r` to include subdirectories.
|
||||||
|
|
||||||
|
- **fzf** (default) runs in the terminal. With `chafa` installed, it shows thumbnail previews.
|
||||||
|
- **rofi** (`-R`, Linux) opens a graphical menu with thumbnails.
|
||||||
|
|
||||||
|
Thumbnails come from your system thumbnail cache, or are generated with `ffmpegthumbnailer` or `ffmpeg`.
|
||||||
|
|
||||||
|
The launcher installs its rofi theme automatically. To use your own, set `SUBMINER_ROFI_THEME`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SUBMINER_ROFI_THEME=/path/to/theme.rasi subminer -R
|
||||||
|
```
|
||||||
|
|
||||||
|
## Watch history
|
||||||
|
|
||||||
|
`subminer -H` lists the shows you have watched, most recent first. Add `-R` to use rofi. Pick a show, then choose:
|
||||||
|
|
||||||
|
- **Previous episode** or **Next episode**, moving into the neighboring season folder when needed
|
||||||
|
- **Replay last watched**
|
||||||
|
- **Browse episodes**, with a season menu first if the show has several season folders
|
||||||
|
- **Quit SubMiner**
|
||||||
|
|
||||||
|
When an episode ends, the menu comes back for the same show. Press `Escape` to leave.
|
||||||
|
|
||||||
|
History comes from the immersion stats database, which SubMiner fills during playback. Shows whose folders are not reachable, such as an unmounted network drive, are hidden.
|
||||||
|
|
||||||
|
## mpv options and profiles
|
||||||
|
|
||||||
|
The launcher starts mpv with these options:
|
||||||
|
|
||||||
|
```
|
||||||
|
--input-ipc-server=/tmp/subminer-socket
|
||||||
|
--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
|
||||||
|
--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
|
||||||
|
--sub-auto=fuzzy
|
||||||
|
--sub-file-paths=.;subs;subtitles
|
||||||
|
--sid=auto
|
||||||
|
--secondary-sid=auto
|
||||||
|
--sub-visibility=no
|
||||||
|
--secondary-sub-visibility=no
|
||||||
|
```
|
||||||
|
|
||||||
|
mpv's own subtitles are hidden because the overlay draws them. Add more options with `-a`, or load an mpv profile with `-p <name>` or `mpv.profile` in the config. No profile is loaded by default.
|
||||||
|
|
||||||
|
To launch mpv yourself with the same setup, put the options in a profile in `~/.config/mpv/mpv.conf`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[subminer]
|
||||||
|
input-ipc-server=/tmp/subminer-socket
|
||||||
|
alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
|
||||||
|
slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us
|
||||||
|
sub-auto=fuzzy
|
||||||
|
sub-file-paths=.;subs;subtitles
|
||||||
|
sid=auto
|
||||||
|
secondary-sid=auto
|
||||||
|
secondary-sub-visibility=no
|
||||||
|
```
|
||||||
|
|
||||||
|
Launches through `subminer` start the overlay automatically unless `mpv.autoStartSubMiner` is off. mpv started outside SubMiner does not start the overlay on its own.
|
||||||
|
|
||||||
|
## Sync between machines
|
||||||
|
|
||||||
|
`subminer sync <host>` merges immersion stats and watch history between two computers over SSH. Both end up with the combined sessions, totals, vocabulary, charts, and `-H` history. `<host>` is anything `ssh` accepts, such as `user@hostname` or an alias from your SSH config.
|
||||||
|
|
||||||
|
Both machines need the same SubMiner version. The remote only needs the app. The `subminer` command is optional there.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer sync macbook # two-way sync
|
||||||
|
subminer sync macbook --push # send local data to macbook only
|
||||||
|
subminer sync macbook --pull # bring macbook data here only
|
||||||
|
subminer sync macbook --check # test SSH and the remote install, change nothing
|
||||||
|
subminer sync macbook --remote-cmd ~/Apps/SubMiner.AppImage # SubMiner in a custom place on the remote
|
||||||
|
subminer sync --ui # open the sync window
|
||||||
|
```
|
||||||
|
|
||||||
|
Syncing only adds data. It never overwrites or double-counts, so you can run it as often as you like. `--push` and `--pull` do not delete anything on the receiving side.
|
||||||
|
|
||||||
|
Before a command-line sync, close SubMiner on both machines and stop the stats server with `subminer stats -s`, or pass `--force`. The sync window does not need this. It syncs while SubMiner and playback are running, and skips the session in progress until it finishes.
|
||||||
|
|
||||||
|
Transfers are compressed. When both machines have `rsync` (macOS and Linux), later syncs send only what changed. Windows machines use `scp`.
|
||||||
|
|
||||||
|
Known-word status from Anki does not sync. Each machine reads it from its own Anki collection.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>More sync options</b></summary>
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| ------------------- | ----------------------------------------------------------- |
|
||||||
|
| `-f, --force` | Skip the check that SubMiner is closed |
|
||||||
|
| `--db <file>` | Use a different local stats database |
|
||||||
|
| `--json` | Print progress as NDJSON |
|
||||||
|
| `--snapshot <file>` | Write a snapshot of the local database, e.g. to copy by USB |
|
||||||
|
| `--merge <file>` | Merge a snapshot file into the local database |
|
||||||
|
|
||||||
|
A Windows remote needs the built-in **OpenSSH Server** enabled. SubMiner finds itself in the default install location there.
|
||||||
|
|
||||||
|
If the remote cannot find SubMiner, point `--remote-cmd` at the app or launcher, or link it as `SubMiner` somewhere on the remote `PATH`.
|
||||||
|
|
||||||
|
Received snapshots are cached in `sync-transfer-cache/` in the config directory to speed up later syncs. Deleting it is safe.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Sync window
|
||||||
|
|
||||||
|
`subminer sync --ui`, or **Sync Stats & History** in the tray, opens a window where you can:
|
||||||
|
|
||||||
|
- Save devices, each with a direction (two-way, push, or pull), and run **Sync now** or **Test**.
|
||||||
|
- Turn on **Auto-sync** for a device. It syncs in the background every 60 minutes by default, including during playback.
|
||||||
|
- Watch progress and see what was merged on each machine.
|
||||||
|
- Create, merge, or delete database snapshots.
|
||||||
|
|
||||||
|
Saved devices live in `sync-hosts.json` in the config directory.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Use |
|
||||||
|
| ------------------------ | --------------------------------------------------------------------- |
|
||||||
|
| `SUBMINER_BINARY_PATH` | Path to the SubMiner app, if it is not in a standard install location |
|
||||||
|
| `SUBMINER_APPIMAGE_PATH` | Same, for an AppImage (Linux) |
|
||||||
|
| `SUBMINER_ROFI_THEME` | Path to a custom rofi theme |
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
- Default log level is `info`
|
The default log level is `warn`. Change it for one run with `--log-level`, or permanently with `logging.level` in the config. The app's `--dev` and `--debug` flags turn on developer mode. They do not change the log level.
|
||||||
- `--background` mode defaults to `warn` unless `--log-level` is explicitly set
|
|
||||||
- `--dev` / `--debug` control app behavior, not logging verbosity — use `--log-level` for that
|
|
||||||