feat(launcher): add subminer sync for cross-machine stats/history merge over SSH (#152)

This commit is contained in:
2026-07-11 00:40:15 -07:00
committed by GitHub
parent beeab564b4
commit 321461c50f
27 changed files with 3707 additions and 1 deletions
+30
View File
@@ -78,6 +78,33 @@ The first menu lists every locally watched series, most recently watched first,
Series whose directories are not currently accessible (e.g. an unmounted network share) are hidden from the list. Watch history requires the immersion tracker database (`immersionTracking.dbPath`, default `<config dir>/immersion.sqlite`), which SubMiner populates during playback.
## Sync Between Machines
`subminer sync <host>` merges immersion stats and watch history between two machines over SSH, so both end up with the union of sessions, lifetime totals, vocabulary counts, daily/monthly charts, and `--history` entries. `<host>` is anything `ssh` accepts (`user@hostname` or an ssh config alias); SubMiner must be installed on both machines at the same version.
```bash
subminer sync macbook # two-way sync with the host "macbook"
subminer sync macbook --push # merge local data into macbook only
subminer sync macbook --pull # merge macbook data into local only
subminer sync user@192.168.1.20 # explicit user@host
subminer sync macbook --remote-cmd ~/bin/subminer # custom remote launcher path
```
How it works: each side takes a consistent snapshot of its database (`VACUUM INTO`), the snapshots are exchanged over `scp`, and each machine merges the other's snapshot into its own database. The merge is an insert-only union keyed on stable identifiers (session UUIDs, video keys, series title keys, word/kanji identity), so it is safe to re-run at any time — syncing twice changes nothing, and nothing is ever overwritten or summed twice. Lifetime totals and rollup charts are updated incrementally, so history older than the session retention window is preserved on both sides.
For a one-way transfer, `--push` snapshots the local database and merges it into the host without changing the local database. `--pull` snapshots the host and merges it into the local database without changing the host. These modes add missing data; they do not delete destination-only data or make the destination an exact mirror.
Close SubMiner (and stop the background stats daemon, `subminer stats -s`) on both machines before syncing; the command refuses to run while a SubMiner process may be writing the database (`--force` overrides). The mpv safety check requires a live socket connection, so a stale socket file left after mpv exits does not block sync. Both machines must be on the same SubMiner version — the sync aborts on a stats schema mismatch. Remote sync checks standard SubMiner and Bun locations (`~/.local/bin`, `~/.bun/bin`, Homebrew, `/usr/local/bin`, `/usr/bin`, and `/bin`) even when the non-interactive SSH shell omits them from `PATH`.
Two lower-level modes are used internally over SSH and also work standalone for manual transfers (e.g. via a USB drive):
```bash
subminer sync --snapshot /tmp/stats.sqlite # write a consistent snapshot of the local database
subminer sync --merge /tmp/stats.sqlite # merge a snapshot file into the local database
```
Unfinished sessions (a crash mid-playback) are skipped until the app finalizes them; they sync on the next run. Word/kanji "known" state from Anki is not part of the database and does not sync — each machine derives it from its own Anki collection.
## Common Commands
```bash
@@ -106,6 +133,9 @@ subminer stats -b # start background stats daemon
| `subminer mpv status` | Check mpv socket readiness |
| `subminer mpv socket` | Print active socket path |
| `subminer mpv idle` | Launch detached idle mpv instance |
| `subminer sync <host>` | Two-way stats/history sync with another machine over SSH |
| `subminer sync <host> --push` | Merge local stats/history into another machine only |
| `subminer sync <host> --pull` | Merge another machine's stats/history into the local database only |
| `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 |