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