goon/app/api/scene_favorites.py
goon-foss 37e6b7f03c perf(api): liczniki +N ulubionych z cache i indeksem, podobne sceny z najrzadszych tagow
- /favorites i /favorites/studios: liczniki w cache procesu (swiezy 5 min, potem
  odswiezanie w tle), mark_seen zeruje od razu; parametr counts=false dla ekranow,
  ktore chca tylko stan gwiazdki
- licznik studiow idzie indeksem ix_scenes_studio_effective_date_desc (migracja 0038)
  i konczy po 200 scenach na studio: 10-25 s -> kilkanascie ms
- okno licznika sortuje tak jak lista (data premiery albo dzien dodania), wczesniej
  rozjechane od 0033
- similar_scenes: pula z 4 najrzadszych tagow zamiast grupowania po wszystkich
  (do 5 s -> ponizej 0,4 s)
- enrich-tags: tagi jednym zapytaniem po slugach i jeden INSERT
- /scene-favorites: serializer wsadowy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 15:05:43 +02:00

92 lines
2.9 KiB
Python

"""Scene favorites, ulubione sceny (single-user, równolegle do /favorites/performers).
Endpointy:
GET /scene-favorites lista ulubionych scen (pełen SceneOut)
POST /scene-favorites/{scene_id} dodaj (idempotent)
DELETE /scene-favorites/{scene_id} usuń
"""
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.device import get_device_id
from app.api.scenes import _build_scenes_out_batch
from app.api.schemas import SceneOut
from app.auth import require_api_key
from app.db import get_session
from app.models.favorite_scene import FavoriteScene
from app.models.scene import Scene
router = APIRouter(
prefix="/scene-favorites",
tags=["scene-favorites"],
dependencies=[Depends(require_api_key)],
)
class SceneFavoriteListOut(BaseModel):
items: list[SceneOut]
total: int
class SceneFavoriteToggleOut(BaseModel):
scene_id: uuid.UUID
favorited: bool
@router.get("", response_model=SceneFavoriteListOut)
def list_scene_favorites(
session: Annotated[Session, Depends(get_session)],
device_id: Annotated[str, Depends(get_device_id)],
) -> SceneFavoriteListOut:
rows = (
session.execute(
select(Scene, FavoriteScene)
.join(FavoriteScene, FavoriteScene.scene_id == Scene.id)
.where(FavoriteScene.device_id == device_id)
.order_by(FavoriteScene.created_at.desc())
)
.all()
)
# Wsadowo jak listy scen: ekran ulubionych rysuje ten sam kafelek, a pojedyncze
# _build_scene_out robilo 7 zapytan na scene (setki ulubionych = sekundy).
items = _build_scenes_out_batch(
session, [scene for scene, _ in rows], light=True, device_id=device_id
)
return SceneFavoriteListOut(items=items, total=len(items))
@router.post(
"/{scene_id}",
response_model=SceneFavoriteToggleOut,
status_code=status.HTTP_201_CREATED,
)
def add_scene_favorite(
scene_id: uuid.UUID,
session: Annotated[Session, Depends(get_session)],
device_id: Annotated[str, Depends(get_device_id)],
) -> SceneFavoriteToggleOut:
scene = session.get(Scene, scene_id)
if scene is None:
raise HTTPException(status_code=404, detail="scene not found")
existing = session.get(FavoriteScene, (device_id, scene_id))
if existing is None:
session.add(FavoriteScene(device_id=device_id, scene_id=scene_id))
return SceneFavoriteToggleOut(scene_id=scene_id, favorited=True)
@router.delete("/{scene_id}", status_code=status.HTTP_204_NO_CONTENT)
def remove_scene_favorite(
scene_id: uuid.UUID,
session: Annotated[Session, Depends(get_session)],
device_id: Annotated[str, Depends(get_device_id)],
) -> None:
fav = session.get(FavoriteScene, (device_id, scene_id))
if fav is not None:
session.delete(fav)