from pydantic import BaseModel, Field, ConfigDict from typing import List, Optional from datetime import datetime, timezone import uuid def _now_iso() -> str: return datetime.now(timezone.utc).isoformat() # ---------- Users ---------- class UserCreate(BaseModel): email: str password: str name: str class UserLogin(BaseModel): email: str password: str class UserPublic(BaseModel): model_config = ConfigDict(extra="ignore") id: str email: str name: str is_admin: bool = False created_at: str # ---------- Profiles ("Who's Watching") ---------- RATING_ORDER = ["G", "PG", "PG-13", "R", "NR"] class ProfileCreate(BaseModel): name: str avatar_color: str = "#D9381E" is_kids: bool = False max_rating: str = "NR" # one of RATING_ORDER class ProfileUpdate(BaseModel): name: Optional[str] = None avatar_color: Optional[str] = None is_kids: Optional[bool] = None max_rating: Optional[str] = None class Profile(BaseModel): model_config = ConfigDict(extra="ignore") id: str = Field(default_factory=lambda: str(uuid.uuid4())) user_id: str name: str avatar_color: str = "#D9381E" is_kids: bool = False max_rating: str = "NR" created_at: str = Field(default_factory=_now_iso) # ---------- Movies ---------- class MovieBase(BaseModel): title: str description: str = "" year: int = 2024 duration_minutes: int = 0 rating: str = "NR" genres: List[str] = [] cast: List[str] = [] director: str = "" poster_url: str = "" backdrop_url: str = "" video_url: str = "" storage_type: str = "external" # external | local | radarr storage_path: Optional[str] = None featured: bool = False tmdb_id: Optional[int] = None radarr_id: Optional[int] = None hls_path: Optional[str] = None hls_status: Optional[str] = None # pending|running|done|failed class MovieCreate(MovieBase): pass class MovieUpdate(BaseModel): title: Optional[str] = None description: Optional[str] = None year: Optional[int] = None duration_minutes: Optional[int] = None rating: Optional[str] = None genres: Optional[List[str]] = None cast: Optional[List[str]] = None director: Optional[str] = None poster_url: Optional[str] = None backdrop_url: Optional[str] = None video_url: Optional[str] = None storage_type: Optional[str] = None storage_path: Optional[str] = None featured: Optional[bool] = None tmdb_id: Optional[int] = None class Movie(MovieBase): model_config = ConfigDict(extra="ignore") id: str = Field(default_factory=lambda: str(uuid.uuid4())) created_at: str = Field(default_factory=_now_iso) # ---------- Subtitles ---------- class Subtitle(BaseModel): model_config = ConfigDict(extra="ignore") id: str = Field(default_factory=lambda: str(uuid.uuid4())) movie_id: str language: str = "en" # ISO code label: str = "English" storage_path: str # filename inside MEDIA_ROOT/subtitles is_default: bool = False created_at: str = Field(default_factory=_now_iso) # ---------- Watchlist / Progress (now profile-scoped) ---------- class WatchlistItem(BaseModel): model_config = ConfigDict(extra="ignore") id: str = Field(default_factory=lambda: str(uuid.uuid4())) profile_id: str user_id: str movie_id: str added_at: str = Field(default_factory=_now_iso) class ProgressUpsert(BaseModel): movie_id: str position_seconds: float duration_seconds: float class Progress(BaseModel): model_config = ConfigDict(extra="ignore") profile_id: str user_id: str movie_id: str position_seconds: float duration_seconds: float updated_at: str = Field(default_factory=_now_iso) # ---------- Requests ---------- class RequestCreate(BaseModel): title: str year: Optional[int] = None notes: str = "" class RequestUpdate(BaseModel): status: str class MovieRequest(BaseModel): model_config = ConfigDict(extra="ignore") id: str = Field(default_factory=lambda: str(uuid.uuid4())) user_id: str user_name: str = "" title: str year: Optional[int] = None notes: str = "" status: str = "pending" created_at: str = Field(default_factory=_now_iso) # ---------- App Settings (admin) ---------- class AppSettings(BaseModel): model_config = ConfigDict(extra="ignore") tmdb_api_key: str = "" radarr_url: str = "" radarr_api_key: str = "" class AppSettingsPublic(BaseModel): """Settings visible to admin UI (does not redact, since admin already has access).""" tmdb_configured: bool = False radarr_configured: bool = False tmdb_api_key: str = "" radarr_url: str = "" radarr_api_key: str = "" # ---------- Auth Tokens ---------- class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" user: UserPublic # ---------- TMDB / Radarr DTOs ---------- class TMDBSearchResult(BaseModel): tmdb_id: int title: str year: Optional[int] = None overview: str = "" poster_url: str = "" backdrop_url: str = "" class RadarrMovieDTO(BaseModel): radarr_id: int title: str year: Optional[int] = None overview: str = "" has_file: bool = False file_path: Optional[str] = None poster_url: str = "" tmdb_id: Optional[int] = None