Zgloszenie 3910b19b: brak przycisku ukrywania stron. Tabela hidden_sites
(0054), POST/DELETE /blacklist/sites/{origin}, pole sites w GET /blacklist.
Scena znika z list tylko gdy wszystkie jej zywe zrodla sa z ukrytych stron
(ta sama scena z innego serwisu zostaje); w szczegolach sceny przyciski
ukrytych stron znikaja, o ile zostaje inne zrodlo.
Cache pustosci blacklist trzyma teraz, ktore listy sa niepuste, wiec ukryte
strony nie doklejaja trzech pustych NOT EXISTS, i jest czyszczony przy kazdej
zmianie (wczesniej pierwszy wpis dzialal po 5 minutach). Kod synchronizacji
kopiuje hidden_sites oraz favorite_sites, ktorych tam brakowalo.
Sandbox na produkcyjnej bazie: strona porndoe 42 z 60 scen tylko z porndoe
przed ukryciem, 0 po; feed 0,34 s, duze tagi 0,3 do 0,8 s z dwiema ukrytymi.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
6.2 KiB
Python
175 lines
6.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}.
|
|
|
|
Ukryte strony (2026-10-06, zgloszenie 3910b19b): origin to napis, nie UUID, wiec osobne
|
|
trasy `/blacklist/sites/{origin}`, zadeklarowane PRZED `/{kind}/{entity_id}`.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import uuid
|
|
from typing import Annotated, Literal
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import exists, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.device import get_device_id
|
|
from app.api.scenes import invalidate_blacklist_cache
|
|
from app.auth import require_api_key
|
|
from app.db import get_session
|
|
from app.models.blacklist import (
|
|
BlacklistedPerformer,
|
|
BlacklistedStudio,
|
|
BlacklistedTag,
|
|
)
|
|
from app.models.hidden_site import HiddenSite
|
|
from app.models.performer import Performer
|
|
from app.models.playback_source import PlaybackSource
|
|
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]
|
|
# Ukryte strony: same originy ('tube:porndoecom'), nazwy apka bierze z /sources.
|
|
sites: list[str] = []
|
|
|
|
|
|
@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()
|
|
sites = session.execute(
|
|
select(HiddenSite.origin).where(HiddenSite.device_id == device_id).order_by(HiddenSite.origin)
|
|
).scalars().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],
|
|
sites=list(sites),
|
|
)
|
|
|
|
|
|
_ORIGIN_RE = re.compile(r"^[a-z0-9][a-z0-9:._-]{1,63}$")
|
|
|
|
|
|
@router.post("/sites/{origin}", status_code=status.HTTP_200_OK)
|
|
def hide_site(
|
|
origin: str,
|
|
session: Annotated[Session, Depends(get_session)],
|
|
device_id: Annotated[str, Depends(get_device_id)],
|
|
) -> dict:
|
|
if not _ORIGIN_RE.match(origin):
|
|
raise HTTPException(status_code=400, detail="bad origin")
|
|
if session.get(HiddenSite, (device_id, origin)) is not None:
|
|
return {"origin": origin, "created": False}
|
|
known = session.execute(
|
|
select(exists(select(1).where(PlaybackSource.origin == origin)))
|
|
).scalar_one()
|
|
if not known:
|
|
raise HTTPException(status_code=404, detail="site not found")
|
|
session.add(HiddenSite(device_id=device_id, origin=origin))
|
|
session.commit()
|
|
invalidate_blacklist_cache(device_id)
|
|
return {"origin": origin, "created": True}
|
|
|
|
|
|
@router.delete("/sites/{origin}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def unhide_site(
|
|
origin: str,
|
|
session: Annotated[Session, Depends(get_session)],
|
|
device_id: Annotated[str, Depends(get_device_id)],
|
|
) -> None:
|
|
row = session.get(HiddenSite, (device_id, origin))
|
|
if row is not None:
|
|
session.delete(row)
|
|
session.commit()
|
|
invalidate_blacklist_cache(device_id)
|
|
|
|
|
|
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()
|
|
invalidate_blacklist_cache(device_id)
|
|
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()
|
|
invalidate_blacklist_cache(device_id)
|