ced72547f7
The domain layer no longer reads YAML files. All knowledge loaders move
from `alfred/domain/*/knowledge/` to `alfred/infrastructure/knowledge/`:
domain/release/knowledge.py
→ infrastructure/knowledge/release.py
domain/shared/knowledge/language_registry.py
→ infrastructure/knowledge/language_registry.py
domain/subtitles/knowledge/{loader,base}.py
→ infrastructure/knowledge/subtitles/{loader,base}.py
Callers in domain/release/{services,value_objects}.py,
domain/subtitles/{aggregates,services/*}.py, and
application/filesystem/manage_subtitles.py updated to absolute imports.
Re-exports of KnowledgeLoader/SubtitleKnowledgeBase from
domain/subtitles/__init__.py dropped (no shim per project convention).
Tests follow the moved targets.
191 lines
7.4 KiB
Python
191 lines
7.4 KiB
Python
"""Tests for ``alfred.domain.subtitles.services.pattern_detector.PatternDetector``.
|
|
|
|
The detector inspects a release folder and returns the best-matching known
|
|
pattern + a confidence score.
|
|
|
|
Coverage:
|
|
|
|
- ``TestEmbeddedDetection`` — ffprobe is mocked; ``embedded`` pattern wins
|
|
when no external subs and ffprobe reports tracks.
|
|
- ``TestAdjacentDetection`` — .srt next to the video → ``adjacent``.
|
|
- ``TestFlatSubsFolder`` — ``Subs/*.srt`` → ``subs_flat``.
|
|
- ``TestEpisodeSubfolder`` — ``Subs/{ep}/*.srt`` → ``episode_subfolder``.
|
|
- ``TestNothingFound`` — empty release returns no pattern.
|
|
- ``TestDescribe`` — human-readable description mentions the right cues.
|
|
|
|
Uses the real ``SubtitleKnowledgeBase`` (loaded from the live builtin
|
|
``patterns/`` folder) since rebuilding all four patterns by hand would
|
|
just duplicate fixture state.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from alfred.infrastructure.knowledge.subtitles.base import SubtitleKnowledgeBase
|
|
from alfred.domain.subtitles.services.pattern_detector import PatternDetector
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def kb():
|
|
return SubtitleKnowledgeBase()
|
|
|
|
|
|
@pytest.fixture
|
|
def detector(kb):
|
|
return PatternDetector(kb)
|
|
|
|
|
|
def _make_video(folder: Path, name: str = "Show.S01E01.mkv") -> Path:
|
|
v = folder / name
|
|
v.write_bytes(b"")
|
|
return v
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Embedded #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestEmbeddedDetection:
|
|
def test_embedded_only(self, detector, tmp_path):
|
|
# Folder has video but no external .srt files anywhere.
|
|
video = _make_video(tmp_path)
|
|
with patch.object(
|
|
PatternDetector, "_has_embedded_subtitles", return_value=True
|
|
):
|
|
result = detector.detect(tmp_path, video)
|
|
assert result["detected"] is not None
|
|
assert result["detected"].id == "embedded"
|
|
assert result["confidence"] > 0
|
|
assert "embedded" in result["description"].lower()
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Adjacent #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestAdjacentDetection:
|
|
def test_srt_next_to_video(self, detector, tmp_path):
|
|
video = _make_video(tmp_path)
|
|
(tmp_path / "Show.S01E01.English.srt").write_text("")
|
|
(tmp_path / "Show.S01E01.French.srt").write_text("")
|
|
with patch.object(
|
|
PatternDetector, "_has_embedded_subtitles", return_value=False
|
|
):
|
|
result = detector.detect(tmp_path, video)
|
|
assert result["detected"] is not None
|
|
assert result["detected"].id == "adjacent"
|
|
assert "adjacent" in result["description"]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Subs flat folder #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestFlatSubsFolder:
|
|
def test_flat_subs_folder_adjacent_to_video(self, detector, tmp_path):
|
|
video = _make_video(tmp_path)
|
|
subs = tmp_path / "Subs"
|
|
subs.mkdir()
|
|
(subs / "Show.S01E01.English.srt").write_text("")
|
|
(subs / "Show.S01E01.French.srt").write_text("")
|
|
with patch.object(
|
|
PatternDetector, "_has_embedded_subtitles", return_value=False
|
|
):
|
|
result = detector.detect(tmp_path, video)
|
|
assert result["detected"] is not None
|
|
assert result["detected"].id == "subs_flat"
|
|
assert "flat" in result["description"]
|
|
|
|
def test_flat_subs_folder_at_release_root(self, detector, tmp_path):
|
|
# Sample video lives one level deep; Subs/ is at the release root.
|
|
season_dir = tmp_path / "Season.01"
|
|
season_dir.mkdir()
|
|
video = _make_video(season_dir)
|
|
subs = tmp_path / "Subs"
|
|
subs.mkdir()
|
|
(subs / "ep01.English.srt").write_text("")
|
|
with patch.object(
|
|
PatternDetector, "_has_embedded_subtitles", return_value=False
|
|
):
|
|
result = detector.detect(tmp_path, video)
|
|
assert result["detected"] is not None
|
|
assert result["detected"].id == "subs_flat"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Episode subfolder #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestEpisodeSubfolder:
|
|
def test_per_episode_subfolder(self, detector, tmp_path):
|
|
video = _make_video(tmp_path, name="Show.S01E01.mkv")
|
|
subs = tmp_path / "Subs" / "Show.S01E01"
|
|
subs.mkdir(parents=True)
|
|
(subs / "2_English.srt").write_text("")
|
|
(subs / "3_French.srt").write_text("")
|
|
with patch.object(
|
|
PatternDetector, "_has_embedded_subtitles", return_value=False
|
|
):
|
|
result = detector.detect(tmp_path, video)
|
|
assert result["detected"] is not None
|
|
assert result["detected"].id == "episode_subfolder"
|
|
desc = result["description"]
|
|
assert "episode_subfolder" in desc
|
|
# Numeric-prefix cue should be reported.
|
|
assert "numeric prefix" in desc
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Nothing #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestNothingFound:
|
|
def test_empty_release_no_pattern(self, detector, tmp_path):
|
|
video = _make_video(tmp_path)
|
|
with patch.object(
|
|
PatternDetector, "_has_embedded_subtitles", return_value=False
|
|
):
|
|
result = detector.detect(tmp_path, video)
|
|
# No external subs and no embedded → adjacent strategy still scores
|
|
# 0.5 (no Subs folder bonus). Best pattern may exist or not depending
|
|
# on threshold (0.4). Either way the description must reflect emptiness.
|
|
assert "no external subtitle files" in result["description"]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Describe #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestDescribe:
|
|
def test_describe_includes_language_token_cue(self, detector, tmp_path):
|
|
video = _make_video(tmp_path)
|
|
subs = tmp_path / "Subs"
|
|
subs.mkdir()
|
|
(subs / "ep01.English.srt").write_text("")
|
|
with patch.object(
|
|
PatternDetector, "_has_embedded_subtitles", return_value=False
|
|
):
|
|
result = detector.detect(tmp_path, video)
|
|
assert "language tokens" in result["description"]
|
|
|
|
def test_describe_combines_external_and_embedded(self, detector, tmp_path):
|
|
video = _make_video(tmp_path)
|
|
(tmp_path / "Show.S01E01.English.srt").write_text("")
|
|
with patch.object(
|
|
PatternDetector, "_has_embedded_subtitles", return_value=True
|
|
):
|
|
result = detector.detect(tmp_path, video)
|
|
desc = result["description"]
|
|
assert "adjacent" in desc
|
|
assert "embedded" in desc.lower()
|