Public instance has no accounts, so all user state was GLOBAL in DB — new users saw/overwrote each other's (and Jan's) favorites, watched badges and blacklists (bug 2026-06-10). Add device_id (VARCHAR 64) to 9 state tables with composite PK (device_id, entity_id); app sends X-Device-Id header (get_device_id dep). All favorites/scene-favorites/blacklist/watch + scene&movie list/detail (is_favorite, watched, blacklist-hide) now filter by device. Existing rows backfilled to 'legacy-shared'; POST /me/adopt-legacy reassigns them to the caller once. Old clients (no header) map to legacy-shared so they keep working until OTA updates. Migration 0022: add col, backfill, composite PK. Verified on prod: 967 progress rows preserved, device isolation holds (new device sees none of legacy state). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
4.2 KiB
Python
123 lines
4.2 KiB
Python
"""Blacklists — globalnie ukryte performerki/studia/tagi.
|
|
|
|
Sceny które MAJĄ blacklisted entity wypadają z każdego /scenes (pełna lista, search,
|
|
performer scenes, tag scenes). Auto-apply w `app/api/scenes.py`.
|
|
|
|
Endpointy:
|
|
GET /blacklist — wszystkie 3 listy w jednym response
|
|
POST /blacklist/{kind}/{entity_id} — dodaj (idempotent)
|
|
DELETE /blacklist/{kind}/{entity_id} — usuń
|
|
|
|
`kind` ∈ {performer, studio, tag}.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Annotated, Literal
|
|
|
|
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.auth import require_api_key
|
|
from app.db import get_session
|
|
from app.models.blacklist import (
|
|
BlacklistedPerformer,
|
|
BlacklistedStudio,
|
|
BlacklistedTag,
|
|
)
|
|
from app.models.performer import Performer
|
|
from app.models.studio import Studio
|
|
from app.models.tag import Tag
|
|
|
|
router = APIRouter(
|
|
prefix="/blacklist", tags=["blacklist"], dependencies=[Depends(require_api_key)]
|
|
)
|
|
|
|
Kind = Literal["performer", "studio", "tag"]
|
|
|
|
|
|
class BlacklistEntry(BaseModel):
|
|
id: uuid.UUID
|
|
name: str # canonical_name (performer) / name (studio/tag)
|
|
slug: str | None = None
|
|
|
|
|
|
class BlacklistOut(BaseModel):
|
|
performers: list[BlacklistEntry]
|
|
studios: list[BlacklistEntry]
|
|
tags: list[BlacklistEntry]
|
|
|
|
|
|
@router.get("", response_model=BlacklistOut)
|
|
def list_blacklist(
|
|
session: Annotated[Session, Depends(get_session)],
|
|
device_id: Annotated[str, Depends(get_device_id)],
|
|
) -> BlacklistOut:
|
|
perfs = session.execute(
|
|
select(BlacklistedPerformer.performer_id, Performer.canonical_name, Performer.slug)
|
|
.join(Performer, Performer.id == BlacklistedPerformer.performer_id)
|
|
.where(BlacklistedPerformer.device_id == device_id)
|
|
.order_by(Performer.canonical_name)
|
|
).all()
|
|
studios = session.execute(
|
|
select(BlacklistedStudio.studio_id, Studio.name, Studio.slug)
|
|
.join(Studio, Studio.id == BlacklistedStudio.studio_id)
|
|
.where(BlacklistedStudio.device_id == device_id)
|
|
.order_by(Studio.name)
|
|
).all()
|
|
tags = session.execute(
|
|
select(BlacklistedTag.tag_id, Tag.name, Tag.slug)
|
|
.join(Tag, Tag.id == BlacklistedTag.tag_id)
|
|
.where(BlacklistedTag.device_id == device_id)
|
|
.order_by(Tag.name)
|
|
).all()
|
|
return BlacklistOut(
|
|
performers=[BlacklistEntry(id=r[0], name=r[1], slug=r[2]) for r in perfs],
|
|
studios=[BlacklistEntry(id=r[0], name=r[1], slug=r[2]) for r in studios],
|
|
tags=[BlacklistEntry(id=r[0], name=r[1], slug=r[2]) for r in tags],
|
|
)
|
|
|
|
|
|
def _kind_to_entity(kind: Kind):
|
|
if kind == "performer":
|
|
return BlacklistedPerformer, Performer, "performer_id"
|
|
if kind == "studio":
|
|
return BlacklistedStudio, Studio, "studio_id"
|
|
if kind == "tag":
|
|
return BlacklistedTag, Tag, "tag_id"
|
|
raise HTTPException(status_code=400, detail="kind must be performer|studio|tag")
|
|
|
|
|
|
@router.post("/{kind}/{entity_id}", status_code=status.HTTP_200_OK)
|
|
def add_blacklist(
|
|
kind: Kind,
|
|
entity_id: uuid.UUID,
|
|
session: Annotated[Session, Depends(get_session)],
|
|
device_id: Annotated[str, Depends(get_device_id)],
|
|
) -> dict:
|
|
bl_model, parent_model, fk = _kind_to_entity(kind)
|
|
if session.get(parent_model, entity_id) is None:
|
|
raise HTTPException(status_code=404, detail=f"{kind} not found")
|
|
if session.get(bl_model, (device_id, entity_id)) is not None:
|
|
return {"kind": kind, "id": str(entity_id), "created": False}
|
|
session.add(bl_model(**{"device_id": device_id, fk: entity_id}))
|
|
session.commit()
|
|
return {"kind": kind, "id": str(entity_id), "created": True}
|
|
|
|
|
|
@router.delete("/{kind}/{entity_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def remove_blacklist(
|
|
kind: Kind,
|
|
entity_id: uuid.UUID,
|
|
session: Annotated[Session, Depends(get_session)],
|
|
device_id: Annotated[str, Depends(get_device_id)],
|
|
) -> None:
|
|
bl_model, _, _ = _kind_to_entity(kind)
|
|
row = session.get(bl_model, (device_id, entity_id))
|
|
if row is None:
|
|
return # idempotent
|
|
session.delete(row)
|
|
session.commit()
|