feat: Complete Sonarr integration with security enhancements
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>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
"""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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user