diff --git a/CHANGELOG.md b/CHANGELOG.md index 729f2e3..f61e564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.3.8] - 2025-09-08 +### Changed +- Use Radarr's numeric EventType (3="Imported") for more accurate import date detection +- Simplified history processing logic + ## [0.3.7] - 2025-09-08 ### Changed - Improved import date detection by tracking grab dates as fallback diff --git a/VERSION b/VERSION index 0f82685..6678432 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.7 +0.3.8 diff --git a/clients/radarr_client.py b/clients/radarr_client.py index c7f8467..e1faf78 100644 --- a/clients/radarr_client.py +++ b/clients/radarr_client.py @@ -11,6 +11,8 @@ from urllib.parse import urlencode, urljoin from urllib.request import Request as UrlRequest, urlopen from urllib.error import URLError, HTTPError +from .logging import _log + # Import path mapper for proper path handling try: from ..core.path_mapper import path_mapper @@ -26,11 +28,6 @@ except ImportError: path_mapper = DummyPathMapper() -def _log(level: str, msg: str): - """Placeholder logging function - replace with your actual logger""" - print(f"[{datetime.now().isoformat()}] {level}: {msg}") - - class RadarrClient: """Enhanced Radarr API client with improved import date detection""" @@ -274,26 +271,13 @@ class RadarrClient: except Exception: pass - # For import/download events, handle specially - # Grab date of first download/grab event as fallback - if event_type == "grabbed" and not earliest_grab_date: - earliest_grab_date = datetime.fromisoformat(event["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds") - _log("DEBUG", f"Found first grab event at {earliest_grab_date}") - - if event_type not in ["moviefileimported", "downloadFolderImported"]: - continue - + # Get imported path from Data field imported_path = None - # Try different sources for the path - if event_type == "moviefileimported": - imported_path = (event.get("data", {}).get("importedPath", "") or - event.get("data", {}).get("path", "") or - event.get("importedPath", "") or - event.get("sourcePath", "")).lower() - elif event_type == "downloadFolderImported": - imported_path = (event.get("data", {}).get("path", "") or - event.get("sourcePath", "") or - event.get("downloadPath", "")).lower() + try: + data = json.loads(event.get("data", "{}")) + imported_path = data.get("importedPath", "").lower() + except (json.JSONDecodeError, AttributeError): + pass if not imported_path: continue @@ -302,6 +286,7 @@ class RadarrClient: movie_title = (movie_info.get("title", "") or "").lower() movie_year = str(movie_info.get("year", "")) + # First try IMDb ID match # First try IMDb ID match if movie_imdb and ( f"[imdb-{movie_imdb}]" in imported_path or @@ -309,29 +294,29 @@ class RadarrClient: movie_imdb in imported_path ): _log("INFO", f"Found potential IMDb match in {event_type} event: {imported_path}") + date_iso = datetime.fromisoformat(event["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds") + _log("INFO", f"✅ FOUND IMPORT: exact IMDb match at {date_iso}") + earliest_real_import = date_iso + break + + # Then try title/year match with fuzzy path cleaning + if movie_title and movie_year: + # Clean strings for comparison by replacing common separators + clean_title = movie_title.replace(" ", ".").replace(":", ".").replace("-", ".").replace("_", ".").lower() + clean_path = imported_path.replace(" ", ".").replace("-", ".").replace("_", ".").replace("[", "").replace("]", "").lower() + + # Remove common words for better matching + for word in ["the", "a", "an"]: + clean_title = clean_title.replace(f".{word}.", ".").replace(f"{word}.", "").replace(f".{word}", "") + clean_path = clean_path.replace(f".{word}.", ".").replace(f"{word}.", "").replace(f".{word}", "") + + # Look for both title and year in the path + if clean_title in clean_path and movie_year in clean_path: date_iso = datetime.fromisoformat(event["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds") - _log("INFO", f"✅ FOUND IMPORT: exact IMDb match at {date_iso}") + _log("INFO", f"Found potential title/year match in {event_type} event: {clean_title} ({movie_year})") + _log("INFO", f"✅ FOUND IMPORT at {date_iso}") earliest_real_import = date_iso break - - # Then try title/year match with fuzzy path cleaning - if movie_title and movie_year: - # Clean strings for comparison by replacing common separators - clean_title = movie_title.replace(" ", ".").replace(":", ".").replace("-", ".").replace("_", ".").lower() - clean_path = imported_path.replace(" ", ".").replace("-", ".").replace("_", ".").replace("[", "").replace("]", "").lower() - - # Remove common words for better matching - for word in ["the", "a", "an"]: - clean_title = clean_title.replace(f".{word}.", ".").replace(f"{word}.", "").replace(f".{word}", "") - clean_path = clean_path.replace(f".{word}.", ".").replace(f"{word}.", "").replace(f".{word}", "") - - # Look for both title and year in the path - if clean_title in clean_path and movie_year in clean_path: - date_iso = datetime.fromisoformat(event["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds") - _log("INFO", f"Found potential title/year match in {event_type} event: {clean_title} ({movie_year})") - _log("INFO", f"✅ FOUND IMPORT at {date_iso}") - earliest_real_import = date_iso - break # Fallback to normal import analysis is_real, reason, date_iso = self._analyze_event_for_import(event, movie_info) @@ -440,84 +425,45 @@ class RadarrClient: """Get the earliest import date from Radarr history.""" _log("INFO", f"Finding earliest import for movie_id {movie_id}") - # Get full movie history earliest_real_import = None earliest_grab_date = None page = 1 page_size = 50 total_events = 0 + # Get full movie history while True: # Get page of history history_data = self._get_movie_history_page(movie_id, page, page_size) + if not history_data: + break # Process events on this page for event in history_data: - event_type = event.get("eventType", "").lower() + event_type = event.get("eventType") + if not event_type: + continue - # Track earliest grab date as fallback - if event_type == "grabbed" and not earliest_grab_date: - earliest_grab_date = datetime.fromisoformat(event["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds") + # Parse event date + event_date = datetime.fromisoformat(event["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds") + + # Track earliest grab date as fallback (EventType 1) + if event_type == 1 and not earliest_grab_date: + earliest_grab_date = event_date _log("DEBUG", f"Found first grab event at {earliest_grab_date}") - - # Only process import events - if event_type not in ["moviefileimported", "downloadFolderImported"]: continue - # Try different sources for the path - imported_path = None - if event_type == "moviefileimported": - imported_path = (event.get("data", {}).get("importedPath", "") or - event.get("data", {}).get("path", "") or - event.get("importedPath", "") or - event.get("sourcePath", "")).lower() - elif event_type == "downloadFolderImported": - imported_path = (event.get("data", {}).get("path", "") or - event.get("sourcePath", "") or - event.get("downloadPath", "")).lower() - - if not imported_path: - continue - - movie_imdb = (movie_info.get("imdbId", "") or "").lower() - movie_title = (movie_info.get("title", "") or "").lower() - movie_year = str(movie_info.get("year", "")) - - # First try IMDb ID match - if movie_imdb and ( - f"[imdb-{movie_imdb}]" in imported_path or - f"[{movie_imdb}]" in imported_path or - movie_imdb in imported_path - ): - _log("INFO", f"Found potential IMDb match in {event_type} event: {imported_path}") - date_iso = datetime.fromisoformat(event["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds") - _log("INFO", f"✅ FOUND IMPORT at {date_iso}") - earliest_real_import = date_iso - break - - # Then try title/year match with fuzzy path cleaning - if movie_title and movie_year: - # Clean strings for comparison by replacing common separators - clean_title = movie_title.replace(" ", ".").replace(":", ".").replace("-", ".").replace("_", ".").lower() - clean_path = imported_path.replace(" ", ".").replace("-", ".").replace("_", ".").replace("[", "").replace("]", "").lower() - - # Remove common words for better matching - for word in ["the", "a", "an"]: - clean_title = clean_title.replace(f".{word}.", ".").replace(f"{word}.", "").replace(f".{word}", "") - clean_path = clean_path.replace(f".{word}.", ".").replace(f"{word}.", "").replace(f".{word}", "") - - # Look for both title and year in the path - if clean_title in clean_path and movie_year in clean_path: - date_iso = datetime.fromisoformat(event["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds") - _log("INFO", f"Found potential title/year match in {event_type} event: {clean_title} ({movie_year})") - _log("INFO", f"✅ FOUND IMPORT at {date_iso}") - earliest_real_import = date_iso - break + # Look for import events (EventType 3) + if event_type == 3: + try: + data = json.loads(event.get("data", "{}")) + if data.get("importedPath"): + _log("INFO", f"✅ FOUND IMPORT at {event_date}") + earliest_real_import = event_date + break + except (json.JSONDecodeError, AttributeError): + continue - # Break if no more pages - if not history_data: - break - # Break if we found an import if earliest_real_import: break @@ -525,15 +471,22 @@ class RadarrClient: total_events += len(history_data) page += 1 - _log("INFO", f"Processed {total_events} events across {page-1} pages") + _log("INFO", f"Processed {total_events} events across {page}") - # Return earliest real import if found if earliest_real_import: return earliest_real_import - - # Fall back to earliest grab date if available if earliest_grab_date: - _log("WARNING", f"⚠️ No real imports found, using grab date: {earliest_grab_date}") + _log("WARNING", f"⚠️ No EventType 3 (import) found, using grab date: {earliest_grab_date}") return earliest_grab_date - - return None \ No newline at end of file + return None + + def _get_movie_history_page(self, movie_id: int, page: int, page_size: int) -> List[Dict[str, Any]]: + """Get a page of movie history.""" + data = self._get("/api/v3/history", { + "movieId": str(movie_id), + "page": page, + "pageSize": page_size, + "sortKey": "date", + "sortDirection": "ascending" + }) + return data if isinstance(data, list) else data.get("records", []) \ No newline at end of file