- 176 unit tests covering ConfigManager, FileClassifier, EpisodeRenamer, TVDBClient, MockTVDBClient, TVDBCache, and episode_matcher CLI - Tests organized in tests/ with conftest.py shared fixtures - pytest-cov configured with 90% coverage threshold in pyproject.toml - CI workflow updated with COVERAGE_CORE=sysmon for Python 3.13 compat - .gitignore updated for coverage artifacts and venv
540 lines
21 KiB
Python
540 lines
21 KiB
Python
"""Tests for src/Matcher/episode_renamer.py - EpisodeRenamer."""
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
import pytest
|
|
from src.Matcher.episode_renamer import EpisodeRenamer
|
|
|
|
|
|
class TestEpisodeRenamerInit:
|
|
def test_init_defaults(self):
|
|
r = EpisodeRenamer("/tmp/test", "Show Name", 1)
|
|
assert r.show_name == "Show Name"
|
|
assert r.season_number == 1
|
|
assert r.disc_mapping is None
|
|
assert r.auto_delete_duplicates is False
|
|
|
|
def test_init_with_disc_mapping(self):
|
|
r = EpisodeRenamer("/tmp/test", "Show", 2, disc_mapping={1: [1, 2], 2: [3, 4]})
|
|
assert r.disc_mapping == {1: [1, 2], 2: [3, 4]}
|
|
|
|
def test_init_auto_delete(self):
|
|
r = EpisodeRenamer("/tmp/test", "Show", 1, auto_delete_duplicates=True)
|
|
assert r.auto_delete_duplicates is True
|
|
|
|
def test_init_folder_paths(self):
|
|
r = EpisodeRenamer("/tmp/test", "Show", 1)
|
|
assert r.extras_folder == Path("/tmp/test/extras")
|
|
assert r.delete_folder == Path("/tmp/test/delete me")
|
|
|
|
|
|
class TestSanitizeFilename:
|
|
def test_remove_invalid_chars(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
result = r._sanitize_filename('a<b>c:d"e/f\\g|h?i*j')
|
|
assert ":" not in result
|
|
assert "<" not in result
|
|
assert ">" not in result
|
|
assert '"' not in result
|
|
assert "/" not in result
|
|
assert "\\" not in result
|
|
assert "|" not in result
|
|
assert "?" not in result
|
|
assert "*" not in result
|
|
assert "a" in result and "b" in result and "c" in result
|
|
|
|
def test_collapse_spaces(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
assert r._sanitize_filename("a b") == "a b"
|
|
|
|
def test_strip(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
assert r._sanitize_filename(" hello ") == "hello"
|
|
|
|
def test_preserve_valid_chars(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
assert r._sanitize_filename("hello_world-1.mkv") == "hello_world-1.mkv"
|
|
|
|
|
|
class TestGenerateEpisodeFilename:
|
|
def test_standard(self):
|
|
r = EpisodeRenamer("/tmp/t", "Show Name", 3)
|
|
assert r._generate_episode_filename(5, ".mkv") == "Show Name s03e05.mkv"
|
|
|
|
def test_single_digit_padding(self):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
assert r._generate_episode_filename(1, ".mp4") == "Show s01e01.mp4"
|
|
|
|
def test_double_digit_episode(self):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 10)
|
|
assert r._generate_episode_filename(12, ".mkv") == "Show s10e12.mkv"
|
|
|
|
def test_sanitizes_output(self):
|
|
r = EpisodeRenamer("/tmp/t", "Show:Name", 1)
|
|
assert r._generate_episode_filename(1, ".mkv") == "ShowName s01e01.mkv"
|
|
|
|
|
|
class TestExtractDiscInfo:
|
|
def test_disc_with_underscore(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
info = r._extract_disc_info("Disc 1_t01.mkv")
|
|
assert info["disc_number"] == 1
|
|
assert info["track_number"] == 1
|
|
|
|
def test_disk_variant(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
info = r._extract_disc_info("Disk2_t03.mkv")
|
|
assert info["disc_number"] == 2
|
|
assert info["track_number"] == 3
|
|
|
|
def test_disc_hyphen(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
info = r._extract_disc_info("disc-3_track05.mkv")
|
|
assert info["disc_number"] == 3
|
|
assert info["track_number"] == 5
|
|
|
|
def test_no_disc_info(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
info = r._extract_disc_info("episode_01.mkv")
|
|
assert info["disc_number"] is None
|
|
assert info["track_number"] is None
|
|
|
|
def test_track_pattern(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
info = r._extract_disc_info("file_t22.mkv")
|
|
assert info["track_number"] == 22
|
|
|
|
def test_disc_only(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
info = r._extract_disc_info("Disc 5.mkv")
|
|
assert info["disc_number"] == 5
|
|
assert info["track_number"] is None
|
|
|
|
|
|
class TestComputeFileHash:
|
|
def test_hash_consistent(self, tmp_path):
|
|
f = tmp_path / "test.mkv"
|
|
f.write_bytes(b"\x00" * 1000)
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
h1 = r._compute_file_hash(f)
|
|
h2 = r._compute_file_hash(f)
|
|
assert h1 == h2
|
|
assert len(h1) == 64
|
|
|
|
def test_hash_different_content(self, tmp_path):
|
|
f1 = tmp_path / "a.mkv"
|
|
f2 = tmp_path / "b.mkv"
|
|
f1.write_bytes(b"\x00" * 100)
|
|
f2.write_bytes(b"\xff" * 100)
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
assert r._compute_file_hash(f1) != r._compute_file_hash(f2)
|
|
|
|
def test_hash_custom_bytes(self, tmp_path):
|
|
f = tmp_path / "test.mkv"
|
|
f.write_bytes(b"\x00" * 10000)
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
h = r._compute_file_hash(f, num_bytes=512)
|
|
assert h
|
|
|
|
def test_hash_missing_file(self, tmp_path):
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
h = r._compute_file_hash(tmp_path / "nope.mkv")
|
|
assert h == ""
|
|
|
|
|
|
class TestCreateFolders:
|
|
def test_create_extras_folder(self, tmp_path):
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
assert r._create_extras_folder() is True
|
|
assert (tmp_path / "extras").exists()
|
|
|
|
def test_create_delete_folder(self, tmp_path):
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
assert r._create_delete_folder() is True
|
|
assert (tmp_path / "delete me").exists()
|
|
|
|
def test_create_extras_already_exists(self, tmp_path):
|
|
(tmp_path / "extras").mkdir()
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
assert r._create_extras_folder() is True
|
|
|
|
|
|
class TestMoveExtras:
|
|
def test_move_extras(self, tmp_path):
|
|
(tmp_path / "extra.mkv").write_bytes(b"\x00" * 100)
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
extras = [{"path": tmp_path / "extra.mkv", "name": "extra.mkv"}]
|
|
moved = r.move_extras_to_folder(extras)
|
|
assert len(moved) == 1
|
|
assert (tmp_path / "extras" / "extra.mkv").exists()
|
|
assert not (tmp_path / "extra.mkv").exists()
|
|
|
|
def test_move_extras_skip_existing(self, tmp_path):
|
|
(tmp_path / "extras").mkdir()
|
|
(tmp_path / "extras" / "extra.mkv").write_bytes(b"\x00")
|
|
(tmp_path / "extra.mkv").write_bytes(b"\x00" * 100)
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
extras = [{"path": tmp_path / "extra.mkv", "name": "extra.mkv"}]
|
|
moved = r.move_extras_to_folder(extras)
|
|
assert len(moved) == 0
|
|
|
|
def test_move_extras_empty(self, tmp_path):
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
assert r.move_extras_to_folder([]) == []
|
|
|
|
|
|
class TestDuplicateDetection:
|
|
def test_detect_duplicates_same_duration(self, tmp_path):
|
|
f1 = tmp_path / "Disc 1_t01.mkv"
|
|
f2 = tmp_path / "Disc 1_t02.mkv"
|
|
f1.write_bytes(b"\x00" * 2000)
|
|
f2.write_bytes(b"\x00" * 2000)
|
|
eps = [
|
|
{"path": f1, "name": "Disc 1_t01.mkv", "size_gb": 2.0, "duration_minutes": 45.0},
|
|
{"path": f2, "name": "Disc 1_t02.mkv", "size_gb": 2.0, "duration_minutes": 45.0},
|
|
]
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
result = r.detect_and_move_duplicates(eps)
|
|
assert isinstance(result, list)
|
|
|
|
def test_no_duplicates_different_duration(self, tmp_path):
|
|
f1 = tmp_path / "Disc 1_t01.mkv"
|
|
f2 = tmp_path / "Disc 2_t01.mkv"
|
|
f1.write_bytes(b"\x00" * 2000)
|
|
f2.write_bytes(b"\x01" * 3000)
|
|
eps = [
|
|
{"path": f1, "name": "Disc 1_t01.mkv", "size_gb": 2.0, "duration_minutes": 45.0},
|
|
{"path": f2, "name": "Disc 2_t01.mkv", "size_gb": 3.0, "duration_minutes": 55.0},
|
|
]
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
r.detect_and_move_duplicates(eps)
|
|
|
|
def test_duplicate_moved_to_delete_me(self, tmp_path):
|
|
f1 = tmp_path / "Disc 1_t01.mkv"
|
|
f2 = tmp_path / "Disc 1_t02.mkv"
|
|
f1.write_bytes(b"\x00" * 2000)
|
|
f2.write_bytes(b"\x00" * 2000)
|
|
eps = [
|
|
{"path": f1, "name": "Disc 1_t01.mkv", "size_gb": 2.0, "duration_minutes": 45.0},
|
|
{"path": f2, "name": "Disc 1_t02.mkv", "size_gb": 2.0, "duration_minutes": 45.0},
|
|
]
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1, auto_delete_duplicates=False)
|
|
result = r.detect_and_move_duplicates(eps)
|
|
assert (tmp_path / "delete me").exists()
|
|
|
|
|
|
class TestMatchEpisodes:
|
|
def test_disc_mapping_match(self, mock_episodes, mock_tvdb_episodes, disc_mapping):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1, disc_mapping=disc_mapping)
|
|
episodes_info = []
|
|
for ep in mock_episodes:
|
|
disc_info = r._extract_disc_info(ep["name"])
|
|
episodes_info.append({
|
|
"file_info": ep,
|
|
"disc_number": disc_info["disc_number"],
|
|
"track_number": disc_info["track_number"],
|
|
"filename": ep["name"],
|
|
"duration": ep["duration_minutes"],
|
|
})
|
|
matched = r._match_using_disc_mapping(episodes_info, mock_tvdb_episodes)
|
|
assert len(matched) > 0
|
|
ep_nums = [m["episode_number"] for m in matched]
|
|
assert len(set(ep_nums)) == len(ep_nums)
|
|
|
|
def test_fallback_sequential(self, mock_episodes, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
files = []
|
|
for ep in mock_episodes:
|
|
files.append({
|
|
"file_info": ep,
|
|
"filename": ep["name"],
|
|
"disc_number": None,
|
|
"duration": ep["duration_minutes"],
|
|
})
|
|
allowed = list(range(1, 11))
|
|
result = r._fallback_sequential(files, mock_tvdb_episodes, allowed)
|
|
assert len(result) > 0
|
|
|
|
def test_dp_match_episodes(self, mock_episodes, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
files = []
|
|
for ep in mock_episodes:
|
|
files.append({
|
|
"file_info": ep,
|
|
"filename": ep["name"],
|
|
"disc_number": None,
|
|
"duration": ep["duration_minutes"],
|
|
})
|
|
allowed = list(range(1, 11))
|
|
result = r._dp_match_episodes(files, mock_tvdb_episodes, allowed)
|
|
assert len(result) > 0
|
|
ep_nums = [m["episode_number"] for m in result]
|
|
assert len(set(ep_nums)) == len(ep_nums)
|
|
|
|
def test_precise_duration_match(self, mock_episodes, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
files = []
|
|
for ep in mock_episodes:
|
|
files.append({
|
|
"file_info": ep,
|
|
"filename": ep["name"],
|
|
"disc_number": None,
|
|
"duration": ep["duration_minutes"],
|
|
})
|
|
allowed = list(range(1, 7))
|
|
result = r._precise_duration_match(files, mock_tvdb_episodes, allowed)
|
|
assert len(result) > 0
|
|
|
|
def test_dp_match_empty_files(self, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
result = r._dp_match_episodes([], mock_tvdb_episodes, list(range(1, 11)))
|
|
assert isinstance(result, list)
|
|
|
|
def test_precise_match_empty(self, mock_episodes, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
files = [{"file_info": {}, "filename": "a.mkv", "duration": 45.0}]
|
|
result = r._precise_duration_match(files, [], [])
|
|
assert isinstance(result, list)
|
|
|
|
def test_fallback_sequential_empty(self, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
result = r._fallback_sequential([], mock_tvdb_episodes, [])
|
|
assert result == []
|
|
|
|
|
|
class TestRenameEpisodes:
|
|
def test_rename_no_episodes(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
assert r.rename_episodes([]) == []
|
|
|
|
def test_rename_with_tvdb(self, tmp_path, mock_episodes, mock_tvdb_episodes):
|
|
for ep in mock_episodes:
|
|
f = tmp_path / ep["name"]
|
|
f.write_bytes(b"\x00" * ep["size_bytes"])
|
|
ep["path"] = f
|
|
r = EpisodeRenamer(str(tmp_path), "TestShow", 1)
|
|
renamed = r.rename_episodes(mock_episodes, mock_tvdb_episodes)
|
|
assert len(renamed) > 0
|
|
for item in renamed:
|
|
assert "original_name" in item
|
|
assert "new_name" in item
|
|
assert "episode_number" in item
|
|
|
|
def test_rename_skip_existing(self, tmp_path, mock_episodes, capsys):
|
|
for ep in mock_episodes[:2]:
|
|
f = tmp_path / ep["name"]
|
|
f.write_bytes(b"\x00" * ep["size_bytes"])
|
|
ep["path"] = f
|
|
(tmp_path / "TestShow s01e01.mkv").write_bytes(b"\x00")
|
|
r = EpisodeRenamer(str(tmp_path), "TestShow", 1)
|
|
renamed = r.rename_episodes(mock_episodes[:2], None)
|
|
out = capsys.readouterr().out
|
|
assert "already exists" in out or len(renamed) < 2
|
|
|
|
|
|
class TestEstimateEpisodesPerDisc:
|
|
def test_estimate_basic(self, tmp_path, mock_episodes):
|
|
for ep in mock_episodes:
|
|
f = tmp_path / ep["name"]
|
|
f.write_bytes(b"\x00" * ep["size_bytes"])
|
|
ep["path"] = f
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
episodes_info = []
|
|
for ep in mock_episodes:
|
|
disc_info = r._extract_disc_info(ep["name"])
|
|
episodes_info.append({
|
|
"file_info": ep,
|
|
"disc_number": disc_info["disc_number"],
|
|
"filename": ep["name"],
|
|
})
|
|
result = r._estimate_episodes_per_disc(episodes_info, 6)
|
|
assert isinstance(result, dict)
|
|
|
|
def test_estimate_no_disc_info(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
result = r._estimate_episodes_per_disc([], 10)
|
|
assert result == {}
|
|
|
|
def test_estimate_no_disc_numbers(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
info = [{"disc_number": None, "filename": "a.mkv", "file_info": {"size_gb": 5.0}}]
|
|
result = r._estimate_episodes_per_disc(info, 10)
|
|
assert result == {}
|
|
|
|
|
|
class TestAnalyzeDiscCapacity:
|
|
def test_analyze_basic(self, tmp_path, mock_episodes):
|
|
for ep in mock_episodes:
|
|
f = tmp_path / ep["name"]
|
|
f.write_bytes(b"\x00" * ep["size_bytes"])
|
|
ep["path"] = f
|
|
r = EpisodeRenamer(str(tmp_path), "S", 1)
|
|
info = []
|
|
for ep in mock_episodes:
|
|
disc_info = r._extract_disc_info(ep["name"])
|
|
info.append({
|
|
"file_info": ep,
|
|
"disc_number": disc_info["disc_number"],
|
|
"filename": ep["name"],
|
|
})
|
|
result = r._analyze_disc_capacity(info)
|
|
assert isinstance(result, dict)
|
|
|
|
def test_analyze_no_discs(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
result = r._analyze_disc_capacity([{"disc_number": None, "file_info": {"size_gb": 5}}])
|
|
assert result == {}
|
|
|
|
|
|
class TestDurationMatching:
|
|
def test_match_by_duration_and_order(self, mock_episodes, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
info = []
|
|
for ep in mock_episodes:
|
|
disc_info = r._extract_disc_info(ep["name"])
|
|
info.append({
|
|
"file_info": ep,
|
|
"disc_number": disc_info["disc_number"],
|
|
"track_number": disc_info["track_number"],
|
|
"filename": ep["name"],
|
|
"duration": ep["duration_minutes"],
|
|
})
|
|
result = r._match_by_duration_and_order(info, mock_tvdb_episodes)
|
|
assert len(result) > 0
|
|
ep_nums = [m["episode_number"] for m in result]
|
|
assert len(set(ep_nums)) == len(ep_nums)
|
|
|
|
def test_flexible_duration_match(self, mock_episodes, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
info = []
|
|
for ep in mock_episodes:
|
|
disc_info = r._extract_disc_info(ep["name"])
|
|
info.append({
|
|
"file_info": ep,
|
|
"disc_number": disc_info["disc_number"],
|
|
"filename": ep["name"],
|
|
"duration": ep["duration_minutes"],
|
|
})
|
|
result = r._flexible_duration_match(info, mock_tvdb_episodes)
|
|
assert isinstance(result, list)
|
|
|
|
def test_sequential_assignment_with_constraints(self, mock_episodes, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
info = []
|
|
for ep in mock_episodes:
|
|
disc_info = r._extract_disc_info(ep["name"])
|
|
info.append({
|
|
"file_info": ep,
|
|
"disc_number": disc_info["disc_number"],
|
|
"filename": ep["name"],
|
|
"duration": ep["duration_minutes"],
|
|
})
|
|
disc_capacity = {1: 3, 2: 3}
|
|
result = r._sequential_assignment_with_constraints(info, mock_tvdb_episodes, disc_capacity)
|
|
assert isinstance(result, list)
|
|
|
|
def test_validate_sequential_assignment_valid(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
file_info = {"duration": 55.0, "disc_number": 1}
|
|
tvdb_ep = {"runtime": 55, "episode_number": 1}
|
|
assert r._validate_sequential_assignment(file_info, tvdb_ep, {}, {}) is True
|
|
|
|
def test_validate_sequential_assignment_duration_mismatch(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
file_info = {"duration": 30.0, "disc_number": 1}
|
|
tvdb_ep = {"runtime": 60, "episode_number": 1}
|
|
result = r._validate_sequential_assignment(file_info, tvdb_ep, {}, {})
|
|
assert result is False
|
|
|
|
def test_validate_sequential_assignment_disc_full(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
file_info = {"duration": 55.0, "disc_number": 1}
|
|
tvdb_ep = {"runtime": 55, "episode_number": 1}
|
|
disc_assignments = {1: 3}
|
|
disc_capacity = {1: 3}
|
|
result = r._validate_sequential_assignment(file_info, tvdb_ep, disc_assignments, disc_capacity)
|
|
assert result is False
|
|
|
|
def test_assignment_satisfies_constraints(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
fake_path = type("P", (), {"name": "Disc 1_t01.mkv"})()
|
|
file_info = {
|
|
"duration": 55.0,
|
|
"disc_number": 1,
|
|
"file_info": {"path": fake_path},
|
|
}
|
|
tvdb_ep = {"runtime": 55, "episode_number": 1}
|
|
result = r._assignment_satisfies_constraints(file_info, tvdb_ep, [], {})
|
|
assert result is True
|
|
|
|
def test_assignment_satisfies_duration_exceeded(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
fake_path = type("P", (), {"name": "a.mkv"})()
|
|
file_info = {
|
|
"duration": 30.0,
|
|
"disc_number": None,
|
|
"file_info": {"path": fake_path},
|
|
}
|
|
tvdb_ep = {"runtime": 60, "episode_number": 1}
|
|
result = r._assignment_satisfies_constraints(file_info, tvdb_ep, [], {})
|
|
assert result is False
|
|
|
|
|
|
class TestMatchByDurationAndDisc:
|
|
def test_match_disc(self, mock_episodes, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
info = []
|
|
for ep in mock_episodes:
|
|
disc_info = r._extract_disc_info(ep["name"])
|
|
info.append({
|
|
"file_info": ep,
|
|
"disc_number": disc_info["disc_number"],
|
|
"filename": ep["name"],
|
|
"duration": ep["duration_minutes"],
|
|
})
|
|
episodes_per_disc = {1: [1, 2, 3], 2: [4, 5, 6]}
|
|
result = r._match_by_duration_and_disc(info, mock_tvdb_episodes, episodes_per_disc)
|
|
assert len(result) > 0
|
|
for m in result:
|
|
assert m["episode_number"] in [1, 2, 3, 4, 5, 6]
|
|
|
|
|
|
class TestFindOptimalAssignment:
|
|
def test_find_optimal_assignment(self, mock_episodes, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
info = []
|
|
for ep in mock_episodes:
|
|
disc_info = r._extract_disc_info(ep["name"])
|
|
info.append({
|
|
"file_info": ep,
|
|
"disc_number": disc_info["disc_number"],
|
|
"filename": ep["name"],
|
|
"duration": ep["duration_minutes"],
|
|
})
|
|
result = r._find_optimal_assignment(info, mock_tvdb_episodes, {1: 3, 2: 3})
|
|
assert isinstance(result, list)
|
|
|
|
def test_find_optimal_empty(self, mock_tvdb_episodes):
|
|
r = EpisodeRenamer("/tmp/t", "Show", 1)
|
|
result = r._find_optimal_assignment([], mock_tvdb_episodes, {})
|
|
assert result == []
|
|
|
|
|
|
class TestIsValidAssignment:
|
|
def test_valid_assignment(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
file_info = {"duration": 55.0, "disc_number": None}
|
|
tvdb_ep = {"runtime": 55, "episode_number": 1}
|
|
result = r._is_valid_assignment(file_info, tvdb_ep, {}, 0, [])
|
|
assert result is True
|
|
|
|
def test_invalid_duration(self):
|
|
r = EpisodeRenamer("/tmp/t", "S", 1)
|
|
file_info = {"duration": 30.0, "disc_number": None}
|
|
tvdb_ep = {"runtime": 60, "episode_number": 1}
|
|
result = r._is_valid_assignment(file_info, tvdb_ep, {}, 0, [])
|
|
assert result is False
|