github derekshreds/Snacks v2.9.0
Snacks v2.9.0

3 hours ago

Snacks v2.9.0

Automated Video Library Encoder

A minor release built around per-node transcode scheduling — every cluster member (master + workers) gets a weekly window of "allowed to accept new jobs" hours, configured from a new Scheduling tab in Settings and gated by a pure ScheduleEvaluator on the master's local clock. Off-schedule nodes drop out of the dispatch candidate pool, get an "Off-schedule" badge on the dashboard, and resume automatically when their window reopens — in-flight jobs run to completion regardless. A node warm-up grace period (10s) holds new dispatches off freshly-joined workers so their HTTP endpoints are up before the master pings them, ending the "first job after join sits in Processing for five minutes until the watchdog rescues it" pattern. Two encoder fixes ride along: Intel iGPUs without an HEVC low-power entrypoint (pre-Tiger Lake, Proxmox LXC without GuC firmware) now correctly fall through from LP → normal mode → software instead of returning a bogus "calibration complete" verdict that ffmpeg then refused to honor; and the post-encode notification path no longer throws FileNotFoundException ("Finalize failed: Could not find file …") when DeleteOriginalFile=true and the source/output extensions differ (.mp4 → .mkv).


Per-node scheduling

ScheduleEvaluator — pure, testable gate

  • Snacks/Services/ScheduleEvaluator.cs (new) — static IsWithinAny(windows, localNow) / IsWithin(window, localNow) decide whether "now" falls inside any configured window. No DI, no clock — takes a DateTime so tests drive it with synthetic values. Rules: empty/null window list = always allowed (pre-feature behavior); empty Days list = never matches; Start == End = full 24 hours on each listed day; Start < End = same-day [Start, End) window with exclusive end so adjacent windows can butt up without a one-minute overlap; Start > End = wraps past midnight, so a 22:00–06:00 window opens on the listed day and closes on the next day.
  • Snacks.Tests/Cluster/ScheduleEvaluatorTests.cs (new) — 17 tests pinning each branch: null/empty schedules, same-day inside/at-start/at-end/wrong-day, wrap-around (Sunday 22:00 → Monday 06:00, the Monday morning leg, wrong day, between windows), 24-hour 00:00 → 00:00, malformed HH:mm strings (defensive — a bad config row shouldn't silently open or close the gate), and the multi-window OR composition.

NodeSettings.Schedule + ClusterNode.OffSchedule

  • NodeSettings.Schedule (new field) — optional List<ScheduleWindow>? per node. ScheduleWindow is { Days: List<DayOfWeek>, Start: "HH:mm", End: "HH:mm" }, master local time. Null/empty = always available. Persisted in the existing nodeSettings.json alongside Only4K / Exclude4K / EncodingOverrides / DeviceSettings.
  • ClusterNode.OffSchedule (new transient flag) — set by the master on every dispatch tick and on settings save, broadcast over the existing WorkerUpdated SignalR channel so the dashboard renders an "Off-schedule" badge without needing its own clock or schedule logic. Always false when no schedule is configured. Master itself is not in _nodes, so ClusterController.GetStatus and ClusterAdminController.GetClusterStatus stamp it explicitly on the self-card payload.

Dispatch gate

  • ClusterService.IsNodeWithinSchedule / IsNodeWithinScheduleById — wired into all three candidate-pool filters (hasAvailableNodes early-exit, per-iteration availableNow, SelectBestSlotForJob). Off-schedule nodes drop out of dispatch; in-flight jobs on a now-off-schedule node are untouched.
  • RefreshOffScheduleFlags (new master-only) — recomputes every node's OffSchedule, fires WorkerUpdated for any node whose value flipped, and tracks the master's own off→on schedule transition. Called from the 2-second dispatch timer and from the SaveNodeSettings hook (so a schedule edit reflects on the dashboard within ms instead of waiting up to 2s for the next tick).
  • Local encoding gate (TranscodingService.SetLocalScheduleGate)ClusterService.StartAsync injects () => IsNodeWithinScheduleById(_config.NodeId) into the local encoder; ProcessQueueAsync checks the gate alongside the existing _localEncodingPaused check and exits cleanly when the master's window is closed (workers in their own open windows still pick up dispatched items). WakeFromSchedule re-triggers ProcessQueueAsync on the off→on edge so pending items resume without waiting for an external wake (queue add, settings change, etc.).

Settings UI: the Scheduling tab

  • Snacks/Views/Shared/_SchedulingSettings.cshtml (new) — collapsible section per node (master first, then workers). Each section holds a list of "allowed-to-transcode" rows, each row = 7 day-of-week chips + start/end <input type="time">. Master-clock banner at the top so a UTC-defaulted server doesn't trip up users configuring "evening" windows expecting their local wall clock.
  • Snacks/wwwroot/js/settings/panels/scheduling-panel.js (new) — render / mutate / save logic. Every chip click, time edit, and row add/remove marks the node dirty and schedules a debounced POST of the full NodeSettings payload (read from a per-node cache so co-existing fields like Only4K / EncodingOverrides aren't clobbered). Master-clock display: fetch once on panel open, advance from the browser's monotonic clock — master's reported time is parsed as if it were UTC and formatted as UTC, so the displayed string reflects the master's wall clock no matter what timezone the browser is in. Ticker is stopped when the settings modal closes.
  • GET /api/cluster-admin/master-time (new) — returns { timeZoneId, displayName, utcOffsetMinutes, localTimeIso, isUtc } so the panel can show a live clock and surface a "UTC" warning badge when the server is running with no real timezone configured.
  • POST /api/cluster-admin/node-settings validation — rejects schedules with empty Days lists, out-of-range DayOfWeek values, or HH:mm strings that don't parse to 0–23 : 0–59. Bad config doesn't make it to disk where it would silently never match.
  • Dashboard "Off-schedule" badge — purple-themed warning chip rendered on both self-card and worker-card paths in cluster-dashboard.js, hover for "New jobs will not be dispatched until the next allowed window."

Weekday chip styling

  • scheduling-day-chips — purple-fill active state matching the existing site purple palette (var(--bs-purple, #6f42c1)). Used by the new scheduling rows and updated to the same purple tone wherever the "selected weekday" pattern surfaces.

Node warm-up on cluster join

10-second grace before first dispatch

  • ClusterService.IsNodeWarmedUpDateTime.UtcNow - node.RegisteredAt >= 10s. Wired into the same three candidate-pool filters as the schedule gate. Without it, the first dispatch to a freshly-registered node could hang on the heartbeat pre-check inside DispatchToNodeAsync long enough to leave items orphaned in Processing until the stuck-item watchdog rescued them five minutes later.
  • ClusterDiscoveryService stamps RegisteredAt master-side — on first join the master's wall clock wins; on re-registration the existing RegisteredAt is preserved. The payload's serialized value is ignored — only the master's clock matters for the warm-up gate.

Encoder fixes

Intel iGPU low-power → normal-mode fallback

  • TranscodingService calibration loop — when every pass in low-power mode failed before producing a measurable bitrate (e.g. iGPUs with no HEVC LP entrypoint — common on pre-Tiger Lake Intel and on Proxmox LXC where GuC firmware isn't loaded), the loop previously logged "Calibration complete after N passes. Using QP X" and returned a bogus QP that ffmpeg then refused to honor on the real encode. Now lowPower failure falls through to normal mode (continue); a normal-mode failure returns (-1, false) to signal "VAAPI cannot encode this file" so the retry chain skips conservative-HW (a no-op for VAAPI) and goes straight to software.

Finalize failed: Could not find file …

  • HandleEncodeCompletion notification path — the post-encode notification was reading new FileInfo(workItem.Path).Length after HandleOutputPlacement had already deleted workItem.Path (which happens when DeleteOriginalFile=true and the source/output extensions differ, e.g. .mp4 → .mkv). The FileNotFoundException surfaced as "Finalize failed: Could not find file …" on a perfectly successful encode. Now passes workItem.OutputSize instead, which is set on the completion path and survives the source-file delete.

Files Changed

Per-node scheduling

  • Snacks/Services/ScheduleEvaluator.cs (new) — pure window evaluation
  • Snacks/Models/NodeSettings.csSchedule: List<ScheduleWindow>? + new ScheduleWindow record
  • Snacks/Models/ClusterNode.csOffSchedule transient flag
  • Snacks/Services/ClusterService.cs — dispatch-pool filters; RefreshOffScheduleFlags; local-encoding gate wiring; _masterOffSchedule edge tracking
  • Snacks/Services/TranscodingService.csSetLocalScheduleGate; WakeFromSchedule; ProcessQueueAsync honors the gate
  • Snacks/Controllers/ClusterAdminController.cs — schedule validation in SaveNodeSettings; GET /master-time; selfOffSchedule on the cluster status
  • Snacks/Controllers/ClusterController.cs — stamps OffSchedule on the self-card in GetStatus
  • Snacks/Views/Shared/_SchedulingSettings.cshtml (new) — Scheduling tab partial
  • Snacks/Views/Shared/_AppModals.cshtml — Scheduling tab nav + pane
  • Snacks/wwwroot/js/settings/panels/scheduling-panel.js (new) — render/save/clock logic
  • Snacks/wwwroot/js/main.js — wires initSchedulingPanel / loadSchedulingPanel
  • Snacks/wwwroot/js/api.jsclusterApi.getMasterTime
  • Snacks/wwwroot/js/cluster/cluster-dashboard.js — "Off-schedule" badge on self + worker cards
  • Snacks/wwwroot/css/site.css.scheduling-day-chips, .scheduling-node-section, .scheduling-window-row; purple weekday chip palette
  • Snacks.Tests/Cluster/ScheduleEvaluatorTests.cs (new) — 17 tests pinning the evaluation rules

Node warm-up

  • Snacks/Services/ClusterService.csIsNodeWarmedUp + NodeWarmupGrace = 10s filter
  • Snacks/Services/ClusterDiscoveryService.cs — master stamps RegisteredAt on first join

Encoder fixes

  • Snacks/Services/TranscodingService.cs — LP-mode → normal-mode → software fallback for iGPUs without HEVC LP entrypoint
  • Snacks/Services/TranscodingService.cs — notification reads workItem.OutputSize instead of FileInfo(workItem.Path).Length

Version bumps

  • Snacks/Controllers/HomeController.cs — health endpoint version
  • Snacks/Services/ClusterDiscoveryService.csClusterVersion protocol bump to 2.9.0
  • Snacks/Views/Shared/_Layout.cshtml — footer version
  • README.md — badge + footer; new Discord chat button
  • build-and-export.bat — Docker tag version
  • electron-app/package.json / package-lock.json

Full documentation: README.md

Don't miss a new Snacks release

NewReleases is sending notifications on new releases.