github derekshreds/Snacks v2.20.0
Snacks v2.20.0

3 hours ago

Snacks v2.20.0

Automated Media Library Transcoder

This release keeps your Arr apps in step with what Snacks writes. Lidarr joins Sonarr and Radarr as a connection, and when an encode replaces its original, Snacks now asks the owning app to rescan just that series, movie, or album folder so the new file is imported instead of showing as missing. Music placement gets the same in-place replacement model video has had — including on cluster encodes, which previously ran music through the video placement settings. Plex and Jellyfin rescans only consider libraries of the file's kind, Jellyfin requests use the authorization header current Jellyfin accepts, and the Sonarr/Radarr folder-name fallback no longer stumbles on directory names containing a dot.


Lidarr connection

Settings → Connections → Lidarr: enable, base URL, API key, and a Test button that hits /api/v1/system/status (Lidarr speaks API v1; Sonarr and Radarr stay on v3). IntegrationConfig.Lidarr is an ArrIntegration like the other two, so the SNACKS_INTEG_Lidarr__BaseUrl / __ApiKey / __Enabled environment overrides lock it the same way. Lidarr's only job is the master's post-replacement album rescan, so the cluster's GET /api/cluster/integrations handout to workers omits it.


Sonarr, Radarr & Lidarr rescans after a replacement

The Arr apps track individual files. An episode, movie, or track whose original Snacks replaced — Movie.mp4 → Movie.mkv, or a smaller file under the same name — reads as missing or stale until the app rescans its folder. With the connection enabled, Snacks requests that rescan itself (IntegrationService.QueueArrRescan, new).

App What is rescanned Command
Sonarr The one series that owns the replaced episode RescanSeries with seriesId → POST /api/v3/command
Radarr The one movie that owns the replaced file RescanMovie with movieId → POST /api/v3/command
Lidarr The album folder of the replaced track RescanFolders with that folder and addNewArtists: false → POST /api/v1/command

When it fires

