refactor: DLO-16/17/18/20 — CLI simplification, config Pydantic, planner split, type system unification

DLO-16: Reduce cli.py from 1073 to 83 lines by registering Click commands from commands/*.py modules
DLO-17: Migrate Config to Pydantic BaseModel for validation
DLO-18: Split planner.py (826 lines) into orchestration, path rendering, and duplicate handling modules
DLO-20: Unify type system — convert 14 dataclasses to Pydantic BaseModel, keep TypedDicts as JSON schema hints

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
windyboy
2026-09-27 10:47:04 +08:00
co-authored by Claude Sonnet 4.5
parent 8a60aaf9a9
commit fe03a31dd4
23 changed files with 1964 additions and 2714 deletions
+2 -74
View File
@@ -1,7 +1,7 @@
"""Inventory scanner for discovering and cataloging video files.
This module implements the core scanning functionality for the Video Library Manager,
including file discovery via `find`, metadata extraction, and categorization based on
including file discovery, metadata extraction, and categorization based on
directory structure.
"""
@@ -143,79 +143,7 @@ def scan_library(
def _discover_video_paths(root: Path, video_extensions: list[str]) -> list[Path]:
"""Discover matching video files under root.
Uses the system `find` command for traversal speed and falls back to Python
recursion if `find` is unavailable.
"""
try:
return _discover_video_paths_with_find(root, video_extensions)
except FileNotFoundError:
logger.warning("`find` command not available - falling back to Python recursion")
return _discover_video_paths_recursive(root, video_extensions)
def _log_find_nonzero_exit(returncode: int, stderr_text: str, discovered_count: int) -> None:
"""Log the explicit contract for non-zero `find` exits.
Contract: if `find` emits partial stdout before failing, keep those paths and
continue with a warning. If no paths were emitted, return an empty result and
log that scan discovery was incomplete.
"""
stderr_suffix = f": {stderr_text}" if stderr_text else ""
if discovered_count > 0:
logger.warning(
"find exited with code %s; using %s partial scan result(s)%s",
returncode,
discovered_count,
stderr_suffix,
)
else:
logger.warning(
"find exited with code %s and produced no scan results%s",
returncode,
stderr_suffix,
)
def _discover_video_paths_with_find(root: Path, video_extensions: list[str]) -> list[Path]:
"""Discover matching video files using the system `find` command."""
normalized_extensions = [ext.lower() for ext in video_extensions if ext]
if not normalized_extensions:
return []
command: list[str] = ["find", str(root), "-type", "f", "("]
for index, extension in enumerate(normalized_extensions):
if index > 0:
command.append("-o")
command.extend(["-iname", f"*{extension}"])
command.extend([")", "-print0"])
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = process.communicate()
discovered_paths: list[Path] = []
for path_bytes in stdout.split(b"\0"):
if not path_bytes:
continue
file_path = Path(os.fsdecode(path_bytes))
if _is_hidden_path(file_path, root):
continue
discovered_paths.append(file_path)
if process.returncode != 0:
stderr_text = stderr.decode(errors="replace").strip()
_log_find_nonzero_exit(process.returncode, stderr_text, len(discovered_paths))
return discovered_paths
def _discover_video_paths_recursive(root: Path, video_extensions: list[str]) -> list[Path]:
"""Fallback discovery using Python directory traversal."""
"""Discover matching video files under root using os.scandir recursion."""
discovered_paths: list[Path] = []
for file_path in _scan_directory_recursive(root, video_extensions):
if _is_hidden_path(file_path, root):