Replace stateful cache with atomic C controller

This commit is contained in:
SpiderFish
2026-08-29 21:56:05 +10:00
parent a66945c245
commit bd2b623633
12 changed files with 1823 additions and 1199 deletions
View File
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
import json
import os
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse
view = os.environ["TEST_VIEW"]
state_path = os.environ["TEST_STATE"]
codec = "hevc"
depth = 10
refresh_attempts = 0
def record_state():
with open(state_path, "w") as handle:
json.dump({"codec": codec, "depth": depth, "attempts": refresh_attempts}, handle)
def refresh_media():
global codec, depth
link = os.path.join(view, "movies/Test/Test.mkv")
try:
target = os.readlink(link)
except OSError:
target = ""
codec, depth = ("h264", 8) if target.startswith("/data/cache/") else ("hevc", 10)
record_state()
class Handler(BaseHTTPRequestHandler):
def send_json(self, value, status=200):
body = json.dumps(value, separators=(",", ":")).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
path = urlparse(self.path).path
if path == "/Sessions":
self.send_json([])
elif path == "/Items/test-item":
self.send_json({"Id": "test-item", "Path": "/data/library/movies/Test/Test.mkv"})
elif path == "/Items":
self.send_json({
"Items": [{
"Id": "test-item",
"Path": "/data/library/movies/Test/Test.mkv",
"RunTimeTicks": 6 * 10_000_000,
"MediaStreams": [{
"Type": "Video", "Codec": codec, "BitDepth": depth,
"Profile": "Main 10" if depth == 10 else "High",
"Width": 1280, "Height": 720,
}],
}],
"TotalRecordCount": 1,
})
else:
self.send_json({"error": "not found"}, 404)
def do_POST(self):
global refresh_attempts
path = urlparse(self.path).path
if path.startswith("/Items/test-item/Refresh"):
refresh_attempts += 1
if refresh_attempts == 1:
record_state()
self.send_response(503)
self.end_headers()
return
refresh_media()
self.send_response(204)
self.end_headers()
elif path == "/Library/Refresh":
self.send_response(204)
self.end_headers()
else:
self.send_json({"error": "not found"}, 404)
def log_message(self, fmt, *args):
pass
record_state()
server = ThreadingHTTPServer(("0.0.0.0", int(sys.argv[1])), Handler)
server.serve_forever()
+128 -29
View File
@@ -1,40 +1,139 @@
#!/bin/bash
set -euo pipefail
: "${JELLYFIN_API_KEY:?}"
IMAGE=${IMAGE:-pretranscode:test}
NETWORK=${MEDIA_NETWORK:-media-internal}
JELLYFIN_URL=${JELLYFIN_URL:-http://jellyfin:8096}
IMAGE=${IMAGE:-localhost/pretranscode-c-review}
ROOT=$(mktemp -d)
trap 'rm -rf "$ROOT"' EXIT
mkdir -p "$ROOT"/{source/movies/Test,view,cache,state}
PORT=$(python3 - <<'PY'
import socket
s=socket.socket(); s.bind(('127.0.0.1', 0)); print(s.getsockname()[1]); s.close()
PY
)
PID=
CONTAINER=
cleanup() {
[ -z "$CONTAINER" ] || podman rm -f "$CONTAINER" >/dev/null 2>&1 || true
[ -z "$PID" ] || kill "$PID" >/dev/null 2>&1 || true
rm -rf "$ROOT"
}
trap cleanup EXIT
mkdir -p "$ROOT"/{source/movies/Test,view,cache/files,state}
touch "$ROOT/cache/.pretranscode-cache"
printf partial >"$ROOT/cache/files/failed.partial"
printf orphan >"$ROOT/cache/files/orphan.bin"
docker build -q -t "$IMAGE" . >/dev/null
docker run --rm --entrypoint ffmpeg -v "$ROOT/source:/source" "$IMAGE" \
cat >"$ROOT/source/movies/Test/embedded.srt" <<'EOF'
1
00:00:00,000 --> 00:00:05,000
Compatibility test
EOF
podman run --rm --userns=keep-id --entrypoint ffmpeg -v "$ROOT/source:/source:Z" "$IMAGE" \
-hide_banner -loglevel error -f lavfi -i testsrc2=size=1280x720:rate=24 \
-t 6 -c:v libx265 -pix_fmt yuv420p10le -an /source/movies/Test/Test.mkv
-f lavfi -i sine=frequency=440:sample_rate=48000 \
-f srt -i /source/movies/Test/embedded.srt -t 6 \
-map 0:v:0 -map 1:a:0 -map 2:s:0 -c:v libx265 -pix_fmt yuv420p10le \
-c:a eac3 -c:s srt /source/movies/Test/Test.mkv
docker run --rm --entrypoint ffmpeg -v "$ROOT/source:/source" "$IMAGE" \
-hide_banner -loglevel error -f lavfi -i testsrc2=size=1280x720:rate=24 \
-t 6 -c:v libx265 -pix_fmt yuv420p10le \
-color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc \
-x265-params 'colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc' \
-an /source/movies/Test/Test.HDR10.mkv
docker run --rm --network "$NETWORK" --entrypoint python3 \
TEST_VIEW="$ROOT/view" TEST_STATE="$ROOT/jellyfin-state.json" \
python3 "$(dirname "$0")/fake_jellyfin.py" "$PORT" &
PID=$!
sleep 0.2
CONTAINER=pretranscode-c-integration-$RANDOM
podman run -d --userns=keep-id --name "$CONTAINER" \
--add-host host.containers.internal:host-gateway \
-e SOURCE_ROOT=/source -e VIEW_ROOT=/view -e CACHE_ROOT=/cache -e STATE_ROOT=/state \
-e JELLYFIN_URL="$JELLYFIN_URL" -e JELLYFIN_API_KEY \
-e JELLYFIN_URL="http://host.containers.internal:$PORT" -e JELLYFIN_API_KEY=test \
-e JELLYFIN_LIBRARY_ROOT=/data/library -e JELLYFIN_ORIGINAL_ROOT=/data/original \
-e JELLYFIN_CACHE_ROOT=/data/cache \
-v "$ROOT/source:/source:ro" -v "$ROOT/view:/view" -v "$ROOT/cache:/cache" -v "$ROOT/state:/state" \
"$IMAGE" -c 'import importlib.machinery; m=importlib.machinery.SourceFileLoader("p","/app/pretranscode").load_module(); x=m.Manager(); assert x.read_sessions(); x.scan_inventory(); x.sync_view(); assert x.encode("movies/Test/Test.mkv",False)=="completed"; assert x.encode("movies/Test/Test.HDR10.mkv",False)=="completed"'
-e JELLYFIN_CACHE_ROOT=/data/cache -e POLL_SECONDS=1 -e RESCAN_SECONDS=1 \
-e H264_PRESET=ultrafast \
-v "$ROOT/source:/source:ro,Z" -v "$ROOT/view:/view:Z" \
-v "$ROOT/cache:/cache:Z" -v "$ROOT/state:/state:Z" "$IMAGE" >/dev/null
SDR_TARGET=$(readlink "$ROOT/view/movies/Test/Test.mkv")
HDR_TARGET=$(readlink "$ROOT/view/movies/Test/Test.HDR10.mkv")
docker run --rm --entrypoint ffprobe -v "$ROOT/cache:/cache:ro" "$IMAGE" \
-v error -select_streams v:0 -show_entries stream=codec_name,pix_fmt \
-of default=nw=1 "${SDR_TARGET/\/data\/cache/\/cache}" | grep -qx 'codec_name=h264'
docker run --rm --entrypoint ffprobe -v "$ROOT/cache:/cache:ro" "$IMAGE" \
-v error -select_streams v:0 -show_entries stream=color_transfer,color_primaries,color_space \
-of default=nw=1 "${HDR_TARGET/\/data\/cache/\/cache}" | grep -qx 'color_transfer=bt709'
for _ in $(seq 1 90); do
LINK=$(readlink "$ROOT/view/movies/Test/Test.mkv" 2>/dev/null || true)
case "$LINK" in
/data/cache/files/*) break ;;
esac
if [ "$(podman inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)" != true ]; then
podman logs "$CONTAINER" >&2 || true
exit 1
fi
sleep 1
done
LINK=$(readlink "$ROOT/view/movies/Test/Test.mkv" 2>/dev/null || true)
if [[ "$LINK" != /data/cache/files/* ]]; then
podman logs "$CONTAINER" >&2 || true
cat "$ROOT/state/ffmpeg.log" >&2 2>/dev/null || true
exit 1
fi
[[ "$LINK" == /data/cache/files/* ]]
BASENAME=${LINK##*/}
[[ "$BASENAME" =~ ^[0-9a-f]{32}\.[0-9a-f]{32}\.[0-9a-f]{32}\.[0-9a-f]{32}\.(mkv|mp4)$ ]]
OUTPUT="$ROOT/cache/${LINK#/data/cache/}"
test -s "$OUTPUT"
! find "$ROOT/cache/files" -type f \( -name '*.json' -o -name '*.partial' \) | grep -q .
test ! -e "$ROOT/cache/files/orphan.bin"
podman run --rm --userns=keep-id --entrypoint ffprobe -v "$ROOT/cache:/cache:ro,Z" "$IMAGE" \
-v error -show_entries stream=codec_type,codec_name,pix_fmt,channels:stream_side_data=side_data_type \
-show_entries chapter=id -of json "/cache/${LINK#/data/cache/}" | python3 -c '
import json,sys
d=json.load(sys.stdin); streams=d.get("streams",[])
video=[s for s in streams if s.get("codec_type")=="video"]
audio=[s for s in streams if s.get("codec_type")=="audio"]
subs=[s for s in streams if s.get("codec_type")=="subtitle"]
assert len(video)==1 and video[0].get("codec_name")=="h264" and video[0].get("pix_fmt")=="yuv420p"
assert len(audio)==1 and audio[0].get("codec_name")=="aac" and audio[0].get("channels")==2
assert not subs and not d.get("chapters")
assert not any("dovi" in x.get("side_data_type","").lower() for x in video[0].get("side_data_list",[]))
'
python3 - "$ROOT/jellyfin-state.json" <<'PY'
import json, sys
state=json.load(open(sys.argv[1]))
assert state["codec"] == "h264" and state["depth"] == 8
assert state["attempts"] >= 2
PY
# A profile change, including a published 1.x object, keeps the old object live
# until its replacement exists.
podman stop -t 10 "$CONTAINER" >/dev/null
podman rm "$CONTAINER" >/dev/null
CONTAINER=
mv "$OUTPUT" "$ROOT/cache/files/legacy.mkv"
ln -sfn /data/cache/files/legacy.mkv "$ROOT/view/movies/Test/Test.mkv"
OLD_LINK=/data/cache/files/legacy.mkv
podman run --rm --userns=keep-id \
--add-host host.containers.internal:host-gateway \
-e SOURCE_ROOT=/source -e VIEW_ROOT=/view -e CACHE_ROOT=/cache -e STATE_ROOT=/state \
-e JELLYFIN_URL="http://host.containers.internal:$PORT" -e JELLYFIN_API_KEY=test \
-e JELLYFIN_LIBRARY_ROOT=/data/library -e JELLYFIN_ORIGINAL_ROOT=/data/original \
-e JELLYFIN_CACHE_ROOT=/data/cache -e POLL_SECONDS=1 -e RESCAN_SECONDS=1 \
-e H264_PRESET=ultrafast -e H264_CRF=19 \
-v "$ROOT/source:/source:ro,Z" -v "$ROOT/view:/view:Z" \
-v "$ROOT/cache:/cache:Z" -v "$ROOT/state:/state:Z" "$IMAGE" sync
[[ "$(readlink "$ROOT/view/movies/Test/Test.mkv")" == "$OLD_LINK" ]]
test -s "$ROOT/cache/${OLD_LINK#/data/cache/}"
CONTAINER=pretranscode-c-integration-$RANDOM
podman run -d --userns=keep-id --name "$CONTAINER" \
--add-host host.containers.internal:host-gateway \
-e SOURCE_ROOT=/source -e VIEW_ROOT=/view -e CACHE_ROOT=/cache -e STATE_ROOT=/state \
-e JELLYFIN_URL="http://host.containers.internal:$PORT" -e JELLYFIN_API_KEY=test \
-e JELLYFIN_LIBRARY_ROOT=/data/library -e JELLYFIN_ORIGINAL_ROOT=/data/original \
-e JELLYFIN_CACHE_ROOT=/data/cache -e POLL_SECONDS=1 -e RESCAN_SECONDS=1 \
-e H264_PRESET=ultrafast -e H264_CRF=19 \
-v "$ROOT/source:/source:ro,Z" -v "$ROOT/view:/view:Z" \
-v "$ROOT/cache:/cache:Z" -v "$ROOT/state:/state:Z" "$IMAGE" >/dev/null
for _ in $(seq 1 90); do
LINK=$(readlink "$ROOT/view/movies/Test/Test.mkv" 2>/dev/null || true)
[[ "$LINK" == /data/cache/files/* && "$LINK" != "$OLD_LINK" ]] && break
sleep 1
done
[[ "$LINK" == /data/cache/files/* && "$LINK" != "$OLD_LINK" ]]
for _ in $(seq 1 10); do
test ! -e "$ROOT/cache/${OLD_LINK#/data/cache/}" && break
sleep 1
done
test ! -e "$ROOT/cache/${OLD_LINK#/data/cache/}"
podman stop -t 10 "$CONTAINER" >/dev/null
podman rm "$CONTAINER" >/dev/null
CONTAINER=
echo "integration test passed"
+159
View File
@@ -0,0 +1,159 @@
#define PRETRANSCODE_TEST
#include "../pretranscode.c"
#include <assert.h>
static void write_file(const char *path, const char *value)
{
char *parent = xstrdup(path), *slash = strrchr(parent, '/');
assert(slash); *slash = 0; assert(mkdirs(parent) == 0); free(parent);
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
assert(fd >= 0); assert(write(fd, value, strlen(value)) == (ssize_t)strlen(value));
assert(close(fd) == 0);
}
static void remove_tree(const char *root)
{
char *roots[] = {(char *)root, NULL};
FTS *tree = fts_open(roots, FTS_PHYSICAL | FTS_NOCHDIR, NULL); assert(tree);
FTSENT *entry;
while ((entry = fts_read(tree))) {
if (entry->fts_info == FTS_DP) assert(rmdir(entry->fts_path) == 0);
else if (entry->fts_info != FTS_D) assert(unlink(entry->fts_path) == 0);
}
fts_close(tree);
}
static void test_hashes(void)
{
char a[33], b[33];
path_hash("tv/Test/one.mkv", a); path_hash("tv/Test/one.mkv", b);
assert(strcmp(a, b) == 0); path_hash("tv/Test/two.mkv", b); assert(strcmp(a, b));
struct stat st = {.st_dev = 1, .st_ino = 2, .st_size = 3};
st.st_mtim.tv_sec = 4; st.st_mtim.tv_nsec = 5;
generation_hash(&st, a); st.st_ino++; generation_hash(&st, b); assert(strcmp(a, b));
char path[] = "/tmp/pretranscode-hash-XXXXXX";
int fd = mkstemp(path); assert(fd >= 0);
const char value[] = "immutable output";
assert(write(fd, value, sizeof(value) - 1) == (ssize_t)(sizeof(value) - 1)); close(fd);
assert(file_hash(path, a) == 0); hash_bytes(value, sizeof(value) - 1, b);
assert(strcmp(a, b) == 0); assert(unlink(path) == 0);
}
static void test_atomic_link_keeps_open_inode(void)
{
char root[] = "/tmp/pretranscode-link-XXXXXX"; assert(mkdtemp(root));
char *view = xprintf("%s/view/file.mkv", root);
char *original = xprintf("%s/original", root), *cached = xprintf("%s/cached", root);
write_file(original, "original"); write_file(cached, "cached");
assert(atomic_link(view, original) == 1);
int old = open(view, O_RDONLY | O_CLOEXEC); assert(old >= 0);
assert(atomic_link(view, cached) == 1);
char data[16] = {0}; assert(read(old, data, sizeof(data)) == 8); assert(!strcmp(data, "original")); close(old);
memset(data, 0, sizeof(data)); int current = open(view, O_RDONLY | O_CLOEXEC); assert(current >= 0);
assert(read(current, data, sizeof(data)) == 6); assert(!strcmp(data, "cached")); close(current);
free(view); free(original); free(cached); remove_tree(root);
}
static void test_cache_name(void)
{
const char *hash = "00112233445566778899aabbccddeeff";
char *name = xprintf("%s.%s.%s.%s.mkv", hash, hash, hash, hash);
struct cache cache = {0}; assert(parse_cache_name(name, &cache)); cache.named = true;
assert(!strcmp(cache.path_hash, hash)); assert(!strcmp(cache.extension, ".mkv"));
struct item item = {.extension = ".mkv"};
memcpy(item.path_hash, hash, HASH_HEX + 1); memcpy(item.generation_hash, hash, HASH_HEX + 1);
assert(cache_matches(&cache, &item));
name[2] = 'X'; assert(!parse_cache_name(name, &cache)); free(name);
}
static void test_lru_does_not_change_media_mtime(void)
{
char root[] = "/tmp/pretranscode-atime-XXXXXX"; assert(mkdtemp(root));
char *view_root = xprintf("%s/view", root), *media = xprintf("%s/media.mkv", root);
char *view = xprintf("%s/movie.mkv", view_root); write_file(media, "cached");
assert(atomic_link(view, "/cache/files/media.mkv") == 1);
struct timespec old[2] = {{.tv_sec = 100}, {.tv_sec = 100}};
assert(utimensat(AT_FDCWD, media, old, 0) == 0);
struct manager manager = {.cfg.view_root = view_root};
struct cache cache = {.path = media, .target = "/cache/files/media.mkv"};
manager.caches.v = &cache; manager.caches.n = 1;
touch_target(&manager, "movie.mkv");
struct stat st; assert(stat(media, &st) == 0);
assert(st.st_mtim.tv_sec == 100); assert(st.st_atim.tv_sec > 100);
manager.caches.v = NULL; manager.caches.n = 0;
free(view_root); free(media); free(view); remove_tree(root);
}
static void test_old_profile_is_replacement_candidate(void)
{
const char *hash = "00112233445566778899aabbccddeeff";
struct cache old = {.named = true, .target = "/cache/files/old.mkv"};
memcpy(old.path_hash, hash, HASH_HEX + 1); memcpy(old.generation_hash, hash, HASH_HEX + 1);
memcpy(old.extension, ".mkv", 5); memset(old.profile_hash, '1', HASH_HEX); old.profile_hash[HASH_HEX] = 0;
struct item item = {.relative = "movie.mkv", .extension = ".mkv", .fallback = &old, .mtime = 1};
memcpy(item.path_hash, hash, HASH_HEX + 1); memcpy(item.generation_hash, hash, HASH_HEX + 1);
struct manager manager = {0}; manager.items.v = &item; manager.items.n = 1;
memset(manager.cfg.profile_hash, '2', HASH_HEX); manager.cfg.profile_hash[HASH_HEX] = 0;
assert(valid_cache(&manager, &item) == NULL);
assert(candidate(&manager) == &item);
}
static void test_single_writer_lock(void)
{
char root[] = "/tmp/pretranscode-lock-XXXXXX";
assert(mkdtemp(root));
int first = open(root, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
int second = open(root, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
assert(first >= 0 && second >= 0);
assert(flock(first, LOCK_EX | LOCK_NB) == 0);
assert(flock(second, LOCK_EX | LOCK_NB) < 0 && (errno == EWOULDBLOCK || errno == EAGAIN));
close(second); close(first); assert(rmdir(root) == 0);
}
static void test_scheduling(void)
{
int pipes[2]; assert(pipe(pipes) == 0); pid_t pid = fork(); assert(pid >= 0);
if (!pid) {
close(pipes[0]); background_or_exit();
int values[2] = {sched_getscheduler(0),
IOPRIO_PRIO_CLASS((int)syscall(SYS_ioprio_get, IOPRIO_WHO_PROCESS, 0))};
if (write(pipes[1], values, sizeof(values)) != (ssize_t)sizeof(values))
_exit(1);
_exit(0);
}
close(pipes[1]); int values[2]; assert(read(pipes[0], values, sizeof(values)) == (ssize_t)sizeof(values));
close(pipes[0]); assert(waitpid(pid, NULL, 0) == pid);
assert(values[0] == SCHED_IDLE); assert(values[1] == IOPRIO_CLASS_IDLE);
}
static void test_output_validation(void)
{
struct media source = {.codec = "hevc", .depth = 10, .width = 1920, .height = 1080,
.audio = 2, .subtitles = 3, .attachments = 1, .chapters = 12, .duration = 1200};
struct media output = {.codec = "h264", .depth = 8, .width = 1920, .height = 1080,
.audio = 1, .audio_compatible = true, .duration = 1200};
assert(valid_output(&source, &output));
output.dolby_vision = true; assert(!valid_output(&source, &output)); output.dolby_vision = false;
output.subtitles = 1; assert(!valid_output(&source, &output)); output.subtitles = 0;
output.audio_compatible = false; assert(!valid_output(&source, &output));
}
static void test_canonical_policy(void)
{
struct media media = {.codec = "h264", .depth = 8, .audio = 1, .audio_compatible = true};
assert(!incompatible(&media));
snprintf(media.codec, sizeof(media.codec), "av1"); assert(incompatible(&media));
snprintf(media.codec, sizeof(media.codec), "h264"); media.audio_compatible = false; assert(incompatible(&media));
media.audio_compatible = true; media.subtitles = 1; assert(incompatible(&media));
}
int main(void)
{
test_hashes(); test_atomic_link_keeps_open_inode(); test_cache_name();
test_lru_does_not_change_media_mtime(); test_old_profile_is_replacement_candidate();
test_single_writer_lock(); test_scheduling();
test_output_validation(); test_canonical_policy();
puts("all core tests passed"); return 0;
}
-163
View File
@@ -1,163 +0,0 @@
import importlib.util
import json
import os
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
class CoreTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temp = tempfile.TemporaryDirectory()
root = Path(cls.temp.name)
for name in ("source", "view", "cache", "state"):
(root / name).mkdir()
(root / "cache" / ".pretranscode-cache").touch()
os.environ.update({
"SOURCE_ROOT": str(root / "source"),
"VIEW_ROOT": str(root / "view"),
"CACHE_ROOT": str(root / "cache"),
"STATE_ROOT": str(root / "state"),
"JELLYFIN_LIBRARY_ROOT": "/library",
"JELLYFIN_ORIGINAL_ROOT": "/original",
"JELLYFIN_CACHE_ROOT": "/cached",
})
spec = importlib.util.spec_from_file_location("pretranscode", Path(__file__).parents[1] / "pretranscode.py")
cls.module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cls.module)
cls.root = root
@classmethod
def tearDownClass(cls):
cls.temp.cleanup()
def test_paths_are_configurable(self):
m = self.module
self.assertEqual(m.relative_source("/library/tv/Show/episode.mkv"), "tv/Show/episode.mkv")
self.assertEqual(m.source_target("tv/Show/episode.mkv"), "/original/tv/Show/episode.mkv")
self.assertTrue(m.cache_target("tv/Show/episode.mkv").startswith("/cached/files/"))
def test_series_lock_key(self):
m = self.module
self.assertEqual(m.item_key("tv/Show/Season 1/Episode.mkv"), "series:Show")
self.assertEqual(m.item_key("movies/Movie/file.mkv"), "item:movies/Movie/file.mkv")
def test_fixed_codec_blacklist(self):
manager = self.module.Manager()
self.assertTrue(manager.incompatible({"codec": "hevc", "bit_depth": 10}))
self.assertTrue(manager.incompatible({"codec": "h264", "bit_depth": 10}))
self.assertFalse(manager.incompatible({"codec": "h264", "bit_depth": 8}))
self.assertFalse(manager.incompatible({"codec": "av1", "bit_depth": 10}))
def test_quality_filters_are_global(self):
manager = self.module.Manager()
sdr = manager.ffmpeg_command(Path("show.mkv"), Path("out.mkv.partial"), {})
hdr = manager.ffmpeg_command(
Path("show.HDR10.mkv"), Path("out.mkv.partial"), {"color_transfer": "smpte2084"}
)
self.assertTrue(any("zscale=dither=error_diffusion,format=yuv420p" in value for value in sdr))
self.assertIn("-filter_complex", sdr)
self.assertIn("[video]", sdr)
self.assertNotIn("-vf", sdr)
self.assertIn("18", sdr)
self.assertTrue(any("tonemap=tonemap=hable" in value for value in hdr))
self.assertIn("bt709", hdr)
def test_probe_detects_dolby_vision_stream_metadata(self):
manager = self.module.Manager()
result = SimpleNamespace(
returncode=0,
stderr="",
stdout=json.dumps({
"streams": [{
"codec_name": "h264",
"profile": "High",
"pix_fmt": "yuv420p",
"bits_per_raw_sample": "8",
"color_transfer": "bt709",
"side_data_list": [{"side_data_type": "DOVI configuration record"}],
}],
"format": {"duration": "10.0"},
}),
)
with mock.patch.object(self.module.subprocess, "run", return_value=result):
details = manager.probe(Path("cached.mkv"))
self.assertTrue(details["dolby_vision"])
def test_validation_rejects_inherited_dolby_vision_metadata(self):
manager = self.module.Manager()
output = self.root / "cache" / "invalid.mkv"
output.write_bytes(b"x" * 1_000_001)
details = {"codec": "h264", "bit_depth": 8, "dolby_vision": True, "duration": 10.0}
with mock.patch.object(manager, "probe", return_value=details):
with self.assertRaisesRegex(RuntimeError, "Dolby Vision"):
manager.validate_output(Path("source.mkv"), output, 10.0)
def test_cache_requires_matching_source_fingerprint(self):
m = self.module
manager = m.Manager()
relative = "movies/Test/Test.mkv"
source = self.root / "source" / relative
source.parent.mkdir(parents=True, exist_ok=True)
source.write_bytes(b"original")
output, manifest = manager.cache_paths(relative)
output.write_bytes(b"cached")
manifest.write_text(json.dumps({
"relative": relative,
"source_fingerprint": m.fingerprint(source),
"encode_profile": m.ENCODE_PROFILE,
}))
self.assertTrue(manager.cache_valid(relative))
source.write_bytes(b"changed")
self.assertFalse(manager.cache_valid(relative))
def test_lru_touches_only_manifest(self):
m = self.module
manager = m.Manager()
relative = "movies/LRU/LRU.mkv"
source = self.root / "source" / relative
source.parent.mkdir(parents=True, exist_ok=True)
source.write_bytes(b"original")
output, manifest = manager.cache_paths(relative)
output.write_bytes(b"cached")
manifest.write_text(json.dumps({
"relative": relative,
"source_fingerprint": m.fingerprint(source),
"encode_profile": m.ENCODE_PROFILE,
}))
os.utime(output, (100, 100))
os.utime(manifest, (100, 100))
manager.touch_cache(relative)
self.assertEqual(output.stat().st_mtime, 100)
self.assertGreater(manifest.stat().st_mtime, 100)
def test_smaller_cache_limit_evicts_oldest_content(self):
m = self.module
manager = m.Manager()
relative = "movies/Resize/Resize.mkv"
source = self.root / "source" / relative
source.parent.mkdir(parents=True, exist_ok=True)
source.write_bytes(b"original")
output, manifest = manager.cache_paths(relative)
output.write_bytes(b"cached rendition")
manifest.write_text(json.dumps({
"relative": relative,
"source_fingerprint": m.fingerprint(source),
"encode_profile": m.ENCODE_PROFILE,
}))
manager.ensure_link(relative, m.cache_target(relative))
old_limit = m.CACHE_MAX_BYTES
try:
m.CACHE_MAX_BYTES = 1
manager.enforce_cache_limit()
finally:
m.CACHE_MAX_BYTES = old_limit
self.assertFalse(output.exists())
self.assertEqual(os.readlink(self.root / "view" / relative), m.source_target(relative))
if __name__ == "__main__":
unittest.main()