# ============================================================================== # Author: Simon-Pierre Boucher # File: src/main.py (ka-twitch) # Desc: Profils Twitch ULTRA DÉTAILLÉS via le GQL PUBLIC du web (Client-ID # public kimne78kx3ncx6brgo4mv6wki5h1ko, celui du site twitch.tv) : # abonnés, partenaire/affilié, live en cours (titre + MINIATURE), # dernière diffusion, jeux récents, vidéos récentes avec MINIATURES et # vues + LIENS SOCIAUX auto-déclarés du panneau « À propos ». # Pas de clé requise. # ============================================================================== from __future__ import annotations import asyncio import json from apify import Actor from .net import Fetcher, UA GQL_URL = "https://gql.twitch.tv/gql" CLIENT_ID = "kimne78kx3ncx6brgo4mv6wki5h1ko" QUERY = """ query($login: String!) { user(login: $login) { id login displayName description createdAt profileImageURL(width: 300) bannerImageURL followers { totalCount } roles { isPartner isAffiliate } primaryTeam { displayName } channel { socialMedias { name title url } } lastBroadcast { startedAt title game { displayName } } stream { viewersCount createdAt title type previewImageURL(width: 640, height: 360) game { displayName } } videos(first: 10, sort: TIME) { totalCount edges { node { id title viewCount lengthSeconds publishedAt previewThumbnailURL(width: 320, height: 180) game { displayName } } } } } } """ async def main() -> None: async with Actor: inp = await Actor.get_input() or {} usernames = [u.strip().lstrip("@").lower() for u in (inp.get("usernames") or []) if u and u.strip()] proxy = await Actor.create_proxy_configuration( actor_proxy_input=inp.get("proxyConfiguration")) fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1)) sem = asyncio.Semaphore(int(inp.get("concurrency") or 4)) async def gql(login: str) -> dict | None: body = json.dumps({"query": QUERY, "variables": {"login": login}}) # fetcher.post = retries/backoff/rotation d'IP du net.py commun resp = await fetcher.post( GQL_URL, data=body, session_id=f"tw{login}", headers={"Client-ID": CLIENT_ID, "User-Agent": UA, "Content-Type": "application/json"}) if resp.status_code != 200: raise RuntimeError(f"gql http_{resp.status_code}") return (resp.json().get("data") or {}).get("user") async def one(u: str) -> None: async with sem: miss = {"kind": "profile", "platform": "twitch", "username": u, "found": False} try: user = await gql(u) except Exception as exc: await Actor.push_data({**miss, "error": str(exc)[:200]}) return if not user: await Actor.push_data({**miss, "error": "not_found"}) return vids = [] for e in ((user.get("videos") or {}).get("edges") or []): n = e.get("node") or {} vids.append({ "id": n.get("id"), "url": f"https://www.twitch.tv/videos/{n.get('id')}", "title": (n.get("title") or "")[:200], "views": n.get("viewCount"), "duration_s": n.get("lengthSeconds"), "published_at": n.get("publishedAt"), "thumbnail": n.get("previewThumbnailURL"), "game": (n.get("game") or {}).get("displayName"), }) views = [v["views"] for v in vids if isinstance(v["views"], int)] games = [v["game"] for v in vids if v["game"]] stream = user.get("stream") or {} last = user.get("lastBroadcast") or {} roles = user.get("roles") or {} await Actor.push_data({ "kind": "profile", "platform": "twitch", "found": True, "id": user.get("id"), "username": user.get("login"), "full_name": user.get("displayName"), "biography": user.get("description"), "followers": ((user.get("followers") or {}) .get("totalCount")), "is_partner": roles.get("isPartner"), "is_affiliate": roles.get("isAffiliate"), "is_verified": bool(roles.get("isPartner")), "team": ((user.get("primaryTeam") or {}) .get("displayName")), "created_at": user.get("createdAt"), "avatar": user.get("profileImageURL"), "banner": user.get("bannerImageURL"), "is_live_now": bool(stream), "live_type": stream.get("type"), "live_started_at": stream.get("createdAt"), "live_viewers": stream.get("viewersCount"), "live_title": (stream.get("title") or "")[:200] or None, "live_thumbnail": stream.get("previewImageURL"), "live_game": (stream.get("game") or {}).get("displayName"), "last_broadcast_at": last.get("startedAt"), "last_broadcast_title": (last.get("title") or "")[:200] or None, "last_broadcast_game": ((last.get("game") or {}) .get("displayName")), "videos_count": ((user.get("videos") or {}) .get("totalCount")), "avg_video_views": (round(sum(views) / len(views)) if views else None), "recent_games": list(dict.fromkeys(games))[:5], "recent_videos": vids, # liens sociaux AUTO-DÉCLARÉS (panneau « À propos ») → # cross_link fort côté crea-ka (§12.1) "social_links": [ {"name": s.get("name"), "title": s.get("title"), "url": s.get("url")} for s in (((user.get("channel") or {}) .get("socialMedias")) or []) if s.get("url")][:10], }) await asyncio.gather(*[one(u) for u in usernames]) Actor.log.info(f"terminé : {len(usernames)} profils")