From c96ee307db6d5bad1d6864b03788f1b5a2e2442c Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 23 May 2025 11:36:04 -0500 Subject: [PATCH] Moved channel URLs to channels_urls table. Create CRUD endpoints for new table. --- app/models/__init__.py | 6 +-- app/models/db.py | 22 ++++++++-- app/models/schemas.py | 35 ++++++++++++++-- app/routers/channels.py | 92 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 142 insertions(+), 13 deletions(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index 4ab99f2..4e7dcad 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,4 @@ -from .db import Base, ChannelDB -from .schemas import ChannelCreate, ChannelUpdate, ChannelResponse +from .db import Base, ChannelDB, ChannelURL +from .schemas import ChannelCreate, ChannelUpdate, ChannelResponse, ChannelURLCreate, ChannelURLResponse -__all__ = ["Base", "ChannelDB", "ChannelCreate", "ChannelUpdate", "ChannelResponse"] \ No newline at end of file +__all__ = ["Base", "ChannelDB", "ChannelCreate", "ChannelUpdate", "ChannelResponse", "ChannelURL", "ChannelURLCreate", "ChannelURLResponse"] \ No newline at end of file diff --git a/app/models/db.py b/app/models/db.py index 5beaf92..7f75fe1 100644 --- a/app/models/db.py +++ b/app/models/db.py @@ -1,8 +1,9 @@ from datetime import datetime, timezone import uuid -from sqlalchemy import Column, String, JSON, DateTime, UniqueConstraint +from sqlalchemy import Column, String, JSON, DateTime, UniqueConstraint, ForeignKey from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship Base = declarative_base() @@ -20,6 +21,21 @@ class ChannelDB(Base): UniqueConstraint('group_title', 'name', name='uix_group_title_name'), ) tvg_logo = Column(String) - urls = Column(JSON) # Stores list of URLs as JSON created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) \ No newline at end of file + updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) + + # Relationship with ChannelURL + urls = relationship("ChannelURL", back_populates="channel", cascade="all, delete-orphan") + +class ChannelURL(Base): + """SQLAlchemy model for channel URLs""" + __tablename__ = "channels_urls" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + channel_id = Column(UUID(as_uuid=True), ForeignKey('channels.id', ondelete='CASCADE'), nullable=False) + url = Column(String, nullable=False) + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) + + # Relationship with ChannelDB + channel = relationship("ChannelDB", back_populates="urls") \ No newline at end of file diff --git a/app/models/schemas.py b/app/models/schemas.py index 34a6a1c..f5376d7 100644 --- a/app/models/schemas.py +++ b/app/models/schemas.py @@ -3,22 +3,49 @@ from typing import List from uuid import UUID from pydantic import BaseModel +class ChannelURLCreate(BaseModel): + """Pydantic model for creating channel URLs""" + url: str + +class ChannelURLBase(ChannelURLCreate): + """Base Pydantic model for channel URL responses""" + id: UUID + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + +class ChannelURLResponse(ChannelURLBase): + """Pydantic model for channel URL responses""" + pass + class ChannelCreate(BaseModel): """Pydantic model for creating channels""" - urls: List[str] + urls: List[str] # List of URLs to create with the channel name: str group_title: str tvg_id: str tvg_logo: str tvg_name: str -class ChannelUpdate(ChannelCreate): +class ChannelUpdate(BaseModel): """Pydantic model for updating channels""" - pass + name: str | None = None + group_title: str | None = None + tvg_id: str | None = None + tvg_logo: str | None = None + tvg_name: str | None = None -class ChannelResponse(ChannelCreate): +class ChannelResponse(BaseModel): """Pydantic model for channel responses""" id: UUID + name: str + group_title: str + tvg_id: str + tvg_logo: str + tvg_name: str + urls: List[ChannelURLResponse] # List of URL objects without channel_id created_at: datetime updated_at: datetime diff --git a/app/routers/channels.py b/app/routers/channels.py index 023a09c..a1045d5 100644 --- a/app/routers/channels.py +++ b/app/routers/channels.py @@ -4,7 +4,15 @@ from typing import List from uuid import UUID from sqlalchemy import and_ -from app.models import ChannelDB, ChannelCreate, ChannelUpdate, ChannelResponse +from app.models import ( + ChannelDB, + ChannelURL, + ChannelCreate, + ChannelUpdate, + ChannelResponse, + ChannelURLCreate, + ChannelURLResponse, +) from app.utils.database import get_db from app.auth.dependencies import get_current_user, require_roles from app.models.auth import CognitoUser @@ -36,10 +44,21 @@ def create_channel( detail="Channel with same group_title and name already exists" ) - db_channel = ChannelDB(**channel.model_dump()) + # Create channel without URLs first + channel_data = channel.model_dump() + urls = channel_data.pop('urls', []) + db_channel = ChannelDB(**channel_data) db.add(db_channel) db.commit() db.refresh(db_channel) + + # Add URLs + for url in urls: + db_url = ChannelURL(channel_id=db_channel.id, url=url) + db.add(db_url) + + db.commit() + db.refresh(db_channel) return db_channel @router.get("/{channel_id}", response_model=ChannelResponse) @@ -121,4 +140,71 @@ def list_channels( user: CognitoUser = Depends(get_current_user) ): """List all channels with pagination""" - return db.query(ChannelDB).offset(skip).limit(limit).all() \ No newline at end of file + return db.query(ChannelDB).offset(skip).limit(limit).all() + +# URL Management Endpoints + +@router.post("/{channel_id}/urls", response_model=ChannelURLResponse, status_code=status.HTTP_201_CREATED) +@require_roles("admin") +def add_channel_url( + channel_id: UUID, + url: ChannelURLCreate, + db: Session = Depends(get_db), + user: CognitoUser = Depends(get_current_user) +): + """Add a new URL to a channel""" + channel = db.query(ChannelDB).filter(ChannelDB.id == channel_id).first() + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Channel not found" + ) + + db_url = ChannelURL(channel_id=channel_id, url=url.url) + db.add(db_url) + db.commit() + db.refresh(db_url) + return db_url + +@router.delete("/{channel_id}/urls/{url_id}", status_code=status.HTTP_204_NO_CONTENT) +@require_roles("admin") +def delete_channel_url( + channel_id: UUID, + url_id: UUID, + db: Session = Depends(get_db), + user: CognitoUser = Depends(get_current_user) +): + """Delete a URL from a channel""" + url = db.query(ChannelURL).filter( + and_( + ChannelURL.id == url_id, + ChannelURL.channel_id == channel_id + ) + ).first() + + if not url: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="URL not found" + ) + + db.delete(url) + db.commit() + return None + +@router.get("/{channel_id}/urls", response_model=List[ChannelURLResponse]) +@require_roles("admin") +def list_channel_urls( + channel_id: UUID, + db: Session = Depends(get_db), + user: CognitoUser = Depends(get_current_user) +): + """List all URLs for a channel""" + channel = db.query(ChannelDB).filter(ChannelDB.id == channel_id).first() + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Channel not found" + ) + + return db.query(ChannelURL).filter(ChannelURL.channel_id == channel_id).all() \ No newline at end of file