From c586a8ad326a2979ee46e5ccab525fd1c5ce6abc Mon Sep 17 00:00:00 2001 From: goon-foss Date: Mon, 3 Aug 2026 10:38:18 +0200 Subject: [PATCH] feat(movies): konektor yourdailypornmovies.ws Sprawdzony NA DUPLIKAT przed budowa, bo poprzedni kandydat (speedporn.net) okazal sie kolejna nakladka na pandanetwork.club - 49/49 miniaturek z tego samego hosta co nasze pandamovies, 0 nowych filmow. Tutaj z 40 pozycji ze strony glownej 13 mielismy po tytule, 9 po id produktu Adult Empire, a 18 bylo nowych. Inna pula tresci: okladki DVD z imgs1cdn.adultempire.com, nie re-hosty. Pilot na 40: seen 40, new 22, updated 18, errors 0. W bazie 36 filmow, z tego 34 z obsada, 36 ze studiem, 14 dopielo sie do pozycji, ktore juz mielismy. WordPress z wlasnym motywem, wiec nie da sie podpiac pod DooplayConnector. Pulapka, ktora trzeba bylo obejsc: /tag/ w calym HTML to 2177 linkow, czyli chmura tagow w sidebarze (ten sam wzorzec co przy sexu). Obsada, tagi i studio czytane WYLACZNIE z blokow div.xmll - efekt to 5-14 tagow na film zamiast 2177. Druga rzecz: /director/ trzyma w praktyce STUDIO (New Sensations, VIXEN, Zero Tolerance Films), nie rezysera. Bierzemy to jako studio, bo tak jest uzyte. Playback: voe i mixdrop maja gotowe moduly hosterow i zweryfikowalem, ze resolwuja (voe m3u8, mixdrop mp4). vidara.to nieznana, leci jako zwykly embed do WebView. Katalog min. 8000 pozycji, siega 2003. Brak dlugosci filmu. Co-Authored-By: Claude Opus 5 --- app/connectors/__init__.py | 9 ++ app/connectors/yourdailypornmovies.py | 225 ++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 app/connectors/yourdailypornmovies.py diff --git a/app/connectors/__init__.py b/app/connectors/__init__.py index a4494a3..de14ea1 100644 --- a/app/connectors/__init__.py +++ b/app/connectors/__init__.py @@ -40,6 +40,7 @@ def get_movie_connectors() -> list[tuple[str, type]]: ) from app.connectors.freeomovie import FreeoMovieConnector from app.connectors.paradisehill import ParadisehillConnector + from app.connectors.yourdailypornmovies import YourDailyPornMoviesConnector # Kolejność ingestu: paradisehill FIRST (canonical primary, mirrory się do # niego przyklejają), potem mangoporn (jedyny mirror z realnym new-content — @@ -58,4 +59,12 @@ def get_movie_connectors() -> list[tuple[str, type]]: # freeomovie.to (bestia theme, nie dooplay) — mirror: świeże filmy DVD, # title-trigram attach do canonical + playback z TABS. Ocena 2026-07-02. ("freeomovie", FreeoMovieConnector), + # yourdailypornmovies.ws (własny motyw WP, nie dooplay) — ocena 2026-07-28. + # SPRAWDZONY NA DUPLIKAT: 18/40 filmów ze strony głównej było nowych, bo to inna + # pula treści (okładki DVD z adultempire, nie re-hosty z pandanetwork.club, na + # których siedzą mangoporn/pandamovies/speedporn). Katalog min. 8000, do 2003. + # Metadane: obsada, studio (kryje się pod `/director/`), tagi, rok, synopsis, + # id produktu Adult Empire. Bez długości. Playback: voe + mixdrop (oba mają + # gotowe moduły hosterów), vidara.to nieznana — leci jako zwykły embed. + ("yourdailypornmovies", YourDailyPornMoviesConnector), ] diff --git a/app/connectors/yourdailypornmovies.py b/app/connectors/yourdailypornmovies.py new file mode 100644 index 0000000..5e8b3c3 --- /dev/null +++ b/app/connectors/yourdailypornmovies.py @@ -0,0 +1,225 @@ +"""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")}, + )