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
20 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%
5.3 KB · 109 lines python
Raw Blame History
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File:   src/main.py (ka-kick)4# Desc:   Chaînes Kick détaillées via l'API publique /api/v2/channels/{slug}5#         (Cloudflare : proxy résidentiel + empreinte Chrome) : abonnés, badge,6#         live en cours, bio + LIENS SOCIAUX AUTO-DÉCLARÉS (instagram/twitter/7#         youtube/tiktok/facebook/discord → cross_link 0.90 côté crea-ka).8# ==============================================================================9from __future__ import annotations1011import asyncio12import json1314from apify import Actor1516from .net import Fetcher1718API_URL = "https://kick.com/api/v2/channels/{u}"19SOCIAL_KEYS = ("instagram", "twitter", "youtube", "tiktok", "facebook",20               "discord")212223async def main() -> None:24    async with Actor:25        inp = await Actor.get_input() or {}26        usernames = [u.strip().lstrip("@").lower()27                     for u in (inp.get("usernames") or []) if u and u.strip()]28        proxy = await Actor.create_proxy_configuration(29            actor_proxy_input=inp.get("proxyConfiguration"))30        fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1))31        sem = asyncio.Semaphore(int(inp.get("concurrency") or 3))3233        async def one(u: str) -> None:34            async with sem:35                miss = {"kind": "profile", "platform": "kick",36                        "username": u, "found": False}37                resp = None38                for attempt in range(3):  # mur Cloudflare → nouvelle IP39                    try:40                        resp = await fetcher.get(41                            API_URL.format(u=u), session_id=f"{u}k{attempt}",42                            headers={"Accept": "application/json"})43                    except Exception as exc:44                        await Actor.push_data({**miss,45                                               "error": str(exc)[:200]})46                        return47                    if resp.status_code == 404:48                        await Actor.push_data({**miss, "error": "http_404"})49                        return50                    if resp.status_code == 200 and \51                            (resp.text or "").startswith("{"):52                        break53                if resp.status_code != 200 or \54                        not (resp.text or "").startswith("{"):55                    await Actor.push_data(56                        {**miss, "error": f"http_{resp.status_code}"})57                    return58                try:59                    ch = json.loads(resp.text)60                except Exception:61                    await Actor.push_data({**miss, "error": "bad_json"})62                    return63                user = ch.get("user") or {}64                live = ch.get("livestream") or {}65                cats = [((c.get("category") or {}).get("name"))66                        for c in (ch.get("recent_categories") or [])67                        if isinstance(c, dict)]68                socials = {k: (user.get(k) or "").strip()69                           for k in SOCIAL_KEYS if (user.get(k) or "").strip()}70                await Actor.push_data({71                    "kind": "profile",72                    "platform": "kick",73                    "found": True,74                    "id": ch.get("id"),75                    "username": ch.get("slug"),76                    "full_name": user.get("username"),77                    "biography": user.get("bio"),78                    "followers": ch.get("followers_count")79                                 or ch.get("followersCount"),80                    "is_verified": bool(ch.get("verified")),81                    "is_banned": bool(ch.get("is_banned")),82                    "subscription_enabled": ch.get("subscription_enabled"),83                    "is_live_now": bool(live),84                    "live_viewers": live.get("viewer_count"),85                    "live_started_at": (live.get("start_time")86                                        or live.get("created_at")),87                    "live_is_mature": (bool(live.get("is_mature"))88                                       if live else None),89                    "live_language": live.get("language"),90                    "live_title": (live.get("session_title") or "")[:200]91                                  or None,92                    "live_thumbnail": ((live.get("thumbnail") or {}).get("url")93                                       if isinstance(live.get("thumbnail"),94                                                     dict) else None),95                    "live_category": next(96                        (c.get("name") for c in (live.get("categories") or [])97                         if isinstance(c, dict) and c.get("name")), None),98                    "vod_enabled": ch.get("vod_enabled"),99                    "avatar": user.get("profile_pic"),100                    "banner": ((ch.get("banner_image") or {}).get("url")101                               if isinstance(ch.get("banner_image"), dict)102                               else None),103                    "recent_categories": [c for c in cats if c][:5],104                    "social_links": socials,105                })106107        await asyncio.gather(*[one(u) for u in usernames])108        Actor.log.info(f"terminé : {len(usernames)} chaînes")109