Skip to content

Releases: Shironex/moekoder

v0.8.0: Electron 44 and security

Choose a tag to compare

@Shironex Shironex released this 16 Oct 18:49

Platform and security release. The desktop app moves to Electron 44 (Chromium 152), which drops macOS 12: macOS 13 or newer is now required. Windows (x64) is unaffected. Alongside that, the advisories published since 0.7.2 are closed, including a critical shell-quote advisory in dev tooling, and pnpm audit goes from 1 critical / 15 high / 14 moderate / 3 low to a single moderate with no upstream fix. No feature or behaviour changes in the app itself.

Changed

  • Electron 43.7.3 → 44.7.0. None of the Electron 44 breaking changes touch Moekoder's code (the renderer already uses navigator.clipboard, the window keeps sandbox: true). electron-builder stays at 26.15.7 and electron-updater at 6.8.9.
  • Major bumps:
    • Desktop and web: vitest 4.1.11 → 5.0.3. Desktop now declares vite as a dev dependency, since vitest 5 needs it as a peer.
    • Landing: @astrojs/react 6.0.6 → 7.0.1, with astro 7.3.8.
    • Landing: satori 0.33.5 → 0.47.1, pinned exactly. Newer satori sizes boxes like a browser, so the OG card frame is now border-box and og-default.png was regenerated. Apart from a few pixels in the rotated seal, the card is unchanged.
  • pnpm 10.9.0 → 10.34.6 in packageManager, CI, the release workflow and the landing Dockerfile. pnpm-workspace.yaml now sets minimumReleaseAge to 7 days. pnpm 10.9.0 ignored the setting, so dependency ranges could resolve to releases only hours old. Every locked version is now at least a week old.

Security

  • concurrently 10.0.5 → 10.0.6: closes the critical shell-quote advisory GHSA-pqg4-j6r4-53mv. Dev tooling only, nothing ships in the app.
  • wait-on 9.1.0 → 9.5.1: pulls axios up to 1.20.0 and closes 13 axios advisories (7 high). Dev tooling only.
  • sharp 0.35.4 → 0.35.5: closes GHSA-wq5f-xc86-pv6w.
  • Override floors raised: these four overrides stopped one patch short of the fixed releases.
    • undici to 8.10.2 (11 advisories).
    • brace-expansion to 5.0.12 (3 advisories, 2 high).
    • fast-uri to 3.1.8.
    • shell-quote to 1.11.0.
  • New overrides: source-map-js ≥1.2.2 (high), http-cache-semantics ≥4.3.0 (high), smol-toml ≥1.9.0.
  • Known and accepted: sprintf-js (moderate, GHSA-hp3w-g68c-fv3c) has no fixed release. It only reaches the build through electron-builder and is never shipped.

v0.7.2: Translation hotfix

Choose a tag to compare

@Shironex Shironex released this 27 Sep 22:15

Translation hotfix. The packaged 0.7.0 and 0.7.1 apps shipped without their English and Polish strings, so the interface showed raw translation keys (cta.begin, hw.title) instead of text. Development builds were fine, which is why it slipped through. This release restores both languages and changes nothing else about how the app behaves.

Fixed

  • Translations missing from release builds (0.7.0, 0.7.1). The renderer entry loads the i18next bootstrap through a bare side-effect import, but apps/web/package.json has declared only CSS as having side effects since 0.1.0. When i18n landed in 0.7.0, the production bundler treated lib/i18n.ts as side-effect free and dropped it, locale resources included. ./src/lib/i18n.ts is now listed in sideEffects, and the packaged renderer contains both locales again.

Added

  • pnpm showcase regenerates the README screenshots and banners with @noctcore/showcase-kit. It builds the renderer alone in a showcase Vite mode with an invented stand-in for the Electron bridge (made-up files, a fixed GPU probe, frozen encode numbers), so no real files or hardware appear on screen. None of it is included in normal builds.
  • data-testid hooks on the titlebar, sidebar, onboarding, Settings and Extract screens, so automated captures don't depend on translated labels.

Changed

  • README rewritten with a hero banner, screenshots of every main screen, a feature table checked against the code and an up-to-date stack. A Polish README.pl.md sits alongside it.
  • New dev dependencies: @noctcore/showcase-kit 0.3.0 and playwright 1.63.0, both pinned exactly. Nothing new ships in the app.

Removed

  • The old CDP screenshot script (scripts/screenshot-app.mjs) and the assets/screenshots/ images it produced, replaced by pnpm showcase.

v0.7.1 — Security patch

Choose a tag to compare

@Shironex Shironex released this 22 Sep 18:12

Security patch release. No feature or behaviour changes. It picks up the advisories published since 0.7.0, including a critical Astro RCE and a js-yaml advisory in the desktop auto-updater path. pnpm audit is back to 0 vulnerabilities.

Changed

  • Electron 43.4.1 → 43.7.3. This stays on the 43.x line and brings in upstream Chromium security fixes. electron-builder is unchanged at 26.15.7.
  • Patch and minor bumps:
    • Desktop: zod 4.6.5.
    • Web: react / react-dom 19.3.0, i18next 26.4.2, react-i18next 17.0.15, lucide-react 1.47.0, tailwind-merge 3.7.0, vite 8.3.0, @vitejs/plugin-react 6.1.1.
    • Landing: astro 7.3.3, @astrojs/react 6.0.6, @astrojs/sitemap 3.7.4, satori 0.33.5, sharp 0.35.4.
    • Root tooling: eslint 10.11.0, typescript-eslint 8.70.1, globals 17.12.0, lint-staged 17.5.1, prettier 3.9.8.

