merge main (0.78.1) into tonemap before release
This commit is contained in:
@@ -144,7 +144,7 @@ class TestCodecsFromStreams:
|
||||
|
||||
|
||||
class TestRunFfprobeJson:
|
||||
"""The single ffprobe-JSON invocation point (shared by probe_codecs + upload._probe). A failed
|
||||
"""The single ffprobe-JSON invocation point (shared by probe_codecs + probe.probe_media). A failed
|
||||
probe or unparseable output must degrade to None, not raise — the malformed-input safety the old
|
||||
parse_probe_streams string parser used to own now lives here."""
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ guards that keep an upload out of paths that assume a re-fetchable source (edit
|
||||
|
||||
DB-backed (real Postgres) — skipped when no database is reachable. `settings.download_root` is
|
||||
pointed at a per-test tmp dir so ingest's on-disk moves are isolated. The ingest/route tests
|
||||
monkeypatch `_probe` so they don't shell out to ffprobe; the classifier's OWN parsing (image →
|
||||
file, cover-art skip, dims, duration) is covered directly by `TestProbeClassifier` below, which
|
||||
stubs one level down (`run_ffprobe_json`) and exercises the real `_probe`.
|
||||
monkeypatch the shared `probe.probe_media` so they don't shell out to ffprobe; the classifier's OWN
|
||||
parsing (image → file, cover-art skip, dims, duration) is covered directly by `TestProbeClassifier`
|
||||
below, which stubs one level down (`run_ffprobe_json`) and exercises the real `probe_media`.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
@@ -18,6 +18,7 @@ import pytest
|
||||
|
||||
from app.config import settings
|
||||
from app.downloads import mediakind, quota, storage
|
||||
from app.downloads import probe as probemod
|
||||
from app.downloads import upload as uploadmod
|
||||
from app.models import DownloadQuota, MediaAsset
|
||||
|
||||
@@ -42,7 +43,9 @@ def _root(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def _probe(monkeypatch, result):
|
||||
monkeypatch.setattr(uploadmod, "_probe", lambda _p: result)
|
||||
# upload ingest now calls the shared `probe.probe_media` (same ground-truth ffprobe the download
|
||||
# worker uses); patch it at that shared home.
|
||||
monkeypatch.setattr(probemod, "probe_media", lambda _p: result)
|
||||
|
||||
|
||||
def test_ingest_generic_file_is_download_only(db, make_user, _root, monkeypatch):
|
||||
@@ -325,16 +328,16 @@ def test_ingest_cleans_up_placed_files_on_commit_failure(db, make_user, _root, m
|
||||
assert db.query(MediaAsset).count() == 0 # the flushed asset rolled back — no dangling row
|
||||
|
||||
|
||||
# --- classifier: the REAL _probe parsing (stubs ffprobe one level down) ----------------------
|
||||
# The ingest/route tests above stub _probe wholesale; these exercise _probe's OWN logic — image
|
||||
# --- classifier: the REAL probe_media parsing (stubs ffprobe one level down) ------------------
|
||||
# The ingest/route tests above stub probe_media wholesale; these exercise its OWN logic — image
|
||||
# detection, the cover-art skip, dimension/duration parsing — by faking only run_ffprobe_json, so a
|
||||
# regression in the classifier the force-download guarantee leans on is actually caught.
|
||||
|
||||
|
||||
class TestProbeClassifier:
|
||||
def _probe_with(self, monkeypatch, data):
|
||||
monkeypatch.setattr(uploadmod.editmod, "run_ffprobe_json", lambda *a, **k: data)
|
||||
return uploadmod._probe(Path("x"))
|
||||
monkeypatch.setattr(probemod.editmod, "run_ffprobe_json", lambda *a, **k: data)
|
||||
return probemod.probe_media(Path("x"))
|
||||
|
||||
def test_probe_failure_is_a_generic_file(self, monkeypatch):
|
||||
# run_ffprobe_json returns None (ffprobe failed / not media) → all None → derive() == FILE.
|
||||
@@ -392,3 +395,109 @@ class TestProbeClassifier:
|
||||
}
|
||||
v, a, w, h, d = self._probe_with(monkeypatch, data)
|
||||
assert d is None and mediakind.derive(v, a) == mediakind.AUDIO
|
||||
|
||||
|
||||
# --- resolve_media_facts: probe-of-the-file wins over yt-dlp's info (the FB regression fix) ---------
|
||||
# The download worker's finalize must classify from the ACTUAL file, not the extractor's `info` dict.
|
||||
# yt-dlp omits vcodec/acodec/dims for some sources (Facebook), and deriving media_kind from those
|
||||
# nulls turned a real H.264 video into a download-only 'file' (no in-app play). These pin that the
|
||||
# probe is authoritative and `info` is only a gap-filler.
|
||||
|
||||
|
||||
class TestResolveMediaFacts:
|
||||
# These stub run_ffprobe_json (one level below probe_media) so the REAL probe parsing + the
|
||||
# probe-vs-info precedence are exercised together — the precedence keys off whether ffprobe RAN.
|
||||
@staticmethod
|
||||
def _ffprobe(monkeypatch, data):
|
||||
monkeypatch.setattr(probemod.editmod, "run_ffprobe_json", lambda *a, **k: data)
|
||||
|
||||
def test_probe_wins_when_info_lacks_codecs(self, monkeypatch):
|
||||
# THE original regression: info has no vcodec/acodec/dims (Facebook), but the file probes as
|
||||
# h264/aac.
|
||||
self._ffprobe(
|
||||
monkeypatch,
|
||||
{
|
||||
"format": {"format_name": "mov,mp4,m4a"},
|
||||
"streams": [
|
||||
{"codec_type": "video", "codec_name": "h264", "width": 720, "height": 1280},
|
||||
{"codec_type": "audio", "codec_name": "aac"},
|
||||
],
|
||||
},
|
||||
)
|
||||
v, a, w, h = probemod.resolve_media_facts(Path("x"), {"width": None, "height": None})
|
||||
assert (v, a, w, h) == ("h264", "aac", 720, 1280)
|
||||
assert mediakind.derive(v, a) == mediakind.VIDEO # NOT 'file'
|
||||
|
||||
def test_probe_wins_over_a_DIFFERING_populated_info(self, monkeypatch):
|
||||
# Both the probe AND info are populated but DISAGREE (info carries a pre-remux/pre-merge codec).
|
||||
# The finished file on disk is ground truth, so the probe wins outright — pins the precedence a
|
||||
# `info or probe` inversion would silently break.
|
||||
self._ffprobe(
|
||||
monkeypatch,
|
||||
{
|
||||
"format": {"format_name": "mov,mp4,m4a"},
|
||||
"streams": [
|
||||
{"codec_type": "video", "codec_name": "h264", "width": 1920, "height": 1080},
|
||||
{"codec_type": "audio", "codec_name": "aac"},
|
||||
],
|
||||
},
|
||||
)
|
||||
v, a, w, h = probemod.resolve_media_facts(
|
||||
Path("x"), {"vcodec": "vp9", "acodec": "opus", "width": 640, "height": 480}
|
||||
)
|
||||
assert (v, a, w, h) == ("h264", "aac", 1920, 1080) # probe, not info
|
||||
|
||||
def test_cover_art_audio_download_is_not_resurrected_to_video_by_info(self, monkeypatch):
|
||||
# THE bug this fix targets: an audio-mode download from a progressive source embeds cover art,
|
||||
# and yt-dlp's info still carries the source's video codec. The probe correctly finds only audio
|
||||
# (the attached_pic is skipped); info's vcodec must NOT resurrect a video track → media_kind
|
||||
# stays audio, not a broken in-app 'video'.
|
||||
self._ffprobe(
|
||||
monkeypatch,
|
||||
{
|
||||
"format": {"format_name": "mp3", "duration": "180.0"},
|
||||
"streams": [
|
||||
{
|
||||
"codec_type": "video",
|
||||
"codec_name": "mjpeg",
|
||||
"disposition": {"attached_pic": 1},
|
||||
},
|
||||
{"codec_type": "audio", "codec_name": "mp3"},
|
||||
],
|
||||
},
|
||||
)
|
||||
v, a, w, h = probemod.resolve_media_facts(
|
||||
Path("x"), {"vcodec": "avc1", "acodec": "mp4a", "width": 1280, "height": 720}
|
||||
)
|
||||
assert v is None and a == "mp3" # info's avc1 did NOT win
|
||||
assert mediakind.derive(v, a) == mediakind.AUDIO # NOT VIDEO
|
||||
|
||||
def test_still_image_download_is_not_resurrected_to_video_by_info(self, monkeypatch):
|
||||
# A downloaded still image: the probe returns no vcodec (image-format guard). info's image
|
||||
# "vcodec" must not turn it into a broken 'video' → stays a generic file.
|
||||
self._ffprobe(
|
||||
monkeypatch,
|
||||
{
|
||||
"format": {"format_name": "png_pipe"},
|
||||
"streams": [{"codec_type": "video", "codec_name": "png", "width": 8, "height": 8}],
|
||||
},
|
||||
)
|
||||
v, a, w, h = probemod.resolve_media_facts(
|
||||
Path("x"), {"vcodec": "png", "width": 8, "height": 8}
|
||||
)
|
||||
assert v is None and mediakind.derive(v, a) == mediakind.FILE
|
||||
|
||||
def test_info_fills_in_only_when_ffprobe_could_not_read_the_file(self, monkeypatch):
|
||||
# ffprobe failed entirely (None) → fall back to info so we never regress the working path.
|
||||
self._ffprobe(monkeypatch, None)
|
||||
v, a, w, h = probemod.resolve_media_facts(
|
||||
Path("x"), {"vcodec": "vp9", "acodec": "opus", "width": 1920, "height": 1080}
|
||||
)
|
||||
assert (v, a, w, h) == ("vp9", "opus", 1920, 1080)
|
||||
|
||||
def test_info_none_codec_string_is_treated_as_absent(self, monkeypatch):
|
||||
# yt-dlp writes the literal "none" for a missing stream — it must not become a codec name.
|
||||
self._ffprobe(monkeypatch, None) # probe failed → info path, where the "none" scrub applies
|
||||
v, a, w, h = probemod.resolve_media_facts(Path("x"), {"vcodec": "none", "acodec": "aac"})
|
||||
assert (v, a) == (None, "aac")
|
||||
assert mediakind.derive(v, a) == mediakind.AUDIO
|
||||
|
||||
Reference in New Issue
Block a user