Files
alfred/tests/application/tv_shows/test_walker.py
T
2026-05-26 21:45:11 +02:00

243 lines
11 KiB
Python

"""Tests for the show-tree walker."""
from __future__ import annotations
from alfred.application.tv_shows_TO_CHECK.walker import walk_show
from alfred.domain.releases_TO_CHECK.value_objects import ReleaseMode
from alfred.infrastructure.filesystem.scanner import PathlibFilesystemScanner
from alfred.infrastructure.knowledge_TO_CHECK.release_kb import YamlReleaseKnowledge
_KB = YamlReleaseKnowledge()
_SCANNER = PathlibFilesystemScanner()
def _file(dir_path, name: str) -> None:
(dir_path / name).write_bytes(b"")
# ════════════════════════════════════════════════════════════════════════════
# Season-folder detection (by Sxx token in folder name)
# ════════════════════════════════════════════════════════════════════════════
class TestSeasonFolderDetection:
def test_keeps_folders_with_season_token(self, tmp_path):
show = tmp_path / "Foundation"
show.mkdir()
(show / "Foundation.S01.1080p.WEB-DL.x265-GROUP").mkdir()
(show / "Foundation.S02.Complete.1080p.WEB-DL-GROUP").mkdir()
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
names = sorted(s.season_dir.name for s in tree.season_folders)
assert names == [
"Foundation.S01.1080p.WEB-DL.x265-GROUP",
"Foundation.S02.Complete.1080p.WEB-DL-GROUP",
]
def test_skips_folders_without_season_token(self, tmp_path):
show = tmp_path / "Foundation"
show.mkdir()
(show / "Sample").mkdir()
(show / "Soundtrack").mkdir()
(show / "Foundation.S01.WEB").mkdir()
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
assert [s.season_dir.name for s in tree.season_folders] == [
"Foundation.S01.WEB"
]
def test_season_token_is_case_insensitive(self, tmp_path):
show = tmp_path / "Foundation"
show.mkdir()
(show / "Foundation.s01.WEB").mkdir()
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
assert len(tree.season_folders) == 1
def test_season_token_rejects_in_word(self, tmp_path):
# "Season01" without separator must not match (we want a real
# Sxx token, not any letter S followed by digits).
show = tmp_path / "WeirdShow"
show.mkdir()
(show / "Pass100").mkdir()
(show / "ABS01XYZ").mkdir()
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
assert tree.season_folders == ()
def test_ignores_files_at_show_root(self, tmp_path):
show = tmp_path / "Foundation"
show.mkdir()
(show / "Foundation.S01.mkv").write_bytes(b"") # file, not a folder
(show / "Foundation.S01.WEB").mkdir()
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
assert [s.season_dir.name for s in tree.season_folders] == [
"Foundation.S01.WEB"
]
# ════════════════════════════════════════════════════════════════════════════
# PACK classification (flat numbered videos inside season folder)
# ════════════════════════════════════════════════════════════════════════════
class TestPackClassification:
def test_flat_videos_yield_pack(self, tmp_path):
show = tmp_path / "Foundation"
show.mkdir()
season = show / "Foundation.S01.1080p.WEB-DL.x265-GROUP"
season.mkdir()
_file(season, "Foundation.S01E01.1080p.WEB-DL.x265-GROUP.mkv")
_file(season, "Foundation.S01E02.1080p.WEB-DL.x265-GROUP.mkv")
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
(folder,) = tree.season_folders
assert folder.mode is ReleaseMode.PACK
assert [p.name for p in folder.video_files] == [
"Foundation.S01E01.1080p.WEB-DL.x265-GROUP.mkv",
"Foundation.S01E02.1080p.WEB-DL.x265-GROUP.mkv",
]
def test_pack_keeps_only_video_extensions(self, tmp_path):
show = tmp_path / "Foundation"
show.mkdir()
season = show / "Foundation.S01.WEB"
season.mkdir()
_file(season, "Foundation.S01E01.mkv")
_file(season, "Foundation.S01E02.mp4")
_file(season, "Foundation.S01E01.eng.srt") # not a video
_file(season, "info.nfo") # not a video
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
(folder,) = tree.season_folders
assert folder.mode is ReleaseMode.PACK
suffixes = sorted(p.suffix for p in folder.video_files)
assert suffixes == [".mkv", ".mp4"]
# ════════════════════════════════════════════════════════════════════════════
# EPISODIC classification (subfolders, one video each)
# ════════════════════════════════════════════════════════════════════════════
class TestEpisodicClassification:
def test_subfolders_yield_episodic(self, tmp_path):
show = tmp_path / "Foundation"
show.mkdir()
season = show / "Foundation.S02"
season.mkdir()
for n in (1, 2):
sub = season / f"Foundation.S02E{n:02d}.1080p.x265-GROUP"
sub.mkdir()
_file(sub, f"Foundation.S02E{n:02d}.1080p.x265-GROUP.mkv")
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
(folder,) = tree.season_folders
assert folder.mode is ReleaseMode.EPISODIC
assert [p.name for p in folder.video_files] == [
"Foundation.S02E01.1080p.x265-GROUP.mkv",
"Foundation.S02E02.1080p.x265-GROUP.mkv",
]
def test_episodic_paths_descend_into_subfolders(self, tmp_path):
show = tmp_path / "Foundation"
show.mkdir()
season = show / "Foundation.S02"
season.mkdir()
sub = season / "Foundation.S02E01-RG"
sub.mkdir()
_file(sub, "Foundation.S02E01-RG.mkv")
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
(folder,) = tree.season_folders
assert folder.video_files == (sub / "Foundation.S02E01-RG.mkv",)
def test_episodic_skips_empty_subfolder(self, tmp_path, caplog):
show = tmp_path / "Foundation"
show.mkdir()
season = show / "Foundation.S02"
season.mkdir()
(season / "Foundation.S02E01-RG").mkdir() # empty subfolder
good_sub = season / "Foundation.S02E02-RG"
good_sub.mkdir()
_file(good_sub, "Foundation.S02E02-RG.mkv")
with caplog.at_level("WARNING"):
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
(folder,) = tree.season_folders
assert folder.mode is ReleaseMode.EPISODIC
assert len(folder.video_files) == 1
assert any("no video" in r.message for r in caplog.records)
def test_episodic_with_two_videos_in_subfolder_skips_season(
self, tmp_path, caplog
):
show = tmp_path / "Foundation"
show.mkdir()
season = show / "Foundation.S02"
season.mkdir()
sub = season / "Foundation.S02E01-RG"
sub.mkdir()
_file(sub, "Foundation.S02E01-RG.mkv")
_file(sub, "Foundation.S02E01-RG-extras.mkv") # second video!
with caplog.at_level("WARNING"):
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
(folder,) = tree.season_folders
assert folder.mode is None
assert folder.video_files == ()
assert any("malformed" in r.message for r in caplog.records)
# ════════════════════════════════════════════════════════════════════════════
# Malformed and empty seasons
# ════════════════════════════════════════════════════════════════════════════
class TestMalformedAndEmpty:
def test_mixed_flat_and_subfolders_is_malformed(self, tmp_path, caplog):
show = tmp_path / "Foundation"
show.mkdir()
season = show / "Foundation.S01.Mixed"
season.mkdir()
_file(season, "Foundation.S01E01.mkv") # flat video
sub = season / "Foundation.S01E02-RG" # AND a subfolder
sub.mkdir()
_file(sub, "Foundation.S01E02-RG.mkv")
with caplog.at_level("WARNING"):
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
(folder,) = tree.season_folders
assert folder.mode is None
assert folder.video_files == ()
assert any("mixes flat videos and subfolders" in r.message for r in caplog.records)
def test_empty_season_folder_yields_unknown_mode(self, tmp_path):
show = tmp_path / "Foundation"
show.mkdir()
(show / "Foundation.S01.WEB").mkdir()
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
(folder,) = tree.season_folders
assert folder.mode is None
assert folder.video_files == ()
def test_season_with_only_non_video_files_is_unknown(self, tmp_path):
# No videos, no subfolders → unknown. The .nfo doesn't count.
show = tmp_path / "Foundation"
show.mkdir()
season = show / "Foundation.S01.WEB"
season.mkdir()
_file(season, "info.nfo")
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
(folder,) = tree.season_folders
assert folder.mode is None
assert folder.video_files == ()
# ════════════════════════════════════════════════════════════════════════════
# Edge cases on the show root itself
# ════════════════════════════════════════════════════════════════════════════
class TestEdgeCases:
def test_empty_show_root(self, tmp_path):
show = tmp_path / "Empty"
show.mkdir()
tree = walk_show(show, scanner=_SCANNER, kb=_KB)
assert tree.show_root == show
assert tree.season_folders == ()
def test_missing_show_root_returns_empty_tree(self, tmp_path):
# Scanner returns [] for non-existent paths; walker must not raise.
tree = walk_show(tmp_path / "Nope", scanner=_SCANNER, kb=_KB)
assert tree.season_folders == ()