Some checks are pending
Backend tests / test (push) Waiting to run
- 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
131 lines
5.7 KiB
Python
131 lines
5.7 KiB
Python
"""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,
|
||
)
|
||
],
|
||
)
|