mirror of
https://github.com/myronblair/kino-app
synced 2026-07-27 21:18:43 -05:00
f7626ced2e
- Removed the "Who's Watching" sub-profile picker entirely: ProfileProvider now auto-resolves the single profile tied to the account immediately after login - Backend: removed POST/DELETE /profiles (no more creating/deleting extra profiles — lifecycle is entirely driven by user create/delete, already wired in the admin Users CRUD); kept GET (internal) and PATCH (edit your own profile's avatar/kids-mode/rating cap) - Deleted ProfileSelect page and the now-unused ProtectedRoute component - Navbar: removed the "switch profile" action, account avatar is now just a static display - api.js: dropped the now-dead X-Profile-Id header logic (backend already falls back to the account's single profile when the header is absent)
372 lines
9.0 KiB
Python
372 lines
9.0 KiB
Python
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 PasswordChange(BaseModel):
|
|
current_password: str
|
|
new_password: str
|
|
|
|
|
|
class UserPublic(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
id: str
|
|
email: str
|
|
name: str
|
|
is_admin: bool = False
|
|
created_at: str
|
|
|
|
|
|
class AdminUserCreate(BaseModel):
|
|
email: str
|
|
password: str
|
|
name: str
|
|
is_admin: bool = False
|
|
|
|
|
|
class AdminUserUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
is_admin: Optional[bool] = None
|
|
password: Optional[str] = None
|
|
|
|
|
|
# ---------- Profile (1:1 with account) ----------
|
|
RATING_ORDER = ["G", "PG", "PG-13", "R", "NR"]
|
|
|
|
|
|
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 = ""
|
|
sonarr_url: str = ""
|
|
sonarr_api_key: str = ""
|
|
opensubs_api_key: str = ""
|
|
trakt_client_id: str = ""
|
|
trakt_client_secret: str = ""
|
|
auto_transcode: str = "off"
|
|
queue_paused: bool = False
|
|
|
|
|
|
class AppSettingsPublic(BaseModel):
|
|
tmdb_configured: bool = False
|
|
radarr_configured: bool = False
|
|
sonarr_configured: bool = False
|
|
opensubs_configured: bool = False
|
|
trakt_configured: bool = False
|
|
tmdb_api_key: str = ""
|
|
radarr_url: str = ""
|
|
radarr_api_key: str = ""
|
|
sonarr_url: str = ""
|
|
sonarr_api_key: str = ""
|
|
opensubs_api_key: str = ""
|
|
trakt_client_id: str = ""
|
|
trakt_client_secret: str = ""
|
|
auto_transcode: str = "off"
|
|
queue_paused: bool = False
|
|
|
|
|
|
# ---------- Shows / Episodes (Sonarr) ----------
|
|
class ShowBase(BaseModel):
|
|
title: str
|
|
description: str = ""
|
|
year: int = 2024
|
|
rating: str = "NR"
|
|
genres: List[str] = []
|
|
cast: List[str] = []
|
|
creator: str = ""
|
|
poster_url: str = ""
|
|
backdrop_url: str = ""
|
|
featured: bool = False
|
|
tvdb_id: Optional[int] = None
|
|
tmdb_id: Optional[int] = None
|
|
sonarr_id: Optional[int] = None
|
|
|
|
|
|
class Show(ShowBase):
|
|
model_config = ConfigDict(extra="ignore")
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
episode_count: int = 0
|
|
season_count: int = 0
|
|
created_at: str = Field(default_factory=_now_iso)
|
|
|
|
|
|
class ShowCreate(ShowBase):
|
|
pass
|
|
|
|
|
|
class ShowUpdate(BaseModel):
|
|
title: Optional[str] = None
|
|
description: Optional[str] = None
|
|
year: Optional[int] = None
|
|
rating: Optional[str] = None
|
|
genres: Optional[List[str]] = None
|
|
poster_url: Optional[str] = None
|
|
backdrop_url: Optional[str] = None
|
|
featured: Optional[bool] = None
|
|
|
|
|
|
class EpisodeBase(BaseModel):
|
|
show_id: str
|
|
season_number: int
|
|
episode_number: int
|
|
title: str
|
|
description: str = ""
|
|
duration_minutes: int = 0
|
|
air_date: str = ""
|
|
still_url: str = ""
|
|
storage_type: str = "external" # local | sonarr | external
|
|
storage_path: Optional[str] = None
|
|
video_url: str = ""
|
|
|
|
|
|
class Episode(EpisodeBase):
|
|
model_config = ConfigDict(extra="ignore")
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
sonarr_episode_id: Optional[int] = None
|
|
hidden: bool = False
|
|
hls_path: Optional[str] = None
|
|
hls_status: Optional[str] = None
|
|
created_at: str = Field(default_factory=_now_iso)
|
|
|
|
|
|
class EpisodeCreate(EpisodeBase):
|
|
sonarr_episode_id: Optional[int] = None
|
|
|
|
|
|
class EpisodeProgressUpsert(BaseModel):
|
|
episode_id: str
|
|
position_seconds: float
|
|
duration_seconds: float
|
|
|
|
|
|
class SonarrSeriesDTO(BaseModel):
|
|
sonarr_id: int
|
|
title: str
|
|
year: Optional[int] = None
|
|
overview: str = ""
|
|
poster_url: str = ""
|
|
tvdb_id: Optional[int] = None
|
|
tmdb_id: Optional[int] = None
|
|
season_count: int = 0
|
|
episode_file_count: int = 0
|
|
|
|
|
|
# ---------- Trakt ----------
|
|
class TraktDeviceCodeResponse(BaseModel):
|
|
device_code: str
|
|
user_code: str
|
|
verification_url: str
|
|
expires_in: int
|
|
interval: int
|
|
|
|
|
|
class TraktStatus(BaseModel):
|
|
connected: bool
|
|
username: str = ""
|
|
|
|
|
|
# ---------- OpenSubtitles ----------
|
|
class OpenSubsResult(BaseModel):
|
|
file_id: int
|
|
language: str
|
|
label: str = ""
|
|
release: str = ""
|
|
fps: float = 0
|
|
downloads: int = 0
|
|
|
|
|
|
# ---------- Transcode Queue ----------
|
|
class TranscodeJob(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
movie_id: str # generic content id (movie_id OR episode_id, depending on content_type)
|
|
movie_title: str = ""
|
|
content_type: str = "movie" # movie | episode
|
|
quality: str = "abr"
|
|
status: str = "pending"
|
|
error: str = ""
|
|
triggered_by: str = "manual"
|
|
created_at: str = Field(default_factory=_now_iso)
|
|
started_at: Optional[str] = None
|
|
finished_at: Optional[str] = None
|
|
|
|
|
|
class QueuePauseToggle(BaseModel):
|
|
paused: bool
|
|
|
|
|
|
# ---------- 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
|