goon/app/connectors/yourdailypornmovies.py
goon-foss c586a8ad32 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 <noreply@anthropic.com>
2026-08-03 10:38:18 +02:00

225 lines
8.5 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: id produktu Adult Empire w miniaturce + URL + tytuł + rok.
_CARD_RE = re.compile(
r'<div class="movie"><div class="imagen">\s*'
r'<img src="(?P<poster>https://imgs\d*cdn\.adultempire\.com/products/\d+/(?P<ae>\d+)h\.jpg)"'
r'[^>]*>\s*<a href="(?P<url>https://yourdailypornmovies\.ws/[^"]+)">.*?'
r'</div><h2>(?P<title>[^<]+)</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")},
)