feat: migrate persistence from JSON to SQLModel (Phase 1)
- Integrated SQLModel with SQLite for robust data persistence - Refactored UserManager and WatchlistManager to use SQL queries - Migrated models to SQLModel with relationships and primary keys - Updated test suite with in-memory database isolation - Removed deprecated JSON storage files
This commit is contained in:
+82
-46
@@ -1,8 +1,11 @@
|
||||
"""Pydantic models for Watchlist and Auto-Download system"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Literal
|
||||
"""Models for Watchlist and Auto-Download system with SQLModel support"""
|
||||
import uuid
|
||||
import json
|
||||
from pydantic import BaseModel, Field as PydanticField
|
||||
from typing import Optional, Literal, List
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from sqlmodel import SQLModel, Field, Relationship, Column, String
|
||||
|
||||
|
||||
class WatchlistStatus(str, Enum):
|
||||
@@ -21,34 +24,80 @@ class QualityPreference(str, Enum):
|
||||
P480 = "480p" # SD
|
||||
|
||||
|
||||
class WatchlistItem(BaseModel):
|
||||
"""An anime being tracked for automatic episode downloads"""
|
||||
id: str = Field(..., description="Unique identifier (UUID)")
|
||||
user_id: str = Field(..., description="User ID who owns this watchlist item")
|
||||
anime_title: str = Field(..., description="Title of the anime")
|
||||
anime_url: str = Field(..., description="URL to the anime page")
|
||||
provider_id: str = Field(..., description="Provider ID (animesama, nekosama, etc.)")
|
||||
lang: Literal["vostfr", "vf"] = Field(default="vostfr", description="Language preference")
|
||||
class WatchlistItemBase(SQLModel):
|
||||
"""Base schema for watchlist items"""
|
||||
anime_title: str = Field(index=True)
|
||||
anime_url: str
|
||||
provider_id: str
|
||||
lang: str = Field(default="vostfr")
|
||||
|
||||
# Tracking state
|
||||
last_checked: Optional[datetime] = Field(None, description="Last time we checked for new episodes")
|
||||
last_episode_downloaded: int = Field(default=0, description="Last episode number downloaded")
|
||||
total_episodes: Optional[int] = Field(None, description="Total episodes if known")
|
||||
last_checked: Optional[datetime] = None
|
||||
last_episode_downloaded: int = Field(default=0)
|
||||
total_episodes: Optional[int] = None
|
||||
|
||||
# Settings
|
||||
auto_download: bool = Field(default=True, description="Automatically download new episodes")
|
||||
quality_preference: QualityPreference = Field(default=QualityPreference.AUTO, description="Preferred quality")
|
||||
status: WatchlistStatus = Field(default=WatchlistStatus.ACTIVE, description="Tracking status")
|
||||
auto_download: bool = Field(default=True)
|
||||
quality_preference: QualityPreference = Field(default=QualityPreference.AUTO)
|
||||
status: WatchlistStatus = Field(default=WatchlistStatus.ACTIVE)
|
||||
|
||||
# Metadata
|
||||
poster_image: Optional[str] = Field(None, description="URL to poster image")
|
||||
cover_image: Optional[str] = Field(None, description="URL to cover image")
|
||||
synopsis: Optional[str] = Field(None, description="Anime synopsis")
|
||||
genres: list[str] = Field(default_factory=list, description="Anime genres")
|
||||
|
||||
poster_image: Optional[str] = None
|
||||
cover_image: Optional[str] = None
|
||||
synopsis: Optional[str] = None
|
||||
|
||||
# Timestamps
|
||||
added_at: datetime = Field(default_factory=datetime.now, description="When added to watchlist")
|
||||
updated_at: datetime = Field(default_factory=datetime.now, description="Last update time")
|
||||
added_at: datetime = Field(default_factory=datetime.now)
|
||||
updated_at: datetime = Field(default_factory=datetime.now)
|
||||
|
||||
|
||||
class WatchlistItemTable(WatchlistItemBase, table=True):
|
||||
"""Database table for watchlist items"""
|
||||
__tablename__ = "watchlist_items"
|
||||
|
||||
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)
|
||||
|
||||
# Store list as JSON string in SQLite
|
||||
genres_json: Optional[str] = Field(default="[]", sa_column=Column(String))
|
||||
|
||||
@property
|
||||
def genres(self) -> List[str]:
|
||||
return json.loads(self.genres_json or "[]")
|
||||
|
||||
@genres.setter
|
||||
def genres(self, value: List[str]):
|
||||
self.genres_json = json.dumps(value or [])
|
||||
|
||||
# Relationships
|
||||
user: Optional["UserTable"] = Relationship(back_populates="watchlist_items")
|
||||
|
||||
|
||||
class WatchlistItem(BaseModel):
|
||||
"""An anime being tracked for automatic episode downloads (API Response)"""
|
||||
id: str
|
||||
user_id: str
|
||||
anime_title: str
|
||||
anime_url: str
|
||||
provider_id: str
|
||||
lang: str
|
||||
last_checked: Optional[datetime] = None
|
||||
last_episode_downloaded: int = 0
|
||||
total_episodes: Optional[int] = None
|
||||
auto_download: bool = True
|
||||
quality_preference: QualityPreference = QualityPreference.AUTO
|
||||
status: WatchlistStatus = WatchlistStatus.ACTIVE
|
||||
poster_image: Optional[str] = None
|
||||
cover_image: Optional[str] = None
|
||||
synopsis: Optional[str] = None
|
||||
genres: List[str] = []
|
||||
added_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
@@ -64,12 +113,10 @@ class WatchlistItemCreate(BaseModel):
|
||||
lang: Literal["vostfr", "vf"] = "vostfr"
|
||||
auto_download: bool = True
|
||||
quality_preference: QualityPreference = QualityPreference.AUTO
|
||||
|
||||
# Optional metadata
|
||||
poster_image: Optional[str] = None
|
||||
cover_image: Optional[str] = None
|
||||
synopsis: Optional[str] = None
|
||||
genres: list[str] = []
|
||||
genres: List[str] = []
|
||||
|
||||
|
||||
class WatchlistItemUpdate(BaseModel):
|
||||
@@ -96,26 +143,15 @@ class AutoDownloadResult(BaseModel):
|
||||
watchlist_item_id: str
|
||||
anime_title: str
|
||||
new_episodes_found: int
|
||||
episodes_downloaded: list[int] = Field(default_factory=list)
|
||||
episodes_failed: list[tuple[int, str]] = Field(default_factory=list) # (episode_number, error_message)
|
||||
checked_at: datetime = Field(default_factory=datetime.now)
|
||||
episodes_downloaded: list[int] = PydanticField(default_factory=list)
|
||||
episodes_failed: list[tuple[int, str]] = PydanticField(default_factory=list)
|
||||
checked_at: datetime = PydanticField(default_factory=datetime.now)
|
||||
|
||||
|
||||
class WatchlistSettings(BaseModel):
|
||||
"""Global watchlist settings"""
|
||||
check_interval_hours: int = Field(default=6, ge=1, le=168, description="Check interval (1-168 hours)")
|
||||
auto_download_enabled: bool = Field(default=True, description="Global auto-download toggle")
|
||||
max_concurrent_auto_downloads: int = Field(default=2, ge=1, le=10, description="Max concurrent auto-downloads")
|
||||
notify_on_new_episodes: bool = Field(default=False, description="Send notifications for new episodes")
|
||||
include_completed_anime: bool = Field(default=False, description="Check completed anime too")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"check_interval_hours": 6,
|
||||
"auto_download_enabled": True,
|
||||
"max_concurrent_auto_downloads": 2,
|
||||
"notify_on_new_episodes": False,
|
||||
"include_completed_anime": False
|
||||
}
|
||||
}
|
||||
check_interval_hours: int = PydanticField(default=6, ge=1, le=168)
|
||||
auto_download_enabled: bool = PydanticField(default=True)
|
||||
max_concurrent_auto_downloads: int = PydanticField(default=2, ge=1, le=10)
|
||||
notify_on_new_episodes: bool = PydanticField(default=False)
|
||||
include_completed_anime: bool = PydanticField(default=False)
|
||||
|
||||
Reference in New Issue
Block a user