Files
ki 9a4909f083
CI / test (push) Has been cancelled
chore(rebrand): rename WatchStack -> Quivio
Quivio (Kofferwort aus Quire + Video) ersetzt den Arbeitstitel WatchStack.
Es ist origineller, frei auf PyPI/npm, und passt perfekt zur Doppelnatur
der App: Buecher (Quire) + Serien (Video).

Geaendert:
- Python-Paket app/ -> quivio/ (+ alle Imports angepasst)
- DB-File watchstack.db -> quivio.db
- Docker-Image: quivio:0.5.1-beta, Container-Name quivio
- Compose-Services: quivio, quivio-local
- ENV-Vars: QUIVIO_VERSION, QUIVIO_PORT, Volume quivio-data
- Frontend-Logo "W" -> "Q", Titel "Quivio", Footer
- Favicon "Q" (Georgia serif fuer klassischen Look)
- Logger-Name: watchstack -> quivio
- README, NOTICE, dist/README, data/README: Quivio
- CI-Workflow: testet quivio/ Pfade
- build-and-push.sh: lokaler Tag "quivio:VERSION"
- service-Feld in /api/health: quivio
- OpenAPI title: Quivio

Bugfix (gefunden beim Renaming):
- seed.py hatte versteckten "from app.database import DB_PATH" — gefixt

Verifiziert end-to-end:
- Python-Import OK
- Backend: service=quivio, version=0.5.1-beta, 8 unique items
- Docker-Build OK, Push in Gitea-Registry OK (neuer sha256-Digest)
- Container: Quivio 0.5.1-beta ready (logger), HTML ohne WatchStack-Rest
- OpenAPI title=Quivio
- 8 unique items nach cold start (Race-Fix haelt)
2026-07-22 00:42:09 +02:00

156 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pydantic-Schemas für API-Ein- und -Ausgabe."""
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from datetime import date, datetime
from typing import List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
# ---------- Typen ------------------------------------------------------------
MediaKind = Literal["book", "series"]
MediaStatus = Literal["plan", "reading", "done", "hold", "dropped"]
# ---------- Eingaben ---------------------------------------------------------
class MediaBase(BaseModel):
kind: MediaKind
title: str = Field(..., max_length=200, min_length=1)
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, min_length=1)
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