diff --git a/.env.template b/.env.template index f041d0a..beec550 100644 --- a/.env.template +++ b/.env.template @@ -79,6 +79,9 @@ MAX_CONCURRENT_SERIES=3 # Movie date strategy: import_then_digital or digital_then_import MOVIE_PRIORITY=import_then_digital +# Smart fallback: prefer digital over file dates for manual imports (true/false) +PREFER_DIGITAL_OVER_FILE_DATES=true + # When to query APIs: always, if_missing, never MOVIE_POLL_MODE=always diff --git a/README.md b/README.md index 302bd3d..bb693cd 100644 --- a/README.md +++ b/README.md @@ -73,25 +73,97 @@ BATCH_DELAY 5.0 Delay before processing batched events DEBUG false Enable verbose logging 🔌 API Endpoints -POST /webhook/sonarr – Sonarr events -POST /webhook/radarr – Radarr events +### Webhook Endpoints +```bash +# Radarr webhook (automatic processing) +POST /webhook/radarr -POST /manual/scan?scan_type=both – Manual scan (TV + Movies) [DEFAULT] +# Sonarr webhook (automatic processing) +POST /webhook/sonarr +``` -POST /manual/scan?scan_type=tv – Manual scan TV only +### Manual Processing +```bash +# Manual scan all media +curl -X POST "http://localhost:8080/manual/scan?scan_type=both" -POST /manual/scan?scan_type=movies – Manual scan Movies only +# Manual scan TV only +curl -X POST "http://localhost:8080/manual/scan?scan_type=tv" -POST /manual/scan?path=/media/movies – Manual scan specific path +# Manual scan movies only +curl -X POST "http://localhost:8080/manual/scan?scan_type=movies" -GET /health – Health check +# Manual scan specific path +curl -X POST "http://localhost:8080/manual/scan?path=/media/movies" -GET /stats – Database stats +# Bulk update all movies from Radarr database +curl -X POST "http://localhost:8080/bulk/update" +``` -GET /batch/status – Current batch queue +### Testing & Validation +```bash +# Test database connections +curl -X POST "http://localhost:8080/test/bulk-update" -GET /debug/movie/{imdb_id} – Debug movie import date detection +# Test movie directory scanning +curl -X POST "http://localhost:8080/test/movie-scan" + +# System health check +curl "http://localhost:8080/health" + +# Database statistics +curl "http://localhost:8080/stats" + +# Batch processing queue status +curl "http://localhost:8080/batch/status" +``` + +### Debugging & Analysis +```bash +# Debug specific movie import date detection +curl "http://localhost:8080/debug/movie/tt1674782" + +# Show complete import history analysis +curl "http://localhost:8080/debug/movie/tt1674782/history" + +# Show date priority logic and available sources +curl "http://localhost:8080/debug/movie/tt1674782/priority" +``` + +### Response Examples + +**Health Check**: +```json +{ + "status": "healthy", + "version": "0.5.1", + "database_status": "healthy", + "radarr_database": {"status": "healthy", "movies": 1500} +} +``` + +**Movie Debug**: +```json +{ + "detected_import_date": "2025-07-08T03:30:04+00:00", + "import_source": "radarr:history.import", + "movie_title": "Movie Name" +} +``` + +**Priority Logic Debug**: +```json +{ + "movie_priority": "import_then_digital", + "priority_explanation": "1st: Radarr import history, 2nd: TMDB digital release, 3rd: file mtime", + "date_sources": { + "radarr_import": {"date": "2025-07-08T03:30:04+00:00", "source": "radarr:history.import"}, + "digital_release": {"date": "2023-05-15T00:00:00+00:00", "source": "tmdb:digital"} + }, + "selected_date": "2025-07-08T03:30:04+00:00" +} +``` 📖 Example Workflow You add The Blacklist in Sonarr. diff --git a/nfoguard.py b/nfoguard.py index ccf206e..62badd5 100644 --- a/nfoguard.py +++ b/nfoguard.py @@ -96,6 +96,7 @@ class NFOGuardConfig: # Movie processing self.movie_priority = os.environ.get("MOVIE_PRIORITY", "import_then_digital").lower() + self.prefer_digital_over_file_dates = _bool_env("PREFER_DIGITAL_OVER_FILE_DATES", True) self.movie_poll_mode = os.environ.get("MOVIE_POLL_MODE", "always").lower() self.movie_update_mode = os.environ.get("MOVIE_DATE_UPDATE_MODE", "backfill_only").lower() @@ -460,16 +461,31 @@ class MovieProcessor: # Try import history first if configured if config.movie_priority == "import_then_digital": + import_date, import_source = None, None if radarr_movie: movie_id = radarr_movie.get("id") if movie_id: import_date, import_source = self.radarr.get_movie_import_date(movie_id) - if import_date: - return import_date, import_source, released - # Fall back to digital release + # Check if we got a real import date or just file date fallback + if import_date and import_source != "radarr:db.file.dateAdded": + return import_date, import_source, released + + # Get digital release date for comparison digital_date, digital_source = self._get_digital_release_date(imdb_id) - if digital_date: + + # If we only have file date and digital date exists, prefer digital if reasonable and enabled + if import_date and import_source == "radarr:db.file.dateAdded" and digital_date and config.prefer_digital_over_file_dates: + # Compare dates - prefer digital if it's not way older than theatrical + if self._should_prefer_digital_over_file_date(digital_date, released, imdb_id): + return digital_date, digital_source, released + else: + return import_date, import_source, released + + # Use whichever we have + if import_date: + return import_date, import_source, released + elif digital_date: return digital_date, digital_source, released else: # digital_then_import @@ -519,6 +535,44 @@ class MovieProcessor: return "MANUAL_REVIEW_NEEDED", "manual_review_required", None + def _should_prefer_digital_over_file_date(self, digital_date: str, theatrical_release: Optional[str], imdb_id: str) -> bool: + """ + Decide if digital release date should be preferred over file date + + Logic: + - If digital date is reasonable (not decades before theatrical), prefer it + - If digital date is way too old (like 2000 for 1983 movie), use file date + """ + try: + digital_dt = datetime.fromisoformat(digital_date.replace("Z", "+00:00")) + + # If we have theatrical release date, compare + if theatrical_release: + theatrical_dt = datetime.fromisoformat(theatrical_release.replace("Z", "+00:00")) + year_diff = digital_dt.year - theatrical_dt.year + + # If digital is more than 10 years before theatrical, it's probably wrong + if year_diff < -10: + _log("INFO", f"Digital date {digital_date} is {abs(year_diff)} years before theatrical {theatrical_release} for {imdb_id}, using file date instead") + return False + + # If digital is within reasonable range (theatrical to +20 years), use it + if -2 <= year_diff <= 20: + _log("INFO", f"Digital date {digital_date} is reasonable for {imdb_id} (theatrical: {theatrical_release}), preferring over file date") + return True + + # If no theatrical date, use digital if it's not absurdly old + if digital_dt.year >= 1990: # Reasonable minimum for digital releases + _log("INFO", f"Digital date {digital_date} seems reasonable for {imdb_id}, preferring over file date") + return True + + _log("INFO", f"Digital date {digital_date} seems too old for {imdb_id}, using file date instead") + return False + + except Exception as e: + _log("WARNING", f"Error comparing dates for {imdb_id}: {e}") + return False + def _parse_date_to_iso(self, date_str: str) -> Optional[str]: """Parse date string to ISO format""" if not date_str: @@ -1152,13 +1206,34 @@ async def debug_movie_priority_logic(imdb_id: str): # Show priority logic if config.movie_priority == "import_then_digital": - result["priority_explanation"] = "1st: Radarr import history, 2nd: TMDB/OMDb digital release, 3rd: file mtime" - if result["date_sources"].get("radarr_import"): - result["selected_date"] = result["date_sources"]["radarr_import"]["date"] - result["selected_source"] = result["date_sources"]["radarr_import"]["source"] - elif result["date_sources"].get("digital_release"): - result["selected_date"] = result["date_sources"]["digital_release"]["date"] - result["selected_source"] = result["date_sources"]["digital_release"]["source"] + result["priority_explanation"] = "1st: Radarr import history, 2nd: TMDB/OMDb digital release, 3rd: file mtime. Note: If import is only file date, prefer reasonable digital dates." + + radarr_import = result["date_sources"].get("radarr_import") + digital_release = result["date_sources"].get("digital_release") + + # Check for file date fallback logic + if radarr_import and radarr_import["source"] == "radarr:db.file.dateAdded" and digital_release: + # Test the smart logic + would_prefer_digital = movie_processor._should_prefer_digital_over_file_date( + digital_release["date"], + None, # We don't have theatrical date in this debug context + imdb_id + ) + result["file_date_detected"] = True + result["would_prefer_digital"] = would_prefer_digital + + if would_prefer_digital: + result["selected_date"] = digital_release["date"] + result["selected_source"] = digital_release["source"] + " (preferred over file date)" + else: + result["selected_date"] = radarr_import["date"] + result["selected_source"] = radarr_import["source"] + " (digital too old)" + elif radarr_import and radarr_import["source"] != "radarr:db.file.dateAdded": + result["selected_date"] = radarr_import["date"] + result["selected_source"] = radarr_import["source"] + elif digital_release: + result["selected_date"] = digital_release["date"] + result["selected_source"] = digital_release["source"] else: # digital_then_import result["priority_explanation"] = "1st: TMDB/OMDb digital release, 2nd: Radarr import history, 3rd: file mtime" if result["date_sources"].get("digital_release"):