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:
co-authored by
Claude Sonnet 4.5
parent
aa0dc8ec47
commit
259e7506d7
+67
-5
@@ -19,6 +19,7 @@ class Config:
|
||||
series_filename_template: Filename template for series (e.g., "S{season:02d}E{episode:02d}{ext}")
|
||||
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
quarantine_dir: Quarantine directory name relative to category root (e.g., ".quarantine")
|
||||
categories: Mapping of category names to directory name lists for file categorization
|
||||
"""
|
||||
library_root: Path
|
||||
video_extensions: list[str] = field(default_factory=lambda: [
|
||||
@@ -30,6 +31,11 @@ class Config:
|
||||
series_filename_template: str = "S{season:02d}E{episode:02d}{ext}"
|
||||
log_level: str = "INFO"
|
||||
quarantine_dir: str = ".quarantine"
|
||||
categories: dict[str, list[str]] = field(default_factory=lambda: {
|
||||
"movie": ["movie"],
|
||||
"series": ["series"],
|
||||
"anime": ["anime"]
|
||||
})
|
||||
|
||||
|
||||
def load_config(path: Path) -> Config:
|
||||
@@ -79,7 +85,14 @@ def load_config(path: Path) -> Config:
|
||||
# Extract other settings
|
||||
quarantine_dir = data.get('quarantine_dir', '.quarantine')
|
||||
log_level = data.get('log_level', 'INFO')
|
||||
|
||||
|
||||
# Extract categories configuration
|
||||
categories = data.get('categories', {
|
||||
"movie": ["movie"],
|
||||
"series": ["series"],
|
||||
"anime": ["anime"]
|
||||
})
|
||||
|
||||
return Config(
|
||||
library_root=library_root,
|
||||
video_extensions=video_extensions,
|
||||
@@ -88,7 +101,8 @@ def load_config(path: Path) -> Config:
|
||||
movie_filename_template=movie_filename_template,
|
||||
series_filename_template=series_filename_template,
|
||||
log_level=log_level,
|
||||
quarantine_dir=quarantine_dir
|
||||
quarantine_dir=quarantine_dir,
|
||||
categories=categories
|
||||
)
|
||||
|
||||
|
||||
@@ -110,7 +124,12 @@ def create_default_config(path: Path) -> Config:
|
||||
movie_filename_template="{title} ({year}){ext}",
|
||||
series_filename_template="S{season:02d}E{episode:02d}{ext}",
|
||||
log_level="INFO",
|
||||
quarantine_dir=".quarantine"
|
||||
quarantine_dir=".quarantine",
|
||||
categories={
|
||||
"movie": ["movie"],
|
||||
"series": ["series"],
|
||||
"anime": ["anime"]
|
||||
}
|
||||
)
|
||||
|
||||
# Create YAML content
|
||||
@@ -124,7 +143,8 @@ def create_default_config(path: Path) -> Config:
|
||||
'series_filename': default_config.series_filename_template
|
||||
},
|
||||
'quarantine_dir': default_config.quarantine_dir,
|
||||
'log_level': default_config.log_level
|
||||
'log_level': default_config.log_level,
|
||||
'categories': default_config.categories
|
||||
}
|
||||
|
||||
# Ensure parent directory exists
|
||||
@@ -204,5 +224,47 @@ def validate_config(config: Config) -> list[str]:
|
||||
errors.append("quarantine_dir must be a string")
|
||||
elif config.quarantine_dir.startswith('/') or config.quarantine_dir.startswith('\\'):
|
||||
errors.append("quarantine_dir must be relative to category root, not absolute")
|
||||
|
||||
|
||||
# Validate categories
|
||||
if not config.categories:
|
||||
errors.append("categories cannot be empty")
|
||||
elif not isinstance(config.categories, dict):
|
||||
errors.append("categories must be a dictionary")
|
||||
else:
|
||||
# Check required category keys exist
|
||||
required_categories = {"movie", "series", "anime"}
|
||||
missing = required_categories - set(config.categories.keys())
|
||||
if missing:
|
||||
errors.append(f"categories must include keys: {sorted(missing)}")
|
||||
|
||||
# Validate each category's directory list and check for duplicates
|
||||
seen_dirs = {}
|
||||
for category, dir_list in config.categories.items():
|
||||
if not isinstance(dir_list, list):
|
||||
errors.append(f"categories['{category}'] must be a list")
|
||||
continue
|
||||
|
||||
if not dir_list:
|
||||
errors.append(f"categories['{category}'] cannot be empty")
|
||||
continue
|
||||
|
||||
for dir_name in dir_list:
|
||||
if not isinstance(dir_name, str):
|
||||
errors.append(f"categories['{category}'] must contain strings")
|
||||
break
|
||||
|
||||
if not dir_name.strip():
|
||||
errors.append(f"categories['{category}'] contains empty directory name")
|
||||
break
|
||||
|
||||
# Check for duplicates (case-insensitive)
|
||||
dir_lower = dir_name.lower()
|
||||
if dir_lower in seen_dirs:
|
||||
errors.append(
|
||||
f"Duplicate directory name '{dir_name}' in categories "
|
||||
f"'{category}' and '{seen_dirs[dir_lower]}'"
|
||||
)
|
||||
else:
|
||||
seen_dirs[dir_lower] = category
|
||||
|
||||
return errors
|
||||
|
||||
Reference in New Issue
Block a user