feat: enable anime parsing and organization with dedicated templates

Anime files with SxxEyy format are now parsed and organized into
anime-specific directories using configurable templates. Files with
absolute episode numbering (no season info) are marked needs_review
for manual handling. Also removes unused imports flagged by ruff.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
windyboy
2026-09-27 11:08:15 +08:00
co-authored by Claude Sonnet 4.5
parent 8832f5da3f
commit 9814b2b917
8 changed files with 248 additions and 19 deletions
+76
View File
@@ -168,3 +168,79 @@ class TestParseCommand:
assert movie2["video_metadata"]["size_bytes"] == 1500000000
assert movie2["video_metadata"]["resolution"] is None
assert movie2["video_metadata"]["codec"] is None
def test_parse_anime_with_season_episode(self, tmp_path):
"""Test parse correctly parses anime files with SxxEyy format."""
inventory_csv = tmp_path / "inventory.csv"
inventory_csv.write_text(
"# vlm inventory\n"
"path,filename,size_bytes,modified_timestamp,category,resolution,codec,duration_seconds,bitrate_kbps\n"
"/library/anime/Naruto Shippuden S01E05.mkv,Naruto Shippuden S01E05.mkv,500000,2024-01-01T00:00:00,anime,,,\n"
)
output_json = tmp_path / "identities.json"
runner = CliRunner()
result = runner.invoke(
main,
[
"--config",
str(tmp_path / "config.yaml"),
"parse",
"--input",
str(inventory_csv),
"--output",
str(output_json),
],
)
assert result.exit_code == 0
assert "Anime: 1" in result.output
with open(output_json) as f:
data = json.load(f)
assert len(data["anime"]) == 1
anime = data["anime"][0]
assert anime["title"] == "Naruto Shippuden"
assert anime["season"] == 1
assert anime["episodes"] == [5]
assert anime["confidence"] == 0.9
assert anime["needs_review"] is False
def test_parse_anime_absolute_numbering_needs_review(self, tmp_path):
"""Test parse marks anime with absolute episode numbering as needs_review."""
inventory_csv = tmp_path / "inventory.csv"
inventory_csv.write_text(
"# vlm inventory\n"
"path,filename,size_bytes,modified_timestamp,category,resolution,codec,duration_seconds,bitrate_kbps\n"
"/library/anime/Naruto - 042.mkv,Naruto - 042.mkv,500000,2024-01-01T00:00:00,anime,,,\n"
)
output_json = tmp_path / "identities.json"
runner = CliRunner()
result = runner.invoke(
main,
[
"--config",
str(tmp_path / "config.yaml"),
"parse",
"--input",
str(inventory_csv),
"--output",
str(output_json),
],
)
assert result.exit_code == 0
with open(output_json) as f:
data = json.load(f)
assert len(data["anime"]) == 1
anime = data["anime"][0]
assert anime["title"] == "Naruto"
assert anime["season"] is None
assert anime["episodes"] == [42]
assert anime["needs_review"] is True
-1
View File
@@ -6,7 +6,6 @@ import re
from pathlib import Path
import pytest
from click.testing import CliRunner
from vlm.cli import main
+30
View File
@@ -349,6 +349,36 @@ def test_generate_plan_for_anime_category(config):
assert "anime" in operation.reason.lower() or "not organized" in operation.reason.lower()
def test_generate_plan_for_anime_with_identity(config):
"""Test plan generation for anime files with parsed identity uses anime templates."""
video_file = VideoFile(
path=Path("/mnt/nas/videos/anime/Some.Anime.S01E05.mkv"),
filename="Some.Anime.S01E05.mkv",
size_bytes=500000,
modified_timestamp=datetime.now(timezone.utc),
category="anime"
)
identity = SeriesIdentity(
title="Some Anime",
season=1,
episodes=[5],
confidence=0.9,
needs_review=False,
original_filename="Some.Anime.S01E05.mkv",
)
plan = generate_plan([(video_file, identity)], config)
assert len(plan.operations) == 1
operation = plan.operations[0]
assert operation.operation_type in ("move", "rename")
assert operation.destination_path is not None
assert "anime" in str(operation.destination_path).lower()
assert "series" not in str(operation.destination_path).lower()
assert "Some_Anime" in operation.destination_path.name or "Some Anime" in str(operation.destination_path)
def test_generate_plan_for_other_category(config):
"""Test plan generation for other category files (no-op in v1)."""
video_file = VideoFile(