feat: add configurable category mappings for directory recognition

Allow users to configure multiple directory names per category (movie/series/anime) to support variations like "movies", "tv", "films". This enables proper categorization of files in directories that don't match the hardcoded singular forms, solving the issue where 635 files in "/mnt/Downloads/movies/" were incorrectly categorized as "other".

Configuration example:
  categories:
    movie: [movie, movies, films]
    series: [series, tv, shows]
    anime: [anime]

Changes include comprehensive validation, backward-compatible defaults, case-insensitive matching, and full test coverage (395 tests passing).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
windyboy
2026-02-09 22:40:48 +08:00
co-authored by Claude Sonnet 4.5
parent aa0dc8ec47
commit 259e7506d7
6 changed files with 1380 additions and 85 deletions
+161 -6
View File
@@ -29,12 +29,36 @@ class TestConfig:
movie_template="movies/{title}-{year}/",
log_level="DEBUG"
)
assert config.library_root == Path("/custom/path")
assert config.video_extensions == [".mp4", ".avi"]
assert config.movie_template == "movies/{title}-{year}/"
assert config.log_level == "DEBUG"
def test_config_default_categories(self):
"""Test Config has default categories."""
config = Config(library_root=Path("/test"))
assert config.categories == {
"movie": ["movie"],
"series": ["series"],
"anime": ["anime"]
}
def test_config_custom_categories(self):
"""Test Config with custom category mappings."""
config = Config(
library_root=Path("/test"),
categories={
"movie": ["movie", "movies", "films"],
"series": ["series", "tv"],
"anime": ["anime"]
}
)
assert "movies" in config.categories["movie"]
assert "tv" in config.categories["series"]
class TestLoadConfig:
"""Test load_config function."""
@@ -127,18 +151,34 @@ class TestLoadConfig:
config_data = {
'library_root': '/mnt/nas/videos'
}
with open(config_file, 'w') as f:
yaml.dump(config_data, f)
config = load_config(config_file)
# Should use default values for missing fields
assert config.library_root == Path('/mnt/nas/videos')
assert ".mp4" in config.video_extensions
assert config.movie_template == "movie/{title} ({year})/"
assert config.log_level == "INFO"
def test_load_config_with_custom_categories(self, tmp_path):
"""Test loading config with custom categories."""
config_file = tmp_path / "config.yaml"
config_file.write_text("""
library_root: /test/library
categories:
movie: [movie, movies, films]
series: [series, tv, shows]
anime: [anime]
""")
config = load_config(config_file)
assert config.categories["movie"] == ["movie", "movies", "films"]
assert config.categories["series"] == ["series", "tv", "shows"]
class TestCreateDefaultConfig:
"""Test create_default_config function."""
@@ -308,12 +348,127 @@ class TestValidateConfig:
movie_template="",
log_level="INVALID"
)
errors = validate_config(config)
# Should have multiple errors
assert len(errors) >= 4
def test_validate_empty_categories(self):
"""Test validating config with empty categories."""
config = Config(library_root=Path("/test"), categories={})
errors = validate_config(config)
assert any("categories" in e and "empty" in e for e in errors)
def test_validate_missing_required_category(self):
"""Test validating config with missing required categories."""
config = Config(
library_root=Path("/test"),
categories={"movie": ["movie"]} # Missing series, anime
)
errors = validate_config(config)
assert any("series" in e or "anime" in e for e in errors)
def test_validate_duplicate_directory_names(self):
"""Test validating config with duplicate directory names."""
config = Config(
library_root=Path("/test"),
categories={
"movie": ["movie", "videos"],
"series": ["series", "videos"], # Duplicate
"anime": ["anime"]
}
)
errors = validate_config(config)
assert any("Duplicate" in e and "videos" in e for e in errors)
def test_validate_case_insensitive_duplicates(self):
"""Test validating config with case-insensitive duplicates."""
config = Config(
library_root=Path("/test"),
categories={
"movie": ["Movie"],
"series": ["movie"], # Case-insensitive duplicate
"anime": ["anime"]
}
)
errors = validate_config(config)
assert any("Duplicate" in e for e in errors)
def test_validate_valid_custom_categories(self):
"""Test validating config with valid custom categories."""
config = Config(
library_root=Path("/test"),
categories={
"movie": ["movie", "movies"],
"series": ["series", "tv"],
"anime": ["anime"]
}
)
errors = validate_config(config)
assert errors == []
def test_validate_categories_not_dict(self):
"""Test validating config with categories not a dict."""
config = Config(
library_root=Path("/test"),
categories=["movie", "series"] # Wrong type
)
errors = validate_config(config)
assert any("must be a dictionary" in e for e in errors)
def test_validate_category_list_not_list(self):
"""Test validating config with category value not a list."""
config = Config(
library_root=Path("/test"),
categories={
"movie": "movie", # Should be a list
"series": ["series"],
"anime": ["anime"]
}
)
errors = validate_config(config)
assert any("must be a list" in e for e in errors)
def test_validate_empty_category_list(self):
"""Test validating config with empty category list."""
config = Config(
library_root=Path("/test"),
categories={
"movie": [], # Empty list
"series": ["series"],
"anime": ["anime"]
}
)
errors = validate_config(config)
assert any("cannot be empty" in e for e in errors)
def test_validate_category_list_with_non_string(self):
"""Test validating config with non-string in category list."""
config = Config(
library_root=Path("/test"),
categories={
"movie": ["movie", 123], # Non-string
"series": ["series"],
"anime": ["anime"]
}
)
errors = validate_config(config)
assert any("must contain strings" in e for e in errors)
def test_validate_category_list_with_empty_string(self):
"""Test validating config with empty string in category list."""
config = Config(
library_root=Path("/test"),
categories={
"movie": ["movie", ""], # Empty string
"series": ["series"],
"anime": ["anime"]
}
)
errors = validate_config(config)
assert any("empty directory name" in e for e in errors)
class TestConfigIntegration:
"""Integration tests for configuration workflow."""