Files
ohm_streaming/app/models/sonarr.py
T
Kimi Agent 520be53901
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled
fix: migrations, auth, providers health check, E2E tests, remove neko-sama
- Add proper Alembic initial migration (0001_initial_schema.py)
- Migrate refresh tokens from JSON file to SQLite (RefreshTokenTable)
- Remove Neko-Sama provider entirely (redirects to Gupy, not a host)
- Fix provider health check always showing UNKNOWN
  - Run check_all_health() on startup
  - Fix POST /providers/health/check background task bug
  - Add HTMX refresh after manual health check trigger
- Fix anime search relevance scoring with MIN_RELEVANCE_THRESHOLD=0.5
- Replace bare 'except:' with 'except Exception:' across codebase
- Add Playwright E2E test suite (12 tests, auth setup, helpers)
- Fix toast container blocking clicks via pointer-events: none
- Remove obsolete Jest/Vite test files and config
- Clean up obsolete test_watchlist scripts
- Update sonarr model comment for active providers
2026-05-12 11:45:56 +00:00

248 lines
7.1 KiB
Python

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