Only when a kept output replaced its original: Replace Original Files for video, Delete original file after success for music (ReplacesOriginal, new — music reads its own toggle, never video's). Keep-both encodes and no-savings outcomes send nothing. All three completion paths are wired — local video, local music, and the master's post-download HandleRemoteCompletion — and only the master ever contacts an Arr; workers never do.

Finding the owner

  • Music maps the track's path onto Lidarr's root folders (GET /api/v1/rootfolder, cached ten minutes) with the same trailing-segment anchor matching the Plex/Jellyfin rescans use, then takes the parent directory in Lidarr's own separator style — a D:\Media\Music root yields D:\Media\Music\Artist\Album.
  • Video uses the Sonarr series list or Radarr movie list with each entry's folder as a root. MediaTypeDetector.Classify decides who is asked first (SxxEyy naming → Sonarr, otherwise Radarr); the other is tried when the first has no match, so a pilot without episode numbering still finds its series. When two entries share a folder name (/tv/Show (2020) and /anime/Show (2020)), the one whose deeper path lines up with Snacks' path wins.
  • No match, no scan. Unlike Plex/Jellyfin there is no library-wide fallback — an unmatched path isn't in the Arr anyway, and a full scan per encode would be heavy on large libraries. The log records "No Sonarr series or Radarr movie matched '…'; skipping rescan".

One command per season or album

Requests are debounced per {app}|{baseUrl}|{series, movie, or folder} and sent once the scope has had no replaced file for 30 seconds (RescanQuietPeriod), so a batch of episodes or a whole album becomes one command. The Arrs fold a duplicate command into one that is already queued or running and answer with that command's id; if Snacks gets its previous id back as started, the running scan may have listed the folder before this file landed, so the request is re-armed for a fresh scan once the scope is quiet again (bounded at five re-arms). A new id reported as started is just the Arr picking the job up quickly and is left alone; a response without an id is never treated as merged.

Radarr keeps the movie monitored

With Unmonitor Deleted Movies on, Radarr unmonitors a movie the moment the rescan finds its old file missing — even though the same scan imports the replacement. Snacks reads the movie's monitored flag before posting the command, polls GET /api/v3/command/{id} every two seconds (up to five minutes; three consecutive read failures end the wait, scattered ones do not) and, if the scan flipped a monitored movie off, switches it back on through PUT /api/v3/movie/editor with only movieIds and monitored in the body. Radarr's own scheduled rescans get no such correction. Sonarr needs none: it defers that decision until after the scan and drops any episode that was re-imported meanwhile.

One catalogue, two consumers

The Sonarr/Radarr series and movie lists now live in a single cache (GetArrCatalogAsync / ArrCatalogEntry, replacing _arrLangCache) that feeds both the original-language lookup and the rescans, keyed by provider, URL, and key with the ten-minute TTL. A failed fetch is still negative-cached for three minutes and now suppresses rescans as well as language lookups; saving the connection settings clears it.


Music placement: replace in place

ConvertMusicAsync used to write Track [snacks].m4a into the music output directory (or beside the source) and, with Delete original file after success on, delete the source and leave the tagged file where it was. It now hands the validated output to the same placement step video uses, and HandleMusicOutputPlacement (new) applies the music settings:

Music settings Result
Delete original file after success on The output drops its [snacks] tag and replaces the source in the source's folder under the source's name via ReplaceOriginalAsync (park the original as .snacks.bak, move the encode in, delete the backup, restore on failure). Track.mp3 → Track.mp3 overwrites the source path. A music output directory only holds the encode until then.
Off, output directory set The tagged file is kept in the music output directory; the original stays.
Off, no output directory The tagged file is kept beside the untouched original.

Either way an output staged elsewhere — a cluster download lands in the master's encode or video output directory — is moved into place. The video Replace Original Files toggle and video output directory never decide where an audio file lands or whether its source survives.

Cluster fix: the master's HandleRemoteCompletion always ran the video placement path, so a cluster music encode obeyed the video replace toggle and the video output directory. Workers now skip placement entirely (ConvertMusicAsync(…, skipPlacement: true) from ConvertMusicForRemoteAsync), keeping the tagged output where the *[snacks]* glob finds it, and the master places it after download under the music settings.

Upgrade notes

  • If you run music with Delete original file after success on, replaced tracks now appear under their original name in the album folder, not as [snacks]-tagged files in the music output directory. This is what Lidarr, Plex, and Jellyfin expect, but it is a change from 2.19.
  • A music placement failure now fails the job (as video does) instead of being logged while the job completes.
  • A replaced track is an ordinary library file on the next scan. The default Skip files already in target codec rule (with its bitrate tolerance) keeps it from being queued again; with that rule off, a replaced track is eligible for another pass.

Plex & Jellyfin rescans know what kind of file finished

TriggerRescansAsync takes the item's MediaKind, and LibraryRoot now carries the library's content type (Plex section type, Jellyfin CollectionType). RootsForKind (new) narrows the roots a file can live in:

  • Music maps only onto music libraries (Plex artist, Jellyfin music). A movie library whose /data/media root happens to share a media segment with the track's path no longer wins and refreshes a folder that isn't there.
  • Video maps onto everything else; a mixed Jellyfin library that reports no CollectionType counts as video.
  • A server that lists libraries but none of that kind is skipped ("Plex: no music library; skipping rescan for '…'") instead of fully refreshed per file — before, every music track asked a video-only Plex to rescan every library.
  • A failed library lookup, or a library of the right kind that doesn't line up with the path, still falls back to the full refresh as before.

Plex scoped refresh keeps Plex's separators. The scoped refresh?path= directory was computed with Path.GetDirectoryName, which applies the Snacks host's rules: on Windows it rewrote /data/movies/X to \data\movies\X (a Linux Plex answers 200 and scans a folder that doesn't exist), and on Linux it found no parent in a D:\… path at all and refreshed the whole section. ParentDirectory (new) trims in the remote server's own style; GetDirectorySafe is gone.

Jellyfin authentication. Every Jellyfin request (/System/Info, /Library/VirtualFolders, /Library/Media/Updated, /Library/Refresh) now sends Authorization: MediaBrowser Token="…". Current Jellyfin ships with legacy authorization disabled and answers the old X-Emby-Token header with 401; the Authorization form works on current and older releases alike (AddJellyfinAuth, new).


Original-language folder fallback

The Sonarr/Radarr fallback that matches a file's folder names against the catalogue when the Arr's path doesn't prefix the local path applied GetFileNameWithoutExtension to every segment, so Mr. Robot (2015) was compared as Mr and never matched. Only the file name loses its extension now (the flat Movies/Title (year).mkv layout still works); directory names are compared whole.


Documentation

  • /docs/index.html gains Sonarr, Radarr & Lidarr: keep replaced files in sync (#arr-sync, linked from the sidebar) — what each app rescans, when it fires, the batching window, path matching across mounts, the quality-profile cutoff caveat, the Radarr monitoring guard, and the Lidarr tag requirement. The integrations table describes the kind-aware Plex/Jellyfin scoping (and Jellyfin's 60-second change delay) and the Lidarr row; the output-placement table gains the music replacement row; the API reference lists POST /api/integrations/test/lidarr.
  • The Sonarr, Radarr, and Lidarr settings panels explain the rescan; the music Delete original file after success toggle explains in-place replacement.
  • README lists scoped Sonarr/Radarr/Lidarr rescans.
  • Three passages that were published at snacksvideo.com/docs but never committed now ship in the in-app copy too: Tracks with undetermined language (the und keep-list entry from 2.18.1), the fuller Exclusions guidance (video and music coverage, the resolution bucketing rule, Analyze and force-add behavior), and the Luna credential-storage notes with the luna-connection.json row in the config table.

Tests

  • ArrRescanTests (new) — Lidarr targets the album folder under its root (trailing-slash and D:\… roots keep their own separators), completions in one folder coalesce into a single scan, an unmatched root sends nothing, music never reaches Sonarr/Radarr, and a disabled Lidarr makes no requests; an episode rescans only its series and a movie only its movie, a whole series across seasons is one command, an episode without SxxEyy naming falls back to Sonarr after Radarr, the deepest folder match wins between same-named series, unmatched video sends nothing; a request merged into a running scan is re-armed, a fresh started command is not, an id-less response is never treated as merged; the Lidarr connection test uses v1; Radarr re-monitors a movie the rescan unmonitored (with only movieIds and monitored in the PUT), leaves monitoring alone when it was already off or unreadable, and scattered status-read failures don't end the wait early.
  • MediaServerRescanTests (new) — Plex scoped paths keep Plex's separators on /data/… and D:\… roots, a Plex with no music library is not refreshed for music, music maps only onto music libraries and video never onto them, a non-aligning music library and a failed library lookup still full-refresh; Jellyfin requests carry Authorization: MediaBrowser Token=… and X-Emby-Token is never sent, Jellyfin without a music library is skipped, music maps onto the music library, and a mixed library counts as video.
  • MusicOutputPlacementTests (new) — through HandleRemoteCompletion: delete-original replaces the source under its clean name, same-extension replace overwrites the source path, the video replace toggle does not delete music originals (and a staged keep-both output still moves beside its original), replacement lands in place even with a music output directory, keep-both uses the output directory.
  • ReplacementRescanWiringTests (new) — a replaced music encode asks Lidarr to rescan its album, a replaced episode asks Sonarr to rescan its series, and a keep-both encode asks nothing.
  • OriginalLanguageLookupTests — folder names containing dots match for Sonarr and Radarr; a flat movie file named like the Arr folder still matches on Windows and POSIX paths.
  • EnvConfigOverridesTests — the Lidarr section locks via SNACKS_INTEG_Lidarr__*.

Files Changed

Arr rescans and Lidarr

  • Snacks/Services/IntegrationService.cs — QueueArrRescan, RescanAsync, ResolveRescanTargetAsync, WaitForQuietAsync, DebouncedRescanAsync, PostArrCommandAsync, RestoreRadarrMonitoringAsync, GetRadarrMonitoredAsync, GetArrJsonStringAsync, GetLidarrRootsAsync, ParseLidarrRoots, ArrRescanTarget, RescanState (all new); GetArrCatalogAsync / ArrCatalogEntry replace _arrLangCache; TestArrAsync takes an apiVersion
  • Snacks/Models/IntegrationConfig.cs — Lidarr
  • Snacks/Controllers/IntegrationsController.cs — POST /api/integrations/test/lidarr
  • Snacks/Controllers/ClusterController.cs — documents that the worker handout omits Lidarr
  • Snacks/Views/Shared/_IntegrationSettings.cshtml, Snacks/wwwroot/js/settings/panels/integrations-panel.js, Snacks/wwwroot/js/api.js — Lidarr panel, config round-trip, and test button; rescan hints on Sonarr/Radarr

Placement and rescan wiring

  • Snacks/Services/TranscodingService.cs — HandleMusicOutputPlacement, ReplacesOriginal (new); HandleOutputPlacement dispatches music; ConvertMusicAsync gains skipPlacement and runs placement instead of a bare delete; ConvertMusicForRemoteAsync skips placement; the local video, local music, and HandleRemoteCompletion completion paths pass MediaKind to TriggerRescansAsync and queue an Arr rescan on replacement
  • Snacks/Views/Shared/_MusicSettings.cshtml — in-place replacement hint

Plex / Jellyfin

  • Snacks/Services/IntegrationService.cs — TriggerRescansAsync, PlexRescanAsync, JellyfinRescanAsync take MediaKind; LibraryRoot.Type; RootsForKind, KindLabel, ParentDirectory, AddJellyfinAuth (new); GetDirectorySafe removed; the Plex and Jellyfin root parsers record the library type

Original language

  • Snacks/Services/IntegrationService.cs — the folder-name fallback strips the extension from the file name only

Documentation

  • Snacks/wwwroot/docs/index.html — Arr library sync section, integrations table, music placement row
  • README.md — feature bullet

Tests

  • Snacks.Tests/Integration/ArrRescanTests.cs (new), Snacks.Tests/Integration/MediaServerRescanTests.cs (new), Snacks.Tests/Pipeline/MusicOutputPlacementTests.cs (new), Snacks.Tests/Pipeline/ReplacementRescanWiringTests.cs (new)
  • Snacks.Tests/Integration/OriginalLanguageLookupTests.cs, Snacks.Tests/Settings/EnvConfigOverridesTests.cs

Version bumps

  • Snacks/Snacks.csproj — <Version>2.20.0</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 via sync-version.mjs; the docs page's updated date moves to 2026-10-06

Full documentation: README.md · /docs/index.html on a running instance

Don't miss a new Snacks release

NewReleases is sending notifications on new releases.