0246f85ef8
The three module-level dicts in enrich_from_probe (ffprobe codec name to scene token, channel count to layout) were exactly the kind of domain lookup table CLAUDE.md says belongs in YAML, not in Python. Move them to alfred/knowledge/release/probe_mappings.yaml, load through a new ReleaseKnowledge.probe_mappings port field, and add a kb parameter to enrich_from_probe so the consumer reads the maps via the same injection pattern as everything else. - New knowledge file: alfred/knowledge/release/probe_mappings.yaml - New loader: load_probe_mappings() in infrastructure/knowledge/release.py (normalizes channel-count keys back to int). - Port: ReleaseKnowledge gains probe_mappings: dict. - Adapter: YamlReleaseKnowledge populates it at __init__. - Consumer: enrich_from_probe(parsed, info, kb) reads the three sub-maps from kb.probe_mappings; unknown codecs still fall back to uppercase raw value, same behaviour as before. - Call sites updated: inspect_release passes kb through; the testing script gets its kb wiring (it was already broken since the ReleaseKnowledge refactor); all 22 enrich_from_probe call sites in tests/application/test_enrich_from_probe.py pass _KB.
255 lines
8.9 KiB
Python
255 lines
8.9 KiB
Python
"""Tests for ``alfred.application.release.enrich_from_probe``.
|
|
|
|
The function mutates a ``ParsedRelease`` in place using ffprobe ``MediaInfo``.
|
|
Token-level values from the release name always win — only ``None`` fields
|
|
are filled.
|
|
|
|
Coverage:
|
|
|
|
- ``TestQuality`` — resolution fill-in (and no-overwrite).
|
|
- ``TestVideoCodec`` — codec map (hevc→x265, …) + uppercase fallback.
|
|
- ``TestAudio`` — default track preferred over first; codec & channel maps
|
|
with unknown-value fallbacks.
|
|
- ``TestLanguages`` — append-only merge; ``und`` skipped; case-insensitive
|
|
duplicate suppression.
|
|
|
|
Uses real ``ParsedRelease`` / ``MediaInfo`` instances — no mocking needed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from alfred.application.release.enrich_from_probe import enrich_from_probe
|
|
from alfred.domain.release.value_objects import ParsedRelease
|
|
from alfred.domain.shared.media import AudioTrack, MediaInfo, VideoTrack
|
|
from alfred.infrastructure.knowledge.release_kb import YamlReleaseKnowledge
|
|
|
|
_KB = YamlReleaseKnowledge()
|
|
|
|
|
|
def _info_with_video(*, width=None, height=None, codec=None, **rest) -> MediaInfo:
|
|
"""Helper: build a MediaInfo with a single video track (the common case)."""
|
|
return MediaInfo(
|
|
video_tracks=[VideoTrack(index=0, codec=codec, width=width, height=height)],
|
|
**rest,
|
|
)
|
|
|
|
|
|
def _bare(**overrides) -> ParsedRelease:
|
|
"""Build a minimal ParsedRelease with all enrichable fields = None."""
|
|
defaults = dict(
|
|
raw="X",
|
|
clean="X",
|
|
title="X",
|
|
title_sanitized="X",
|
|
year=None,
|
|
season=None,
|
|
episode=None,
|
|
episode_end=None,
|
|
quality=None,
|
|
source=None,
|
|
codec=None,
|
|
group="UNKNOWN",
|
|
)
|
|
defaults.update(overrides)
|
|
return ParsedRelease(**defaults)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Quality / resolution #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestQuality:
|
|
def test_fills_when_none(self):
|
|
p = _bare()
|
|
enrich_from_probe(p, _info_with_video(width=1920, height=1080), _KB)
|
|
assert p.quality == "1080p"
|
|
|
|
def test_does_not_overwrite_existing(self):
|
|
p = _bare(quality="2160p")
|
|
enrich_from_probe(p, _info_with_video(width=1920, height=1080), _KB)
|
|
assert p.quality == "2160p"
|
|
|
|
def test_no_dims_leaves_none(self):
|
|
p = _bare()
|
|
enrich_from_probe(p, MediaInfo(), _KB)
|
|
assert p.quality is None
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Video codec #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestVideoCodec:
|
|
def test_hevc_to_x265(self):
|
|
p = _bare()
|
|
enrich_from_probe(p, _info_with_video(codec="hevc"), _KB)
|
|
assert p.codec == "x265"
|
|
|
|
def test_h264_to_x264(self):
|
|
p = _bare()
|
|
enrich_from_probe(p, _info_with_video(codec="h264"), _KB)
|
|
assert p.codec == "x264"
|
|
|
|
def test_unknown_codec_uppercased(self):
|
|
p = _bare()
|
|
enrich_from_probe(p, _info_with_video(codec="weird"), _KB)
|
|
assert p.codec == "WEIRD"
|
|
|
|
def test_does_not_overwrite_existing(self):
|
|
p = _bare(codec="HEVC")
|
|
enrich_from_probe(p, _info_with_video(codec="h264"), _KB)
|
|
assert p.codec == "HEVC"
|
|
|
|
def test_no_codec_leaves_none(self):
|
|
p = _bare()
|
|
enrich_from_probe(p, MediaInfo(), _KB)
|
|
assert p.codec is None
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Audio #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestAudio:
|
|
def test_uses_default_track(self):
|
|
info = MediaInfo(
|
|
audio_tracks=[
|
|
AudioTrack(0, "aac", 2, "stereo", "eng", is_default=False),
|
|
AudioTrack(1, "eac3", 6, "5.1", "eng", is_default=True),
|
|
]
|
|
)
|
|
p = _bare()
|
|
enrich_from_probe(p, info, _KB)
|
|
assert p.audio_codec == "EAC3"
|
|
assert p.audio_channels == "5.1"
|
|
|
|
def test_falls_back_to_first_track_when_no_default(self):
|
|
info = MediaInfo(
|
|
audio_tracks=[
|
|
AudioTrack(0, "ac3", 6, "5.1", "eng"),
|
|
AudioTrack(1, "aac", 2, "stereo", "fre"),
|
|
]
|
|
)
|
|
p = _bare()
|
|
enrich_from_probe(p, info, _KB)
|
|
assert p.audio_codec == "AC3"
|
|
assert p.audio_channels == "5.1"
|
|
|
|
def test_channel_count_unknown_falls_back(self):
|
|
info = MediaInfo(audio_tracks=[AudioTrack(0, "aac", 4, "quad", "eng")])
|
|
p = _bare()
|
|
enrich_from_probe(p, info, _KB)
|
|
assert p.audio_channels == "4ch"
|
|
|
|
def test_unknown_audio_codec_uppercased(self):
|
|
info = MediaInfo(audio_tracks=[AudioTrack(0, "newcodec", 2, "stereo", "eng")])
|
|
p = _bare()
|
|
enrich_from_probe(p, info, _KB)
|
|
assert p.audio_codec == "NEWCODEC"
|
|
|
|
def test_no_audio_tracks(self):
|
|
p = _bare()
|
|
enrich_from_probe(p, MediaInfo(), _KB)
|
|
assert p.audio_codec is None
|
|
assert p.audio_channels is None
|
|
|
|
def test_does_not_overwrite_existing_audio_fields(self):
|
|
info = MediaInfo(audio_tracks=[AudioTrack(0, "ac3", 6, "5.1", "eng")])
|
|
p = _bare(audio_codec="DTS-HD.MA", audio_channels="7.1")
|
|
enrich_from_probe(p, info, _KB)
|
|
assert p.audio_codec == "DTS-HD.MA"
|
|
assert p.audio_channels == "7.1"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Languages #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestLanguages:
|
|
def test_appends_new(self):
|
|
info = MediaInfo(
|
|
audio_tracks=[
|
|
AudioTrack(0, "aac", 2, "stereo", "eng"),
|
|
AudioTrack(1, "aac", 2, "stereo", "fre"),
|
|
]
|
|
)
|
|
p = _bare()
|
|
enrich_from_probe(p, info, _KB)
|
|
assert p.languages == ["eng", "fre"]
|
|
|
|
def test_skips_und(self):
|
|
info = MediaInfo(
|
|
audio_tracks=[
|
|
AudioTrack(0, "aac", 2, "stereo", "und"),
|
|
AudioTrack(1, "aac", 2, "stereo", "eng"),
|
|
]
|
|
)
|
|
p = _bare()
|
|
enrich_from_probe(p, info, _KB)
|
|
assert p.languages == ["eng"]
|
|
|
|
def test_dedup_against_existing_case_insensitive(self):
|
|
# existing token-level languages are typically upper-case ("FRENCH", "ENG")
|
|
# The current logic compares track.lang.upper() against existing —
|
|
# so a track with "eng" is suppressed if "ENG" is already in languages.
|
|
info = MediaInfo(
|
|
audio_tracks=[
|
|
AudioTrack(0, "aac", 2, "stereo", "eng"),
|
|
AudioTrack(1, "aac", 2, "stereo", "fre"),
|
|
]
|
|
)
|
|
p = _bare()
|
|
p.languages = ["ENG"]
|
|
enrich_from_probe(p, info, _KB)
|
|
# "eng" → upper "ENG" already present → skipped. "fre" → "FRE" new → kept.
|
|
assert p.languages == ["ENG", "fre"]
|
|
|
|
def test_no_audio_tracks_leaves_languages_empty(self):
|
|
p = _bare()
|
|
enrich_from_probe(p, MediaInfo(), _KB)
|
|
assert p.languages == []
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# tech_string #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestTechString:
|
|
"""tech_string is a derived property on ParsedRelease: it always
|
|
reflects the current quality/source/codec. Enrichment never writes
|
|
it directly — it stays in sync by construction."""
|
|
|
|
def test_rebuilt_from_filled_quality_and_codec(self):
|
|
p = _bare()
|
|
enrich_from_probe(
|
|
p, _info_with_video(width=1920, height=1080, codec="hevc"), _KB
|
|
)
|
|
assert p.quality == "1080p"
|
|
assert p.codec == "x265"
|
|
assert p.tech_string == "1080p.x265"
|
|
|
|
def test_keeps_existing_source_when_enriching(self):
|
|
# Token-level source must stay; probe fills only None fields.
|
|
p = _bare(source="BluRay")
|
|
enrich_from_probe(
|
|
p, _info_with_video(width=1920, height=1080, codec="hevc"), _KB
|
|
)
|
|
assert p.tech_string == "1080p.BluRay.x265"
|
|
|
|
def test_unchanged_when_no_enrichable_video_info(self):
|
|
# No video info → nothing to fill → derived tech_string stays as it was.
|
|
p = _bare(quality="2160p", source="WEB-DL", codec="x265")
|
|
assert p.tech_string == "2160p.WEB-DL.x265"
|
|
enrich_from_probe(p, MediaInfo(), _KB)
|
|
assert p.tech_string == "2160p.WEB-DL.x265"
|
|
|
|
def test_empty_when_nothing_known(self):
|
|
p = _bare()
|
|
enrich_from_probe(p, MediaInfo(), _KB)
|
|
assert p.tech_string == ""
|