diff --git a/app/api/seo.py b/app/api/seo.py index 977c365..694cd8a 100644 --- a/app/api/seo.py +++ b/app/api/seo.py @@ -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"{bu}/sitemap/{kind}-{p}.xml") parts.append("") - return Response("\n".join(parts), media_type="application/xml") + return "\n".join(parts) @router.get("/sitemap/{kind}-{page}.xml")