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 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-08-04 14:25:00 +02:00
parent 6199577ae7
commit 81f0074d34

View file

@ -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()