From 07b32626a6719487a2e1b54db65876c7a458cc66 Mon Sep 17 00:00:00 2001 From: sudacode Date: Mon, 28 Sep 2026 20:35:47 -0700 Subject: [PATCH] feat(youtube): add YouTube browser window and Whisper subtitle source (#273) --- changes/closed-pipe-crash.md | 4 + changes/youtube-browser-window.md | 7 + changes/youtube-translated-caption-429.md | 4 + changes/youtube-whisper-subtitles.md | 7 + config.example.jsonc | 3 +- docs-site/configuration.md | 5 +- docs-site/launcher-script.md | 1 + docs-site/mpv-plugin.md | 2 +- docs-site/public/config.example.jsonc | 3 +- docs-site/subtitle-generation.md | 6 +- docs-site/usage.md | 1 + docs-site/websocket-texthooker-api.md | 2 +- docs-site/youtube-integration.md | 37 +++ launcher/commands/app-command.ts | 14 ++ launcher/commands/command-modules.test.ts | 21 ++ launcher/commands/playback-command.test.ts | 14 +- launcher/commands/playback-command.ts | 3 +- launcher/config-domain-parsers.test.ts | 27 --- launcher/config/args-normalizer.test.ts | 36 ++- launcher/config/args-normalizer.ts | 38 +--- launcher/config/cli-parser-builder.ts | 16 ++ launcher/config/youtube-subgen-config.ts | 53 ----- launcher/jellyfin.test.ts | 12 +- launcher/main.test.ts | 21 ++ launcher/mpv.test.ts | 22 +- launcher/mpv.ts | 23 -- launcher/parse-args.test.ts | 9 + launcher/types.ts | 43 +--- launcher/util.ts | 8 - plugin/subminer/messages.lua | 3 + plugin/subminer/process.lua | 12 + release/release-notes.md | 159 ------------- scripts/package-audit.cjs | 1 + scripts/test-plugin-start-gate.lua | 37 +++ src/cli/args.test.ts | 15 +- src/cli/args.ts | 15 +- src/cli/help.ts | 3 + src/config/config.test.ts | 40 ++-- src/config/definitions.ts | 2 - src/config/definitions/defaults-core.ts | 1 + .../definitions/defaults-integrations.ts | 12 - .../definitions/domain-registry.test.ts | 7 - src/config/definitions/options-core.ts | 12 + src/config/definitions/template-sections.ts | 4 +- src/config/hot-reload.ts | 1 + src/config/resolve/core-domains.ts | 12 + src/config/resolve/subtitle-domains.ts | 99 -------- src/config/settings/registry.test.ts | 1 - src/config/settings/registry.ts | 13 +- src/config/template.ts | 7 - src/core/services/app-lifecycle.test.ts | 1 + src/core/services/cli-command.test.ts | 48 ++-- src/core/services/cli-command.ts | 12 +- src/core/services/startup-bootstrap.test.ts | 1 + .../services/subtitle-generation-process.ts | 5 +- src/core/services/subtitle-generation.ts | 36 ++- .../services/youtube/track-download.test.ts | 29 +++ src/core/services/youtube/track-download.ts | 15 +- src/core/services/youtube/track-probe.test.ts | 25 ++ src/core/services/youtube/track-probe.ts | 17 +- .../youtube/whisper-subtitles.test.ts | 168 ++++++++++++++ .../services/youtube/whisper-subtitles.ts | 148 ++++++++++++ src/logger.test.ts | 12 +- src/logger.ts | 21 ++ src/main.ts | 114 +++++++++- src/main/cli-runtime.ts | 2 + src/main/dependencies.ts | 2 + .../runtime/cli-command-context-deps.test.ts | 1 + src/main/runtime/cli-command-context-deps.ts | 2 + .../cli-command-context-factory.test.ts | 1 + .../cli-command-context-main-deps.test.ts | 1 + .../runtime/cli-command-context-main-deps.ts | 2 + src/main/runtime/cli-command-context.test.ts | 1 + src/main/runtime/cli-command-context.ts | 2 + .../composers/cli-startup-composer.test.ts | 1 + .../composers/jellyfin-runtime-composer.ts | 3 + .../runtime/first-run-setup-service.test.ts | 1 + .../subtitle-generation-runtime.test.ts | 130 +++++++++++ .../runtime/subtitle-generation-runtime.ts | 121 +++++++++- src/main/runtime/tray-main-actions.test.ts | 4 + src/main/runtime/tray-main-actions.ts | 5 + src/main/runtime/tray-main-deps.test.ts | 2 + src/main/runtime/tray-main-deps.ts | 3 + .../runtime/tray-runtime-handlers.test.ts | 1 + src/main/runtime/tray-runtime.test.ts | 8 + src/main/runtime/tray-runtime.ts | 5 + .../runtime/youtube-browser-playback.test.ts | 210 +++++++++++++++++ src/main/runtime/youtube-browser-playback.ts | 186 +++++++++++++++ .../runtime/youtube-browser-window.test.ts | 12 + src/main/runtime/youtube-browser-window.ts | 214 ++++++++++++++++++ src/main/runtime/youtube-flow.test.ts | 139 +++++++++++- src/main/runtime/youtube-flow.ts | 177 +++++++++++++-- .../runtime/youtube-playback-runtime.test.ts | 24 +- src/main/runtime/youtube-playback-runtime.ts | 18 +- src/main/runtime/youtube-playback.test.ts | 23 ++ src/main/runtime/youtube-playback.ts | 15 +- src/preload-youtube-browser.ts | 97 ++++++++ src/renderer/modals/subtitle-generation.ts | 5 +- src/shared/ipc/contracts.ts | 2 + src/types/config.ts | 12 +- src/types/integrations.ts | 13 +- 101 files changed, 2269 insertions(+), 726 deletions(-) create mode 100644 changes/closed-pipe-crash.md create mode 100644 changes/youtube-browser-window.md create mode 100644 changes/youtube-translated-caption-429.md create mode 100644 changes/youtube-whisper-subtitles.md delete mode 100644 release/release-notes.md create mode 100644 src/core/services/youtube/whisper-subtitles.test.ts create mode 100644 src/core/services/youtube/whisper-subtitles.ts create mode 100644 src/main/runtime/youtube-browser-playback.test.ts create mode 100644 src/main/runtime/youtube-browser-playback.ts create mode 100644 src/main/runtime/youtube-browser-window.test.ts create mode 100644 src/main/runtime/youtube-browser-window.ts create mode 100644 src/preload-youtube-browser.ts diff --git a/changes/closed-pipe-crash.md b/changes/closed-pipe-crash.md new file mode 100644 index 00000000..06bad486 --- /dev/null +++ b/changes/closed-pipe-crash.md @@ -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. diff --git a/changes/youtube-browser-window.md b/changes/youtube-browser-window.md new file mode 100644 index 00000000..3b33ac24 --- /dev/null +++ b/changes/youtube-browser-window.md @@ -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). diff --git a/changes/youtube-translated-caption-429.md b/changes/youtube-translated-caption-429.md new file mode 100644 index 00000000..aa6acfd1 --- /dev/null +++ b/changes/youtube-translated-caption-429.md @@ -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. diff --git a/changes/youtube-whisper-subtitles.md b/changes/youtube-whisper-subtitles.md new file mode 100644 index 00000000..ae3cc246 --- /dev/null +++ b/changes/youtube-whisper-subtitles.md @@ -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. diff --git a/config.example.jsonc b/config.example.jsonc index 22640179..fe7f4f96 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -682,13 +682,14 @@ // ========================================== // YouTube Playback Settings // Defaults for managed subtitle language preferences and YouTube subtitle loading. - // Hot-reload: primarySubLanguages applies to the next YouTube subtitle load. + // Hot-reload: primarySubLanguages and subtitleSource apply to the next YouTube subtitle load. // ========================================== "youtube": { "primarySubLanguages": [ "ja", "jpn" ], // 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. diff --git a/docs-site/configuration.md b/docs-site/configuration.md index b3de3142..1ec87475 100644 --- a/docs-site/configuration.md +++ b/docs-site/configuration.md @@ -54,7 +54,7 @@ These apply live: - `subtitleStyle`, `subtitleSidebar`, `subtitleSelection`, `keybindings`, `shortcuts` - `logging.level`, `logging.rotation`, `logging.files` -- `secondarySub.defaultMode`, `youtube.primarySubLanguages` +- `secondarySub.defaultMode`, `youtube.primarySubLanguages`, `youtube.subtitleSource` - `mpv.aniskipEnabled`, `mpv.aniskipButtonKey`, `stats.toggleKey`, `stats.markWatchedKey` - `ankiConnect.deck`, `ankiConnect.fields.*`, `ankiConnect.behavior.autoUpdateNewCards` - `ankiConnect.media.normalizeAudio`, `media.mirrorMpvVolume`, `media.reviewTiming` @@ -594,11 +594,12 @@ Settings for mpv instances that SubMiner starts, and for the bundled mpv plugin. ### YouTube playback settings -Language and card-media settings for YouTube playback. YouTube always loads a Japanese primary and English secondary track, preferring manual uploads over auto captions. See [YouTube integration](/youtube-integration). +Subtitle, language, and card-media settings for YouTube playback. With YouTube captions, SubMiner loads a Japanese primary and English secondary track, preferring manual uploads over auto captions. See [YouTube integration](/youtube-integration). | Key | Default | What it does | | ------------------------------ | --------------- | -------------------------------------------------------------------------------------------- | | `youtube.primarySubLanguages` | `["ja", "jpn"]` | Languages that count as a valid primary track, also used for local playback | +| `youtube.subtitleSource` | `"youtube"` | `youtube` downloads YouTube's captions. `whisper` transcribes the audio with Whisper | | `youtube.mediaCache.mode` | `"direct"` | `direct` cuts card media from the stream. `background` downloads the video with yt-dlp first | | `youtube.mediaCache.maxHeight` | `720` | Maximum download height in `background` mode. `0` is unlimited | diff --git a/docs-site/launcher-script.md b/docs-site/launcher-script.md index bd459095..f0741a5e 100644 --- a/docs-site/launcher-script.md +++ b/docs-site/launcher-script.md @@ -48,6 +48,7 @@ App flags such as `--setup` and `--dev` are not launcher flags. Pass them throug | `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 ` / `dict` | Build a [character dictionary](/character-dictionary) for a file or directory | diff --git a/docs-site/mpv-plugin.md b/docs-site/mpv-plugin.md index 0f05818e..40732c9e 100644 --- a/docs-site/mpv-plugin.md +++ b/docs-site/mpv-plugin.md @@ -124,7 +124,7 @@ script-message subminer-start backend=hyprland socket=/custom/path texthooker=no `log-level` sets SubMiner's log verbosity. Do not use `--debug` for this; it turns on the app's dev mode. -The plugin also handles messages the SubMiner app sends it (`subminer-autoplay-ready`, `subminer-visible-overlay-shown`, `subminer-visible-overlay-hidden`, `subminer-managed-subtitles-loading`, `subminer-overlay-loading-ready`, `subminer-reload-session-bindings`). You do not need to send these yourself. The AniSkip messages are listed on the [AniSkip page](/aniskip-integration#triggering-from-mpv). +The plugin also handles messages the SubMiner app sends it (`subminer-autoplay-ready`, `subminer-autoplay-hold`, `subminer-visible-overlay-shown`, `subminer-visible-overlay-hidden`, `subminer-managed-subtitles-loading`, `subminer-overlay-loading-ready`, `subminer-reload-session-bindings`). You do not need to send these yourself. The AniSkip messages are listed on the [AniSkip page](/aniskip-integration#triggering-from-mpv). ## Auto-start behavior diff --git a/docs-site/public/config.example.jsonc b/docs-site/public/config.example.jsonc index 22640179..fe7f4f96 100644 --- a/docs-site/public/config.example.jsonc +++ b/docs-site/public/config.example.jsonc @@ -682,13 +682,14 @@ // ========================================== // YouTube Playback Settings // Defaults for managed subtitle language preferences and YouTube subtitle loading. - // Hot-reload: primarySubLanguages applies to the next YouTube subtitle load. + // Hot-reload: primarySubLanguages and subtitleSource apply to the next YouTube subtitle load. // ========================================== "youtube": { "primarySubLanguages": [ "ja", "jpn" ], // 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. diff --git a/docs-site/subtitle-generation.md b/docs-site/subtitle-generation.md index 0c2da357..3447d13f 100644 --- a/docs-site/subtitle-generation.md +++ b/docs-site/subtitle-generation.md @@ -10,11 +10,13 @@ When a video has no Japanese subtitles, SubMiner can transcribe its audio into a Downloaded models go to `models/whisper/` next to your SubMiner config file. A configured `modelPath` always wins over the modal's choice. +YouTube videos can use the same setup automatically in place of YouTube's captions. See [Generate subtitles with Whisper](/youtube-integration#generate-subtitles-with-whisper). + The modal's **Local tools** section lists anything missing. After you install a tool or change a path, click **Check again**. ## Generating from the overlay -1. Open a local video in mpv and select its Japanese audio track. +1. Open a local video in mpv and select its Japanese audio track, or play a YouTube video. 2. Press `Ctrl+Shift+G`. If the subtitle sidebar is empty, its **Generate Japanese subtitles** button opens the same modal. 3. Pick a model and download it if needed. 4. Optionally check **Focus on spoken dialogue** (see below). @@ -24,6 +26,8 @@ The modal shows progress. **Cancel** stops the job. Closing the modal lets the j SubMiner saves `