Recode, link and announce Radarr imports (smart-recode, manage-linkfarm, Jellyfin)
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Paul Provost 53ee41e6fc radarr-recode: recode, link and announce Radarr imports
Moved out of sulaco-playbook (files/radarr), with a README covering
both scripts, the queue and the systemd setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:23:46 -04:00
radarr-recode radarr-recode: recode, link and announce Radarr imports 2026-10-27 11:23:46 -04:00
README.md radarr-recode: recode, link and announce Radarr imports 2026-10-27 11:23:46 -04:00
smart-recode-enqueue radarr-recode: recode, link and announce Radarr imports 2026-10-27 11:23:46 -04:00

radarr-recode

Post-processing for Radarr imports. Every movie file Radarr imports is:

  1. moved into the 09/ bucket if its title starts with a digit,
  2. re-encoded with smart-recode (720p HEVC on the GPU), replacing the original,
  3. rescanned in Radarr, so it tracks the new file,
  4. linked into a link farm on the NAS with manage-linkfarm,
  5. announced to Jellyfin, which reads that farm.

Two scripts share the work, because Radarr 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 Radarr container, as a Custom Script connection appends each imported file to a queue file in Radarr's /config
radarr-recode on the host, started by a systemd path unit when the queue exists works through the queue: steps 1–5 above
Radarr import ──▶ smart-recode-enqueue ──▶ <config>/smart-recode/queue
                                                    │ (radarr-recode.path)
                                                    ▼
             radarr-recode: 09/ bucket ─▶ smart-recode ─▶ RescanMovie
                                     ─▶ manage-linkfarm (ssh) ─▶ Jellyfin

Library layout it expects

Radarr's library (mounted at /movies in the container) is canonical storage, split by language and bucketed by initial:

/movies/<language>/<initial>/<movie folder>/<file>
        english     A, B, … Z, 09
        french

Radarr creates new folders with the naming format {Movie TitleFirstCharacter}/{Movie Title} ({Release Year})[m]. That token puts "1408" in 1/, but the library and manage-linkfarm keep every title that starts with a digit in 09/, so radarr-recode moves those folders (step 1).

The link farm mirrors the languages: <farm>/<language>/<link>, where each link points at a movie folder and is named after it, minus any trailing [m].

smart-recode-enqueue

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

Reads Radarr's radarr_eventtype environment variable:

  • Download (sent for new imports and upgrades): appends <radarr_movie_id>\t<radarr_moviefile_path> to /config/smart-recode/queue.
  • Test (Radarr's "Test" button): succeeds only if /config/smart-recode exists, so a missing queue directory shows up when the connection is saved.

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

radarr-recode

radarr-recode --queue-dir DIR --movies-dir DIR
              --config CONFIG_XML --port PORT
              --nas-host HOST --linkfarm-script PATH
              --farm-dir DIR
              [--jellyfin-url URL --jellyfin-farm-dir DIR]
Option Meaning
--queue-dir host path of the container's /config/smart-recode
--movies-dir host path of the container's /movies
--config Radarr's config.xml, for its API key
--port Radarr's port on localhost
--nas-host ssh destination where manage-linkfarm runs (batch mode, no prompts)
--linkfarm-script path of manage-linkfarm.py on that host
--farm-dir the movie link farm on that host; links go in <farm-dir>/<language>
--jellyfin-url Jellyfin's base URL (optional)
--jellyfin-farm-dir the same 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 5 is skipped with a log message.

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

What happens to each queued file

Queue lines are <movie id>\t<path inside the container>. The /movies/ prefix is mapped to --movies-dir. A file that no longer exists is skipped.

  1. 09/ bucket. If the movie folder sits in a single-digit bucket (english/1/…), it is moved to english/09/…. Radarr is given the new path (PUT movie/<id>?moveFiles=false), and the empty digit bucket is removed. If 09/ already has a folder of that name, the movie stays where it is.

  2. Recode. Files already named *.yify.mp4 are left alone. Otherwise smart-recode --yes <file> runs in the movie folder. The original is deleted only when smart-recode succeeds and exactly one new .yify.mp4 appeared next to it.

  3. Rescan. Radarr is asked to RescanMovie, so it swaps the deleted original for the .yify.mp4.

  4. Link. On --nas-host, in <farm-dir>/<language>: manage-linkfarm -y --only <folder>. This runs whether or not the recode worked, so a movie still reaches the farm when its recode failed. Its exit status is logged:

    Status Meaning
    0 linked (or already linked)
    2 not linked: the folder was rejected earlier in that farm
    3 not found: the folder isn't in 09/ or a letter bucket
  5. Jellyfin. After status 0, POST <jellyfin-url>/Library/Media/Updated with <jellyfin-farm-dir>/<language>/<link>. Jellyfin can't see links made on the NAS through its SMB mount, so without this a new movie only shows up after its scheduled library scan. Jellyfin picks the change up after its library monitor delay, about a minute.

A failure in steps 1, 3, 4 or 5 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 radarr-recode takes it over
queue.work the lines being processed; removed once all are handled
failed lines whose recode failed; their originals were kept

radarr-recode moves queue aside, waits a second for any write already in progress, and appends it to queue.work. If a run is interrupted, queue.work is still there and the path unit starts radarr-recode again to retry it. A retried line whose recode already happened is skipped (the original is gone, or it's a .yify.mp4).

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

Writing a line to the queue by hand is also a cheap way to test the pipeline. For a movie that is already recoded, it only relinks and notifies Jellyfin.

Running it under systemd

radarr-recode is meant to be started by a path unit that watches the queue. Use PathExists, not PathModified: systemd checks again when the service exits, so a line queued during a run starts another one.

# radarr-recode.path
[Path]
PathExists=/srv/radarr/config/smart-recode/queue
PathExists=/srv/radarr/config/smart-recode/queue.work

[Install]
WantedBy=paths.target
# radarr-recode.service
[Service]
Type=oneshot
User=paul
Nice=15
IOSchedulingClass=idle
Environment=PATH=/home/paul/local/bin:/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin
# JELLYFIN_API_KEY=..., readable by root only
EnvironmentFile=/etc/radarr-recode.env
ExecStart=/home/paul/local/bin/radarr-recode \
  --queue-dir /srv/radarr/config/smart-recode \
  --movies-dir /mnt/movies \
  --config /srv/radarr/config/config.xml \
  --port 7878 \
  --nas-host root@nas \
  --linkfarm-script /mnt/user/appdata/manage-linkfarm/manage-linkfarm.py \
  --farm-dir /mnt/user/paul/media/video/movies \
  --jellyfin-url http://localhost:8096 \
  --jellyfin-farm-dir /media/paul/video/movies

Logs go to the journal: journalctl -u radarr-recode.

On sulaco, the units, the env file and the Radarr side (quality caps, naming, root folders, the Custom Script connection) are set up by the radarr.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, ssh
  • ssh access to --nas-host without a prompt

On the NAS: manage-linkfarm.py and python3.

In the container: bash.