Snacks v2.6.0
Automated Video Library Encoder
A minor release that adds directory dry-run analysis — a per-file Queue/Mux/Skip preview the user can review before committing to a real process-directory run. The same release also closes a long-standing wasted-work hole: HEVC files just above the bitrate ceiling that would have produced a near-identical -c:v copy output (no audio/sub work, no filters) are now skipped at scan time instead of running ffmpeg for nothing.
New Features
Directory Analyze (Dry Run)
- New "Analyze (Dry Run)" action in the library modal -- alongside the existing "Process Folder" / "Process Folder + Subfolders" actions, the directory drawer now offers a third option that opens a per-file preview before anything is queued or written to the database. Useful on large folders where you want to know whether a different bitrate / codec / mux-mode setting would actually do anything before committing to it.
- Per-file decisions mirror the real run -- each row shows the predicted action (
Encode,Shrink,Copy,Mux,Skip,Excluded,Done,Failed,Cancelled,Error), source codec/bitrate/resolution/size, and a human-readable reason. The decision logic inAnalyzeFileAsyncmirrorsAddFileAsync's skip ladder rung-for-rung — same exclusion rules, same DB-status short-circuit (split into distinctAlreadyCompleted/AlreadyFailed/AlreadyCancelleddecisions for filtering), same 4K-skip and codec/bitrate ceilings, same VAAPI-can't-compress-H.264 gate, same Hybrid-mux bypass, same finalCalculateBitratesladder for Queue/Shrink/Copy. - Borderline flag for files near the skip ceiling -- the analyze pass uses the file-level bitrate (filesize × 8 / duration) and skips the 15-second video-only remeasurement that the real run does. Files within +30% of the applicable ceiling are flagged with a warning icon and hover tooltip ("real run remeasures video-only bitrate and may decide differently"), so the user knows the prediction is at the edge of its accuracy.
- DB-cached probe reuse -- when the file's size and mtime match its
MediaFilesrow, the analyzer reads codec / bitrate / resolution / duration /IsHevcstraight from the DB instead of re-running ffprobe. Same change-detection thresholds asAddFileAsync(mtime tick match + ≤10% size delta). Audio/subtitle stream summaries come from the cachedAudioStreams/SubtitleStreamsJSON blobs the v2.5.0 migration added, so the no-op gate still has the data it needs to answer "would the audio/sub mapping actually change anything?" without probing. - Summary cards + filter tabs + name filter -- six summary cards across the top (Total Files / Will Encode / Mux Pass / Copy / Will Skip / Already Done) with rough byte totals, a tab strip below for filtering the table to one bucket, and a free-text filename filter. Footer "Proceed with Conversion" button submits the directory to the real
process-directoryendpoint with the same options the analyze used (button label includes the will-process count, disabled when there's nothing to act on). - Cancellable -- closing the modal mid-analysis (X button, footer Close, Escape, backdrop click) aborts the in-flight
analyze-directoryrequest viaAbortController; the server honors it throughHttpContext.RequestAbortedand returns499. Reopening before the previous run finishes also aborts it, with an identity check on theAbortControllerso a late-arriving response from a since-aborted request can't paint over the new one.
Bug Fixes & Reliability
Skip-Ladder: No-Op Encode Detection
- Don't run ffmpeg to produce a near-identical output -- when an HEVC source sits just above the skip ceiling but under
TargetBitrate + 700,CalculateBitrates's final override would flip the encode tovideoCopy = true. If the audio/subtitle pipeline also has nothing to change (no codec conversion, no track filtering, no language fixups) and no filter is active (no crop / downscale / HDR tonemap), the resulting ffmpeg run is a near-no-op — same video stream copied, same audio/sub mapping, slightly different container. We were burning a transcode pass on those files.AddFileAsyncnow runs aWouldEncodeBeNoOpcheck at the top of the skip ladder and marks the fileSkippedin the DB before ever invoking the encoder. - Same gate applied to the scan-phase bitrate filter --
MeetsBitrateTarget(MediaFile, options)is the pure-function variant used during library scans (no probe required, reads only DB fields). It now mirrors the same no-op logic: HEVC underTargetBitrate + 700with no filter / audio / sub work counts as "meets target" and is filtered out of the scan results. Uses the cachedAudioStreams/SubtitleStreamsJSON; HDR is treated as inactive at scan time (the DB doesn't carry HDR metadata) so the worst case is the file falls through toAddFileAsync's probe-aware path, which catches the HDR case correctly. - Helper extraction -- the active-filter and would-downscale checks (
HasActiveFilter,WouldDownscale) were factored into shared helpers so the no-op gate, the analyze preview, and the scan filter all use the same logic. This also closes a small bug where the scan filter previously didn't account for downscale/tonemap filters at all.
Files Changed
Directory analyze (dry run)
Snacks/Controllers/LibraryController.cs-- newPOST /api/library/analyze-directoryendpoint, returns per-fileFileAnalysisResult[]without writing to the DB or queueing work; honorsHttpContext.RequestAborted(returns499when the client aborts)Snacks/Models/FileAnalysisResult.cs(new) -- single-file dry-run result: path, source metadata, decision label, reason text, predicted encode-target kbps, borderline flagSnacks/Services/TranscodingService.cs-- newAnalyzeDirectoryAsync/AnalyzeFileAsyncmirroringAddFileAsync's skip ladder; DB-cached probe reuse with the same mtime + size-delta thresholdsSnacks/Views/Home/Index.cshtml-- newanalyzeResultsModalmarkup (summary cards, filter tabs, name filter, results table, Proceed footer button)Snacks/wwwroot/js/library/analyze-modal.js(new) --AnalyzeModalclass: open/fetch/render/proceed flow, decision-to-badge mapping, AbortController-based cancellation, late-response identity guardSnacks/wwwroot/js/library/library-browser.js-- new "Analyze (Dry Run)" entry in the directory drawer;analyzeCurrentDirectoryhook;onAnalyzeRequestedcallback propSnacks/wwwroot/js/api.js--libraryApi.analyzeDirectory(dirPath, recursive, options, signal);postJsongains an optionalAbortSignalparameterSnacks/wwwroot/js/main.js-- composition root wiresAnalyzeModaltoLibraryBrowservia the newonAnalyzeRequestedcallback
Skip-ladder no-op detection
Snacks/Services/TranscodingService.cs-- newWouldEncodeBeNoOprung inAddFileAsync's skip ladder;MeetsBitrateTarget(MediaFile, options)extended with the same no-op check;HasActiveFilter/WouldDownscaleextracted as shared helpers used by the no-op gate, the analyze preview, and the scan filter
Version Bumps
Snacks/Controllers/HomeController.csSnacks/Services/ClusterDiscoveryService.cs-- protocol version bump to 2.6.0Snacks/Views/Shared/_Layout.cshtmlREADME.mdbuild-and-export.batelectron-app/package.json/package-lock.json
Full documentation: README.md