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%
5.4 KB · 123 lines python
Raw Blame History
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File:   creaka/connectors/instagram.py4# Desc:   Connecteur ENRICHISSEMENT Instagram — profil public par créateur :5#         abonnés, badge vérifié, bio, liens de bio (→ découverte croisée des6#         autres plateformes + page link-in-bio). Mode d'accès : Scrapfly7#         (plateforme hostile au scraping, pas d'API publique de lecture) —8#         palier 4 du catalogue (§9), CGU/rate-limit respectés (§15).9# ==============================================================================10"""Enrichissement Instagram, créateur par créateur.1112Pour chaque fiche ayant un compte Instagram rattaché, on lit le profil PUBLIC13via l'endpoint web_profile_info (JSON servi par le web public d'Instagram),14à travers Scrapfly (anti-bot). On en tire :15- followers / verified / bio du compte Instagram (métriques, §13-14) ;16- `external_url` + `bio_links` : si c'est une page link-in-bio connue →17  `link_in_bio_url` (traitée ensuite par le connecteur link-in-bio) ; si c'est18  un autre réseau → nouveau compte rattaché avec signal `cross_link` (0.90,19  §12.1 : le créateur pointe LUI-MÊME vers cet autre compte).2021Uniquement des profils publics ; un profil privé/introuvable est ignoré.22"""23from __future__ import annotations2425from datetime import datetime, timedelta, timezone2627import json2829from ..identity import account, merge_accounts30from ..normalize import parse_count, platform_from_url31from ..schema import Creator, now_iso32from .base import BaseConnector33from .linkinbio import is_supported as is_linkinbio3435PROFILE_API = ("https://i.instagram.com/api/v1/users/web_profile_info/"36               "?username={h}")37IG_APP_ID = "936619743392459"  # app id du web public instagram.com38394041def _fresh(last_checked: str, hours: int = 72) -> bool:42    """Compte déjà rafraîchi récemment (acteur Apify) — épargner Scrapfly."""43    if not last_checked:44        return False45    try:46        seen = datetime.fromisoformat(last_checked.replace("Z", "+00:00"))47    except ValueError:48        return False49    return datetime.now(timezone.utc) - seen < timedelta(hours=hours)5051class InstagramConnector(BaseConnector):52    source_id = "instagram-profil"53    kind = "enrichment"54    request_delay = 1.555    max_profiles = 700       # garde-fou par passage5657    def fetch_profile(self, handle: str) -> dict | None:58        """Profil public JSON d'un handle Instagram (None si privé/introuvable)."""59        result = self.scrapfly(PROFILE_API.format(h=handle), render_js=False,60                               headers={"x-ig-app-id": IG_APP_ID,61                                        "accept": "application/json"})62        if result.get("status_code") != 200:63            return None64        try:65            data = json.loads(result.get("content") or "{}")66        except Exception:67            return None68        user = ((data.get("data") or {}).get("user")) or None69        if not user or user.get("is_private"):70            return None71        return user7273    def enrich(self, creators: list[Creator]) -> list[Creator]:74        enriched: list[Creator] = []75        fetched = 076        for cr in creators:77            ig = next((a for a in cr.platforms if a.platform == "instagram"), None)78            if ig is None or fetched >= self.max_profiles:79                continue80            if _fresh(ig.last_checked):81                continue82            try:83                user = self.fetch_profile(ig.handle)84            except Exception:85                continue86            fetched += 187            if not user:88                continue89            # métriques du compte lui-même (étendues : la référence, pas le minimum)90            ig.followers = parse_count(91                (user.get("edge_followed_by") or {}).get("count")) or ig.followers92            ig.verified = bool(user.get("is_verified"))93            ig.last_checked = now_iso()94            extra = {95                "following": (user.get("edge_follow") or {}).get("count"),96                "posts": (user.get("edge_owner_to_timeline_media") or {}).get("count"),97                "category": user.get("category_name"),98                "is_business": user.get("is_business_account"),99            }100            ig.metrics.update({k: v for k, v in extra.items() if v not in (None, "")})101            if not cr.avatar_url and user.get("profile_pic_url_hd"):102                cr.avatar_url = user["profile_pic_url_hd"]103            if not cr.bio and user.get("biography"):104                cr.bio = user["biography"]105            # liens de bio → découverte croisée (§12.1 signal 2)106            new_accounts = []107            links = [user.get("external_url") or ""]108            links += [(b.get("url") or "") for b in (user.get("bio_links") or [])]109            for url in links:110                if not url:111                    continue112                if is_linkinbio(url) and not cr.link_in_bio_url:113                    cr.link_in_bio_url = url114                    continue115                hit = platform_from_url(url)116                if hit and hit[0] != "instagram":117                    new_accounts.append(118                        account(hit[0], hit[1], "cross_link", url=url).finalize())119            if new_accounts:120                cr.platforms = merge_accounts(cr.platforms, new_accounts)121            enriched.append(cr)122        return enriched123