Files
siftlode/backend/tests/test_download_deprioritize.py
peter 5561d9a379 feat(downloads): every ready file is phone-safe — uploads and clips too
A share link is opened on whatever the friend holds. Measured on an iPhone and an iPad: AV1,
Matroska, HEVC tagged hev1 and MP3 inside an .mp4 fail there; Chrome and Firefox add HEVC and
10-bit / 4:4:4 H.264. The old check judged the codec NAME only and ran on downloads only, so a
10-bit H.264 passed, an upload went out exactly as it came (an iPhone's HEVC .mov, a webm, an mkv)
and a fast trim kept its source's codecs.

One rule now, `downloads/playable.py`: video is H.264 8-bit 4:2:0 + AAC in .mp4, audio-only is
.mp3 or AAC .m4a; `plan` says what must change, `build_cmd` re-encodes only the failing stream (or
remuxes). The worker applies it to a download, an editor clip and — new — an upload: media that
fails the rule is queued (asset pending, job queued) and the worker converts it, keeping only the
converted file (owner's choice). Until then the original sits at rel_path, so Retry starts from it
and a delete removes it; an upload that lost its last holder mid-conversion is reaped by the GC.
Every libx264 run (edit, concat, conversion) now pins `-pix_fmt yuv420p` — without it a 10-bit
source came out High 10. The download cache signature moves to v5 so a cached 10-bit asset
re-derives. The upload toast says when the file is still converting (HU/EN).

Tests: the plan for every measured class; real ffmpeg turning a 10-bit Matroska + Opus and an
mp3-in-mp4 into files the rule accepts (red without the pixel-format pin); the queued upload's
delete, cancel-then-GC, failed conversion and real worker conversion (the cleanup tests red
without their rules); the toast (red without the branch). Live on localdev: 9 fixture uploads
converted, every served file probed H.264 High yuv420p + AAC, or mp3 / m4a.
2026-10-05 03:16:27 +02:00

104 lines
4.4 KiB
Python

"""Worker-child resource de-prioritization: the encoder thread cap, the `-threads` flag on the
browser-safe re-encode, and the child's startup renice/ionice/OMP handicap. These keep a CPU-heavy
transcode from pegging every core on the box it shares with the API, Postgres, and Plex playback."""
import subprocess
import pytest
from app.config import settings
from app.downloads import edit as editmod
from app.downloads import playable
from app.downloads import runner
class TestEncodeThreadCap:
def test_pinned_setting_wins(self, monkeypatch):
monkeypatch.setattr(settings, "download_ffmpeg_threads", 3)
assert editmod.encode_thread_cap() == 3
def test_zero_reserves_one_core(self, monkeypatch):
monkeypatch.setattr(settings, "download_ffmpeg_threads", 0)
monkeypatch.setattr(editmod.os, "cpu_count", lambda: 8)
assert editmod.encode_thread_cap() == 7
def test_single_core_box_never_caps_below_one(self, monkeypatch):
monkeypatch.setattr(settings, "download_ffmpeg_threads", 0)
monkeypatch.setattr(editmod.os, "cpu_count", lambda: 1)
assert editmod.encode_thread_cap() == 1
def test_unknown_cpu_count_falls_back(self, monkeypatch):
monkeypatch.setattr(settings, "download_ffmpeg_threads", 0)
monkeypatch.setattr(editmod.os, "cpu_count", lambda: None)
assert editmod.encode_thread_cap() == 1 # max(1, 2 - 1)
_REENCODE_VIDEO = playable.Plan("mp4", True, True, False, True)
class TestPhoneSafeThreads:
def test_threads_flag_added_when_capped(self):
cmd = playable.build_cmd("/in.mkv", "/out.mp4", _REENCODE_VIDEO, threads=2)
assert "-threads" in cmd
assert cmd[cmd.index("-threads") + 1] == "2"
# The cap precedes the output file, so it binds the encoder (not input decoding).
assert cmd.index("-threads") < cmd.index("/out.mp4")
@pytest.mark.parametrize("threads", [0, None])
def test_no_threads_flag_when_uncapped(self, threads):
cmd = playable.build_cmd("/in.mkv", "/out.mp4", _REENCODE_VIDEO, threads=threads)
assert "-threads" not in cmd
class TestDeprioritize:
def test_renices_by_configured_increment(self, monkeypatch):
monkeypatch.setattr(settings, "download_child_nice", 12)
monkeypatch.setattr(settings, "download_child_ionice_class", 0) # skip ionice branch
calls = []
monkeypatch.setattr(runner.os, "nice", lambda n: calls.append(n))
monkeypatch.setattr(runner.os, "environ", {})
runner._deprioritize()
assert calls == [12]
def test_nice_zero_does_not_renice(self, monkeypatch):
monkeypatch.setattr(settings, "download_child_nice", 0)
monkeypatch.setattr(settings, "download_child_ionice_class", 0)
calls = []
monkeypatch.setattr(runner.os, "nice", lambda n: calls.append(n))
monkeypatch.setattr(runner.os, "environ", {})
runner._deprioritize()
assert calls == []
def test_sets_omp_budget_to_thread_cap(self, monkeypatch):
monkeypatch.setattr(settings, "download_child_nice", 0)
monkeypatch.setattr(settings, "download_child_ionice_class", 0)
monkeypatch.setattr(settings, "download_ffmpeg_threads", 4)
env: dict = {}
monkeypatch.setattr(runner.os, "environ", env)
runner._deprioritize()
assert env["OMP_NUM_THREADS"] == "4"
def test_ionice_invoked_when_binary_present(self, monkeypatch):
monkeypatch.setattr(settings, "download_child_nice", 0)
monkeypatch.setattr(settings, "download_child_ionice_class", 3)
monkeypatch.setattr(runner.os, "environ", {})
monkeypatch.setattr(runner.shutil, "which", lambda name: "/usr/bin/ionice")
ran = []
monkeypatch.setattr(
runner.subprocess,
"run",
lambda cmd, **kw: ran.append(cmd) or subprocess.CompletedProcess(cmd, 0),
)
runner._deprioritize()
assert ran and ran[0][:3] == ["ionice", "-c", "3"]
def test_ionice_skipped_when_binary_absent(self, monkeypatch):
monkeypatch.setattr(settings, "download_child_nice", 0)
monkeypatch.setattr(settings, "download_child_ionice_class", 3)
monkeypatch.setattr(runner.os, "environ", {})
monkeypatch.setattr(runner.shutil, "which", lambda name: None)
ran = []
monkeypatch.setattr(runner.subprocess, "run", lambda cmd, **kw: ran.append(cmd))
runner._deprioritize()
assert ran == []