Files
SubMiner/docs-site/plugins.md
T
sudacode 10424a57d5 fix(plugins): tag cards with the episode playing when they were added
- 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
2026-10-27 22:41:32 -07:00

14 KiB

Plugins

Plugins are JavaScript files that SubMiner loads at startup. A plugin can react to playback, run code on a timer, add actions you bind to keys, run programs, and show results in an overlay panel, all without rebuilding SubMiner.

::: warning Only run plugins you trust Plugins run inside SubMiner with the same access to your files and system as SubMiner itself. There is no sandbox. :::

These are SubMiner app plugins. The Lua script that runs inside mpv is the separate mpv plugin.

Install a plugin

  1. Put the plugin in its own folder in plugins/ next to your config file, for example ~/.config/SubMiner/plugins/ai-explain/. The folder name is the plugin id.

  2. Enable it in config.jsonc. settings is passed to the plugin as-is:

    {
      "plugins": {
        "ai-explain": { "enabled": true, "settings": { "reasoningEffort": "medium" } },
      },
    }
    
  3. Bind its actions to keys with the __plugin-action:<plugin id>/<action> command:

    {
      "keybindings": [
        { "key": "Ctrl+KeyE", "command": ["__plugin-action:ai-explain/explain"] },
        { "key": "Ctrl+KeyT", "command": ["__plugin-action:ai-explain/translate"] },
      ],
    }
    

    To run an action from a script or a window-manager shortcut, use subminer app --plugin-action ai-explain/explain. It goes to the running SubMiner, or starts one if none is running.

  4. Restart SubMiner. Plugin changes apply on the next start; keybinding changes apply immediately.

Load errors and plugin log output go to the app log with a plugin:<id> scope.

Example: explain the current line with Codex

The repository ships assets/plugins/ai-explain. It sends the current line, plus the lines around it, to codex exec and shows the answer in a panel. It needs a logged-in Codex CLI.

Action What it does
explain Translation plus a breakdown of the grammar and vocabulary
translate Natural English translation, with a note only when it's unclear
Setting Default What it does
codexPath "codex" Codex binary. Use a full path if SubMiner can't find it on PATH
model "" Model passed to codex exec -m; empty uses your Codex config
reasoningEffort "low" Codex reasoning effort; empty uses your Codex config
before, after 2, 1 Lines of context sent before and after the current line
pause true Pause playback when the panel opens
timeoutMs 120000 Give up after this long
prompts {} Replace an action's instructions, e.g. { "translate": "..." }

Press Esc to close the panel. Closing it, or starting another request, stops the running Codex process.

Example: sync Anki after mining

assets/plugins/anki-sync runs AnkiConnect's sync after you mine cards, so they reach AnkiWeb without opening Anki. It waits until you stop mining for a while, so a burst of cards becomes one sync. It also adds a sync action you can bind to a key.

Setting Default What it does
delayMs 30000 Sync after no card has been mined for this long
syncOnEnd true Sync right away when playback ends or mpv quits
timeoutMs 60000 Give up on a sync after this long
notify true Show a notification after each sync. Failures always do

Anki must be running and logged in to AnkiWeb. Cards mined from the stats dashboard don't trigger a sync.

Example: tag cards with the episode

assets/plugins/episode-tags tags each mined card with the show and episode it came from, such as show::Sousou_no_Frieren::E07. It reads them from the file name ([Group] Show - 07 (1080p).mkv, Show.S01E07.mkv, Show 第7話.mkv), or from the title mpv shows for streams. Each card is tagged with the file that was playing when it was added, so a card that finishes later (YouTube media, field grouping) still gets its own episode, and cards added before the plugin started are left alone. Spaces become _.

Setting Default What it does
tags ["show::{show}::{ep}"] Tags to add, one template each
notify false Show a notification with the tags for each card

Templates can use {show}, {season}, {episode}, and {ep} (S01E07, or E07 when there is no season). When a placeholder has no value, the tag stops before that :: level, so a movie gets show::<title>. The parsing is a guess from the name: a title ending in a number, like Mob Psycho 100, reads that number as the episode.

Example: open Jimaku when a file has no Japanese subtitles

assets/plugins/auto-jimaku opens the Jimaku search a few seconds after a file starts if none of its subtitle tracks is Japanese. A track counts as Japanese when its language code is in languages, or its title contains "Japanese" or 日本語.

Setting Default What it does
action "openJimaku" Built-in action to run, e.g. openTsukihime
languages ["ja", "jp", "jpn"] Track language codes that count as Japanese
delayMs 3000 Wait this long after the file starts, so external subs load
streams false Also check http(s) streams, which Jimaku usually can't search

Example: daily mining goal

assets/plugins/mining-goal counts the cards you mine each day and shows a notification when you reach your goal. Its stats action opens a panel with today's count, your streak of days at the goal, and the last 7 days. Counts are kept in plugin-data/mining-goal/counts.json.