Security

  • astro 7.2.4 → 7.3.3 (landing): closes GHSA-26w7-cxv4-gfx2, a critical RCE in AVIF image optimisation via libheif. The landing site is static, so the exposure was limited to build time and the dev server.
  • sharp 0.35.3 → 0.35.4: closes GHSA-rgj7-g3m4-5g8c, a high-severity libheif overflow.
  • js-yaml override floor raised to 4.3.2 (GHSA-2883-xcg3-v3hh). This is the highest-priority fix for end users, because electron-updater parses update manifests fetched over the network with js-yaml at runtime.
  • Other override floors raised:
    • fast-uri to 3.1.6 (4 high-severity advisories).
    • svgo to 4.1.0, now capped below v5.
    • devalue to 5.9.2 (GHSA-9rgm-9g3h-6x36). Dependabot reported this one; pnpm audit didn't.
  • New overrides:
    • @xmldom/xmldom ≥0.8.15 <0.9 (10 advisories). It's capped because plist only declares ^0.8.8.
    • fflate ≥0.7.5 <0.8. satori still exact-pins 0.7.3.
    • joi ≥18.2.5 <19 (low severity, dev-only).

v0.7.0 — English + Polish UI, AMD encoding, dependency sweep

Choose a tag to compare

@Shironex Shironex released this 21 Aug 09:59

Localization and AMD hardware encoding, on top of a three-month dependency and security sweep. Every UI string now ships in English and Polish behind a language picker that lives in both Settings and the onboarding Welcome step. AMD GPUs finally get a hardware path — the encoder had no AMF branch at all, so an AMD-only machine on the default hwAccel:'nvenc' emitted h264_nvenc and failed with "unknown encoder"; AMF now has real args and the GPU probe is wired into the orchestrator so a detected encoder actually gets used. The probe also stopped taking ffmpeg's word for it: every detected encoder is confirmed with a 1-frame test-encode before it is offered. Alongside that, thirty-odd dependency commits took pnpm audit from 1 critical / 15 high / 20 moderate to zero, and carried Electron to 43, React to 19 and Astro to 7.

Added

  • UI localization (English + Polish) — every user-facing string across screens, chrome components and the 14-step onboarding is extracted into 14 namespaced en/pl locale files and routed through useTranslation. i18next initialises synchronously before React mounts (the init side-effect is imported ahead of App in the renderer entry, so a module-scope lookup can never race it). Decorative kanji/romaji eyebrows are left inline. A self-contained LanguagePicker appears in Settings and on the onboarding Welcome step.
  • uiLanguage setting — nullable, OS-detect-then-sticky: unset means "follow the OS", and the first explicit pick freezes the choice. useUiLanguage / useUiLanguageSync read a localStorage boot mirror for the first paint and treat electron-store as the durable source. The main-process queue-complete notification is localized off the persisted value, since it can't reach the renderer's i18next instance. Includes key-parity, plural and configured-instance tests, so a locale file that drifts out of sync fails CI.
  • AMF (AMD) H.264/HEVC encoding — new H264AmfSettings / HevcAmfSettings union members and h264_amf / hevc_amf branches in args.ts, written in AMF's own rate-control vocabulary (-quality {speed|balanced|quality} -rc cqp -qp_i/-qp_p/-qp_b, pixel format yuv420p, p010le for 10-bit HEVC) and verified against ffmpeg's amfenc source rather than analogized from NVENC. {codec:'av1',hwAccel:'amf'} is intentionally non-representable — ffmpeg has no av1_amf.
  • resolveHwAccel() and probe-driven encoder selection — a best-effort probeGpu dependency is wired into the orchestrator and soft-fails back to the requested settings when the probe is unavailable, mirroring the font-extractor pattern. This is what closes the AMD gap end to end: detection already worked, nothing consumed it.
  • Verified encoder detection — verifyEncoderWorks() runs a minimal 1-frame lavfi→null test-encode per detected encoder and keeps it only on a clean exit; non-zero exit, spawn error and timeout (SIGTERM→SIGKILL) all drop it. An advertised-but-broken hardware encoder is now caught at probe time instead of at the user's first real job. Results are cached in electron-store behind a version key with a 7-day TTL, so the test runs once rather than on every launch. The IPC contract gains a verified flag and a zod gpuProbeResultSchema.
  • Per-dialog last-used directory — UserSettings.lastDialogDirs maps a DialogDirKind (video / subtitle / save-file / output-folder) to the directory it was last opened at, so the media library, the subtitle folder and the output folder each keep separate memory. Folder picks remember the folder itself (reopening lands inside it); file picks remember dirname(). A cancelled dialog remembers nothing, a directory that no longer resolves is dropped at read time rather than handed to Electron dead, and an explicit defaultPath still wins over the remembered one.
  • Test coverage — desktop 321 passing / 2 skipped (was 263), web 51 (was 28). Includes dialog.test.ts (20 cases) for a channel that previously had none.

Changed

  • Electron 42.9.3 → 43.4.1. Five of the six entries in Electron's 43.0 breaking-change list resolve to zero call sites here (three are Linux-only and the build targets only win/mac; the NativeImage.toBitmap() sRGB change and the chrome.scripting fallback-frame change have no matching usage). The sixth — dialogs defaulting to the Downloads folder instead of the OS's remembered location — is what the per-dialog directory memory above was written to absorb, and it landed first, so the major itself carries no known UX regression.
  • React 18 → 19 across @moekoder/web and @moekoder/landing, bumped in lockstep so a single @types/react major exists in the store. No source changes were needed — the renderer was already on createRoot and every ref passes an initial argument.
  • Astro 6 → 7 on the landing app (Rust compiler, Vite 8), with @astrojs/react 5 → 6. Also tailwind-merge 2 → 3 (Tailwind v4 was already in place, so v2 was a latent class-group mismatch), sharp 0.34 → 0.35, concurrently 9 → 10, lint-staged 16 → 17, @types/yauzl 2 → 3, and electron-builder pinned to 26.15.7.
  • engines.node raised to >=22.22.1 — required by lint-staged 17's Node floor.
  • Icon set and OG card regenerated — the libvips 8.16 → 8.18.3 shift under sharp 0.35 moved a few pixels in icon-16.png / icon-32.png, and satori 0.33.0 made HarfBuzz shaping unconditional, which changes text measurement. Both artifacts are tracked, so both were re-rendered from the current toolchain.
  • GPU-probe verifications run sequentially. They were concurrent, which is faster in principle; see Fixed.

