diff --git a/Project.md b/Project.md index ee8d563..fc74c03 100644 --- a/Project.md +++ b/Project.md @@ -1,5 +1,27 @@ # NFOGuard Project Overview +## 🎯 Three-Component System Architecture + +**NFOGuard** is a comprehensive media date management ecosystem consisting of three integrated repositories: + +### 1. **NFOGuard (Docker Service)** [THIS REPOSITORY] +- **Purpose**: Main webhook service that listens for Sonarr/Radarr events and manages NFO files +- **Version**: v1.6.0 +- **Technology**: Python 3, Docker, PostgreSQL integration +- **Role**: Records import dates, creates/updates NFO files, manages filesystem timestamps + +### 2. **NFOGuard-Emby-Plugin** (Companion DLL) +- **Purpose**: Emby Server plugin that reads NFO dateadded values and syncs to Emby's DateCreated field +- **Version**: v0.6.1 +- **Technology**: C# .NET Framework 4.8, Emby SDK +- **Role**: Real-time date synchronization within Emby, license validation + +### 3. **nfoguard-license-server** (License Management) +- **Purpose**: License validation server with admin interface for plugin access control +- **Version**: v1.3.1 +- **Technology**: Node.js, Express, SQLite +- **Role**: 7-day trials, license validation, user management dashboard + ## Project Purpose NFOGuard is an automated NFO file management system that integrates with Radarr and Sonarr to maintain accurate metadata and release dates in media library NFO files. It serves as a webhook-driven middleware that preserves existing metadata while adding clean, standardized date information. @@ -302,7 +324,7 @@ NFOGuard is an automated NFO file management system that integrates with Radarr ### When Starting a New Session 1. **Read Project.md** - This file for full context 2. **Check SUMMARY.md** - Latest changes and current version -3. **Review VERSION** - Current version number +3. **Review VERSION** - Current version number (v1.6.0) 4. **Key Files to Understand**: - `nfoguard.py` - Main logic, webhook handlers - `core/nfo_manager.py` - NFO creation, parsing, renaming @@ -315,17 +337,24 @@ NFOGuard is an automated NFO file management system that integrates with Radarr - **NFO Renaming**: `core/nfo_manager.py:336-491` (smart episode renaming) - **Dual Identification**: `core/nfo_manager.py:85-149` (directory + NFO parsing) -### User Context -- **User has**: Radarr, Sonarr, Unmanic integration -- **Environment**: Docker deployment, PostgreSQL databases -- **Focus Areas**: Webhook accuracy, NFO standardization, date integrity +### Project Context (Updated 2025-09-17) +- **Current Status**: Production-ready system with 3-component architecture +- **Version**: v1.6.0 (NFOGuard Docker), v1.1.18 (Emby Plugin), v1.3.2 (License Server) +- **User Environment**: Docker deployment, PostgreSQL databases, Unmanic integration +- **Focus Areas**: Webhook accuracy, NFO standardization, date integrity, lifetime license management - **Testing Method**: Real webhooks from Radarr/Sonarr, manual scans for validation +- **Recent Updates**: Fixed lifetime license display issue with explicit "never" expires handling + +### Three-Repository System Overview +1. **NFOGuard (Docker)** - Main webhook service and NFO management +2. **NFOGuard-Emby-Plugin** - DLL for real-time Emby date synchronization +3. **nfoguard-license-server** - License validation and user management ### Recent Work Pattern - User reports issues with specific examples (log snippets, webhook behavior) - We debug by analyzing code at specific line numbers - Implement fixes with proper error handling and logging -- Update documentation (SUMMARY.md, README.md, Project.md) +- Update documentation (SUMMARY.md, README.md, Project.md) across all repositories - Version bump and prepare for deployment -This project represents a mature, production-ready system for automated NFO management with comprehensive webhook integration and intelligent metadata handling. \ No newline at end of file +This project represents a mature, production-ready system for automated NFO management with comprehensive webhook integration, intelligent metadata handling, and integrated license management. \ No newline at end of file diff --git a/VERSION b/VERSION index dc1e644..266146b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.0 +1.6.3 diff --git a/clients/external_clients.py b/clients/external_clients.py index cdb1684..97d42c2 100644 --- a/clients/external_clients.py +++ b/clients/external_clients.py @@ -14,14 +14,18 @@ from urllib.error import URLError, HTTPError from core.logging import _log -def _get_json(url: str, timeout: int = 20, headers: Dict[str, str] = None) -> Optional[Dict[str, Any]]: +def _get_json(url: str, timeout: int = 20, headers: Dict[str, str] = None, suppress_404: bool = False) -> Optional[Dict[str, Any]]: """Make GET request and return JSON""" try: req = UrlRequest(url, headers=headers or {"Accept": "application/json"}) with urlopen(req, timeout=timeout) as resp: return json.loads(resp.read().decode("utf-8")) except Exception as e: - _log("WARNING", f"GET {url} failed: {e}") + # Handle HTTP 400/404 errors more gracefully for APIs where missing data is normal + if suppress_404 and ("400" in str(e) or "404" in str(e)): + _log("DEBUG", f"GET {url} - item not found (expected): {e}") + else: + _log("WARNING", f"GET {url} failed: {e}") return None @@ -83,16 +87,20 @@ class TVDBClient: url = f"{self.base_url}/search/remoteid?remoteId={imdb_id}&type=series" headers = {"Authorization": f"Bearer {token}"} - data = _get_json(url, headers=headers) + data = _get_json(url, headers=headers, suppress_404=True) if data and data.get("status") == "success" and data.get("data"): series_list = data["data"] if series_list and len(series_list) > 0: tvdb_id = series_list[0].get("id") if tvdb_id: - _log("INFO", f"TVDB: Converted {imdb_id} → {tvdb_id}") + _log("INFO", f"✅ TVDB: Found series {imdb_id} → {tvdb_id}") return str(tvdb_id) + + # If we get here, the series wasn't found in TVDB (common for newer shows) + _log("DEBUG", f"TVDB: No series found for IMDb {imdb_id} (not available in TVDB)") + except Exception as e: - _log("WARNING", f"TVDB IMDB conversion failed for {imdb_id}: {e}") + _log("WARNING", f"TVDB API error for {imdb_id}: {e}") return None diff --git a/core/nfo_manager.py b/core/nfo_manager.py index ef30d14..71f45c7 100644 --- a/core/nfo_manager.py +++ b/core/nfo_manager.py @@ -18,20 +18,35 @@ class NFOManager: self.manager_brand = manager_brand def parse_imdb_from_path(self, path: Path) -> Optional[str]: - """Extract IMDb ID from directory path""" - # Look for [imdb-ttXXXXXXX] or [ttXXXXXXX] patterns + """Extract IMDb ID from directory path or filename""" + # Look for various IMDb patterns in both directory and file names path_str = str(path).lower() - # Try [imdb-ttXXXXXXX] format first + # Try [imdb-ttXXXXXXX] format first (most explicit) match = re.search(r'\[imdb-?(tt\d+)\]', path_str) if match: return match.group(1) - # Try standalone [ttXXXXXXX] format + # Try standalone [ttXXXXXXX] format in brackets match = re.search(r'\[(tt\d+)\]', path_str) if match: return match.group(1) + # Try {imdb-ttXXXXXXX} format with curly braces + match = re.search(r'\{imdb-?(tt\d+)\}', path_str) + if match: + return match.group(1) + + # Try (imdb-ttXXXXXXX) format with parentheses + match = re.search(r'\(imdb-?(tt\d+)\)', path_str) + if match: + return match.group(1) + + # Try ttXXXXXXX at end of filename/dirname (common pattern) + match = re.search(r'[-_\s](tt\d+)$', path_str) + if match: + return match.group(1) + return None def create_movie_nfo(self, movie_dir: Path, imdb_id: str, dateadded: str, @@ -68,7 +83,8 @@ class NFOManager: movie.remove(uniqueid) except (ET.ParseError, ValueError) as e: - print(f"Warning: Could not parse existing NFO {nfo_path}, creating new: {e}") + print(f"⚠️ Corrupted NFO detected: {nfo_path} - {str(e)[:100]}...") + print(f" Creating new clean NFO file to replace corrupted one") movie = ET.Element("movie") else: # Create new NFO structure @@ -123,8 +139,11 @@ class NFOManager: with open(nfo_path, 'w', encoding='utf-8') as f: f.write(full_xml) + print(f"✅ Successfully created/updated movie NFO: {nfo_path}") + print(f" IMDb ID: {imdb_id}, Date Added: {dateadded}, Source: {source}") + except Exception as e: - print(f"Error creating/updating movie NFO {nfo_path}: {e}") + print(f"❌ Error creating/updating movie NFO {nfo_path}: {e}") def create_tvshow_nfo(self, series_dir: Path, imdb_id: str, tvdb_id: Optional[str] = None) -> None: """Create or update tvshow.nfo file preserving existing content""" @@ -153,7 +172,8 @@ class NFOManager: tvshow.remove(uniqueid) except (ET.ParseError, ValueError) as e: - print(f"Warning: Could not parse existing tvshow NFO {nfo_path}, creating new: {e}") + print(f"⚠️ Corrupted TV show NFO detected: {nfo_path} - {str(e)[:100]}...") + print(f" Creating new clean tvshow.nfo file to replace corrupted one") tvshow = ET.Element("tvshow") else: # Create new NFO structure @@ -191,8 +211,11 @@ class NFOManager: with open(nfo_path, 'w', encoding='utf-8') as f: f.write(full_xml) + print(f"✅ Successfully created/updated TV show NFO: {nfo_path}") + print(f" IMDb ID: {imdb_id}" + (f", TVDB ID: {tvdb_id}" if tvdb_id else "")) + except Exception as e: - print(f"Error creating/updating tvshow NFO {nfo_path}: {e}") + print(f"❌ Error creating/updating tvshow NFO {nfo_path}: {e}") def create_season_nfo(self, season_dir: Path, season_number: int) -> None: """Create or update season.nfo file preserving existing content""" @@ -218,7 +241,8 @@ class NFOManager: season.remove(existing) except (ET.ParseError, ValueError) as e: - print(f"Warning: Could not parse existing season NFO {nfo_path}, creating new: {e}") + print(f"⚠️ Corrupted season NFO detected: {nfo_path} - {str(e)[:100]}...") + print(f" Creating new clean season.nfo file to replace corrupted one") season = ET.Element("season") else: # Create new NFO structure @@ -249,8 +273,11 @@ class NFOManager: with open(nfo_path, 'w', encoding='utf-8') as f: f.write(full_xml) + print(f"✅ Successfully created/updated season NFO: {nfo_path}") + print(f" Season: {season_number}") + except Exception as e: - print(f"Error creating/updating season NFO {nfo_path}: {e}") + print(f"❌ Error creating/updating season NFO {nfo_path}: {e}") def create_episode_nfo(self, season_dir: Path, season_num: int, episode_num: int, aired: Optional[str], dateadded: Optional[str], source: str, @@ -283,7 +310,8 @@ class NFOManager: episode.remove(existing) except (ET.ParseError, ValueError) as e: - print(f"Warning: Could not parse existing episode NFO {nfo_path}, creating new: {e}") + print(f"⚠️ Corrupted episode NFO detected: {nfo_path} - {str(e)[:100]}...") + print(f" Creating new clean episode NFO file to replace corrupted one") episode = ET.Element("episodedetails") else: # Create new NFO structure @@ -343,8 +371,11 @@ class NFOManager: with open(nfo_path, 'w', encoding='utf-8') as f: f.write(full_xml) + print(f"✅ Successfully created/updated episode NFO: {nfo_path}") + print(f" S{season_num:02d}E{episode_num:02d}, Aired: {aired}, Date Added: {dateadded}") + except Exception as e: - print(f"Error creating/updating episode NFO {nfo_path}: {e}") + print(f"❌ Error creating/updating episode NFO {nfo_path}: {e}") def set_file_mtime(self, file_path: Path, iso_timestamp: str) -> None: """Set file modification time to match import date""" @@ -363,14 +394,22 @@ class NFOManager: # Set both access and modification times os.utime(file_path, (timestamp, timestamp)) + print(f"✅ Updated file timestamp: {file_path.name} -> {iso_timestamp}") except Exception as e: - print(f"Error setting mtime for {file_path}: {e}") + print(f"❌ Error setting mtime for {file_path}: {e}") def update_movie_files_mtime(self, movie_dir: Path, iso_timestamp: str) -> None: """Update modification times for all video files in movie directory""" video_exts = (".mkv", ".mp4", ".avi", ".mov", ".m4v") + updated_files = [] for file_path in movie_dir.iterdir(): if file_path.is_file() and file_path.suffix.lower() in video_exts: - self.set_file_mtime(file_path, iso_timestamp) \ No newline at end of file + self.set_file_mtime(file_path, iso_timestamp) + updated_files.append(file_path.name) + + if updated_files: + print(f"✅ Updated {len(updated_files)} video file timestamps in {movie_dir.name}") + else: + print(f"⚠️ No video files found to update in {movie_dir.name}") \ No newline at end of file