- subtitles.current() maps playback time onto the subtitle clock so retimed and repeated lines resolve to the right cue
- Add e2e coverage for repeated lines under sub-delay
- Document the timing behavior and update the changelog fragment
- Bump version to 0.21.1 and add changelog and release notes
- Follow mpv sub-delay in mining, sidebar, navigation, and replay
- Redesign the expanded stats session view with a reading density chart
- Default ankiConnect.fields.audio to SentenceAudio
- Add missing AniList entries on exact or pinned matches
- Keep the AppImage mount alive under uutils coreutils
- Add standalone src/plugin-api.ts types and manifest apiVersion check
- Add ctx.media playback snapshots, ctx.signal, ctx.onDispose, and awaitable ctx.mpv.request
- Bound async activation; actions wait only for their own plugin
- card-mined now carries per-note provenance (media + subtitle captured at mining start) through merges and YouTube queues
- Port episode-tags and auto-jimaku to provenance and media snapshots; drop mining-goal docs
- Generated sentence audio no longer overwrites Yomitan's ExpressionAudio on default configs
- fields.wordAudio still defaults to ExpressionAudio
- Regenerate config.example.jsonc and update the Anki and configuration docs
- Add changelog fragment
- uutils readlink (Ubuntu 26.04+) aborts at the first unreadable /proc/<pid>/exe, so the keepalive released the mount early
- Add mount_busy helper that readlinks each link individually
- Add changelog fragment
- Name the copied straggler binary `sleep` so uutils/busybox multicall builds dispatch on argv[0] instead of exiting immediately
- Stop waiting on the straggler exec if it dies early, so the drain assertion reports the failure instead of hanging
- Fixes a flaky keepalive test where, under load, the supervisor could scan /proc while the forked child was still /bin/sh and release the mount too early
- AppRun now polls /proc/$!/exe until it resolves to the straggler binary
- episode-tags: match each note id to the file history instead of the current media, so late card-mined events (YouTube media, field grouping) get the right episode
- episode-tags: group notes per file into one addTags call; skip cards added before the plugin started
- auto-jimaku: track a generation counter so a pending check goes stale when the same file reloads
- docs: describe the per-card episode matching in plugins.md
- episode-tags: tag mined cards with show/episode parsed from the file name or stream title
- auto-jimaku: open Jimaku search when a file starts without a Japanese subtitle track
- Hold card-mined for notes merged into a note still waiting on queued YouTube media
- Route YouTube media settle reports through reportCardsReady
- Document both plugins in docs-site and changelog fragment
- Resolve note ids merged away by field grouping to the surviving note before emitting card-mined, deduplicating results
- Update plugin docs and setCardsReadyCallback comment to match
- Hold a sentence card's ready report until field grouping finishes
- Report cards waiting on queued YouTube media once it is applied or the download fails
- Proxy reports added notes ready right away when auto-update is off
- Polling keeps enriching the rest of a batch after one note fails
- Update plugin and architecture docs
- Add ctx.anki.on('card-mined') fed by polling, proxy, and card creation once enrichment finishes
- Add ctx.anki.invoke to send one AnkiConnect action to the effective url with no retries
- Ship example anki-sync plugin that syncs to AnkiWeb after mining goes quiet or playback ends
- Unload plugins that finish activating after unloadAll gave up on them
- Document the Anki API and anki-sync plugin in docs-site and architecture docs
- unloadAll waits for an in-progress load so plugins still activating get deactivated too
- One 2 s budget covers the load wait and all deactivates
- Quit cleanup awaits plugin unload before any other teardown
- Force-quit timer now allows the plugin budget plus 2 s
- Move plugin unload into the will-quit cleanup handler so it starts first and quit waits for it
- Give each plugin's deactivate 3 s (configurable via deactivateTimeoutMs) before disposing it anyway
- Document the quit timeout in docs-site/plugins.md
handleModalClosed now reports whether teardown should continue. A late
close notice for a replaced panel returns before the generic modal-close
flow, so it no longer hides the modal window showing the newer panel.
- Add `--plugin-action <id>/<name>` CLI flag, dispatched via runPluginAction
- Add ctx.ui.openWindow for plugin-owned BrowserWindows with sandboxed preload
- Add ctx.dataDir (`plugin-data/<id>/`), ctx.actions.run, ctx.mpv.ensureRunning
- Emit mpv end-file events to plugins with reason, entry id, and error
- Run Windows .cmd/.bat shims through cmd.exe in exec, killing the tree on abort
- Update plugin docs, usage, architecture notes, and e2e coverage
The overlay's modal-closed notice now carries the panel id, and the
runtime only finishes the active panel when the ids match. A late notice
for a replaced panel no longer aborts the newer one.
- Escalate timed-out or aborted plugin exec children to SIGKILL after a grace period
- Move ai-explain example to assets/plugins and convert it to CommonJS
- Update plugin docs for the new path and the .js/.mjs module formats
- Load plugins from <config dir>/plugins/<id>/ when enabled under `plugins` in config
- Expose ctx with mpv events/commands, timers, subtitle context, actions, notify, exec, and panel
- Bind plugin actions via `__plugin-action:<id>/<action>` keybindings (runPluginAction session action)
- Add plugin-panel modal with markdown rendering; closing aborts the panel signal
- Add ai-explain example plugin that queries `codex exec` for the current line
- Add e2e harness `files` option and plugin e2e scenario; document plugins in docs-site and internal docs
- Shorten the Hachidori backend fragment to its user-visible outcomes
- Move the fork submodule note into an internal fragment
- Split stats dashboard history-line mining into its own fixed fragment
- Reclassify the setup launcher label fragment as internal
- Drop the Electron/undici security fragment
* fix(subtitles): widen halfwidth katakana where subtitle text enters the app
Broadcast-derived Japanese subs write katakana halfwidth (アンタ, ダキニ), so
overlay lines mixed squashed and normal-width text. Normalize live mpv
sub-text and parsed cue text to fullwidth so display, lookups, mining,
and stats all see one form. The tokenizer reuses the shared helper.
* fix(subtitles): fold stray voiced marks and widen before cue dedup
- A lone halfwidth ゙ after fullwidth kana (ガ) now composes to ガ
instead of leaving a combining mark that broke name matching.
- Parsed cue text is widened before duplicate merging so ガク and ガク
on the same span collapse into one sidebar row.
- The YouTube track-load refresh widens the sub-text it reads directly.
* fix(stats): widen halfwidth katakana in stored lines and mined cards (#296)
Lines recorded before ingest widening still hold halfwidth katakana.
Occurrence and sentence-search queries widen line text on read, and the
mine-card route widens the sentence and word it receives, so the
dashboard and cards mined from history match newer rows. Word identity
columns are left as stored since the dashboard queries by them.
* fix(stats): match katakana in either width in sentence search
- Add narrowFullwidthKana, derived from widenHalfwidthKana so the two stay in sync
- Match subtitle text against the original, widened, and narrowed forms of each term so a search finds older lines stored as halfwidth
* feat(subtitles): generate Japanese subtitles during Jellyfin playback
- Read the item's original file via toJellyfinOriginalStreamUrl and write output to a temp dir
- Transcoded playback uses Jellyfin's audio index and only external references
- Show Jellyfin media by title and redact URL tokens in subprocess errors
- Update overlay copy, docs, and changelog fragment
* fix(subtitles): redact full stream tokens and reuse one Jellyfin output folder
* test(subtitles): make Jellyfin output path assertions platform-neutral
- Compare dirname/basename via node:path instead of POSIX-only regexes
- Assert the full sub-add command uses the generated output path
Drop 16 overlay preload methods with no caller (onSubtitleAss,
getCurrentSubtitleAss, onVisibility, getOverlayVisibility,
openYomitanSettings, quitApp, toggleOverlay, getMecabStatus,
setMecabEnabled, getKeybindings, saveControllerPreference,
getAnkiConnectStatus, setAnkiConnectEnabled, clearAnkiConnectHistory,
cycleRuntimeOption, appendClipboardVideoToQueue) along with their
IPC_CHANNELS keys, handlers, validators and dep plumbing.
With both ASS consumers gone, stop observing sub-text/ass in mpv and
remove the subtitle-ass-change event chain and appState.currentSubAssText.
The main-process functions these wrapped stay; tray, CLI, shortcuts and
session actions still use them.
* refactor: remove dead code and enable noUnusedLocals
- delete launcher/jimaku.ts and the launcher Args/config jimaku* fields
that only fed it (the SUBMINER_JIMAKU_* env vars had no effect)
- delete plugin/subminer/hover.lua; nothing has sent subminer-hover-token
since Overlay 2.0
- drop never-read anilist.characterDictionary.refreshTtlHours and
evictionPolicy (still hidden in the settings UI for old configs)
- drop unused root deps: eslint, @fontsource-variable/geist{,-mono}
(stats/ declares its own)
- delete the committed draft renderer-performance plan doc
- turn on noUnusedLocals for typecheck and remove what it surfaced,
including Windows focus-handoff values computed but never consumed
* docs(changes): add docs fragment and note retired hover messages
* test: add headless end-to-end harness and CI job
Boot the built app against a real mpv, a synthetic clip, a fixture Yomitan
dictionary, and a fake AnkiConnect inside a private Xvfb display, then assert
on the overlay DOM and the AnkiConnect requests. Adds the test:e2e lane, an
e2e session CLI for exploratory checks, and a three-platform CI job.
* ci: set e2e temp dir from a step
* test: make e2e session startup robust on slow machines
Bun caps node:test hooks at 5s, so boot the session inside the first test.
Keep pausing until the app's resume retries stop, read cue tokens in one
round trip, and resolve Electron through its package entry so CI downloads
the binary.
* test: harden e2e harness for CI runners
Fail fast when mpv cannot be spawned, read every Yomitan popup frame instead
of trusting target order, and put the chocolatey mpv directory on PATH.
* test: add e2e failure diagnostics and mark Windows job experimental
Report what the overlay showed when a cue never renders, dispose the session
when the overlay cannot be opened, skip popup frames that vanish mid-poll,
and turn off AniSkip lookups in the harness config.
* test: read the lookup popup entry from textContent
* test: capture renderer console and main-side subtitle state in e2e failures
* test: expose the Electron main-process inspector to the e2e harness
Adds session.mainProcess() and 'e2e eval main', and reports main's window
list alongside the overlay state when a cue never renders.
* test: tap subtitle sends and receipts in e2e failure diagnostics
* test: probe alternate main-to-renderer paths in e2e failure diagnostics
* test: select the visible overlay layer, not the primed modal window
Windows and macOS pre-create a hidden modal window that loads the same
renderer document, and the harness could attach to it instead of the
visible overlay. Drops the delivery taps that found this.
* ci: run the Windows e2e job as a normal matrix entry
* test: address review on e2e harness
Slice the app log at a byte offset before decoding, create the session state
file owner-only since it carries the inspector URL, and keep the cue failure
when inspector diagnostics are unavailable.
* test: create the e2e session state file exclusively