243 lines
11 KiB
Python
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 == ()
|