diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml index 62feb3f5..c04894ca 100644 --- a/.github/workflows/package-release.yml +++ b/.github/workflows/package-release.yml @@ -59,17 +59,6 @@ jobs: bun install --frozen-lockfile bun run build - - name: Download previous package size reports - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p .tmp/package-baseline - previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty') - if [ -n "$previous" ]; then - gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.' - fi - - name: Build AppImage run: bun run build:appimage env: @@ -98,7 +87,6 @@ jobs: release/*.AppImage release/latest*.yml release/*.blockmap - release/package-size-*.json if-no-files-found: error build-macos: @@ -159,17 +147,6 @@ jobs: bun install --frozen-lockfile bun run build - - name: Download previous package size reports - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p .tmp/package-baseline - previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty') - if [ -n "$previous" ]; then - gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.' - fi - - name: Build signed + notarized macOS artifacts run: bun run build:mac env: @@ -193,7 +170,6 @@ jobs: release/*.zip release/latest*.yml release/*.blockmap - release/package-size-*.json if-no-files-found: error build-windows: @@ -235,17 +211,6 @@ jobs: bun install --frozen-lockfile bun run build - - name: Download previous package size reports - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p .tmp/package-baseline - previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty') - if [ -n "$previous" ]; then - gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.' - fi - - name: Verify managed Windows launcher run: bun test src/main/runtime/managed-launcher.test.ts @@ -270,5 +235,4 @@ jobs: release/*.zip release/latest*.yml release/*.blockmap - release/package-size-*.json if-no-files-found: error diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index fc64e1b9..6135476e 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -105,7 +105,6 @@ jobs: run: | shopt -s nullglob files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd) - files+=(release/package-size-*.json) if [ "${#files[@]}" -eq 0 ]; then echo "No release artifacts found for checksum generation." exit 1 @@ -152,7 +151,6 @@ jobs: release/latest*.yml release/*.blockmap release/SHA256SUMS.txt - release/package-size-*.json dist/launcher/subminer dist/launcher/subminer.cmd ) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a608aaf6..1bb0e414 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,6 @@ jobs: run: | shopt -s nullglob files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd) - files+=(release/package-size-*.json) if [ "${#files[@]}" -eq 0 ]; then echo "No release artifacts found for checksum generation." exit 1 @@ -171,7 +170,6 @@ jobs: release/latest*.yml release/*.blockmap release/SHA256SUMS.txt - release/package-size-*.json dist/launcher/subminer dist/launcher/subminer.cmd ) @@ -255,22 +253,31 @@ jobs: echo "skip=true" >> "$GITHUB_OUTPUT" - name: Download release assets for AUR + id: aur_assets if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_VERSION: ${{ steps.version.outputs.VERSION }} run: | set -euo pipefail version="$RELEASE_VERSION" install -dm755 .tmp/aur-release-assets - gh release download "$version" \ - --dir .tmp/aur-release-assets \ - --pattern "SubMiner-${version#v}.AppImage" \ - --pattern "subminer" \ - --pattern "subminer-assets.tar.gz" + for asset in "SubMiner-${version#v}.AppImage" subminer subminer-assets.tar.gz; do + destination=".tmp/aur-release-assets/$asset" + if ! curl --fail --silent --show-error --location \ + --retry 3 --retry-delay 1 --retry-all-errors \ + --connect-timeout 30 --max-time 600 \ + --output "$destination.partial" \ + "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$version/$asset"; then + echo "::warning::Unable to download $asset after retries; skipping automated AUR publish." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + mv "$destination.partial" "$destination" + done + echo "skip=false" >> "$GITHUB_OUTPUT" - name: Update AUR packaging metadata - if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' + if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true' env: RELEASE_VERSION: ${{ steps.version.outputs.VERSION }} run: | @@ -287,7 +294,7 @@ jobs: --assets ".tmp/aur-release-assets/subminer-assets.tar.gz" - name: Commit and push AUR update - if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' + if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true' working-directory: aur-subminer-bin env: GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b75bec..f064625f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,126 @@ # Changelog +## v0.20.0 (2026-09-23) + +### Added +- **Japanese Subtitle Generation**: + - Generate Japanese SRT subtitles locally with whisper.cpp. Start it from a new modal (Ctrl+Shift+G), from the generate button in an empty subtitle sidebar, or with `subminer generate-subs`. + - Generation shows progress, can be cancelled, and loads the finished subtitles into mpv automatically. + - Pick an official multilingual Whisper model, including quantized variants, with size and accuracy guidance. You can download it in the app or point Settings at a model you already have. + - `large-v3-turbo` is recommended when CUDA support is detected, and `small` otherwise. + - whisper-cli, ffmpeg, and ffprobe are found on PATH unless you override them. SubMiner names any missing tools before a download starts. + - An optional "Focus on spoken dialogue" mode uses a separately downloaded Silero VAD model. It keeps audible sections it is unsure about, so dialogue under music is not dropped, but songs may also be transcribed. + - Long passages are split near speech starts or quiet pauses to reduce subtitles that appear too early. When an eligible embedded or external subtitle track is loaded in mpv, it guides the split points. + - Each passage runs in a fresh Whisper process, which prevents repeated-character output. +- **Subtitle Selection Modal**: + - An optional modal for choosing primary and secondary mpv subtitle tracks. + - Turn it on in Settings under Behavior, then press g followed by s to open it. Turning it off restores mpv's own subtitle selection binding. + - Single-key actions take priority over configured key sequence prefixes. + - Conflicting sequences are disabled with a warning, and the existing y commands stay reserved. +- **Subtitle Sidebar Copy**: + - Select dialogue across several sidebar rows and copy it without timestamps using Ctrl/Cmd+C or the Copy button. + - Selecting text does not seek playback and does not require mining a card. +- **Media Timing Review Screenshot Picker**: + - Choose the still screenshot separately from the audio range, with a live preview and its own time slider. + - Step through decoded frames one at a time to get the exact frame you want. + - Works with local video and with seekable remote streams such as Jellyfin. +- **mpv Keybindings in the Overlay**: + - The overlay now picks up keyboard bindings from mpv defaults, `input.conf`, and loaded scripts when they do not conflict with SubMiner. + - SubMiner controls and bindings you explicitly disabled take precedence. + - These bindings apply only to the current session and are not listed in the help menu. +- **Jimaku Live Action Search**: The Jimaku modal has new Anime and Live action tabs, so you can search Jimaku's live action catalogue as well as anime. Use Arrow Left and Arrow Right to switch tabs. +- **TMDB Live-Action Library**: + - Live-action dramas and movies in the stats Library now get posters, synopses, and titles from TMDB. + - Release builds include a project key. Setting `tmdb.apiKey` or `tmdb.apiKeyCommand` overrides it, and one of them is required when running from source. + - Titles that AniList cannot match are looked up on TMDB automatically when the parsed filename exactly matches a Japanese live-action title. For everything else, use the new **Link to TMDB** action. + - Entries linked to the same TMDB title merge into one card, and the Library kind selector has a new Live Action option. + - If a replacement download fails during provider reassignment, the previous link and artwork are kept. Merges and sync keep AniList and TMDB identities separate, and the merge dialog explains mixed selections instead of failing. +- **YouTube Library Kind**: + - YouTube channels are now their own Library media kind. Existing channel entries migrate automatically, and viewing history and manual video assignments are unchanged. + - New All Titles, Anime, and YouTube Library filters. + - Channels are excluded from AniList matching, season repair, and duplicate recommendations. + - Merges and video moves can no longer combine an anime entry with a YouTube channel. + +### Changed +- **Launcher Uses Bundled Bun**: + - Every installed and downloadable launcher now runs on the Bun runtime that ships with SubMiner. A system Bun is no longer needed. + - Recognized legacy launchers migrate automatically. + - Windows gets a `subminer.cmd` launcher download. + - First-run setup is reduced to one optional launcher control. Runtime repair guidance appears only when it is needed. +- **Faster Sync Transfers**: + - Sync between compatible macOS and Linux machines now uses compressed, incremental rsync transfers. + - The last snapshot received from each peer is cached, which reduces traffic on later syncs. + - Machines without a compatible rsync, including Windows, fall back to compressed scp. + - Older peers still work without the upload cache. + - Transfers abort after 30 minutes. +- **Stats Server Request Safety**: + - The stats server now accepts loopback hosts only and rejects requests from browser origins other than its own. + - Requests that change data must send an `application/json` body. Scripts that POST to the server need to set a JSON content type. + - The in-app stats overlay now loads from the local server, so it gets the same protection. + - Dashboards served through a reverse proxy or Tailscale Serve are no longer supported. +- **Smaller Downloads**: + - Installers and unpacked apps are smaller. Demo media, source maps, TypeScript sources, test fixtures, and unused Koffi binaries are no longer packaged. + - All windows now share one Japanese UI font. + - Release builds publish package size reports that compare against the previous release. +- **Bundled Yomitan**: Updated with upstream Yomitan 26.9.8 changes, including historical Japanese kana transformations, Ukrainian language support, and improvements to Anki duplicate searches and audio retrieval. + +### Fixed +- **Jellyfin 12 Compatibility**: + - Playback, subtitle, artwork, and remote-control requests now authenticate with the `ApiKey` query parameter, so the integration works on Jellyfin 12, where legacy authorization is off by default. + - "Play on SubMiner" stays available. The cast connection now answers keep-alive requests and reconnects when the server stops responding, instead of silently dying after about a minute. + - The Jellyfin "now playing" bar clears when you close or finish a cast video instead of running on to the end of the episode. + - Cards mined during Jellyfin playback get the episode title in the misc info field again instead of "Unknown media". +- **Jellyfin Privacy and Playback**: + - Jellyfin streams no longer leak titles taken from the stream URL, or stream URLs that contain credentials, into metadata lookups, Anki source fields, Discord presence, stats, or AniList retries. + - Previously cached metadata that contained credentials is cleaned up. Watch history and library assignments are not touched. + - Jellyfin playback and casting now use your configured mpv executable, so they work when mpv is installed outside PATH. Portable plugins next to that executable are detected. +- **Anki Mining**: + - New `ankiConnect.fields.wordAudio` setting reads word audio separately from the sentence audio field. This fixes animated images that started moving immediately when `fields.audio` pointed to `SentenceAudio`. Existing animated images need to be regenerated to pick up the fix. + - Sentence furigana on word cards stays in sync with the full stats-search context and with expanded timing review selections. Stale readings are cleared if regeneration fails. + - Closing the overlay while media timing review is still loading now cancels the review, resumes playback if the review paused it, and cleans up the hidden preview player. + - `ankiConnect.media.maxMediaDuration: 0` now means unlimited when mining from the stats dashboard, matching overlay mining. + - Invalid AnkiConnect, Kiku, and Senren settings are now rejected with a warning and fall back to defaults. +- **Stats Server Stability**: + - A port conflict is now reported in a status notification instead of crashing SubMiner. + - Simultaneous startup requests share one server start. Stopping the background server no longer disconnects dashboards open in the foreground. + - Shutdown waits only a limited time for active requests to finish. + - Malformed resource IDs, and ID lists with any invalid entries, are rejected before Library changes or cover backfills run. +- **Subtitle Sidebar**: + - Clicking a cue no longer leaves the row focused, and Space no longer seeks back to a focused cue. Enter still seeks to the focused cue, and Space keeps its configured playback action. + - The sidebar stays near the current playback position during gaps when the subtitle file has a cue that starts at zero. +- **Settings Save Feedback**: + - Settings marked LIVE no longer show false restart warnings, including for notifications and subtitle generation. + - When a save mixes live and restart-only changes, the live changes apply right away and only the changed sections that need a restart are listed. +- **Overlay Windows**: + - On Hyprland, recovery dialogs stay above SubMiner windows so overlay placement updates no longer cover their Wait and Close buttons. + - On Linux, a delayed close callback during teardown can no longer reopen the overlay. +- **First Launch on macOS**: SubMiner no longer exits on first launch when the config directory does not exist yet. + +### Docs +- **Launcher**: Documented the launcher install that uses the bundled runtime, migration from legacy launchers, package-managed updates, and the bundled Bun runtime's MIT and LGPL notices. The AUR package installs these notices under `/usr/share/licenses/subminer-bin`, and they are also included in `subminer-assets.tar.gz`. +- **Subtitle Generation**: Documented model choice, VAD behavior, splitting guided by a reference track, fallback behavior, and known limits. +- **Subtitle Selection**: Documented the subtitle selector setting, its shortcut override, and the primary and secondary track controls. +- **Settings**: Clarified save feedback for live settings, warnings for saves that mix live and restart-only changes, and how subtitle generation settings reload. +- **Jellyfin**: + - Clarified that Windows mpv playback and Jellyfin casting can use a configured executable path instead of PATH. + - Documented how Jellyfin media titles and stats identities keep stream credentials out of metadata. +- **Stats Library**: + - Documented TMDB linking, provider reassignment, merge compatibility, and caching of the credential command's output. + - Documented YouTube channel filtering and video statistics in the Library. +- **Mining**: + - Documented choosing the screenshot separately in media timing review. + - Documented the separate word audio field mapping, including that existing animated images need to be regenerated. +- **Sync**: Documented compressed transfers, where the incremental sync cache is stored, and compatibility with older peers. + +
+Internal changes + +### Internal +- Removed duplicate source and launcher smoke runs from the reusable CI quality gate. Every distinct test lane and failure artifact is kept. +- Replaced mislabeled dist source reruns with a small Electron-runtime smoke check. It covers compiled stats startup, the HTTP service, native SQLite, port conflicts, and cleanup. + +
+ ## v0.19.6 (2026-09-04) ### Added diff --git a/changes/bundled-bun-runtime-docs.md b/changes/bundled-bun-runtime-docs.md deleted file mode 100644 index b6faa3aa..00000000 --- a/changes/bundled-bun-runtime-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: launcher - -- Documented the bundled-runtime launcher install, legacy launcher migration, package-managed updates, and the bundled Bun runtime's MIT and LGPL notices (installed under `/usr/share/licenses/subminer-bin` by the AUR package and included in `subminer-assets.tar.gz`). diff --git a/changes/bundled-bun-runtime.md b/changes/bundled-bun-runtime.md deleted file mode 100644 index 6faea56a..00000000 --- a/changes/bundled-bun-runtime.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: launcher - -- Every installed and downloadable launcher now uses the Bun runtime bundled with SubMiner instead of a system Bun. Recognized legacy launchers migrate automatically, Windows gets a `subminer.cmd` download, and first-run setup is reduced to a single optional launcher control with runtime repair guidance shown only when needed. diff --git a/changes/ci-test-deduplication.md b/changes/ci-test-deduplication.md deleted file mode 100644 index 7aa0feb0..00000000 --- a/changes/ci-test-deduplication.md +++ /dev/null @@ -1,4 +0,0 @@ -type: internal -area: ci - -- Removed duplicate source and launcher smoke executions from the reusable quality gate while preserving every distinct test lane and failure artifact. diff --git a/changes/compiled-runtime-smoke.md b/changes/compiled-runtime-smoke.md deleted file mode 100644 index 10be32b0..00000000 --- a/changes/compiled-runtime-smoke.md +++ /dev/null @@ -1,4 +0,0 @@ -type: internal -area: verification - -- Replaced mislabeled dist source reruns with a small Electron-runtime smoke check for compiled stats startup, HTTP service, native SQLite, port conflicts, and cleanup. diff --git a/changes/compressed-incremental-sync.md b/changes/compressed-incremental-sync.md deleted file mode 100644 index 13bedad6..00000000 --- a/changes/compressed-incremental-sync.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: sync - -- Sync now uses compressed, incremental rsync transfers between compatible macOS and Linux machines, caching the last received snapshot per peer to cut traffic on later syncs. Machines without compatible rsync (including Windows) fall back to compressed scp, and older peers still work without the upload cache. Transfers abort after 30 minutes. diff --git a/changes/fix-animated-word-audio-sync.md b/changes/fix-animated-word-audio-sync.md deleted file mode 100644 index 0f2ef4bd..00000000 --- a/changes/fix-animated-word-audio-sync.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: anki - -- Added `ankiConnect.fields.wordAudio` so word audio is read separately from the sentence-audio destination, fixing animated images that started moving immediately when `fields.audio` pointed to `SentenceAudio`. diff --git a/changes/fix-anki-config-validation.md b/changes/fix-anki-config-validation.md deleted file mode 100644 index 9cbfa760..00000000 --- a/changes/fix-anki-config-validation.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: config - -- Validate direct AnkiConnect, Kiku, and Senren settings before admitting them to runtime config, with warnings and defaults for invalid values. diff --git a/changes/fix-first-launch-config-directory.md b/changes/fix-first-launch-config-directory.md deleted file mode 100644 index b50c9ef0..00000000 --- a/changes/fix-first-launch-config-directory.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: startup - -- Fixed first launch exiting on macOS when the SubMiner config directory did not yet exist by creating it before acquiring the startup lock. diff --git a/changes/fix-jellyfin-media-identity.md b/changes/fix-jellyfin-media-identity.md deleted file mode 100644 index 846dacfb..00000000 --- a/changes/fix-jellyfin-media-identity.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: jellyfin - -- Jellyfin streams no longer leak URL-derived titles or credential-bearing stream URLs into metadata lookups, Anki source fields, Discord presence, stats identities, or AniList retry keys. Previously cached credential-bearing parser metadata is cleaned up without touching watch history or library assignments. diff --git a/changes/fix-jellyfin-modern-auth.md b/changes/fix-jellyfin-modern-auth.md deleted file mode 100644 index e0e90de5..00000000 --- a/changes/fix-jellyfin-modern-auth.md +++ /dev/null @@ -1,7 +0,0 @@ -type: fixed -area: jellyfin - -- Jellyfin playback, subtitle, artwork, and remote-control URLs now authenticate with the `ApiKey` query parameter instead of the legacy `X-Emby-*` headers, so the integration works on Jellyfin 12 where legacy authorization is disabled by default. -- "Play on SubMiner" keeps working on Jellyfin 12: the cast-target websocket answers keep-alive requests and reconnects when the server stops replying instead of silently dying after about a minute. -- The Jellyfin "now playing" bar clears when you close or finish a cast video instead of running on to the end of the episode. -- Anki cards mined from Jellyfin playback get the episode title in the misc info field again instead of "Unknown media". diff --git a/changes/fix-sidebar-space-seek.md b/changes/fix-sidebar-space-seek.md deleted file mode 100644 index 4d677d8b..00000000 --- a/changes/fix-sidebar-space-seek.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Clicking a subtitle sidebar cue releases row focus, and Space no longer seeks back to a focused cue. Enter still seeks the focused cue, and Space keeps its configured playback action. diff --git a/changes/fix-stats-resource-id-validation.md b/changes/fix-stats-resource-id-validation.md deleted file mode 100644 index c41ed3d1..00000000 --- a/changes/fix-stats-resource-id-validation.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: stats - -- Reject malformed resource IDs and partly invalid ID lists before stats library mutations or cover backfills run. diff --git a/changes/fix-stats-server-lifecycle.md b/changes/fix-stats-server-lifecycle.md deleted file mode 100644 index 7e9e860e..00000000 --- a/changes/fix-stats-server-lifecycle.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: stats - -- Stats server port conflicts are reported through status notifications instead of crashing SubMiner. Startup and shutdown are also more robust: concurrent startup requests are shared, background stop no longer disconnects foreground dashboards, and shutdown bounds how long it waits for active requests. diff --git a/changes/fix-timing-review-cancellation.md b/changes/fix-timing-review-cancellation.md deleted file mode 100644 index ffd18909..00000000 --- a/changes/fix-timing-review-cancellation.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: anki - -- Closing the overlay while media timing review is still loading now cancels setup and modal retries, restores playback if the review paused it, and cleans up the hidden preview player. diff --git a/changes/fix-unlimited-mining-duration.md b/changes/fix-unlimited-mining-duration.md deleted file mode 100644 index 76a58a3c..00000000 --- a/changes/fix-unlimited-mining-duration.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: anki - -- Treat `ankiConnect.media.maxMediaDuration: 0` as unlimited for stats dashboard mining, matching overlay mining and configuration. diff --git a/changes/hyprland-recovery-dialog.md b/changes/hyprland-recovery-dialog.md deleted file mode 100644 index 7cd63c11..00000000 --- a/changes/hyprland-recovery-dialog.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Keep Hyprland recovery dialogs above SubMiner windows so overlay placement updates do not cover their Wait and Close buttons. diff --git a/changes/japanese-subtitle-generation-docs.md b/changes/japanese-subtitle-generation-docs.md deleted file mode 100644 index df868ee1..00000000 --- a/changes/japanese-subtitle-generation-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: subtitles - -- Documented subtitle generation: model choice, VAD behavior, reference-track guided splitting, fallback behavior, and known limits. diff --git a/changes/japanese-subtitle-generation.md b/changes/japanese-subtitle-generation.md deleted file mode 100644 index de255349..00000000 --- a/changes/japanese-subtitle-generation.md +++ /dev/null @@ -1,7 +0,0 @@ -type: added -area: subtitles - -- Generate Japanese SRT subtitles locally with whisper.cpp from a modal (Ctrl+Shift+G), the empty subtitle sidebar's generation button, or `subminer generate-subs`, with progress, cancellation, and automatic loading into mpv. -- Pick an official multilingual model (including quantized variants) with size and accuracy guidance and download it in-app, or point Settings at an existing model. `large-v3-turbo` is recommended when CUDA support is detected, `small` otherwise. whisper-cli, ffmpeg, and ffprobe are found on PATH unless overridden, and missing tools are named before any download starts. -- Optional "Focus on spoken dialogue" mode uses a separately downloadable Silero VAD model, keeping uncertain audible sections so dialogue under music is not dropped (songs may be transcribed too). -- Long passages are split near detected speech starts or quiet pauses, guided by an eligible embedded or external subtitle track already loaded in mpv when one is available, to reduce early subtitle timing. Each passage runs in a fresh Whisper process to avoid repeated-character output. diff --git a/changes/jellyfin-configured-mpv-docs.md b/changes/jellyfin-configured-mpv-docs.md deleted file mode 100644 index 78282abf..00000000 --- a/changes/jellyfin-configured-mpv-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: jellyfin - -- Clarify that Windows mpv playback and Jellyfin casting can use a configured executable path instead of PATH. diff --git a/changes/jellyfin-configured-mpv.md b/changes/jellyfin-configured-mpv.md deleted file mode 100644 index 70d61f28..00000000 --- a/changes/jellyfin-configured-mpv.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: jellyfin - -- Honor the configured mpv executable when Jellyfin starts playback, allowing casting when mpv is installed outside PATH, and detect portable plugins beside the selected executable. diff --git a/changes/jellyfin-media-identity-docs.md b/changes/jellyfin-media-identity-docs.md deleted file mode 100644 index bcd2ede3..00000000 --- a/changes/jellyfin-media-identity-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: jellyfin - -- Documented how Jellyfin media titles and stats identities keep stream credentials out of metadata. diff --git a/changes/jimaku-live-action-search.md b/changes/jimaku-live-action-search.md deleted file mode 100644 index 2a08bc4a..00000000 --- a/changes/jimaku-live-action-search.md +++ /dev/null @@ -1,4 +0,0 @@ -type: added -area: jimaku - -- Jimaku modal gains Anime / Live action tabs so searches can pull from Jimaku's live action catalogue instead of only anime entries (Arrow Left / Right switch tabs). diff --git a/changes/media-review-frame-picker-docs.md b/changes/media-review-frame-picker-docs.md deleted file mode 100644 index da48ad59..00000000 --- a/changes/media-review-frame-picker-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: mining - -- Document independent screenshot selection in media timing review. diff --git a/changes/media-review-frame-picker.md b/changes/media-review-frame-picker.md deleted file mode 100644 index f919dbde..00000000 --- a/changes/media-review-frame-picker.md +++ /dev/null @@ -1,4 +0,0 @@ -type: added -area: mining - -- Choose a still screenshot independently of the audio in media timing review, with a live preview, a separate time slider, and decoded-frame stepping. Local video and seekable remote streams such as Jellyfin are supported. diff --git a/changes/mpv-overlay-bindings.md b/changes/mpv-overlay-bindings.md deleted file mode 100644 index f9cbe3b8..00000000 --- a/changes/mpv-overlay-bindings.md +++ /dev/null @@ -1,4 +0,0 @@ -type: added -area: overlay - -- The overlay picks up non-conflicting keyboard bindings from mpv defaults, `input.conf`, and loaded scripts. SubMiner controls and explicitly disabled bindings take precedence; discovered bindings are session-only and not listed in the help menu. diff --git a/changes/package-size-cleanup.md b/changes/package-size-cleanup.md deleted file mode 100644 index 7395c4ff..00000000 --- a/changes/package-size-cleanup.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: release - -- Reduced installer and unpacked app size by dropping demo media, source maps, TypeScript sources, test fixtures, and unused Koffi binaries from the package, and sharing one Japanese UI font across windows. Release builds now publish package size reports with comparisons against the previous release. diff --git a/changes/remove-package-size-reports.md b/changes/remove-package-size-reports.md new file mode 100644 index 00000000..e21b3c44 --- /dev/null +++ b/changes/remove-package-size-reports.md @@ -0,0 +1,4 @@ +type: changed +area: release + +- Removed package-size JSON reports from future releases and their CI size comparisons. Package-content validation remains enabled. diff --git a/changes/runtime-ownership.md b/changes/runtime-ownership.md deleted file mode 100644 index 5182284b..00000000 --- a/changes/runtime-ownership.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Cancel pending Linux overlay window replacements during teardown so a delayed close callback cannot reopen the overlay. diff --git a/changes/sentence-furigana.md b/changes/sentence-furigana.md deleted file mode 100644 index 94c01506..00000000 --- a/changes/sentence-furigana.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: anki - -- Keep word-card sentence furigana in sync with full stats-search context and expanded timing-review selections. Clear stale readings if regeneration fails. diff --git a/changes/settings-live-save-feedback-docs.md b/changes/settings-live-save-feedback-docs.md deleted file mode 100644 index 827a3e99..00000000 --- a/changes/settings-live-save-feedback-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: config - -- Clarified live-setting save feedback, mixed restart warnings, and subtitle-generation reload behavior. diff --git a/changes/settings-live-save-feedback.md b/changes/settings-live-save-feedback.md deleted file mode 100644 index e51b0365..00000000 --- a/changes/settings-live-save-feedback.md +++ /dev/null @@ -1,5 +0,0 @@ -type: fixed -area: config - -- Settings marked LIVE now use the same reload policy as save results, fixing false restart warnings for notifications and subtitle generation. -- Mixed saves apply live changes and list only sections with changed fields that require a restart. diff --git a/changes/sidebar-selection-copy.md b/changes/sidebar-selection-copy.md deleted file mode 100644 index 25d2afbe..00000000 --- a/changes/sidebar-selection-copy.md +++ /dev/null @@ -1,4 +0,0 @@ -type: added -area: overlay - -- Select dialogue across subtitle sidebar rows and copy it without timestamps using Ctrl/Cmd+C or the Copy button. Selecting does not seek or require mining a card. diff --git a/changes/stats-request-safety.md b/changes/stats-request-safety.md deleted file mode 100644 index cace18db..00000000 --- a/changes/stats-request-safety.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: stats - -- The stats server now rejects requests from non-loopback hosts and browser origins and requires `application/json` for mutation bodies. The in-app stats overlay loads from the local server so it shares the same protection. Reverse-proxied or Tailscale Serve dashboards are unsupported; scripts that POST must set a JSON content type. diff --git a/changes/subtitle-selection-docs.md b/changes/subtitle-selection-docs.md deleted file mode 100644 index 84069255..00000000 --- a/changes/subtitle-selection-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: config - -- Documented the subtitle selector setting, shortcut override, and primary/secondary track controls. diff --git a/changes/subtitle-selection.md b/changes/subtitle-selection.md deleted file mode 100644 index 9e67a2e6..00000000 --- a/changes/subtitle-selection.md +++ /dev/null @@ -1,5 +0,0 @@ -type: added -area: overlay - -- Added an optional Catppuccin subtitle selection modal for primary and secondary mpv tracks. Enable it in Settings under Behavior, then press g followed by s. Disabling it restores mpv's subtitle selection binding. -- Single-key actions take priority over configured sequence prefixes. Conflicting sequences are disabled with a warning, and the existing y commands stay reserved. diff --git a/changes/subtitle-sidebar-gap-follow.md b/changes/subtitle-sidebar-gap-follow.md deleted file mode 100644 index 03d18f75..00000000 --- a/changes/subtitle-sidebar-gap-follow.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: subtitles - -- Keep the subtitle sidebar near playback during gaps when the subtitle file has a cue starting at zero. diff --git a/changes/sync-transfer-docs.md b/changes/sync-transfer-docs.md deleted file mode 100644 index 74f6ec49..00000000 --- a/changes/sync-transfer-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: sync - -- Documented compressed transfers, incremental sync cache storage, and compatibility with older peers. diff --git a/changes/tmdb-linking-docs.md b/changes/tmdb-linking-docs.md deleted file mode 100644 index 470db8ed..00000000 --- a/changes/tmdb-linking-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: stats - -- Documented TMDB linking, provider reassignment, merge compatibility, and credential command caching. diff --git a/changes/tmdb-live-action-library.md b/changes/tmdb-live-action-library.md deleted file mode 100644 index 7298a5c7..00000000 --- a/changes/tmdb-live-action-library.md +++ /dev/null @@ -1,6 +0,0 @@ -type: added -area: stats - -- Live-action dramas and movies in the stats Library get posters, synopses, and titles from TMDB. Release builds include a project key; `tmdb.apiKey` (or `tmdb.apiKeyCommand`) overrides it and is required when running from source. -- Titles AniList cannot match are looked up on TMDB automatically when the parsed filename matches a Japanese live-action title exactly; otherwise use the new **Link to TMDB** action. Entries linked to the same TMDB title merge into one card, and the Library kind selector gained a Live Action option. -- Provider reassignment keeps the previous link and artwork if the replacement download fails. Merges and sync keep AniList and TMDB identities separate, and the merge dialog explains mixed selections instead of failing. diff --git a/changes/word-audio-mapping-docs.md b/changes/word-audio-mapping-docs.md deleted file mode 100644 index b4cf7151..00000000 --- a/changes/word-audio-mapping-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: anki - -- Documented the separate word-audio field mapping; existing animated images need regeneration to pick up the fix. diff --git a/changes/yomitan-upstream-26-9-8.md b/changes/yomitan-upstream-26-9-8.md deleted file mode 100644 index 1e069022..00000000 --- a/changes/yomitan-upstream-26-9-8.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: yomitan - -- Updated bundled Yomitan with upstream 26.9.8 changes, including historical Japanese kana transformations, Ukrainian language support, and improvements to Anki duplicate searches and audio retrieval. diff --git a/changes/youtube-library-kind-docs.md b/changes/youtube-library-kind-docs.md deleted file mode 100644 index bdaa80d1..00000000 --- a/changes/youtube-library-kind-docs.md +++ /dev/null @@ -1,4 +0,0 @@ -type: docs -area: stats - -- Document YouTube channel filtering and video statistics in the Library. diff --git a/changes/youtube-library-kind.md b/changes/youtube-library-kind.md deleted file mode 100644 index 70285c3c..00000000 --- a/changes/youtube-library-kind.md +++ /dev/null @@ -1,5 +0,0 @@ -type: added -area: stats - -- YouTube channels are now a separate Library media kind. Existing channel entries migrate automatically without changing viewing history or manual video assignments. -- Added All Titles, Anime, and YouTube Library filters. Channels stay out of AniList matching, season repair, and duplicate recommendations, and merges or video moves cannot mix an anime entry with a YouTube channel. diff --git a/docs-site/changelog.md b/docs-site/changelog.md index 95b712dc..4717bb91 100644 --- a/docs-site/changelog.md +++ b/docs-site/changelog.md @@ -1,6 +1,132 @@ # Changelog -## v0.19.6 (2026-09-04) +## v0.20.0 (2026-09-23) + +**Added** +- **Japanese Subtitle Generation**: + - Generate Japanese SRT subtitles locally with whisper.cpp. Start it from a new modal (Ctrl+Shift+G), from the generate button in an empty subtitle sidebar, or with `subminer generate-subs`. + - Generation shows progress, can be cancelled, and loads the finished subtitles into mpv automatically. + - Pick an official multilingual Whisper model, including quantized variants, with size and accuracy guidance. You can download it in the app or point Settings at a model you already have. + - `large-v3-turbo` is recommended when CUDA support is detected, and `small` otherwise. + - whisper-cli, ffmpeg, and ffprobe are found on PATH unless you override them. SubMiner names any missing tools before a download starts. + - An optional "Focus on spoken dialogue" mode uses a separately downloaded Silero VAD model. It keeps audible sections it is unsure about, so dialogue under music is not dropped, but songs may also be transcribed. + - Long passages are split near speech starts or quiet pauses to reduce subtitles that appear too early. When an eligible embedded or external subtitle track is loaded in mpv, it guides the split points. + - Each passage runs in a fresh Whisper process, which prevents repeated-character output. +- **Subtitle Selection Modal**: + - An optional modal for choosing primary and secondary mpv subtitle tracks. + - Turn it on in Settings under Behavior, then press g followed by s to open it. Turning it off restores mpv's own subtitle selection binding. + - Single-key actions take priority over configured key sequence prefixes. + - Conflicting sequences are disabled with a warning, and the existing y commands stay reserved. +- **Subtitle Sidebar Copy**: + - Select dialogue across several sidebar rows and copy it without timestamps using Ctrl/Cmd+C or the Copy button. + - Selecting text does not seek playback and does not require mining a card. +- **Media Timing Review Screenshot Picker**: + - Choose the still screenshot separately from the audio range, with a live preview and its own time slider. + - Step through decoded frames one at a time to get the exact frame you want. + - Works with local video and with seekable remote streams such as Jellyfin. +- **mpv Keybindings in the Overlay**: + - The overlay now picks up keyboard bindings from mpv defaults, `input.conf`, and loaded scripts when they do not conflict with SubMiner. + - SubMiner controls and bindings you explicitly disabled take precedence. + - These bindings apply only to the current session and are not listed in the help menu. +- **Jimaku Live Action Search**: The Jimaku modal has new Anime and Live action tabs, so you can search Jimaku's live action catalogue as well as anime. Use Arrow Left and Arrow Right to switch tabs. +- **TMDB Live-Action Library**: + - Live-action dramas and movies in the stats Library now get posters, synopses, and titles from TMDB. + - Release builds include a project key. Setting `tmdb.apiKey` or `tmdb.apiKeyCommand` overrides it, and one of them is required when running from source. + - Titles that AniList cannot match are looked up on TMDB automatically when the parsed filename exactly matches a Japanese live-action title. For everything else, use the new **Link to TMDB** action. + - Entries linked to the same TMDB title merge into one card, and the Library kind selector has a new Live Action option. + - If a replacement download fails during provider reassignment, the previous link and artwork are kept. Merges and sync keep AniList and TMDB identities separate, and the merge dialog explains mixed selections instead of failing. +- **YouTube Library Kind**: + - YouTube channels are now their own Library media kind. Existing channel entries migrate automatically, and viewing history and manual video assignments are unchanged. + - New All Titles, Anime, and YouTube Library filters. + - Channels are excluded from AniList matching, season repair, and duplicate recommendations. + - Merges and video moves can no longer combine an anime entry with a YouTube channel. + +**Changed** +- **Launcher Uses Bundled Bun**: + - Every installed and downloadable launcher now runs on the Bun runtime that ships with SubMiner. A system Bun is no longer needed. + - Recognized legacy launchers migrate automatically. + - Windows gets a `subminer.cmd` launcher download. + - First-run setup is reduced to one optional launcher control. Runtime repair guidance appears only when it is needed. +- **Faster Sync Transfers**: + - Sync between compatible macOS and Linux machines now uses compressed, incremental rsync transfers. + - The last snapshot received from each peer is cached, which reduces traffic on later syncs. + - Machines without a compatible rsync, including Windows, fall back to compressed scp. + - Older peers still work without the upload cache. + - Transfers abort after 30 minutes. +- **Stats Server Request Safety**: + - The stats server now accepts loopback hosts only and rejects requests from browser origins other than its own. + - Requests that change data must send an `application/json` body. Scripts that POST to the server need to set a JSON content type. + - The in-app stats overlay now loads from the local server, so it gets the same protection. + - Dashboards served through a reverse proxy or Tailscale Serve are no longer supported. +- **Smaller Downloads**: + - Installers and unpacked apps are smaller. Demo media, source maps, TypeScript sources, test fixtures, and unused Koffi binaries are no longer packaged. + - All windows now share one Japanese UI font. + - Release builds publish package size reports that compare against the previous release. +- **Bundled Yomitan**: Updated with upstream Yomitan 26.9.8 changes, including historical Japanese kana transformations, Ukrainian language support, and improvements to Anki duplicate searches and audio retrieval. + +**Fixed** +- **Jellyfin 12 Compatibility**: + - Playback, subtitle, artwork, and remote-control requests now authenticate with the `ApiKey` query parameter, so the integration works on Jellyfin 12, where legacy authorization is off by default. + - "Play on SubMiner" stays available. The cast connection now answers keep-alive requests and reconnects when the server stops responding, instead of silently dying after about a minute. + - The Jellyfin "now playing" bar clears when you close or finish a cast video instead of running on to the end of the episode. + - Cards mined during Jellyfin playback get the episode title in the misc info field again instead of "Unknown media". +- **Jellyfin Privacy and Playback**: + - Jellyfin streams no longer leak titles taken from the stream URL, or stream URLs that contain credentials, into metadata lookups, Anki source fields, Discord presence, stats, or AniList retries. + - Previously cached metadata that contained credentials is cleaned up. Watch history and library assignments are not touched. + - Jellyfin playback and casting now use your configured mpv executable, so they work when mpv is installed outside PATH. Portable plugins next to that executable are detected. +- **Anki Mining**: + - New `ankiConnect.fields.wordAudio` setting reads word audio separately from the sentence audio field. This fixes animated images that started moving immediately when `fields.audio` pointed to `SentenceAudio`. Existing animated images need to be regenerated to pick up the fix. + - Sentence furigana on word cards stays in sync with the full stats-search context and with expanded timing review selections. Stale readings are cleared if regeneration fails. + - Closing the overlay while media timing review is still loading now cancels the review, resumes playback if the review paused it, and cleans up the hidden preview player. + - `ankiConnect.media.maxMediaDuration: 0` now means unlimited when mining from the stats dashboard, matching overlay mining. + - Invalid AnkiConnect, Kiku, and Senren settings are now rejected with a warning and fall back to defaults. +- **Stats Server Stability**: + - A port conflict is now reported in a status notification instead of crashing SubMiner. + - Simultaneous startup requests share one server start. Stopping the background server no longer disconnects dashboards open in the foreground. + - Shutdown waits only a limited time for active requests to finish. + - Malformed resource IDs, and ID lists with any invalid entries, are rejected before Library changes or cover backfills run. +- **Subtitle Sidebar**: + - Clicking a cue no longer leaves the row focused, and Space no longer seeks back to a focused cue. Enter still seeks to the focused cue, and Space keeps its configured playback action. + - The sidebar stays near the current playback position during gaps when the subtitle file has a cue that starts at zero. +- **Settings Save Feedback**: + - Settings marked LIVE no longer show false restart warnings, including for notifications and subtitle generation. + - When a save mixes live and restart-only changes, the live changes apply right away and only the changed sections that need a restart are listed. +- **Overlay Windows**: + - On Hyprland, recovery dialogs stay above SubMiner windows so overlay placement updates no longer cover their Wait and Close buttons. + - On Linux, a delayed close callback during teardown can no longer reopen the overlay. +- **First Launch on macOS**: SubMiner no longer exits on first launch when the config directory does not exist yet. + +**Docs** +- **Launcher**: Documented the launcher install that uses the bundled runtime, migration from legacy launchers, package-managed updates, and the bundled Bun runtime's MIT and LGPL notices. The AUR package installs these notices under `/usr/share/licenses/subminer-bin`, and they are also included in `subminer-assets.tar.gz`. +- **Subtitle Generation**: Documented model choice, VAD behavior, splitting guided by a reference track, fallback behavior, and known limits. +- **Subtitle Selection**: Documented the subtitle selector setting, its shortcut override, and the primary and secondary track controls. +- **Settings**: Clarified save feedback for live settings, warnings for saves that mix live and restart-only changes, and how subtitle generation settings reload. +- **Jellyfin**: + - Clarified that Windows mpv playback and Jellyfin casting can use a configured executable path instead of PATH. + - Documented how Jellyfin media titles and stats identities keep stream credentials out of metadata. +- **Stats Library**: + - Documented TMDB linking, provider reassignment, merge compatibility, and caching of the credential command's output. + - Documented YouTube channel filtering and video statistics in the Library. +- **Mining**: + - Documented choosing the screenshot separately in media timing review. + - Documented the separate word audio field mapping, including that existing animated images need to be regenerated. +- **Sync**: Documented compressed transfers, where the incremental sync cache is stored, and compatibility with older peers. + +
+Internal changes + +**Internal** +- Removed duplicate source and launcher smoke runs from the reusable CI quality gate. Every distinct test lane and failure artifact is kept. +- Replaced mislabeled dist source reruns with a small Electron-runtime smoke check. It covers compiled stats startup, the HTTP service, native SQLite, port conflicts, and cleanup. + +
+ +## Previous Versions + +
+v0.19.x + +

v0.19.6 (2026-09-04)

**Added** @@ -31,7 +157,7 @@ - **Jellyfin Subtitle Sync**: Jellyfin subtitle files now load with zero mpv delay instead of inferring and saving an offset from Japanese and English cue timelines. - **Secondary Subtitle Visibility**: Native mpv secondary subtitles stay hidden when switching secondary subtitle tracks during playback. -## v0.19.5 (2026-08-30) +

v0.19.5 (2026-08-30)

**Fixed** @@ -46,7 +172,7 @@ - Long speech is paged instead of covering the video with a wall of text. - Explicitly timed sound cues like `[音楽]` no longer cover later dialogue. -## v0.19.4 (2026-08-25) +

v0.19.4 (2026-08-25)

**Added** - **Library Merge & Move**: Duplicate library cards for the same show can now be combined. Select cards in the library grid and use "Merge Selected" to pick which entry to keep and move every episode onto it, preserving sessions, mined cards, and watch time. Episodes can also be reassigned individually via the "→" button, useful when a file lands under a stray title; manual assignments survive later filename parsing, Jellyfin refreshes, and season repair. Exact AniList title matches with compatible seasons now merge automatically, while fuzzy matches surface as dismissible "Possible duplicate" reviews instead of merging silently. @@ -87,7 +213,7 @@
-## v0.19.3 (2026-08-13) +

v0.19.3 (2026-08-13)

**Added** - Changelog Modal: Adds an in-app changelog you can open from the tray ("View Changelog") or the "What's New" button on the update notification, so the notification stays reachable while you read. It shows the newest published release notes (falling back to the bundled changelog if that fetch fails), folds older versions while keeping the current one expanded, and supports keyboard navigation (`J`/`K`/arrows, `Enter`, `R`, `Esc`). @@ -111,7 +237,7 @@ -## v0.19.2 (2026-08-04) +

v0.19.2 (2026-08-04)

**Changed** - Subsync: The sync modal now lets you choose both the reference subtitle (correct timing) and the out-of-sync subtitle to retime, for both alass and ffsubsync. alass can also use the loaded video's audio as a reference for local files. Retiming the secondary track now reloads the result into the secondary slot instead of overwriting the primary subtitle. @@ -129,7 +255,7 @@ -## v0.19.1 (2026-08-01) +

v0.19.1 (2026-08-01)

**Added** - Word Card Type: Adds a setting (Settings > Mining/Anki > Kiku/Lapis Features > "Word Card Type") to choose which card-type flag SubMiner marks on Kiku/Lapis word cards — `word-and-sentence` (default), `click`, `sentence`, `audio`, or `none`. Click cards (`IsClickCard`) can now be flagged, and setting any card-type flag clears the others so a note can't claim two types at once. @@ -138,7 +264,7 @@ - Yomitan Popup: Fixes the macOS Yomitan popup going inert after mining a card — clicks outside the popup no longer pass through to mpv, and scrolling over the popup scrolls its definitions instead of seeking playback. - YouTube Playlist Links: Fixes opening a video from a playlist URL (e.g. a Watch Later link with `list=`/`index=`) timing out while probing subtitles, metadata, or the playback URL. -## v0.19.0 (2026-07-29) +

v0.19.0 (2026-07-29)

**Added** - Anki Maturity Highlighting: Known-word subtitle highlights can now be colored by Anki card maturity (new, learning, young, mature), similar to asbplayer. Tier thresholds and colors are configurable, with a runtime toggle and an updated help legend. @@ -174,7 +300,7 @@ -## Previous Versions +
v0.18.x diff --git a/docs/RELEASING.md b/docs/RELEASING.md index ce583053..e11b7e91 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -6,12 +6,12 @@ - `claude` (Claude Code CLI) installed, on `PATH`, and authenticated. `changelog:build` and `changelog:prerelease-notes` invoke - `claude -p --model sonnet` to merge and rewrite `changes/*.md` fragments into + `claude -p --model opus --effort medium` to merge and rewrite `changes/*.md` fragments into a polished, user-facing release body. Either OAuth login (`claude /login`) or `ANTHROPIC_API_KEY` works. Install from if you don't already have it. -## Package contents and size checks +## Package contents checks Stable and prerelease workflows share `.github/workflows/package-release.yml`. Both callers explicitly pass the five required macOS signing/notarization @@ -19,11 +19,9 @@ secrets plus the optional `SUBMINER_TMDB_API_KEY` (the project TMDB key that `scripts/prepare-build-assets.mjs` stages into `dist/bundled-integration-keys.json`; artifacts built without it simply require users to set `tmdb.apiKey`). `GITHUB_TOKEN` remains automatically available to the reusable workflow. -Each platform verifies its ASAR and external resources before signing, then -measures the signed app and installers before upload. Missing runtime assets, -foreign SQLite/Koffi binaries, duplicate UI fonts, demo media, source maps, -TypeScript files, and nested test or fixture directories -fail the build. Size measurements are informational and do not block releases. +Each platform verifies its ASAR and external resources before signing. Missing +runtime assets, foreign SQLite/Koffi binaries, duplicate UI fonts, demo media, +source maps, TypeScript files, and nested test or fixture directories fail the build. Current targets are Linux x64, macOS arm64, and Windows x64. The runtime allowlist includes `dist/`, `stats/dist/`, and @@ -34,14 +32,6 @@ exclusion-only platform list as a separate include-all matcher. Windows keeps only its target Koffi binary; other platforms omit Koffi. Desktop UIs share the original M PLUS 1 TTF in `dist/fonts/`. -`release/package-size--.json` reports unpacked bytes, largest -files inside and outside ASAR, native binaries, and compressed artifact sizes. -Framework symlinks are not counted twice. Reports are checksummed and published. -CI downloads the preceding release's reports for comparison; older releases -without reports skip comparison. Review the inventory and reason for growth -when comparing releases. An AppImage normally -runs compressed; its extracted size is a separate measurement. - The shared workflow runs `bun run test:package ` with the pinned Electron runtime and temporary user data. On headless Linux, prefix it with `xvfb-run -a`. This checks packaged SQLite, Windows FFI loading/polling, @@ -152,7 +142,7 @@ Notes: - Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication. - Stable release tags update `https://docs.subminer.moe/` and `https://docs.subminer.moe/v//` through `.github/workflows/docs-pages.yml`; `/main/` continues to show development docs from `main`. - Keep Cloudflare Pages Git auto-deploy disabled for `docs.subminer.moe`. Production docs are direct-uploaded by Wrangler from GitHub Actions with `--branch main`. -- AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed. +- AUR publish is best-effort: the workflow downloads the three known assets directly from the tagged release URLs, avoiding GitHub's sometimes-stale release asset listing. Downloads and SSH clone/push operations retry transient failures, then warn and skip AUR publication if retries are exhausted. Follow up with a manual `git push aur master` from the AUR checkout when needed. - Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation. - Release and prerelease workflows upload updater metadata (`latest*.yml`) and blockmaps (`*.blockmap`) alongside platform artifacts. Do not remove those files while `electron-updater` is enabled. - Release and prerelease workflows publish `subminer` for POSIX systems and `subminer.cmd` for Windows. Both locate a packaged app and use its private Bun runtime. Keep the corresponding-source archive named `bun-v1.3.5-source.tar.gz`. diff --git a/docs/workflow/verification.md b/docs/workflow/verification.md index 16ad4fc2..fbdceedd 100644 --- a/docs/workflow/verification.md +++ b/docs/workflow/verification.md @@ -60,9 +60,9 @@ bun run docs:build - Build/release scripts (`scripts/**`): `bun run test:scripts` - Packaging: build the platform package, then run `bun run test:package `. On headless Linux: `xvfb-run -a bun run test:package release/linux-unpacked/resources`. - Content checks and informational size reporting run inside electron-builder hooks. See the - [release guide](../RELEASING.md#package-contents-and-size-checks) for size reports - and the installed-app verification checklist. + Content checks run inside the electron-builder afterPack hook. See the + [release guide](../RELEASING.md#package-contents-checks) for the + installed-app verification checklist. - Dictionary backend windows: after a full build, run `xvfb-run -a bun run test:dictionary:electron` on headless Linux. It uses temporary profiles to check both named settings commands, backend session isolation, the overlay's external-link bridge, and Hachidori's native dictionary parser. - Coverage for the maintained source lane: `bun run test:coverage:src` - Deep/local full gate: default handoff gate above diff --git a/package.json b/package.json index 053c18dd..b39caee8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "subminer", "productName": "SubMiner", "desktopName": "SubMiner.desktop", - "version": "0.20.0-beta.1", + "version": "0.20.0", "description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration", "packageManager": "bun@1.3.5", "main": "dist/main-entry.js", @@ -275,8 +275,7 @@ "from": "CHANGELOG.md", "to": "CHANGELOG.md" } - ], - "afterAllArtifactBuild": "scripts/package-audit.cjs" + ] }, "patchedDependencies": { "@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch" diff --git a/release/prerelease-notes.md b/release/prerelease-notes.md index 75d5e852..60889b1f 100644 --- a/release/prerelease-notes.md +++ b/release/prerelease-notes.md @@ -1,8 +1,16 @@ > This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release. - + + +## Changes since v0.20.0-beta.1 + +- Added an optional subtitle selection modal for primary and secondary mpv subtitle tracks. Enable it in Settings > Behavior, then press `g` followed by `s`. Disabling it restores mpv's subtitle selection binding. + - Single-key shortcut actions now take priority over configured multi-key sequence prefixes, conflicting sequences are disabled with a warning, and existing `y` commands remain reserved. +- Jellyfin casting and playback now honor a configured mpv executable path, allowing playback when mpv is installed outside the system PATH, and portable plugins located beside that executable are now detected. +- Fixed word-card sentence furigana falling out of sync with full stats-search context and expanded timing-review selections; stale furigana is now cleared if regeneration fails. ## Highlights + ### Added - **Japanese Subtitle Generation**: @@ -17,6 +25,11 @@ - **Overlay Keybinding Pickup**: The overlay now recognizes your mpv keybindings (from mpv's defaults, `input.conf`, and loaded scripts) as long as they don't conflict with SubMiner's own controls. Picked-up bindings work for the session but won't show up in the help menu. +- **Subtitle Selection Modal**: + - An optional subtitle selection modal lets you pick primary and secondary mpv subtitle tracks without leaving the overlay. + - Enable it in Settings under Behavior, then trigger it with `g` followed by `s`; turning it off restores mpv's normal subtitle selection binding. + - Single-key shortcuts always take priority over multi-key sequences, and any conflicting sequence is disabled with a warning instead of misbehaving. + - **Subtitle Sidebar Selection & Copy**: You can now select dialogue across multiple subtitle sidebar rows and copy it, without timestamps, using Ctrl/Cmd+C or the Copy button, without seeking or mining a card. - **Jimaku Live Action Search**: The Jimaku modal has separate Anime and Live Action tabs (switch with Arrow Left/Right) so you can search Jimaku's live-action subtitle catalogue directly. @@ -47,6 +60,7 @@ - **Jellyfin**: - Playback, subtitles, artwork, and remote control now authenticate with an `ApiKey` parameter instead of legacy headers, so Jellyfin 12 works correctly even with legacy authorization disabled. - "Play on SubMiner" no longer silently drops the connection after about a minute on Jellyfin 12. + - Casting now honors your configured mpv executable path, so playback works and portable plugins are detected correctly when mpv isn't on PATH. - The "now playing" bar clears when you close or finish a cast video instead of running to the end of the episode. - Anki cards mined from Jellyfin now get the real episode title in the misc info field instead of "Unknown media". - Jellyfin streams no longer leak URL-derived titles or credential-bearing URLs into metadata, Anki fields, Discord presence, stats, or AniList lookups; previously cached data that had credentials in it is cleaned up automatically. @@ -55,6 +69,7 @@ - Word audio now reads from its own configured field (`ankiConnect.fields.wordAudio`) instead of the sentence-audio field, fixing animated word images that started moving immediately instead of on demand. - Setting `ankiConnect.media.maxMediaDuration` to `0` for unlimited duration now also applies when mining from the stats dashboard, matching overlay mining. - Closing the overlay while a media timing review is still loading now properly cancels setup, restores playback if the review had paused it, and cleans up the hidden preview player. + - Word-card sentence furigana now stays in sync with the full stats-search context and expanded timing-review selections, and clears stale readings automatically if regeneration fails. - **Settings**: - AnkiConnect, Kiku, and Senren settings are now validated before use, with a warning and a safe default for anything invalid instead of a bad value reaching runtime. @@ -98,6 +113,9 @@ - fix(overlay): cancel pending window transitions and timing reviews by @ksyasuda in #262 - fix(stats): restrict local requests and serve the dashboard over HTTP by @ksyasuda in #263 - fix(jellyfin): support modern authentication by @ksyasuda in #264 +- feat(overlay): add optional subtitle selection modal by @ksyasuda in #265 +- fix(jellyfin): respect Windows mpv configuration when casting by @aalhendi in #267 +- fix(anki): regenerate sentence furigana from the final sentence by @ksyasuda in #268 ## New Contributors diff --git a/release/release-notes.md b/release/release-notes.md new file mode 100644 index 00000000..93dcd0ef --- /dev/null +++ b/release/release-notes.md @@ -0,0 +1,159 @@ +## Highlights +### Added +- **Japanese Subtitle Generation**: + - Generate Japanese SRT subtitles locally with whisper.cpp. Start it from a new modal (Ctrl+Shift+G), from the generate button in an empty subtitle sidebar, or with `subminer generate-subs`. + - Generation shows progress, can be cancelled, and loads the finished subtitles into mpv automatically. + - Pick an official multilingual Whisper model, including quantized variants, with size and accuracy guidance. You can download it in the app or point Settings at a model you already have. + - `large-v3-turbo` is recommended when CUDA support is detected, and `small` otherwise. + - whisper-cli, ffmpeg, and ffprobe are found on PATH unless you override them. SubMiner names any missing tools before a download starts. + - An optional "Focus on spoken dialogue" mode uses a separately downloaded Silero VAD model. It keeps audible sections it is unsure about, so dialogue under music is not dropped, but songs may also be transcribed. + - Long passages are split near speech starts or quiet pauses to reduce subtitles that appear too early. When an eligible embedded or external subtitle track is loaded in mpv, it guides the split points. + - Each passage runs in a fresh Whisper process, which prevents repeated-character output. +- **Subtitle Selection Modal**: + - An optional modal for choosing primary and secondary mpv subtitle tracks. + - Turn it on in Settings under Behavior, then press g followed by s to open it. Turning it off restores mpv's own subtitle selection binding. + - Single-key actions take priority over configured key sequence prefixes. + - Conflicting sequences are disabled with a warning, and the existing y commands stay reserved. +- **Subtitle Sidebar Copy**: + - Select dialogue across several sidebar rows and copy it without timestamps using Ctrl/Cmd+C or the Copy button. + - Selecting text does not seek playback and does not require mining a card. +- **Media Timing Review Screenshot Picker**: + - Choose the still screenshot separately from the audio range, with a live preview and its own time slider. + - Step through decoded frames one at a time to get the exact frame you want. + - Works with local video and with seekable remote streams such as Jellyfin. +- **mpv Keybindings in the Overlay**: + - The overlay now picks up keyboard bindings from mpv defaults, `input.conf`, and loaded scripts when they do not conflict with SubMiner. + - SubMiner controls and bindings you explicitly disabled take precedence. + - These bindings apply only to the current session and are not listed in the help menu. +- **Jimaku Live Action Search**: The Jimaku modal has new Anime and Live action tabs, so you can search Jimaku's live action catalogue as well as anime. Use Arrow Left and Arrow Right to switch tabs. +- **TMDB Live-Action Library**: + - Live-action dramas and movies in the stats Library now get posters, synopses, and titles from TMDB. + - Release builds include a project key. Setting `tmdb.apiKey` or `tmdb.apiKeyCommand` overrides it, and one of them is required when running from source. + - Titles that AniList cannot match are looked up on TMDB automatically when the parsed filename exactly matches a Japanese live-action title. For everything else, use the new **Link to TMDB** action. + - Entries linked to the same TMDB title merge into one card, and the Library kind selector has a new Live Action option. + - If a replacement download fails during provider reassignment, the previous link and artwork are kept. Merges and sync keep AniList and TMDB identities separate, and the merge dialog explains mixed selections instead of failing. +- **YouTube Library Kind**: + - YouTube channels are now their own Library media kind. Existing channel entries migrate automatically, and viewing history and manual video assignments are unchanged. + - New All Titles, Anime, and YouTube Library filters. + - Channels are excluded from AniList matching, season repair, and duplicate recommendations. + - Merges and video moves can no longer combine an anime entry with a YouTube channel. + +### Changed +- **Launcher Uses Bundled Bun**: + - Every installed and downloadable launcher now runs on the Bun runtime that ships with SubMiner. A system Bun is no longer needed. + - Recognized legacy launchers migrate automatically. + - Windows gets a `subminer.cmd` launcher download. + - First-run setup is reduced to one optional launcher control. Runtime repair guidance appears only when it is needed. +- **Faster Sync Transfers**: + - Sync between compatible macOS and Linux machines now uses compressed, incremental rsync transfers. + - The last snapshot received from each peer is cached, which reduces traffic on later syncs. + - Machines without a compatible rsync, including Windows, fall back to compressed scp. + - Older peers still work without the upload cache. + - Transfers abort after 30 minutes. +- **Stats Server Request Safety**: + - The stats server now accepts loopback hosts only and rejects requests from browser origins other than its own. + - Requests that change data must send an `application/json` body. Scripts that POST to the server need to set a JSON content type. + - The in-app stats overlay now loads from the local server, so it gets the same protection. + - Dashboards served through a reverse proxy or Tailscale Serve are no longer supported. +- **Smaller Downloads**: + - Installers and unpacked apps are smaller. Demo media, source maps, TypeScript sources, test fixtures, and unused Koffi binaries are no longer packaged. + - All windows now share one Japanese UI font. + - Release builds publish package size reports that compare against the previous release. +- **Bundled Yomitan**: Updated with upstream Yomitan 26.9.8 changes, including historical Japanese kana transformations, Ukrainian language support, and improvements to Anki duplicate searches and audio retrieval. + +### Fixed +- **Jellyfin 12 Compatibility**: + - Playback, subtitle, artwork, and remote-control requests now authenticate with the `ApiKey` query parameter, so the integration works on Jellyfin 12, where legacy authorization is off by default. + - "Play on SubMiner" stays available. The cast connection now answers keep-alive requests and reconnects when the server stops responding, instead of silently dying after about a minute. + - The Jellyfin "now playing" bar clears when you close or finish a cast video instead of running on to the end of the episode. + - Cards mined during Jellyfin playback get the episode title in the misc info field again instead of "Unknown media". +- **Jellyfin Privacy and Playback**: + - Jellyfin streams no longer leak titles taken from the stream URL, or stream URLs that contain credentials, into metadata lookups, Anki source fields, Discord presence, stats, or AniList retries. + - Previously cached metadata that contained credentials is cleaned up. Watch history and library assignments are not touched. + - Jellyfin playback and casting now use your configured mpv executable, so they work when mpv is installed outside PATH. Portable plugins next to that executable are detected. +- **Anki Mining**: + - New `ankiConnect.fields.wordAudio` setting reads word audio separately from the sentence audio field. This fixes animated images that started moving immediately when `fields.audio` pointed to `SentenceAudio`. Existing animated images need to be regenerated to pick up the fix. + - Sentence furigana on word cards stays in sync with the full stats-search context and with expanded timing review selections. Stale readings are cleared if regeneration fails. + - Closing the overlay while media timing review is still loading now cancels the review, resumes playback if the review paused it, and cleans up the hidden preview player. + - `ankiConnect.media.maxMediaDuration: 0` now means unlimited when mining from the stats dashboard, matching overlay mining. + - Invalid AnkiConnect, Kiku, and Senren settings are now rejected with a warning and fall back to defaults. +- **Stats Server Stability**: + - A port conflict is now reported in a status notification instead of crashing SubMiner. + - Simultaneous startup requests share one server start. Stopping the background server no longer disconnects dashboards open in the foreground. + - Shutdown waits only a limited time for active requests to finish. + - Malformed resource IDs, and ID lists with any invalid entries, are rejected before Library changes or cover backfills run. +- **Subtitle Sidebar**: + - Clicking a cue no longer leaves the row focused, and Space no longer seeks back to a focused cue. Enter still seeks to the focused cue, and Space keeps its configured playback action. + - The sidebar stays near the current playback position during gaps when the subtitle file has a cue that starts at zero. +- **Settings Save Feedback**: + - Settings marked LIVE no longer show false restart warnings, including for notifications and subtitle generation. + - When a save mixes live and restart-only changes, the live changes apply right away and only the changed sections that need a restart are listed. +- **Overlay Windows**: + - On Hyprland, recovery dialogs stay above SubMiner windows so overlay placement updates no longer cover their Wait and Close buttons. + - On Linux, a delayed close callback during teardown can no longer reopen the overlay. +- **First Launch on macOS**: SubMiner no longer exits on first launch when the config directory does not exist yet. + +### Docs +- **Launcher**: Documented the launcher install that uses the bundled runtime, migration from legacy launchers, package-managed updates, and the bundled Bun runtime's MIT and LGPL notices. The AUR package installs these notices under `/usr/share/licenses/subminer-bin`, and they are also included in `subminer-assets.tar.gz`. +- **Subtitle Generation**: Documented model choice, VAD behavior, splitting guided by a reference track, fallback behavior, and known limits. +- **Subtitle Selection**: Documented the subtitle selector setting, its shortcut override, and the primary and secondary track controls. +- **Settings**: Clarified save feedback for live settings, warnings for saves that mix live and restart-only changes, and how subtitle generation settings reload. +- **Jellyfin**: + - Clarified that Windows mpv playback and Jellyfin casting can use a configured executable path instead of PATH. + - Documented how Jellyfin media titles and stats identities keep stream credentials out of metadata. +- **Stats Library**: + - Documented TMDB linking, provider reassignment, merge compatibility, and caching of the credential command's output. + - Documented YouTube channel filtering and video statistics in the Library. +- **Mining**: + - Documented choosing the screenshot separately in media timing review. + - Documented the separate word audio field mapping, including that existing animated images need to be regenerated. +- **Sync**: Documented compressed transfers, where the incremental sync cache is stored, and compatibility with older peers. + +## What's Changed + +- feat(sidebar): add dialogue selection and copying by @ksyasuda in #238 +- feat(subtitles): add local Japanese subtitle generation by @ksyasuda in #240 +- perf(stats): use compressed incremental snapshot transfers by @ksyasuda in #241 +- fix(startup): create config directory before singleton lock by @ksyasuda in #242 +- feat(launcher): bundle Bun and use it across all launchers by @ksyasuda in #243 +- build(release): reduce package size and report release sizes by @ksyasuda in #244 +- fix(overlay): keep Hyprland recovery dialogs above overlays by @ksyasuda in #245 +- feat(overlay): discover unclaimed mpv key bindings by @ksyasuda in #246 +- fix(sidebar): preserve Space playback after cue seeking by @ksyasuda in #247 +- fix(jellyfin): fix jellyfin media metadata by @ksyasuda in #250 +- feat(jimaku): add live-action subtitle search by @ksyasuda in #251 +- feat(stats): add TMDB metadata for live-action dramas in the Library by @ksyasuda in #252 +- feat(stats): separate YouTube channels in the Library by @ksyasuda in #253 +- feat(mining): add a screenshot frame picker to media review by @aalhendi in #254 +- fix(config): align live save feedback with hot reload policy by @ksyasuda in #255 +- fix(anki): separate word audio mapping for animation sync by @ksyasuda in #256 +- fix(config): validate AnkiConnect and field grouping settings by @ksyasuda in #257 +- fix(anki): honor unlimited duration in stats mining by @ksyasuda in #258 +- fix(stats): reject malformed resource IDs before mutations by @ksyasuda in #259 +- fix(stats): harden server lifecycle and verify compiled runtime by @ksyasuda in #261 +- fix(overlay): cancel pending window transitions and timing reviews by @ksyasuda in #262 +- fix(stats): restrict local requests and serve the dashboard over HTTP by @ksyasuda in #263 +- fix(jellyfin): support modern authentication by @ksyasuda in #264 +- feat(overlay): add optional subtitle selection modal by @ksyasuda in #265 +- fix(jellyfin): respect Windows mpv configuration when casting by @aalhendi in #267 +- fix(anki): regenerate sentence furigana from the final sentence by @ksyasuda in #268 + +## New Contributors + +- @aalhendi made their first contribution in #254 + +## Installation + +See the README and docs/installation guide for full setup steps. + +## Assets + +- Linux: `SubMiner.AppImage` +- macOS: `SubMiner-*.dmg` and `SubMiner-*.zip` +- Windows: `SubMiner-*.exe` and `SubMiner-*-win.zip` +- Optional extras: `subminer-assets.tar.gz`, the `subminer` launcher, and the Windows `subminer.cmd` launcher +- Bun corresponding source: `bun-v1.3.5-source.tar.gz` and its `.sha256` file + +Both launcher downloads use Bun included with the SubMiner app. Download `subminer` on Linux or macOS and `subminer.cmd` on Windows. + +The app bundles an unmodified Bun 1.3.5 runtime. Bun is MIT licensed and statically links JavaScriptCore (LGPL 2.0) and TinyCC (LGPL 2.1). License texts and third-party notices ship inside the app under `resources/bun/licenses`, and the source archive above contains the matching Bun, WebKit, and dependency sources for relinking. diff --git a/scripts/aur-release-download.test.ts b/scripts/aur-release-download.test.ts new file mode 100644 index 00000000..4dfaa76a --- /dev/null +++ b/scripts/aur-release-download.test.ts @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { test } from 'bun:test'; + +test.each([false, true])( + 'AUR downloads handle empty release metadata, unavailable=%s', + async (unavailable) => { + const workflow = await readFile( + new URL('../.github/workflows/release.yml', import.meta.url), + 'utf8', + ); + const step = workflow + .split(' - name: Download release assets for AUR\n')[1] + ?.split('\n - name:')[0]; + const script = step?.split(' run: |\n')[1]?.replace(/^ /gm, ''); + assert.ok(script, 'AUR download step must have a shell script'); + + const workspace = await mkdtemp(path.join(os.tmpdir(), 'subminer-aur-download-')); + const requests: string[] = []; + const files = new Map([ + ['SubMiner-0.20.0.AppImage', 'appimage bytes'], + ['subminer', 'launcher bytes'], + ['subminer-assets.tar.gz', 'optional assets bytes'], + ]); + const server = Bun.serve({ + hostname: '127.0.0.1', + port: 0, + fetch(request) { + const pathname = new URL(request.url).pathname; + requests.push(pathname); + if (unavailable || requests.length === 1) return new Response('try again', { status: 503 }); + const name = pathname.split('/').at(-1); + const body = name ? files.get(name) : undefined; + return new Response(body ?? 'not found', { status: body ? 200 : 404 }); + }, + }); + + try { + const bin = path.join(workspace, 'bin'); + await mkdir(bin); + await writeFile( + path.join(bin, 'gh'), + '#!/bin/sh\necho "no assets to download" >&2\nexit 1\n', + { mode: 0o755 }, + ); + const output = path.join(workspace, 'output'); + const proc = Bun.spawn(['bash', '-c', script], { + cwd: workspace, + env: { + ...process.env, + PATH: `${bin}${path.delimiter}${process.env.PATH}`, + RELEASE_VERSION: 'v0.20.0', + GITHUB_SERVER_URL: server.url.origin, + GITHUB_REPOSITORY: 'ksyasuda/SubMiner', + GITHUB_OUTPUT: output, + }, + stdout: 'pipe', + stderr: 'pipe', + }); + const [status, stderr, stdout] = await Promise.all([ + proc.exited, + new Response(proc.stderr).text(), + new Response(proc.stdout).text(), + ]); + assert.equal(status, 0, stderr); + if (unavailable) { + assert.equal(requests.length, 4, 'failed downloads stop after three retries'); + assert.match(await readFile(output, 'utf8'), /^skip=true$/m); + assert.match(stdout, /::warning::Unable to download/); + await assert.rejects( + readFile(path.join(workspace, '.tmp/aur-release-assets/SubMiner-0.20.0.AppImage')), + { code: 'ENOENT' }, + ); + return; + } + for (const [name, body] of files) { + assert.equal( + await readFile(path.join(workspace, '.tmp/aur-release-assets', name), 'utf8'), + body, + ); + assert.ok(requests.includes(`/ksyasuda/SubMiner/releases/download/v0.20.0/${name}`)); + } + assert.equal(requests.length, 4, 'the first failed download must be retried'); + assert.match(await readFile(output, 'utf8'), /^skip=false$/m); + } finally { + server.stop(true); + await rm(workspace, { recursive: true, force: true }); + } + }, + 15_000, +); diff --git a/scripts/build-changelog.ts b/scripts/build-changelog.ts index 01cf7227..2eba9bf9 100644 --- a/scripts/build-changelog.ts +++ b/scripts/build-changelog.ts @@ -436,7 +436,9 @@ function readChangeFragments(cwd: string, deps?: ChangelogFsDeps): ChangeFragmen const CLAUDE_CLI_ARGS = [ '-p', '--model', - 'sonnet', + 'opus', + '--effort', + 'medium', '--permission-mode', 'bypassPermissions', '--output-format', diff --git a/scripts/package-audit.cjs b/scripts/package-audit.cjs index 6625175b..c2742789 100644 --- a/scripts/package-audit.cjs +++ b/scripts/package-audit.cjs @@ -4,8 +4,6 @@ const assert = require('node:assert/strict'); const asar = require('@electron/asar'); const { Arch } = require('builder-util'); -const MIB = 1024 * 1024; -const currentReports = new Set(); const REQUIRED_APP_FILES = [ 'package.json', 'LICENSE', @@ -49,13 +47,13 @@ const REQUIRED_RESOURCES = [ 'CHANGELOG.md', ]; -// Do not follow framework symlinks or count ASAR unpacked entries twice. +// Skip symlinks when checking resource contents. function listFiles(root, prefix = '') { return fs.readdirSync(path.join(root, prefix), { withFileTypes: true }).flatMap((entry) => { const name = prefix ? `${prefix}/${entry.name}` : entry.name; if (entry.isSymbolicLink()) return []; if (entry.isDirectory()) return listFiles(root, name); - return [{ path: name, bytes: fs.statSync(path.join(root, name)).size }]; + return [name]; }); } @@ -66,7 +64,7 @@ function listAppFiles(archive) { const native = entry.replace(/^[\\/]/, ''); const stat = asar.statFile(archive, native); const name = native.replaceAll('\\', '/'); - return 'size' in stat ? [{ path: name, bytes: stat.size }] : []; + return 'size' in stat ? [name] : []; }); } @@ -107,13 +105,13 @@ function verifyAppPath(name, platform, arch) { function verifyContents(archive, resources, platform, arch) { const entries = listAppFiles(archive); - const names = new Set(entries.map((entry) => entry.path)); + const names = new Set(entries); for (const name of REQUIRED_APP_FILES) assert(names.has(name), `Missing app file: ${name}`); for (const name of REQUIRED_RESOURCES) { assert(fs.statSync(path.join(resources, name)).size > 0, `Empty resource: ${name}`); } assert(listFiles(path.join(resources, 'yomitan-jlpt-vocab')).length > 0, 'Missing JLPT data'); - for (const { path: name } of entries) verifyAppPath(name, platform, arch); + for (const name of entries) verifyAppPath(name, platform, arch); const libsqlPlatform = { linux: `linux-${arch}-gnu`, darwin: `darwin-${arch}`, @@ -137,7 +135,7 @@ function verifyContents(archive, resources, platform, arch) { } } for (const name of listFiles(path.join(resources, 'assets'))) { - assert(!name.path.startsWith('minecard'), `Demo media shipped: ${name.path}`); + assert(!name.startsWith('minecard'), `Demo media shipped: ${name}`); } for (const ui of ['renderer', 'settings', 'syncui']) { const css = asar.extractFile(archive, path.join('dist', ui, 'style.css')).toString(); @@ -155,92 +153,8 @@ async function auditPackage(context) { ? path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`) : context.appOutDir; const resources = path.join(appRoot, platform === 'darwin' ? 'Contents/Resources' : 'resources'); - const appFiles = verifyContents(path.join(resources, 'app.asar'), resources, platform, arch); - const files = listFiles(appRoot); - const unpackedBytes = files.reduce((sum, entry) => sum + entry.bytes, 0); - const report = { - version: context.packager.appInfo.version, - platform, - arch, - unpackedBytes, - appDirectory: path.relative(context.outDir, appRoot), - largestFiles: [...files].sort((a, b) => b.bytes - a.bytes).slice(0, 25), - largestAppFiles: [...appFiles].sort((a, b) => b.bytes - a.bytes).slice(0, 25), - nativeBinaries: files.filter((entry) => /\.(node|dll|dylib)$|\.so(?:\.|$)/.test(entry.path)), - artifacts: [], - }; - const output = path.join(context.outDir, `package-size-${key}.json`); - fs.mkdirSync(path.dirname(output), { recursive: true }); - fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`); - currentReports.add(output); - console.log( - `Package contents verified: ${key}, ${(unpackedBytes / MIB).toFixed(2)} MiB unpacked`, - ); -} - -function artifactKind(name) { - if (name.endsWith('-mac.zip')) return 'mac.zip'; - if (name.endsWith('-win.zip')) return 'win.zip'; - const extension = path.extname(name).slice(1); - return ['AppImage', 'dmg', 'exe'].includes(extension) ? extension : undefined; -} - -function compareSizes(report, previous) { - assert.equal(previous.platform, report.platform); - assert.equal(previous.arch, report.arch); - assert(Number.isFinite(previous.unpackedBytes), 'Invalid previous size report'); - const previousArtifacts = Array.isArray(previous.artifacts) ? previous.artifacts : []; - return { - version: previous.version, - unpackedDeltaBytes: report.unpackedBytes - previous.unpackedBytes, - artifacts: report.artifacts.flatMap((artifact) => { - const old = previousArtifacts.find( - (entry) => entry && entry.kind === artifact.kind && Number.isFinite(entry.bytes), - ); - return old ? [{ kind: artifact.kind, deltaBytes: artifact.bytes - old.bytes }] : []; - }), - }; -} - -// Runs after signing and installer creation, before release upload. -async function afterAllArtifactBuild(result) { - const reports = []; - for (const reportPath of currentReports) { - const filename = path.basename(reportPath); - const report = JSON.parse(fs.readFileSync(reportPath, 'utf8')); - const key = `${report.platform}-${report.arch}`; - const files = listFiles(path.join(result.outDir, report.appDirectory)); - report.unpackedBytes = files.reduce((sum, entry) => sum + entry.bytes, 0); - report.largestFiles = [...files].sort((a, b) => b.bytes - a.bytes).slice(0, 25); - report.artifacts = result.artifactPaths.flatMap((file) => { - const kind = artifactKind(file); - if (!kind) return []; - const bytes = fs.statSync(file).size; - return [{ name: path.basename(file), kind, bytes }]; - }); - const previousPath = path.join(result.outDir, '..', '.tmp', 'package-baseline', filename); - if (fs.existsSync(previousPath)) { - const previous = JSON.parse(fs.readFileSync(previousPath, 'utf8')); - report.comparison = compareSizes(report, previous); - } - fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); - const summary = [ - `### Package size: ${key}`, - '', - `Unpacked: ${(report.unpackedBytes / MIB).toFixed(2)} MiB`, - ...report.artifacts.map((entry) => `${entry.name}: ${(entry.bytes / MIB).toFixed(2)} MiB`), - report.comparison - ? `Change from ${report.comparison.version}: ${(report.comparison.unpackedDeltaBytes / MIB).toFixed(2)} MiB unpacked` - : 'No previous size report available.', - '', - ].join('\n'); - console.log(summary); - if (process.env.GITHUB_STEP_SUMMARY) - fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary); - reports.push(reportPath); - } - assert(reports.length > 0, 'No package size reports generated by afterPack'); - return reports; + verifyContents(path.join(resources, 'app.asar'), resources, platform, arch); + console.log(`Package contents verified: ${key}`); } module.exports = { @@ -249,6 +163,4 @@ module.exports = { verifyAppPath, listFiles, listAppFiles, - compareSizes, - default: afterAllArtifactBuild, }; diff --git a/scripts/package-audit.test.ts b/scripts/package-audit.test.ts index 548c2440..285ccd84 100644 --- a/scripts/package-audit.test.ts +++ b/scripts/package-audit.test.ts @@ -6,7 +6,7 @@ import test from 'node:test'; import { createPackageFromStreams } from '@electron/asar'; import { FileMatcher, getFileMatchers } from 'app-builder-lib/out/fileMatcher'; import config from '../package.json'; -import { listAppFiles, listFiles, compareSizes, verifyAppPath } from './package-audit.cjs'; +import { listAppFiles, listFiles, verifyAppPath } from './package-audit.cjs'; test('platform packaging preserves the runtime allowlist after builder normalizes global filters', () => { const root = process.cwd(); @@ -125,7 +125,7 @@ test('content audit rejects development files beneath approved roots', () => { } }); -test('archive inventory handles native files without counting them twice on disk', async () => { +test('content inventory includes packed and unpacked native files', async () => { const root = mkdtempSync(path.join(tmpdir(), 'subminer-audit-')); try { const input = path.join(root, 'input'); @@ -147,35 +147,9 @@ test('archive inventory handles native files without counting them twice on disk streamGenerator: () => createReadStream(path.join(input, name)), })), ); - assert.deepEqual(listAppFiles(archive), [ - { path: 'main.js', bytes: 5 }, - { path: 'native.node', bytes: 6 }, - { path: 'dist/ai/client.js', bytes: 6 }, - ]); - assert.equal( - listFiles(output).reduce((sum: number, entry: { bytes: number }) => sum + entry.bytes, 0), - statSync(archive).size + 6, - ); + assert.deepEqual(listAppFiles(archive), ['main.js', 'native.node', 'dist/ai/client.js']); + assert.deepEqual(listFiles(output).sort(), ['app.asar', 'app.asar.unpacked/native.node']); } finally { rmSync(root, { recursive: true, force: true }); } }); - -test('size comparison tolerates older reports without artifact measurements', () => { - const previous = { version: '0.19.6', platform: 'linux', arch: 'x64', unpackedBytes: 100 }; - const current = { ...previous, unpackedBytes: 80, artifacts: [{ kind: 'AppImage', bytes: 40 }] }; - assert.deepEqual(compareSizes(current, previous), { - version: '0.19.6', - unpackedDeltaBytes: -20, - artifacts: [], - }); - assert.deepEqual( - compareSizes(current, { ...previous, artifacts: [null, { kind: 'AppImage', bytes: 50 }] }) - .artifacts, - [{ kind: 'AppImage', deltaBytes: -10 }], - ); - assert.throws( - () => compareSizes(current, { ...previous, unpackedBytes: 'unknown' }), - /Invalid previous size report/, - ); -}); diff --git a/src/prerelease-workflow.test.ts b/src/prerelease-workflow.test.ts index 2d884663..29127f0b 100644 --- a/src/prerelease-workflow.test.ts +++ b/src/prerelease-workflow.test.ts @@ -3,7 +3,6 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { - executableRunLines, jobSteps, readWorkflow, stepRunsCommand, @@ -109,20 +108,12 @@ test('prerelease workflow builds and uploads all release platforms', () => { ...paths, 'release/latest*.yml', 'release/*.blockmap', - 'release/package-size-*.json', ]); const download = jobSteps(parsedPrereleaseWorkflow, 'release').find( (step) => step.uses === 'actions/download-artifact@v4' && step.with?.name === name, ); assert.equal(download?.with?.path, 'release'); } - const steps = jobSteps(parsedPrereleaseWorkflow, 'release'); - const checksum = steps.find((step) => step.name === 'Generate checksums'); - const publish = steps.find((step) => step.name === 'Publish Prerelease'); - assert.ok(checksum); - assert.ok(publish); - assert.ok(executableRunLines(checksum).includes('files+=(release/package-size-*.json)')); - assert.ok(executableRunLines(publish).includes('release/package-size-*.json')); }); test('release callers pass only the declared packaging secrets', () => { diff --git a/src/release-workflow.test.ts b/src/release-workflow.test.ts index 8f796f01..55d4ee9b 100644 --- a/src/release-workflow.test.ts +++ b/src/release-workflow.test.ts @@ -289,7 +289,6 @@ test('stable and prerelease builds use the same packaging gate', () => { for (const workflow of [releaseWorkflow, prerelease]) { assert.match(workflow, /uses: \.\/\.github\/workflows\/package-release\.yml/); assert.match(workflow, /needs: \[package\]/); - assert.match(workflow, /release\/package-size-\*\.json/); } assert.deepEqual( templateExpressionsInRunBodies(