Fixed

  • Concurrent probe verification dropped working encoders — running the test-encodes through Promise.allSettled let them race on GPU-context initialisation and hit driver concurrent-session limits (NVENC / AMF / QSV), so a perfectly good encoder could be marked unavailable. They now run one at a time; a 1-frame test is under 100 ms, so total probe latency stays sub-second.
  • Polish word order in container labels — "{c.name} {containerSuffix}" rendered "MP4 kontener". The two fragments are now a single containerLabel key with a {{name}} placeholder, so each locale places the noun itself ("{{name}} container" / "kontener {{name}}").
  • Install-stage label was force-lowercased — .toLowerCase() on a translated string applies locale-independent casing rules (wrong for languages like Turkish) and overrides the translator's intent. The stage label renders as authored.
  • Queue retry button's accessible name — the retry aria-label pulled from the common namespace while its sibling buttons used queue-namespace keys; it now uses queue.retryLabel.
  • Inconsistent Windows install-path casing in onboarding copy — the engine-gate destination and dl.install.sub disagreed (moekoder vs MoeKoder); both now match APP_NAME, which is the actual %LOCALAPPDATA% folder name.
  • Landing OG card spacing under satori 0.33.0 — HarfBuzz shaping stopped applying letter-spacing to collapsible whitespace, collapsing the word gaps in the eyebrow and meta rows into letter gaps, and re-measured the 萌コーダー run 18px narrower, sliding the decorative rule left into the kana. Whitespace runs are now NBSP (which is shaped as a real glyph and still takes letter-spacing) and the width delta is compensated with padding. The card differs from the 0.26.0 baseline by 5 pixels out of 756,000, all antialiasing.

Security

pnpm audit on the full graph went from 1 critical / 15 high / 20 moderate / 1 low to 0 vulnerabilities, with no ignoreCves suppressions left in place.

  • electron-updater 6.8.3 → 6.8.9 — the highest-priority fix in the sweep, since it runs on end-user machines. Pulls builder-util-runtime 9.7.0, closing GHSA-p2f4-r6v6-j797 (a cross-origin redirect leaks Authorization / PRIVATE-TOKEN headers) plus a js-yaml quadratic-DoS advisory.
  • Electron 42.5.0 → 42.8.1 ahead of the 43 bump closed CVE-2026-70606 (protocol-handler session-isolation bypass). No custom protocol handlers exist here, so the surface wasn't reachable either way.
  • pnpm.overrides floors raised and bounded — fast-uri, shell-quote, form-data, brace-expansion, undici, tmp, svgo, yaml, js-yaml, nanoid, vite, @babel/core, devalue, ip-address. Several floors were unbounded above and pnpm had resolved them past what their consumers declare (nanoid to an ESM-only v6 behind postcss's ^3.3.17, @babel/core to v8 behind @vitejs/plugin-react's ^7.29.0); each is now capped at the reviewed major.
  • A stale electron-builder-squirrel-windows@26.8.1 chain was collapsed — it dragged in its own old app-builder-lib with vulnerable tar, js-yaml, fast-uri and builder-util-runtime, duplicating packages the top-level resolution had already fixed. Because it and app-builder-lib pin each other as exact mutual peers, pnpm kept reusing the stale snapshot; adding it as an explicit devDependency gave the resolver a real edge and cleared the critical tar advisory along with several highs.
  • Dev-path fixes — vite 8.0.13 → 8.1.5 (CVE-2026-53571, server.fs.deny bypass on Windows alternate paths), esbuild 0.28.0 → 0.28.1 (GHSA-g7r4-m6w7-qqqr, arbitrary file read via the dev server), wait-on 9.0.10 → 9.1.0 (clears all 8 axios advisories), and astro 7.0.4 → 7.1.3 (3 moderates, cascading svgo and postcss fixes).
  • Full audit trail: docs/deps/2026-08-06-audit-standard-2026-08.md, docs/deps/2026-08-20-audit-standard.md, docs/migrate/2026-08-20-electron-43.md, docs/migrate/2026-08-20-satori-0.33.0.md.

Known Limitations

  • The AMF encoder path has not been validated on real AMD hardware. There is no AMD GPU in CI, so args.test.ts asserts arg shape only — the flags were derived from ffmpeg's amfenc source, not from a successful encode. If you have an AMD card, a 1-frame encode confirming ffmpeg ...
Read more

v0.6.0 — Mux, don't burn.

Choose a tag to compare

@Shironex Shironex released this 22 May 11:01

Soft subs, both directions. v0.6.0 adds a path that skips libass entirely: mux an external .ass into an MKV as a separate, selectable subtitle track — video and audio stream-copied, no re-encode, done in seconds — and the inverse, extract embedded subtitle tracks back out of a container to standalone .ass / .srt files. The hardsub burn-in pipeline is untouched; this is a second way to ship subtitles, for when you'd rather keep them toggleable.

Headline feature: "Mux only (soft subs)" turns a 1.5 GB episode + an .ass into a playable MKV with a language-labeled, selectable subtitle track in seconds instead of minutes — because nothing is encoded, only copied.

Install

  • Windows — Moekoder-0.6.0-win-x64.exe. Still unsigned; SmartScreen will show More info → Run anyway on first launch.
  • macOS — Moekoder-0.6.0-mac-arm64.dmg. Still unsigned; after dragging into /Applications, run:
    xattr -cr /Applications/Moekoder.app
    to clear the Gatekeeper quarantine.
  • Linux — not shipped this release. Still on the roadmap.

