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>
147 lines
5.4 KiB
Python
147 lines
5.4 KiB
Python
"""porndish.com, latest browse scraper via WordPress REST API.
|
|
|
|
Historia: dawniej search scraper (`?s=`), zamarzł 2026-05-07 (search przestał dawać
|
|
nowe sceny, 1151h cisza, watchdog GOON-16). To WordPress (g1/bimber theme), VPS
|
|
dociera, więc czysty kanał to REST API: `/wp-json/wp/v2/posts?_embed=1` daje
|
|
ustrukturyzowany JSON jednym requestem na stronę. Przerobione na browse 2026-06-24
|
|
(ten sam wzorzec co perverzija).
|
|
|
|
Z REST `_embed`: tytuł, data, miniatura (featured_media), STUDIO (taksonomia
|
|
`category`, np. "Freeuse Fantasy", content studyjny) i tagi (`post_tag`, porndish
|
|
miesza w nich performerów z gatunkami, bierzemy jak jest; canonical-merge i tak
|
|
dorabia performerów z TPDB/StashDB, a tytuł ma nazwiska). Performerów osobno nie
|
|
wyciągamy (post_tag ich nie rozdziela od gatunków bez listy known-performers).
|
|
|
|
Playback: post page embeduje hoster iframe → extractor `porndishcom` → `_embed_iframe`
|
|
→ resolwowany phone-side.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
import json
|
|
import logging
|
|
from datetime import date, datetime
|
|
|
|
from app.connectors.base import (
|
|
RawFingerprint,
|
|
RawPlaybackSource,
|
|
RawScene,
|
|
RawStudio,
|
|
RawTag,
|
|
)
|
|
from app.connectors.direct_scrapers._browse_base import (
|
|
BaseBrowseScraper,
|
|
compute_thumbnail_phash,
|
|
)
|
|
from app.extractors import browser_get
|
|
from app.normalize.text import slugify
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_BASE = "https://www.porndish.com"
|
|
_PER_PAGE = 20
|
|
|
|
|
|
def _parse_date(value: str | None) -> date | None:
|
|
if not value:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(value.replace("Z", "+00:00")).date()
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
class PornDishScraper(BaseBrowseScraper):
|
|
sitetag = "porndishcom"
|
|
|
|
def _listing_url(self, page: int) -> str:
|
|
return f"{_BASE}/wp-json/wp/v2/posts?per_page={_PER_PAGE}&page={page}&_embed=1"
|
|
|
|
# crawl_page nadpisany (REST JSON, nie HTML) → abstrakcje nieużywane.
|
|
def _extract_scene_urls(self, listing_html: str) -> list[str]:
|
|
return []
|
|
|
|
def _parse_detail(self, scene_url: str, detail_html: str) -> RawScene | None:
|
|
return None
|
|
|
|
def crawl_page(self, page: int) -> list[RawScene] | None:
|
|
url = self._listing_url(page)
|
|
try:
|
|
res = browser_get(url, timeout=self._timeout)
|
|
except Exception as e:
|
|
log.warning("porndish REST fetch failed (page %d): %s", page, e)
|
|
return None
|
|
# WP zwraca 400 (rest_post_invalid_page_number) za ostatnią stroną → exhausted.
|
|
if res.status_code != 200:
|
|
return []
|
|
try:
|
|
posts = json.loads(res.text)
|
|
except (json.JSONDecodeError, ValueError):
|
|
log.warning("porndish REST: bad JSON page %d", page)
|
|
return None
|
|
if not isinstance(posts, list) or not posts:
|
|
return []
|
|
|
|
out: list[RawScene] = []
|
|
for p in posts:
|
|
link = (p.get("link") or "").strip()
|
|
title = html.unescape((p.get("title") or {}).get("rendered", "")).strip()
|
|
if not link or not title:
|
|
continue
|
|
release_date = _parse_date(p.get("date"))
|
|
|
|
emb = p.get("_embedded") or {}
|
|
fm = emb.get("wp:featuredmedia") or []
|
|
thumb = (fm[0].get("source_url") if fm and isinstance(fm[0], dict) else None) or None
|
|
|
|
studio: RawStudio | None = None
|
|
tags: list[RawTag] = []
|
|
seen_tag: set[str] = set()
|
|
for group in emb.get("wp:term") or []:
|
|
if not group:
|
|
continue
|
|
tax = group[0].get("taxonomy")
|
|
if tax == "category" and studio is None:
|
|
sname = (group[0].get("name") or "").strip()
|
|
if sname:
|
|
studio = RawStudio(
|
|
external_id=f"{self.sitetag}:studio:{slugify(sname)}",
|
|
name=sname, slug=slugify(sname),
|
|
)
|
|
elif tax == "post_tag":
|
|
for g in group:
|
|
name = (g.get("name") or "").strip()
|
|
sl = (g.get("slug") or slugify(name)).strip()
|
|
if not name or sl in seen_tag:
|
|
continue
|
|
seen_tag.add(sl)
|
|
tags.append(RawTag(external_id=f"{self.sitetag}:tag:{sl}", name=name, slug=sl))
|
|
|
|
fingerprints: list[RawFingerprint] = []
|
|
if thumb:
|
|
ph = compute_thumbnail_phash(thumb, referer=_BASE + "/")
|
|
if ph:
|
|
fingerprints.append(RawFingerprint(kind="phash", value=ph))
|
|
|
|
out.append(
|
|
RawScene(
|
|
external_id=f"{self.sitetag}:{link}",
|
|
title=title,
|
|
release_date=release_date,
|
|
url=link,
|
|
studio=studio,
|
|
performers=[],
|
|
tags=tags,
|
|
fingerprints=fingerprints,
|
|
playback_sources=[
|
|
RawPlaybackSource(
|
|
origin=f"tube:{self.sitetag}",
|
|
page_url=link,
|
|
thumbnail_url=thumb,
|
|
)
|
|
],
|
|
)
|
|
)
|
|
|
|
log.info("porndish REST page %d: %d scenes", page, len(out))
|
|
return out
|