from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session from sqlalchemy import text from ..auth_utils import get_current_user from ..database import get_db from ..models import Collection, CollectionItem, User, Video router = APIRouter() class CollectionOut(BaseModel): id: int name: str created_at: datetime video_count: int = 0 thumbnails: list[str] = [] model_config = {"from_attributes": True} class CollectionCreate(BaseModel): name: str @router.get("", response_model=list[CollectionOut]) def list_collections( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): rows = db.execute( text(""" SELECT c.id, c.name, c.created_at, COUNT(ci.id) AS video_count FROM collections c LEFT JOIN collection_items ci ON c.id = ci.collection_id WHERE c.user_id = :uid GROUP BY c.id ORDER BY c.created_at DESC """), {"uid": current_user.id}, ).mappings().all() result = [] for row in rows: # Grab up to 4 thumbnails for mosaic preview thumbs = db.execute( text(""" SELECT v.thumbnail_url FROM collection_items ci JOIN videos v ON ci.video_id = v.id WHERE ci.collection_id = :cid AND v.thumbnail_url IS NOT NULL ORDER BY ci.added_at DESC LIMIT 4 """), {"cid": row["id"]}, ).scalars().all() result.append(CollectionOut( id=row["id"], name=row["name"], created_at=row["created_at"], video_count=row["video_count"], thumbnails=list(thumbs), )) return result @router.post("", response_model=CollectionOut, status_code=201) def create_collection( body: CollectionCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): name = (body.name or "").strip() if not name: raise HTTPException(status_code=400, detail="Name required") col = Collection(user_id=current_user.id, name=name) db.add(col) db.commit() db.refresh(col) return CollectionOut(id=col.id, name=col.name, created_at=col.created_at, video_count=0, thumbnails=[]) @router.patch("/{collection_id}", response_model=CollectionOut) def rename_collection( collection_id: int, body: CollectionCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): col = db.query(Collection).filter_by(id=collection_id, user_id=current_user.id).first() if not col: raise HTTPException(status_code=404, detail="Not found") col.name = (body.name or "").strip() or col.name db.commit() count = db.query(CollectionItem).filter_by(collection_id=col.id).count() return CollectionOut(id=col.id, name=col.name, created_at=col.created_at, video_count=count) @router.delete("/{collection_id}", status_code=204) def delete_collection( collection_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): col = db.query(Collection).filter_by(id=collection_id, user_id=current_user.id).first() if col: db.delete(col) db.commit() @router.get("/{collection_id}/videos") def get_collection_videos( collection_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): col = db.query(Collection).filter_by(id=collection_id, user_id=current_user.id).first() if not col: raise HTTPException(status_code=404, detail="Not found") rows = db.execute( text(""" SELECT v.id, v.youtube_video_id, v.title, v.thumbnail_url, v.duration_seconds, v.published_at, v.description, c.id AS channel_id, c.name AS channel_name, COALESCE(uv.watched, 0) AS is_watched, COALESCE(uv.downloaded, 0) AS is_downloaded, COALESCE(uv.liked, 0) AS liked, COALESCE(uv.queued, 0) AS queued, ci.added_at FROM collection_items ci JOIN videos v ON ci.video_id = v.id LEFT JOIN channels c ON v.channel_id = c.id LEFT JOIN user_videos uv ON v.id = uv.video_id AND uv.user_id = :uid WHERE ci.collection_id = :cid ORDER BY ci.added_at DESC """), {"uid": current_user.id, "cid": collection_id}, ).mappings().all() return {"collection": {"id": col.id, "name": col.name}, "videos": [dict(r) for r in rows]} @router.post("/{collection_id}/videos", status_code=201) def add_to_collection( collection_id: int, body: dict, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): col = db.query(Collection).filter_by(id=collection_id, user_id=current_user.id).first() if not col: raise HTTPException(status_code=404, detail="Not found") video_id = body.get("video_id") if not video_id: raise HTTPException(status_code=400, detail="video_id required") existing = db.query(CollectionItem).filter_by(collection_id=collection_id, video_id=video_id).first() if not existing: db.add(CollectionItem(collection_id=collection_id, video_id=video_id)) db.commit() return {"ok": True} @router.delete("/{collection_id}/videos/{video_id}", status_code=204) def remove_from_collection( collection_id: int, video_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): col = db.query(Collection).filter_by(id=collection_id, user_id=current_user.id).first() if not col: raise HTTPException(status_code=404, detail="Not found") item = db.query(CollectionItem).filter_by(collection_id=collection_id, video_id=video_id).first() if item: db.delete(item) db.commit()