"""Pydantic models for Sonarr webhook integration""" from pydantic import BaseModel, Field as PydanticField, validator from typing import Optional, Dict, Any, List from datetime import datetime from enum import Enum from sqlmodel import SQLModel, Field import uuid 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 = PydanticField(..., 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 SonarrMappingBase(SQLModel): sonarr_series_id: int = Field(index=True, unique=True) sonarr_title: str anime_provider: str anime_url: str anime_title: str lang: str = Field(default="vostfr") quality_preference: Optional[str] = None auto_download: bool = Field(default=True) created_at: datetime = Field(default_factory=datetime.now) updated_at: datetime = Field(default_factory=datetime.now) class SonarrMappingTable(SonarrMappingBase, table=True): """Database table for Sonarr mappings""" __tablename__ = "sonarr_mappings" id: str = Field( default_factory=lambda: str(uuid.uuid4()), primary_key=True, index=True, nullable=False ) user_id: str = Field(foreign_key="users.id", index=True, default="default") class SonarrMapping(BaseModel): """Mapping between Sonarr series and anime providers (API model)""" sonarr_series_id: int sonarr_title: str anime_provider: str # 'anime-sama', 'anime-ultime', 'vostfree', 'french-manga', 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 = PydanticField(default_factory=datetime.now) updated_at: datetime = PydanticField(default_factory=datetime.now) class Config: json_encoders = { datetime: lambda v: v.isoformat() } class SonarrConfigBase(SQLModel): webhook_enabled: bool = Field(default=False) webhook_secret: Optional[str] = None auto_download_enabled: bool = Field(default=True) default_language: str = Field(default="vostfr") default_quality: Optional[str] = None default_provider: str = Field(default="anime-sama") verify_hmac: bool = Field(default=False) log_webhooks: bool = Field(default=True) class SonarrConfigTable(SonarrConfigBase, table=True): """Database table for Sonarr configuration (singleton)""" __tablename__ = "sonarr_config" id: str = Field( default_factory=lambda: str(uuid.uuid4()), primary_key=True, index=True, nullable=False ) class SonarrConfig(BaseModel): """Sonarr webhook configuration (API Model)""" 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" } }