fix(seo): bez prywatnych blacklist i z cache indeksu sitemapy

Blacklisty sa per urzadzenie od 2026-06-10, a SEO dalej wycinalo sume blacklist
wszystkich urzadzen: 465 osob, 4477 tagow, z publicznych stron znikalo ~86% scen.
To samo zapytanie przy liczeniu do /sitemap.xml pisalo 3,7 GB plikow tymczasowych
i przy kilku wejsciach robota naraz zapychalo dysk (GOON-22). Teraz strony SEO
pokazuja to, co katalog nowemu urzadzeniu, a indeks sitemapy liczy sie raz na 6 h
(blokada na rownolegle zadania). Liczenie 135 MB plikow tymczasowych, indeks 13 s,
potem 0,2 s. Wykonawcy 20 tys. -> 81 tys., sceny 3,5 mln.

Na VPS dodatkowo temp_file_limit=2GB dla API w override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-09-22 11:09:26 +02:00
parent f33f37f117
commit 7accd4692f

View file

@ -13,8 +13,11 @@ Zasady (świadome, nie przypadkowe):
* **Age-gate = client-side overlay** (cookie `age_ok`). Treść jest w HTML, więc
crawler ją indeksuje; overlay zasłania ją tylko ludziom do potwierdzenia 18+.
Dodatkowo `RTA` meta tag dla filtrów rodzicielskich.
* **Blacklist respektowany** te same wykluczenia performer/studio/tag co w /scenes.
Nie publikujemy SEO-stron dla zblacklistowanej treści.
* **Bez blacklist.** Blacklisty sa prywatne, per urzadzenie (od 2026-06-10), a strony
SEO sa anonimowe, wiec pokazuja to, co katalog pokazuje nowemu urzadzeniu. Wczesniej
wycinalismy sume blacklist WSZYSTKICH urzadzen: 465 osob ukrylo 4477 tagow i z SEO
znikalo ~86% scen, a liczenie do /sitemap.xml pisalo 3,7 GB plikow tymczasowych
i przy kilku naraz zapychalo dysk (Sentry GOON-22, 2026-09-22).
* **Anti-thin-page** strona encji powstaje tylko gdy ma realną zawartość
(performer/studio z ≥1 żywą sceną; scena z ≥1 żywym source). Pusta encja → 404,
nie pusty doorway (Google karze masowe cienkie strony).
@ -22,6 +25,8 @@ Zasady (świadome, nie przypadkowe):
from __future__ import annotations
import os
import threading
import time
import uuid
from datetime import date
from pathlib import Path
@ -35,11 +40,6 @@ from sqlalchemy.orm import Session
from app.api.scenes import _needs_proxy, _wrap_image_proxy
from app.db import get_session
from app.models.blacklist import (
BlacklistedPerformer,
BlacklistedStudio,
BlacklistedTag,
)
from app.models.performer import Performer, PerformerAlias
from app.models.playback_source import PlaybackSource
from app.models.scene import Scene, ScenePerformer, SceneTag
@ -83,55 +83,24 @@ def _live_playback_exists():
)
def _not_blacklisted():
"""Lista warunków WHERE wykluczających zblacklistowaną treść (performer/studio/tag).
Te same reguły co w GET /scenes, żeby SEO nie publikowało tego, co katalog ukrywa.
"""
return [
~exists(
select(1)
.select_from(ScenePerformer)
.join(
BlacklistedPerformer,
BlacklistedPerformer.performer_id == ScenePerformer.performer_id,
)
.where(ScenePerformer.scene_id == Scene.id)
),
~Scene.studio_id.in_(select(BlacklistedStudio.studio_id)),
~exists(
select(1)
.select_from(SceneTag)
.join(BlacklistedTag, BlacklistedTag.tag_id == SceneTag.tag_id)
.where(SceneTag.scene_id == Scene.id)
),
]
def _indexable_scenes():
"""SELECT Scene żywych, nie-zblacklistowanych scen, baza pod listy i sitemap."""
stmt = select(Scene).where(_live_playback_exists())
for cond in _not_blacklisted():
stmt = stmt.where(cond)
return stmt
"""SELECT Scene z żywym źródłem, baza pod listy i sitemap."""
return select(Scene).where(_live_playback_exists())
def _performer_indexable_exists():
"""EXISTS: performer ma ≥1 indeksowalną (żywą, nie-blacklisted) scenę.
"""EXISTS: performer ma ≥1 indeksowalną (żywą) scenę.
Trzyma sitemap performerów w zgodzie z tym, co realnie renderuje
`performer_page`, inaczej sitemap zgłaszałby URL-e dające 404.
"""
sub = (
return exists(
select(1)
.select_from(ScenePerformer)
.join(Scene, Scene.id == ScenePerformer.scene_id)
.where(ScenePerformer.performer_id == Performer.id)
.where(_live_playback_exists())
)
for cond in _not_blacklisted():
sub = sub.where(cond)
return exists(sub)
def _scene_card_rows(session: Session, scene_ids: list[uuid.UUID]) -> dict[uuid.UUID, dict]:
@ -438,9 +407,27 @@ def _count(session: Session, stmt) -> int:
return session.execute(select(func.count()).select_from(stmt.subquery())).scalar_one()
# Indeks sitemapy liczy trzy agregaty po calym katalogu, a roboty potrafia zapytac kilka
# razy w tej samej sekundzie (2026-09-04: 5 naraz). Liczymy raz na _SITEMAP_INDEX_TTL
# w procesie; blokada sprawia, ze rownolegle zadania czekaja na jeden wynik.
_SITEMAP_INDEX_TTL = 6 * 3600.0
_sitemap_index_cache: tuple[float, str] | None = None
_sitemap_index_lock = threading.Lock()
@router.get("/sitemap.xml")
def sitemap_index(session: Annotated[Session, Depends(get_session)]) -> Response:
"""Sitemap index, listuje paginowane pod-sitemapy per typ encji."""
global _sitemap_index_cache
with _sitemap_index_lock:
cached = _sitemap_index_cache
if cached is None or time.monotonic() - cached[0] > _SITEMAP_INDEX_TTL:
cached = (time.monotonic(), _build_sitemap_index(session))
_sitemap_index_cache = cached
return Response(cached[1], media_type="application/xml")
def _build_sitemap_index(session: Session) -> str:
n_perf = _count(
session,
select(Performer.id).where(_performer_indexable_exists()),
@ -461,7 +448,7 @@ def sitemap_index(session: Annotated[Session, Depends(get_session)]) -> Response
for p in range(pages):
parts.append(f"<sitemap><loc>{bu}/sitemap/{kind}-{p}.xml</loc></sitemap>")
parts.append("</sitemapindex>")
return Response("\n".join(parts), media_type="application/xml")
return "\n".join(parts)
@router.get("/sitemap/{kind}-{page}.xml")