Skip to content

Snacks v2.18.0

Latest

Choose a tag to compare

@derekshreds derekshreds released this 10 Aug 01:21
88cada4

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 factsCodec, Width, Height, ResolutionClass, BitrateKbps, FileSizeBytes, DurationSeconds, PixelFormat, BitDepth, IsHdr, Is4K, with operators Is/IsNot/In/NotIn, numeric comparisons, Between, and IsKnown/IsUnknown.
  • VideoSourceFacts — normalizes ffprobe output before matching: codec aliases collapse (hevc/h.265/x265h265, av01av1), resolution class derives from the short edge (sd2160p+), bit depth is inferred from the pixel format (yuv420p10le, p010le, …), HDR from smpte2084/arib-std-b67/bt2020. Unknown values match only IsUnknown — never accidentally.
  • ActionsUseSimpleSettings (legacy ladder, unchanged), TranscodeWithProfile (forces a re-encode even where the legacy bitrate/codec ladder would skip), MuxOnly, and Skip.
  • Shadowed-rule analysisAdvancedVideoRuleAnalysis proves 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 0 for libaom, quantizer for rav1e, -rc vbr -cq for NVENC, ICQ (-global_quality) for QSV, CQP for VAAPI, -rc cqp -qp_i/-qp_p for AMF, -q:v for 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-encoders is a union of encoders detected on the local FFmpeg (FfmpegCapabilityService, parsing -encoders plus per-encoder -h encoder=<name> for real pixel formats and private options, cached 10 minutes), encoders advertised by connected cluster workers, and the 20-entry VideoEncoderRegistry.KnownEncoders catalog. 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. AdvancedVideoValidator blocks 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 retentionSmallerOnly (default, the legacy no-savings discard) or AlwaysKeep for quality-first profiles where predictable quality matters more than final size; the validator warns when Quality-mode output is combined with SmallerOnly.

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 AddAdvancedVideoHistoryLabels adds AdvancedProfileId, AdvancedProfileName, and AdvancedRuleName to EncodeHistory, 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-badged libaom-expert (exact libaom-av1, tiered CQ 35/32, 10-bit, guarded -aom-params/-arnr-* rows) — byte-identical to the shipped examples/advanced-video-policy.json.
  • User templates — save the current policy by name (POST /api/settings/advanced-video/templates, max 20, persisted to config/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/settings returns 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

  • VideoJobPlan protocol v1 — the coordinator resolves the policy once and ships only the frozen plan; CloneOptionsForWorkerAsync strips the rule/profile catalog from worker options. Workers advertise AdvancedVideoProtocolVersion in 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 workerVideoPolicyResolver.ValidateResolvedPlan re-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 RoutingCapabilitySignature hash (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, force Simple, or pin a specific Profile (Advanced Video Policy section 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=0 disables) 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 stringtheme=dark|light, tab=stats|queue|workers, limit (1–30 queue rows), refresh; all values are clamped server-side. Responsive down to ~320 px tiles, honors prefers-reduced-motion.
  • Embed-safe headersContent-Security-Policy: frame-ancestors built from the configured origin allowlist, Cache-Control: no-store, Referrer-Policy: no-referrer.

Scoped iframe access

  • snk_embed_ tokensSettings → 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 allowlistAllowed 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 return 401 instead 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/apiKey query 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 consumesPOST /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 table1 with 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 baseline libx264, libx265, and libsvtav1 encoders (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 to examples/advanced-video-policy.json, and the generated C# fixture (quick-start-templates.json) must match the JS source of truth (--write regenerates).
  • 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 VideoAdvancedVideoPolicyTests (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 in VideoJobRouterScoreTests (protocol/encoder gating), EncodedOutputKeepDecisionTests (Always Keep vs Smaller Only), VideoFilterTests (additional-filter ordering), DeviceSlotSelectionTests, and EncoderOptionsJsonContractTests (advancedVideo round-trip).
  • Dashboards & authDashboardCompatibilityTests (Tdarr field-name/unit contract, zero-segment suppression, token-escaping tab links, AV1 codec preservation), embed-token scoping and origin-normalization cases in ApiKeyAuthTests, query-key path restrictions in AuthMiddlewareTests, and pending/active de-duplication in DbQueueTests.

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 inventory
  • Snacks/Services/ClusterService.cs, ClusterNodeJobService.cs, ClusterDiscoveryService.cs, Routing/VideoJobRouter.cs — plan-only dispatch, protocol/encoder scoring gates, receipt validation, capability-signature redispatch
  • Snacks/Services/VideoFilterBuilder.cs — additional-filter chain placement
  • Snacks/Models/EncoderOptions.cs, EncoderOptionsOverride.cs, WorkItem.cs, JobMetadata.cs, EncodeHistory.cs, FileAnalysisResult.cs, ClusterNode.cs — policy fields, source facts, wait reasons, history labels
  • Snacks/Migrations/20260808084045_AddAdvancedVideoHistoryLabels.cs — three nullable label columns on EncodeHistory (new)
  • Snacks/Data/EncodeHistoryRepository.cs, MediaFileRepository.cs — labeled-history projection, savings series, queued paging with exclusions
  • Snacks/Controllers/SettingsController.cs — validate/impact/measured/templates/video-encoders endpoints; save-time validation gate
  • Snacks/Controllers/ClusterAdminController.cs, LibraryController.cs — folder policy referential checks; pass-through Process Item
  • Snacks/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 UI
  • examples/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 semantics
  • Snacks/Views/Shared/_SecuritySettings.cshtml, Snacks/wwwroot/js/settings/panels/auth-panel.js, js/api.js — Iframe Access panel and embed URL management
  • Snacks/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 gates
  • scripts/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 troubleshooting
  • README.md, docs/BUILDING.md — feature overview, Homarr comparison table, release checklist
  • docs/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 via sync-version.mjs

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