115 lines
3.7 KiB
Markdown
115 lines
3.7 KiB
Markdown
# 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
|
|
```
|