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-twitch)4# Desc: Profils Twitch ULTRA DÉTAILLÉS via le GQL PUBLIC du web (Client-ID5# public kimne78kx3ncx6brgo4mv6wki5h1ko, celui du site twitch.tv) :6# abonnés, partenaire/affilié, live en cours (titre + MINIATURE),7# dernière diffusion, jeux récents, vidéos récentes avec MINIATURES et8# vues + LIENS SOCIAUX auto-déclarés du panneau « À propos ».9# Pas de clé requise.10# ==============================================================================11from __future__ import annotations1213import asyncio14import json1516from apify import Actor1718from .net import Fetcher, UA1920GQL_URL = "https://gql.twitch.tv/gql"21CLIENT_ID = "kimne78kx3ncx6brgo4mv6wki5h1ko"2223QUERY = """24query($login: String!) {25 user(login: $login) {26 id27 login28 displayName29 description30 createdAt31 profileImageURL(width: 300)32 bannerImageURL33 followers { totalCount }34 roles { isPartner isAffiliate }35 primaryTeam { displayName }36 channel { socialMedias { name title url } }37 lastBroadcast { startedAt title game { displayName } }38 stream { viewersCount createdAt title type39 previewImageURL(width: 640, height: 360)40 game { displayName } }41 videos(first: 10, sort: TIME) {42 totalCount43 edges { node {44 id title viewCount lengthSeconds publishedAt45 previewThumbnailURL(width: 320, height: 180)46 game { displayName }47 } }48 }49 }50}51"""525354async def main() -> None:55 async with Actor:56 inp = await Actor.get_input() or {}57 usernames = [u.strip().lstrip("@").lower()58 for u in (inp.get("usernames") or []) if u and u.strip()]59 proxy = await Actor.create_proxy_configuration(60 actor_proxy_input=inp.get("proxyConfiguration"))61 fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1))62 sem = asyncio.Semaphore(int(inp.get("concurrency") or 4))6364 async def gql(login: str) -> dict | None:65 body = json.dumps({"query": QUERY,66 "variables": {"login": login}})67 # fetcher.post = retries/backoff/rotation d'IP du net.py commun68 resp = await fetcher.post(69 GQL_URL, data=body, session_id=f"tw{login}",70 headers={"Client-ID": CLIENT_ID, "User-Agent": UA,71 "Content-Type": "application/json"})72 if resp.status_code != 200:73 raise RuntimeError(f"gql http_{resp.status_code}")74 return (resp.json().get("data") or {}).get("user")7576 async def one(u: str) -> None:77 async with sem:78 miss = {"kind": "profile", "platform": "twitch",79 "username": u, "found": False}80 try:81 user = await gql(u)82 except Exception as exc:83 await Actor.push_data({**miss, "error": str(exc)[:200]})84 return85 if not user:86 await Actor.push_data({**miss, "error": "not_found"})87 return88 vids = []89 for e in ((user.get("videos") or {}).get("edges") or []):90 n = e.get("node") or {}91 vids.append({92 "id": n.get("id"),93 "url": f"https://www.twitch.tv/videos/{n.get('id')}",94 "title": (n.get("title") or "")[:200],95 "views": n.get("viewCount"),96 "duration_s": n.get("lengthSeconds"),97 "published_at": n.get("publishedAt"),98 "thumbnail": n.get("previewThumbnailURL"),99 "game": (n.get("game") or {}).get("displayName"),100 })101 views = [v["views"] for v in vids102 if isinstance(v["views"], int)]103 games = [v["game"] for v in vids if v["game"]]104 stream = user.get("stream") or {}105 last = user.get("lastBroadcast") or {}106 roles = user.get("roles") or {}107 await Actor.push_data({108 "kind": "profile",109 "platform": "twitch",110 "found": True,111 "id": user.get("id"),112 "username": user.get("login"),113 "full_name": user.get("displayName"),114 "biography": user.get("description"),115 "followers": ((user.get("followers") or {})116 .get("totalCount")),117 "is_partner": roles.get("isPartner"),118 "is_affiliate": roles.get("isAffiliate"),119 "is_verified": bool(roles.get("isPartner")),120 "team": ((user.get("primaryTeam") or {})121 .get("displayName")),122 "created_at": user.get("createdAt"),123 "avatar": user.get("profileImageURL"),124 "banner": user.get("bannerImageURL"),125 "is_live_now": bool(stream),126 "live_type": stream.get("type"),127 "live_started_at": stream.get("createdAt"),128 "live_viewers": stream.get("viewersCount"),129 "live_title": (stream.get("title") or "")[:200] or None,130 "live_thumbnail": stream.get("previewImageURL"),131 "live_game": (stream.get("game") or {}).get("displayName"),132 "last_broadcast_at": last.get("startedAt"),133 "last_broadcast_title": (last.get("title") or "")[:200]134 or None,135 "last_broadcast_game": ((last.get("game") or {})136 .get("displayName")),137 "videos_count": ((user.get("videos") or {})138 .get("totalCount")),139 "avg_video_views": (round(sum(views) / len(views))140 if views else None),141 "recent_games": list(dict.fromkeys(games))[:5],142 "recent_videos": vids,143 # liens sociaux AUTO-DÉCLARÉS (panneau « À propos ») →144 # cross_link fort côté crea-ka (§12.1)145 "social_links": [146 {"name": s.get("name"), "title": s.get("title"),147 "url": s.get("url")}148 for s in (((user.get("channel") or {})149 .get("socialMedias")) or [])150 if s.get("url")][:10],151 })152153 await asyncio.gather(*[one(u) for u in usernames])154 Actor.log.info(f"terminé : {len(usernames)} profils")155