"""Shaders router — CRUD, submit, fork, search.""" from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, or_ from app.database import get_db from app.models import User, Shader from app.schemas import ShaderCreate, ShaderUpdate, ShaderPublic from app.middleware.auth import get_current_user, get_optional_user router = APIRouter() @router.get("", response_model=list[ShaderPublic]) async def list_shaders( q: str | None = Query(None, description="Search query"), tags: list[str] | None = Query(None, description="Filter by tags"), shader_type: str | None = Query(None, description="Filter by type: 2d, 3d, audio-reactive"), sort: str = Query("trending", description="Sort: trending, new, top"), limit: int = Query(20, ge=1, le=50), offset: int = Query(0, ge=0), db: AsyncSession = Depends(get_db), ): query = select(Shader).where(Shader.is_public == True, Shader.render_status == "ready") if q: query = query.where(Shader.title.ilike(f"%{q}%")) if tags: query = query.where(Shader.tags.overlap(tags)) if shader_type: query = query.where(Shader.shader_type == shader_type) if sort == "new": query = query.order_by(Shader.created_at.desc()) elif sort == "top": query = query.order_by(Shader.score.desc()) else: # trending query = query.order_by(Shader.score.desc(), Shader.created_at.desc()) query = query.limit(limit).offset(offset) result = await db.execute(query) return result.scalars().all() @router.get("/{shader_id}", response_model=ShaderPublic) async def get_shader( shader_id: UUID, db: AsyncSession = Depends(get_db), user: User | None = Depends(get_optional_user), ): result = await db.execute(select(Shader).where(Shader.id == shader_id)) shader = result.scalar_one_or_none() if not shader: raise HTTPException(status_code=404, detail="Shader not found") if not shader.is_public and (not user or user.id != shader.author_id): raise HTTPException(status_code=404, detail="Shader not found") # Increment view count shader.view_count += 1 return shader @router.post("", response_model=ShaderPublic, status_code=status.HTTP_201_CREATED) async def create_shader( body: ShaderCreate, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): # TODO: Turnstile verification for submit # TODO: Rate limit check (free tier: 5/month) # TODO: GLSL validation via glslang # TODO: Enqueue render job shader = Shader( author_id=user.id, title=body.title, description=body.description, glsl_code=body.glsl_code, tags=body.tags, shader_type=body.shader_type, is_public=body.is_public, style_metadata=body.style_metadata, render_status="pending", ) db.add(shader) await db.flush() return shader @router.put("/{shader_id}", response_model=ShaderPublic) async def update_shader( shader_id: UUID, body: ShaderUpdate, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): result = await db.execute(select(Shader).where(Shader.id == shader_id)) shader = result.scalar_one_or_none() if not shader: raise HTTPException(status_code=404, detail="Shader not found") if shader.author_id != user.id and user.role != "admin": raise HTTPException(status_code=403, detail="Not the shader owner") for field, value in body.model_dump(exclude_unset=True).items(): setattr(shader, field, value) return shader @router.delete("/{shader_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_shader( shader_id: UUID, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): result = await db.execute(select(Shader).where(Shader.id == shader_id)) shader = result.scalar_one_or_none() if not shader: raise HTTPException(status_code=404, detail="Shader not found") if shader.author_id != user.id and user.role != "admin": raise HTTPException(status_code=403, detail="Not the shader owner") await db.delete(shader) @router.post("/{shader_id}/fork", response_model=ShaderPublic, status_code=status.HTTP_201_CREATED) async def fork_shader( shader_id: UUID, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): result = await db.execute(select(Shader).where(Shader.id == shader_id)) original = result.scalar_one_or_none() if not original: raise HTTPException(status_code=404, detail="Shader not found") if not original.is_public: raise HTTPException(status_code=404, detail="Shader not found") forked = Shader( author_id=user.id, title=f"Fork of {original.title}", description=f"Forked from {original.title}", glsl_code=original.glsl_code, tags=original.tags, shader_type=original.shader_type, forked_from=original.id, render_status="pending", ) db.add(forked) await db.flush() return forked