feat: add support for Maintainarr webhook
Local Docker Build (Dev) / build-dev (push) Successful in 4s
Local Docker Build (Dev) / build-dev (push) Successful in 4s
This commit is contained in:
+166
-1
@@ -12,7 +12,7 @@ from typing import Optional
|
||||
|
||||
# Import models
|
||||
from api.models import (
|
||||
SonarrWebhook, RadarrWebhook, HealthResponse, TVSeasonRequest, TVEpisodeRequest,
|
||||
SonarrWebhook, RadarrWebhook, MaintainarrWebhook, HealthResponse, TVSeasonRequest, TVEpisodeRequest,
|
||||
MovieUpdateRequest, EpisodeUpdateRequest, BulkUpdateRequest
|
||||
)
|
||||
# Web routes removed - handled by separate web container
|
||||
@@ -327,6 +327,167 @@ async def radarr_webhook(request: Request, background_tasks: BackgroundTasks, de
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
async def maintainarr_webhook(request: Request, background_tasks: BackgroundTasks, dependencies: dict):
|
||||
"""Handle Maintainarr webhooks for media deletion"""
|
||||
db = dependencies["db"]
|
||||
config = dependencies["config"]
|
||||
|
||||
try:
|
||||
payload = await _read_payload(request)
|
||||
if not payload:
|
||||
raise HTTPException(status_code=422, detail="Empty Maintainarr payload")
|
||||
|
||||
webhook = MaintainarrWebhook(**payload)
|
||||
print(f"INFO: Received Maintainarr webhook: {webhook.notification_type}")
|
||||
print(f"DEBUG: Full Maintainarr webhook payload: {payload}")
|
||||
|
||||
# Only process media removal notifications
|
||||
notification_type = webhook.notification_type or ""
|
||||
if "removed" not in notification_type.lower() and "delete" not in notification_type.lower():
|
||||
return {"status": "ignored", "reason": f"Notification type '{notification_type}' not processed"}
|
||||
|
||||
# Parse message to extract media information
|
||||
message = webhook.message or ""
|
||||
subject = webhook.subject or ""
|
||||
extra = webhook.extra or ""
|
||||
|
||||
# Try to extract IMDb ID from message, subject, or extra data
|
||||
import re
|
||||
imdb_pattern = r'tt\d{7,8}|\b\d{7,8}\b'
|
||||
|
||||
imdb_id = None
|
||||
title = "Unknown Media"
|
||||
media_type = "Unknown"
|
||||
|
||||
# Look for IMDb ID in all fields
|
||||
for field in [message, subject, extra]:
|
||||
if field:
|
||||
imdb_matches = re.findall(imdb_pattern, field)
|
||||
if imdb_matches:
|
||||
imdb_id = imdb_matches[0]
|
||||
if not imdb_id.startswith("tt"):
|
||||
imdb_id = f"tt{imdb_id}"
|
||||
break
|
||||
|
||||
if not imdb_id:
|
||||
print(f"WARNING: No IMDb ID found in Maintainarr webhook - Message: '{message}', Subject: '{subject}'")
|
||||
return {"status": "ignored", "reason": "No IMDb ID found in webhook payload"}
|
||||
|
||||
# Try to extract title from subject or message
|
||||
if subject and subject.strip():
|
||||
title = subject.strip()
|
||||
elif message and message.strip():
|
||||
# Extract title from message if possible
|
||||
title_match = re.search(r'(?:movie|series|show)\s*[:\-]?\s*(.+?)(?:\s*\(|$)', message, re.IGNORECASE)
|
||||
if title_match:
|
||||
title = title_match.group(1).strip()
|
||||
else:
|
||||
title = message.strip()
|
||||
|
||||
# Try to determine media type from message content
|
||||
if any(word in message.lower() for word in ["series", "show", "tv", "season", "episode"]):
|
||||
media_type = "Series"
|
||||
elif any(word in message.lower() for word in ["movie", "film"]):
|
||||
media_type = "Movie"
|
||||
else:
|
||||
# Try both types - first check if it's a series, then movie
|
||||
series = db.get_series_by_imdb(imdb_id)
|
||||
movie = db.get_movie_by_imdb(imdb_id)
|
||||
|
||||
if series:
|
||||
media_type = "Series"
|
||||
elif movie:
|
||||
media_type = "Movie"
|
||||
else:
|
||||
print(f"INFO: Media {title} ({imdb_id}) not found in database")
|
||||
return {"status": "ignored", "reason": f"Media {imdb_id} not found in database"}
|
||||
|
||||
# Process deletion based on media type
|
||||
removed_count = 0
|
||||
removed_items = []
|
||||
|
||||
if media_type == "Movie":
|
||||
print(f"INFO: Processing movie deletion for {title} ({imdb_id})")
|
||||
|
||||
# Check if movie exists in database
|
||||
movie = db.get_movie_by_imdb(imdb_id)
|
||||
if movie:
|
||||
if db.delete_movie(imdb_id):
|
||||
removed_count += 1
|
||||
removed_items.append(f"Movie: {title} ({imdb_id})")
|
||||
print(f"SUCCESS: Removed movie {title} ({imdb_id}) from database")
|
||||
else:
|
||||
print(f"WARNING: Failed to remove movie {title} ({imdb_id}) from database")
|
||||
else:
|
||||
print(f"INFO: Movie {title} ({imdb_id}) not found in database")
|
||||
|
||||
elif media_type == "Series":
|
||||
print(f"INFO: Processing series deletion for {title} ({imdb_id})")
|
||||
|
||||
# Check if series exists in database
|
||||
series = db.get_series_by_imdb(imdb_id)
|
||||
if series:
|
||||
# Delete all episodes for the series
|
||||
episodes_removed = db.delete_series_episodes(imdb_id)
|
||||
if episodes_removed > 0:
|
||||
removed_count += episodes_removed
|
||||
removed_items.append(f"Series episodes: {title} ({imdb_id}) - {episodes_removed} episodes")
|
||||
print(f"SUCCESS: Removed {episodes_removed} episodes for series {title} ({imdb_id})")
|
||||
|
||||
# Delete the series record
|
||||
if db.delete_series(imdb_id):
|
||||
removed_count += 1
|
||||
removed_items.append(f"Series: {title} ({imdb_id})")
|
||||
print(f"SUCCESS: Removed series {title} ({imdb_id}) from database")
|
||||
else:
|
||||
print(f"WARNING: Failed to remove series {title} ({imdb_id}) from database")
|
||||
else:
|
||||
print(f"INFO: Series {title} ({imdb_id}) not found in database")
|
||||
|
||||
# Log the cleanup operation
|
||||
if removed_count > 0:
|
||||
background_tasks.add_task(
|
||||
_log_maintainarr_cleanup,
|
||||
notification_type,
|
||||
media_type,
|
||||
title,
|
||||
imdb_id,
|
||||
removed_items,
|
||||
webhook.subject
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Processed {notification_type} for {title}",
|
||||
"media_type": media_type,
|
||||
"imdb_id": imdb_id,
|
||||
"removed_count": removed_count,
|
||||
"removed_items": removed_items
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Maintainarr webhook error: {e}")
|
||||
import traceback
|
||||
print(f"ERROR: Traceback: {traceback.format_exc()}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
async def _log_maintainarr_cleanup(event_type: str, media_type: str, title: str, imdb_id: str, removed_items: list, collection_name: str = None):
|
||||
"""Background task to log Maintainarr cleanup operations"""
|
||||
try:
|
||||
log_message = f"Maintainarr cleanup: {event_type} - {media_type} '{title}' ({imdb_id})"
|
||||
if collection_name:
|
||||
log_message += f" from collection '{collection_name}'"
|
||||
log_message += f". Removed from database: {', '.join(removed_items)}"
|
||||
|
||||
print(f"INFO: {log_message}")
|
||||
|
||||
# Could extend this to write to a cleanup log file or database table
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to log Maintainarr cleanup: {e}")
|
||||
|
||||
|
||||
async def health(dependencies: dict) -> HealthResponse:
|
||||
"""Health check endpoint with Radarr database status"""
|
||||
db = dependencies["db"]
|
||||
@@ -1933,6 +2094,10 @@ def register_routes(app, dependencies: dict):
|
||||
async def _radarr_webhook(request: Request, background_tasks: BackgroundTasks):
|
||||
return await radarr_webhook(request, background_tasks, dependencies)
|
||||
|
||||
@app.post("/webhook/maintainarr")
|
||||
async def _maintainarr_webhook(request: Request, background_tasks: BackgroundTasks):
|
||||
return await maintainarr_webhook(request, background_tasks, dependencies)
|
||||
|
||||
@app.get("/health")
|
||||
async def _health() -> HealthResponse:
|
||||
return await health(dependencies)
|
||||
|
||||
Reference in New Issue
Block a user