Files
ALwrity/backend/api/content_assets/router.py

668 lines
24 KiB
Python

"""
Content Assets API Router
API endpoints for managing unified content assets across all modules.
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Body
from sqlalchemy.orm import Session
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
from datetime import datetime
from services.database import get_db
from middleware.auth_middleware import get_current_user
from services.content_asset_service import ContentAssetService
from models.content_asset_models import AssetType, AssetSource, AssetCollection
router = APIRouter(prefix="/api/content-assets", tags=["Content Assets"])
class AssetResponse(BaseModel):
"""Response model for asset data."""
id: int
user_id: str
asset_type: str
source_module: str
filename: str
file_url: str
file_path: Optional[str] = None
file_size: Optional[int] = None
mime_type: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
prompt: Optional[str] = None
tags: List[str] = []
asset_metadata: Dict[str, Any] = {}
provider: Optional[str] = None
model: Optional[str] = None
cost: float = 0.0
generation_time: Optional[float] = None
is_favorite: bool = False
download_count: int = 0
share_count: int = 0
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class AssetListResponse(BaseModel):
"""Response model for asset list."""
assets: List[AssetResponse]
total: int
limit: int
offset: int
@router.get("/", response_model=AssetListResponse)
async def get_assets(
asset_type: Optional[str] = Query(None, description="Filter by asset type"),
source_module: Optional[str] = Query(None, description="Filter by source module"),
search: Optional[str] = Query(None, description="Search query"),
tags: Optional[str] = Query(None, description="Comma-separated tags"),
favorites_only: bool = Query(False, description="Only favorites"),
collection_id: Optional[int] = Query(None, description="Filter by collection ID"),
date_from: Optional[str] = Query(None, description="Filter from date (ISO format)"),
date_to: Optional[str] = Query(None, description="Filter to date (ISO format)"),
sort_by: str = Query("created_at", description="Sort by: created_at, updated_at, cost, file_size, title"),
sort_order: str = Query("desc", description="Sort order: asc or desc"),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get user's content assets with optional filtering."""
try:
# Auth middleware returns 'id' as the primary key
user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("clerk_user_id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
# Parse filters
asset_type_enum = None
if asset_type:
try:
asset_type_enum = AssetType(asset_type.lower())
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid asset type: {asset_type}")
source_module_enum = None
if source_module:
try:
source_module_enum = AssetSource(source_module.lower())
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid source module: {source_module}")
tags_list = None
if tags:
tags_list = [tag.strip() for tag in tags.split(",")]
# Parse date filters
date_from_obj = None
if date_from:
try:
date_from_obj = datetime.fromisoformat(date_from.replace('Z', '+00:00'))
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_from format. Use ISO format.")
date_to_obj = None
if date_to:
try:
date_to_obj = datetime.fromisoformat(date_to.replace('Z', '+00:00'))
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_to format. Use ISO format.")
# Validate sort parameters
valid_sort_by = ["created_at", "updated_at", "cost", "file_size", "title"]
if sort_by not in valid_sort_by:
raise HTTPException(status_code=400, detail=f"Invalid sort_by. Must be one of: {', '.join(valid_sort_by)}")
if sort_order not in ["asc", "desc"]:
raise HTTPException(status_code=400, detail="Invalid sort_order. Must be 'asc' or 'desc'")
assets, total = service.get_user_assets(
user_id=user_id,
asset_type=asset_type_enum,
source_module=source_module_enum,
search_query=search,
tags=tags_list,
favorites_only=favorites_only,
collection_id=collection_id,
date_from=date_from_obj,
date_to=date_to_obj,
sort_by=sort_by,
sort_order=sort_order,
limit=limit,
offset=offset,
)
return AssetListResponse(
assets=[AssetResponse.model_validate(asset) for asset in assets],
total=total,
limit=limit,
offset=offset,
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching assets: {str(e)}")
class AssetCreateRequest(BaseModel):
"""Request model for creating a new asset."""
asset_type: str = Field(..., description="Asset type: text, image, video, or audio")
source_module: str = Field(..., description="Source module that generated the asset")
filename: str = Field(..., description="Original filename")
file_url: str = Field(..., description="Public URL to access the asset")
file_path: Optional[str] = Field(None, description="Server file path (optional)")
file_size: Optional[int] = Field(None, description="File size in bytes")
mime_type: Optional[str] = Field(None, description="MIME type")
title: Optional[str] = Field(None, description="Asset title")
description: Optional[str] = Field(None, description="Asset description")
prompt: Optional[str] = Field(None, description="Generation prompt")
tags: Optional[List[str]] = Field(default_factory=list, description="List of tags")
asset_metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional metadata")
provider: Optional[str] = Field(None, description="AI provider used")
model: Optional[str] = Field(None, description="Model used")
cost: Optional[float] = Field(0.0, description="Generation cost")
generation_time: Optional[float] = Field(None, description="Generation time in seconds")
@router.post("/", response_model=AssetResponse)
async def create_asset(
asset_data: AssetCreateRequest,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Create a new content asset."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
# Validate asset type
try:
asset_type_enum = AssetType(asset_data.asset_type.lower())
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid asset type: {asset_data.asset_type}")
# Validate source module
try:
source_module_enum = AssetSource(asset_data.source_module.lower())
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid source module: {asset_data.source_module}")
service = ContentAssetService(db)
asset = service.create_asset(
user_id=user_id,
asset_type=asset_type_enum,
source_module=source_module_enum,
filename=asset_data.filename,
file_url=asset_data.file_url,
file_path=asset_data.file_path,
file_size=asset_data.file_size,
mime_type=asset_data.mime_type,
title=asset_data.title,
description=asset_data.description,
prompt=asset_data.prompt,
tags=asset_data.tags or [],
asset_metadata=asset_data.asset_metadata or {},
provider=asset_data.provider,
model=asset_data.model,
cost=asset_data.cost,
generation_time=asset_data.generation_time,
)
return AssetResponse.model_validate(asset)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating asset: {str(e)}")
@router.post("/{asset_id}/favorite", response_model=Dict[str, Any])
async def toggle_favorite(
asset_id: int,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Toggle favorite status of an asset."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
is_favorite = service.toggle_favorite(asset_id, user_id)
return {"asset_id": asset_id, "is_favorite": is_favorite}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error toggling favorite: {str(e)}")
@router.delete("/{asset_id}", response_model=Dict[str, Any])
async def delete_asset(
asset_id: int,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Delete an asset."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
success = service.delete_asset(asset_id, user_id)
if not success:
raise HTTPException(status_code=404, detail="Asset not found")
return {"asset_id": asset_id, "deleted": True}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error deleting asset: {str(e)}")
@router.post("/{asset_id}/usage", response_model=Dict[str, Any])
async def track_usage(
asset_id: int,
action: str = Query(..., description="Action: download, share, or access"),
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Track asset usage (download, share, access)."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
if action not in ["download", "share", "access"]:
raise HTTPException(status_code=400, detail="Invalid action")
service = ContentAssetService(db)
service.update_asset_usage(asset_id, user_id, action)
return {"asset_id": asset_id, "action": action, "tracked": True}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error tracking usage: {str(e)}")
class AssetUpdateRequest(BaseModel):
"""Request model for updating asset metadata."""
title: Optional[str] = None
description: Optional[str] = None
tags: Optional[List[str]] = None
asset_metadata: Optional[Dict[str, Any]] = None
@router.put("/{asset_id}", response_model=AssetResponse)
async def update_asset(
asset_id: int,
update_data: AssetUpdateRequest,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Update asset metadata."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
asset = service.update_asset(
asset_id=asset_id,
user_id=user_id,
title=update_data.title,
description=update_data.description,
tags=update_data.tags,
asset_metadata=update_data.asset_metadata,
)
if not asset:
raise HTTPException(status_code=404, detail="Asset not found")
return AssetResponse.model_validate(asset)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error updating asset: {str(e)}")
@router.get("/statistics", response_model=Dict[str, Any])
async def get_statistics(
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get asset statistics for the current user."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
stats = service.get_asset_statistics(user_id)
return stats
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching statistics: {str(e)}")
# ==================== Collection Endpoints ====================
class CollectionResponse(BaseModel):
"""Response model for collection data."""
id: int
user_id: str
name: str
description: Optional[str] = None
is_public: bool = False
cover_asset_id: Optional[int] = None
asset_count: int = 0
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class CollectionListResponse(BaseModel):
"""Response model for collection list."""
collections: List[CollectionResponse]
total: int
limit: int
offset: int
class CollectionCreateRequest(BaseModel):
"""Request model for creating a collection."""
name: str = Field(..., description="Collection name")
description: Optional[str] = Field(None, description="Collection description")
is_public: bool = Field(False, description="Whether collection is public")
class CollectionUpdateRequest(BaseModel):
"""Request model for updating a collection."""
name: Optional[str] = None
description: Optional[str] = None
is_public: Optional[bool] = None
cover_asset_id: Optional[int] = None
@router.post("/collections", response_model=CollectionResponse)
async def create_collection(
collection_data: CollectionCreateRequest,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Create a new asset collection."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
collection = service.create_collection(
user_id=user_id,
name=collection_data.name,
description=collection_data.description,
is_public=collection_data.is_public,
)
# Get asset count
assets, _ = service.get_collection_assets(collection.id, user_id, limit=1, offset=0)
asset_count = len(assets)
response = CollectionResponse.model_validate(collection)
response.asset_count = asset_count
return response
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating collection: {str(e)}")
@router.get("/collections", response_model=CollectionListResponse)
async def get_collections(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get user's collections."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
collections, total = service.get_user_collections(user_id, limit=limit, offset=offset)
# Get asset counts for each collection
collection_responses = []
for collection in collections:
assets, _ = service.get_collection_assets(collection.id, user_id, limit=1, offset=0)
response = CollectionResponse.model_validate(collection)
response.asset_count = len(assets)
collection_responses.append(response)
return CollectionListResponse(
collections=collection_responses,
total=total,
limit=limit,
offset=offset,
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching collections: {str(e)}")
@router.get("/collections/{collection_id}", response_model=CollectionResponse)
async def get_collection(
collection_id: int,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get a specific collection."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
collection = service.get_collection_by_id(collection_id, user_id)
if not collection:
raise HTTPException(status_code=404, detail="Collection not found")
assets, _ = service.get_collection_assets(collection.id, user_id, limit=1, offset=0)
response = CollectionResponse.model_validate(collection)
response.asset_count = len(assets)
return response
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching collection: {str(e)}")
@router.put("/collections/{collection_id}", response_model=CollectionResponse)
async def update_collection(
collection_id: int,
update_data: CollectionUpdateRequest,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Update collection metadata."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
collection = service.update_collection(
collection_id=collection_id,
user_id=user_id,
name=update_data.name,
description=update_data.description,
is_public=update_data.is_public,
cover_asset_id=update_data.cover_asset_id,
)
if not collection:
raise HTTPException(status_code=404, detail="Collection not found")
assets, _ = service.get_collection_assets(collection.id, user_id, limit=1, offset=0)
response = CollectionResponse.model_validate(collection)
response.asset_count = len(assets)
return response
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error updating collection: {str(e)}")
@router.delete("/collections/{collection_id}", response_model=Dict[str, Any])
async def delete_collection(
collection_id: int,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Delete a collection."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
success = service.delete_collection(collection_id, user_id)
if not success:
raise HTTPException(status_code=404, detail="Collection not found")
return {"collection_id": collection_id, "deleted": True}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error deleting collection: {str(e)}")
@router.get("/collections/{collection_id}/assets", response_model=AssetListResponse)
async def get_collection_assets(
collection_id: int,
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get all assets in a collection."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
collection = service.get_collection_by_id(collection_id, user_id)
if not collection:
raise HTTPException(status_code=404, detail="Collection not found")
assets, total = service.get_collection_assets(collection_id, user_id, limit=limit, offset=offset)
return AssetListResponse(
assets=[AssetResponse.model_validate(asset) for asset in assets],
total=total,
limit=limit,
offset=offset,
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching collection assets: {str(e)}")
class CollectionAssetsRequest(BaseModel):
"""Request model for adding/removing assets from collection."""
asset_ids: List[int] = Field(..., description="List of asset IDs")
@router.post("/collections/{collection_id}/assets", response_model=Dict[str, Any])
async def add_assets_to_collection(
collection_id: int,
request: CollectionAssetsRequest,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Add assets to a collection."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
count = service.add_assets_to_collection(collection_id, user_id, request.asset_ids)
return {
"collection_id": collection_id,
"assets_added": count,
"asset_ids": request.asset_ids,
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error adding assets to collection: {str(e)}")
@router.delete("/collections/{collection_id}/assets", response_model=Dict[str, Any])
async def remove_assets_from_collection(
collection_id: int,
request: CollectionAssetsRequest,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Remove assets from a collection."""
try:
user_id = current_user.get("user_id") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found")
service = ContentAssetService(db)
count = service.remove_assets_from_collection(collection_id, user_id, request.asset_ids)
return {
"collection_id": collection_id,
"assets_removed": count,
"asset_ids": request.asset_ids,
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error removing assets from collection: {str(e)}")