Recode non-x265 and 1080p Sonarr imports (smart-recode, Jellyfin)
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Paul Provost c24272de49 sonarr-recode: recode non-x265 and 1080p Sonarr imports
Series counterpart of radarr-recode: a Custom Script queues every 1080p
import and every 720p import not matching the x265 custom format, and a
host-side worker recodes them, rescans, re-monitors and tells Jellyfin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:03:24 -04:00
README.md sonarr-recode: recode non-x265 and 1080p Sonarr imports 2026-10-31 12:03:24 -04:00
smart-recode-enqueue sonarr-recode: recode non-x265 and 1080p Sonarr imports 2026-10-31 12:03:24 -04:00
sonarr-recode sonarr-recode: recode non-x265 and 1080p Sonarr imports 2026-10-31 12:03:24 -04:00

sonarr-recode

Post-processing for Sonarr imports. Every imported 1080p episode, and every 720p episode that doesn't match the x265 custom format, is:

  1. re-encoded with smart-recode (720p HEVC on the GPU), replacing the original,
  2. rescanned in Sonarr, so it tracks the new file,
  3. set back to monitored,
  4. announced to Jellyfin.

720p x265 files are left as they are. This is the series counterpart of radarr-recode.

Two scripts share the work, because Sonarr runs in a container that has none of the tools the encode needs (fish, gum, the VAAPI ffmpeg, the GPU):

Script Runs Job
smart-recode-enqueue inside the Sonarr container, as a Custom Script connection appends eligible imports to a queue file in Sonarr's /config
sonarr-recode on the host, started by a systemd path unit when the queue exists works through the queue: steps 1–4 above

smart-recode-enqueue

smart-recode-enqueue        # called by Sonarr, no arguments

Reads Sonarr's environment variables:

  • sonarr_eventtype=Test (Sonarr's "Test" button): succeeds only if /config/smart-recode exists.
  • sonarr_eventtype=Download (new imports and upgrades): appends <series id>\t<episode ids>\t<path> to /config/smart-recode/queue, unless the file is one of these:
    File Why it is skipped
    *.yify.mp4 already recoded
    HDR or Dolby Vision recode-video-yify-gpu does no tonemapping, so colours would come out wrong
    720p matching the x265 custom format already the wanted result
    neither 720p nor 1080p outside what this recodes

The format is matched by name, so renaming x265 in Sonarr means changing this script too.

Install it somewhere under the container's /config and add it in Sonarr under Settings → Connect → Custom Script, with On File Import and On File Upgrade enabled.

sonarr-recode

sonarr-recode --queue-dir DIR --series-dir DIR
              --config CONFIG_XML --port PORT
              [--jellyfin-url URL --jellyfin-farm-dir DIR]
Option Meaning
--queue-dir host path of the container's /config/smart-recode
--series-dir host path of the container's /tv
--config Sonarr's config.xml, for its API key
--port Sonarr's port on localhost
--jellyfin-url Jellyfin's base URL (optional)
--jellyfin-farm-dir the series farm as Jellyfin sees it (optional)

Environment:

  • JELLYFIN_API_KEY: the key for Jellyfin's API. Kept out of the command line, where ps would show it. Without it, or without the two Jellyfin options, step 4 is skipped with a log message.

sonarr-recode loops until the queue is empty, then exits.

What happens to each queued file

The /tv/ prefix is mapped to --series-dir. A file that no longer exists is skipped, and so is a *.yify.mp4.

  1. Recode. smart-recode --yes runs in a .sonarr-recode folder next to the file, through a symlink to it. The encoder writes its output in stages, and Sonarr's scheduled refresh skips dot-folders, so it can't import a half-written file. The original is deleted only when smart-recode succeeds and exactly one .yify.mp4 came out and was moved next to it.
  2. Rescan. Sonarr is asked to RescanSeries, so it swaps the deleted original for the .yify.mp4, and sonarr-recode waits for that to finish (up to 10 minutes).
  3. Monitor. With Sonarr's "unmonitor deleted episodes" setting on, the rescan unmonitors episodes whose file went missing. PUT episode/monitor sets them back.
  4. Jellyfin. POST <jellyfin-url>/Library/Media/Updated with <jellyfin-farm-dir>/<language>/<series>.

A failure in steps 2, 3 or 4 is logged and doesn't stop the rest.

Queue files

All in --queue-dir:

File Purpose
queue written by smart-recode-enqueue
queue.incoming queue, moved aside while sonarr-recode takes it over
queue.work the lines being processed; removed once all are handled
failed lines whose recode failed; their originals were kept

Retrying a failed recode

Lines in failed are not retried automatically. Once the cause is fixed, put the line back in the queue and remove it from failed:

cat <queue-dir>/failed >> <queue-dir>/queue
rm <queue-dir>/failed

Running it under systemd

Use a path unit with PathExists, not PathModified: systemd checks again when the service exits, so a line queued during a run starts another one. Watch both queue and queue.work, so an interrupted run is retried. Logs go to the journal: journalctl -u sonarr-recode.

On sulaco, the units, the env file and the Sonarr side (profile, delay profile, custom formats, the Custom Script connection) are set up by the sonarr.yml playbook in sulaco-playbook, which also checks out this repo.

Requirements

On the host:

  • fish 3.5 or later (for path)
  • smart-recode and what it needs (recode-video-yify-gpu, the VAAPI ffmpeg, gum)
  • curl, jq
  • a series share that supports symlinks and dot-folders

In the container: bash.