Pretranscode
A small, single-writer Jellyfin compatibility cache.
Immutable source media is the only source of truth. The cache is a disposable, filesystem-derived projection and contains no manifests, inventory database, or migration state.
Compatibility output
A cached rendition contains only:
- one H.264 High, 8-bit
yuv420pvideo stream; - one AAC-LC, 192 kbps stereo audio stream when the source has audio;
- BT.709 SDR output for HDR10, HLG, and Dolby Vision sources.
Embedded subtitles, attachments, chapters, and inherited metadata are omitted. Sidecar subtitle files are mirrored unchanged. This deliberately small profile prevents EAC3/Atmos, PGS, or stale Dolby Vision metadata from forcing another Jellyfin transcode.
A source needs a compatibility rendition unless it is already H.264 8-bit with AAC stereo audio and no embedded subtitles.
Durable model
- Originals are mounted read-only at
/source. - Jellyfin reads
/data/library, a symlink mirror under/view. - A link targets either
/data/originalor an immutable cache object. - Cache names encode hashes of the logical path, source generation, complete encoding profile, and output bytes.
- FFmpeg writes only a
.partialfile. - Publication is validation,
fsync, an atomic output rename, then an atomic symlink rename. - Open clients retain their old inode during publication.
- A profile change builds a new object while the published old-profile object remains available. The old object is collected only after replacement.
- LRU updates file access time only; published media modification time and bytes never change.
- Scheduling, retries, priorities, and cooldowns exist only in process memory.
- One controller owns the cache directory through
flock().
There is no cache migration command. Restarting with a changed profile is enough.
Installation
-
Copy
.env.exampleto.envand set the host paths and Jellyfin API key. -
Provision the cache filesystem:
sudo scripts/provision-cache.sh .env -
Mount the same paths in Jellyfin:
volumes: - ${PRETRANSCODE_VIEW_PATH}:/data/library:ro - ${MEDIA_LIBRARY_PATH}:/data/original:ro - ${PRETRANSCODE_CACHE_PATH}:/data/cache:ro -
Start the controller:
docker compose --env-file .env up -d --build
For Coolify, import compose.yaml as a Custom Docker Compose resource and enter
.env.example values through the environment-variable UI.
Operation
The controller has one worker. FFmpeg and file hashing run with SCHED_IDLE and
idle I/O priority so foreground services take precedence.
Playback locking rules:
- an active movie locks that movie;
- an active episode locks its series;
- an encode is cancelled if its item becomes locked;
- a series remains locked for
SERIES_COOLDOWN_SECONDSafter playback; - recently watched content receives in-memory queue priority.
Commands:
pretranscode run
pretranscode sync
pretranscode active
pretranscode trim BYTES
pretranscode purge
pretranscode --version
purge and trim refuse to operate during active playback.
Failure behaviour
Partial outputs are never published. Startup removes stale partials and
unreferenced objects. A missing cache mount marker, second controller, failed
probe, failed validation, failed fsync, or unavailable Jellyfin session API
causes work to stop safely rather than mutate published media.
Tests
The image build treats compiler warnings as errors and runs the C core tests. The integration test creates incompatible media and verifies canonical streams, atomic publication, refresh retry, and stateless profile replacement:
podman build -t pretranscode-test .
IMAGE=pretranscode-test tests/integration.sh