from pathlib import Path from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.database import get_db from app.models import Library, MediaItem, Tag from app.schemas import MediaItemOut, TagIdList from app.services.thumbnails import get_or_create_thumbnail router = APIRouter(prefix="/media", tags=["media"]) async def _get_item_and_lib(media_id: int, db: AsyncSession) -> tuple[MediaItem, Library]: result = await db.execute( select(MediaItem).where(MediaItem.id == media_id) ) item = result.scalars().first() if not item: raise HTTPException(404, "Media item not found") lib_result = await db.execute(select(Library).where(Library.id == item.library_id)) lib = lib_result.scalars().first() return item, lib def _resolve_safe(lib: Library, item: MediaItem) -> Path: root = Path(lib.path) abs_path = (root / item.rel_path).resolve() if not str(abs_path).startswith(str(root.resolve())): raise HTTPException(400, "Invalid path") return abs_path @router.get("/{media_id}", response_model=MediaItemOut) async def get_media_item(media_id: int, db: AsyncSession = Depends(get_db)): result = await db.execute( select(MediaItem).where(MediaItem.id == media_id) ) item = result.scalars().first() if not item: raise HTTPException(404, "Media item not found") # Eagerly load tags await db.refresh(item, ["tags"]) return item @router.get("/{media_id}/file") async def serve_file(media_id: int, db: AsyncSession = Depends(get_db)): item, lib = await _get_item_and_lib(media_id, db) if item.missing: raise HTTPException(404, "File is missing from disk") abs_path = _resolve_safe(lib, item) if not abs_path.exists(): raise HTTPException(404, "File not found on disk") return FileResponse(str(abs_path)) @router.get("/{media_id}/thumbnail") async def serve_thumbnail(media_id: int, db: AsyncSession = Depends(get_db)): item, lib = await _get_item_and_lib(media_id, db) abs_path = _resolve_safe(lib, item) thumb = get_or_create_thumbnail(media_id, str(abs_path), item.media_type) if not thumb: raise HTTPException(404, "Thumbnail could not be generated") return FileResponse(str(thumb), media_type="image/jpeg") @router.put("/{media_id}/tags", response_model=MediaItemOut) async def set_tags(media_id: int, body: TagIdList, db: AsyncSession = Depends(get_db)): result = await db.execute(select(MediaItem).where(MediaItem.id == media_id)) item = result.scalars().first() if not item: raise HTTPException(404, "Media item not found") tags_result = await db.execute(select(Tag).where(Tag.id.in_(body.tag_ids))) tags = tags_result.scalars().all() if len(tags) != len(body.tag_ids): raise HTTPException(400, "One or more tag IDs not found") await db.refresh(item, ["tags"]) item.tags = list(tags) await db.commit() await db.refresh(item, ["tags"]) return item