86 lines
3.0 KiB
Python
86 lines
3.0 KiB
Python
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
|