Add path canonicalization to prevent symlink duplicates

Implements centralized path canonicalization to ensure symlinks and paths
with . or .. segments are resolved to their canonical form, preventing
false duplicate detection.

Changes to utils.py:
- Add canonical_path() function using Path.resolve()
- Add canonical_path_str() for dictionary key usage

Changes to models.py:
- Add __post_init__ to VideoFile to auto-canonicalize paths
- All VideoFile instances now have canonical paths automatically

Impact:
- Path-based dictionary lookups now work correctly with symlinks
- io.py: path_to_inventory dictionary uses canonical keys
- planner.py: path_to_index dictionary uses canonical keys
- analysis.py: Duplicate detection uses canonical paths
- No false duplicates from symlinks or relative paths

Testing:
- Added comprehensive tests for canonical_path functions
- Tests verify symlink resolution, absolute path conversion, .. removal
- All 449 tests pass (added 10 new tests)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
windyboy
2026-02-13 09:52:14 +08:00
co-authored by Claude Sonnet 4.5
parent d6c8852e1e
commit e3069a287e
3 changed files with 151 additions and 3 deletions
+8 -3
View File
@@ -13,9 +13,9 @@ from typing import Optional
@dataclass
class VideoFile:
"""Represents a video file discovered during inventory scanning.
Attributes:
path: Full path to the video file
path: Full path to the video file (automatically canonicalized)
filename: Name of the file (without directory path)
size_bytes: File size in bytes
modified_timestamp: Last modification timestamp
@@ -30,13 +30,18 @@ class VideoFile:
size_bytes: int
modified_timestamp: datetime
category: str
# Optional metadata (if ffprobe available)
resolution: Optional[str] = None
codec: Optional[str] = None
duration_seconds: Optional[float] = None
bitrate_kbps: Optional[int] = None
def __post_init__(self):
"""Canonicalize path on creation."""
from vlm.utils import canonical_path
object.__setattr__(self, 'path', canonical_path(self.path))
@dataclass
class MovieIdentity:
+25
View File
@@ -1,6 +1,7 @@
"""Shared utilities for Video Library Manager."""
from datetime import datetime, timezone
from pathlib import Path
def utc_now() -> datetime:
@@ -22,3 +23,27 @@ def format_size(size_bytes: int) -> str:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} PB"
def canonical_path(path: Path) -> Path:
"""Return canonical absolute path, resolving symlinks.
Args:
path: Path to canonicalize
Returns:
Canonical absolute path with symlinks resolved
"""
return path.resolve()
def canonical_path_str(path: Path) -> str:
"""Return canonical path as string for use as dictionary key.
Args:
path: Path to canonicalize
Returns:
String representation of canonical path
"""
return str(canonical_path(path))