""" Cache and media routes for L1 server. Handles content retrieval, metadata, media preview, and publishing. """ import logging from pathlib import Path from typing import Optional, Dict, Any from fastapi import APIRouter, Request, Depends, HTTPException, UploadFile, File from fastapi.responses import HTMLResponse, FileResponse from pydantic import BaseModel from artdag_common import render from artdag_common.middleware import wants_html, wants_json from artdag_common.middleware.auth import UserContext from ..dependencies import ( require_auth, get_templates, get_redis_client, get_cache_manager, get_current_user ) from ..services.auth_service import AuthService from ..services.cache_service import CacheService router = APIRouter() logger = logging.getLogger(__name__) class UpdateMetadataRequest(BaseModel): title: Optional[str] = None description: Optional[str] = None tags: Optional[list] = None custom: Optional[Dict[str, Any]] = None def get_cache_service(): """Get cache service instance.""" import database return CacheService(database, get_cache_manager()) @router.get("/{cid}") async def get_cached( cid: str, request: Request, cache_service: CacheService = Depends(get_cache_service), ): """Get cached content by hash. Content negotiation: HTML for browsers, JSON for APIs.""" auth_service = AuthService(get_redis_client()) ctx = auth_service.get_user_from_cookie(request) cache_item = await cache_service.get_cache_item(cid) if not cache_item: if wants_html(request): templates = get_templates(request) return render(templates, "cache/not_found.html", request, cid=cid, user=ctx, active_tab="media", ) raise HTTPException(404, f"Content {cid} not in cache") # JSON response if wants_json(request): return cache_item # HTML response if not ctx: from fastapi.responses import RedirectResponse return RedirectResponse(url="/auth", status_code=302) # Check access has_access = await cache_service.check_access(cid, ctx.actor_id, ctx.username) if not has_access: raise HTTPException(403, "Access denied") templates = get_templates(request) return render(templates, "cache/detail.html", request, cache=cache_item, user=ctx, active_tab="media", ) @router.get("/{cid}/raw") async def get_cached_raw( cid: str, cache_service: CacheService = Depends(get_cache_service), ): """Get raw cached content (file download).""" file_path, media_type, filename = await cache_service.get_raw_file(cid) if not file_path: raise HTTPException(404, f"Content {cid} not in cache") return FileResponse(file_path, media_type=media_type, filename=filename) @router.get("/{cid}/mp4") async def get_cached_mp4( cid: str, cache_service: CacheService = Depends(get_cache_service), ): """Get cached content as MP4 (transcodes MKV on first request).""" mp4_path, error = await cache_service.get_as_mp4(cid) if error: raise HTTPException(400 if "not a video" in error else 404, error) return FileResponse(mp4_path, media_type="video/mp4") @router.get("/{cid}/meta") async def get_metadata( cid: str, ctx: UserContext = Depends(require_auth), cache_service: CacheService = Depends(get_cache_service), ): """Get content metadata.""" meta = await cache_service.get_metadata(cid, ctx.actor_id) if meta is None: raise HTTPException(404, "Content not found") return meta @router.patch("/{cid}/meta") async def update_metadata( cid: str, req: UpdateMetadataRequest, ctx: UserContext = Depends(require_auth), cache_service: CacheService = Depends(get_cache_service), ): """Update content metadata.""" success, error = await cache_service.update_metadata( cid=cid, actor_id=ctx.actor_id, title=req.title, description=req.description, tags=req.tags, custom=req.custom, ) if error: raise HTTPException(400, error) return {"updated": True} @router.post("/{cid}/publish") async def publish_content( cid: str, request: Request, ctx: UserContext = Depends(require_auth), cache_service: CacheService = Depends(get_cache_service), ): """Publish content to L2 and IPFS.""" ipfs_cid, error = await cache_service.publish_to_l2( cid=cid, actor_id=ctx.actor_id, l2_server=ctx.l2_server, auth_token=request.cookies.get("auth_token"), ) if error: if wants_html(request): return HTMLResponse(f'{error}') raise HTTPException(400, error) if wants_html(request): return HTMLResponse(f'Published: {ipfs_cid[:16]}...') return {"ipfs_cid": ipfs_cid, "published": True} @router.delete("/{cid}") async def delete_content( cid: str, ctx: UserContext = Depends(require_auth), cache_service: CacheService = Depends(get_cache_service), ): """Delete content from cache.""" success, error = await cache_service.delete_content(cid, ctx.actor_id) if error: raise HTTPException(400 if "Cannot" in error or "pinned" in error else 404, error) return {"deleted": True} @router.post("/import") async def import_from_ipfs( ipfs_cid: str, ctx: UserContext = Depends(require_auth), cache_service: CacheService = Depends(get_cache_service), ): """Import content from IPFS.""" cid, error = await cache_service.import_from_ipfs(ipfs_cid, ctx.actor_id) if error: raise HTTPException(400, error) return {"cid": cid, "imported": True} @router.post("/upload") async def upload_content( file: UploadFile = File(...), ctx: UserContext = Depends(require_auth), cache_service: CacheService = Depends(get_cache_service), ): """Upload content to cache and IPFS.""" content = await file.read() cid, ipfs_cid, error = await cache_service.upload_content( content=content, filename=file.filename, actor_id=ctx.actor_id, ) if error: raise HTTPException(400, error) return { "cid": ipfs_cid or cid, "content_hash": cid, # Legacy, for backwards compatibility "filename": file.filename, "size": len(content), "uploaded": True, } # Media listing endpoint @router.get("") async def list_media( request: Request, offset: int = 0, limit: int = 24, media_type: Optional[str] = None, cache_service: CacheService = Depends(get_cache_service), ): """List all media in cache.""" auth_service = AuthService(get_redis_client()) ctx = auth_service.get_user_from_cookie(request) if not ctx: if wants_json(request): raise HTTPException(401, "Authentication required") from fastapi.responses import RedirectResponse return RedirectResponse(url="/auth", status_code=302) items = await cache_service.list_media( actor_id=ctx.actor_id, username=ctx.username, offset=offset, limit=limit, media_type=media_type, ) has_more = len(items) >= limit if wants_json(request): return {"items": items, "offset": offset, "limit": limit, "has_more": has_more} templates = get_templates(request) return render(templates, "cache/media_list.html", request, items=items, user=ctx, offset=offset, limit=limit, has_more=has_more, active_tab="media", ) # HTMX metadata form @router.get("/{cid}/meta-form", response_class=HTMLResponse) async def get_metadata_form( cid: str, request: Request, cache_service: CacheService = Depends(get_cache_service), ): """Get metadata editing form (HTMX).""" auth_service = AuthService(get_redis_client()) ctx = auth_service.get_user_from_cookie(request) if not ctx: return HTMLResponse('