Upgrading from v0.5.x: the Windows auto-updater pulls this in the background; the next launch comes up on v0.6.0. macOS users grab the DMG manually until code-signing lands.

Soft-sub mux

  • "Mux only (soft subs)" toggle in Settings → Encoding. When on, the encoder skips the libass burn-in filter and emits ffmpeg -i <video> -i <subs> -map 0:v -map 0:a -map 1:0 -c copy -c:s ass -metadata:s:s:0 language=<lang> -disposition:s:0 default <out.mkv>. Video + audio are stream-copied; the .ass becomes a default-flagged subtitle track. Opens in VLC / mpv with the subs selectable.
  • MKV forced — soft-subs in MP4 are a pain, so mux mode forces the MKV container and warns if you had MP4 selected.
  • Language from the filename — .en.ass / .pl.ass → eng / pol (ISO-639-2), with a manual override field.
  • I/O-bound, not encode-bound — reuses the existing -progress parser; the bar fills near-instantly.

Subtitle extraction

  • Dedicated Extract screen (new Titlebar entry) — open a container and its embedded subtitle streams are probed and listed (index / codec / language / title) in a multi-select table. Pick an output folder, pull the selected text tracks out. Image subtitles (PGS / VOBSUB) are shown but can't be selected.
  • Output-format selector — ASS / SRT / Match source, default ASS. Copies verbatim when the source already matches; transcodes otherwise (SubRip → .ass, or .ass → .srt with a styling-loss note).
  • No collisions — output filenames carry the track ordinal (<stem>.<lang>.s<N>.<ext>), so a full + a signs track both tagged eng don't overwrite each other.

Added

  • Soft-sub mux mode, the dedicated Extract screen, the subtitle:extract IPC channel, and a shared resolveSubtitleOutput codec ⇄ extension resolver.

Changed

  • Removed the deprecated escapeSubtitlePath / escapeSubtitlePathFor libass-escape aliases — no remaining callers; mux and extract args go straight to spawn (no shell, no libass filter), so no path escaping is needed.

Fixed

  • text-codec extraction no longer fabricates a .ass file that ffmpeg rejects — the generic ffprobe text tag is no longer treated as a selectable text codec.
  • Dropped-queue container extension is now correct when mux mode is toggled (stale-closure dependency fix on the drop-to-queue handler).

Tests

Desktop suite: 263 (was 234). Web suite: 28. New mux-arg, subtitle-codec, extractor, and renderer codec / language coverage. Lint, typecheck, and prettier clean.

Known limitations

  • Multiple subtitle tracks in a single mux is out of scope — one external .ass per mux (planned for v0.7).
  • Extraction handles text subtitles only (copy or transcode between .ass / .srt); image subtitles (PGS / VOBSUB) can't be exported.
  • Same as v0.5.x: binaries are unsigned, the auto-updater is Windows-only, there's no Linux build, and embedded-font extraction still runs per-job (no font cache).

Source under the Moekoder Source Available License.

v0.5.1 — Windows download hotfix

Choose a tag to compare

@Shironex Shironex released this 22 May 09:02

Windows onboarding hotfix. The pinned BtbN autobuild-YYYY-MM-DD-HH-MM ffmpeg download URL started returning 404 — BtbN prunes old dated autobuilds on a rolling schedule, so the snapshot tag hard-coded for v0.5.0 eventually disappeared and first-run ffmpeg auto-install broke on Windows. MoeKoder now resolves the Windows build at runtime against the permanent rolling latest tag and verifies it with the SHA-256 the GitHub Releases API reports per asset, so supply-chain verification is preserved without depending on a tag that can vanish.

Headline fix: fresh Windows installs can download ffmpeg again. No pinned tag to rot, no unverified fallback — the win64 build is resolved live from the BtbN latest release and still SHA-256-checked before it's trusted.

Install

  • Windows — Moekoder-0.5.1-win-x64.exe. Still unsigned; SmartScreen will show More info → Run anyway on first launch.
  • macOS — Moekoder-0.5.1-mac-arm64.dmg. Still unsigned; after dragging into /Applications, run:
    xattr -cr /Applications/Moekoder.app
    to clear the Gatekeeper quarantine.
  • Linux — not shipped this release. Still on the roadmap.

If you're upgrading from v0.5.0, the auto-updater on Windows pulls this release in the background; the next launch comes up on v0.5.1. macOS users grab the DMG manually until code-signing lands. (If your v0.5.0 install never finished its first-run ffmpeg download because of the 404, this is the build that fixes it.)

Windows ffmpeg download

  • The 404, fixed — getSourceForPlatform('win32') no longer points at a dated autobuild-* tag that BtbN later deletes. New resolveWindowsSource() queries api.github.com/repos/BtbN/FFmpeg-Builds/releases/tags/latest, selects the stable ffmpeg-n8.1-latest-win64-gpl-8.1.zip asset, and builds the BinaryArchive from its live browser_download_url and the API-reported digest.
  • Verification never silently dropped — resolveWindowsSource() throws a clear, actionable error on any failure (network, missing asset, or a missing/malformed sha256:<hex> digest) rather than degrading to an unverified download. The zip's top-level folder is derived from the asset name, so the ffmpeg.exe / ffprobe.exe entry paths stay correct across rebuilds.
  • macOS unchanged — still a static evermeet.cx pin.

Added

  • fetchGitHubJson<T>() in main/http.ts — gated JSON GET that rides the same per-host rate-limit clock as downloadToFile, sends the User-Agent + Accept: application/vnd.github+json headers the GitHub REST API requires, and throws a host-prefixed error (status + statusText) on any non-2xx so callers never parse an HTML error page as JSON.
  • api.github.com host gate (1000 ms) — spacing keeps a single install's resolution calls clear of GitHub's 60 req/hr/IP unauthenticated ceiling.

Changed

  • getSourceForPlatform is now async — both platform branches share one awaitable call site; ensureInstalled awaits the resolved source before downloading. The Windows path is resolved on demand; macOS returns its static source.

