Snacks v2.17.0
Automated Media Library Transcoder
A release centered on documentation, CI, and hardware-detection reliability. Snacks now ships a full in-app user guide, operations handbook, and public API reference at /docs/index.html, backed by a machine-readable OpenAPI contract at /openapi/v1.json — and a new GitHub Actions pipeline builds, tests, audits, smoke-boots, and validates that contract on every push. GPU auto-detection no longer gates a whole vendor on its HEVC probe alone: any passing codec now admits the device, and AMF gets a second explicitly-configured probe attempt for RDNA4 driver quirks. The login redirect is hardened against open-redirect crafting, the SignalR hub now requires authentication, and the version reported by /api/health, the cluster protocol, and the page footer now all derive from the single <Version> in Snacks.csproj instead of hand-maintained strings that each release had to remember to bump.
In-app documentation & public API contract
Documentation site at /docs/index.html
A self-contained user guide, operations handbook, and public API reference, served from wwwroot on every running instance (http://YOUR-SNACKS-HOST:6767/docs/index.html) and linked from the app footer and README. Covers setup, encoding behavior, cluster operation, port binding, API automation, and the full public route list — destructive operations are explicitly marked.
OpenAPI 3 contract at /openapi/v1.json
AddOpenApiinProgram.cs— generates a machine-readable contract for the supported UI/public JSON API (/api/*and/metrics). Cluster RPC routes (/api/cluster/*) are an internal protocol and are deliberately excluded from the document.scripts/validate-openapi.mjs(new) — asserts the document is OpenAPI 3, has a sane path count, contains/api/healthand/api/queue/items, and leaks no internal cluster routes. CI runs it against a live server.
Port binding, documented at startup
The explicit Kestrel config overrides HTTP_PORTS / DOTNET_URLS / appsettings endpoints, which made moving the port a guessing game. Startup now logs the effective bind address plus the one variable that actually works (ASPNETCORE_URLS, e.g. http://0.0.0.0:7070), and the docs cover it.
Hardware detection
Any passing codec admits the device
Windows GPU detection gated each vendor on its HEVC probe alone — if HEVC init failed, the whole device was erased even when other codecs worked. That dropped h264-only NVENC silicon (Kepler / first-gen Maxwell) and RDNA4 cards where HEVC/AV1 AMF init fails while H.264 AMF works. NVENC, QSV, and AMF are now probed per-codec (HEVC, H.264, AV1) and the device is admitted when any probe passes — the same rule the Linux VAAPI path already used — with the codec list built from the actual probe results. macOS VideoToolbox gets the same treatment (HEVC + H.264).
AMF probed twice: bare, then an explicit session
Default-parameter AMF init alone can't be trusted to prove absence: an RX 9070 XT (RDNA4, Adrenalin 26.7.1) reported AMF undetected while an explicitly configured hevc_amf session encoded fine, and per-codec AMF init failures on RDNA4 are a known driver defect class.
BuildEncoderProbeAttempts— AMF encoders now get two attempts: bare defaults first (a pass skips the second run), then an explicit-usage transcoding -quality qualitysession with-qp_i 24 -qp_p 24for H.264/HEVC.-qp_i/-qp_pare H.264/HEVC-only options and stay off the AV1 attempt.- Probe logging — attempts now carry a named variant (
low_power,explicit_session) so the detection log shows exactly which probe form passed or failed.
Security
Login redirect hardened against open redirects
AuthController echoed the returnUrl query parameter into the post-login redirect, so a crafted login link could bounce a successful sign-in to an external site.
NormalizeReturnUrl(new) — accepts only application-local paths (matching ASP.NET Core's local-URL rules:/pathand~/pathare local; scheme-relative//hostand slash-backslash/\hostvariants are not), falling back to/. The redirect itself now goes throughLocalRedirectas a second guard.
SignalR hub requires authentication
/transcodingHub was on the auth middleware allowlist, so the real-time progress hub was reachable without a session. It has been removed — hub connections now authenticate like every other page. The allowlist is down to the login form, secret-authenticated cluster RPC, the /api/health liveness probe, static assets, and the exact-match Prometheus /metrics scrape.
One version, everywhere
The version reported by /api/health, the cluster discovery protocol, and the page footer were hand-maintained hardcoded strings — every release had to bump each one individually (the 2.16.0 release notes list all three under "Version bumps").
AppVersion(new) — resolves the MSBuild<Version>from assembly metadata at runtime, stripping build metadata. The health endpoint,ClusterDiscoveryService.ClusterVersion, and the page footer all read from it now, so the per-release manual bumps disappear and the values can never drift from the csproj.scripts/sync-version.mjs(new) — propagates theSnacks.csprojversion to the electronpackage.json/package-lock.json,build-and-export.bat, the README badge/footer, the docs site, and the version test.--checkmode fails when anything drifts, and CI runs it on every push.AppController(new) —api/healthandapi/restartmoved out ofHomeControllerinto a dedicated attribute-routed API controller (which also puts them in the OpenAPI document).
CI pipeline
.github/workflows/ci.yml (new) — two jobs on every push and PR:
- Backend — .NET 10 restore/build/test, a NuGet vulnerability audit (
--vulnerable --include-transitive), then a smoke boot of the real server followed by live checks of/api/health,/docs/index.html, and the OpenAPI contract validator. - Frontend —
npm ci, syntax checks on the electron main/notarize scripts, the version-sync check, and thenode --testsuites: every public controller route must appear in the HTML API reference, docs fragment links must resolve, and the browser modules (escapeHtml, queue API client, library path encoding) are exercised directly.
Operational logging
Cluster and transcoding diagnostics went to Console.WriteLine, so they reached the console but never the rolling application log.
- Serilog everywhere — a
global using Serilogplus conversion of all cluster, dispatch, transfer, scan, and probe messages toLog.Information/Log.Warning. Dispatch failures, node timeouts, heartbeat anomalies, and probe results are now retained in the file log. - Silent catches now log — empty
catch { }blocks (cluster config read at startup, heartbeat capability parsing, operation-log persistence/reads, remote log lines) now emit warnings with context instead of swallowing the exception. Log.CloseAndFlush()— the process now flushes the file sink on shutdown, so the last moments before an exit are no longer lost.
Refactoring & tests
ClusterCapacityPolicy(new) — the pure cluster scheduling rules (device enablement, effective per-device capacity, free-slot detection, dispatchable node states) extracted fromClusterServiceand covered byClusterCapacityPolicyTests.VideoTransformPlanner(new) — the pure video geometry/frame-rate planning (downscale policy and target height, scale expression, fixed-frame letterbox filter, fps cap, frame-rate parsing) extracted fromTranscodingService, which now delegates.AuthMiddleware.IsAllowlisted— madeinternaland locked down byAuthMiddlewareTests(hub requires auth, health/cluster/static stay open) andAuthRedirectTests(open-redirect vectors).AppVersionTests— asserts the runtime version matches the released version and that the cluster protocol version tracks it;sync-version.mjskeeps the assertion current.- Electron test harness (new) —
api-contract.test.cjs,docs.test.cjs,frontend.test.cjsundernode --test, wired intonpm run check.
Dependency upgrades
- SkiaSharp 2.88.8 → 4.150.1 — major bump;
ImagePreprocessormigrated from the removedSKFilterQualityAPI toSKSamplingOptionswith a cubic resampler (same high-quality scaling for OCR preprocessing). - EF Core 10.0.0 → 10.0.10,
SQLitePCLRaw.bundle_e_sqlite33.0.4 pinned explicitly. Microsoft.AspNetCore.OpenApi10.0.10 (new), withMicrosoft.OpenApipinned to 2.7.5 to stay above the vulnerable 2.0.0 transitive floor.- Serilog.AspNetCore 9 → 10, Serilog.Sinks.File 6 → 7, Newtonsoft.Json 13.0.3 → 13.0.4, TesseractOCR 5.3.5 → 5.5.2.
- Electron 41 → 43, @electron/notarize 2.5 → 3.1, electron-builder 26.8 → 26.15;
package-lock.jsonregenerated. - Test stack — Microsoft.NET.Test.Sdk 17 → 18, xunit 2.9.3, FluentAssertions major bump (API rename
BeGreaterOrEqualTo→BeGreaterThanOrEqualTo), coverlet 10.0.1.
Files Changed
Documentation & OpenAPI
Snacks/wwwroot/docs/index.html— user guide, operations handbook, and API reference (new)Snacks/Program.cs— OpenAPI document with public-route filter; startup port-binding log; Serilog flush on exitscripts/validate-openapi.mjs— contract validation (new)Snacks/Views/Shared/_Layout.cshtml— footer Documentation linkREADME.md— documentation pointers
Hardware detection
Snacks/Services/TranscodingService.cs— per-codec vendor probes with any-pass admission (Windows NVENC/QSV/AMF, macOS VideoToolbox); AMF explicit-session probe attempt; named probe variantsSnacks.Tests/Video/EncoderProbeTests.cs— AMF two-attempt coverage, variant naming
Security
Snacks/Controllers/AuthController.cs—NormalizeReturnUrl+LocalRedirectopen-redirect fixSnacks/Services/AuthMiddleware.cs—/transcodingHubremoved from the allowlistSnacks.Tests/Security/AuthMiddlewareTests.cs,AuthRedirectTests.cs— allowlist and redirect coverage (new)
Versioning
Snacks/AppVersion.cs— runtime version from assembly metadata (new)Snacks/Controllers/AppController.cs—api/health/api/restartextracted fromHomeController(new)Snacks/Services/ClusterDiscoveryService.cs— cluster protocol version derives fromAppVersionscripts/sync-version.mjs— version propagation with--check(new)Snacks.Tests/Settings/AppVersionTests.cs— version lock (new)
CI
.github/workflows/ci.yml— backend build/test/audit/smoke/contract job + frontend checks job (new)electron-app/package.json—test,check,check:syntax,check:version,sync:versionscriptselectron-app/tests/api-contract.test.cjs,docs.test.cjs,frontend.test.cjs— electron/browser test suites (new)
Logging
Snacks/GlobalUsings.cs— global Serilog using (new)Snacks/Services/ClusterService.cs,ClusterNodeJobService.cs,ClusterFileTransferService.cs,ClusterDiscoveryService.cs,AutoScanService.cs,IntegrationService.cs,TranscodingService.cs,FileService.cs,RollingVerificationService.cs,NotificationService.cs, and controllers —Console.WriteLine→ Serilog; empty catches now log
Refactoring
Snacks/Services/ClusterCapacityPolicy.cs— pure cluster scheduling rules (new),Snacks.Tests/Cluster/ClusterCapacityPolicyTests.cs(new)Snacks/Services/VideoTransformPlanner.cs— pure video transform planning (new)Snacks/Services/Ocr/ImagePreprocessor.cs— SkiaSharp 4 sampling API migration
Version bumps
Snacks/Snacks.csproj—<Version>2.17.0</Version>(single source of truth) and package upgradesSnacks.Tests/Snacks.Tests.csproj— test stack upgradeselectron-app/package.json/package-lock.json,build-and-export.bat,README.md,Snacks/wwwroot/docs/index.html— synchronized viasync-version.mjs
Full documentation: README.md · /docs/index.html on a running instance