Initial commit: WatchStack v0.1.0
- FastAPI + SQLite backend with REST API - Polymorphic Media model (book + series) - Cover lookup via Open Library - Vanilla HTML/CSS/JS frontend (MAL-inspired) - Dark theme, status tabs, drawer detail, stats modal - 8 seed demo entries
This commit is contained in:
+152
@@ -0,0 +1,152 @@
|
||||
"""Pydantic-Schemas für API-Ein- und -Ausgabe."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
# ---------- Eingaben ---------------------------------------------------------
|
||||
|
||||
MediaKind = Literal["book", "series"]
|
||||
MediaStatus = Literal["plan", "reading", "done", "hold", "dropped"]
|
||||
|
||||
|
||||
class MediaBase(BaseModel):
|
||||
kind: MediaKind
|
||||
title: str = Field(..., max_length=200)
|
||||
original_title: Optional[str] = Field(None, max_length=200)
|
||||
description: Optional[str] = None
|
||||
cover_url: Optional[str] = Field(None, max_length=600)
|
||||
release_year: Optional[int] = Field(None, ge=1800, le=2200)
|
||||
status: MediaStatus = "plan"
|
||||
rating: Optional[float] = Field(None, ge=0, le=10)
|
||||
notes: Optional[str] = None
|
||||
|
||||
# Buch-Felder
|
||||
author: Optional[str] = Field(None, max_length=200)
|
||||
isbn: Optional[str] = Field(None, max_length=20)
|
||||
total_volumes: Optional[int] = Field(None, ge=0)
|
||||
total_chapters: Optional[int] = Field(None, ge=0)
|
||||
total_pages: Optional[int] = Field(None, ge=0)
|
||||
volumes_read: Optional[int] = Field(None, ge=0)
|
||||
chapters_read: Optional[int] = Field(None, ge=0)
|
||||
pages_read: Optional[int] = Field(None, ge=0)
|
||||
|
||||
# Serien-Felder
|
||||
network: Optional[str] = Field(None, max_length=120)
|
||||
total_seasons: Optional[int] = Field(None, ge=0)
|
||||
total_episodes: Optional[int] = Field(None, ge=0)
|
||||
episodes_watched: Optional[int] = Field(None, ge=0)
|
||||
season_watching: Optional[int] = Field(None, ge=0)
|
||||
start_date: Optional[date] = None
|
||||
end_date: Optional[date] = None
|
||||
|
||||
genres: List[str] = []
|
||||
tags: List[str] = []
|
||||
|
||||
|
||||
class MediaCreate(MediaBase):
|
||||
pass
|
||||
|
||||
|
||||
class MediaUpdate(BaseModel):
|
||||
"""Alle Felder optional – PATCH-Semantik."""
|
||||
title: Optional[str] = Field(None, max_length=200)
|
||||
original_title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
cover_url: Optional[str] = None
|
||||
release_year: Optional[int] = None
|
||||
status: Optional[MediaStatus] = None
|
||||
rating: Optional[float] = Field(None, ge=0, le=10)
|
||||
notes: Optional[str] = None
|
||||
author: Optional[str] = None
|
||||
isbn: Optional[str] = None
|
||||
total_volumes: Optional[int] = None
|
||||
total_chapters: Optional[int] = None
|
||||
total_pages: Optional[int] = None
|
||||
volumes_read: Optional[int] = None
|
||||
chapters_read: Optional[int] = None
|
||||
pages_read: Optional[int] = None
|
||||
network: Optional[str] = None
|
||||
total_seasons: Optional[int] = None
|
||||
total_episodes: Optional[int] = None
|
||||
episodes_watched: Optional[int] = None
|
||||
season_watching: Optional[int] = None
|
||||
start_date: Optional[date] = None
|
||||
end_date: Optional[date] = None
|
||||
genres: Optional[List[str]] = None
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
|
||||
class ProgressUpdate(BaseModel):
|
||||
"""Schnelles Fortschritts-Update."""
|
||||
delta: int = 1
|
||||
set_to: Optional[int] = None # wenn angegeben, setzt absolut
|
||||
|
||||
|
||||
# ---------- Ausgaben ---------------------------------------------------------
|
||||
|
||||
class GenreOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
class TagOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
class MediaOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
kind: str
|
||||
title: str
|
||||
original_title: Optional[str]
|
||||
description: Optional[str]
|
||||
cover_url: Optional[str]
|
||||
release_year: Optional[int]
|
||||
status: str
|
||||
rating: Optional[float]
|
||||
notes: Optional[str]
|
||||
author: Optional[str]
|
||||
isbn: Optional[str]
|
||||
total_volumes: Optional[int]
|
||||
total_chapters: Optional[int]
|
||||
total_pages: Optional[int]
|
||||
volumes_read: Optional[int]
|
||||
chapters_read: Optional[int]
|
||||
pages_read: Optional[int]
|
||||
network: Optional[str]
|
||||
total_seasons: Optional[int]
|
||||
total_episodes: Optional[int]
|
||||
episodes_watched: Optional[int]
|
||||
season_watching: Optional[int]
|
||||
start_date: Optional[date]
|
||||
end_date: Optional[date]
|
||||
progress_percent: float
|
||||
genres: List[GenreOut]
|
||||
tags: List[TagOut]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class StatsBucket(BaseModel):
|
||||
status: str
|
||||
count: int
|
||||
|
||||
|
||||
class StatsPerKind(BaseModel):
|
||||
kind: str
|
||||
total: int
|
||||
avg_rating: Optional[float]
|
||||
by_status: List[StatsBucket]
|
||||
|
||||
|
||||
class StatsResponse(BaseModel):
|
||||
books: StatsPerKind
|
||||
series: StatsPerKind
|
||||
total_entries: int
|
||||
Reference in New Issue
Block a user