How it works¶
Source folder (polling observer)
|
v
New or renamed file detected ──> Wait for file completion (size-stable for 60s)
|
v
Resolution check ──> Skip if <= 720p
|
v
FFmpeg transcode ──> scale to 720p, encode video and audio, copy/convert subtitles
|
v
Verify output (ffprobe duration check)
|
v
Atomic rename .tmp -> .mkv/.mp4 ──> Create Jellyfin version symlink (optional)
Key design decisions:
- Polling observer (
watchdog.PollingObserver) instead of inotify, ensuring compatibility with NFS, CIFS, and other network filesystems. - Temp-file workflow -- encodes to a
.tmpfile first and atomically renames on success, preventing Jellyfin from indexing incomplete files. - File-growth detection -- before deleting stale
.tmpfiles, the cleanup routine checks whether the file is still being written by another instance. - ProcessPoolExecutor behind a priority queue -- one worker for hardware encoding (GPU is the bottleneck), multiple workers for software encoding (CPU-bound). Files wait in the encoder's own queue, and a dispatcher thread hands the executor the next one by priority whenever a worker frees up, so the executor never holds more than it can run.
- Container-agnostic output lookup -- an encode is located by filename stem across every container the tool writes, so changing codec or container never re-encodes a library that is already done.
Polling interval¶
Every poll takes a snapshot of the whole source tree: one stat for every file and folder
under SOURCE_FOLDER. The watcher waits POLL_INTERVAL seconds, takes the snapshot, and
reports what changed since the previous one. A new file is therefore noticed within one
interval plus one snapshot.
On a local disk a snapshot is cheap. On a network share holding tens of thousands of files it
is not: a snapshot of a 54k-entry CIFS tree took about 80 seconds, and with the old
one-second wait the file server answered around 1,500 metadata requests per second around
the clock for nothing. POLL_INTERVAL defaults to 60. Raise it to 300 or 600 for a large
library on NFS or CIFS; encoding one film takes longer than any of these intervals, so the
wait never decides throughput. A value that is not a number, not above zero, or above
86400 logs a warning and falls back to 60. The startup Config: line prints the value in
use.
The watcher matches files by inode, so a rename inside the source tree arrives as a move: a
download finishing its rename from .part or .!qB into .mkv, a folder renamed by hand,
or a file renamed by hand is handled within one poll. A finished encode follows its source:
it is renamed in the destination, the manifest entry and the version symlink move with it,
and nothing is encoded again, so renaming a whole folder costs a rename per file. A source
is encoded under the new name only when its encode is missing, still being written, or
cannot be renamed in place, for instance across filesystems. A file copied in from outside is
a plain create and is handled the same way.
Features in detail¶
- Automatic folder monitoring -- watches source directories for new, renamed and deleted files using polling (NFS/CIFS compatible)
- Hardware-accelerated encoding -- NVIDIA NVENC and Intel Quick Sync Video (QSV), with transparent software fallback (libx265 / libx264 / libsvtav1)
- Smart skip logic -- detects files already at 720p or lower via filename heuristics and ffprobe resolution analysis
- Jellyfin multi-version support -- creates version symlinks so Jellyfin presents both original and transcoded copies to the user
- H.264 / AAC / MP4 output -- set
ENCODING_CODEC: "h264"for MP4 output that Jellyfin clients direct play without transcoding, and without re-encoding the library you already have (see H.264, AAC and MP4 Output) - Audio normalization -- re-encodes audio for consistent playback: AAC keeping up to 5.1 for MP4, stereo AC3 at 192 kbps for MKV
- Subtitle preservation -- copies MKV-native subtitle codecs and converts incompatible ones (MOV text, WebVTT) to SRT; converts text subtitles to
mov_textfor MP4 - Guarded automatic cleanup -- periodically removes orphaned encodes and stale symlinks with mount-health checks to prevent mass deletion (see Safety and cleanup)
- Temp-file workflow -- encodes to
.tmpand atomically renames on success, so Jellyfin never indexes incomplete files (note: no cross-container locking — avoid pointing two encoders at the same destination subfolder) - Configurable quality presets -- LOW, MEDIUM, and HIGH profiles with per-codec CQ/CRF tuning