Snacks v2.18.1
Automated Media Library Transcoder
A point release with one new capability and two classes of fixes. Undetermined language selection — the audio and subtitle keep lists gain an und (Undetermined) entry that claims tracks whose language nothing else could: a missing or empty tag, a literal und/mul/zxx, or an unrecognized code with no language-bearing title. Exclusion rules now apply to music/audio files (they previously only gated video), and a long-standing resolution-bucketing bug is fixed: the effective height was derived at 4:3 from the width, so a plain 1920×1080 file bucketed as 1440p and a "1080p" exclusion never matched widescreen content. Dispatcher hardening — an unreachable or slow Sonarr/Radarr instance could throw a timeout out of the pre-dispatch original-language lookup, unwind the local scheduler, and leak a device-slot reservation that wedged the queue until restart; timeouts now degrade to the configured keep lists, Arr failures are negative-cached, dispatch recovers in place, and a reaper releases any leaked reservation on its own.
Undetermined language selection (und)
The audio and subtitle Languages to Keep chip lists can now include und (Undetermined) — type und, undetermined, or unknown into the chip box. It keeps tracks whose language no other chip could claim, and like any chip its position sets the output order, so untagged tracks are no longer pinned last.
Claim-based matching
LanguageMatcher.KeepEntryIndex(new) — every track is claimed by exactly one keep-list entry, resolved in precedence order: canonical language match first (tag, or title inference — anund-tagged track titled "English SDH" belongs toen, never the undetermined chip), then raw-exact match for exotic tags not in the language table (private-useqaastill works), and only then the undetermined chip. A French track under keep list[en, und]is dropped, not swallowed byund.- One shared claim everywhere — filtering (
Matches), audio bucketing, output ordering (PreferenceIndex), and the "does this file need audio/subtitle work" gates all delegate to the same claim, so the keep/drop decision, the bucket a track lands in, and its output position can never disagree. Tracks the sentinel claims form one bucket (raw-tag grouping would have split a missing-tag track and amultrack into singletons and broken the sibling-drop gate). - Sentinel, not a language —
undis deliberately absent from the backend language table:ToTwoLetterkeeps returning null forund/mul/zxx, so title inference, sidecar naming, and metadata suppression continue treating those tags as "no known language". Bucket names normalize the typed aliases to the canonicalund. - Carried like any entry — per-folder / per-node overrides and
SNACKS_SET_AudioLanguagesToKeep=en,undenv overrides pass the sentinel (and its free-typed aliases) straight through; no special casing.
Surfaces
- Chip UI —
iso-languages.jscarries an Undetermined row (last on purpose, so it stays out of empty-query suggestions; typingund…surfaces it) plus anunknownalias, and both settings panels explain the sentinel in their help text. - Subtitles too — the subtitle keep list honors
undfor stream mapping and sidecar extraction, with chip position ordering undetermined subs like any other language.
Exclusion rules
Music/audio files now honor exclusions
AddMusicFileAsync and the analyze path now run library exclusion rules before probing — filename glob and size threshold apply (the resolution rung is video-only and is skipped via a null label). Manual force-adds still bypass the check, so Process Item re-adds aren't silently dropped, and the analyze modal reports the same outcome the real run would produce.
Widescreen resolution buckets fixed
ExclusionRules.ClassifyResolution derived effective height from the width at 4:3 (width * 3/4), which overshot every widescreen file: a plain 1920×1080 computed 1440 and bucketed as 1440p, so a "1080p" exclusion never matched it. The derivation is now 16:9 (width * 9/16) — 1920×1080 buckets as 1080p, and ultra-wide crops still bucket with their native vertical resolution (1920×800 → 1080p).
Arr timeouts and dispatcher hardening
The failure chain this release closes: a slow or unreachable Sonarr/Radarr made the pre-dispatch original-language lookup throw TaskCanceledException after the 15s HTTP timeout — which is an OperationCanceledException, so the blanket catch (OperationCanceledException) { throw; } rethrew it even though no caller had cancelled. The throw unwound the local scheduler mid-dispatch, leaking the item's device-slot reservation (capacity is 1 per device — the queue wedged until restart) and orphaning the item as in-memory Pending, which blocked its own DB re-hydration. And because dispatches are serialized, every subsequent dispatch re-paid the full timeout against the same dead endpoint.
Each link is fixed independently:
- Timeouts degrade, cancellation propagates — every original-language lookup path (
IntegrationService, the cluster pre-resolve, the library backfill, the dispatch finalisation) now rethrows only genuine caller cancellation (when (ct.IsCancellationRequested)); an HTTP timeout falls through to null and the configured keep lists, like any other lookup failure. - Arr failures are negative-cached — a failed Sonarr/Radarr catalogue fetch (timeout, connection failure, non-success status, malformed payload) is cached as an empty map for 3 minutes, so a struggling instance costs one timeout per window instead of one per dispatch. A settings save clears the cache, so credential/URL fixes apply immediately.
- Local dispatch recovers in place — the stretch between dequeue and job-task spawn is now exception-safe: a throw releases the slot reservation (idempotent if none was made yet), requeues the item — or unregisters it if it went terminal mid-dispatch — and defers it so a repeat throw retries at most once per scheduler-progress event (
RecoverFailedLocalDispatch, new). The cluster dispatcher gained the same contract: a pre-dispatch finalisation throw requeues the item instead of unwinding the remote loop. - The scheduler is keep-alive — no single-iteration failure (a DB blip in the window sync, a transient IO error) can kill the local scheduling loop anymore; it logs, paces, and retries instead of exiting until an external kick.
- Orphan-reservation reaper — a safety net beneath all of the above: any local ledger reservation with no backing active job that is older than a 2-minute grace window is released with the new
StaleReservationreason. It runs rate-limited (60s) at scheduler entry and whenever a slot reservation fails — exactly the moments a leaked reservation would manifest as "device full forever". Live encodes always have a backing entry (registered before the job task spawns), and remote reservations belong to ClusterService's reconciliation and are never touched.
Tests
AudioPlannerTests(new) —undchip bucketing: untagged tracks as a normal bucket, chip position driving output order, profile fan-out into the undetermined bucket, no double-bucketing of raw-matched tracks, and the keep-nothing safeguard still firing when the chip matches nothing.HasAudioWorkTests(new) /HasSubtitleWorkTests— the work gates agree with the planner: keeping an untagged track viaundis not work, dropping it without the chip is, and a chip-position change counts as reorder work; undetermined siblings group like the planner with preserve off.SubtitleMappingTests—undsubs kept/dropped/ordered by chip, sidecar extraction includesundtracks.LanguageMatcherTests— the claim-precedence matrix, env aliases, and keep-list normalization.OriginalLanguageLookupTests(new) — timeout-with-uncancelled-token returns null, genuine cancellation propagates, failures and non-success statuses negative-cache, successes cache the catalogue.DispatchRecoveryTests(new) — recovery releases the reservation and requeues, is a safe no-op before any reservation, never resurrects terminal items; the reaper releases unbacked reservations past the grace window and never touches backed, young, or remote ones; finalisation with a timing-out integration doesn't throw.ExclusionRulesTests(new) — glob/size/resolution rungs, the 16:9 bucket matrix, and the null-label (music) skip.EncoderOptionOverrideTests,EnvConfigOverridesTests— theundsentinel rides through folder/node overrides and env-var lists unchanged.
Files Changed
Undetermined language selection
Snacks/Services/LanguageMatcher.cs—Undeterminedsentinel,IsUndeterminedKeepEntry, claim-basedKeepEntryIndex;Matchesdelegates to itSnacks/Services/FfprobeService.cs— claim-based audio bucketing (one undetermined bucket, keep-list order),PreferenceIndexvia the shared claimSnacks/Services/TranscodingService.cs— audio/subtitle work gates and reorder detection use the shared claimSnacks/wwwroot/js/settings/iso-languages.js— Undetermined chip row andunknownaliasSnacks/Views/Shared/_AudioSettings.cshtml,_SubtitleSettings.cshtml— help text for theundchip
Exclusion rules
Snacks/Models/ExclusionRules.cs— 16:9 effective-height derivation inClassifyResolutionSnacks/Services/TranscodingService.cs— exclusion rungs inAddMusicFileAsyncand the music analyze path
Arr timeouts & dispatcher hardening
Snacks/Services/IntegrationService.cs— cancellation-aware OCE handling, 3-minute negative cache for failed Arr catalogue fetches (CacheArrFailure, new)Snacks/Services/TranscodingService.cs— keep-alive scheduler loop, exception-safe dispatch stretch,RecoverFailedLocalDispatch(new), orphan-reservation reaper (ReapOrphanedLocalReservations, new), cancellation-aware backfillSnacks/Services/ClusterService.cs— pre-dispatch finalisation throws requeue instead of unwinding; cancellation-aware pre-resolveSnacks/Services/Slots/ReleaseReason.cs—StaleReservationrelease reason (new)
Tests
Snacks.Tests/Audio/AudioPlannerTests.cs,HasAudioWorkTests.cs— undetermined bucketing and work-gate suites (new)Snacks.Tests/Subtitles/HasSubtitleWorkTests.cs,SubtitleMappingTests.cs—undsubtitle mapping and sidecar casesSnacks.Tests/Audio/LanguageMatcherTests.cs— claim-precedence and alias coverageSnacks.Tests/Integration/OriginalLanguageLookupTests.cs— Arr timeout/negative-cache contract (new)Snacks.Tests/Pipeline/DispatchRecoveryTests.cs— dispatch recovery and reaper suite (new)Snacks.Tests/Settings/ExclusionRulesTests.cs— exclusion rung and resolution-bucket matrix (new)Snacks.Tests/Overrides/EncoderOptionOverrideTests.cs,Settings/EnvConfigOverridesTests.cs— sentinel pass-through cases
Version bumps
Snacks/Snacks.csproj—<Version>2.18.1</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 viasync-version.mjs
Full documentation: README.md · /docs/index.html on a running instance