Setting Default What it does
goal 10 Cards per day
dayStartHour 4 Cards mined before this hour count toward the day before, like Anki
notify true Show a notification when you reach the goal

Only cards SubMiner mines count. Cards mined from the stats dashboard or added in Anki don't.

Write a plugin

A plugin folder needs a plugin.json:

{ "name": "My Plugin", "version": "1.0.0", "main": "main.js" }

main points to a CommonJS module (.js) or an ES module (.mjs) that exports activate(ctx). It may return a cleanup function, or the module can export deactivate(). On quit, SubMiner waits up to 2 seconds for it, including any promise it returns. Everything a plugin registers through ctx (listeners, timers, actions, panels, running programs) is cleaned up automatically when SubMiner quits.

function activate(ctx) {
  ctx.mpv.on('media-path-change', ({ path }) => ctx.log.info(`Now playing ${path}`));
  ctx.every(60_000, () => ctx.log.info('Still watching'));
  ctx.actions.register('hello', () => {
    const line = ctx.subtitles.current();
    ctx.ui.panel({ title: 'Hello', markdown: line ? `> ${line.text}` : '_No subtitle_' });
  });
}

module.exports = { activate };

An error thrown by a plugin is logged and does not crash SubMiner. A plugin that blocks with a long synchronous loop still freezes the app, so keep slow work asynchronous.

ctx reference

Member What it does
id, dir, settings Plugin id, plugin folder path, and plugins.<id>.settings
dataDir plugin-data/<id>/ next to your config file, for caches and state. Exists before activate runs
log.debug/info/warn/error(message) Write to the app log
every(ms, fn), after(ms, fn) Repeating and one-shot timers. Return a function that cancels them
mpv.on(event, fn) Listen to mpv events (below). Returns an unsubscribe function
mpv.command(args) Send an mpv command, e.g. ['set', 'pause', 'yes']
mpv.getProperty(name) Read an mpv property (Promise)
mpv.connected Whether mpv is connected
mpv.ensureRunning() Connect to mpv, starting an idle mpv if none is running. Resolves false if that fails
anki.on(event, fn) Listen to Anki events (below). Returns an unsubscribe function
anki.invoke(action, params, options) Send an AnkiConnect action to ankiConnect.url, e.g. ('sync'). Resolves its result (below)
subtitles.current() The displayed line as { text, startTime, endTime }, or null
subtitles.context({ before, after }) { current, before, after }. Neighbors come from the loaded subtitle file, or recent lines for streams
actions.register(name, fn) Add an action, bindable as __plugin-action:<id>/<name>
actions.run(actionId, payload) Run a built-in action, the same ids keybindings use, e.g. openJimaku or mineSentence
notify(message, { variant }) Overlay notification. variant is info, warning, or error
ui.panel({ title, markdown }) Open the plugin panel. Returns { update(content), close(), closed, signal }
ui.openWindow({ html, preload }) Open a window from the plugin folder (below). Returns the Electron BrowserWindow
exec(command, args, options) Run a program without a shell (Windows .cmd/.bat shims go through cmd.exe). Resolves { exitCode, stdout, stderr }
unstable SubMiner internals. These change between releases without notice

A panel's signal aborts when the user closes it or another panel replaces it. Pass it to exec to stop work nobody will see. Only one panel is open at a time. Panel markdown supports headings, lists, quotes, code, bold, and italics; HTML is shown as plain text.

exec options are stdin, cwd, env, timeoutMs, and signal. It resolves for any exit code and rejects when the program can't start, times out, or is aborted.

ui.openWindow takes html and an optional preload, both relative to the plugin folder, plus title, width, and height. Opening the same html again focuses the open window. The page runs with context isolation and no Node access. The preload is sandboxed too: it can require('electron') but nothing else, so bundle anything more into it. Expose what the page needs from the preload with contextBridge, and handle its messages with ipcMain from require('electron') in the plugin. Prefix channel names with the plugin id. Plugin windows close when SubMiner quits.

mpv events: connection-change, subtitle-change, subtitle-timing, time-pos-change, duration-change, pause-change, fullscreen-change, media-path-change, media-title-change, subtitle-track-change, subtitle-track-list-change, secondary-subtitle-change, secondary-subtitle-track-change, secondary-subtitle-delay-change, secondary-subtitle-visibility, client-message, and end-file. end-file gives { reason, playlistEntryId, error }, where reason is eof, stop, quit, error, redirect, or unknown.

Anki events: card-mined gives { noteIds } once SubMiner has finished filling in a mined card (sentence, audio, image, and field grouping). For YouTube cards whose media waits on the download, it fires once that media is added or the download fails. With ankiConnect.behavior.autoUpdateNewCards off, it fires as soon as SubMiner sees the card. If field grouping merged the card into an older one, you get the older card's id.

anki.invoke takes { timeoutMs } (default 10 seconds), rejects on an Anki error, and never retries, so an action like sync or addNote runs at most once.