Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)
Python 73.6%
HTML 13.2%
TypeScript 6%
JavaScript 4.5%
CSS 1.7%
Dockerfile 0.6%
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File: src/main.py (ka-patreon)4# Desc: Pages Patreon publiques — la page /<vanity> embarque l'objet campagne5# (patron_count, creation_count, is_monthly, pay_per_name, résumé) +6# og:title/og:image. Aucune clé. Proxy résidentiel recommandé.7# ==============================================================================8from __future__ import annotations910import asyncio11import html as htmllib12import json13import re1415from apify import Actor1617from .net import Fetcher1819PAGE_URL = "https://www.patreon.com/{u}"20_NUM = {21 "patrons": re.compile(r'"patron_count"\s*:\s*(\d+)'),22 "posts": re.compile(r'"creation_count"\s*:\s*(\d+)'),23 "paid_posts": re.compile(r'"paid_member_count"\s*:\s*(\d+)'),24}25_OG_TITLE = re.compile(r'<meta property="og:title" content="([^"]*)"')26_OG_IMG = re.compile(r'<meta property="og:image" content="([^"]*)"')27_OG_DESC = re.compile(r'<meta property="og:description" content="([^"]*)"')28_MONTHLY = re.compile(r'"is_monthly"\s*:\s*(true|false)')29_NSFW = re.compile(r'"is_nsfw"\s*:\s*(true|false)')30_COVER = re.compile(r'"cover_photo_url"\s*:\s*"((?:[^"\\]|\\.)*)"')31_CREATION = re.compile(r'"creation_name"\s*:\s*"((?:[^"\\]|\\.)*)"')32# liens sociaux auto-déclarés de la campagne (objets campaign de l'app Patreon)33_SOCIALS = {34 "facebook": re.compile(r'"facebook"\s*:\s*"(https?:(?:[^"\\]|\\.)+)"'),35 "twitter": re.compile(r'"twitter"\s*:\s*"(https?:(?:[^"\\]|\\.)+)"'),36 "youtube": re.compile(r'"youtube"\s*:\s*"(https?:(?:[^"\\]|\\.)+)"'),37 "instagram": re.compile(r'"instagram"\s*:\s*"(https?:(?:[^"\\]|\\.)+)"'),38 "tiktok": re.compile(r'"tiktok"\s*:\s*"(https?:(?:[^"\\]|\\.)+)"'),39 "twitch": re.compile(r'"twitch"\s*:\s*"(https?:(?:[^"\\]|\\.)+)"'),40 "discord": re.compile(r'"discord"\s*:\s*"(https?:(?:[^"\\]|\\.)+)"'),41}42_PUBLISHED_RE = re.compile(r'"published_at"\s*:\s*"([^"]+)"')43_PPN_RE = re.compile(r'"pay_per_name"\s*:\s*"([^"]+)"')444546def _dec(raw: str) -> str:47 try:48 return htmllib.unescape(json.loads(f'"{raw}"'))49 except Exception:50 return raw515253async def main() -> None:54 async with Actor:55 inp = await Actor.get_input() or {}56 usernames = [u.strip().lstrip("@").lower()57 for u in (inp.get("usernames") or []) if u and u.strip()]58 proxy = await Actor.create_proxy_configuration(59 actor_proxy_input=inp.get("proxyConfiguration"))60 fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1))61 sem = asyncio.Semaphore(int(inp.get("concurrency") or 4))6263 async def one(u: str) -> None:64 async with sem:65 miss = {"kind": "profile", "platform": "patreon",66 "username": u, "found": False}67 try:68 resp = await fetcher.get(PAGE_URL.format(u=u),69 session_id=u)70 except Exception as exc:71 await Actor.push_data({**miss, "error": str(exc)[:200]})72 return73 text = resp.text or ""74 if resp.status_code == 404:75 await Actor.push_data({**miss, "error": "http_404"})76 return77 m = _NUM["patrons"].search(text)78 if resp.status_code != 200 or not m:79 await Actor.push_data(80 {**miss, "error": f"http_{resp.status_code}"})81 return8283 def num(key):84 mm = _NUM[key].search(text)85 return int(mm.group(1)) if mm else None86 title = _OG_TITLE.search(text)87 img = _OG_IMG.search(text)88 desc = _OG_DESC.search(text)89 monthly = _MONTHLY.search(text)90 nsfw = _NSFW.search(text)91 cover = _COVER.search(text)92 creation = _CREATION.search(text)93 socials = {k: _dec(m.group(1))94 for k, rx in _SOCIALS.items()95 if (m := rx.search(text))}96 name = htmllib.unescape(title.group(1)) if title else u97 name = re.sub(r"\s*\|\s*Patreon\s*$", "", name).strip()98 await Actor.push_data({99 "kind": "profile",100 "platform": "patreon",101 "found": True,102 "username": u,103 "full_name": name,104 "biography": (htmllib.unescape(desc.group(1))105 if desc else None),106 "followers": num("patrons"), # patrons = « abonnés » KA107 "patrons": num("patrons"),108 "posts_count": num("posts"),109 "is_monthly": (monthly.group(1) == "true"110 if monthly else None),111 "is_nsfw": nsfw.group(1) == "true" if nsfw else None,112 "published_at": (pub.group(1) if113 (pub := _PUBLISHED_RE.search(text))114 else None),115 "pay_per_name": (ppn.group(1)[:40] if116 (ppn := _PPN_RE.search(text))117 else None),118 "creation_name": (_dec(creation.group(1))[:200]119 if creation else None),120 "avatar": htmllib.unescape(img.group(1)) if img else None,121 "banner": _dec(cover.group(1)) if cover else None,122 "social_links": socials or None,123 })124125 await asyncio.gather(*[one(u) for u in usernames])126 Actor.log.info(f"terminé : {len(usernames)} pages")127