# 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 `yuv420p` video 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/original` or an immutable cache object. - Cache names encode hashes of the logical path, source generation, complete encoding profile, and output bytes. - FFmpeg writes only a `.partial` file. - 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 1. Copy `.env.example` to `.env` and set the host paths and Jellyfin API key. 2. Provision the cache filesystem: ```bash sudo scripts/provision-cache.sh .env ``` 3. Mount the same paths in Jellyfin: ```yaml volumes: - ${PRETRANSCODE_VIEW_PATH}:/data/library:ro - ${MEDIA_LIBRARY_PATH}:/data/original:ro - ${PRETRANSCODE_CACHE_PATH}:/data/cache:ro ``` 4. Start the controller: ```bash 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_SECONDS` after playback; - recently watched content receives in-memory queue priority. Commands: ```bash 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: ```bash podman build -t pretranscode-test . IMAGE=pretranscode-test tests/integration.sh ```