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:
root
2026-01-24 21:25:47 +00:00
parent 92ef76ed2a
commit 1fe7392063
49 changed files with 8651 additions and 2110 deletions
+198
View File
@@ -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"
}
}