From 81f0074d342d21c30bb1c7d5c8ebb0de009102f0 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Tue, 4 Aug 2026 14:25:00 +0200 Subject: [PATCH] fix(vjav): bramka jakosci tytulu + wlasna iteracja stron Po naprawie dostepu vjav ruszyl, ale zaczal sypac smieciem: pomiar na 295 scenach z doby po fixie dal 78,6 proc. tytulow slug-concat i ZERO przypisanej obsady. Takie sceny sa w apce nieodnajdywalne - nie maja ani tytulu, ani performera. Bramka odrzuca cztery wzorce: prefiks id_JAV, hash x1x..., kod typu QCT062-KPUDYLMLBL913295279 oraz ciagi bez samoglosek/spacji (walniecie w klawiature). Zmierzone na probce 6000 tytulow: odrzuca 80 proc. swiezych i 27 proc. starych, przy 39 przepuszczonych na 200 obejrzanych - zrodlo nie jest wyzerowane. WAZNE, znalezione przy pomiarze pierwszej wersji: str.isalpha() jest prawdziwe dla kana i kanji, a japonski nie stawia spacji, wiec reguly "za malo samoglosek" i "brak spacji" odrzucalyby PRAWDZIWE tytuly na serwisie JAV - 708 z 708 tytulow CJK z probki. Stad jawny wyjatek _has_cjk. Kontrola jednostkowa: 8/8 przypadkow, w tym dwa CJK. Drugi blad, wprowadzony przez sama bramke i zlapany dopiero pilotem: strona 1 jest w 100 proc. smieciowa, wiec po odsianiu zwracala [], a bazowe latest_scenes traktuje pusta strone jako koniec katalogu i konczylo caly run. Zrodlo dawalo seen: 0 mimo 39 dobrych scen na stronach 2-5. Wlasna iteracja rozstrzyga wyczerpanie po dlugosci listy id, nie po liczbie scen po filtrze. To ta sama pulapka co w _load_ids. Pilot po obu poprawkach: seen 39 (wszystkie skipped - weszly przed bramka). Co-Authored-By: Claude Opus 5 --- app/connectors/direct_scrapers/vjav.py | 66 ++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) 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()