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