Tests

Offline-deterministic coverage — manager.test.ts stubs fetch with a representative latest release payload (throwaway digest) to cover asset selection, the missing-asset throw, and the missing/invalid-digest throw, all without touching the network. Test count: 234 (was 229).

Known limitations

  • The Windows resolver depends on the BtbN latest release exposing the ffmpeg-n8.1-latest-win64-gpl-8.1.zip asset with an API-reported digest. If BtbN renames the asset or stops emitting digests, resolution fails loudly (with an actionable error) rather than falling back — by design, since the alternative is an unverified download.
  • Same as v0.5.0: the missing-font diagnostic is best-effort string matching and doesn't enumerate system fonts; soft-subbed mux is still v0.6 territory; embedded-font extraction runs per-job (no <userData>/fonts/<sourceHash>/ cache yet).
  • Same as v0.4.0: binaries are unsigned, auto-updater is Windows-only, no Linux build.

Source under the Moekoder Source Available License.

v0.5.0 — Embedded fonts

Choose a tag to compare

@Shironex Shironex released this 17 May 20:25

MKV embedded-font extraction release. When the source is an MKV with attached fonts — standard practice for anime fansubs that ship \fn(CustomFont) typesetting in their ASS scripts — MoeKoder now extracts every font attachment into a per-job temp dir and feeds the path to libass via the subtitles=...:fontsdir= option. Burned output finally renders typeset cues with the fonts the author intended instead of silently falling back to Arial.

Headline feature: drop in an MKV with attached fonts, hit burn, get the typesetting the release group actually shipped. No system-font install dance, no manual font extraction, no Arial fallback surprise.

Install

  • Windows — Moekoder-0.5.0-win-x64.exe. Still unsigned; SmartScreen will show More info → Run anyway on first launch.
  • macOS — Moekoder-0.5.0-mac-arm64.dmg. Still unsigned; after dragging into /Applications, run:
    xattr -cr /Applications/Moekoder.app
    to clear the Gatekeeper quarantine.
  • Linux — not shipped this release. Still on the roadmap.

If you're upgrading from v0.4.0, the auto-updater on Windows pulls this release in the background; the next launch comes up on v0.5.0. macOS users grab the DMG manually until code-signing lands.

MKV attachment extraction

  • New font-extractor module runs ffmpeg -dump_attachment:t '' -i <video> against a per-job os.tmpdir()/mkfont-* directory, accepts the well-known exit-code-1-on-success quirk, filters the dump to font extensions (.ttf .otf .ttc .woff .woff2) plus mime hints, and returns the dir + basenames.
  • Returns null when there are no font-shaped attachments to dump (cover-art-only MKVs short-circuit before invoking ffmpeg).
  • The orchestrator probes attachments before encode, calls extractFonts(...), threads the resulting dir onto EncodeJob.fontsDir, and both terminal callbacks (onComplete and onError / CANCELLED) run cleanupFontsDir(...) so the temp dir is removed whether the encode succeeded, errored, or was user-cancelled.

libass fontsdir= wiring

  • buildFilterChain learns an optional fontsDir argument and emits subtitles='<sub>':fontsdir='<dir>' ahead of the codec-aware pixel-format filter when set.
  • v0.4 byte-for-byte regression locked — no fontsDir ⇒ the exact same arg array as v0.4.
  • escapeSubtitlePath is now escapeLibassPath — same helper handles both subtitles= and fontsdir= arguments. The old name is kept as a @deprecated alias for one release.

Settings → Embedded fonts

  • New section in apps/web/src/screens/Settings.tsx ("字 · ji"), wired to the new useEmbeddedFonts setting key.
  • Defaults to on so anime fansubs render correctly out of the box; flip off to restore v0.4 behaviour (libass falls back to system fonts only).

Missing-font diagnostic

  • After a successful extraction the orchestrator regex-scans the ASS subtitle for \fn(Name) overrides and emits a warn per reference not present among the extracted basenames (case-insensitive, stem-matched).
  • Surfaces in the same job-log channel that feeds both the Single-route log panel and the queue card's expand drawer.
  • Matches both libass forms — bare (\fnFontName) and parenthesized (\fn(FontName)).

Fixed

  • Font tempdir cleanup on every failure path — the extractor now wraps the ffmpeg spawn + readdir in try/catch so spawn errors, non-0/1 exit codes, signal kills, readdir throws, and zero-files-produced all remove the temp dir before propagating. Previously a mid-extraction failure could leave mkfont-* directories behind in os.tmpdir().
  • Signal-killed ffmpeg surfaces a distinct error — null exit code (process killed by signal) is now distinguished from a non-zero code so failures don't masquerade as "exit code null" in the job log.
  • fontStem switched to path.parse(...).name — replaces the regex-based extension strip; correctly handles compound names like Bauhaus 93.ttf so the missing-font diagnostic matches the right filename.

Changed

  • EncodeJob gains fontsDir?: string — opt-in field; callers that don't set it produce the v0.4 filter chain byte-for-byte.
  • OrchestratorDeps seams — probeAttachments, extractFonts, cleanupFontsDir, getUseEmbeddedFonts, and readSubtitleFile are now injectable so every branch (toggle on/off, attachments yes/no, extractor success/failure, missing-font diagnostic, cleanup-on-cancel) runs in vitest without spawning ffmpeg or touching disk.

Tests

39 new vitest tests — 2 in probe.test.ts (multi-font fixture + missing-tag fallback), 3 in path-escape.test.ts (fontsdir-style inputs + alias), 4 in args.test.ts (fontsdir token + v0.4 regression lock + NVENC pixel-format ordering + libx264 path), 21 in font-extractor.test.ts (filter + extract + cleanup + \fn scan + diff), and 9 in orchestrator.test.ts (toggle off, no attachments, extract + cleanup on success / cancel / error, soft-fail on extractor throw, missing-font warn, subtitle-read failure tolerated). Test count: 229 (was 190).

