# ============================================================================== # Author: Simon-Pierre Boucher # File: src/main.py (ka-patreon) # Desc: Pages Patreon publiques — la page / embarque l'objet campagne # (patron_count, creation_count, is_monthly, pay_per_name, résumé) + # og:title/og:image. Aucune clé. Proxy résidentiel recommandé. # ============================================================================== from __future__ import annotations import asyncio import html as htmllib import json import re from apify import Actor from .net import Fetcher PAGE_URL = "https://www.patreon.com/{u}" _NUM = { "patrons": re.compile(r'"patron_count"\s*:\s*(\d+)'), "posts": re.compile(r'"creation_count"\s*:\s*(\d+)'), "paid_posts": re.compile(r'"paid_member_count"\s*:\s*(\d+)'), } _OG_TITLE = re.compile(r' str: try: return htmllib.unescape(json.loads(f'"{raw}"')) except Exception: return raw async def main() -> None: async with Actor: inp = await Actor.get_input() or {} usernames = [u.strip().lstrip("@").lower() for u in (inp.get("usernames") or []) if u and u.strip()] proxy = await Actor.create_proxy_configuration( actor_proxy_input=inp.get("proxyConfiguration")) fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1)) sem = asyncio.Semaphore(int(inp.get("concurrency") or 4)) async def one(u: str) -> None: async with sem: miss = {"kind": "profile", "platform": "patreon", "username": u, "found": False} try: resp = await fetcher.get(PAGE_URL.format(u=u), session_id=u) except Exception as exc: await Actor.push_data({**miss, "error": str(exc)[:200]}) return text = resp.text or "" if resp.status_code == 404: await Actor.push_data({**miss, "error": "http_404"}) return m = _NUM["patrons"].search(text) if resp.status_code != 200 or not m: await Actor.push_data( {**miss, "error": f"http_{resp.status_code}"}) return def num(key): mm = _NUM[key].search(text) return int(mm.group(1)) if mm else None title = _OG_TITLE.search(text) img = _OG_IMG.search(text) desc = _OG_DESC.search(text) monthly = _MONTHLY.search(text) nsfw = _NSFW.search(text) cover = _COVER.search(text) creation = _CREATION.search(text) socials = {k: _dec(m.group(1)) for k, rx in _SOCIALS.items() if (m := rx.search(text))} name = htmllib.unescape(title.group(1)) if title else u name = re.sub(r"\s*\|\s*Patreon\s*$", "", name).strip() await Actor.push_data({ "kind": "profile", "platform": "patreon", "found": True, "username": u, "full_name": name, "biography": (htmllib.unescape(desc.group(1)) if desc else None), "followers": num("patrons"), # patrons = « abonnés » KA "patrons": num("patrons"), "posts_count": num("posts"), "is_monthly": (monthly.group(1) == "true" if monthly else None), "is_nsfw": nsfw.group(1) == "true" if nsfw else None, "published_at": (pub.group(1) if (pub := _PUBLISHED_RE.search(text)) else None), "pay_per_name": (ppn.group(1)[:40] if (ppn := _PPN_RE.search(text)) else None), "creation_name": (_dec(creation.group(1))[:200] if creation else None), "avatar": htmllib.unescape(img.group(1)) if img else None, "banner": _dec(cover.group(1)) if cover else None, "social_links": socials or None, }) await asyncio.gather(*[one(u) for u in usernames]) Actor.log.info(f"terminé : {len(usernames)} pages")