feat: add support for Maintainarr webhook
Local Docker Build (Dev) / build-dev (push) Successful in 4s

This commit is contained in:
2025-10-24 16:30:08 -04:00
parent f6c5ccd2b6
commit e054c945ff
4 changed files with 397 additions and 1 deletions
+12
View File
@@ -31,6 +31,18 @@ class RadarrWebhook(BaseModel):
extra = "allow"
class MaintainarrWebhook(BaseModel):
"""Maintainarr webhook payload model - uses template variables"""
notification_type: Optional[str] = None # e.g., "Media Removed"
subject: Optional[str] = None
message: Optional[str] = None
image: Optional[str] = None
extra: Optional[str] = None
class Config:
extra = "allow"
class HealthResponse(BaseModel):
"""Health check response model"""
status: str
+166 -1
View File
@@ -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)