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 * **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+. crawler ją indeksuje; overlay zasłania ją tylko ludziom do potwierdzenia 18+.
Dodatkowo `RTA` meta tag dla filtrów rodzicielskich. Dodatkowo `RTA` meta tag dla filtrów rodzicielskich.
* **Blacklist respektowany** te same wykluczenia performer/studio/tag co w /scenes. * **Bez blacklist.** Blacklisty sa prywatne, per urzadzenie (od 2026-06-10), a strony
Nie publikujemy SEO-stron dla zblacklistowanej treści. 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ść * **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, (performer/studio z ≥1 żywą sceną; scena z ≥1 żywym source). Pusta encja → 404,
nie pusty doorway (Google karze masowe cienkie strony). nie pusty doorway (Google karze masowe cienkie strony).
@ -22,6 +25,8 @@ Zasady (świadome, nie przypadkowe):
from __future__ import annotations from __future__ import annotations
import os import os
import threading
import time
import uuid import uuid
from datetime import date from datetime import date
from pathlib import Path 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.api.scenes import _needs_proxy, _wrap_image_proxy
from app.db import get_session from app.db import get_session
from app.models.blacklist import (
BlacklistedPerformer,
BlacklistedStudio,
BlacklistedTag,
)
from app.models.performer import Performer, PerformerAlias from app.models.performer import Performer, PerformerAlias
from app.models.playback_source import PlaybackSource from app.models.playback_source import PlaybackSource
from app.models.scene import Scene, ScenePerformer, SceneTag 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(): def _indexable_scenes():
"""SELECT Scene żywych, nie-zblacklistowanych scen, baza pod listy i sitemap.""" """SELECT Scene z żywym źródłem, baza pod listy i sitemap."""
stmt = select(Scene).where(_live_playback_exists()) return select(Scene).where(_live_playback_exists())
for cond in _not_blacklisted():
stmt = stmt.where(cond)
return stmt
def _performer_indexable_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 Trzyma sitemap performerów w zgodzie z tym, co realnie renderuje
`performer_page`, inaczej sitemap zgłaszałby URL-e dające 404. `performer_page`, inaczej sitemap zgłaszałby URL-e dające 404.
""" """
sub = ( return exists(
select(1) select(1)
.select_from(ScenePerformer) .select_from(ScenePerformer)
.join(Scene, Scene.id == ScenePerformer.scene_id) .join(Scene, Scene.id == ScenePerformer.scene_id)
.where(ScenePerformer.performer_id == Performer.id) .where(ScenePerformer.performer_id == Performer.id)
.where(_live_playback_exists()) .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]: 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() 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") @router.get("/sitemap.xml")
def sitemap_index(session: Annotated[Session, Depends(get_session)]) -> Response: def sitemap_index(session: Annotated[Session, Depends(get_session)]) -> Response:
"""Sitemap index, listuje paginowane pod-sitemapy per typ encji.""" """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( n_perf = _count(
session, session,
select(Performer.id).where(_performer_indexable_exists()), 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): for p in range(pages):
parts.append(f"<sitemap><loc>{bu}/sitemap/{kind}-{p}.xml</loc></sitemap>") parts.append(f"<sitemap><loc>{bu}/sitemap/{kind}-{p}.xml</loc></sitemap>")
parts.append("</sitemapindex>") parts.append("</sitemapindex>")
return Response("\n".join(parts), media_type="application/xml") return "\n".join(parts)
@router.get("/sitemap/{kind}-{page}.xml") @router.get("/sitemap/{kind}-{page}.xml")