239 lines
9.1 KiB
Python
239 lines
9.1 KiB
Python
"""Unit tests for duplicate resolution strategies."""
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
from vlm.models import MovieIdentity
|
|
from vlm.duplicate_resolve import DuplicateResolutionError, choose_keep_index
|
|
|
|
|
|
def _mi(title: str = "Test", year: int | None = 2020) -> MovieIdentity:
|
|
return MovieIdentity(
|
|
title=title,
|
|
year=year,
|
|
confidence=0.9,
|
|
needs_review=False,
|
|
original_filename="test.mkv",
|
|
)
|
|
|
|
|
|
class TestByQuality:
|
|
"""Tests for by_quality strategy."""
|
|
|
|
def test_by_quality_prefers_higher_resolution(self):
|
|
"""1080p vs 720p -> keep 1080p."""
|
|
p1 = Path("/movies/Test.2020.720p.BluRay.mkv")
|
|
p2 = Path("/movies/Test.2020.1080p.BluRay.mkv")
|
|
items = [(p1, _mi()), (p2, _mi())]
|
|
qc = [
|
|
{"path": str(p1), "resolution": "1280x720", "size_bytes": 1000000},
|
|
{"path": str(p2), "resolution": "1920x1080", "size_bytes": 2000000},
|
|
]
|
|
idx = choose_keep_index(items, "by_quality", quality_comparison=qc)
|
|
assert idx == 1 # 1080p
|
|
|
|
def test_by_quality_prefers_bluray_over_webdl(self):
|
|
"""Same resolution: BluRay > WEB-DL."""
|
|
p1 = Path("/movies/Test.2020.1080p.WEB-DL.mkv")
|
|
p2 = Path("/movies/Test.2020.1080p.BluRay.mkv")
|
|
items = [(p1, _mi()), (p2, _mi())]
|
|
qc = [
|
|
{"path": str(p1), "resolution": "1920x1080", "size_bytes": 2000000},
|
|
{"path": str(p2), "resolution": "1920x1080", "size_bytes": 2000000},
|
|
]
|
|
idx = choose_keep_index(items, "by_quality", quality_comparison=qc)
|
|
assert idx == 1 # BluRay
|
|
|
|
def test_by_quality_prefers_hevc_over_h264(self):
|
|
"""Same resolution/source: x265 > x264."""
|
|
p1 = Path("/movies/Test.2020.1080p.BluRay.x264.mkv")
|
|
p2 = Path("/movies/Test.2020.1080p.BluRay.x265.mkv")
|
|
items = [(p1, _mi()), (p2, _mi())]
|
|
qc = [
|
|
{"path": str(p1), "resolution": "1920x1080", "codec": "h264", "size_bytes": 1500000},
|
|
{"path": str(p2), "resolution": "1920x1080", "codec": "hevc", "size_bytes": 1200000},
|
|
]
|
|
idx = choose_keep_index(items, "by_quality", quality_comparison=qc)
|
|
assert idx == 1 # x265
|
|
|
|
def test_by_quality_uses_size_as_tiebreaker(self):
|
|
"""Same resolution/source/codec: larger file wins."""
|
|
p1 = Path("/movies/Test.2020.1080p.BluRay.x264.mkv")
|
|
p2 = Path("/movies/Test.2020.1080p.BluRay.x264.mkv")
|
|
items = [(p1, _mi()), (p2, _mi())]
|
|
qc = [
|
|
{"path": str(p1), "resolution": "1920x1080", "size_bytes": 1000000},
|
|
{"path": str(p2), "resolution": "1920x1080", "size_bytes": 2000000},
|
|
]
|
|
idx = choose_keep_index(items, "by_quality", quality_comparison=qc)
|
|
assert idx == 1 # larger
|
|
|
|
def test_by_quality_fallback_to_filename(self):
|
|
"""No resolution/codec in qc -> parse from filename."""
|
|
p1 = Path("/movies/Test.2020.720p.WEB-DL.mkv")
|
|
p2 = Path("/movies/Test.2020.2160p.BluRay.x265.mkv")
|
|
items = [(p1, _mi()), (p2, _mi())]
|
|
qc = [
|
|
{"path": str(p1), "size_bytes": 0},
|
|
{"path": str(p2), "size_bytes": 0},
|
|
]
|
|
idx = choose_keep_index(items, "by_quality", quality_comparison=qc)
|
|
assert idx == 1 # 2160p from filename
|
|
|
|
def test_by_quality_returns_first_when_all_equal(self):
|
|
"""All equal -> keep first (index 0)."""
|
|
p1 = Path("/movies/Test.2020.1080p.BluRay.mkv")
|
|
p2 = Path("/movies/Test.2020.1080p.BluRay.mkv")
|
|
items = [(p1, _mi()), (p2, _mi())]
|
|
qc = [
|
|
{"path": str(p1), "resolution": "1920x1080", "size_bytes": 1000000},
|
|
{"path": str(p2), "resolution": "1920x1080", "size_bytes": 1000000},
|
|
]
|
|
idx = choose_keep_index(items, "by_quality", quality_comparison=qc)
|
|
assert idx == 0
|
|
|
|
def test_by_quality_deprioritizes_sample(self):
|
|
"""Sample clip should lose to normal release when quality is equal."""
|
|
p1 = Path("/movies/Test.2020.Sample.1080p.BluRay.mkv")
|
|
p2 = Path("/movies/Test.2020.1080p.BluRay.mkv")
|
|
items = [(p1, _mi()), (p2, _mi())]
|
|
qc = [
|
|
{"path": str(p1), "resolution": "1920x1080", "size_bytes": 1000000},
|
|
{"path": str(p2), "resolution": "1920x1080", "size_bytes": 1000000},
|
|
]
|
|
idx = choose_keep_index(items, "by_quality", quality_comparison=qc)
|
|
assert idx == 1
|
|
|
|
|
|
class TestOtherStrategies:
|
|
"""Tests for by_reputation, first_seen, manual."""
|
|
|
|
def test_manual_returns_none(self):
|
|
items = [(Path("/a.mkv"), _mi()), (Path("/b.mkv"), _mi())]
|
|
assert choose_keep_index(items, "manual") is None
|
|
|
|
def test_first_seen_returns_zero(self):
|
|
items = [(Path("/a.mkv"), _mi()), (Path("/b.mkv"), _mi())]
|
|
assert choose_keep_index(items, "first_seen") == 0
|
|
|
|
def test_by_reputation_prefers_higher_score(self):
|
|
mi_low = _mi()
|
|
mi_low.reputation_score = 6.0
|
|
mi_low.reputation_votes = 100
|
|
mi_high = _mi()
|
|
mi_high.reputation_score = 8.5
|
|
mi_high.reputation_votes = 500
|
|
items = [
|
|
(Path("/a.mkv"), mi_low),
|
|
(Path("/b.mkv"), mi_high),
|
|
]
|
|
idx = choose_keep_index(items, "by_reputation")
|
|
assert idx == 1
|
|
|
|
def test_by_reputation_deprioritizes_sample(self):
|
|
"""Sample clip should not be selected even with identical reputation."""
|
|
a = _mi()
|
|
a.reputation_score = 8.0
|
|
a.reputation_votes = 300
|
|
b = _mi()
|
|
b.reputation_score = 8.0
|
|
b.reputation_votes = 300
|
|
items = [
|
|
(Path("/a/Test.Sample.mkv"), a),
|
|
(Path("/a/Test.Final.mkv"), b),
|
|
]
|
|
idx = choose_keep_index(items, "by_reputation")
|
|
assert idx == 1
|
|
|
|
def test_by_reputation_falls_back_to_quality_when_reputation_missing(self):
|
|
"""When reputation is missing, BluRay should beat WEB-DL."""
|
|
a = _mi()
|
|
a.reputation_score = None
|
|
a.reputation_votes = None
|
|
b = _mi()
|
|
b.reputation_score = None
|
|
b.reputation_votes = None
|
|
p_web = Path("/a/Test.2020.2160p.WEB-DL.HEVC.mkv")
|
|
p_bluray = Path("/a/Test.2020.1080p.BluRay.x265.mkv")
|
|
items = [(p_web, a), (p_bluray, b)]
|
|
qc = [
|
|
{"path": str(p_web), "resolution": "3840x2160", "codec": "hevc", "size_bytes": 24000000000},
|
|
{"path": str(p_bluray), "resolution": "1920x1080", "codec": "hevc", "size_bytes": 8000000000},
|
|
]
|
|
idx = choose_keep_index(items, "by_reputation", quality_comparison=qc)
|
|
assert idx == 1
|
|
|
|
def test_by_reputation_quality_time_prefers_quality_after_reputation(self):
|
|
"""When reputation ties, quality should decide before modified time."""
|
|
a = _mi()
|
|
a.reputation_score = 8.0
|
|
a.reputation_votes = 300
|
|
b = _mi()
|
|
b.reputation_score = 8.0
|
|
b.reputation_votes = 300
|
|
p1 = Path("/a/Test.2020.720p.WEB-DL.mkv")
|
|
p2 = Path("/a/Test.2020.1080p.BluRay.mkv")
|
|
items = [(p1, a), (p2, b)]
|
|
qc = [
|
|
{
|
|
"path": str(p1),
|
|
"resolution": "1280x720",
|
|
"size_bytes": 1000000,
|
|
"modified_timestamp": "2026-02-01T00:00:00+00:00",
|
|
},
|
|
{
|
|
"path": str(p2),
|
|
"resolution": "1920x1080",
|
|
"size_bytes": 2000000,
|
|
"modified_timestamp": "2025-01-01T00:00:00+00:00",
|
|
},
|
|
]
|
|
idx = choose_keep_index(items, "by_reputation_quality_time", quality_comparison=qc)
|
|
assert idx == 1
|
|
|
|
def test_by_reputation_quality_time_prefers_newer_when_rep_and_quality_tie(self):
|
|
"""When reputation and quality tie, newer modified timestamp wins."""
|
|
a = _mi()
|
|
a.reputation_score = 8.0
|
|
a.reputation_votes = 300
|
|
b = _mi()
|
|
b.reputation_score = 8.0
|
|
b.reputation_votes = 300
|
|
p1 = Path("/a/Test.2020.1080p.BluRay.mkv")
|
|
p2 = Path("/a/Test.2020.1080p.BluRay.mkv")
|
|
items = [(p1, a), (p2, b)]
|
|
qc = [
|
|
{
|
|
"path": str(p1),
|
|
"resolution": "1920x1080",
|
|
"size_bytes": 2000000,
|
|
"modified_timestamp": "2025-01-01T00:00:00+00:00",
|
|
},
|
|
{
|
|
"path": str(p2),
|
|
"resolution": "1920x1080",
|
|
"size_bytes": 2000000,
|
|
"modified_timestamp": "2026-01-01T00:00:00+00:00",
|
|
},
|
|
]
|
|
idx = choose_keep_index(items, "by_reputation_quality_time", quality_comparison=qc)
|
|
assert idx == 1
|
|
|
|
|
|
def test_by_quality_requires_aligned_quality_data():
|
|
items = [(Path("/a.mkv"), _mi()), (Path("/b.mkv"), _mi())]
|
|
|
|
with pytest.raises(
|
|
DuplicateResolutionError,
|
|
match="requires quality data aligned with duplicate items",
|
|
):
|
|
choose_keep_index(items, "by_quality", quality_comparison=[{"path": "/a.mkv"}])
|
|
|
|
|
|
def test_unknown_strategy_raises_duplicate_resolution_error():
|
|
items = [(Path("/a.mkv"), _mi()), (Path("/b.mkv"), _mi())]
|
|
|
|
with pytest.raises(DuplicateResolutionError, match="Unsupported duplicate strategy"):
|
|
choose_keep_index(items, "unexpected")
|
|
|