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 (veryslow … veryfast) 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/slow→quality,medium→balanced,fast/veryfast→speed. Anything unrecognised (including the empty string and ladder names AMF doesn't expose likeultrafast) lands onbalanced, which is AMF's own default and matches the UI's default ofmedium. Comparison is case-insensitive.TranscodingService.BuildFfmpegCommandpreset selection — gained anisAmf = 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 veryslowreturned Unable to parse preset option value, with no fallback — the encode just failed.
Tests
Snacks.Tests/Video/HardwareEncoderTests.cs— new theoryMapAmfPreset_handles_known_and_unknown_inputspins all five UI preset values, case-insensitive matching, the empty-string default, and the not-in-ladderultrafast→balanceddefault.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 ofBuildVaapiOrSoftwareCompressionFlags, the VAAPI HDR-fallback path inBuildFfmpegHdrFallbackCommand, andRunTestEncodeAsync'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_qualityto 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 25in 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_workItemsand_workQueue, deletes the matchingMediaFilerow immediately viaMediaFileRepository.RemoveByPathAsync(so the next scan doesn't have to re-process the deletion), and pushes aWorkItemRemovedSignalR 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— afterPruneDeletedFilesAsync(DB-only), now also callsPruneMissingWorkItemsAsyncso the in-memory queue mirrors the DB prune. Pre-fix, a queued or failedWorkItemwhose source had vanished would linger in memory (and the queue UI) until the next process restart.TranscodingServicelocal video dispatcher (ProcessQueueAsyncvideo branch) —File.Exists(workItem.Path)guard runs before slot reservation. Missing sources are dropped viaDropMissingWorkItemAsyncinstead of burning a device slot.TranscodingServicelocal 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-flightFile.Existsrejects 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 batchedPruneDeletedFilesAsyncto 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. ReadsEncodingLogRetentionDaysfromsettings.jsondirectly (viaJsonDocument, not the fullEncoderOptionsdeserializer) 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*.login the directory whoseLastWriteTimeUtcis older than the retention window, except files matching Serilog'ssnacks-*rolling pattern.retentionDays <= 0is 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 anowUtcparameter so tests can pin a deterministic clock.EncoderOptions.EncodingLogRetentionDays(new, default7) — included in theCloneso 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 the0 = keep foreverescape hatch and clarifies that the app's rolling log isn't affected.Snacks/wwwroot/js/settings/encoder-form.js—getEncoderOptions/restoreEncoderOptionsround-trip the new field with a clamp at0and a default of7.Snacks/Program.cs—LogRetentionServiceregistered 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*.logfiles (a stray.txtin the same directory is left alone).
Files Changed
AMD AMF preset mapping
Snacks/Services/TranscodingService.cs—MapAmfPreset(new);BuildFfmpegCommandAMF branch in the preset ladderSnacks.Tests/Video/HardwareEncoderTests.cs—MapAmfPreset_handles_known_and_unknown_inputstheorySnacks.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:vsites (compression flags, VAAPI/software helper, HDR fallback, calibration encode)Snacks.Tests/Video/RateControlAndScaleTests.cs— VAAPI rate-control assertionSnacks.Tests/Pipeline/FullCommandScenarioTests.cs— VAAPI scenario assertions
Source-vanished work item handling
Snacks/Services/TranscodingService.cs—DropMissingWorkItemAsync/PruneMissingWorkItemsAsync(new);File.Existsguard in local video dispatcher, local music dispatcher, andAddFileAsyncSnacks/Services/ClusterService.cs—File.Existsguard inDispatchLoopAsyncbefore remote uploadSnacks/Services/AutoScanService.cs—PruneMissingWorkItemsAsynccall afterPruneDeletedFilesAsyncSnacks/Data/MediaFileRepository.cs—RemoveByPathAsync(new)
Per-job log retention
Snacks/Services/LogRetentionService.cs— new hosted service + staticSweepSnacks/Models/EncoderOptions.cs—EncodingLogRetentionDays(new field, default 7);CloneupdatedSnacks/Views/Shared/_AdvancedSettings.cshtml— Logs panel + retention inputSnacks/wwwroot/js/settings/encoder-form.js— round-trip the new fieldSnacks/Program.cs— register the hosted serviceSnacks.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:latestand 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 versionSnacks/Services/ClusterDiscoveryService.cs—ClusterVersionprotocol bump to 2.13.1Snacks/Views/Shared/_Layout.cshtml— footer versionREADME.md— version badgebuild-and-export.bat— Docker tag versionelectron-app/package.json/package-lock.json— version
Full documentation: README.md