goon/app/connectors/direct_scrapers/mypornerleak.py
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

144 lines
5.5 KiB
Python

"""mypornerleak.com, performer-page listing scrape (search-based, performer-driven).
2026-06-16 fix (zamrożony od 05-07): treść serwowana jest pod subdomeną
`w8.mypornerleak.com` (load-balancer wN.), a stary regex szukał scen na gołym
`mypornerleak.com/<slug>/` → 0 trafień. Kanoniczny host i tak serwuje stronę
listingu i sceny, więc fetchujemy `mypornerleak.com/actor/<slug>/` a linki scen
normalizujemy wN.→ kanoniczny (stabilny page_url/dedup).
Wszystko z LISTINGU (bez detail-fetcha), karta `<article>` koduje metadane:
- klasa `actors-<slug>` (multi) → performerzy; `category-<slug>` (multi) → tagi
- `<a href title="...">` → URL sceny + czysty tytuł
- `<span class="duration">MM:SS|HH:MM:SS`
- `<img data-src>` → thumbnail
Bez studia: mypornerleak to repost leaków OnlyFans/amatorskich (brak studyjnego
źródła). Playback przez extractor `mypornerleakcom` (_embed_iframe → hoster).
"""
from __future__ import annotations
import html
import logging
import re
from collections.abc import Iterator
from app.connectors.base import RawPerformer, RawPlaybackSource, RawScene, RawTag
from app.connectors.direct_scrapers._search_base import BaseSearchScraper
from app.extractors import browser_get
from app.normalize.text import slugify
log = logging.getLogger(__name__)
_BASE = "https://mypornerleak.com"
_ARTICLE_RE = re.compile(r'<article[^>]*\bclass="([^"]+)"', re.IGNORECASE)
_LINK_RE = re.compile(r'<a\s+href="([^"]+)"\s+title="([^"]+)"', re.IGNORECASE)
_DURATION_RE = re.compile(r'class="duration">(?:<[^>]+>)*\s*(\d{1,2}:\d{2}(?::\d{2})?)', re.IGNORECASE)
_THUMB_RE = re.compile(r'data-src="([^"]+)"', re.IGNORECASE)
_WN_HOST_RE = re.compile(r"https?://w\d+\.mypornerleak\.com", re.IGNORECASE)
_CLASS_ACTOR_RE = re.compile(r"\bactors-([a-z0-9-]+)")
_CLASS_CAT_RE = re.compile(r"\bcategory-([a-z0-9-]+)")
def _parse_duration(s: str) -> int | None:
parts = s.split(":")
try:
if len(parts) == 2:
return int(parts[0]) * 60 + int(parts[1])
if len(parts) == 3:
return int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2])
except ValueError:
return None
return None
def _name_from_slug(slug: str) -> str:
return " ".join(w.capitalize() for w in slug.split("-") if w)
class MyPornerLeakScraper(BaseSearchScraper):
sitetag = "mypornerleakcom"
def search(
self, query: str, *, page: int = 1, limit: int | None = None
) -> Iterator[RawScene]:
actor_slug = slugify(query)
if not actor_slug:
return
url = f"{_BASE}/actor/{actor_slug}/" + (f"page/{page}/" if page > 1 else "")
try:
r = browser_get(url, timeout=self._timeout)
except Exception as e:
log.warning("mypornerleak actor-page fetch failed (%s): %s", url, e)
return
if r.status_code != 200:
return
text = r.text
anchors = list(_ARTICLE_RE.finditer(text))
seen: set[str] = set()
yielded = 0
for idx, m in enumerate(anchors):
cls = m.group(1)
win_end = anchors[idx + 1].start() if idx + 1 < len(anchors) else m.end() + 1500
window = text[m.start():win_end]
link_m = _LINK_RE.search(window)
if not link_m:
continue
scene_url = _WN_HOST_RE.sub(_BASE, link_m.group(1)).rstrip("/") + "/"
if scene_url in seen:
continue
seen.add(scene_url)
title = html.unescape(link_m.group(2)).strip()
if not title:
continue
dur_m = _DURATION_RE.search(window)
duration_sec = _parse_duration(dur_m.group(1)) if dur_m else None
thumb_m = _THUMB_RE.search(window)
thumb = thumb_m.group(1) if thumb_m else None
# Performerzy + tagi z klasy <article>.
performers: list[RawPerformer] = []
seen_perf: set[str] = set()
for am in _CLASS_ACTOR_RE.finditer(cls):
sl = am.group(1)
if sl in seen_perf:
continue
seen_perf.add(sl)
performers.append(
RawPerformer(external_id=f"{self.sitetag}:performer:{sl}", name=_name_from_slug(sl))
)
if not performers:
performers.append(
RawPerformer(external_id=f"{self.sitetag}:performer:{actor_slug}", name=query.strip())
)
tags: list[RawTag] = []
seen_tag: set[str] = set()
for cm in _CLASS_CAT_RE.finditer(cls):
sl = re.sub(r"-(porn|leaks?|videos?)$", "", cm.group(1))
if not sl or sl in seen_tag:
continue
seen_tag.add(sl)
tags.append(RawTag(external_id=f"{self.sitetag}:tag:{sl}", name=_name_from_slug(sl), slug=sl))
yield RawScene(
external_id=f"{self.sitetag}:{scene_url}",
title=title,
duration_sec=duration_sec,
url=scene_url,
performers=performers,
tags=tags,
playback_sources=[
RawPlaybackSource(
origin=f"tube:{self.sitetag}",
page_url=scene_url,
duration_sec=duration_sec,
thumbnail_url=thumb,
)
],
)
yielded += 1
if limit is not None and yielded >= limit:
return