refactor CLI command modules and synchronize docs

This commit is contained in:
windyboy
2026-02-16 12:31:26 +08:00
parent 0be802eac5
commit 2dd329cba9
41 changed files with 1066 additions and 753 deletions
+29 -29
View File
@@ -5,7 +5,7 @@ Tests series completeness analysis, duplicate detection, and quality comparison.
import pytest
from pathlib import Path
from datetime import datetime
from datetime import datetime, timezone
from vlm.models import SeriesIdentity, SeasonCompleteness, MovieIdentity, VideoFile, DuplicateGroup
from vlm.analysis import analyze_series_completeness, detect_duplicates, compare_quality
@@ -193,7 +193,7 @@ class TestDuplicateDetection:
Path("/movies/The.Matrix.1999.1080p.mkv"),
"The.Matrix.1999.1080p.mkv",
2000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1920x1080",
codec="h264"
@@ -202,7 +202,7 @@ class TestDuplicateDetection:
Path("/movies/The.Matrix.1999.720p.mkv"),
"The.Matrix.1999.720p.mkv",
1000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1280x720",
codec="h264"
@@ -211,7 +211,7 @@ class TestDuplicateDetection:
Path("/movies/Inception.2010.mkv"),
"Inception.2010.mkv",
1500000000,
datetime.now(),
datetime.now(timezone.utc),
"movie"
),
]
@@ -239,7 +239,7 @@ class TestDuplicateDetection:
Path("/series/Breaking.Bad.S01E01.1080p.mkv"),
"Breaking.Bad.S01E01.1080p.mkv",
1500000000,
datetime.now(),
datetime.now(timezone.utc),
"series",
resolution="1920x1080"
),
@@ -247,7 +247,7 @@ class TestDuplicateDetection:
Path("/series/Breaking.Bad.S01E01.720p.mkv"),
"Breaking.Bad.S01E01.720p.mkv",
800000000,
datetime.now(),
datetime.now(timezone.utc),
"series",
resolution="1280x720"
),
@@ -255,7 +255,7 @@ class TestDuplicateDetection:
Path("/series/Breaking.Bad.S01E02.mkv"),
"Breaking.Bad.S01E02.mkv",
1200000000,
datetime.now(),
datetime.now(timezone.utc),
"series"
),
]
@@ -278,8 +278,8 @@ class TestDuplicateDetection:
]
files = [
VideoFile(Path("/movies/Movie.A.2020.mkv"), "Movie.A.2020.mkv", 1000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Movie.B.2021.mkv"), "Movie.B.2021.mkv", 1000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Movie.A.2020.mkv"), "Movie.A.2020.mkv", 1000000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/Movie.B.2021.mkv"), "Movie.B.2021.mkv", 1000000000, datetime.now(timezone.utc), "movie"),
]
result = detect_duplicates(list(zip(identities, files)))
@@ -294,8 +294,8 @@ class TestDuplicateDetection:
]
files = [
VideoFile(Path("/movies/Unknown.Movie.mkv"), "Unknown.Movie.mkv", 1000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Unknown.Movie.2.mkv"), "Unknown.Movie.2.mkv", 1000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Unknown.Movie.mkv"), "Unknown.Movie.mkv", 1000000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/Unknown.Movie.2.mkv"), "Unknown.Movie.2.mkv", 1000000000, datetime.now(timezone.utc), "movie"),
]
result = detect_duplicates(list(zip(identities, files)))
@@ -311,8 +311,8 @@ class TestDuplicateDetection:
]
files = [
VideoFile(Path("/series/Unknown.Show.E01.mkv"), "Unknown.Show.E01.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Unknown.Show.Episode.1.mkv"), "Unknown.Show.Episode.1.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Unknown.Show.E01.mkv"), "Unknown.Show.E01.mkv", 1000000000, datetime.now(timezone.utc), "series"),
VideoFile(Path("/series/Unknown.Show.Episode.1.mkv"), "Unknown.Show.Episode.1.mkv", 1000000000, datetime.now(timezone.utc), "series"),
]
result = detect_duplicates(list(zip(identities, files)))
@@ -327,8 +327,8 @@ class TestDuplicateDetection:
]
files = [
VideoFile(Path("/series/Show.Name.S01.mkv"), "Show.Name.S01.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Show.Name.Season.1.mkv"), "Show.Name.Season.1.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Show.Name.S01.mkv"), "Show.Name.S01.mkv", 1000000000, datetime.now(timezone.utc), "series"),
VideoFile(Path("/series/Show.Name.Season.1.mkv"), "Show.Name.Season.1.mkv", 1000000000, datetime.now(timezone.utc), "series"),
]
result = detect_duplicates(list(zip(identities, files)))
@@ -347,7 +347,7 @@ class TestDuplicateDetection:
Path("/movies/Test.Movie.2020.1080p.mkv"),
"Test.Movie.2020.1080p.mkv",
2000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1920x1080",
codec="h264",
@@ -358,7 +358,7 @@ class TestDuplicateDetection:
Path("/movies/Test.Movie.2020.720p.mkv"),
"Test.Movie.2020.720p.mkv",
1000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1280x720",
codec="h264",
@@ -395,9 +395,9 @@ class TestDuplicateDetection:
]
files = [
VideoFile(Path("/series/Show.S01E01-E02.mkv"), "Show.S01E01-E02.mkv", 2000000000, datetime.now(), "series"),
VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Show.S01E02.mkv"), "Show.S01E02.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Show.S01E01-E02.mkv"), "Show.S01E01-E02.mkv", 2000000000, datetime.now(timezone.utc), "series"),
VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(timezone.utc), "series"),
VideoFile(Path("/series/Show.S01E02.mkv"), "Show.S01E02.mkv", 1000000000, datetime.now(timezone.utc), "series"),
]
result = detect_duplicates(list(zip(identities, files)))
@@ -413,8 +413,8 @@ class TestDuplicateDetection:
]
files = [
VideoFile(Path("/movies/The.Thing.1982.mkv"), "The.Thing.1982.mkv", 1000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/The.Thing.2011.mkv"), "The.Thing.2011.mkv", 1000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/The.Thing.1982.mkv"), "The.Thing.1982.mkv", 1000000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/The.Thing.2011.mkv"), "The.Thing.2011.mkv", 1000000000, datetime.now(timezone.utc), "movie"),
]
result = detect_duplicates(list(zip(identities, files)))
@@ -429,8 +429,8 @@ class TestDuplicateDetection:
]
files = [
VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Show.S02E01.mkv"), "Show.S02E01.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(timezone.utc), "series"),
VideoFile(Path("/series/Show.S02E01.mkv"), "Show.S02E01.mkv", 1000000000, datetime.now(timezone.utc), "series"),
]
result = detect_duplicates(list(zip(identities, files)))
@@ -448,7 +448,7 @@ class TestQualityComparison:
Path("/test/file1.mkv"),
"file1.mkv",
2000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1920x1080",
codec="h264",
@@ -459,7 +459,7 @@ class TestQualityComparison:
Path("/test/file2.mkv"),
"file2.mkv",
1000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1280x720",
codec="h265",
@@ -490,7 +490,7 @@ class TestQualityComparison:
Path("/test/file1.mkv"),
"file1.mkv",
2000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1920x1080"
# codec, duration, bitrate not available
@@ -499,7 +499,7 @@ class TestQualityComparison:
Path("/test/file2.mkv"),
"file2.mkv",
1000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie"
# No optional metadata
),
@@ -532,7 +532,7 @@ class TestQualityComparison:
Path("/test/file.mkv"),
"file.mkv",
1500000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1920x1080",
codec="h264"
+8 -8
View File
@@ -6,7 +6,7 @@ Each test validates a specific property from the design document.
import pytest
from pathlib import Path
from datetime import datetime
from datetime import datetime, timezone
from hypothesis import given, strategies as st, settings
from vlm.models import (
SeriesIdentity, SeasonCompleteness, MovieIdentity, VideoFile, DuplicateGroup
@@ -74,7 +74,7 @@ def video_file_strategy(draw, filename=None, category="movie"):
path = Path(f"/{category}/{filename}")
size_bytes = draw(st.integers(min_value=1000000, max_value=10000000000))
modified_timestamp = datetime.now()
modified_timestamp = datetime.now(timezone.utc)
# Optional metadata
has_metadata = draw(st.booleans())
@@ -226,7 +226,7 @@ def test_property_12_duplicate_detection_movies(title, year, duplicate_count):
Path(f"/movies/{filename}"),
filename,
1000000000 + i * 100000000,
datetime.now(),
datetime.now(timezone.utc),
"movie"
))
@@ -272,7 +272,7 @@ def test_property_13_duplicate_detection_series(title, season, episode, duplicat
Path(f"/series/{filename}"),
filename,
1000000000 + i * 100000000,
datetime.now(),
datetime.now(timezone.utc),
"series"
))
@@ -321,7 +321,7 @@ def test_property_14_duplicate_quality_comparison(title, year, file_count):
Path(f"/movies/{filename}"),
filename,
1000000000 + i * 100000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1920x1080",
codec="h264",
@@ -333,7 +333,7 @@ def test_property_14_duplicate_quality_comparison(title, year, file_count):
Path(f"/movies/{filename}"),
filename,
1000000000 + i * 100000000,
datetime.now(),
datetime.now(timezone.utc),
"movie"
))
@@ -429,7 +429,7 @@ def test_property_43_duplicate_report_grouping(duplicate_count, format):
Path(f"/movies/{filename}"),
filename,
1000000000 + j * 500000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1920x1080" if j == 0 else "1280x720",
codec="h264"
@@ -492,7 +492,7 @@ def test_property_44_summary_report_accuracy(file_count, categories):
Path(f"/{category}/{filename}"),
filename,
size,
datetime.now(),
datetime.now(timezone.utc),
category
))
+4 -4
View File
@@ -2,7 +2,7 @@
import json
from pathlib import Path
from datetime import datetime
from datetime import datetime, timezone
import pytest
from click.testing import CliRunner
@@ -46,7 +46,7 @@ def sample_rollback_log(tmp_path):
),
success=True,
error_message=None,
executed_at=datetime.now()
executed_at=datetime.now(timezone.utc)
),
OperationResult(
operation=FileOperation(
@@ -59,14 +59,14 @@ def sample_rollback_log(tmp_path):
),
success=True,
error_message=None,
executed_at=datetime.now()
executed_at=datetime.now(timezone.utc)
)
]
rollback_log = RollbackLog(
log_id="test-log-id",
execution_plan_id="test-plan-id",
executed_at=datetime.now(),
executed_at=datetime.now(timezone.utc),
operations=operations
)
+14 -14
View File
@@ -4,7 +4,7 @@ Tests execution mode handling, dry-run simulation, and actual file operations.
"""
import logging
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
@@ -73,7 +73,7 @@ def sample_plan(temp_test_dir):
return ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=operations,
summary={"move": 1, "rename": 1}
)
@@ -208,7 +208,7 @@ class TestDryRunSimulation:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=[no_op_operation],
summary={"no-op": 1}
)
@@ -233,7 +233,7 @@ class TestDryRunSimulation:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=[conflicted_operation],
summary={"move": 1}
)
@@ -285,7 +285,7 @@ class TestExecuteMode:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=[operation],
summary={"move": 1}
)
@@ -319,7 +319,7 @@ class TestExecuteMode:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=[operation],
summary={"move": 1}
)
@@ -352,7 +352,7 @@ class TestExecuteMode:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=[conflicted_operation],
summary={"move": 1}
)
@@ -427,7 +427,7 @@ class TestRollbackLog:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=operations,
summary={"move": 2}
)
@@ -480,7 +480,7 @@ class TestExecutionSummary:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=operations,
summary={"move": 2, "no-op": 1}
)
@@ -573,7 +573,7 @@ class TestRollbackLogSaving:
data = json.load(f)
# Verify timestamps are in ISO format
from datetime import datetime
from datetime import datetime, timezone
executed_at = datetime.fromisoformat(data["executed_at"])
assert executed_at is not None
@@ -670,7 +670,7 @@ class TestRollbackExecution:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=operations,
summary={"move": 2}
)
@@ -759,7 +759,7 @@ class TestRollbackExecution:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=operations,
summary={"move": 1, "no-op": 1}
)
@@ -877,7 +877,7 @@ class TestRollbackExecution:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=operations,
summary={"move": 2}
)
@@ -920,7 +920,7 @@ class TestRollbackExecution:
plan = ExecutionPlan(
plan_id=str(uuid4()),
created_at=datetime.now(),
created_at=datetime.now(timezone.utc),
operations=[
FileOperation(
operation_type="quarantine",
+46 -46
View File
@@ -1,7 +1,7 @@
"""Unit tests for plan generator."""
import pytest
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from vlm.config import Config
@@ -36,7 +36,7 @@ def test_generate_plan_for_movie_with_year(config):
path=Path("/mnt/nas/videos/movie/Some.Movie.2020.1080p.mkv"),
filename="Some.Movie.2020.1080p.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -67,7 +67,7 @@ def test_generate_plan_for_movie_without_year(config):
path=Path("/mnt/nas/videos/movie/random_movie.mkv"),
filename="random_movie.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -94,7 +94,7 @@ def test_generate_plan_for_series_with_season_and_episode(config):
path=Path("/mnt/nas/videos/series/Show.Name.S01E05.mkv"),
filename="Show.Name.S01E05.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
)
@@ -123,7 +123,7 @@ def test_generate_plan_blocks_series_with_high_season(config):
path=Path("/mnt/nas/videos/series/Show.Name.S20E01.mkv"),
filename="Show.Name.S20E01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
)
identity = SeriesIdentity(
@@ -147,7 +147,7 @@ def test_generate_plan_blocks_series_with_high_episode(config):
path=Path("/mnt/nas/videos/series/Show.Name.S01E120.mkv"),
filename="Show.Name.S01E120.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
)
identity = SeriesIdentity(
@@ -171,7 +171,7 @@ def test_generate_plan_sample_is_noop_by_default(config):
path=Path("/mnt/nas/videos/series/Show.Name.Sample.S01E01.mkv"),
filename="Show.Name.Sample.S01E01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
)
identity = SeriesIdentity(
@@ -196,7 +196,7 @@ def test_generate_plan_sample_can_be_included_via_config(config):
path=Path("/mnt/nas/videos/series/Show.Name.Sample.S01E01.mkv"),
filename="Show.Name.Sample.S01E01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
)
identity = SeriesIdentity(
@@ -219,7 +219,7 @@ def test_generate_plan_for_series_without_season(config):
path=Path("/mnt/nas/videos/series/ambiguous_show.mkv"),
filename="ambiguous_show.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
)
@@ -247,7 +247,7 @@ def test_generate_plan_for_anime_category(config):
path=Path("/mnt/nas/videos/anime/Some.Anime.01.mkv"),
filename="Some.Anime.01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="anime"
)
@@ -267,7 +267,7 @@ def test_generate_plan_for_other_category(config):
path=Path("/mnt/nas/videos/other/random.mkv"),
filename="random.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="other"
)
@@ -286,7 +286,7 @@ def test_generate_plan_preserves_category_boundaries(config):
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -311,7 +311,7 @@ def test_generate_plan_for_multi_episode_file(config):
path=Path("/mnt/nas/videos/series/Show.S01E01E02.mkv"),
filename="Show.S01E01E02.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
)
@@ -338,7 +338,7 @@ def test_generate_plan_file_already_at_target(config):
path=Path("/mnt/nas/videos/movie/Some Movie (2020)/Some Movie (2020).mkv"),
filename="Some Movie (2020).mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -364,7 +364,7 @@ def test_generate_plan_rename_vs_move(config):
path=Path("/mnt/nas/videos/movie/Some Movie (2020)/old_name.mkv"),
filename="old_name.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -385,7 +385,7 @@ def test_generate_plan_rename_vs_move(config):
path=Path("/mnt/nas/videos/movie/wrong_dir/Some Movie (2020).mkv"),
filename="Some Movie (2020).mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -403,7 +403,7 @@ def test_generate_plan_summary(config):
path=Path("/mnt/nas/videos/movie/Movie1.2020.mkv"),
filename="Movie1.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
),
MovieIdentity(
@@ -420,7 +420,7 @@ def test_generate_plan_summary(config):
path=Path("/mnt/nas/videos/movie/Movie2.mkv"),
filename="Movie2.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
),
MovieIdentity(
@@ -437,7 +437,7 @@ def test_generate_plan_summary(config):
path=Path("/mnt/nas/videos/anime/Anime.01.mkv"),
filename="Anime.01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="anime"
),
None
@@ -448,7 +448,7 @@ def test_generate_plan_summary(config):
path=Path("/mnt/nas/videos/series/Show.S01E01.mkv"),
filename="Show.S01E01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
),
SeriesIdentity(
@@ -480,14 +480,14 @@ def test_generate_plan_with_analysis_by_quality(config):
path=p_720,
filename="Test.2020.720p.WEB-DL.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie",
)
vf_1080 = VideoFile(
path=p_1080,
filename="Test.2020.1080p.BluRay.mkv",
size_bytes=2000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie",
)
identity = MovieIdentity(
@@ -528,14 +528,14 @@ def test_generate_plan_with_analysis_by_reputation_quality_time_reason(config):
path=p_old,
filename="Test.2020.720p.WEB-DL.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie",
)
vf_new = VideoFile(
path=p_new,
filename="Test.2020.1080p.BluRay.mkv",
size_bytes=2000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie",
)
identity = MovieIdentity(
@@ -574,14 +574,14 @@ def test_generate_plan_with_analysis_by_reputation_missing_scores_uses_fallback_
path=p_web,
filename="Test.2020.2160p.WEB-DL.mkv",
size_bytes=3000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie",
)
vf_bluray = VideoFile(
path=p_bluray,
filename="Test.2020.1080p.BluRay.mkv",
size_bytes=2000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie",
)
identity = MovieIdentity(
@@ -622,14 +622,14 @@ def test_generate_plan_human_summary_marks_disc_files_as_high_risk(config):
path=p_disc1,
filename="The.Best.of.Youth.DISC1.mkv",
size_bytes=2000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie",
)
vf2 = VideoFile(
path=p_disc2,
filename="The.Best.of.Youth.DISC2.mkv",
size_bytes=1800000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie",
)
identity = MovieIdentity(
@@ -669,7 +669,7 @@ def test_generate_plan_with_different_extensions(config):
path=Path(f"/mnt/nas/videos/movie/Movie.2020{ext}"),
filename=f"Movie.2020{ext}",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -704,7 +704,7 @@ def test_conflict_detection_for_movie(config, tmp_path):
path=tmp_path / "movie" / "Some.Movie.2020.1080p.mkv",
filename="Some.Movie.2020.1080p.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -741,7 +741,7 @@ def test_conflict_detection_for_series(config, tmp_path):
path=tmp_path / "series" / "Show.Name.S01E05.1080p.mkv",
filename="Show.Name.S01E05.1080p.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
)
@@ -776,7 +776,7 @@ def test_no_conflict_when_destination_does_not_exist(config, tmp_path):
path=source_dir / "Some.Movie.2020.mkv",
filename="Some.Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -814,7 +814,7 @@ def test_conflict_detection_with_multiple_files(config, tmp_path):
path=tmp_path / "movie" / "Movie1.2020.mkv",
filename="Movie1.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
),
MovieIdentity(
@@ -831,7 +831,7 @@ def test_conflict_detection_with_multiple_files(config, tmp_path):
path=tmp_path / "movie" / "Movie2.2021.mkv",
filename="Movie2.2021.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
),
MovieIdentity(
@@ -864,7 +864,7 @@ def test_save_plan_to_json(config, tmp_path):
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -909,7 +909,7 @@ def test_load_plan_from_json(config, tmp_path):
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -963,7 +963,7 @@ def test_save_and_load_plan_with_conflicts(config, tmp_path):
path=tmp_path / "movie" / "Movie.2020.mkv",
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -1003,7 +1003,7 @@ def test_save_and_load_plan_with_no_op_operations(config, tmp_path):
path=Path("/mnt/nas/videos/anime/Anime.01.mkv"),
filename="Anime.01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="anime"
),
None
@@ -1014,7 +1014,7 @@ def test_save_and_load_plan_with_no_op_operations(config, tmp_path):
path=Path("/mnt/nas/videos/movie/Movie.mkv"),
filename="Movie.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
),
MovieIdentity(
@@ -1048,7 +1048,7 @@ def test_save_plan_json_is_human_readable(config, tmp_path):
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -1089,7 +1089,7 @@ def test_save_plan_with_multiple_operations(config, tmp_path):
path=Path("/mnt/nas/videos/movie/Movie1.2020.mkv"),
filename="Movie1.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
),
MovieIdentity(
@@ -1106,7 +1106,7 @@ def test_save_plan_with_multiple_operations(config, tmp_path):
path=Path("/mnt/nas/videos/series/Show.S01E01.mkv"),
filename="Show.S01E01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
),
SeriesIdentity(
@@ -1124,7 +1124,7 @@ def test_save_plan_with_multiple_operations(config, tmp_path):
path=Path("/mnt/nas/videos/anime/Anime.01.mkv"),
filename="Anime.01.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="anime"
),
None
@@ -1176,7 +1176,7 @@ def test_plan_json_includes_all_required_fields(config, tmp_path):
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -1214,7 +1214,7 @@ def test_movie_rejected_by_review_generates_noop(config):
path=Path("/mnt/nas/videos/movie/Movie.2020.mkv"),
filename="Movie.2020.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="movie"
)
@@ -1238,7 +1238,7 @@ def test_series_rejected_by_review_generates_noop(config):
path=Path("/mnt/nas/videos/series/Show.S01E01.mkv"),
filename="Show.S01E01.mkv",
size_bytes=1000000,
modified_timestamp=datetime.now(),
modified_timestamp=datetime.now(timezone.utc),
category="series"
)
+1 -1
View File
@@ -3,7 +3,7 @@
import json
import pytest
from pathlib import Path
from datetime import datetime
from datetime import datetime, timezone
from vlm.quarantine import QuarantineManager
from vlm.config import Config
+23 -23
View File
@@ -185,7 +185,7 @@ class TestInventoryReport:
Path("/test.mkv"),
"test.mkv",
1000,
datetime.now(),
datetime.now(timezone.utc),
"movie"
)
]
@@ -377,7 +377,7 @@ class TestDuplicateReport:
Path("/movies/The.Matrix.1999.1080p.mkv"),
"The.Matrix.1999.1080p.mkv",
2000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1920x1080",
codec="h264",
@@ -388,7 +388,7 @@ class TestDuplicateReport:
Path("/movies/The.Matrix.1999.720p.mkv"),
"The.Matrix.1999.720p.mkv",
1000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1280x720",
codec="h264"
@@ -448,14 +448,14 @@ class TestDuplicateReport:
Path("/movies/Inception.2010.1080p.mkv"),
"Inception.2010.1080p.mkv",
2000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie"
),
VideoFile(
Path("/movies/Inception.2010.720p.mkv"),
"Inception.2010.720p.mkv",
1000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie"
),
]
@@ -501,14 +501,14 @@ class TestDuplicateReport:
Path("/series/Breaking.Bad.S01E01.1080p.mkv"),
"Breaking.Bad.S01E01.1080p.mkv",
1500000000,
datetime.now(),
datetime.now(timezone.utc),
"series"
),
VideoFile(
Path("/series/Breaking.Bad.S01E01.720p.mkv"),
"Breaking.Bad.S01E01.720p.mkv",
800000000,
datetime.now(),
datetime.now(timezone.utc),
"series"
),
]
@@ -564,8 +564,8 @@ class TestDuplicateReport:
# Create two duplicate groups with different sizes
identity1 = MovieIdentity("Small Movie", 2020, 0.9, False, "Small.Movie.mkv")
files1 = [
VideoFile(Path("/movies/Small.Movie.1.mkv"), "Small.Movie.1.mkv", 500000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Small.Movie.2.mkv"), "Small.Movie.2.mkv", 600000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Small.Movie.1.mkv"), "Small.Movie.1.mkv", 500000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/Small.Movie.2.mkv"), "Small.Movie.2.mkv", 600000000, datetime.now(timezone.utc), "movie"),
]
quality1 = [
{'filename': 'Small.Movie.1.mkv', 'path': '/movies/Small.Movie.1.mkv', 'size_bytes': 500000000},
@@ -574,8 +574,8 @@ class TestDuplicateReport:
identity2 = MovieIdentity("Large Movie", 2021, 0.9, False, "Large.Movie.mkv")
files2 = [
VideoFile(Path("/movies/Large.Movie.1.mkv"), "Large.Movie.1.mkv", 2000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Large.Movie.2.mkv"), "Large.Movie.2.mkv", 1800000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Large.Movie.1.mkv"), "Large.Movie.1.mkv", 2000000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/Large.Movie.2.mkv"), "Large.Movie.2.mkv", 1800000000, datetime.now(timezone.utc), "movie"),
]
quality2 = [
{'filename': 'Large.Movie.1.mkv', 'path': '/movies/Large.Movie.1.mkv', 'size_bytes': 2000000000},
@@ -600,8 +600,8 @@ class TestDuplicateReport:
"""Sort order should use quality_comparison sizes when VideoFile sizes are zero."""
identity1 = MovieIdentity("Tiny", 2020, 0.9, False, "Tiny.mkv")
files1 = [
VideoFile(Path("/movies/Tiny.1.mkv"), "Tiny.1.mkv", 0, datetime.now(), "movie"),
VideoFile(Path("/movies/Tiny.2.mkv"), "Tiny.2.mkv", 0, datetime.now(), "movie"),
VideoFile(Path("/movies/Tiny.1.mkv"), "Tiny.1.mkv", 0, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/Tiny.2.mkv"), "Tiny.2.mkv", 0, datetime.now(timezone.utc), "movie"),
]
quality1 = [
{"filename": "Tiny.1.mkv", "path": "/movies/Tiny.1.mkv", "size_bytes": 600000000},
@@ -610,8 +610,8 @@ class TestDuplicateReport:
identity2 = MovieIdentity("Huge", 2021, 0.9, False, "Huge.mkv")
files2 = [
VideoFile(Path("/movies/Huge.1.mkv"), "Huge.1.mkv", 0, datetime.now(), "movie"),
VideoFile(Path("/movies/Huge.2.mkv"), "Huge.2.mkv", 0, datetime.now(), "movie"),
VideoFile(Path("/movies/Huge.1.mkv"), "Huge.1.mkv", 0, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/Huge.2.mkv"), "Huge.2.mkv", 0, datetime.now(timezone.utc), "movie"),
]
quality2 = [
{"filename": "Huge.1.mkv", "path": "/movies/Huge.1.mkv", "size_bytes": 3000000000},
@@ -632,12 +632,12 @@ class TestSummaryReport:
def test_generate_summary_report(self):
"""Test generating summary report with various files."""
files = [
VideoFile(Path("/movies/Movie1.mkv"), "Movie1.mkv", 2000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Movie2.mkv"), "Movie2.mkv", 1500000000, datetime.now(), "movie"),
VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Show.S01E02.mkv"), "Show.S01E02.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/anime/Anime1.mkv"), "Anime1.mkv", 800000000, datetime.now(), "anime"),
VideoFile(Path("/other/Random.mkv"), "Random.mkv", 500000000, datetime.now(), "other"),
VideoFile(Path("/movies/Movie1.mkv"), "Movie1.mkv", 2000000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/Movie2.mkv"), "Movie2.mkv", 1500000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(timezone.utc), "series"),
VideoFile(Path("/series/Show.S01E02.mkv"), "Show.S01E02.mkv", 1000000000, datetime.now(timezone.utc), "series"),
VideoFile(Path("/anime/Anime1.mkv"), "Anime1.mkv", 800000000, datetime.now(timezone.utc), "anime"),
VideoFile(Path("/other/Random.mkv"), "Random.mkv", 500000000, datetime.now(timezone.utc), "other"),
]
library_root = Path("/mnt/nas/videos")
@@ -677,8 +677,8 @@ class TestSummaryReport:
def test_generate_summary_report_single_category(self):
"""Test generating summary report with files in single category."""
files = [
VideoFile(Path("/movies/Movie1.mkv"), "Movie1.mkv", 1000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Movie2.mkv"), "Movie2.mkv", 2000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Movie1.mkv"), "Movie1.mkv", 1000000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/Movie2.mkv"), "Movie2.mkv", 2000000000, datetime.now(timezone.utc), "movie"),
]
library_root = Path("/mnt/nas/videos")
+9 -9
View File
@@ -5,7 +5,7 @@ Tests the complete workflow from analysis to report generation.
import json
from pathlib import Path
from datetime import datetime
from datetime import datetime, timezone
from vlm.models import SeriesIdentity, VideoFile, MovieIdentity
from vlm.analysis import analyze_series_completeness, detect_duplicates
from vlm.reports import generate_completeness_report, generate_duplicate_report, generate_summary_report
@@ -59,7 +59,7 @@ class TestReportsIntegration:
Path("/movies/The.Matrix.1999.1080p.mkv"),
"The.Matrix.1999.1080p.mkv",
2000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1920x1080",
codec="h264"
@@ -68,7 +68,7 @@ class TestReportsIntegration:
Path("/movies/The.Matrix.1999.720p.mkv"),
"The.Matrix.1999.720p.mkv",
1000000000,
datetime.now(),
datetime.now(timezone.utc),
"movie",
resolution="1280x720",
codec="h264"
@@ -77,7 +77,7 @@ class TestReportsIntegration:
Path("/movies/Inception.2010.mkv"),
"Inception.2010.mkv",
1500000000,
datetime.now(),
datetime.now(timezone.utc),
"movie"
),
]
@@ -107,11 +107,11 @@ class TestReportsIntegration:
"""Test summary report generation with mixed file types."""
# Create test files
files = [
VideoFile(Path("/movies/Movie1.mkv"), "Movie1.mkv", 2000000000, datetime.now(), "movie"),
VideoFile(Path("/movies/Movie2.mkv"), "Movie2.mkv", 1500000000, datetime.now(), "movie"),
VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/series/Show.S01E02.mkv"), "Show.S01E02.mkv", 1000000000, datetime.now(), "series"),
VideoFile(Path("/anime/Anime1.mkv"), "Anime1.mkv", 800000000, datetime.now(), "anime"),
VideoFile(Path("/movies/Movie1.mkv"), "Movie1.mkv", 2000000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/movies/Movie2.mkv"), "Movie2.mkv", 1500000000, datetime.now(timezone.utc), "movie"),
VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(timezone.utc), "series"),
VideoFile(Path("/series/Show.S01E02.mkv"), "Show.S01E02.mkv", 1000000000, datetime.now(timezone.utc), "series"),
VideoFile(Path("/anime/Anime1.mkv"), "Anime1.mkv", 800000000, datetime.now(timezone.utc), "anime"),
]
# Generate summary report
+1 -1
View File
@@ -92,7 +92,7 @@ class TestLoadSaveState:
store = StateStore(
states={},
version='1.0',
last_updated=datetime.now()
last_updated=datetime.now(timezone.utc)
)
save_state(store, state_path)