Known limitations

  • The missing-font diagnostic is best-effort string matching on the ASS file (\fn(Name) overrides, stem-matched against extracted basenames). It does not enumerate system fonts and cannot distinguish "missing on disk" from "present elsewhere on the system" — every warning reads as "may fall back to system default". Full system-font enumeration is deferred to v0.6.
  • Soft-subbed mux (no burn-in, font attachments preserved in the output MKV) is still v0.6 territory.
  • Embedded-font extraction runs per-job. A queue of 24 episodes from the same release will re-extract identical fonts 24 times — a <userData>/fonts/<sourceHash>/ cache is on the v0.6 roadmap.
  • Same as v0.4.0: binaries are unsigned, auto-updater is Windows-only, no Linux build.

Source under the Moekoder Source Available License.

v0.4.0 — Codec expansion

Choose a tag to compare

@Shironex Shironex released this 15 May 12:46

Codec expansion release. HEVC and AV1 join H.264 across NVENC + software paths, with a per-codec preset editor, three quality tiers (Fast / Balanced / Pristine), custom presets that survive an app restart, and a benchmark mode that scores 2–4 candidate profiles on a 10-second sample with size, encode time, and PSNR.

Headline feature: the encoder is no longer a single fixed pipeline. Pick H.264, HEVC, or AV1 — each gets its own hardware path (NVENC) and software fallback (libx264 / libx265 / libsvtav1), its own preset family, and its own quality scale. Save your favourite knob combination as a custom preset and apply it later in one click; spin up a benchmark to see which codec actually wins on your specific source.

Install

  • Windows — Moekoder-0.4.0-win-x64.exe. Still unsigned; SmartScreen will show More info → Run anyway on first launch.
  • macOS — Moekoder-0.4.0-mac-arm64.dmg. Still unsigned; after dragging into /Applications, run:
    xattr -cr /Applications/Moekoder.app
    to clear the Gatekeeper quarantine.
  • Linux — not shipped this release. Still on the roadmap.

If you're upgrading from v0.3.0, the auto-updater on Windows pulls this release in the background; the next launch comes up on v0.4.0. macOS users grab the DMG manually until code-signing lands.

