Some checks are pending
Backend tests / test (push) Waiting to run
1) eporner/phone-resolve: gdy strona parsuje sie OK, ale CDN jest nieosiagalny z
sieci usera (audit: siec zglaszajacego nulluje vid-*.eporner.com -> 0.0.0.0, a
www.eporner.com dziala), phone re-resolve zwracal TEN SAM URL, a lancuch fallback
nie ratowal: dla phone-resolved linkow fallbackProxyUrl === url (resolver ustawia
stream_url = direct_url), wiec krok proxy byl no-opem i szlo prosto do WebView =
strona hostera z reklamami ('zaladowala sie reklama zamiast wideo'). Teraz po
nieudanym phone-resolve bierzemy PRAWDZIWE proxy z backendu (VPS pobiera swoim
DNS-em). Tylko na sciezce bledu. Eporner globalnie zdrowy (89-100%, ~100 urzadzen
dziennie), wiec to fix dla userow z blokowanym CDN, nie regresja globalna.
2) porntrex zmienil markup kafelka: class="duration">MM:SS< ->
<div class="durations"><i class="fa fa-clock-o"></i> MM:SS</div>. Stary wzorzec
przestal matchowac -> duration_sec=NULL dla ~95% swiezych scen (6479/6815 w 7 dni),
a mobile domyslnie filtruje min_duration_sec=60, wiec te sceny byly NIEWIDOCZNE
('nie widze nowych scen z porntrex'). Wzorzec obsluguje oba warianty + H:MM:SS.
Zweryfikowane na zywym listingu: 84/85 kafelkow, 20:45 -> 1245s.
130 lines
5.1 KiB
Python
130 lines
5.1 KiB
Python
"""porntrex.com — latest-vids BROWSE scraper (KVS), obok istniejącego search scrapera.
|
|
|
|
PornTrexScraper (search, performer-driven) zostaje w ALL_DIRECT_SCRAPERS — daje
|
|
pokrycie back-catalogu performerów. Ten browse dokłada gwarancję świeżości wprost
|
|
z feedu `/latest-updates/<n>/` (próg watchdog 48h zamiast 168h, nie zależy od kolejki
|
|
performerów). Wzorzec jak xvideos (search + browse równolegle).
|
|
|
|
KVS listing tile:
|
|
<div ... data-item-id="<id>"><a href="https://www.porntrex.com/video/<id>/<slug>">
|
|
<img data-src="//ptx.cdntrex.com/contents/.../300x168/1.jpg" alt="<Tytuł>">
|
|
<div class="duration">MM:SS</div>
|
|
Playback: KVS, natywny extractor `porntrexcom` (token expires+md5, portable) — bez zmian.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
import logging
|
|
import re
|
|
|
|
from app.connectors.base import RawFingerprint, RawPlaybackSource, RawScene
|
|
from app.connectors.direct_scrapers._browse_base import (
|
|
BaseBrowseScraper,
|
|
compute_thumbnail_phash,
|
|
)
|
|
from app.extractors import browser_get
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_BASE = "https://www.porntrex.com"
|
|
_A_RE = re.compile(
|
|
r'<a\s+href="(?P<url>https?://(?:www\.)?porntrex\.com/video/\d+/[^"]*)"', re.IGNORECASE
|
|
)
|
|
_ALT_RE = re.compile(r'alt="([^"]*)"')
|
|
_THUMB_RE = re.compile(r'data-src="(//[^"]+\.(?:jpg|jpeg|webp|png)[^"]*)"', re.IGNORECASE)
|
|
# porntrex zmienił markup kafelka (audit 2026-07-26): `class="duration">MM:SS<` →
|
|
# `<div class="durations"><i class="fa fa-clock-o"></i> MM:SS</div>`. Stary wzorzec
|
|
# przestał matchować → duration_sec=NULL dla ~95% świeżych scen, a mobile domyślnie
|
|
# filtruje `min_duration_sec=60`, więc te sceny BYŁY NIEWIDOCZNE w apce (6479 scen w
|
|
# 7 dni; user: "nie widzę nowych scen z porntrex"). Wzorzec obsługuje oba warianty:
|
|
# liczbę mnogą, opcjonalną ikonę <i> przed wartością i H:MM:SS.
|
|
_DUR_RE = re.compile(
|
|
r'class="durations?"[^>]*>(?:\s*<i[^>]*>\s*</i>)?\s*([\d]{1,2}(?:\s*:\s*[\d]{2}){1,2})'
|
|
)
|
|
|
|
|
|
def _parse_duration(text: str | None) -> int | None:
|
|
if not text:
|
|
return None
|
|
try:
|
|
nums = [int(p.strip()) for p in text.split(":")]
|
|
except ValueError:
|
|
return None
|
|
if len(nums) == 2:
|
|
return nums[0] * 60 + nums[1]
|
|
if len(nums) == 3:
|
|
return nums[0] * 3600 + nums[1] * 60 + nums[2]
|
|
return None
|
|
|
|
|
|
class PornTrexBrowseScraper(BaseBrowseScraper):
|
|
sitetag = "porntrexcom"
|
|
|
|
def _listing_url(self, page: int) -> str:
|
|
return f"{_BASE}/latest-updates/{page}/"
|
|
|
|
def _extract_scene_urls(self, listing_html: str) -> list[str]:
|
|
return [m.group("url") for m in _A_RE.finditer(listing_html)]
|
|
|
|
def _parse_detail(self, scene_url: str, detail_html: str) -> RawScene | None:
|
|
return None
|
|
|
|
def crawl_page(self, page: int) -> list[RawScene] | None:
|
|
url = self._listing_url(page)
|
|
try:
|
|
res = browser_get(url, timeout=self._timeout)
|
|
text = res.text if hasattr(res, "text") else res
|
|
except Exception as e:
|
|
log.warning("porntrex browse fetch failed (page %d): %s", page, e)
|
|
return None
|
|
|
|
out: list[RawScene] = []
|
|
seen: set[str] = set()
|
|
anchors = list(_A_RE.finditer(text))
|
|
for idx, m in enumerate(anchors):
|
|
scene_url = m.group("url").replace("://www.", "://").rstrip("/")
|
|
if scene_url in seen:
|
|
continue
|
|
seen.add(scene_url)
|
|
win = text[m.start(): (anchors[idx + 1].start() if idx + 1 < len(anchors) else m.end() + 700)]
|
|
|
|
am = _ALT_RE.search(win)
|
|
title = html.unescape(am.group(1)).strip() if am else ""
|
|
if not title:
|
|
# fallback: slug → tytuł
|
|
sl = re.search(r"/video/\d+/([a-z0-9\-]+)", scene_url)
|
|
title = sl.group(1).replace("-", " ").strip().title() if sl else ""
|
|
if not title:
|
|
continue
|
|
tm = _THUMB_RE.search(win)
|
|
thumb = ("https:" + tm.group(1)) if tm else None
|
|
dm = _DUR_RE.search(win)
|
|
duration_sec = _parse_duration(dm.group(1) if dm else None)
|
|
|
|
fingerprints: list[RawFingerprint] = []
|
|
if thumb:
|
|
ph = compute_thumbnail_phash(thumb, referer=_BASE + "/")
|
|
if ph:
|
|
fingerprints.append(RawFingerprint(kind="phash", value=ph))
|
|
|
|
out.append(
|
|
RawScene(
|
|
external_id=f"{self.sitetag}:{scene_url}",
|
|
title=title,
|
|
duration_sec=duration_sec,
|
|
url=scene_url,
|
|
performers=[],
|
|
tags=[],
|
|
fingerprints=fingerprints,
|
|
playback_sources=[
|
|
RawPlaybackSource(
|
|
origin=f"tube:{self.sitetag}",
|
|
page_url=scene_url,
|
|
duration_sec=duration_sec,
|
|
thumbnail_url=thumb,
|
|
)
|
|
],
|
|
)
|
|
)
|
|
log.info("porntrex browse page %d: %d scenes", page, len(out))
|
|
return out
|