"""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: id produktu Adult Empire w miniaturce + URL + tytuł + rok. _CARD_RE = re.compile( r'
\s*' r']*>\s*.*?' r'

(?P[^<]+)</h2>\s*<span class="year">(?P<year>\d{4})</span>', re.DOTALL, ) # 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] return RawMovie( external_id=slug, title=title, description=desc, release_year=year, url=url, poster_url=card.group("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": card.group("ae")}, )