New codecs

  • HEVC encode path — hevc_nvenc (10-bit main10) and libx265 software encoder. NVENC main10 emits format=yuv420p10le upstream for the encoder; libx265 inherits source pixel format.
  • AV1 encode path — av1_nvenc (RTX 40-series, gated by gpu.probe()'s per-encoder name list) and libsvtav1 software encoder with integer -preset 0..13.
  • HEVC + MP4 mux flag — -tag:v hvc1 appended on the HEVC + MP4 combination so QuickTime / iOS pick up the stream as HEVC.
  • Discriminated-union EncodingSettings — the shape is now tagged on codec, eliminating illegal combinations (libx265 + AV1, etc.) at compile time. Each branch carries only its valid hardware paths and codec-specific knobs (NVENC pN, libx265 preset family, SVT-AV1 0..13).

Settings → Encoding

A new section in Settings exposes every codec knob in one place:

  • Codec radio (H.264 / HEVC / AV1) and hardware-encoder picker, filtered against gpu.probe() so disabled options aren't even clickable.
  • Fast / Balanced / Pristine quick-set buttons per codec. Balanced defaults: H.264 CQ 19 / NVENC p4 (the v0.1 default), HEVC CQ 22 / 10-bit, AV1 CQ 28 / 10-bit. Fast drops the CQ ceiling + NVENC p2; Pristine raises the ceiling + p7. Nine presets total.
  • CQ slider clamped + labelled per codec (libsvtav1 goes to 63, the rest stop at 51).
  • Per-encoder preset knob (NVENC pN / libx265 preset family / SVT-AV1 0..13).
  • 10-bit toggle for HEVC + AV1 NVENC.
  • Container picker.

Settings persist as a single encoding profile blob in electron-store.

Settings → Custom presets

  • Name + save the live encoding profile, apply it later in one click, delete entries.
  • Up to 20 presets; names must be unique.
  • Each entry carries version: 1 from day one for forward-compat migrations.

Benchmark mode

  • Encodes a 10-second sample of a chosen video against 2–4 candidate profiles.
  • Reports per-row size, elapsed time, and PSNR (dB).
  • Reachable from the Encoding section's "Run benchmark" button.
  • Defaults to the user's currently-selected codec at Fast / Balanced / Pristine; an inline codec cycler lets you promote a slot to a different codec to compare across families.
  • Temp files live under <userData>/benchmark/<runId>/ and clean up on completion.
  • One-shot -lavfi psnr runner (ffmpeg/psnr.ts) parses average: from stderr with a 60-second hard cap to bound a malformed candidate.
  • Per-candidate failures surface in the result row rather than rejecting the whole run, so a single bad config doesn't lose the other rows.
  • New benchmark:run IPC with zod-validated tuple schema (max 4 candidates) and dedicated event channels (benchmark:progress, benchmark:log).
  • New EncodeJob.clipWindow — optional { startSec, durationSec } propagates into -ss <start> -t <duration> ahead of -i. Benchmark uses it; Single + Queue routes don't set it.

Changed

  • EncodingSettings is now a discriminated union over codec — every existing call site keeps compiling because the v0.1 H.264 NVENC defaults still satisfy the H.264 branch shape, but new code that touches partial overrides should call defaultsFor(codec) rather than spreading BALANCED_PRESET directly.
  • Filter chain pixel format is now codec-aware — NVENC h264 still emits format=yuv420p; HEVC + AV1 NVENC emit format=yuv420p10le when tenBit is set. libx265 + libsvtav1 software branches don't force a pixel format and inherit from the source.
  • App.tsx prefers the persisted encoding profile when present and falls back to the legacy onboarding-derived overrides only when the user hasn't opened the new Encoding section yet — no behavioural change for existing user flows.

Maintenance

  • Toolchain bumps — electron 41.2.1 → 42.0.1, vite 7 → 8 + @vitejs/plugin-react 4.7.0 → 6.0.2 (with resolve.dedupe + dropped minify:'esbuild' for vite 8 compat), vitest 3 → 4 in web + desktop (explicit include glob pinned for desktop), zod 3 → 4 in apps/desktop.
  • Node engine floor raised to >=22.12.0 to satisfy electron 42's runtime requirements.
  • Security fixes — Phase 1 safe bumps + Phase 2 pnpm overrides clear 4 high-severity CVEs (fast-uri <3.1.2, ip-address <10.1.1, devalue >=5.6.3 <5.8.1 pinned forward).

Tests

14 new vitest tests — 10 in args.test.ts (hevc_nvenc / hevc + libx265 / av1_nvenc / libsvtav1 branches, 8-bit fallback, MKV-strips-hvc1, clipWindow plumbing) and 4 in benchmark.test.ts (sequential runs, candidate failures don't kill the run, candidate cap, zero-candidate no-op). Test count: 190 (was 176).

Known limitations

  • Custom preset import / export to JSON file isn't shipped — deferred to v0.5.
  • Per-job overrides in the queue still aren't supported — every queue item uses the global encoding profile. Lands in v0.5 alongside import/export so the UI work happens together.
  • Per-codec preflight bitrate estimate isn't shipped; preflight stays pinned to the H.264 number (2500 kbps) and over-reserves disk for HEVC/AV1. Safe direction for a guard.
  • AV1 NVENC needs an RTX 40-series GPU. Pre-RTX-40 hardware sees the AV1 NVENC option disabled with a requires RTX 40-series tooltip; AV1 software (libsvtav1) is always available.
  • Benchmark mode runs candidates sequentially. Parallel candidates are tractable (the orchestrator already supports concurrency) but the benchmark deliberately serialises so the timing numbers aren't contaminated by GPU contention.
  • Same as v0.3.0: binaries are unsigned, auto-updater is Windows-only, no Linux build.

Source under the Moekoder Source Available License.

v0.3.0 — A whole stack to burn.

Choose a tag to compare

@Shironex Shironex released this 04 May 20:14

The Queue tab in the titlebar — dormant since v0.1 — is finally alive. Stack up 20 episodes, walk away, come back to a folder full of finished files.

Headline feature: persistent batch queue at userData/queue.json. Atomic writes survive force-kill; boot recovery demotes orphaned active items back to wait and flags missing source files. Soft-pause lets in-flight encodes finish naturally instead of cancel-and-requeue. 1–4 parallel encodes via the orchestrator's new configurable concurrency cap.

Install

  • Windows — Moekoder-0.3.0-win-x64.exe. Still unsigned; SmartScreen will show More info → Run anyway on first launch.
  • macOS — Moekoder-0.3.0-mac-arm64.dmg. Still unsigned; after dragging into /Applications, run:
    xattr -cr /Applications/Moekoder.app
    to clear the Gatekeeper quarantine.
  • Linux — not shipped this release. Still on the roadmap.

If you're upgrading from v0.2.0, the auto-updater on Windows pulls this release in the background; the next launch comes up on v0.3.0. macOS users grab the DMG manually until code-signing lands.

What you get when you queue

  • Drop a folder of episodes onto the Queue screen — auto-pair runs across every match, one drag enqueues the whole batch.
  • Click Start queue → sequential (or 1–4 parallel) encodes drain the wait list while you walk away.
  • Failed item? Configurable retry budget with exponential backoff (default 2 retries, 4s → 8s → 16s) before it flips to error and the queue keeps moving.
  • Pause halts the dispatcher but lets in-flight encodes finish; Resume picks up at the next waiting item.
  • Force-kill the app mid-queue → relaunch → the snapshot is restored, the formerly-active item is back at wait, and Start picks up where you left off.
  • Drag-reorder the wait list. Active items have their handle suppressed so a mid-encode drop can't shuffle the running job.
  • Per-card chevron drops an inline ffmpeg · stderr log panel below the card. Copy button writes timestamped lines to your clipboard for bug reports.
  • Total-queue disk-space preflight at Start sums every item's estimate per output dir and rejects with a structured error listing every shortfall.
  • Desktop notification fires when the queue drains. Opt out via Settings → Queue.

Queue mechanics

  • Status pills on every card: Wait / Live / Done / Error / Stopped, with per-card mini progress bars and kanji-numbered positions in the wait list.
  • Whole-screen drop overlay on the Queue screen — auto-pairs every match it finds in one shot.
  • Atomic persistence — writeFile(tmp) → rename survives force-kill. The in-memory state debounce-flushes every 200ms and synchronously on before-quit.
  • Boot recovery — active items at shutdown demote to wait and have their attempts counter reset; items whose source video or subtitle vanished off disk demote to error with Source file missing. The queue does not auto-resume — Start is always a deliberate user action.
  • Concurrency cap (1–4 parallel encodes) wired through electron-store. The segmented control on the Queue screen mirrors the same setting; the orchestrator's cap follows the queue while it's running and reverts to 1 when the queue drains so the Single route's "another encode is already running" guarantee comes back unchanged.
  • Per-item retry budget with exponential backoff: queueMaxRetries failed attempts (default 2) before an item flips to error. Each retry waits queueBackoffMs * 2^attempts (default 4s → 8s → 16s).
  • Per-card actions — Force stop on active items (SIGTERM via the existing orchestrator path), Retry on error / cancelled items, Remove on anything else (with a click-twice-to-confirm guard).
  • Add pair multi-file picker on both the Queue screen and the QueueSidebar — runs through the same auto-pair pipeline as drag-and-drop, so a 12-episode batch is one dialog open.
  • queueDefaultRoute routes the app straight into the Queue screen on boot for power-users; default stays single.

Settings → Queue

Five new prefs surfaced under Settings (previously devtools-only):

  • Concurrency (1–4) — mirrors the segmented control on the Queue screen.
  • Max retries (0–10) — how many times a failed item retries before giving up.
  • Retry backoff (1–60s) — base wait; doubles each retry.
  • Notify on complete — desktop notification when the queue drains.
  • Default screen — Single or Queue on launch.

Changed

  • The encode orchestrator's single-job lock at startEncode is now a configurable concurrency cap (setConcurrencyCap, default 1). Single-route behaviour is unchanged because the cap only rises while the queue manager is actively dispatching.
  • The Single / Queue route switcher in the titlebar is finally wired — the markup has been there since v0.1, but the renderer never passed an onRouteChange callback.

Tests

31 new vitest tests across queue/manager.test.ts, queue/persistence.test.ts, queue/preflight.test.ts, and ipc/handlers/queue.test.ts — concurrency dispatch, soft-pause, retries with fake backoff timers, atomic writes, boot recovery, queue-complete notification opt-out, total-queue preflight (empty queue / wait-only filtering / dir grouping / shortfall reporting / probe-failure tolerance).

Known limitations

  • Per-item logs are session-scoped (memory only, capped at 500 lines per item). Relaunching loses completed-item log buffers — by design, to keep queue.json small.
  • Per-item encoding overrides aren't supported. The whole queue uses the current global preset; per-item editing arrives with v0.4.0's advanced preset editor.
  • Preflight rejection currently logs to devtools console.error. A user-facing toast / dialog lands in v0.3.1 — the renderer call sites already point at lib/queue-errors.ts:reportQueueStartError so the upgrade is a single-file swap.
  • Same as v0.2.0: binaries are unsigned, auto-updater is Windows-only, no Linux build.
  • Embedded-font extraction from MKV attachments still TODO — v0.5.0.

Source under the Moekoder Source Available License.

v0.2.0 — Drop it in.

Choose a tag to compare

@Shironex Shironex released this 03 May 20:08

The whole Idle screen is now a target. Drop a video and a subtitle (or a folder containing them) onto the window — Moekoder auto-pairs by filename and pre-fills the three ingredients. No dialogs, no preview popups, just the burn.

Headline feature: drag-and-drop with anime-aware auto-pairing. Works with bare files, multi-file selects, and whole folders. When a drop produces multiple matching candidates, an inline dropdown on the affected stage lets you swap between them without re-running the picker.

Install

  • Windows — Moekoder-0.2.0-win-x64.exe. Still unsigned; SmartScreen will show More info → Run anyway on first launch.
  • macOS — Moekoder-0.2.0-mac-arm64.dmg. Still unsigned; after dragging into /Applications, run:
    xattr -cr /Applications/Moekoder.app
    to clear the Gatekeeper quarantine.
  • Linux — not shipped this release. Still on the roadmap.

If you're upgrading from v0.1.0, the auto-updater on Windows pulls this release in the background; the next launch comes up on v0.2.0. macOS users grab the DMG manually until code-signing lands.

What you get when you drop

  • Drop one video → fills the video slot.
  • Drop a video + subtitle pair → both slots fill via the three-strategy filename matcher (exact → contains → contained-by, with word-boundary checks so ep10 no longer mispairs with ep1).
  • Drop a folder → folder routes to the output slot, contents are scanned and auto-paired.
  • Drop multiple matching subtitles (or videos) → an inline chevron next to the stage opens a swap menu listing every candidate.

Under the hood

  • Whole-Idle drop overlay with a tinted veil and an 投 sigil; covers the screen, not just an inner card.
  • Three-strategy filename auto-pairing (exact match → video-base contains subtitle-base → subtitle-base contains video-base) with word-boundary checks so ep10.mkv no longer mispairs with ep1.ass when the matching ep10.ass is missing.
  • Folder drops scan their immediate children for media (non-recursive), surface videos + subtitles to the auto-pair pipeline, and route the folder itself to the output slot — drop one folder, all three slots fill.
  • Inline candidates dropdown on the video and subtitle stages: when a drop produces multiple matches, a chevron next to the slot opens a swap menu. Built on Radix Popover with a portal-rendered, scrollable, collision-aware panel.
  • Multi-select OS file picker (dialog:open-files) wired through to the same setter pipeline as drag-drop.
  • New fs:list-folder IPC channel — extension-filtered, numeric-aware sort so ep2 precedes ep10 in the candidates list.
  • Extended renderer whitelist — video accepts .mkv .mp4 .m4v .webm .avi .mov .ts .m2ts; subtitle accepts .ass .ssa .srt .vtt. The same constants drive both drop classification and dialog filters.
  • Output-override guard (outUserDirty) — swapping the active video candidate no longer clobbers an output folder the user manually picked via the Output stage.
  • webUtils.getPathForFile exposed via the preload bridge so drag-and-drop continues to resolve real filesystem paths under Electron's modern contextIsolation (the legacy File.path was removed in 32+).

Changed

  • File classification is now extension-only across the whole renderer — File.type was unreliable for .ass (often arrives as empty MIME) and .mkv (Linux-only on Chromium).
  • Click and drop pipelines now share a single setVideoFromPath / setSubsFromPath / setOutFromFolder setter layer so the auto-output-folder derivation cannot drift between entry points.

Tests

Vitest landed in apps/web for the first time, with 13 unit tests covering filename categorisation, the auto-pair matcher, and edge cases (numeric prefixes, case-insensitive matching, empty inputs).

Known limitations

  • Same as v0.1.0: binaries are unsigned, auto-updater is Windows-only, no Linux build, single-file encode (queue lands in v0.3).
  • Folder scan is non-recursive — top-level files only. Nested release-folder layouts surface only the directly-dropped folder's children.

Source under the Moekoder Source Available License.