SPB Git forge

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

52commits 1branches 0releases
11.3 MBsize
maindefault branch
19 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%
5.8 KB · 127 lines python
Raw Blame History
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