Snacks v2.18.0
Automated Media Library Transcoder
A release with two headline features. Advanced Video policies add an opt-in decision layer on top of the Simple settings: ordered rules match source properties (codec, resolution class, bitrate, bit depth, HDR, …) and route each file to a reusable encoding recipe — with per-encoder-family quality modes (CRF/CQ/QP/ICQ), exact runtime-detected encoders that are never silently substituted, guarded FFmpeg options, an Always Keep retention mode for quality-first encoding, and a library-wide impact preview that shows what a staged policy would do before it is applied. Homarr dashboards get two integration paths: a compact server-rendered Snacks tile embedded through a scoped iframe token, and Homarr's native Media Transcoding widget backed by a read-only Tdarr-compatible adapter — both fed by a new versioned public API. Rounding it out: AMF hardware probes now use a 1080p test frame so RDNA4 cards no longer lose HEVC/AV1 detection, FFmpeg execution moved from a shell-quoted argument string to a literal argument vector, and native package builds gate on new FFmpeg-inventory and template validation scripts.
Advanced Video is off by default. An existing settings file follows the exact legacy pipeline after upgrading — nothing changes until the layer is enabled and a policy passes Validate & Apply.
Advanced Video policies (opt-in)
A new Advanced Video Policies section under Settings → Video, stored as the advancedVideo block (schema version: 1) inside the encoder options. The panel walks three numbered steps: Decision flow, Encoding recipes, and What would happen to your library.
Decision flow: ordered rules, first match wins
VideoRule— name, All/Any match mode, and a flat list of conditions; evaluated top-to-bottom over enabled rules, first match wins, unmatched files fall to a configurable default action.- Matchable source facts —
Codec,Width,Height,ResolutionClass,BitrateKbps,FileSizeBytes,DurationSeconds,PixelFormat,BitDepth,IsHdr,Is4K, with operatorsIs/IsNot/In/NotIn, numeric comparisons,Between, andIsKnown/IsUnknown. VideoSourceFacts— normalizes ffprobe output before matching: codec aliases collapse (hevc/h.265/x265→h265,av01→av1), resolution class derives from the short edge (sd–2160p+), bit depth is inferred from the pixel format (yuv420p10le,p010le, …), HDR fromsmpte2084/arib-std-b67/bt2020. Unknown values match onlyIsUnknown— never accidentally.- Actions —
UseSimpleSettings(legacy ladder, unchanged),TranscodeWithProfile(forces a re-encode even where the legacy bitrate/codec ladder would skip),MuxOnly, andSkip. - Shadowed-rule analysis —
AdvancedVideoRuleAnalysisproves when a later rule can never fire (subsumption over normalized text sets and numeric ranges, deliberately conservative) and flags it inline: "Never reached — '<earlier rule>' always claims these files first."
Encoding recipes
VideoEncodingProfile— a complete video-side spec: codec, encoder selection, rate control, preset/speed, threads, pixel format, GOP, profile/level, downscale policy, fixed frame size, fps cap, HDR tonemap, crop, additional filters, custom options, and output retention. From Simple seeds a recipe from the current Simple form.- Rate control: Bitrate, Quality, or Custom — Quality maps to each encoder family's native control via
VideoEncoderRegistry: CRF for x264/x265/SVT-AV1, CRF +-b:v 0for libaom, quantizer for rav1e,-rc vbr -cqfor NVENC, ICQ (-global_quality) for QSV, CQP for VAAPI,-rc cqp -qp_i/-qp_pfor AMF,-q:vfor VideoToolbox — each with its real range and label in the UI. Bitrate mode emits target/min/max/bufsize with family-specific mode flags; strict mode pins min=max=target. Custom emits no generated rate control at all — the recipe's own options take over. - Exact encoders, detected at runtime — the picker at
GET /api/settings/video-encodersis a union of encoders detected on the local FFmpeg (FfmpegCapabilityService, parsing-encodersplus per-encoder-h encoder=<name>for real pixel formats and private options, cached 10 minutes), encoders advertised by connected cluster workers, and the 20-entryVideoEncoderRegistry.KnownEncoderscatalog. Undetected encoders stay selectable so a policy is portable to hardware that joins later. An explicit encoder is never substituted: no HW→SW fallback, no retry-ladder swap — if it can't run anywhere, the job waits with a visible reason instead. - Guarded FFmpeg options — the "Power user" editor takes one option token plus at most one literal value per row, never a shell string.
AdvancedVideoValidatorblocks structural options (~50 exact tokens like-i,-f,-vf,-c:v,-map… plus prefix families like-metadata,-hwaccel,-hls_) on both names and values; typed options (-crf,-preset, …) are allowed with a duplicate warning and win because custom options are appended last. Encoder-private options (-aom-params,-aq-mode, …) pass freely. Additional filters are a single ordered chain validated against topology splits and input-producing filters. - Output retention —
SmallerOnly(default, the legacy no-savings discard) orAlwaysKeepfor quality-first profiles where predictable quality matters more than final size; the validator warns when Quality-mode output is combined withSmallerOnly.
Preview before apply, measure after
- Library impact preview (
POST /api/settings/advanced-video/impact) — runs the staged, unsaved policy through the real resolver over every tracked video: per-outcome buckets with counts, sample filenames, and disk usage, per-rule match badges on the flow cards, projected output size for bitrate-mode recipes, and a "What happens to…" per-file search. Read-only, and capped at 20,000 analyzed rows (uniform sampling beyond, flagged as such). - Measured results (
GET /api/settings/advanced-video/measured) — "Measured so far — completed encodes by recipe": real jobs, kept/discarded counts, bytes saved, and duration-weighted average output bitrate per profile from the encode-history ledger. - Test a hypothetical file — post sample source facts and watch the matching rule light up; every recipe shows a live FFmpeg argument preview.
- History labels — migration
AddAdvancedVideoHistoryLabelsaddsAdvancedProfileId,AdvancedProfileName, andAdvancedRuleNametoEncodeHistory, snapshotted at write time by both the local and cluster history writers so renames and deletions never break the ledger.
Templates and portable policies
- Four quick-start templates ("Start from a template") —
av1-everything(one AV1 CRF 32 quality recipe, Always Keep),av1-tiered(CRF 32 for 4K, CRF 35 below),hevc-saver(HEVC CRF 24, skip files already h265/av1), and the EXPERT-badgedlibaom-expert(exactlibaom-av1, tiered CQ 35/32, 10-bit, guarded-aom-params/-arnr-*rows) — byte-identical to the shippedexamples/advanced-video-policy.json. - User templates — save the current policy by name (
POST /api/settings/advanced-video/templates, max 20, persisted toconfig/advanced-video-templates.json); only validation-clean policies are accepted. - Export / import — policies download as
snacks-video-policy.json; import accepts the wrapper or a bare block, mints fresh GUIDs, remaps profile references, and lands as a staged draft — nothing applies until Validate & Apply.
Validation is the gate, everywhere
- Transactional editor — the advanced block is excluded from the debounced settings auto-save; the panel stages changes with an "Unapplied changes" badge, Validate & Apply, Cancel, and a session-scoped Restore previous rollback.
AdvancedVideoValidator— one server-side authority for saved settings, presets, previews, imports, templates, and dispatched plans, emitting stable diagnostic codes (profile_id,encoder_codec,rate_control_adapter,option_reserved,filter_topology,rule_shadowed, …).POST /api/settingsreturns 400 with the diagnostics array instead of writing a broken file, and also verifies every watched folder pinned to a profile still resolves.- Blocking, not silent fallback — an invalid or unsatisfiable plan parks the affected jobs as Pending with a human-readable
WaitReason(shown as a queue badge alert), and Analyze reports "Waiting for valid advanced video settings" rather than guessing.
Cluster-aware dispatch
VideoJobPlanprotocol v1 — the coordinator resolves the policy once and ships only the frozen plan;CloneOptionsForWorkerAsyncstrips the rule/profile catalog from worker options. Workers advertiseAdvancedVideoProtocolVersionin heartbeat capabilities (legacy workers default to 0).VideoJobRouter— capability-aware scoring: advanced jobs are hard-rejected (−100) on nodes below the plan's protocol version or on devices that don't advertise a pinned exact encoder; a pinned encoder skips the hardware-preference gates and outranks vendor matching. Worker heartbeats now advertise the encoder list recomposed from the real FFmpeg inventory intersected with tested hardware devices, instead of a hardcoded vendor table.- Receipt validation on the worker —
VideoPolicyResolver.ValidateResolvedPlanre-checks protocol, action/mode agreement, profile integrity, and exact-encoder↔codec consistency before accepting a job; the assigned device must actually carry the exact encoder. - Reconnect wakes pinned jobs — a
RoutingCapabilitySignaturehash (protocol + encoders + devices) triggers an immediate dispatch pass when a worker's capabilities change, so jobs waiting on an exact encoder unpark the moment a capable worker returns. Crash recovery re-resolves the policy against current settings before resuming. - Folder policy overrides — watched folders can
Inherit, forceSimple, or pin a specificProfile(Advanced Video Policysection in the folder dialog); node overrides deliberately can't. Referential checks reject a folder pinned to a deleted profile, and a conflicting scalar encoder override blocks with a reason instead of silently substituting. Manual Process Item now passes options straight through — policy resolution happens once, inside the pipeline. - Local scheduling — the policy is resolved before device-slot reservation; an unschedulable exact-encoder item defers instead of pinning the queue head, devices are filtered by advertised encoder, and the CPU device now derives its encoder list from the actual FFmpeg build.
FFmpeg execution: argument vector, not shell string
FfmpegArgumentList (new) carries literal tokens end-to-end into ProcessStartInfo.ArgumentList — paths, filter chains, and option values are single tokens that can no longer be split or re-quoted; the legacy command string remains only for display and log snapshots. Queue and Analyze surfaces gained policy provenance: rule/profile/encoder badges on work items and a Rule · Profile · plan summary line in Analyze results.
Homarr dashboards
Two integration paths, both documented in the new in-app guide (/docs/index.html#homarr):
| Option | What it provides | Credential |
|---|---|---|
| Snacks compact tile | Read-only iFrame with Stats, Queue, and Workers tabs, rendered by each viewer's browser | Scoped iframe URL from Settings → Security → Iframe Access |
| Media Transcoding widget | Homarr's native transcoding UI, fetched by the Homarr server | A Snacks API key on Homarr's Tdarr integration |
The compact tile at /iframe/homarr
- Server-rendered, zero JavaScript — the whole tile is one self-contained HTML document; it auto-updates via
<meta http-equiv="refresh">(default 30 s, clamped 10–3600,refresh=0disables) and switches tabs with plain links, so it works under the strictest embed sandboxes. - Stats — hero space-saved figure, a server-rendered 14-day savings sparkline, Queued/Processing/Failed KPI chips, and a Now encoding panel with live progress. Queue — active + pending work with middle-truncated filenames (quality tags and extensions survive). Workers — local node first, then discovered cluster nodes de-duplicated by node id, with paused pills and per-job device/phase lines.
- Tuning via query string —
theme=dark|light,tab=stats|queue|workers,limit(1–30 queue rows),refresh; all values are clamped server-side. Responsive down to ~320 px tiles, honorsprefers-reduced-motion. - Embed-safe headers —
Content-Security-Policy: frame-ancestorsbuilt from the configured origin allowlist,Cache-Control: no-store,Referrer-Policy: no-referrer.
Scoped iframe access
snk_embed_tokens — Settings → Security → Iframe Access generates a 32-byte scoped credential and a ready-to-copy embed URL. The token is accepted only as?embedToken=on/iframe/*— never on/api/*— and an API key is never valid as an embed token (both compared in constant time). Generate to rotate, Revoke to kill immediately.- Origin allowlist — Allowed Homarr origins normalizes entries to scheme + authority (paths and trailing slashes stripped, wildcards/credentials/non-web schemes rejected with a 400) and feeds
frame-ancestors; when empty, only Snacks' own origin may embed. - No login page inside the widget — unauthenticated
/iframe/*requests return401instead of redirecting to the sign-in form, and the route is deliberately not on the auth allowlist. - Log hygiene — request-start logging is lowered to warning so
embedToken/apiKeyquery values never land in the rolling application log.
Tdarr-compatible adapter for the native widget
TdarrCompatibilityController (new) exposes exactly the four routes Homarr's Tdarr integration consumes — POST /api/v2/is-server-alive, POST /api/v2/stats/get-pies, GET /api/v2/get-nodes, POST /api/v2/client/status-tables — and nothing else: no job control, no plugin execution, no mutation routes to inherit.
- Statistics — encode totals, savings in Tdarr's GB units, queue/processing/error segments, codec mix, and 4K split; zero-value pie segments are omitted because Homarr renders them as
NaN%. - Workers — node → worker dictionaries with progress, phase, and device; the local node is synthesized first and discovered duplicates are filtered so Homarr never renders it twice. Fields Snacks doesn't track (fps, ETA, size estimates) are neutral zeros in the exact shape Homarr parses.
- Queue — Tdarr's
table1with exact case-sensitive field names and MB file sizes; the health-check table intentionally returns empty.
Public read-only API at /api/v1/*
PublicApiController (new) is a versioned, deliberately write-free seam modeled on Sonarr/Radarr envelopes: GET /api/v1/system/status (version, role, node id, uptime), /api/v1/stats, /api/v1/queue (paged, clamped 1–100), and /api/v1/workers. Cancel/retry/clear stay on the internal /api/queue/* surface. All of it is fed by DashboardIntegrationService (new), a database-backed read model shared with the tile and the Tdarr adapter: pending rows come from SQLite (the authoritative queue), active and recent terminal items from the bounded in-memory registry, with active paths excluded from the pending page so a file never shows up twice. Queue records now preserve the real source codec — AV1 sources are reported as av1 instead of the old h264/hevc guess.
API keys on the query string — narrowly
?apiKey= is now accepted for integrations that can't send headers (Homarr's Tdarr client), but only on read-only integration paths: the /api/v1/* prefix and the four Tdarr routes by exact match — a future /api/v2/* mutation route cannot inherit it, and /api/settings, /api/queue/*, /api/auth/* still require a header, bearer token, or session. New embed-token management endpoints: GET/DELETE /api/auth/embed, POST /api/auth/embed/generate, POST /api/auth/embed/origins; GET /api/auth/config now reports hasEmbedToken and iframeAllowedOrigins.
Hardware detection
AMF probed with a 1080p frame — RDNA4 HEVC/AV1 fix
The synthetic probe source used to verify encoders was a 256×256 test frame. RDNA4 (RX 9070 XT, Adrenalin 26.7.1) accepts that for H.264 but rejects it for HEVC and AV1 AMF sessions with AMF_OUT_OF_RANGE — so the card was registered as H.264-only even though real 1080p HEVC/AV1 encodes worked, and those jobs routed to CPU. AMF encoders (h264_amf, hevc_amf, av1_amf) are now probed against a 1920x1080 30 fps black frame on both probe attempts; all other vendors keep the cheap 256×256 source, and EncoderProbeTests pins both choices.
Packaging validation
Native package builds (build-installer.bat, build-mac.sh) now fail fast on three new checks, documented in BUILDING.md:
scripts/validate-ffmpeg-inventory.mjs— the staged FFmpeg must advertise the baselinelibx264,libx265, andlibsvtav1encoders (libaom/rav1e are runtime-optional, reported but not required).scripts/validate-advanced-templates.mjs— every quick-start template must build validation-clean with no dangling profile references, the expert template must stay byte-identical toexamples/advanced-video-policy.json, and the generated C# fixture (quick-start-templates.json) must match the JS source of truth (--writeregenerates).scripts/test-advanced-video-ui.mjs— 22 zero-dependency assertions over the panel's pure display logic (plain-language rule sentences, byte humanization, import id-remapping).
Tests
Roughly 160 new tests across the two feature areas:
- Advanced Video —
AdvancedVideoPolicyTests(rule matching, overrides, clone safety, worker receipt contract),AdvancedVideoArgumentsTests(per-family quality options, blocklist, filter topology, literal token vector),AdvancedVideoShadowTests(the full subsumption matrix),AdvancedVideoImpactTests,AdvancedVideoMeasuredTests,AdvancedVideoTemplateTests(every shipped template validates clean and routes as advertised),AdvancedVideoHistoryLabelTests, plus new cases inVideoJobRouterScoreTests(protocol/encoder gating),EncodedOutputKeepDecisionTests(Always Keep vs Smaller Only),VideoFilterTests(additional-filter ordering),DeviceSlotSelectionTests, andEncoderOptionsJsonContractTests(advancedVideoround-trip). - Dashboards & auth —
DashboardCompatibilityTests(Tdarr field-name/unit contract, zero-segment suppression, token-escaping tab links, AV1 codec preservation), embed-token scoping and origin-normalization cases inApiKeyAuthTests, query-key path restrictions inAuthMiddlewareTests, and pending/active de-duplication inDbQueueTests.
Files Changed
Advanced Video
Snacks/Models/AdvancedVideoOptions.cs— policy schema: profiles, rules, conditions, rate control, retention (new)Snacks/Services/VideoPolicyResolver.cs— pure policy resolution + worker receipt validation (new)Snacks/Services/AdvancedVideoValidator.cs— the single validation authority with stable diagnostic codes (new)Snacks/Services/VideoEncoderRegistry.cs— encoder-family knowledge and profile argument builder (new)Snacks/Services/FfmpegCapabilityService.cs— runtime FFmpeg encoder inventory, cached (new)Snacks/Services/FfmpegArgumentList.cs— literal argument vector; no shell string execution (new)Snacks/Services/AdvancedVideoImpactService.cs,AdvancedVideoMeasuredService.cs,AdvancedVideoRuleAnalysis.cs— impact buckets, per-recipe measurements, shadowed-rule proofs (new)Snacks/Services/TranscodingService.cs— policy resolution in scan/analyze/schedule/convert, exact-encoder pinning, deferred candidates, argument-vector execution, real CPU encoder inventorySnacks/Services/ClusterService.cs,ClusterNodeJobService.cs,ClusterDiscoveryService.cs,Routing/VideoJobRouter.cs— plan-only dispatch, protocol/encoder scoring gates, receipt validation, capability-signature redispatchSnacks/Services/VideoFilterBuilder.cs— additional-filter chain placementSnacks/Models/EncoderOptions.cs,EncoderOptionsOverride.cs,WorkItem.cs,JobMetadata.cs,EncodeHistory.cs,FileAnalysisResult.cs,ClusterNode.cs— policy fields, source facts, wait reasons, history labelsSnacks/Migrations/20260808084045_AddAdvancedVideoHistoryLabels.cs— three nullable label columns onEncodeHistory(new)Snacks/Data/EncodeHistoryRepository.cs,MediaFileRepository.cs— labeled-history projection, savings series, queued paging with exclusionsSnacks/Controllers/SettingsController.cs— validate/impact/measured/templates/video-encoders endpoints; save-time validation gateSnacks/Controllers/ClusterAdminController.cs,LibraryController.cs— folder policy referential checks; pass-through Process ItemSnacks/Views/Shared/_AdvancedVideoSettings.cshtml,Snacks/wwwroot/js/settings/advanced-video.js— the three-step policy panel (new)Snacks/wwwroot/js/settings/encoder-form.js,presets.js,main.js,js/queue/work-item-renderer.js,js/library/analyze-modal.js,js/cluster/override-dialog.js,css/site.css— staged persistence, preset interaction, policy badges, folder override UIexamples/advanced-video-policy.json— shipped expert policy (new)
Homarr & public API
Snacks/Controllers/PublicApiController.cs—/api/v1/*read-only API +/iframe/homarr(new)Snacks/Controllers/TdarrCompatibilityController.cs— four-route read-only Tdarr adapter (new)Snacks/Services/DashboardIntegrationService.cs— shared DB-backed queue read model (new)Snacks/Views/Homarr/Index.cshtml— self-contained zero-JS tile (new)Snacks/Controllers/AuthApiController.cs,Snacks/Services/AuthService.cs,AuthMiddleware.cs,Snacks/Models/AuthConfig.cs— embed tokens, origin allowlist, read-only query-key gating, iframe 401 semanticsSnacks/Views/Shared/_SecuritySettings.cshtml,Snacks/wwwroot/js/settings/panels/auth-panel.js,js/api.js— Iframe Access panel and embed URL managementSnacks/Program.cs— new service registrations; request-log hygiene for URL credentials
Hardware detection
Snacks/Services/TranscodingService.cs— 1080p AMF probe source (BuildEncoderProbeAttempts)Snacks.Tests/Video/EncoderProbeTests.cs— probe-source assertions
Build & docs
build-installer.bat,build-mac.sh— FFmpeg inventory, template, and UI-logic validation gatesscripts/validate-ffmpeg-inventory.mjs,validate-advanced-templates.mjs,test-advanced-video-ui.mjs— packaging validators (new)Snacks/wwwroot/docs/index.html— Advanced Video guide and Homarr dashboards guide (#advanced-video,#homarr) with troubleshootingREADME.md,docs/BUILDING.md— feature overview, Homarr comparison table, release checklistdocs/images/advanced-flow.png,advanced-impact.png— panel screenshots (new)
Tests
Snacks.Tests/Video/AdvancedVideo*.cs,Fixtures/quick-start-templates.json— the Advanced Video suites and generated template fixture (new)Snacks.Tests/Integration/DashboardCompatibilityTests.cs— Tdarr/tile contract coverage (new)Snacks.Tests/Auth/ApiKeyAuthTests.cs,Security/AuthMiddlewareTests.cs,Pipeline/DbQueueTests.cs,Pipeline/EncodedOutputKeepDecisionTests.cs,Cluster/VideoJobRouterScoreTests.cs,Video/VideoFilterTests.cs,DeviceSlotSelectionTests.cs,Settings/EncoderOptionsJsonContractTests.cs— extended
Version bumps
Snacks/Snacks.csproj—<Version>2.18.0</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 viasync-version.mjs
Full documentation: README.md · /docs/index.html on a running instance