Files
ohm_streaming/app/models/sonarr.py
T
root 1fe7392063 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>
2026-01-24 21:25:47 +00:00

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"
}
}