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/linkinbio.py4# Desc: Connecteur ENRICHISSEMENT générique « link-in-bio » (templatisé) —5# Linktree, Beacons, Bio.link, Milkshake, Campsite, Snipfeed.6# Mode d'accès : requêtes directes (pages publiques SSR), repli7# Firecrawl. Palier 1 du catalogue (§9) — le levier nº 1 : une page8# de liens révèle D'UN COUP tous les comptes d'un créateur.9# ==============================================================================10"""Enrichissement par pages de liens (link-in-bio).1112Pour chaque créateur dont `link_in_bio_url` est connue (déclarée par le13créateur dans une de ses bios — jamais devinée), on récupère la page et on14en extrait les comptes de plateformes. Signal d'identité : `link_in_bio`15(le créateur liste lui-même ses comptes) → confidence 0.95, le plus fort16(§12.1).1718Templatisé : chaque service a son extracteur ; le repli générique parcourt19les ancres de la page et classe chaque URL via normalize.platform_from_url.20"""21from __future__ import annotations2223import json24import re2526from ..identity import account, merge_accounts27from ..normalize import platform_from_url28from ..schema import Creator29from .base import BaseConnector3031# services couverts par le gabarit (§9 palier 1)32SERVICES = ("linktr.ee", "beacons.ai", "bio.link", "msha.ke",33 "campsite.bio", "snipfeed.co", "linkin.bio")3435_NEXT_DATA_RE = re.compile(36 r'<script id="__NEXT_DATA__"[^>]*>(.*?)</script>', re.S)37_HREF_RE = re.compile(r'href="([^"]+)"', re.I)383940def is_supported(url: str | None) -> bool:41 return bool(url) and any(s in url for s in SERVICES)424344def parse_links(html: str) -> list[str]:45 """Extrait toutes les URLs sortantes d'une page link-in-bio.4647 Linktree/Beacons sont des apps Next.js : les liens vivent dans le JSON48 __NEXT_DATA__ (plus fiable que le DOM). Repli : ancres du HTML.49 """50 urls: list[str] = []51 m = _NEXT_DATA_RE.search(html)52 if m:53 try:54 data = json.loads(m.group(1))55 urls.extend(_walk_urls(data))56 except Exception:57 pass58 for href in _HREF_RE.findall(html):59 if href.startswith("http"):60 urls.append(href)61 # dédupliquer en conservant l'ordre (l'ordre de la page = priorité du créateur)62 seen: set[str] = set()63 out = []64 for u in urls:65 u = u.split("?utm")[0].rstrip("/")66 if u not in seen:67 seen.add(u)68 out.append(u)69 return out707172def _walk_urls(node) -> list[str]:73 """Parcourt récursivement le JSON Next.js et récolte toute valeur 'url'."""74 found: list[str] = []75 if isinstance(node, dict):76 for k, v in node.items():77 if k in ("url", "href") and isinstance(v, str) and v.startswith("http"):78 found.append(v)79 else:80 found.extend(_walk_urls(v))81 elif isinstance(node, list):82 for item in node:83 found.extend(_walk_urls(item))84 return found858687def extract_accounts(html: str) -> list:88 """Page link-in-bio → comptes de plateformes (signal fort link_in_bio)."""89 accounts = []90 seen: set[str] = set()91 for url in parse_links(html):92 hit = platform_from_url(url)93 if not hit:94 continue95 platform, handle = hit96 key = f"{platform}:{handle}"97 if key in seen:98 continue99 seen.add(key)100 accounts.append(account(platform, handle, "link_in_bio", url=url))101 return accounts102103104class LinkInBioConnector(BaseConnector):105 source_id = "link-in-bio"106 kind = "enrichment"107 request_delay = 1.2108 max_pages = 500 # garde-fou par passage109110 def enrich(self, creators: list[Creator]) -> list[Creator]:111 enriched: list[Creator] = []112 fetched = 0113 for cr in creators:114 if fetched >= self.max_pages:115 break116 if not is_supported(cr.link_in_bio_url):117 continue118 try:119 html = self.get(cr.link_in_bio_url).text120 except Exception:121 try:122 html = self.get_rendered(cr.link_in_bio_url)123 except Exception:124 continue125 fetched += 1126 accounts = extract_accounts(html)127 if not accounts:128 continue129 cr.platforms = merge_accounts(cr.platforms, [a.finalize()130 for a in accounts])131 enriched.append(cr)132 return enriched133