1fe7392063
This commit adds comprehensive Sonarr webhook integration and implements critical security improvements identified in code review. ## Sonarr Integration - Full webhook support for Grab, Download, Rename, Delete, and Test events - HMAC SHA256 signature verification for webhook authentication - Series mapping system (Sonarr TVDB ID → Anime Provider URL) - 11 new API endpoints for configuration, mappings, search, and downloads - Comprehensive test suite (31 tests, all passing) - Complete documentation in docs/SONARR_INTEGRATION.md ## Security Enhancements - CORS restricted to specific origins (user's IP: 192.168.1.204:3000) - Path traversal prevention via sanitize_filename() and is_safe_filename() - Structured logging infrastructure (replaced all print() statements) - Environment-based configuration with .env support - Filename sanitization prevents malicious path attacks ## New Features - Lpayer and Sibnet downloader support - Kitsu API integration for anime metadata - Recommendation engine based on download history - Latest releases endpoint for new anime - Modular web interface with component-based templates ## Configuration - Centralized settings via app/config.py with pydantic-settings - Sonarr config auto-created in config/ directory - Example configurations provided for easy setup ## Tests - 31 Sonarr integration tests (23 functionality + 9 security) - 100+ tests passing in core test files - Security utilities fully tested ## Documentation - Updated CLAUDE.md with Sonarr and testing info - Added IMPROVEMENTS_2024-01-24.md analysis - Added SONARR_IMPLEMENTATION.md technical summary Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
199 lines
5.5 KiB
Python
199 lines
5.5 KiB
Python
"""Pydantic models for Sonarr webhook integration"""
|
|
from pydantic import BaseModel, Field, validator
|
|
from typing import Optional, Dict, Any, List
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
|
|
class SonarrEventType(str, Enum):
|
|
"""Sonarr event types"""
|
|
GRAB = "Grab"
|
|
DOWNLOAD = "Download"
|
|
MOVIE_DELETE = "MovieDelete"
|
|
MOVIE_FILE_DELETE = "MovieFileDelete"
|
|
RENAME = "Rename"
|
|
DELETE = "Delete"
|
|
TEST = "Test"
|
|
|
|
|
|
class SonarrQuality(BaseModel):
|
|
"""Quality information from Sonarr"""
|
|
quality: Dict[str, Any]
|
|
revision: Dict[str, Any]
|
|
|
|
|
|
class SonarrRelease(BaseModel):
|
|
"""Release information from Sonarr"""
|
|
indexer: str
|
|
releaseTitle: str
|
|
quality: SonarrQuality
|
|
|
|
|
|
class SonarrEpisodeFile(BaseModel):
|
|
"""Episode file information"""
|
|
id: int
|
|
seriesId: int
|
|
seasonNumber: int
|
|
episodeNumber: int
|
|
relativePath: str
|
|
path: str
|
|
size: int
|
|
dateAdded: datetime
|
|
quality: SonarrQuality
|
|
mediaInfo: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class SonarrSeries(BaseModel):
|
|
"""Series information from Sonarr"""
|
|
tvdbId: int = Field(..., alias="tvdbId")
|
|
title: str
|
|
sortTitle: str
|
|
status: str
|
|
ended: bool
|
|
overview: str
|
|
network: str
|
|
airTime: str
|
|
images: List[Dict[str, Any]]
|
|
seasons: List[int]
|
|
year: int
|
|
path: str
|
|
qualityProfileId: int
|
|
languageProfileId: int
|
|
seasonFolder: bool
|
|
monitored: bool
|
|
useSceneNumbering: bool
|
|
runtime: int
|
|
tvRageId: Optional[int] = None
|
|
tvMazeId: Optional[int] = None
|
|
firstAired: Optional[datetime] = None
|
|
seriesType: str = "standard"
|
|
cleanTitle: str
|
|
imdbId: str
|
|
titleSlug: str
|
|
certification: str
|
|
genres: List[str]
|
|
tags: List[int]
|
|
added: datetime
|
|
ratings: Dict[str, Any]
|
|
id: int
|
|
|
|
class Config:
|
|
populate_by_name = True
|
|
|
|
|
|
class SonarrEpisode(BaseModel):
|
|
"""Episode information from Sonarr"""
|
|
seriesId: int
|
|
episodeFileId: int
|
|
seasonNumber: int
|
|
episodeNumber: int
|
|
title: str
|
|
airDate: str
|
|
airDateUtc: datetime
|
|
overview: str
|
|
hasFile: bool
|
|
monitored: bool
|
|
absoluteEpisodeNumber: Optional[int] = None
|
|
unverifiedSceneNumbering: bool = False
|
|
id: int
|
|
|
|
|
|
class SonarrWebhookPayload(BaseModel):
|
|
"""Main Sonarr webhook payload"""
|
|
eventType: SonarrEventType
|
|
instanceName: str
|
|
applicationUrl: str
|
|
series: Optional[SonarrSeries] = None
|
|
episodes: Optional[List[SonarrEpisode]] = None
|
|
release: Optional[SonarrRelease] = None
|
|
episodeFile: Optional[SonarrEpisodeFile] = None
|
|
deletedFiles: Optional[List[str]] = None
|
|
deleteEpisodeFiles: bool = False
|
|
|
|
@validator('episodes')
|
|
def validate_episodes(cls, v, values):
|
|
"""Ensure episodes are present for relevant event types"""
|
|
event_type = values.get('eventType')
|
|
if event_type in [SonarrEventType.GRAB, SonarrEventType.DOWNLOAD, SonarrEventType.RENAME]:
|
|
if not v or len(v) == 0:
|
|
raise ValueError(f"Event type {event_type} requires episodes")
|
|
return v
|
|
|
|
@validator('series')
|
|
def validate_series(cls, v, values):
|
|
"""Ensure series is present for relevant event types"""
|
|
event_type = values.get('eventType')
|
|
if event_type in [SonarrEventType.GRAB, SonarrEventType.DOWNLOAD, SonarrEventType.RENAME, SonarrEventType.DELETE]:
|
|
if not v:
|
|
raise ValueError(f"Event type {event_type} requires series")
|
|
return v
|
|
|
|
|
|
class SonarrMapping(BaseModel):
|
|
"""Mapping between Sonarr series and anime providers"""
|
|
sonarr_series_id: int
|
|
sonarr_title: str
|
|
anime_provider: str # 'anime-sama', 'neko-sama', etc.
|
|
anime_url: str
|
|
anime_title: str
|
|
lang: str = "vostfr"
|
|
quality_preference: Optional[str] = None # '1080p', '720p', etc.
|
|
auto_download: bool = True
|
|
created_at: datetime = Field(default_factory=datetime.now)
|
|
updated_at: datetime = Field(default_factory=datetime.now)
|
|
|
|
class Config:
|
|
json_encoders = {
|
|
datetime: lambda v: v.isoformat()
|
|
}
|
|
|
|
|
|
class SonarrConfig(BaseModel):
|
|
"""Sonarr webhook configuration"""
|
|
webhook_enabled: bool = False
|
|
webhook_secret: Optional[str] = None # HMAC SHA256 secret
|
|
auto_download_enabled: bool = True
|
|
default_language: str = "vostfr"
|
|
default_quality: Optional[str] = None
|
|
default_provider: str = "anime-sama"
|
|
verify_hmac: bool = False
|
|
log_webhooks: bool = True
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"webhook_enabled": True,
|
|
"webhook_secret": "your-secret-key-here",
|
|
"auto_download_enabled": True,
|
|
"default_language": "vostfr",
|
|
"default_quality": "1080p",
|
|
"default_provider": "anime-sama",
|
|
"verify_hmac": True,
|
|
"log_webhooks": True
|
|
}
|
|
}
|
|
|
|
|
|
class SonarrDownloadRequest(BaseModel):
|
|
"""Request to download anime based on Sonarr event"""
|
|
sonarr_series_id: int
|
|
sonarr_title: str
|
|
season_number: int
|
|
episode_number: int
|
|
quality: Optional[str] = None
|
|
lang: str = "vostfr"
|
|
provider: str = "anime-sama"
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"sonarr_series_id": 123,
|
|
"sonarr_title": "Naruto Shippuden",
|
|
"season_number": 1,
|
|
"episode_number": 1,
|
|
"quality": "1080p",
|
|
"lang": "vostfr",
|
|
"provider": "anime-sama"
|
|
}
|
|
}
|