from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import and_ from sqlalchemy.orm import Session from app.auth.dependencies import get_current_user, require_roles from app.models import ( ChannelCreate, ChannelDB, ChannelResponse, ChannelUpdate, ChannelURL, ChannelURLCreate, ChannelURLResponse, Group, Priority, # Added Priority import ) from app.models.auth import CognitoUser from app.models.schemas import ChannelURLUpdate from app.utils.database import get_db router = APIRouter(prefix="/channels", tags=["channels"]) @router.post("/", response_model=ChannelResponse, status_code=status.HTTP_201_CREATED) @require_roles("admin") def create_channel( channel: ChannelCreate, db: Session = Depends(get_db), user: CognitoUser = Depends(get_current_user), ): """Create a new channel""" # Check if group exists group = db.query(Group).filter(Group.id == channel.group_id).first() if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Group not found", ) # Check for duplicate channel (same group_id + name) existing_channel = ( db.query(ChannelDB) .filter( and_( ChannelDB.group_id == channel.group_id, ChannelDB.name == channel.name, ) ) .first() ) if existing_channel: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Channel with same group_id and name already exists", ) # Create channel without URLs first channel_data = channel.model_dump(exclude={"urls"}) urls = channel.urls db_channel = ChannelDB(**channel_data) db.add(db_channel) db.commit() db.refresh(db_channel) # Add URLs with priority for url in urls: db_url = ChannelURL( channel_id=db_channel.id, url=url.url, priority_id=url.priority_id, in_use=False, ) db.add(db_url) db.commit() db.refresh(db_channel) return db_channel @router.get("/{channel_id}", response_model=ChannelResponse) def get_channel(channel_id: UUID, db: Session = Depends(get_db)): """Get a channel by id""" 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 channel @router.put("/{channel_id}", response_model=ChannelResponse) @require_roles("admin") def update_channel( channel_id: UUID, channel: ChannelUpdate, db: Session = Depends(get_db), user: CognitoUser = Depends(get_current_user), ): """Update a channel""" db_channel = db.query(ChannelDB).filter(ChannelDB.id == channel_id).first() if not db_channel: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Channel not found" ) # Only check for duplicates if name or group_id are being updated if channel.name is not None or channel.group_id is not None: name = channel.name if channel.name is not None else db_channel.name group_id = ( channel.group_id if channel.group_id is not None else db_channel.group_id ) # Check if new group exists if channel.group_id is not None: group = db.query(Group).filter(Group.id == channel.group_id).first() if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Group not found", ) existing_channel = ( db.query(ChannelDB) .filter( and_( ChannelDB.group_id == group_id, ChannelDB.name == name, ChannelDB.id != channel_id, ) ) .first() ) if existing_channel: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Channel with same group_id and name already exists", ) # Update only provided fields update_data = channel.model_dump(exclude_unset=True) for key, value in update_data.items(): setattr(db_channel, key, value) db.commit() db.refresh(db_channel) return db_channel @router.delete("/", status_code=status.HTTP_200_OK) @require_roles("admin") def delete_channels( db: Session = Depends(get_db), user: CognitoUser = Depends(get_current_user), ): """Delete all channels""" count = 0 try: count = db.query(ChannelDB).count() # First delete all channels db.query(ChannelDB).delete() # Then delete any URLs that are now orphaned (no channel references) db.query(ChannelURL).filter( ~ChannelURL.channel_id.in_(db.query(ChannelDB.id)) ).delete(synchronize_session=False) # Then delete any groups that are now empty db.query(Group).filter(~Group.id.in_(db.query(ChannelDB.group_id))).delete( synchronize_session=False ) db.commit() except Exception as e: print(f"Error deleting channels: {e}") db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete channels", ) return {"deleted": count} @router.delete("/{channel_id}", status_code=status.HTTP_204_NO_CONTENT) @require_roles("admin") def delete_channel( channel_id: UUID, db: Session = Depends(get_db), user: CognitoUser = Depends(get_current_user), ): """Delete 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.delete(channel) db.commit() return None @router.get("/", response_model=list[ChannelResponse]) @require_roles("admin") def list_channels( skip: int = 0, limit: int = 100, db: Session = Depends(get_db), user: CognitoUser = Depends(get_current_user), ): """List all channels with pagination""" return db.query(ChannelDB).offset(skip).limit(limit).all() # New endpoint to get channels by group @router.get("/groups/{group_id}/channels", response_model=list[ChannelResponse]) def get_channels_by_group( group_id: UUID, db: Session = Depends(get_db), ): """Get all channels for a specific group""" group = db.query(Group).filter(Group.id == group_id).first() if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Group not found" ) return db.query(ChannelDB).filter(ChannelDB.group_id == group_id).all() # New endpoint to update a channel's group @router.put("/{channel_id}/group", response_model=ChannelResponse) @require_roles("admin") def update_channel_group( channel_id: UUID, group_id: UUID, db: Session = Depends(get_db), user: CognitoUser = Depends(get_current_user), ): """Update a channel's group""" 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" ) group = db.query(Group).filter(Group.id == group_id).first() if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Group not found" ) # Check for duplicate channel name in new group existing_channel = ( db.query(ChannelDB) .filter( and_( ChannelDB.group_id == group_id, ChannelDB.name == channel.name, ChannelDB.id != channel_id, ) ) .first() ) if existing_channel: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Channel with same name already exists in target group", ) channel.group_id = group_id db.commit() db.refresh(channel) return channel # Bulk Upload and Reset Endpoints @router.post("/bulk-upload", status_code=status.HTTP_200_OK) @require_roles("admin") def bulk_upload_channels( channels: list[dict], db: Session = Depends(get_db), user: CognitoUser = Depends(get_current_user), ): """Bulk upload channels from JSON array""" processed = 0 # Fetch all priorities from the database, ordered by id priorities = db.query(Priority).order_by(Priority.id).all() priority_map = {i: p.id for i, p in enumerate(priorities)} # Get the highest priority_id (which corresponds to the lowest priority level) max_priority_id = None if priorities: max_priority_id = db.query(Priority.id).order_by(Priority.id.desc()).first()[0] for channel_data in channels: try: # Get or create group group_name = channel_data.get("group-title") if not group_name: continue group = db.query(Group).filter(Group.name == group_name).first() if not group: group = Group(name=group_name) db.add(group) db.flush() # Use flush to make the group available in the session db.refresh(group) # Prepare channel data urls = channel_data.get("urls", []) if not isinstance(urls, list): urls = [urls] # Assign priorities dynamically based on fetched priorities url_objects = [] for i, url in enumerate(urls): # Process all URLs priority_id = priority_map.get(i) if priority_id is None: # If index is out of bounds, # assign the highest priority_id (lowest priority) if max_priority_id is not None: priority_id = max_priority_id else: print( f"Warning: No priorities defined in database. " f"Skipping URL {url}" ) continue url_objects.append({"url": url, "priority_id": priority_id}) # Create channel object with required fields channel_obj = ChannelDB( tvg_id=channel_data.get("tvg-id", ""), name=channel_data.get("name", ""), group_id=group.id, tvg_name=channel_data.get("tvg-name", ""), tvg_logo=channel_data.get("tvg-logo", ""), ) # Upsert channel existing_channel = ( db.query(ChannelDB) .filter( and_( ChannelDB.group_id == group.id, ChannelDB.name == channel_obj.name, ) ) .first() ) if existing_channel: # Update existing existing_channel.tvg_id = channel_obj.tvg_id existing_channel.tvg_name = channel_obj.tvg_name existing_channel.tvg_logo = channel_obj.tvg_logo # Clear and recreate URLs db.query(ChannelURL).filter( ChannelURL.channel_id == existing_channel.id ).delete() for url in url_objects: db_url = ChannelURL( channel_id=existing_channel.id, url=url["url"], priority_id=url["priority_id"], in_use=False, ) db.add(db_url) else: # Create new db.add(channel_obj) db.flush() # Flush to get the new channel's ID db.refresh(channel_obj) # Add URLs for new channel for url in url_objects: db_url = ChannelURL( channel_id=channel_obj.id, url=url["url"], priority_id=url["priority_id"], in_use=False, ) db.add(db_url) db.commit() # Commit all changes for this channel atomically processed += 1 except Exception as e: print(f"Error processing channel: {channel_data.get('name', 'Unknown')}") print(f"Exception details: {e}") db.rollback() # Rollback the entire transaction for the failed channel continue return {"processed": processed} # 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, priority_id=url.priority_id, in_use=False, # Default to not in use ) db.add(db_url) db.commit() db.refresh(db_url) return db_url @router.put("/{channel_id}/urls/{url_id}", response_model=ChannelURLResponse) @require_roles("admin") def update_channel_url( channel_id: UUID, url_id: UUID, url_update: ChannelURLUpdate, db: Session = Depends(get_db), user: CognitoUser = Depends(get_current_user), ): """Update a channel URL (url, in_use, or priority_id)""" db_url = ( db.query(ChannelURL) .filter(and_(ChannelURL.id == url_id, ChannelURL.channel_id == channel_id)) .first() ) if not db_url: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="URL not found" ) if url_update.url is not None: db_url.url = url_update.url if url_update.in_use is not None: db_url.in_use = url_update.in_use if url_update.priority_id is not None: db_url.priority_id = url_update.priority_id 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()