diff --git a/app/api/scenes.py b/app/api/scenes.py index eb568af..6807359 100644 --- a/app/api/scenes.py +++ b/app/api/scenes.py @@ -59,7 +59,9 @@ def _default_scene_count(session: Session) -> int: PlaybackSource.scene_id == Scene.id, PlaybackSource.dead_at.is_(None), ) - ) + ), + # Domyślny feed wyklucza JAV (osobna sekcja) → licznik też. + ~_jav_source_exists(), ).subquery() ) total = session.execute(count_query).scalar_one() @@ -68,6 +70,23 @@ def _default_scene_count(session: Session) -> int: return total +# JAV vertical — osobna sekcja. Sceny z tych originów NIE wchodzą do głównego feedu +# (domyślnie wykluczone), tylko do zakładki JAV (?jav=true). JAV to osobny namespace +# (kody typu BKD-368, azjatyckie tytuły), nie deduplikuje się z zachodnim katalogiem, +# więc scena JAV ma WYŁĄCZNIE origin JAV → wykluczenie/inkluzja po tym originie jest pewna. +JAV_ORIGINS = ("tube:javflix", "tube:javguru", "tube:vjav", "tube:supjav") + + +def _jav_source_exists(): + return exists( + select(1).where( + PlaybackSource.scene_id == Scene.id, + PlaybackSource.dead_at.is_(None), + PlaybackSource.origin.in_(JAV_ORIGINS), + ) + ) + + # Blacklisty (performer/studio/tag) są zwykle PUSTE (self-hosted, single-user). Mimo to # 3 NOT EXISTS klauzule doklejały się do KAŻDEJ filtrowanej listy scen i były ewaluowane # per-row — przy filtrze typu duży-tag/has_playback planer chodzi po ~176k scen, więc te @@ -222,6 +241,13 @@ def list_scenes( "z jedynym playback z hqporner (~7-min Brazzers trailer clipy zalewają katalog)." ), ), + jav: bool = Query( + default=False, + description=( + "False (default): ukrywa sceny JAV (osobny vertical). True: TYLKO sceny JAV. " + "JAV to osobna sekcja w apce, nie zalewa głównego feedu (origin javflix/javguru/vjav/supjav)." + ), + ), sort: str = Query(default="created_at", description="created_at|release_date|title|studio"), page: int = Query(default=1, ge=1), per_page: int = Query(default=50, ge=1, le=200), @@ -322,6 +348,13 @@ def list_scenes( ) ) + # JAV vertical gate: domyślnie JAV wykluczone z głównego feedu; ?jav=true → TYLKO JAV. + # (origin=... diagnostyka nadal podlega bramie — do JAV użyj jav=true.) + if jav: + base = base.where(_jav_source_exists()) + else: + base = base.where(~_jav_source_exists()) + # Blacklisty device (performer/studio/tag) — globalne wykluczenia, współdzielone z # licznikiem +N ulubionych. Puste blacklisty → [] (typowy single-user, zero kosztu). for _bl_clause in blacklist_clauses(session, device_id): @@ -373,7 +406,7 @@ def list_scenes( and not perf_id_strings and origin is None and has_playback is None and min_duration_sec is None and max_duration_sec is None and released_within_days is None - and min_quality_p is None + and min_quality_p is None and not jav ) # Count strategy: # - PURE default: cached pełny licznik katalogu (TTL 10 min). diff --git a/app/connectors/direct_scrapers/__init__.py b/app/connectors/direct_scrapers/__init__.py index 5841c91..0c76da1 100644 --- a/app/connectors/direct_scrapers/__init__.py +++ b/app/connectors/direct_scrapers/__init__.py @@ -135,6 +135,7 @@ from app.connectors.direct_scrapers.yesporn import YesPornVipScraper # noqa: E4 from app.connectors.direct_scrapers.fullmovies import FullmoviesScraper # noqa: E402 from app.connectors.direct_scrapers.hdporngg import HDPornGGScraper # noqa: E402 from app.connectors.direct_scrapers.hqfap import HQFapScraper # noqa: E402 +from app.connectors.direct_scrapers.javflix import JavflixScraper # noqa: E402 from app.connectors.direct_scrapers.neporn import NepornScraper # noqa: E402 from app.connectors.direct_scrapers.superporn import SuperpornScraper # noqa: E402 from app.connectors.direct_scrapers.eporner_api import EpornerApiScraper # noqa: E402 @@ -218,6 +219,9 @@ ALL_BROWSE_SCRAPERS: list[type[BaseBrowseScraper]] = [ # bo strona wróciła na CDN vstor.top z realnymi plikami (portable cross-IP), user request. HQFapScraper, # FourK69Scraper — USUNIĘTY 2026-06-25 (ten sam stub), NIE sprawdzany ponownie. + # JavflixScraper — JAV vertical (osobna sekcja). origin tube:javflix jest w + # JAV_ORIGINS → list_scenes wyklucza je z głównego feedu (tylko zakładka JAV). + JavflixScraper, # NepornScraper — dołączony 2026-06-10 (user request). KVS engine (jak freshporno/ # porn00), /latest-updates/N/. JSON-LD (title+desc+uploadDate+thumb) + video:duration # meta + /models/ performerzy + /categories/ tagi. Brak studio (tytuł bywa diff --git a/app/connectors/direct_scrapers/javflix.py b/app/connectors/direct_scrapers/javflix.py index a21cbbd..d36a2af 100644 --- a/app/connectors/direct_scrapers/javflix.py +++ b/app/connectors/direct_scrapers/javflix.py @@ -13,6 +13,7 @@ Struktura (RE 2026-07-10): """ from __future__ import annotations +import html import re from app.connectors.base import RawPlaybackSource, RawScene, RawTag @@ -26,6 +27,7 @@ _NON_POST = ( "/page/", "/category/", "/categories/", "/genre/", "/maker/", "/actress/", "/actors/", "/tag/", "/tags/", "/studio/", "/label/", "/series/", "/wp-", "/18-usc", "/dmca", "/contact", "/privacy", "/about", "/2257", + "/terms", "/faq", "/policy", "/disclaimer", "/sitemap", ) _CODE_RE = re.compile(r"^([a-z]+-?\d+[a-z]?)", re.IGNORECASE) @@ -58,12 +60,17 @@ class JavflixScraper(BaseBrowseScraper): return out def _parse_detail(self, scene_url: str, detail_html: str) -> RawScene | None: + # Prawdziwy post video ma przyciski serwerów (`class="myLink"`). Strony statyczne + # (Terms/FAQ/DMCA) ich nie mają → pomijamy (URL-filter nie łapie wszystkich). + if 'class="myLink"' not in detail_html: + return None title = _itemprop(detail_html, "name") if not title: tm = re.search(r"