Sentence mining from Japanese Jellyfin subtitles into Anki
  • TypeScript 36.4%
  • C# 32.7%
  • JavaScript 25.6%
  • HTML 5.3%
Find a file
2026-08-13 17:30:12 +02:00
.impeccable/live design: the sign-in is a Cue 2026-08-10 15:11:22 +02:00
docs docs: the deploy brief was aiming at the wrong loopback 2026-08-13 15:41:11 +02:00
mockups feat(web): the word is carried to the count, not fired at it 2026-08-13 11:26:20 +02:00
src/Jellyfin.Plugin.Kurageyomi fix: let the Export report say only what arrived 2026-08-13 17:26:05 +02:00
tests/Jellyfin.Plugin.Kurageyomi.Tests fix: cut the Clip from a stream confirmed to be audio 2026-08-13 17:26:05 +02:00
web fix: let the Export report say only what arrived 2026-08-13 17:26:05 +02:00
.gitattributes chore: normalize line endings before code lands 2026-08-09 23:01:10 +02:00
.gitignore design: the sign-in is a Cue 2026-08-10 15:11:22 +02:00
bp2.cs feat: follow sessions over the WebSocket, with polling as the fallback 2026-08-10 10:27:42 +02:00
CONTEXT.md docs: the Export slice, settled 2026-08-13 12:35:30 +02:00
DESIGN.md docs: the count is the control, and the key is a layer 2026-08-13 15:14:53 +02:00
jc.zip chore: normalize line endings before code lands 2026-08-09 23:01:10 +02:00
Kurageyomi.sln feat: plugin skeleton with anonymous shell and authorized data endpoint 2026-08-09 23:53:55 +02:00
LICENSE docs: design for Kurageyomi 2026-08-09 21:59:46 +02:00
PRODUCT.md docs: the reader configures a deck, not a host 2026-08-13 15:21:30 +02:00
README.md docs: the queue exists, the writing does not 2026-08-12 12:13:12 +02:00
se.cs feat: read subtitle tracks as Cues and show the transcript 2026-08-10 00:28:43 +02:00
t11.json feat: read subtitle tracks as Cues and show the transcript 2026-08-10 00:28:43 +02:00
tr.js docs: ADR 0005 — when in doubt, steal from Yomitan 2026-08-10 10:50:14 +02:00

Kurageyomi

海月読み — "jellyfish reading".

Sentence mining from Japanese Jellyfin subtitles, straight into Anki.

A Jellyfin plugin serves a page to your phone. While an episode plays, the page shows the Japanese subtitle line currently on screen. Tap an unknown word: playback pauses, a Yomichan-style lookup deinflects it and shows the entry, and one more tap saves it with the whole sentence. Later, an export cuts the audio and a still frame from the source file and pushes finished notes into Anki.

The design is:

Clients

The plugin follows whatever is playing on any Jellyfin client, so reading works with every client: the Cue strip, lookups, and mining only ever read the server's own view of a Playback Session.

Controlling the show is different. Pausing, seeking, and stepping a Cue at a time are Jellyfin playstate commands, and a client only receives those if it registers as remote-controllable. A client that does not will accept the command and do nothing — the endpoints answer, the picture does not move.

Check before assuming: start something playing, then open the Jellyfin web UI's Play On menu. If the session is listed there, Jellyfin considers it controllable and this plugin's transport will work. That list reads the same SupportsRemoteControl the app does.

Client Reading Control Notes
Official Jellyfin Android TV works works Confirmed on dionysus: pause, unpause, and per-Cue stepping all drive it. This is the client to use.
Moonfin (Android TV) works no Confirmed on dionysus: absent from Play On, and Jellyfin's own UI cannot drive it either. Moonfin casts to other devices; it does not accept being cast to.
Anything else works test it One minute with Play On settles it.

When the followed client cannot be driven, the transport controls are disabled with a reason rather than silently doing nothing, and auto-pause has no effect. Reading is unaffected.

Building

The phone app is TypeScript compiled by tsc straight into the plugin's Web/ folder, which the C# build then embeds. No bundler — the browser loads native ES modules.

cd web && npm install && npm run build   # emits .js into src/Jellyfin.Plugin.Kurageyomi/Web
dotnet test                              # C# suite
cd web && npm test                       # phone-side suite
dotnet build -c Release                  # produces the dll and meta.json to deploy

The emitted .js files are committed so dotnet build works without Node installed. Re-run npm run build after changing anything under web/src, or the plugin ships the old script — a test fails if the shell references an asset that is not compiled in, which is the usual symptom.

Deploying

Jellyfin runs in a container on dionysus (10.11.11) and is reached over https via tailscale serve, so the page runs in a secure context — Wake Lock and home-screen install work, and the app's "served over http" warning stays hidden.

Nothing is built on dionysus. dex-win builds and publishes the artifacts over the tailnet:

cd web && npm run build && cd ..
dotnet build -c Release
cp src/Jellyfin.Plugin.Kurageyomi/bin/Release/net9.0/Jellyfin.Plugin.Kurageyomi.dll \
   src/Jellyfin.Plugin.Kurageyomi/bin/Release/net9.0/meta.json \
   ../tailscale-artifacts/kurageyomi/
cd ../tailscale-artifacts/kurageyomi && sha256sum Jellyfin.Plugin.Kurageyomi.dll meta.json > SHA256SUMS

tailscale serve publishes F:\Projects\tailscale-artifacts tailnet-only at https://dex-win.tail1ee5d.ts.net, so those land at /kurageyomi/. dionysus fetches them per docs/deploy-brief-hermes.md, which is discovery-first about the container's paths rather than assuming them.

Two things that will waste your time if forgotten: dex-win has to be awake for the fetch, and the phone caches main.js — hard-reload after a deploy or you will debug the previous build.

State

Working end to end: the phone follows what is playing, shows one Cue at a time, and resolves a tapped word — deinflecting it and showing JMdict entries with the matched Surface Form painted in the Cue. Playback controls exist for clients that accept them (see Clients).

Mining is not built. Capture, the duplicate check, and Export to Anki are specified in docs/PRD.md and settled by the ADRs, but nothing writes a Capture yet. Tapping a word lets you read it; it does not keep it. The Mining Queue's store exists and the status bar reads its count from the server, so the number is currently always zero.

The design the interface is built against is DESIGN.md; what the product is for is PRODUCT.md.

Licence

GPL-3.0. Dictionary data is JMdict, used under the EDRDG licence — attribution to the Electronic Dictionary Research and Development Group is displayed in the app wherever definitions appear.