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%
6.8 KB · 155 lines python
Raw Blame History
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