Repository navigation
Releases: Shironex/moekoder
Release list
v0.8.0: Electron 44 and security
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 keepssandbox: true).electron-builderstays at 26.15.7 andelectron-updaterat 6.8.9. - Major bumps:
- Desktop and web:
vitest4.1.11 → 5.0.3. Desktop now declaresviteas a dev dependency, since vitest 5 needs it as a peer. - Landing:
@astrojs/react6.0.6 → 7.0.1, withastro7.3.8. - Landing:
satori0.33.5 → 0.47.1, pinned exactly. Newer satori sizes boxes like a browser, so the OG card frame is nowborder-boxandog-default.pngwas regenerated. Apart from a few pixels in the rotated seal, the card is unchanged.
- Desktop and web:
- pnpm 10.9.0 → 10.34.6 in
packageManager, CI, the release workflow and the landing Dockerfile. pnpm-workspace.yaml now setsminimumReleaseAgeto 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
concurrently10.0.5 → 10.0.6: closes the criticalshell-quoteadvisory GHSA-pqg4-j6r4-53mv. Dev tooling only, nothing ships in the app.wait-on9.1.0 → 9.5.1: pullsaxiosup to 1.20.0 and closes 13 axios advisories (7 high). Dev tooling only.sharp0.35.4 → 0.35.5: closes GHSA-wq5f-xc86-pv6w.- Override floors raised: these four overrides stopped one patch short of the fixed releases.
undicito 8.10.2 (11 advisories).brace-expansionto 5.0.12 (3 advisories, 2 high).fast-urito 3.1.8.shell-quoteto 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 throughelectron-builderand is never shipped.
v0.7.2: Translation hotfix
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.jsonhas declared only CSS as having side effects since 0.1.0. When i18n landed in 0.7.0, the production bundler treatedlib/i18n.tsas side-effect free and dropped it, locale resources included../src/lib/i18n.tsis now listed insideEffects, and the packaged renderer contains both locales again.
Added
pnpm showcaseregenerates the README screenshots and banners with@noctcore/showcase-kit. It builds the renderer alone in ashowcaseVite 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-testidhooks 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.mdsits alongside it. - New dev dependencies:
@noctcore/showcase-kit0.3.0 andplaywright1.63.0, both pinned exactly. Nothing new ships in the app.
Removed
- The old CDP screenshot script (
scripts/screenshot-app.mjs) and theassets/screenshots/images it produced, replaced bypnpm showcase.
v0.7.1 — Security patch
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-builderis unchanged at 26.15.7. - Patch and minor bumps:
- Desktop:
zod4.6.5. - Web:
react/react-dom19.3.0,i18next26.4.2,react-i18next17.0.15,lucide-react1.47.0,tailwind-merge3.7.0,vite8.3.0,@vitejs/plugin-react6.1.1. - Landing:
astro7.3.3,@astrojs/react6.0.6,@astrojs/sitemap3.7.4,satori0.33.5,sharp0.35.4. - Root tooling:
eslint10.11.0,typescript-eslint8.70.1,globals17.12.0,lint-staged17.5.1,prettier3.9.8.
- Desktop:
Security
astro7.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.sharp0.35.3 → 0.35.4: closes GHSA-rgj7-g3m4-5g8c, a high-severity libheif overflow.js-yamloverride floor raised to 4.3.2 (GHSA-2883-xcg3-v3hh). This is the highest-priority fix for end users, becauseelectron-updaterparses update manifests fetched over the network with js-yaml at runtime.- Other override floors raised:
fast-urito 3.1.6 (4 high-severity advisories).svgoto 4.1.0, now capped below v5.devalueto 5.9.2 (GHSA-9rgm-9g3h-6x36). Dependabot reported this one;pnpm auditdidn't.
- New overrides:
@xmldom/xmldom≥0.8.15 <0.9 (10 advisories). It's capped becauseplistonly declares^0.8.8.fflate≥0.7.5 <0.8.satoristill exact-pins 0.7.3.joi≥18.2.5 <19 (low severity, dev-only).
v0.7.0 — English + Polish UI, AMD encoding, dependency sweep
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 ofAppin the renderer entry, so a module-scope lookup can never race it). Decorative kanji/romaji eyebrows are left inline. A self-containedLanguagePickerappears in Settings and on the onboarding Welcome step. uiLanguagesetting — nullable, OS-detect-then-sticky: unset means "follow the OS", and the first explicit pick freezes the choice.useUiLanguage/useUiLanguageSyncread 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/HevcAmfSettingsunion members andh264_amf/hevc_amfbranches inargs.ts, written in AMF's own rate-control vocabulary (-quality {speed|balanced|quality} -rc cqp -qp_i/-qp_p/-qp_b, pixel formatyuv420p,p010lefor 10-bit HEVC) and verified against ffmpeg'samfencsource rather than analogized from NVENC.{codec:'av1',hwAccel:'amf'}is intentionally non-representable — ffmpeg has noav1_amf. resolveHwAccel()and probe-driven encoder selection — a best-effortprobeGpudependency 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 averifiedflag and a zodgpuProbeResultSchema. - Per-dialog last-used directory —
UserSettings.lastDialogDirsmaps aDialogDirKind(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 rememberdirname(). A cancelled dialog remembers nothing, a directory that no longer resolves is dropped at read time rather than handed to Electron dead, and an explicitdefaultPathstill 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 thechrome.scriptingfallback-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/weband@moekoder/landing, bumped in lockstep so a single@types/reactmajor exists in the store. No source changes were needed — the renderer was already oncreateRootand every ref passes an initial argument. - Astro 6 → 7 on the landing app (Rust compiler, Vite 8), with
@astrojs/react5 → 6. Alsotailwind-merge2 → 3 (Tailwind v4 was already in place, so v2 was a latent class-group mismatch),sharp0.34 → 0.35,concurrently9 → 10,lint-staged16 → 17,@types/yauzl2 → 3, andelectron-builderpinned to 26.15.7. engines.noderaised 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.allSettledlet 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 singlecontainerLabelkey 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-labelpulled from thecommonnamespace while its sibling buttons used queue-namespace keys; it now usesqueue.retryLabel. - Inconsistent Windows install-path casing in onboarding copy — the engine-gate destination and
dl.install.subdisagreed (moekodervsMoeKoder); both now matchAPP_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-updater6.8.3 → 6.8.9 — the highest-priority fix in the sweep, since it runs on end-user machines. Pullsbuilder-util-runtime9.7.0, closing GHSA-p2f4-r6v6-j797 (a cross-origin redirect leaksAuthorization/PRIVATE-TOKENheaders) 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.overridesfloors 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 (nanoidto an ESM-only v6 behind postcss's^3.3.17,@babel/coreto 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.1chain was collapsed — it dragged in its own oldapp-builder-libwith vulnerabletar,js-yaml,fast-uriandbuilder-util-runtime, duplicating packages the top-level resolution had already fixed. Because it andapp-builder-libpin 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 criticaltaradvisory along with several highs. - Dev-path fixes —
vite8.0.13 → 8.1.5 (CVE-2026-53571,server.fs.denybypass on Windows alternate paths),esbuild0.28.0 → 0.28.1 (GHSA-g7r4-m6w7-qqqr, arbitrary file read via the dev server),wait-on9.0.10 → 9.1.0 (clears all 8 axios advisories), andastro7.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.tsasserts arg shape only — the flags were derived from ffmpeg'samfencsource, not from a successful encode. If you have an AMD card, a 1-frame encode confirming ffmpeg ...
v0.6.0 — Mux, don't burn.
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:to clear the Gatekeeper quarantine.xattr -cr /Applications/Moekoder.app
- 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.assbecomes 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
-progressparser; 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→.srtwith a styling-loss note). - No collisions — output filenames carry the track ordinal (
<stem>.<lang>.s<N>.<ext>), so a full + a signs track both taggedengdon't overwrite each other.
Added
- Soft-sub mux mode, the dedicated Extract screen, the
subtitle:extractIPC channel, and a sharedresolveSubtitleOutputcodec ⇄ extension resolver.
Changed
- Removed the deprecated
escapeSubtitlePath/escapeSubtitlePathForlibass-escape aliases — no remaining callers; mux and extract args go straight tospawn(no shell, no libass filter), so no path escaping is needed.
Fixed
text-codec extraction no longer fabricates a.assfile that ffmpeg rejects — the generic ffprobetexttag 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
.assper 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
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:to clear the Gatekeeper quarantine.xattr -cr /Applications/Moekoder.app
- 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 datedautobuild-*tag that BtbN later deletes. NewresolveWindowsSource()queriesapi.github.com/repos/BtbN/FFmpeg-Builds/releases/tags/latest, selects the stableffmpeg-n8.1-latest-win64-gpl-8.1.zipasset, and builds theBinaryArchivefrom its livebrowser_download_urland the API-reporteddigest. - Verification never silently dropped —
resolveWindowsSource()throws a clear, actionable error on any failure (network, missing asset, or a missing/malformedsha256:<hex>digest) rather than degrading to an unverified download. The zip's top-level folder is derived from the asset name, so theffmpeg.exe/ffprobe.exeentry paths stay correct across rebuilds. - macOS unchanged — still a static evermeet.cx pin.
Added
fetchGitHubJson<T>()inmain/http.ts— gated JSON GET that rides the same per-host rate-limit clock asdownloadToFile, sends theUser-Agent+Accept: application/vnd.github+jsonheaders 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.comhost gate (1000 ms) — spacing keeps a single install's resolution calls clear of GitHub's 60 req/hr/IP unauthenticated ceiling.
Changed
getSourceForPlatformis now async — both platform branches share one awaitable call site;ensureInstalledawaits 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
latestrelease exposing theffmpeg-n8.1-latest-win64-gpl-8.1.zipasset with an API-reporteddigest. 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
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:to clear the Gatekeeper quarantine.xattr -cr /Applications/Moekoder.app
- 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-extractormodule runsffmpeg -dump_attachment:t '' -i <video>against a per-jobos.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
nullwhen 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 ontoEncodeJob.fontsDir, and both terminal callbacks (onCompleteandonError/ CANCELLED) runcleanupFontsDir(...)so the temp dir is removed whether the encode succeeded, errored, or was user-cancelled.
libass fontsdir= wiring
buildFilterChainlearns an optionalfontsDirargument and emitssubtitles='<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. escapeSubtitlePathis nowescapeLibassPath— same helper handles bothsubtitles=andfontsdir=arguments. The old name is kept as a@deprecatedalias for one release.
Settings → Embedded fonts
- New section in
apps/web/src/screens/Settings.tsx("字 · ji"), wired to the newuseEmbeddedFontssetting 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 awarnper 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 inos.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.
fontStemswitched topath.parse(...).name— replaces the regex-based extension strip; correctly handles compound names likeBauhaus 93.ttfso the missing-font diagnostic matches the right filename.
Changed
EncodeJobgainsfontsDir?: string— opt-in field; callers that don't set it produce the v0.4 filter chain byte-for-byte.OrchestratorDepsseams —probeAttachments,extractFonts,cleanupFontsDir,getUseEmbeddedFonts, andreadSubtitleFileare 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
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:to clear the Gatekeeper quarantine.xattr -cr /Applications/Moekoder.app
- 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) andlibx265software encoder. NVENC main10 emitsformat=yuv420p10leupstream for the encoder; libx265 inherits source pixel format. - AV1 encode path —
av1_nvenc(RTX 40-series, gated bygpu.probe()'s per-encoder name list) andlibsvtav1software encoder with integer-preset 0..13. - HEVC + MP4 mux flag —
-tag:v hvc1appended on the HEVC + MP4 combination so QuickTime / iOS pick up the stream as HEVC. - Discriminated-union
EncodingSettings— the shape is now tagged oncodec, eliminating illegal combinations (libx265 + AV1, etc.) at compile time. Each branch carries only its valid hardware paths and codec-specific knobs (NVENCpN, 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: 1from 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 psnrrunner (ffmpeg/psnr.ts) parsesaverage: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:runIPC 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
EncodingSettingsis now a discriminated union overcodec— 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 calldefaultsFor(codec)rather than spreadingBALANCED_PRESETdirectly.- Filter chain pixel format is now codec-aware — NVENC h264 still emits
format=yuv420p; HEVC + AV1 NVENC emitformat=yuv420p10lewhentenBitis set. libx265 + libsvtav1 software branches don't force a pixel format and inherit from the source. - App.tsx prefers the persisted
encodingprofile 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-react4.7.0 → 6.0.2 (withresolve.dedupe+ droppedminify:'esbuild'for vite 8 compat), vitest 3 → 4 in web + desktop (explicitincludeglob pinned for desktop), zod 3 → 4 in apps/desktop. - Node engine floor raised to
>=22.12.0to 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.1pinned 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
encodingprofile. 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-seriestooltip; 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.
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:to clear the Gatekeeper quarantine.xattr -cr /Applications/Moekoder.app
- 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
errorand 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 · stderrlog 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) → renamesurvives force-kill. The in-memory state debounce-flushes every 200ms and synchronously onbefore-quit. - Boot recovery —
activeitems at shutdown demote towaitand have their attempts counter reset; items whose source video or subtitle vanished off disk demote toerrorwithSource 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:
queueMaxRetriesfailed attempts (default 2) before an item flips toerror. Each retry waitsqueueBackoffMs * 2^attempts(default 4s → 8s → 16s). - Per-card actions — Force stop on
activeitems (SIGTERM via the existing orchestrator path), Retry onerror/cancelleditems, 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.
queueDefaultRouteroutes the app straight into the Queue screen on boot for power-users; default stayssingle.
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
startEncodeis 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
onRouteChangecallback.
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.jsonsmall. - 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 atlib/queue-errors.ts:reportQueueStartErrorso 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.
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:to clear the Gatekeeper quarantine.xattr -cr /Applications/Moekoder.app
- 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
ep10no longer mispairs withep1). - 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.mkvno longer mispairs withep1.asswhen the matchingep10.assis 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-folderIPC channel — extension-filtered, numeric-aware sort soep2precedesep10in 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.getPathForFileexposed via the preload bridge so drag-and-drop continues to resolve real filesystem paths under Electron's moderncontextIsolation(the legacyFile.pathwas removed in 32+).
Changed
- File classification is now extension-only across the whole renderer —
File.typewas unreliable for.ass(often arrives as empty MIME) and.mkv(Linux-only on Chromium). - Click and drop pipelines now share a single
setVideoFromPath/setSubsFromPath/setOutFromFoldersetter 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.