Phase 2 Complete: SQL migration with SQLModel and Alembic
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled

This commit is contained in:
root
2026-03-25 13:46:15 +00:00
parent 96b12b66e2
commit a684237725
21 changed files with 1148 additions and 466 deletions
+6
View File
@@ -63,3 +63,9 @@ class AnimeSearchResult(BaseModel):
cover_image: Optional[str] = None
type: str # "search_result" or "direct"
metadata: Optional[AnimeMetadata] = None
# Import all SQLModel tables here to ensure they are registered together
from .auth import UserTable
from .watchlist import WatchlistItemTable, WatchlistSettingsTable
from .favorites import FavoriteTable
from .sonarr import SonarrMappingTable, SonarrConfigTable
+4 -1
View File
@@ -28,7 +28,7 @@ class UserTable(UserBase, table=True):
created_at: datetime = Field(default_factory=datetime.now)
last_login: Optional[datetime] = None
# Relationships
# Relationships - Using string reference to avoid circular import errors
watchlist_items: List["WatchlistItemTable"] = Relationship(back_populates="user")
@@ -60,3 +60,6 @@ class Token(BaseModel):
class UserInDB(User):
"""Schema for user stored in database (with hashed password)"""
hashed_password: str
# Import WatchlistItemTable here to resolve SQLModel Relationship mappings
from .watchlist import WatchlistItemTable
+44
View File
@@ -0,0 +1,44 @@
"""Models for Favorites system with SQLModel support"""
import uuid
import json
from typing import Optional, Dict, List
from datetime import datetime
from sqlmodel import SQLModel, Field, Column, String
class FavoriteBase(SQLModel):
"""Base schema for favorite anime"""
anime_id: str = Field(index=True)
title: str = Field(index=True)
url: str
provider: str
poster_url: Optional[str] = None
# Timestamps
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
class FavoriteTable(FavoriteBase, table=True):
"""Database table for favorites"""
__tablename__ = "favorites"
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, default="default")
# Store metadata dictionary as JSON string in SQLite
metadata_json: Optional[str] = Field(default="{}", sa_column=Column(String))
@property
def anime_metadata(self) -> Dict:
try:
return json.loads(self.metadata_json or "{}")
except json.JSONDecodeError:
return {}
@anime_metadata.setter
def anime_metadata(self, value: Dict):
self.metadata_json = json.dumps(value or {})
+55 -6
View File
@@ -1,8 +1,10 @@
"""Pydantic models for Sonarr webhook integration"""
from pydantic import BaseModel, Field, validator
from pydantic import BaseModel, Field as PydanticField, validator
from typing import Optional, Dict, Any, List
from datetime import datetime
from enum import Enum
from sqlmodel import SQLModel, Field
import uuid
class SonarrEventType(str, Enum):
@@ -45,7 +47,7 @@ class SonarrEpisodeFile(BaseModel):
class SonarrSeries(BaseModel):
"""Series information from Sonarr"""
tvdbId: int = Field(..., alias="tvdbId")
tvdbId: int = PydanticField(..., alias="tvdbId")
title: str
sortTitle: str
status: str
@@ -129,8 +131,33 @@ class SonarrWebhookPayload(BaseModel):
return v
class SonarrMappingBase(SQLModel):
sonarr_series_id: int = Field(index=True, unique=True)
sonarr_title: str
anime_provider: str
anime_url: str
anime_title: str
lang: str = Field(default="vostfr")
quality_preference: Optional[str] = None
auto_download: bool = Field(default=True)
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
class SonarrMappingTable(SonarrMappingBase, table=True):
"""Database table for Sonarr mappings"""
__tablename__ = "sonarr_mappings"
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, default="default")
class SonarrMapping(BaseModel):
"""Mapping between Sonarr series and anime providers"""
"""Mapping between Sonarr series and anime providers (API model)"""
sonarr_series_id: int
sonarr_title: str
anime_provider: str # 'anime-sama', 'neko-sama', etc.
@@ -139,8 +166,8 @@ class SonarrMapping(BaseModel):
lang: str = "vostfr"
quality_preference: Optional[str] = None # '1080p', '720p', etc.
auto_download: bool = True
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
created_at: datetime = PydanticField(default_factory=datetime.now)
updated_at: datetime = PydanticField(default_factory=datetime.now)
class Config:
json_encoders = {
@@ -148,8 +175,30 @@ class SonarrMapping(BaseModel):
}
class SonarrConfigBase(SQLModel):
webhook_enabled: bool = Field(default=False)
webhook_secret: Optional[str] = None
auto_download_enabled: bool = Field(default=True)
default_language: str = Field(default="vostfr")
default_quality: Optional[str] = None
default_provider: str = Field(default="anime-sama")
verify_hmac: bool = Field(default=False)
log_webhooks: bool = Field(default=True)
class SonarrConfigTable(SonarrConfigBase, table=True):
"""Database table for Sonarr configuration (singleton)"""
__tablename__ = "sonarr_config"
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
primary_key=True,
index=True,
nullable=False
)
class SonarrConfig(BaseModel):
"""Sonarr webhook configuration"""
"""Sonarr webhook configuration (API Model)"""
webhook_enabled: bool = False
webhook_secret: Optional[str] = None # HMAC SHA256 secret
auto_download_enabled: bool = True
+22 -1
View File
@@ -74,7 +74,7 @@ class WatchlistItemTable(WatchlistItemBase, table=True):
def genres(self, value: List[str]):
self.genres_json = json.dumps(value or [])
# Relationships
# Relationships - Using string reference
user: Optional["UserTable"] = Relationship(back_populates="watchlist_items")
@@ -148,6 +148,24 @@ class AutoDownloadResult(BaseModel):
checked_at: datetime = PydanticField(default_factory=datetime.now)
class WatchlistSettingsBase(SQLModel):
check_interval_hours: int = Field(default=6)
auto_download_enabled: bool = Field(default=True)
max_concurrent_auto_downloads: int = Field(default=2)
notify_on_new_episodes: bool = Field(default=False)
include_completed_anime: bool = Field(default=False)
class WatchlistSettingsTable(WatchlistSettingsBase, table=True):
"""Database table for global watchlist settings"""
__tablename__ = "watchlist_settings"
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, default="default")
class WatchlistSettings(BaseModel):
"""Global watchlist settings"""
check_interval_hours: int = PydanticField(default=6, ge=1, le=168)
@@ -155,3 +173,6 @@ class WatchlistSettings(BaseModel):
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)
# Import UserTable here to resolve SQLModel Relationship mappings
from .auth import UserTable