fix: isolate movie and TV webhook processing to prevent cross-contamination

- Add prefixed batch keys (movie:imdbid, tv:imdbid) to prevent IMDb ID collisions
- Add path existence validation for Radarr webhooks to reject invalid mappings early
- Remove duplicate Radarr webhook handler code
- Add debug scripts for troubleshooting webhook and path mapping issues
- Create corrected .env template with fixed TV_PATHS and SONARR_ROOT_FOLDERS

This fixes the issue where TV path mapping failures caused movie webhooks
to process wrong movies due to shared batch queue corruption.
This commit is contained in:
2025-09-14 12:43:07 -04:00
parent 91881262ce
commit e741e4b046
7 changed files with 384 additions and 43 deletions
+46 -43
View File
@@ -942,7 +942,7 @@ class MovieProcessor:
_log("ERROR", f"No IMDb ID found in movie path: {movie_path}")
return
_log("INFO", f"Processing movie: {movie_path.name}")
_log("INFO", f"Processing movie: {movie_path.name} (IMDb: {imdb_id})")
# Update database
self.db.upsert_movie(imdb_id, str(movie_path))
@@ -1259,6 +1259,7 @@ class WebhookBatcher:
webhook_data['media_type'] = media_type
self.pending[key] = webhook_data
_log("INFO", f"Batched {media_type} webhook for {key}")
_log("DEBUG", f"Batch added - key: {key}, media_type: {media_type}, timer scheduled for {config.batch_delay}s")
timer = threading.Timer(config.batch_delay, self._process_item, args=[key])
self.timers[key] = timer
@@ -1424,7 +1425,8 @@ async def sonarr_webhook(request: Request, background_tasks: BackgroundTasks):
_log("ERROR", f"Could not find series directory: {series_title} ({imdb_id})")
return {"status": "error", "reason": "Series directory not found"}
# Add to batch queue
# Add to batch queue with TV-prefixed key to avoid movie conflicts
tv_batch_key = f"tv:{imdb_id}"
webhook_dict = {
'path': str(series_path),
'series_info': series_info,
@@ -1432,9 +1434,9 @@ async def sonarr_webhook(request: Request, background_tasks: BackgroundTasks):
'episodes': webhook.episodes or [], # Include episode data for targeted processing
'processing_mode': config.tv_webhook_processing_mode
}
batcher.add_webhook(imdb_id, webhook_dict, 'tv')
batcher.add_webhook(tv_batch_key, webhook_dict, 'tv')
return {"status": "accepted", "message": "Sonarr webhook queued"}
return {"status": "accepted", "message": f"Sonarr webhook queued for {tv_batch_key}"}
except Exception as e:
_log("ERROR", f"Sonarr webhook error: {e}")
@@ -1445,55 +1447,56 @@ async def radarr_webhook(request: Request, background_tasks: BackgroundTasks):
"""Handle Radarr webhooks"""
try:
payload = await _read_payload(request)
if not payload:
raise HTTPException(status_code=422, detail="Empty Radarr payload")
_log("INFO", f"Received Radarr webhook: {payload.get('eventType', 'Unknown')}")
_log("DEBUG", f"Full Radarr webhook payload: {payload}")
webhook = RadarrWebhook(**payload)
_log("INFO", f"Received Radarr webhook: {webhook.eventType}")
# Extract movie info
movie_data = payload.get("movie", {})
if not movie_data:
_log("WARNING", "No movie data in Radarr webhook")
return {"status": "error", "message": "No movie data"}
if webhook.eventType not in ["Download", "Upgrade", "Rename", "Test"]:
return {"status": "ignored", "reason": f"Event type {webhook.eventType} not processed"}
if webhook.eventType == "Test":
return {"status": "ok", "message": "Test received"}
if not webhook.movie:
return {"status": "ignored", "reason": "No movie data"}
movie_info = webhook.movie
movie_title = movie_info.get("title", "")
imdb_id = (movie_info.get("imdbId") or "").strip()
if imdb_id:
imdb_id = f"tt{imdb_id.replace('tt','')}"
radarr_path = movie_info.get("folderPath") or movie_info.get("path", "")
# Find movie path
movie_path = movie_processor.find_movie_path(movie_title, imdb_id, radarr_path)
if not movie_path:
_log("ERROR", f"Could not find movie directory: {movie_title} ({imdb_id})")
return {"status": "error", "reason": "Movie directory not found"}
# Extract IMDb ID from path if not in webhook
# Get IMDb ID for batching key
imdb_id = movie_data.get("imdbId", "").lower()
if not imdb_id:
imdb_id = nfo_manager.parse_imdb_from_path(movie_path)
_log("WARNING", "No IMDb ID in Radarr webhook movie data")
return {"status": "error", "message": "No IMDb ID"}
if not imdb_id:
_log("ERROR", f"No IMDb ID available for movie: {movie_title}")
return {"status": "error", "reason": "No IMDb ID"}
# Get movie path for verification
movie_path = movie_data.get("path", "")
if movie_path:
container_path = path_mapper.radarr_path_to_container_path(movie_path)
_log("DEBUG", f"Mapped Radarr path {movie_path} -> {container_path}")
# CRITICAL: Verify the mapped path actually exists
from pathlib import Path
if not Path(container_path).exists():
_log("ERROR", f"RADARR WEBHOOK REJECTED: Mapped path does not exist: {container_path}")
_log("ERROR", f"This prevents processing wrong movies due to path mapping issues")
return {"status": "error", "message": f"Mapped movie path does not exist: {container_path}"}
# Verify the path contains the expected IMDb ID
if imdb_id not in container_path.lower():
_log("WARNING", f"IMDb ID {imdb_id} not found in container path {container_path}")
# Add to batch queue
webhook_dict = {
'path': str(movie_path),
'movie_info': movie_info,
'event_type': webhook.eventType
# Create movie-specific webhook data with proper path validation
movie_webhook_data = {
'path': container_path, # Use verified container path
'movie_info': movie_data,
'event_type': payload.get('eventType'),
'original_payload': payload
}
batcher.add_webhook(imdb_id, webhook_dict, 'movie')
return {"status": "accepted", "message": "Radarr webhook queued"}
# Add to batch queue with movie-prefixed key to avoid TV conflicts
movie_batch_key = f"movie:{imdb_id}"
_log("DEBUG", f"Adding Radarr webhook to batch: key={movie_batch_key}, movie_title={movie_data.get('title', 'Unknown')}")
batcher.add_webhook(movie_batch_key, movie_webhook_data, "movie")
return {"status": "success", "message": f"Radarr webhook queued for {movie_batch_key}"}
except Exception as e:
_log("ERROR", f"Radarr webhook error: {e}")
raise HTTPException(status_code=422, detail=f"Invalid webhook: {e}")
return {"status": "error", "message": str(e)}
# ---------------------------
# API Endpoints