From e98ef6577e951563364201cedb6ca4a3e57ab104 Mon Sep 17 00:00:00 2001 From: jtrzupek Date: Tue, 9 Jun 2026 09:47:16 +0200 Subject: [PATCH] feat(api): scene hide + merge-duplicate endpoints for long-press actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /scenes/{id}/hide — marks all playback_sources dead so the scene drops out of has_playback lists (reversible via dead_at; row kept for dedup/refs). POST /scenes/{keep_id}/merge/{drop_id} — merges drop into keep via scene_merge (moves refs/performers/tags/fingerprints/playback). Backs the new tile long-press menu (hide / mark-duplicate) replacing the dead animated-preview gesture. Co-Authored-By: Claude Opus 4.8 --- app/api/scenes.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/app/api/scenes.py b/app/api/scenes.py index 45da51d..5c87914 100644 --- a/app/api/scenes.py +++ b/app/api/scenes.py @@ -831,6 +831,67 @@ def remove_performer_from_scene( session.commit() +class SceneHideOut(BaseModel): + scene_id: uuid.UUID + playback_marked_dead: int + + +@router.post("/{scene_id}/hide", response_model=SceneHideOut) +def hide_scene( + scene_id: uuid.UUID, + session: Annotated[Session, Depends(get_session)], +) -> SceneHideOut: + """Ukryj scenę (user long-press → „usuń"). Oznacza wszystkie playback_sources + jako dead → scena wypada z list (has_playback=false). Odwracalne w DB (dead_at). + Nie kasujemy wiersza sceny — zachowujemy refs/dedup, tylko znika z UI.""" + from datetime import UTC, datetime + + from app.models.playback_source import PlaybackSource + + if session.get(Scene, scene_id) is None: + raise HTTPException(status_code=404, detail="scene not found") + rows = session.execute( + select(PlaybackSource).where( + PlaybackSource.scene_id == scene_id, + PlaybackSource.dead_at.is_(None), + ) + ).scalars().all() + now = datetime.now(UTC) + for p in rows: + p.dead_at = now + p.dead_reason = "user hid scene (long-press)" + session.commit() + return SceneHideOut(scene_id=scene_id, playback_marked_dead=len(rows)) + + +class SceneMergeOut(BaseModel): + keep_id: uuid.UUID + dropped_id: uuid.UUID + + +@router.post("/{keep_id}/merge/{drop_id}", response_model=SceneMergeOut) +def merge_duplicate_scene( + keep_id: uuid.UUID, + drop_id: uuid.UUID, + session: Annotated[Session, Depends(get_session)], +) -> SceneMergeOut: + """Scal `drop_id` w `keep_id` (user long-press → „oznacz duplikat" → wybór drugiej + sceny). Przenosi refs/performers/tags/fingerprints/playback (scene_merge), kasuje + `drop`. keep = scena na której user trzyma (zostaje), drop = wskazany duplikat.""" + from app.resolve.scene_merge import MergeError, merge_scenes + + if keep_id == drop_id: + raise HTTPException(status_code=400, detail="keep_id == drop_id") + if session.get(Scene, keep_id) is None or session.get(Scene, drop_id) is None: + raise HTTPException(status_code=404, detail="scene not found") + try: + merge_scenes(session, keep_id=keep_id, drop_id=drop_id, resolved_by="user_long_press_duplicate") + except MergeError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + session.commit() + return SceneMergeOut(keep_id=keep_id, dropped_id=drop_id) + + class EnrichTagsOut(BaseModel): scene_id: uuid.UUID added: int