github derekshreds/Snacks v2.20.1
Snacks v2.20.1

3 hours ago

Snacks v2.20.1

Automated Media Library Transcoder

A security release, recommended for every install and essential for anyone who reaches Snacks from outside their own network. Snacks ships with sign-in turned off, so an instance that was port-forwarded or put behind a reverse proxy without a password handed anonymous visitors full control. Without a configured password, Snacks now only answers local-network clients. FFmpeg moves to builds that fix PixelSmash (CVE-2026-8461), a heap overflow triggered just by probing a crafted video, in the Docker image and both desktop apps. Every place Snacks could decode Brotli data (CVE-2026-111497) is switched off. File names, settings, and imported presets can no longer add their own FFmpeg options, and names shown in the UI can no longer run script. Sign-in gains attempt limiting and a current-password check, and the desktop app moves to Electron 43.8.3.


Upgrade notes

  • Remote access without sign-in is refused. Requests from public addresses get 403 while no password is configured. Enable sign-in under Settings → System → Security before exposing Snacks. If an upstream proxy (Authelia, Authentik, …) already authenticates every user, set SNACKS_ALLOW_REMOTE_WITHOUT_AUTH=true. To treat extra address ranges as local, list them in SNACKS_LOCAL_NETWORKS (for example 203.0.113.0/24, 2001:db8:1::/48).
  • Remote dashboards with sign-in off (Homarr, Homepage, Tdarr-style widgets on another network) need an API key, or the embed token for /iframe/*.
  • Changing the password signs out every session. While sign-in is on, changing the username or password, or turning sign-in off, asks for the current password.
  • Ten failed sign-ins from one client within five minutes pause further attempts from that client until the window passes. Successful sign-ins never count.
  • Library Health → Delete only removes files the page lists.
  • Advanced video profiles that spell a reserved option another way (-filter, -filter:v:0, -c:v:0, -pass:v) or use FFmpeg's -/option file form are now rejected by the validator. No shipped template uses them.
  • Building from source: self-contained desktop builds need a .NET 10 SDK that bundles runtime 10.0.13 or newer. On macOS, build the bundled FFmpeg with electron-app/scripts/build-ffmpeg-mac.sh before ./build-mac.sh; build-mac.sh refuses an FFmpeg older than 8.1.2 (or 7.1.5) or one without zscale and libvorbis. On Windows, stage gyan.dev's ffmpeg-8.1.2-full_build (or a newer 8.1.x) in electron-app\ffmpeg; build-installer.bat and build-electron.bat apply the same checks, and name git-snapshot builds (whose version can't be compared) in the error.

Remote access requires sign-in

AuthMiddleware previously let every request through whenever sign-in was off. It now does so only for local clients, decided by ClientNetwork (new):

Counts as local Notes
Loopback, RFC1918 (10/8, 172.16/12, 192.168/16), CGNAT / Tailscale (100.64/10), link-local, IPv6 unique-local (fc00::/7) IPv4-mapped IPv6 (::ffff:a.b.c.d) is normalized first
IPv6 prefixes (/64 or narrower) assigned to the machine's own interfaces Dual-stack home networks give clients public IPv6 addresses, and browsers prefer IPv6 when a hostname resolves both ways. Rescanned once a minute. IPv4 subnets are not included because cloud hosts often sit in large shared public subnets. On a VPS whose provider shares one /64 across customers, those neighbors count as local — enable sign-in there
Ranges listed in SNACKS_LOCAL_NETWORKS Comma, semicolon, or space separated; bare addresses and CIDR ranges; host bits are masked off

Forwarding headers — X-Forwarded-For, Forwarded, X-Real-IP, CF-Connecting-IP, True-Client-IP — are only ever used to make a request less trusted: if any address in them is public (or unparseable), the request is treated as remote. A forged header can only lock its sender out, so no trusted-proxy list is needed, and an internet client relayed by a LAN reverse proxy is recognized. A proxy that sends no forwarding headers at all still looks local; Caddy, Traefik, and Nginx Proxy Manager send them by default, plain Nginx needs proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;.

Still reachable from anywhere: /api/health, /metrics, /openapi/v1.json, the sign-in page, static assets, secret-authenticated cluster RPC (/api/cluster/*), any /api/* call with a valid API key, and /iframe/* with the embed token. Everything else gets a plain-text 403 explaining how to enable sign-in; the server logs the refusal at most once a minute.

Static-file allowlist no longer matches by extension

The sign-in check treated any non-/api/ path containing a . as a static file. Real static files are served before the check runs, so that rule only ever matched routes — including the default MVC route's {id} segment, which let a dotted path reach the main page without signing in. Assets are now allowlisted by folder (/lib/, /css/, /js/, /img/, /images/, /webfonts/, /docs/, /favicon), and the published API contract by its exact path.


Sign-in hardening

  • Attempt limiting — LoginThrottle (new) counts failed sign-ins per client and refuses further attempts for the rest of a five-minute window after ten failures; the sign-in page shows how long to wait and the response carries Retry-After. Behind a LAN reverse proxy the client is identified by the proxy-appended (right-most) X-Forwarded-For entry, so one attacker can't lock out everyone else.
  • Current password — POST /api/auth/config takes currentPassword (SaveAuthRequest.CurrentPassword, new). While sign-in is active, a save that changes the username or password or turns sign-in off is refused unless it matches, so a borrowed session or an API key alone can't take over the account. Wrong guesses count toward the same throttle. The Security panel shows a Current password field while a password is set.
  • Sessions end on password change — AuthService.UpdateConfig rotates the session secret whenever a new password is saved (it already did when sign-in was turned off), invalidating every outstanding cookie.
  • Secure cookie behind TLS proxies — the session cookie is marked Secure when X-Forwarded-Proto is https, not only when Kestrel itself sees HTTPS.

FFmpeg: PixelSmash (CVE-2026-8461)

A heap out-of-bounds write in FFmpeg's MagicYUV decoder fires while a crafted AVI / MKV / MOV is merely probed — and Snacks probes every file its scans find, usually files fetched by Sonarr, Radarr, or Lidarr.

Build 2.20.0 2.20.1
Docker jellyfin-ffmpeg7 7.1.3-5 jellyfin-ffmpeg7 7.1.4-3 — upstream FFmpeg 7.1.4 plus Jellyfin's backport of the MagicYUV fixes (0094-backport-a-fix-from-upstream-for-magicyuv-decoder.patch). Same 7.1 line.
macOS app FFmpeg 8.1 FFmpeg 8.1.3, built by the new electron-app/scripts/build-ffmpeg-mac.sh with Homebrew ffmpeg@8's configure options plus libzimg and libvorbis, bundled with x265 4.3, SVT-AV1 4.2, LAME 4.0, libvpx 1.17, dav1d 1.5.4
Windows app gyan.dev git snapshot 2026-04-09-git-d3d0b7a5ee gyan.dev 8.1.2 full_build — the first 8.1 release with the MagicYUV fix

Both updates were compared against the 2.20.0 builds on a synthetic corpus run through Snacks' own command shapes:

  • Docker — outputs are bit-identical apart from the Lavf version string in container metadata, and ffprobe output, crop detection, bitrate sampling, health verification, and subtitle extraction are unchanged. Hardware encoders (VAAPI, QSV, NVENC, AMF) could not be exercised; upstream FFmpeg made no functional changes to them between 7.1.3 and 7.1.4, and Jellyfin's VAAPI / QSV patches are unchanged.
  • macOS — everything Snacks reads or decides on is unchanged: ffprobe output, crop detection, bitrate sampling, the VideoToolbox encoder probes, and VideoToolbox encodes (stream layout, tags, hvc1, bitrate). The updated x265, SVT-AV1, and LAME produce different output bytes for the same settings — x265 bitrates within ±0.1%, MP3 bitrates and durations identical — and MP3 files now report LAME4.0 as their encoder.
  • macOS Library Health — FFmpeg 8.1 logged "PPS changed between slices" when verification seeked into some multi-slice HEVC files, which Snacks counted as a decode failure. FFmpeg 8.1.3 decodes them the same way without the message, so those healthy files no longer show as verify-failed.

The macOS FFmpeg build

Homebrew's FFmpeg is built without libzimg and libvorbis, so the Mac app could never tonemap HDR to SDR (the zscale filter) or encode Ogg Vorbis music. build-ffmpeg-mac.sh (new) installs ffmpeg@8's Homebrew dependencies plus zimg and libvorbis, verifies the FFmpeg 8.1.3 source tarball's SHA-256, builds it with ffmpeg@8's configure options plus --enable-libzimg --enable-libvorbis (outside the repository, since FFmpeg's build can't handle a path with spaces), and bundles the result into electron-app/ffmpeg. Run it once per FFmpeg update.

Compared with Homebrew's ffmpeg@8 8.1.3 across the same test suite, the only differences are the additions: zscale, the libvorbis encoder and decoder, and Vorbis as the Matroska / Ogg / NUT muxers' default audio codec (Snacks always names its audio codec). HDR tonemapping, VideoToolbox encodes with tonemapping, and Ogg Vorbis encodes now succeed; every other test produced the same results as the Homebrew build, including its run-to-run encoder variation. The tonemapped frames match the Docker build's (SSIM 0.997). The yadif_videotoolbox filter is left out unless Xcode's Metal Toolchain is installed; Snacks doesn't use it.

build-mac.sh now reads the staged FFmpeg's version and stops if it predates the fix (8.1.2 on the 8.1 line, 7.1.5 on 7.1), warning on any major version other than 8; SNACKS_ALLOW_UNPATCHED_FFMPEG=1 overrides for an emergency build. It also stops if the staged FFmpeg lacks zscale or libvorbis, and points to build-ffmpeg-mac.sh for both.

The Windows FFmpeg build

The Windows app moves from a gyan.dev master snapshot taken before the fix to gyan.dev's 8.1.2 release build. Its advertised encoders, decoders, filters, and hardware accelerations match the snapshot's apart from two master-only additions Snacks doesn't use (the pdv encoder and the v360_vulkan filter); AMF, NVENC, QSV (libvpl), and D3D11VA are all present. gyan.dev's ffmpeg-release-full link now serves FFmpeg 9, which the desktop build hasn't been validated against, so the build scripts point to the 8.1 release archive instead.

scripts/validate-ffmpeg-version.mjs (new) is the Windows counterpart of build-mac.sh's check, run by build-installer.bat and build-electron.bat: it refuses an FFmpeg older than 8.1.2 (or 7.1.5), refuses a git snapshot whose version can't be compared, warns on a major version other than 8, and requires zscale and libvorbis. SNACKS_ALLOW_UNPATCHED_FFMPEG=1 overrides the version check.


FFmpeg arguments from file names and settings

File paths are always single arguments

Bitrate sampling (MeasureVideoBitrateAsync), VAAPI quality calibration (RunTestEncodeAsync), crop detection, and music encodes built their commands as strings with the file path in double quotes, then had them split back into arguments. A path containing a " could therefore end its argument early and add FFmpeg options of its own. All four now pass the path as one literal argument (FfmpegArgumentList.Add("-i", path)), and MusicEncoderArgs.Build returns an FfmpegArgumentList instead of a string. The main encode path already did this.

For ordinary file names — spaces, parentheses, brackets, apostrophes, &, $, back-ticks, Unicode — the argument vectors are byte-for-byte identical to 2.20.0 across every combination of hardware init flags, filters, and music options (2,850 cases compared). The only differences are paths containing " and music paths containing \.

Windows music fix: the string tokenizer treated \ as an escape character, so every Windows music path (C:\Music\…) reached FFmpeg with its separators stripped and the encode failed. Music encodes now work on Windows.

Encoder, preset, and level are plain values

Encoder, FfmpegQualityPreset, and VideoLevel come from settings, imported presets, per-request options, folder overrides, and cluster job metadata, and were placed into the same split-on-whitespace strings. They must now be a single plain token (letters, digits, _, ., -, not starting with -; surrounding whitespace is trimmed as before):

  • TranscodingService.RequirePlainEncoder (new) fails the job with "'…' is not a valid FFmpeg encoder name" before the encoder probe runs.
  • An invalid preset falls back to medium and an invalid level is omitted, each with a job-log line.
  • FfmpegArgumentList.IsPlainValue (new) is the shared check. Every value FFmpeg accepts for these options passes.

Advanced video: reserved options in any spelling

AdvancedVideoValidator reserves options that control command structure, but compared them exactly, so -filter (no stream specifier), -filter:v:0, -filter_script:v, -c:v:0, and -pass:v slipped past entries for -vf, -filter:v, -c:v, and -pass. IsReservedToken (new) also checks the option's base name before the first :, adds -filter, and rejects FFmpeg 7+'s -/option form, which reads the option's value from a file. Custom option values get the same check.


Brotli (CVE-2026-111497)

An out-of-bounds write in Google's Brotli decoder (1.0.3–1.2.0) affects the copy built into .NET 10 and Ubuntu's libbrotli1; neither vendor has shipped a fixed build yet. Snacks now never hands data to a Brotli decoder:

  • Luna connector — the only HTTP client with automatic decompression used DecompressionMethods.All. It now decompresses gzip and deflate only, so br is neither requested nor decoded.
  • Request bodies — a request carrying any Content-Encoding other than identity is refused with 415 before routing. Snacks never decompressed request bodies, so no working client is affected; this keeps any future middleware from starting to.
  • Desktop window — the Electron window may only load from its own backend (http://localhost:<port>, including the SignalR WebSocket). Chromium's response decoders never see third-party content; external links still open in the system browser.
  • BrotliExposureTests (new) fails the build if DecompressionMethods.All / .Brotli, BrotliStream, BrotliDecoder, or request decompression appears in the application code.

The vulnerable library code remains present but unused inside the .NET runtime and the image's libbrotli1 (a FreeType dependency); a later image rebuild picks up the vendors' fixes once they ship.


Web UI: names render as text

File, folder, preset, template, and cluster node names come from media, shared preset files, and remote nodes. Several places inserted them into the page as HTML:

  • showToast (site.js) now sets the message as text. None of its 131 callers passed markup.
  • Confirm dialogs that interpolate a preset label, preset name, template name, or imported file name (presets.js, advanced-video.js) escape it.
  • The cluster dashboard escapes node IDs in data-node-id attributes.

Cluster

  • ClusterAuthFilter and LocalNetworkOnlyFilter are now authorization filters, so a caller without the shared secret is refused before its request body is read or model-validated.
  • GET /api/cluster/integrations (credential hand-out to workers) also refuses requests whose forwarding headers name a public client, so a reverse proxy in front of the master no longer makes internet callers look like LAN workers.
  • Upload names — a chunked upload named _shared.json or _metadata.json (any case) is stored as input_shared.json / input_metadata.json (ClusterController.ToUploadFileName, new), so it can't replace the job's own control files. Names are compared in the form Windows resolves them to, so on a Windows worker a trailing dot or space (_shared.json.), a stream suffix (_shared.json::$DATA), or an 8.3 short name (_SHARE~1.JSO) is also stored under the prefixed name. Media names, including ones that start with an underscore, are unchanged.

Encoding fixes

These failed before 2.20.1 on every FFmpeg version; each fix changes only the command for the case that used to fail.

  • Opus music with a VBR quality setting — MusicEncoderArgs.Build turned VbrQuality into -q:a, which FFmpeg's libopus encoder rejects, so every Opus encode with it set failed. Opus has no quality scale and is always VBR, so it now uses the configured bitrate as its VBR target; MP3, Vorbis, and AAC keep -q:a.
  • MP4 timed-text subtitles into MKV — Matroska has no codec ID for MP4's mov_text, so keeping one of those tracks in an MKV output aborted the mux ("Subtitle codec mov_text is not supported"). Both subtitle paths — the language-filtered pass-through (FfprobeService.MapSub) and the OCR/sidecar mux — now convert just those tracks to SubRip (FfprobeService.NeedsSrtForMatroska, new). Every other subtitle codec is still stream-copied.
  • 8-bit profiles on 10/12-bit sources — libx264 rejects baseline / main / high and libx265 rejects main / mainstillpicture when the source has more than 8 bits per sample ("high profile doesn't support a bit depth of 10"). The simple video path now drops an 8-bit-only profile for such a source, with a job-log line, and the encoder chooses High 10 / Main 10 — the same output Snacks produces when no profile is configured. A tonemapped encode ends in 8-bit and keeps its profile; advanced profiles are untouched, since they can set their own pixel format (TranscodingService.IsEightBitOnlyProfile / IsHighBitDepthSource, new).
  • macOS: HDR→SDR tonemapping and Ogg Vorbis music — the Mac app's FFmpeg had neither the zscale filter (libzimg) nor the libvorbis encoder, so every tonemapped encode and every Ogg Vorbis music encode failed. The Mac build now includes both (see the FFmpeg section). The unused VideoFilterBuilder.TonemapSwChainFallback is removed: it could never tonemap correctly, since colorspace can't linearize PQ or HLG.

Library Health

POST /api/library/health/delete checked only that the path was inside the library. It now also requires the file to be listed on the Library Health page under any category (MediaFileRepository.IsHealthFlaggedAsync, new); other paths get 400 "File is not flagged in Library Health". Delete all was already driven by the health list.


Desktop app

  • Electron 43.8.3 (from 43.1.1) resolves twelve Electron security advisories; build tooling dependencies (undici, fast-uri, http-cache-semantics) were updated within their ranges, and npm audit is clean.
  • In-window navigation is checked against the parsed backend origin instead of a string prefix, so a URL such as http://localhost:6767@other.host/ opens in the system browser like any other external link.
  • .NET runtime 10.0.13 — self-contained builds ship whatever runtime the building SDK bundles. Snacks.csproj gains MinimumSelfContainedRuntimeVersion and a target that fails a self-contained build below it, so desktop builds include the October 2026 ASP.NET Core security update. The Docker image's aspnet:10.0 base already resolves to 10.0.13.

Response headers

Every response now carries X-Content-Type-Options: nosniff and Referrer-Policy: same-origin. X-Frame-Options is deliberately not set: dashboards such as Organizr embed the full UI in a frame, and /iframe/* keeps its own frame-ancestors allowlist.


Documentation

  • /docs/index.html — the authentication section explains remote access (what counts as local, forwarding headers, SNACKS_LOCAL_NETWORKS, SNACKS_ALLOW_REMOTE_WITHOUT_AUTH), the sign-in throttle, and the current-password requirement; the API reference documents currentPassword on POST /api/auth/config and the Library Health delete scope.
  • README — the Docker quick start notes that remote access requires sign-in.
  • docs/BUILDING.md and the in-app build card — macOS builds install ffmpeg@8 and copy its binaries.
  • docs/BUILDING.md — Windows builds stage gyan.dev's FFmpeg 8.1.x full_build instead of ffmpeg-release-full (now FFmpeg 9), and need a .NET SDK that bundles runtime 10.0.13.

Tests

  • RemoteAccessGateTests (new) — loopback, RFC1918, CGNAT, link-local, unique-local, and IPv4-mapped peers are local, public IPv4 / IPv6 peers are remote; a LAN proxy relaying a public client is remote for every supported header and address form (ports, brackets, quoted Forwarded, unknown); configured ranges extend the local set; the rate-limit key uses the proxy-appended client and ignores forged headers from public peers; with sign-in off, LAN clients pass, public and proxied-public clients get 403, infrastructure routes stay reachable, and API keys and embed tokens admit public clients only on their own routes.
  • LoginThrottleTests (new) — the budget, per-client isolation, and window expiry.
  • SessionRotationTests (new) — a password change invalidates existing sessions; a save without a new password keeps them.
  • ArgumentInjectionTests (new) — plain-value acceptance and rejection, refusal of an unrecognized encoder that GetEncoder passes through, music paths containing quotes staying single arguments, reserved option spellings rejected and ordinary encoder options accepted, and reserved upload names, including their Windows aliases (on Windows, an aliased upload is written next to a real _shared.json and the control file must be unchanged).
  • HealthDeleteScopeTests (new) — only rows the health page lists count as flagged.
  • BrotliExposureTests (new) — no Brotli decode path in application code.
  • AuthMiddlewareTests — dotted non-asset paths require the session; asset folders and /openapi/v1.json stay public.
  • MusicEncoderArgsTests (new) — Opus ignores VbrQuality and targets the bitrate; MP3, Vorbis, and AAC keep -q:a; without VbrQuality every lossy encoder uses the bitrate.
  • SubtitleMappingTests — Matroska copy flags are unchanged when no track needs conversion, and a mov_text track becomes -c:s:N srt.
  • RateControlAndScaleTests — the 8-bit-only profile set and high-bit-depth detection by pixel format (yuv420p10le, p010le, 12- and 16-bit) and profile name.

Files Changed

Remote access and sign-in

  • Snacks/Services/ClientNetwork.cs (new) — local-network classification, forwarding-header parsing, interface IPv6 prefixes, SNACKS_LOCAL_NETWORKS, rate-limit identity
  • Snacks/Services/LoginThrottle.cs (new) — failed sign-in counter
  • Snacks/Services/AuthMiddleware.cs — remote gate, folder-based asset allowlist, embed-token and API-key admission with sign-in off, throttled rejection log
  • Snacks/Services/AuthService.cs — session secret rotates on password change
  • Snacks/Controllers/AuthController.cs — throttle and Secure cookie behind TLS proxies
  • Snacks/Controllers/AuthApiController.cs, Snacks/Models/Requests/SaveAuthRequest.cs — current-password requirement
  • Snacks/Views/Shared/_SecuritySettings.cshtml, Snacks/wwwroot/js/settings/panels/auth-panel.js, Snacks/wwwroot/js/api.js — Current password field

FFmpeg

  • Snacks/Dockerfile — jellyfin-ffmpeg7=7.1.4-3*
  • electron-app/scripts/build-ffmpeg-mac.sh (new) — builds and stages the macOS FFmpeg
  • build-mac.sh — FFmpeg version and zscale / libvorbis checks; points to build-ffmpeg-mac.sh
  • scripts/validate-ffmpeg-version.mjs (new) — the same checks for Windows builds
  • build-installer.bat, build-electron.bat — run validate-ffmpeg-version.mjs; point to gyan.dev's 8.1 release archive
  • run-electron-dev.bat — points to gyan.dev's 8.1 release archive
  • Snacks/Services/TranscodingService.cs — literal path arguments for bitrate sampling, calibration, and crop detection; RequirePlainEncoder, IsEightBitOnlyProfile, IsHighBitDepthSource (new); preset and level validation; 8-bit-only profiles dropped for high-bit-depth sources; mov_text → SubRip in the sidecar mux
  • Snacks/Services/MusicEncoderArgs.cs — returns FfmpegArgumentList; Opus ignores VbrQuality
  • Snacks/Models/MusicEncoderOptions.cs — VbrQuality documents the Opus exception
  • Snacks/Services/FfprobeService.cs — NeedsSrtForMatroska (new); MapSub converts mov_text for Matroska
  • Snacks/Services/VideoFilterBuilder.cs — TonemapSwChainFallback removed; the zscale requirement documented
  • Snacks/Services/FfmpegArgumentList.cs — IsPlainValue (new)
  • Snacks/Services/AdvancedVideoValidator.cs — IsReservedToken (new), -filter reserved

Brotli, headers, and request handling

  • Snacks/Program.cs — Luna client without Brotli, 415 for encoded request bodies, nosniff / Referrer-Policy, LoginThrottle registration

Web UI

  • Snacks/wwwroot/js/site.js, Snacks/wwwroot/js/settings/presets.js, Snacks/wwwroot/js/settings/advanced-video.js, Snacks/wwwroot/js/cluster/cluster-dashboard.js

Cluster and library

  • Snacks/Services/ClusterAuthMiddleware.cs, Snacks/Services/LocalNetworkOnlyFilter.cs — authorization filters; forwarding-header check
  • Snacks/Controllers/ClusterController.cs — ToUploadFileName (new), including Windows name aliases
  • Snacks/Controllers/LibraryController.cs, Snacks/Data/MediaFileRepository.cs — IsHealthFlaggedAsync (new)

Desktop app

  • electron-app/main.js — window restricted to the backend origin; parsed-origin navigation check
  • electron-app/package.json / package-lock.json — Electron 43.8.3 and tooling updates
  • Snacks/Snacks.csproj — MinimumSelfContainedRuntimeVersion and RequirePatchedSelfContainedRuntime

Documentation

  • Snacks/wwwroot/docs/index.html, README.md, docs/BUILDING.md

E2E harness

  • e2e/lib.sh — scenarios start instances inside $(...), so the PIDs never reached the EXIT trap and every run left its instances running (listening on all interfaces with SNACKS_ALLOW_ALL_PATHS); PIDs are now also recorded in a temp file the trap reads

Tests

  • Snacks.Tests/Security/RemoteAccessGateTests.cs, LoginThrottleTests.cs, ArgumentInjectionTests.cs, HealthDeleteScopeTests.cs, BrotliExposureTests.cs, Snacks.Tests/Auth/SessionRotationTests.cs, Snacks.Tests/Audio/MusicEncoderArgsTests.cs (all new)
  • Snacks.Tests/Security/AuthMiddlewareTests.cs, Snacks.Tests/Subtitles/SubtitleMappingTests.cs, Snacks.Tests/Video/RateControlAndScaleTests.cs, Snacks.Tests/Pipeline/FullCommandScenarioTests.cs

Version bumps

  • Snacks/Snacks.csproj — <Version>2.20.1</Version> (single source of truth)
  • electron-app/package.json / package-lock.json, build-and-export.bat, Snacks.Tests/Settings/AppVersionTests.cs, Snacks/wwwroot/docs/index.html — synchronized via sync-version.mjs; the docs page's updated date moves to 2026-10-24

Full documentation: README.md · /docs/index.html on a running instance

Don't miss a new Snacks release

NewReleases is sending notifications on new releases.