Od ~15.09 nowe karty maja okladke imgs4cdn.adultempire.com/movie/<id>?w=... (czesc i.ibb.co albo bez okladki), a regex karty wymagal starego products/NN/<id>h.jpg. 33 z 40 filmow strony 1 odpadalo po cichu, ingest przez tydzien: 0 nowych. Okladka teraz opcjonalna i w dowolnym formacie, id Adult Empire z obu wariantow. Nadrobione: 26 nowych filmow, 6 dopietych do istniejacych. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
233 lines
9 KiB
Python
233 lines
9 KiB
Python
"""yourdailypornmovies.ws, konektor filmów. Dodany 2026-07-28.
|
|
|
|
WordPress z własnym motywem (NIE dooplay), więc nie da się podpiąć pod
|
|
`DooplayConnector` jak mangoporn/pandamovies/streamporn.
|
|
|
|
**Sprawdzony na duplikat przed dodaniem** (2026-07-28), bo poprzedni kandydat
|
|
(speedporn.net) okazał się kolejną nakładką na `pandanetwork.club`, czyli 0 nowych
|
|
filmów. Tutaj z 40 filmów ze strony głównej: 13 mieliśmy po tytule, 9 po id produktu
|
|
Adult Empire, a **18 (45%) było nowych**. Inna pula treści: miniaturki to oficjalne
|
|
okładki DVD z `imgs1cdn.adultempire.com`, nie re-hosty z pandanetwork.
|
|
|
|
Katalog: min. 8000 pozycji (strona 200 nadal oddaje wyniki, po 40 na stronę), sięga
|
|
2003 roku, strona 1 ma bieżący rok.
|
|
|
|
**Pułapka: `/tag/` w całym HTML to 2177 linków** to chmura tagów w sidebarze,
|
|
identyczny wzorzec jak przy sexu. Tagi (i obsada) MUSZĄ być czytane z bloków
|
|
`div.xmll`, inaczej każdy film dostanie te same dwa tysiące śmieci.
|
|
|
|
**`/director/` trzyma w praktyce STUDIO**, nie reżysera („New Sensations", „Brazzers").
|
|
Bierzemy to jako studio, bo tak jest użyte, a reżyserów strona i tak nie podaje.
|
|
|
|
Czego strona nie ma: długości filmu.
|
|
|
|
Playback: 3 hostery w iframe. `voe.sx` i `mxdrop.sx` zweryfikowane, że resolwują
|
|
istniejącymi modułami (`app/extractors/hosters/voe.py` → m3u8, `mixdrop.py` → mp4).
|
|
`vidara.to` jest nam nieznany, zapisujemy go jako zwykły embed, telefon otworzy go
|
|
w WebView, ale nie liczymy na niego.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import html as html_mod
|
|
import logging
|
|
import re
|
|
from collections.abc import Iterator
|
|
from datetime import datetime
|
|
from urllib.parse import urlparse
|
|
|
|
from app.connectors.base import (
|
|
BaseMovieConnector,
|
|
RawMovie,
|
|
RawPerformer,
|
|
RawPlaybackSource,
|
|
RawStudio,
|
|
RawTag,
|
|
)
|
|
from app.extractors import browser_get
|
|
from app.models.source import SourceKind
|
|
from app.normalize.text import slugify
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_BASE = "https://yourdailypornmovies.ws"
|
|
|
|
# Karta listingu: okładka + URL + tytuł + rok. Okładka jest opcjonalna i w dowolnym
|
|
# formacie: od ~2026-09-15 nowe filmy mają `imgs4cdn.adultempire.com/movie/<id>?w=...`
|
|
# zamiast `.../products/NN/<id>h.jpg`, część ma okładkę z i.ibb.co. Regex wymagający
|
|
# starego formatu po cichu gubił 33 z 40 filmów strony 1 (ingest: 0 nowych przez tydzień).
|
|
_CARD_RE = re.compile(
|
|
r'<div class="movie"><div class="imagen">\s*'
|
|
r'(?:<img src="(?P<poster>[^"]*)"[^>]*>\s*)?'
|
|
r'<a href="(?P<url>https://yourdailypornmovies\.ws/[^"]+)">.*?'
|
|
r'</div><h2>(?P<title>[^<]+)</h2>\s*<span class="year">(?P<year>\d{4})</span>',
|
|
re.DOTALL,
|
|
)
|
|
# Id produktu Adult Empire z adresu okładki, w obu formatach (to ta sama numeracja).
|
|
_AE_ID_RE = re.compile(r"adultempire\.com/(?:products/\d+/(\d+)h\.jpg|movie/(\d+))")
|
|
|
|
# Blok metadanych filmu. WSZYSTKO czytamy TYLKO stąd, patrz pułapka w docstringu.
|
|
_XMLL_RE = re.compile(r'<div class="xmll">.*?</div>', re.DOTALL)
|
|
_ACTOR_RE = re.compile(r'href="[^"]*/actor/[^"]+"[^>]*>([^<]+)</a>')
|
|
_TAG_RE = re.compile(r'href="[^"]*/tag/[^"]+"[^>]*>([^<]+)</a>')
|
|
_DIRECTOR_RE = re.compile(r'href="[^"]*/director/[^"]+"[^>]*>([^<]+)</a>')
|
|
_YEAR_RE = re.compile(r'href="[^"]*/fecha-estreno/(\d{4})/"')
|
|
_OG_DESC_RE = re.compile(r'<meta property="og:description" content="([^"]*)"')
|
|
_IFRAME_RE = re.compile(r'<iframe[^>]+src="((?:https?:)?//[^"]+)"', re.IGNORECASE)
|
|
|
|
# Hosty w iframe, które NIE są odtwarzaczem (reklamy).
|
|
_AD_HOSTS = ("realsrv", "magsrv", "exosrv", "juicyads", "google", "doubleclick")
|
|
|
|
|
|
def _host_label(url: str) -> str | None:
|
|
host = (urlparse(url).hostname or "").lower().replace("www.", "")
|
|
if not host:
|
|
return None
|
|
parts = host.split(".")
|
|
return parts[-2] if len(parts) >= 2 else parts[0]
|
|
|
|
|
|
class YourDailyPornMoviesConnector(BaseMovieConnector):
|
|
kind = SourceKind.scraper
|
|
name = "yourdailypornmovies"
|
|
base_url = _BASE
|
|
|
|
_MAX_PAGES_DELTA = 3
|
|
_MAX_PAGES_FULL = 40
|
|
|
|
def __init__(self, *, timeout: float = 30.0) -> None:
|
|
self._timeout = timeout
|
|
|
|
def close(self) -> None:
|
|
pass
|
|
|
|
def _fetch(self, url: str) -> str:
|
|
if not url.startswith("http"):
|
|
url = _BASE + url
|
|
r = browser_get(url, timeout=self._timeout, follow_redirects=True)
|
|
if r.status_code >= 400:
|
|
raise RuntimeError(f"{r.status_code} for {url}")
|
|
return r.text
|
|
|
|
def fetch_movies(
|
|
self, *, since: datetime | None = None, limit: int | None = None
|
|
) -> Iterator[RawMovie]:
|
|
seen = 0
|
|
seen_urls: set[str] = set()
|
|
max_pages = self._MAX_PAGES_DELTA if since is not None else self._MAX_PAGES_FULL
|
|
for page in range(1, max_pages + 1):
|
|
path = "/" if page == 1 else f"/page/{page}/"
|
|
try:
|
|
listing = self._fetch(path)
|
|
except Exception as e:
|
|
log.warning("%s listing page=%d failed: %s", self.name, page, e)
|
|
return
|
|
cards = list(_CARD_RE.finditer(listing))
|
|
if not cards:
|
|
log.info("%s: pusta strona=%d, stop", self.name, page)
|
|
return
|
|
for m in cards:
|
|
url = m.group("url")
|
|
if url in seen_urls:
|
|
continue
|
|
seen_urls.add(url)
|
|
try:
|
|
movie = self._parse_detail(m, url)
|
|
except Exception as e:
|
|
log.warning("%s detail failed %s: %s", self.name, url, e)
|
|
continue
|
|
if movie is None:
|
|
continue
|
|
yield movie
|
|
seen += 1
|
|
if limit is not None and seen >= limit:
|
|
return
|
|
|
|
def _parse_detail(self, card: re.Match[str], url: str) -> RawMovie | None:
|
|
detail = self._fetch(url)
|
|
title = html_mod.unescape(card.group("title")).strip()
|
|
if not title:
|
|
return None
|
|
|
|
# Playback najpierw, bez niego film jest bezużyteczny, nie tworzymy orphana.
|
|
playback: list[RawPlaybackSource] = []
|
|
seen_host: set[str] = set()
|
|
for m in _IFRAME_RE.finditer(detail):
|
|
embed = m.group(1)
|
|
if embed.startswith("//"):
|
|
embed = "https:" + embed
|
|
host = _host_label(embed)
|
|
if not host or host in seen_host or any(a in embed for a in _AD_HOSTS):
|
|
continue
|
|
seen_host.add(host)
|
|
playback.append(
|
|
RawPlaybackSource(
|
|
origin=f"{self.name}:{host}",
|
|
page_url=embed,
|
|
embed_url=embed,
|
|
)
|
|
)
|
|
if not playback:
|
|
return None
|
|
|
|
# Obsada, tagi i studio WYŁĄCZNIE z bloków `div.xmll` (chmura tagów z sidebara
|
|
# ma 2177 linków i zaśmieciłaby każdy film tym samym zestawem).
|
|
blocks = " ".join(_XMLL_RE.findall(detail))
|
|
|
|
performers: list[RawPerformer] = []
|
|
seen_perf: set[str] = set()
|
|
for name in _ACTOR_RE.findall(blocks):
|
|
name = html_mod.unescape(name).strip()
|
|
slug = slugify(name)
|
|
if slug and slug not in seen_perf:
|
|
seen_perf.add(slug)
|
|
performers.append(
|
|
RawPerformer(external_id=f"{self.name}:performer:{slug}", name=name)
|
|
)
|
|
|
|
tags: list[RawTag] = []
|
|
seen_tag: set[str] = set()
|
|
for name in _TAG_RE.findall(blocks):
|
|
name = html_mod.unescape(name).strip()
|
|
slug = slugify(name)
|
|
if slug and slug not in seen_tag:
|
|
seen_tag.add(slug)
|
|
tags.append(RawTag(external_id=f"{self.name}:tag:{slug}", name=name, slug=slug))
|
|
|
|
studio = None
|
|
dm = _DIRECTOR_RE.search(blocks)
|
|
if dm:
|
|
sname = html_mod.unescape(dm.group(1)).strip()
|
|
if sname and slugify(sname):
|
|
studio = RawStudio(
|
|
external_id=f"{self.name}:studio:{slugify(sname)}",
|
|
name=sname,
|
|
slug=slugify(sname),
|
|
)
|
|
|
|
ym = _YEAR_RE.search(detail)
|
|
year = int(ym.group(1)) if ym else int(card.group("year"))
|
|
|
|
desc = None
|
|
om = _OG_DESC_RE.search(detail)
|
|
if om:
|
|
desc = html_mod.unescape(om.group(1)).strip() or None
|
|
|
|
slug = urlparse(url).path.strip("/").split("/")[-1]
|
|
poster = html_mod.unescape(card.group("poster") or "").strip() or None
|
|
aem = _AE_ID_RE.search(poster or "")
|
|
ae_id = (aem.group(1) or aem.group(2)) if aem else None
|
|
return RawMovie(
|
|
external_id=slug,
|
|
title=title,
|
|
description=desc,
|
|
release_year=year,
|
|
url=url,
|
|
poster_url=poster,
|
|
studio=studio,
|
|
performers=performers,
|
|
tags=tags,
|
|
playback_sources=playback,
|
|
# id produktu Adult Empire, mocniejsza kotwica do dedupu niż tytuł
|
|
# (przy ocenie źródła złapało 9 filmów, których tytuł się nie zgadzał).
|
|
raw={"source": self.name, "url": url, "adultempire_id": ae_id},
|
|
)
|