76 lines
3.1 KiB
Python
76 lines
3.1 KiB
Python
"""Source model - platform URLs for subscriptions."""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from sqlalchemy import String, Boolean, Integer, Text, ForeignKey, UniqueConstraint
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.models.base import Base, TimestampMixin, format_datetime
|
|
|
|
|
|
class Source(Base, TimestampMixin):
|
|
"""A source represents a platform URL for a subscription.
|
|
|
|
Sources belong to subscriptions - one subscription (artist) can have
|
|
multiple sources (Patreon, SubscribeStar, etc.).
|
|
"""
|
|
|
|
__tablename__ = "sources"
|
|
__table_args__ = (UniqueConstraint("platform", "url", name="uq_source_platform_url"),)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
subscription_id: Mapped[int] = mapped_column(
|
|
ForeignKey("subscriptions.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True
|
|
)
|
|
platform: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
|
url: Mapped[str] = mapped_column(Text, nullable=False)
|
|
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
check_interval: Mapped[int] = mapped_column(Integer, default=28800) # 8 hours in seconds
|
|
|
|
# Status tracking
|
|
last_check: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
last_success: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
error_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
|
|
# Flexible metadata storage (per-source config overrides)
|
|
metadata_: Mapped[dict] = mapped_column("metadata", JSONB, default=dict)
|
|
|
|
# Relationships
|
|
subscription: Mapped["Subscription"] = relationship(back_populates="sources")
|
|
downloads: Mapped[list["Download"]] = relationship(back_populates="source")
|
|
content_items: Mapped[list["ContentItem"]] = relationship(back_populates="source")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Source {self.subscription.name if self.subscription else '?'}/{self.platform}>"
|
|
|
|
def to_dict(self, include_subscription: bool = True) -> dict:
|
|
"""Convert to dictionary for API responses."""
|
|
result = {
|
|
"id": self.id,
|
|
"subscription_id": self.subscription_id,
|
|
"platform": self.platform,
|
|
"url": self.url,
|
|
"enabled": self.enabled,
|
|
"check_interval": self.check_interval,
|
|
"last_check": format_datetime(self.last_check),
|
|
"last_success": format_datetime(self.last_success),
|
|
"error_count": self.error_count,
|
|
"metadata": self.metadata_,
|
|
"created_at": format_datetime(self.created_at),
|
|
"updated_at": format_datetime(self.updated_at),
|
|
}
|
|
if include_subscription and self.subscription:
|
|
result["subscription_name"] = self.subscription.name
|
|
return result
|
|
|
|
|
|
# Import for type hints (avoid circular imports)
|
|
from typing import TYPE_CHECKING
|
|
if TYPE_CHECKING:
|
|
from app.models.subscription import Subscription
|
|
from app.models.download import Download
|
|
from app.models.content import ContentItem
|