github derekshreds/Snacks v2.13.1
Snacks v2.13.1

3 hours ago

Snacks v2.13.1

Automated Media Library Transcoder

A point release covering three production fixes shipped on top of v2.13.0. AMD AMF encoders (h264_amf / hevc_amf / av1_amf) stopped accepting the libx265-style preset names (veryslowveryfast) somewhere along the FFmpeg evolution and now fail outright with Unable to parse preset option value — Snacks now maps the shared five-step UI preset onto AMF's own three-step speed / balanced / quality ladder and passes -quality instead of -preset for AMF encoders. libopus on Linux rejected the unstreamed -global_quality 25 form when the audio path inherited it from a VAAPI video-only flag, throwing Invalid argument; the four VAAPI sites that emit that flag now use the stream-specific -global_quality:v 25. Source-vanished work items (file deleted, folder unmounted, share dropped between scan-time enqueue and dispatch) are now dropped from the queue and the DB at the dispatch boundary in all three paths — local video, music, and cluster — instead of being uploaded to a worker or burning a device slot only to fail mid-encode. The release also adds a daily sweep for per-job FFmpeg logs so long-running installs don't accumulate a six-figure file count in {workdir}/logs/.


AMD AMF preset mapping

AMF encoders only accept speed / balanced / quality

  • TranscodingService.MapAmfPreset (new internal static) — maps the shared UI preset onto AMF's three-step ladder: veryslow / slowquality, mediumbalanced, fast / veryfastspeed. Anything unrecognised (including the empty string and ladder names AMF doesn't expose like ultrafast) lands on balanced, which is AMF's own default and matches the UI's default of medium. Comparison is case-insensitive.
  • TranscodingService.BuildFfmpegCommand preset selection — gained an isAmf = encoder.Contains("amf") branch in the encoder ladder. AMF now emits -quality <mapped> instead of -preset <ui-string>; libx265 / libsvtav1 / VAAPI paths are unchanged. Without this, h264_amf -preset veryslow returned Unable to parse preset option value, with no fallback — the encode just failed.

Tests

  • Snacks.Tests/Video/HardwareEncoderTests.cs — new theory MapAmfPreset_handles_known_and_unknown_inputs pins all five UI preset values, case-insensitive matching, the empty-string default, and the not-in-ladder ultrafastbalanced default.
  • Snacks.Tests/Pipeline/FullCommandScenarioTests.cs — the assembled-command oracle gained the same AMF branch so end-to-end scenarios that pick an AMF encoder verify -quality <preset> lands in the emitted command instead of the libx265-style -preset.

libopus on Linux: stream-specific -global_quality:v

-global_quality-global_quality:v everywhere VAAPI emits it

  • TranscodingService (four sites) — the VAAPI compression-flag block, the VAAPI branch of BuildVaapiOrSoftwareCompressionFlags, the VAAPI HDR-fallback path in BuildFfmpegHdrFallbackCommand, and RunTestEncodeAsync's calibration command now all emit -global_quality:v <q> instead of the unstreamed form. On Linux with libopus selected for audio, ffmpeg was applying the unstreamed -global_quality to the audio encoder too and failing with Invalid argument before the first frame was written. Stream-specifying the flag to video is the documented form and matches what every other rate-control flag in the same blocks already does.

Tests

  • Snacks.Tests/Video/RateControlAndScaleTests.cs — the VAAPI rate-control assertion now expects -global_quality:v 25.
  • Snacks.Tests/Pipeline/FullCommandScenarioTests.cs — both VAAPI scenarios (Scenario_VAAPI_*) updated to assert -global_quality:v 25 in the assembled command.

Source-vanished work items are dropped at every dispatch boundary

Drop missing sources before they reach the encoder or a remote worker

