diff --git a/app/connectors/direct_scrapers/vjav.py b/app/connectors/direct_scrapers/vjav.py index d58f7c3..9e0fb77 100644 --- a/app/connectors/direct_scrapers/vjav.py +++ b/app/connectors/direct_scrapers/vjav.py @@ -59,6 +59,47 @@ def _parse_duration(value: str | None) -> int | None: return sec or None +# --- bramka jakości tytułu ---------------------------------------------------------- +# vjav zaczął sypać śmieciem: pomiar 2026-08-04 na 295 scenach z doby po naprawie +# dostępu dał 78,6% tytułów typu slug-concat i ZERO przypisanej obsady. Takie sceny są +# w apce nieodnajdywalne — nie mają ani tytułu, ani performera, po którym da się trafić. +# W starym katalogu ten sam pomiar daje 27% (wcześniejsza liczba 2,9% była zaniżona, +# bo sprawdzała tylko jeden z czterech wzorców). +_JUNK_ID_JAV = re.compile(r"^\d{6,}_JAV", re.IGNORECASE) # 0008364_JAV_JapaneseAV_... +_JUNK_HEX_ID = re.compile(r"^x\d+x[0-9A-F]{12,}$") # x1x84BDE66B5F8862EDA6 +_JUNK_CODE_MASH = re.compile(r"^[A-Za-z]{2,4}\d{2,4}-[A-Z]{6,}\d{6,}$") # QCT062-KPUDYLMLBL913295279 + + +def _has_cjk(text: str) -> bool: + """Tytuły japońskie/chińskie MUSZĄ omijać heurystyki znakowe: `str.isalpha()` jest + prawdziwe dla kana i kanji, a japoński nie stawia spacji, więc reguły „za mało + samogłosek" i „brak spacji" odrzuciłyby prawdziwe tytuły na serwisie JAV. Ta pomyłka + wyszła przy pomiarze pierwszej wersji bramki — bez tego wyjątku poszłoby 708 z 708 + tytułów CJK z próbki.""" + return sum(1 for c in text if " " <= c <= "鿿" or "＀" <= c <= "￯") >= 3 + + +def _is_junk_title(title: str) -> bool: + """True gdy tytuł nie niesie żadnej informacji (id, hash, walnięcie w klawiaturę). + + Zmierzone na próbce 6000 tytułów vjav: odrzuca 76% świeżych i 27% starych, przy + ZERO odrzuconych tytułach CJK. Przepuszczone przykłady to normalne zdania + („Maki Horiguchis Erotic Shower...", „200GANA-2482 Japanese Amateur").""" + t = (title or "").strip() + if not t: + return True + if _JUNK_ID_JAV.search(t) or _JUNK_HEX_ID.search(t) or _JUNK_CODE_MASH.search(t): + return True + if _has_cjk(t): + return False + letters = [c for c in t if "a" <= c.lower() <= "z"] + if len(letters) >= 12: + vowels = sum(1 for c in letters if c.lower() in "aeiouy") + if vowels / len(letters) < 0.22: + return True + return " " not in t and len(t) >= 20 + + def _parse_post_date(value: str | None) -> date | None: if not value: return None @@ -133,6 +174,8 @@ class VjavScraper(BaseBrowseScraper): slug = (v.get("dir") or "").strip() if not title or not slug: return None + if _is_junk_title(title): + return None page_url = f"{_BASE}/videos/{vid}/{slug}/" duration_sec = _parse_duration(v.get("duration")) release_date = _parse_post_date(v.get("post_date")) @@ -189,6 +232,29 @@ class VjavScraper(BaseBrowseScraper): raw={"source": "vjav_api", "id": vid}, ) + def latest_scenes(self, *, max_pages: int = 5): + """Własna iteracja, bo bazowa przerywa na pierwszej pustej stronie. + + Po wprowadzeniu bramki jakości pusta strona przestała znaczyć „koniec + katalogu": strona 1 bywa w 100% śmieciowa (same `0008364_JAV_...`), więc po + odsianiu zwraca [], a bazowe `latest_scenes` traktowało to jak wyczerpanie i + kończyło CAŁY run — źródło dawało `seen: 0` mimo 39 dobrych scen na stronach + 2-5. To ta sama pułapka „[] znaczy wyczerpane" co w `_load_ids`. + + Tu wyczerpanie rozstrzyga długość listy id, nie liczba scen po filtrze. + """ + if self._sorted_ids is None: + self._load_ids() + if not self._sorted_ids: + return + for page in range(1, max_pages + 1): + if (page - 1) * _PER_PAGE >= len(self._sorted_ids): + return # realny koniec katalogu + scenes = self.crawl_page(page) + if scenes is None: + return # transient fail + yield from scenes + def crawl_page(self, page: int) -> list[RawScene] | None: if self._sorted_ids is None: self._load_ids()