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
+115
View File
@@ -0,0 +1,115 @@
# ===========================================
# NFOGuard Configuration - CORRECTED VERSION
# ===========================================
# Main configuration file - safe to share for debugging
# Sensitive data (API keys, passwords) are in .env.secrets
# ===========================================
# MEDIA PATHS (REQUIRED) - FIXED
# ===========================================
# Container paths (what NFOGuard sees inside container)
MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6
TV_PATHS=/media/TV/tv,/media/TV/tv6
# Radarr paths (what Radarr sees on the host system)
RADARR_ROOT_FOLDERS=/mnt/unionfs/Media/Movies/movies,/mnt/unionfs/Media/Movies/movies6
# Sonarr paths (what Sonarr sees on the host system) - FIXED VARIABLE NAME AND PATHS
SONARR_ROOT_FOLDERS=/mnt/unionfs/Media/TV/tv,/mnt/unionfs/Media/TV/tv6
# Download detection paths (for identifying downloads vs existing files)
DOWNLOAD_PATH_INDICATORS=/downloads/,/nzbget/,/completed/,/sabnzbd/,/torrents/
# ===========================================
# DATABASE CONFIGURATION
# ===========================================
# NFOGuard SQLite database location
DB_PATH=/app/data/media_dates.db
# ===========================================
# RADARR DATABASE CONNECTION (RECOMMENDED)
# ===========================================
# Direct database access for better performance
RADARR_DB_TYPE=postgresql
RADARR_DB_HOST=192.168.255.50
RADARR_DB_PORT=5432
RADARR_DB_NAME=radarr-main
RADARR_DB_USER=postgres
# ===========================================
# API CONNECTIONS (OPTIONAL)
# ===========================================
# API keys are stored in .env.secrets for security
RADARR_URL=http://radarr:7878
SONARR_URL=http://sonarr:8989
JELLYSEERR_URL=http://jellyseerr:5055
# ===========================================
# RELEASE DATE PROCESSING
# ===========================================
# Priority order for release date fallbacks (digital, physical, theatrical)
RELEASE_DATE_PRIORITY=digital,physical,theatrical
#RELEASE_DATE_PRIORITY=digital,theatrical,physical
ENABLE_SMART_DATE_VALIDATION=true
MAX_RELEASE_DATE_GAP_YEARS=10
# Prefer API release dates over file modification dates for manual imports
PREFER_RELEASE_DATES_OVER_FILE_DATES=true
# Disable file date fallback completely (recommended for clean imports)
ALLOW_FILE_DATE_FALLBACK=false
# TMDB country for regional release date preferences
TMDB_COUNTRY=US
# ===========================================
# NFO FILE MANAGEMENT
# ===========================================
# Create/update .nfo files
MANAGE_NFO=true
# Update file modification times to match import dates
FIX_DIR_MTIMES=true
# Add lockdata tags to prevent metadata overwrites
LOCK_METADATA=true
# Brand name in NFO comments
MANAGER_BRAND=NFOGuard
# ===========================================
# PROCESSING BEHAVIOR
# ===========================================
# Movie date update strategy
MOVIE_DATE_UPDATE_MODE=overwrite
MOVIE_PRESERVE_EXISTING=false
# When to update existing dates (always, missing_only, never)
UPDATE_MODE=always
# File modification time behavior (update, leave_alone)
MTIME_BEHAVIOR=update
# ===========================================
# PERFORMANCE & BATCHING
# ===========================================
# Delay before processing batched events (seconds)
BATCH_DELAY=5.0
# Maximum concurrent series processing
MAX_CONCURRENT_SERIES=3
# API timeout in seconds
TIMEOUT_SECONDS=45
# ===========================================
# DEBUGGING
# ===========================================
# Enable verbose logging (true/false)
DEBUG=true
# ===========================================
# SERVER CONFIGURATION
# ===========================================
# Port for webhook server
PORT=8080
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Git commit script for webhook isolation fixes
echo "🔧 Adding files to git..."
git add nfoguard.py
git add debug_webhook_issue.py
git add debug_path_mapping.py
git add .env.fixed
echo "📝 Committing changes..."
git commit -m "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.
Closes: webhook processing wrong movies (Annabelle → Harry Potter bug)"
echo "✅ Commit completed!"
echo ""
echo "📋 Summary of changes:"
echo " • Fixed webhook batch queue isolation"
echo " • Added path validation to prevent processing failures"
echo " • Created debugging tools for future troubleshooting"
echo " • Provided corrected .env configuration template"
+6
View File
@@ -123,6 +123,12 @@ class PathMapper:
relative_path = radarr_path[len(radarr_root):].lstrip("/") relative_path = radarr_path[len(radarr_root):].lstrip("/")
container_path = f"{container_root.rstrip('/')}/{relative_path}" if relative_path else container_root.rstrip("/") 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}") _log("DEBUG", f"Mapped Radarr path {radarr_path} -> {container_path}")
# Check if the mapped path actually exists
from pathlib import Path
if not Path(container_path).exists():
_log("WARNING", f"Mapped container path does not exist: {container_path}")
return container_path return container_path
_log("WARNING", f"No container mapping found for Radarr path: {radarr_path}") _log("WARNING", f"No container mapping found for Radarr path: {radarr_path}")
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
Debug path mapping configuration
"""
import os
from pathlib import Path
def check_path_mapping():
"""Check the current path mapping configuration"""
print("🔍 Path Mapping Configuration Debug")
print("=" * 50)
# Check environment variables
movie_paths = os.environ.get("MOVIE_PATHS", "NOT SET")
radarr_paths = os.environ.get("RADARR_ROOT_FOLDERS", "NOT SET")
print(f"MOVIE_PATHS: {movie_paths}")
print(f"RADARR_ROOT_FOLDERS: {radarr_paths}")
print()
# Parse paths
if movie_paths != "NOT SET":
container_paths = [p.strip() for p in movie_paths.split(",") if p.strip()]
print("Container paths:")
for i, path in enumerate(container_paths):
exists = Path(path).exists()
print(f" {i+1}. {path} {'' if exists else '❌ (does not exist)'}")
if radarr_paths != "NOT SET":
radarr_root_paths = [p.strip() for p in radarr_paths.split(",") if p.strip()]
print("\nRadarr root paths:")
for i, path in enumerate(radarr_root_paths):
print(f" {i+1}. {path}")
print("\n🔍 Expected Path Mappings:")
if movie_paths != "NOT SET" and radarr_paths != "NOT SET":
container_list = [p.strip() for p in movie_paths.split(",") if p.strip()]
radarr_list = [p.strip() for p in radarr_paths.split(",") if p.strip()]
for i, container_path in enumerate(container_list):
if i < len(radarr_list):
radarr_path = radarr_list[i]
print(f" {radarr_path}{container_path}")
else:
print(f" ❌ No Radarr mapping for {container_path}")
print("\n🧪 Test Case:")
test_radarr_path = "/mnt/unionfs/Media/Movies/movies6/Annabelle Comes Home (2019) [tt8350360]"
print(f"Radarr path: {test_radarr_path}")
# Simulate mapping
if radarr_paths != "NOT SET" and movie_paths != "NOT SET":
container_list = [p.strip() for p in movie_paths.split(",") if p.strip()]
radarr_list = [p.strip() for p in radarr_paths.split(",") if p.strip()]
for i, radarr_root in enumerate(radarr_list):
if test_radarr_path.startswith(radarr_root):
container_root = container_list[i] if i < len(container_list) else container_list[0]
relative_path = test_radarr_path[len(radarr_root):].lstrip("/")
mapped_path = f"{container_root.rstrip('/')}/{relative_path}" if relative_path else container_root.rstrip("/")
print(f"Expected mapping: {mapped_path}")
# Check if mapped path exists
if Path(mapped_path).exists():
print("✅ Mapped path exists!")
else:
print("❌ Mapped path does not exist!")
print(f" This could be why the wrong movie is being processed.")
break
else:
print("❌ No mapping found for test path")
print("\n🛠️ Recommended Fix:")
print("Make sure your MOVIE_PATHS and RADARR_ROOT_FOLDERS match exactly:")
print("Example:")
print(" MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6")
print(" RADARR_ROOT_FOLDERS=/mnt/unionfs/Media/Movies/movies,/mnt/unionfs/Media/Movies/movies6")
if __name__ == "__main__":
check_path_mapping()
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
Debug script for webhook processing issue
Helps identify why wrong movies are being processed
"""
import requests
import json
import time
from datetime import datetime
def check_batch_status():
"""Check the current batch queue status"""
try:
response = requests.get("http://localhost:8080/batch/status")
if response.status_code == 200:
data = response.json()
print(f"[{datetime.now().isoformat()}] Batch Status:")
print(f" Pending batches: {data.get('pending_batches', [])}")
print(f" Active timers: {data.get('active_timers', [])}")
if 'batch_details' in data:
print(" Batch details:")
for key, details in data['batch_details'].items():
print(f" {key}: {details['movie_title']} (IMDb: {details['imdb_id']})")
print()
else:
print(f"Failed to get batch status: {response.status_code}")
except Exception as e:
print(f"Error checking batch status: {e}")
def simulate_radarr_webhook(imdb_id, title, movie_path):
"""Simulate a Radarr webhook"""
webhook_payload = {
"eventType": "Download",
"movie": {
"id": 1,
"title": title,
"imdbId": imdb_id,
"path": movie_path
},
"movieFile": {
"id": 1,
"path": f"{movie_path}/movie.mkv"
}
}
try:
print(f"[{datetime.now().isoformat()}] Sending webhook for {title} ({imdb_id})")
response = requests.post(
"http://localhost:8080/webhook/radarr",
json=webhook_payload,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
result = response.json()
print(f" ✅ Webhook accepted: {result}")
else:
print(f" ❌ Webhook failed: {response.status_code} - {response.text}")
except Exception as e:
print(f" ❌ Error sending webhook: {e}")
def main():
print("🔍 NFOGuard Webhook Debugging Tool")
print("=" * 50)
# Check initial batch status
print("1. Initial batch queue status:")
check_batch_status()
# Send test webhook for Annabelle Comes Home
print("2. Sending test webhook for Annabelle Comes Home:")
simulate_radarr_webhook(
"tt8350360",
"Annabelle Comes Home",
"/mnt/unionfs/Media/Movies/movies6/Annabelle Comes Home (2019) [tt8350360]"
)
# Check batch status immediately after
print("3. Batch status immediately after webhook:")
check_batch_status()
# Wait for processing
print("4. Waiting 6 seconds for processing...")
time.sleep(6)
# Check final batch status
print("5. Final batch status:")
check_batch_status()
print("\n🔍 Check the NFOGuard logs to see which movie was actually processed!")
print("Expected: Annabelle Comes Home (2019) [tt8350360]")
print("If you see a different movie, there's a batching bug.")
if __name__ == "__main__":
main()
+46 -43
View File
@@ -942,7 +942,7 @@ class MovieProcessor:
_log("ERROR", f"No IMDb ID found in movie path: {movie_path}") _log("ERROR", f"No IMDb ID found in movie path: {movie_path}")
return return
_log("INFO", f"Processing movie: {movie_path.name}") _log("INFO", f"Processing movie: {movie_path.name} (IMDb: {imdb_id})")
# Update database # Update database
self.db.upsert_movie(imdb_id, str(movie_path)) self.db.upsert_movie(imdb_id, str(movie_path))
@@ -1259,6 +1259,7 @@ class WebhookBatcher:
webhook_data['media_type'] = media_type webhook_data['media_type'] = media_type
self.pending[key] = webhook_data self.pending[key] = webhook_data
_log("INFO", f"Batched {media_type} webhook for {key}") _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]) timer = threading.Timer(config.batch_delay, self._process_item, args=[key])
self.timers[key] = timer 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})") _log("ERROR", f"Could not find series directory: {series_title} ({imdb_id})")
return {"status": "error", "reason": "Series directory not found"} 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 = { webhook_dict = {
'path': str(series_path), 'path': str(series_path),
'series_info': series_info, '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 'episodes': webhook.episodes or [], # Include episode data for targeted processing
'processing_mode': config.tv_webhook_processing_mode '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: except Exception as e:
_log("ERROR", f"Sonarr webhook error: {e}") _log("ERROR", f"Sonarr webhook error: {e}")
@@ -1445,55 +1447,56 @@ async def radarr_webhook(request: Request, background_tasks: BackgroundTasks):
"""Handle Radarr webhooks""" """Handle Radarr webhooks"""
try: try:
payload = await _read_payload(request) payload = await _read_payload(request)
if not payload: _log("INFO", f"Received Radarr webhook: {payload.get('eventType', 'Unknown')}")
raise HTTPException(status_code=422, detail="Empty Radarr payload") _log("DEBUG", f"Full Radarr webhook payload: {payload}")
webhook = RadarrWebhook(**payload) # Extract movie info
_log("INFO", f"Received Radarr webhook: {webhook.eventType}") 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"]: # Get IMDb ID for batching key
return {"status": "ignored", "reason": f"Event type {webhook.eventType} not processed"} imdb_id = movie_data.get("imdbId", "").lower()
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
if not imdb_id: 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: # Get movie path for verification
_log("ERROR", f"No IMDb ID available for movie: {movie_title}") movie_path = movie_data.get("path", "")
return {"status": "error", "reason": "No IMDb ID"} 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 # Create movie-specific webhook data with proper path validation
webhook_dict = { movie_webhook_data = {
'path': str(movie_path), 'path': container_path, # Use verified container path
'movie_info': movie_info, 'movie_info': movie_data,
'event_type': webhook.eventType '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: except Exception as e:
_log("ERROR", f"Radarr webhook error: {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 # API Endpoints
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Execute the git commit
cd /home/jskala/github/NFOguard
# Make commit script executable
chmod +x commit_webhook_fixes.sh
# Run the commit
./commit_webhook_fixes.sh