#!/usr/bin/env python3 """ Path mapping module for NFOGuard Handles path translation between container, Radarr/Sonarr, and download client paths """ import os import re from pathlib import Path from datetime import datetime from typing import Dict, List, Optional, Tuple from core.logging import _log class PathMapper: """Handles path mapping between different contexts (container, Radarr/Sonarr, downloads)""" def __init__(self): # Load path mappings from environment self.container_to_radarr_movie_paths = self._load_movie_path_mappings() self.container_to_sonarr_tv_paths = self._load_tv_path_mappings() self.download_path_indicators = self._load_download_indicators() # Reverse mappings for efficiency self.radarr_to_container_movie_paths = {v: k for k, v in self.container_to_radarr_movie_paths.items()} self.sonarr_to_container_tv_paths = {v: k for k, v in self.container_to_sonarr_tv_paths.items()} def _load_movie_path_mappings(self) -> Dict[str, str]: """Load movie path mappings from environment""" # Container paths (what NFOGuard sees) container_paths_str = os.environ.get("MOVIE_PATHS", "/media/movies,/media/movies6") container_paths = [p.strip() for p in container_paths_str.split(",") if p.strip()] # Radarr paths (what Radarr sees) radarr_paths_str = os.environ.get("RADARR_ROOT_FOLDERS", "/mnt/unionfs/Media/Movies/movies,/mnt/unionfs/Media/Movies/movies6") radarr_paths = [p.strip() for p in radarr_paths_str.split(",") if p.strip()] # Create mappings (container -> radarr) mappings = {} for i, container_path in enumerate(container_paths): if i < len(radarr_paths): mappings[container_path] = radarr_paths[i] else: # Default mapping if not enough Radarr paths specified _log("WARNING", f"No Radarr path mapping for container path: {container_path}") mappings[container_path] = container_path _log("INFO", f"Movie path mappings: {mappings}") return mappings def _load_tv_path_mappings(self) -> Dict[str, str]: """Load TV path mappings from environment""" # Container paths container_paths_str = os.environ.get("TV_PATHS", "/media/tv,/media/tv6") container_paths = [p.strip() for p in container_paths_str.split(",") if p.strip()] # Sonarr paths sonarr_paths_str = os.environ.get("SONARR_ROOT_FOLDERS", "/mnt/unionfs/Media/TV/tv,/mnt/unionfs/Media/TV/tv6") sonarr_paths = [p.strip() for p in sonarr_paths_str.split(",") if p.strip()] # Create mappings mappings = {} for i, container_path in enumerate(container_paths): if i < len(sonarr_paths): mappings[container_path] = sonarr_paths[i] else: _log("WARNING", f"No Sonarr path mapping for container path: {container_path}") mappings[container_path] = container_path _log("INFO", f"TV path mappings: {mappings}") return mappings def _load_download_indicators(self) -> List[str]: """Load download path indicators from environment""" default_indicators = [ # nzbget paths '/downloads/', '/download/', '/completed/', '/nzb-complete/', '/complete/', # Transmission/Deluge '/torrents/', '/torrent/', '/seed/', '/seeding/', # SABnzbd '/sabnzbd/', '/sab/', '/usenet/', # qBittorrent '/qbittorrent/', '/qbt/', # Generic '/importing/', '/processing/', '/temp/', '/tmp/', '/staging/', # Client names 'nzbget', 'sabnzbd', 'transmission', 'deluge', 'qbittorrent', 'rtorrent' ] # Allow custom indicators from environment custom_indicators_str = os.environ.get("DOWNLOAD_PATH_INDICATORS", "") if custom_indicators_str: custom_indicators = [p.strip() for p in custom_indicators_str.split(",") if p.strip()] default_indicators.extend(custom_indicators) _log("DEBUG", f"Download path indicators: {default_indicators}") return default_indicators def container_path_to_radarr_path(self, container_path: str) -> str: """Convert container path to Radarr path""" container_path = str(container_path).rstrip("/") for container_root, radarr_root in self.container_to_radarr_movie_paths.items(): container_root = container_root.rstrip("/") if container_path.startswith(container_root): # Replace container root with Radarr root relative_path = container_path[len(container_root):].lstrip("/") radarr_path = f"{radarr_root.rstrip('/')}/{relative_path}" if relative_path else radarr_root.rstrip("/") _log("DEBUG", f"Mapped container path {container_path} -> {radarr_path}") return radarr_path _log("WARNING", f"No Radarr mapping found for container path: {container_path}") return container_path def radarr_path_to_container_path(self, radarr_path: str) -> str: """Convert Radarr path to container path""" radarr_path = str(radarr_path).rstrip("/") for radarr_root, container_root in self.radarr_to_container_movie_paths.items(): radarr_root = radarr_root.rstrip("/") if radarr_path.startswith(radarr_root): # Replace Radarr root with container root relative_path = radarr_path[len(radarr_root):].lstrip("/") container_path = f"{container_root.rstrip('/')}/{relative_path}" if relative_path else container_root.rstrip("/") _log("DEBUG", f"Mapped Radarr path {radarr_path} -> {container_path}") return container_path _log("WARNING", f"No container mapping found for Radarr path: {radarr_path}") return radarr_path def container_path_to_sonarr_path(self, container_path: str) -> str: """Convert container path to Sonarr path""" container_path = str(container_path).rstrip("/") for container_root, sonarr_root in self.container_to_sonarr_tv_paths.items(): container_root = container_root.rstrip("/") if container_path.startswith(container_root): relative_path = container_path[len(container_root):].lstrip("/") sonarr_path = f"{sonarr_root.rstrip('/')}/{relative_path}" if relative_path else sonarr_root.rstrip("/") _log("DEBUG", f"Mapped container path {container_path} -> {sonarr_path}") return sonarr_path _log("WARNING", f"No Sonarr mapping found for container path: {container_path}") return container_path def sonarr_path_to_container_path(self, sonarr_path: str) -> str: """Convert Sonarr path to container path""" sonarr_path = str(sonarr_path).rstrip("/") for sonarr_root, container_root in self.sonarr_to_container_tv_paths.items(): sonarr_root = sonarr_root.rstrip("/") if sonarr_path.startswith(sonarr_root): relative_path = sonarr_path[len(sonarr_root):].lstrip("/") container_path = f"{container_root.rstrip('/')}/{relative_path}" if relative_path else container_root.rstrip("/") _log("DEBUG", f"Mapped Sonarr path {sonarr_path} -> {container_path}") return container_path _log("WARNING", f"No container mapping found for Sonarr path: {sonarr_path}") return sonarr_path def is_download_path(self, path: str) -> bool: """Check if path indicates a download/temporary location""" path_lower = str(path).lower() return any(indicator in path_lower for indicator in self.download_path_indicators) def analyze_import_source_path(self, source_path: str) -> Tuple[bool, str]: """ Analyze import source path to determine if it's from downloads. Returns: (is_from_downloads, reason) """ if not source_path: return False, "no_source_path" path_lower = str(source_path).lower() # Extract potential IMDb ID from path imdb_match = re.search(r'(?:imdb-|tt)(\d{6,8})', path_lower) if imdb_match: imdb_id = f"tt{imdb_match.group(1)}" _log("DEBUG", f"Found IMDb ID in path: {imdb_id}") return True, f"imdb_in_path({imdb_id})" # Check for download indicators for indicator in self.download_path_indicators: if indicator in path_lower: return True, f"download_indicator({indicator})" # Check if path is outside media roots (likely a download) media_roots = [] media_roots.extend(self.container_to_radarr_movie_paths.values()) media_roots.extend(self.container_to_sonarr_tv_paths.values()) is_in_media = any(source_path.startswith(root.rstrip('/')) for root in media_roots) if not is_in_media: return True, "outside_media_roots" return False, "appears_to_be_media_path" def find_container_path_from_webhook(self, webhook_path: str, media_type: str = "movie") -> Optional[Path]: """ Find container path from webhook path information. Args: webhook_path: Path from Radarr/Sonarr webhook media_type: "movie" or "tv" """ if not webhook_path: return None if media_type == "movie": container_path = self.radarr_path_to_container_path(webhook_path) else: container_path = self.sonarr_path_to_container_path(webhook_path) path_obj = Path(container_path) if path_obj.exists(): _log("INFO", f"Found container path: {path_obj}") return path_obj _log("WARNING", f"Container path does not exist: {path_obj}") return None def get_debug_info(self) -> Dict[str, any]: """Get debug information about path mappings""" return { "movie_mappings": { "container_to_radarr": self.container_to_radarr_movie_paths, "radarr_to_container": self.radarr_to_container_movie_paths }, "tv_mappings": { "container_to_sonarr": self.container_to_sonarr_tv_paths, "sonarr_to_container": self.sonarr_to_container_tv_paths }, "download_indicators": self.download_path_indicators } # Global instance path_mapper = PathMapper() if __name__ == "__main__": # Test path mapping import os from datetime import datetime # Set up test environment os.environ["MOVIE_PATHS"] = "/media/movies,/media/movies6" os.environ["RADARR_ROOT_FOLDERS"] = "/mnt/unionfs/Media/Movies/movies,/mnt/unionfs/Media/Movies/movies6" os.environ["TV_PATHS"] = "/media/tv,/media/tv6" os.environ["SONARR_ROOT_FOLDERS"] = "/mnt/unionfs/Media/TV/tv,/mnt/unionfs/Media/TV/tv6" mapper = PathMapper() # Test movie path mapping container_movie = "/media/movies/Test Movie [imdb-tt1234567]" radarr_movie = mapper.container_path_to_radarr_path(container_movie) print(f"Container -> Radarr: {container_movie} -> {radarr_movie}") back_to_container = mapper.radarr_path_to_container_path(radarr_movie) print(f"Radarr -> Container: {radarr_movie} -> {back_to_container}") # Test download path detection test_paths = [ "/downloads/complete/Test.Movie.2023.1080p.BluRay.x264", "/mnt/unionfs/Media/Movies/movies/Test Movie [imdb-tt1234567]", "/nzbget/complete/Test.Series.S01E01.1080p.WEB.x264", "/torrents/seed/Test.Movie.2023" ] for path in test_paths: is_download, reason = mapper.analyze_import_source_path(path) print(f"Path analysis: {path} -> is_download={is_download} ({reason})") # Show debug info debug_info = mapper.get_debug_info() print(f"\nDebug info: {debug_info}")