The source file can disappear in the window between scan-time enqueue and dispatch — the user deletes the folder, a downloader renames the file, a network share drops the path. Pre-fix, the work item stayed in the queue and either burned a local device slot on ConvertVideoAsync throwing Source file not found mid-encode, or was uploaded to a cluster worker which then failed the same way after wasting the transfer. The fix adds a File.Exists guard at every dispatch boundary plus an immediate DB + UI cleanup so the UI doesn't show a ghost row until the next page refresh.

  • TranscodingService.DropMissingWorkItemAsync (new) — removes the work item from _workItems and _workQueue, deletes the matching MediaFile row immediately via MediaFileRepository.RemoveByPathAsync (so the next scan doesn't have to re-process the deletion), and pushes a WorkItemRemoved SignalR event so the UI clears the stale row without waiting for a refresh. Callers must already have verified the item is not actively encoding.
  • TranscodingService.PruneMissingWorkItemsAsync (new) — sweeps every non-active work item and drops any whose source has disappeared. Active encodes (Processing / Uploading / Downloading) are left alone — yanking them mid-stream would race with ffmpeg or the cluster transfer, and their completion paths already surface the failure if the file really has gone.
  • AutoScanService.RunScanCycle — after PruneDeletedFilesAsync (DB-only), now also calls PruneMissingWorkItemsAsync so the in-memory queue mirrors the DB prune. Pre-fix, a queued or failed WorkItem whose source had vanished would linger in memory (and the queue UI) until the next process restart.
  • TranscodingService local video dispatcher (ProcessQueueAsync video branch)File.Exists(workItem.Path) guard runs before slot reservation. Missing sources are dropped via DropMissingWorkItemAsync instead of burning a device slot.
  • TranscodingService local music dispatcher — same guard for the music path. A missing music source is dropped before the slot ledger is consulted.
  • ClusterService.DispatchLoopAsync (master-side) — same guard before uploading to a remote worker. Pre-fix, a stale path could be uploaded to a worker only to have the worker fail with "source missing"; now the master drops the item locally and never initiates the transfer.
  • TranscodingService.AddFileAsync — pre-flight File.Exists rejects files that no longer exist before they're queued. Scan-time callers pass files they just enumerated so they exist by definition; the new guard catches the retry button and other ad-hoc entry points called minutes or hours after a file was last seen. The matching DB row is removed inline.
  • MediaFileRepository.RemoveByPathAsync (new) — single-row deletion by normalized path so the dispatch-time cleanup doesn't have to wait for the next scan's batched PruneDeletedFilesAsync to remove it.

Per-job FFmpeg log retention

{workdir}/logs/ no longer grows unbounded

Each encode writes one *.log file in {workdir}/logs/. There was no cleanup — a heavy library could land in the six-figure file count over a year, which slows directory listing and wastes disk. Serilog's rolling app log (snacks-*.log) already has its own 7-day / 10MB cap and is explicitly excluded from the new sweep.

  • LogRetentionService (new hosted service) — runs once 30 seconds after boot, then every 24 hours. Reads EncodingLogRetentionDays from settings.json directly (via JsonDocument, not the full EncoderOptions deserializer) so it stays decoupled from the legacy-audio migration path and can run before any other component has touched settings. Logs a single info line per sweep when files are deleted, a warning when the sweep itself throws.
  • LogRetentionService.Sweep (static, test-only seam) — deletes every *.log in the directory whose LastWriteTimeUtc is older than the retention window, except files matching Serilog's snacks-* rolling pattern. retentionDays <= 0 is a no-op (keep forever); a missing directory is a no-op; per-file errors (lock, permissions, vanished mid-enumeration) are swallowed so a transient failure doesn't abort the whole sweep. Takes a nowUtc parameter so tests can pin a deterministic clock.
  • EncoderOptions.EncodingLogRetentionDays (new, default 7) — included in the Clone so per-folder / per-node overrides round-trip correctly.
  • Snacks/Views/Shared/_AdvancedSettings.cshtml — new "Logs" panel with the retention-days input. Helper text calls out the 0 = keep forever escape hatch and clarifies that the app's rolling log isn't affected.
  • Snacks/wwwroot/js/settings/encoder-form.jsgetEncoderOptions / restoreEncoderOptions round-trip the new field with a clamp at 0 and a default of 7.
  • Snacks/Program.csLogRetentionService registered as a hosted service.

Tests

  • Snacks.Tests/Settings/LogRetentionTests.cs (new file) — pins the sweep behaviour: per-job logs older than retention are deleted, Serilog's rolling app log is preserved, non-positive retention is a no-op, a missing directory is a no-op, and the sweep only touches *.log files (a stray .txt in the same directory is left alone).

Files Changed

AMD AMF preset mapping

  • Snacks/Services/TranscodingService.csMapAmfPreset (new); BuildFfmpegCommand AMF branch in the preset ladder
  • Snacks.Tests/Video/HardwareEncoderTests.csMapAmfPreset_handles_known_and_unknown_inputs theory
  • Snacks.Tests/Pipeline/FullCommandScenarioTests.cs — AMF branch in the assembled-command oracle

libopus on Linux fix

  • Snacks/Services/TranscodingService.cs — four VAAPI -global_quality-global_quality:v sites (compression flags, VAAPI/software helper, HDR fallback, calibration encode)
  • Snacks.Tests/Video/RateControlAndScaleTests.cs — VAAPI rate-control assertion
  • Snacks.Tests/Pipeline/FullCommandScenarioTests.cs — VAAPI scenario assertions

Source-vanished work item handling

  • Snacks/Services/TranscodingService.csDropMissingWorkItemAsync / PruneMissingWorkItemsAsync (new); File.Exists guard in local video dispatcher, local music dispatcher, and AddFileAsync
  • Snacks/Services/ClusterService.csFile.Exists guard in DispatchLoopAsync before remote upload
  • Snacks/Services/AutoScanService.csPruneMissingWorkItemsAsync call after PruneDeletedFilesAsync
  • Snacks/Data/MediaFileRepository.csRemoveByPathAsync (new)

Per-job log retention

  • Snacks/Services/LogRetentionService.cs — new hosted service + static Sweep
  • Snacks/Models/EncoderOptions.csEncodingLogRetentionDays (new field, default 7); Clone updated
  • Snacks/Views/Shared/_AdvancedSettings.cshtml — Logs panel + retention input
  • Snacks/wwwroot/js/settings/encoder-form.js — round-trip the new field
  • Snacks/Program.cs — register the hosted service
  • Snacks.Tests/Settings/LogRetentionTests.cs — new test file pinning sweep behaviour

Build script confirmation

  • build-and-export.bat — adds an explicit "Type YES to continue" prompt before tagging and pushing both :latest and the pinned version to Docker Hub, so a stray double-click can't ship an unintended build

Version bumps

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

Full documentation: README.md

Don't miss a new Snacks release

NewReleases is sending notifications on new releases.