goon/app/connectors/direct_scrapers/javflix.py
goon-foss 09f389100a
Some checks are pending
Backend tests / test (push) Waiting to run
feat(watchdog): jakosc zrodel, progi adaptacyjne, dlugosci perverzija i mypornerleak, poprawki parserow
- browse_runs (migracja 0056): jeden wiersz per scraper i przebieg browse-latest
- quality_watchdog: listing pusty albo bez nowych, skok lub zanik obsady, sceny bez
  dlugosci i miniatur, spadek udanych odtworzen per origin; fingerprint per zrodlo i rodzaj
- ingest_watchdog: prog swiezosci z mediany odstepow zrodla (3x, max 14 d) zamiast stalej
- duration backfill: resolvery perverzija (JSON-LD) i mypornerleak (itemprop), paczka
  po rowno z originow, 404/410 zamyka wiersz od razu
- parsery: galaxyporn data z JSON-LD, hentaimama dlugosc, javflix aktorki, fullmovies
  i hdporngg data z uploadDate
2026-10-14 12:13:13 +02:00

131 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""javflix.cc, JAV browse scraper (WordPress, English-subbed JAV).
Osobna pula JAV (kody typu BKD-368, tytuły azjatyckie), origin `tube:javflix`.
NIE deduplikuje się z zachodnim katalogiem, świadomie orphan vertical (sekcja JAV).
Struktura (RE 2026-07-10):
- listing: `/page/N/` (WordPress archive), posty pod `/<slug>/` (np. /bkd-368-english-subtitle/)
- detail: metadane w `itemprop` (name/thumbnailUrl/uploadDate; BRAK duration),
playback = przyciski serwerów `<a class="myLink" name="<hoster>" href="<embed>">`
(streamtape/voe/doodstream/emturbovid). Href jest w SUROWYM HTML (theme strippuje go
po renderze JS, ale scraper widzi surowy). Embed-hostery obsługuje generyczny
ekstraktor `_embed_iframe` (anchor-hoster pattern) → rejestrujemy go pod `javflix`.
"""
from __future__ import annotations
import html
import re
from app.connectors.base import RawPlaybackSource, RawScene, RawTag
from app.connectors.direct_scrapers._browse_base import BaseBrowseScraper
from app.connectors.base import RawPerformer
from app.connectors.direct_scrapers._playtube import _parse_iso_date
_BASE = "https://javflix.cc"
# Posty to `javflix.cc/<slug>/`. Odsiewamy strony nie-postowe (taksonomie, statyczne).
_POST_RE = re.compile(r'href="(https://javflix\.cc/[a-z0-9][a-z0-9\-]{4,}/)"', re.IGNORECASE)
_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)
# Aktorki: tylko z bloku `<div id="video-actors">Actress: <a href=".../actor/<slug>/">`,
# jeden na scene zwykle. Do 2026-10-14 scraper ich nie bral (0 performerow na scene),
# a JAV bez aktorki nie da sie laczyc z kanonem ani filtrowac po osobie.
_ACTORS_BLOCK_RE = re.compile(r'<div id="video-actors">(.*?)</div>', re.IGNORECASE | re.DOTALL)
_ACTOR_RE = re.compile(r'href="https://javflix\.cc/actor/([a-z0-9\-]+)/"[^>]*>([^<]+)</a>', re.IGNORECASE)
def _itemprop(html: str, name: str) -> str | None:
m = re.search(
rf'itemprop="{name}"\s+content="([^"]+)"', html, re.IGNORECASE
)
return m.group(1).strip() if m else None
class JavflixScraper(BaseBrowseScraper):
sitetag = "javflix"
def _listing_url(self, page: int) -> str:
# `/new/` PRZED stroną główną, bo tylko tam trafiają świeże posty: audyt
# 2026-08-20 dał na `/new/` 7 nieznanych scen z 9, a na stronie głównej 0 z 16.
# Strona główna to stały zestaw, ktory od dawna mamy w calosci.
return f"{_BASE}/new/" if page <= 1 else f"{_BASE}/"
def crawl_page(self, page: int):
# Paginacji tu NIE MA, mimo ze WordPress ja udaje: `/page/2/` i `/page/3/`
# oddaja identyczna tresc co strona glowna, a `/new/page/2/` to samo co `/new/`.
# Bez tego ograniczenia browse-latest mielil ten sam zestaw piec razy na run.
if page > 2:
return []
return super().crawl_page(page)
def _extract_scene_urls(self, listing_html: str) -> list[str]:
seen: set[str] = set()
out: list[str] = []
for m in _POST_RE.finditer(listing_html):
url = m.group(1)
if any(x in url for x in _NON_POST):
continue
if url.rstrip("/") == _BASE:
continue
if url not in seen:
seen.add(url)
out.append(url)
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"<title>([^<]+)</title>", detail_html)
title = tm.group(1).split(" – ")[0].strip() if tm else None
if not title:
return None
title = html.unescape(title).strip()
thumb = _itemprop(detail_html, "thumbnailUrl")
up = _itemprop(detail_html, "uploadDate")
release_date = _parse_iso_date(up) if up else None
# JAV code (BKD-368) ze sluga, kanoniczny identyfikator, dodajemy jako tag
# (searchable) bo javflix nie ma osobnego pola kodu.
slug = scene_url.rstrip("/").rsplit("/", 1)[-1]
cm = _CODE_RE.match(slug)
tags: list[RawTag] = []
if cm:
code = cm.group(1).upper()
tags.append(RawTag(external_id=f"javcode:{code}", name=code, slug=cm.group(1).lower()))
performers: list[RawPerformer] = []
ab = _ACTORS_BLOCK_RE.search(detail_html)
if ab:
seen_p: set[str] = set()
for am in _ACTOR_RE.finditer(ab.group(1)):
a_slug = am.group(1)
a_name = html.unescape(am.group(2)).strip()
if a_slug in seen_p or not a_name:
continue
seen_p.add(a_slug)
performers.append(RawPerformer(external_id=f"{self.sitetag}:actor:{a_slug}", name=a_name))
return RawScene(
external_id=f"{self.sitetag}:{scene_url}",
title=title,
release_date=release_date,
url=scene_url,
performers=performers,
tags=tags,
playback_sources=[
RawPlaybackSource(
origin=f"tube:{self.sitetag}",
page_url=scene_url,
thumbnail_url=thumb,
)
],
)