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-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