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/youtube_recherche.py4# Desc: Connecteur DÉCOUVERTE — chaînes YouTube québécoises via la recherche5# publique (filtre « chaînes »), requêtes thématiques × marqueurs QC.6# Mode d'accès : requêtes directes (HTML public, ytInitialData).7# Palier 3 (§9). Filtre conservateur : marqueur québécois OBLIGATOIRE8# dans le titre ou la description de la chaîne.9# ==============================================================================10"""Découverte massive de chaînes YouTube québécoises.1112La page de résultats (filtre chaînes, sp=EgIQAg==) embarque un JSON13ytInitialData avec, pour chaque chaîne : handle, nom, nb d'abonnés et extrait14de description. On ne crée une fiche QUE si un marqueur québécois figure dans15le titre/la description (jamais d'inférence, §7/§15). Le compte YouTube est le16compte SOURCE de la fiche → signal profil_source (0.98) ; l'enrichissement17(liens de bio, link-in-bio) rattache ensuite ses autres plateformes.18"""19from __future__ import annotations2021import json22import re2324from ..identity import account25from ..normalize import parse_count, slugify, strip_accents26from ..schema import Creator27from .base import BaseConnector2829SEARCH_URL = "https://www.youtube.com/results"30INNERTUBE_URL = "https://www.youtube.com/youtubei/v1/search"31CHANNEL_FILTER = "EgIQAg%3D%3D" # sp= filtre « chaînes »32_YTID_RE = re.compile(r"var ytInitialData = (\{.*?\});</script>", re.S)33_KEY_RE = re.compile(r'"INNERTUBE_API_KEY":"([^"]+)"')34_VER_RE = re.compile(r'"INNERTUBE_CONTEXT_CLIENT_VERSION":"([^"]+)"')3536# marqueurs québécois (comparés SANS accents, en minuscules)37# ⚠ frontières de mot sur les toponymes courts/piégeux : « levis » matchait38# dans « teLEVISion », « estrie » dans « tapESTRIEs » (corrigé vague 3) ;39# « queb » nu matchait « QUEBra » (portugais, tatiQUEBrabarraco) et40# « jacQUEBrazil » (corrigé vague 4) — « quebec » complet n'importe où, ou41# « queb » en fin de mot (onlyqueb, grainsduquebec restent couverts)42_QC_MARKERS = re.compile(43 r"quebec|queb(?![a-z])|montreal|\bmtl\b|saguenay|gaspesie|mauricie|"44 r"\bestrie\b|outaouais|"45 r"abitibi|\blevis\b|sherbrooke|trois-rivieres|gatineau|\blaval\b|longueuil|"46 r"rimouski|chicoutimi|drummondville|\bgranby\b|\bbeauce\b|charlevoix|"47 r"lanaudiere|laurentides|monteregie|temiscamingue|\bjoliette\b|"48 r"victoriaville|\bfleuve\b|rive-sud|rive-nord|ville de quebec")4950# requêtes thématiques → niche par défaut de la fiche51QUERIES: list[tuple[str, str]] = [52 ("vlog québécois", "lifestyle"), ("youtubeur québécois", "lifestyle"),53 ("humour québécois", "humour"), ("humoriste québécois", "humour"),54 ("cuisine québécoise", "cuisine"), ("recettes québec", "cuisine"),55 ("gaming québécois", "gaming"), ("gamer québec", "gaming"),56 ("chasse québec", "plein-air"), ("pêche québec", "plein-air"),57 ("plein air québec", "plein-air"), ("camping québec", "plein-air"),58 ("musique québécoise", "musique"), ("rap québécois", "musique"),59 ("maquillage québec", "beaute"), ("beauté québécoise", "beaute"),60 ("mode québécoise", "mode"), ("fitness québec", "sport-fitness"),61 ("entraînement québécois", "sport-fitness"), ("hockey québec", "sport-fitness"),62 ("famille québécoise vlog", "famille-parentalite"),63 ("maman québécoise", "famille-parentalite"),64 ("voyage québécois", "voyage"), ("finance québec", "finance-affaires"),65 ("immobilier québec", "finance-affaires"), ("tech québec", "tech"),66 ("balado québécois", "actualite-opinion"), ("podcast québécois", "actualite-opinion"),67 ("éducation québec", "education"), ("science québec", "education"),68 ("art québécois", "arts"), ("danse québec", "danse"),69 ("resto montréal", "bouffe-resto"), ("bouffe québec", "bouffe-resto"),70 ("vlog montréal", "lifestyle"), ("vlog ville de québec", "lifestyle"),71 ("agriculture québec", "autre"), ("auto québec", "autre"),72 ("santé bien-être québec", "sante-mieux-etre"),73 ("actualité québécoise", "actualite-opinion"),74 # villes × vlog/scène locale75 ("vlog sherbrooke", "lifestyle"), ("vlog gatineau", "lifestyle"),76 ("vlog saguenay", "lifestyle"), ("vlog trois-rivières", "lifestyle"),77 ("vlog gaspésie", "voyage"), ("vlog rimouski", "lifestyle"),78 ("vlog abitibi", "lifestyle"), ("vlog lévis", "lifestyle"),79 ("vlog laval", "lifestyle"), ("vlog longueuil", "lifestyle"),80 ("vlog drummondville", "lifestyle"), ("vlog granby", "lifestyle"),81 # gaming & streaming82 ("minecraft québécois", "gaming"), ("fortnite québécois", "gaming"),83 ("streamer québécois", "gaming"), ("twitch québécois", "gaming"),84 ("jeux vidéo québec", "gaming"), ("rétro gaming québec", "gaming"),85 # niches ciblées86 ("asmr québécois", "lifestyle"), ("vanlife québec", "voyage"),87 ("ski québec", "plein-air"), ("vélo québec", "sport-fitness"),88 ("course à pied québec", "sport-fitness"), ("musculation québec", "sport-fitness"),89 ("nutrition québec", "sante-mieux-etre"), ("yoga québec", "sante-mieux-etre"),90 ("rénovation québec", "autre"), ("menuiserie québec", "autre"),91 ("jardinage québec", "plein-air"), ("moto québec", "autre"),92 ("auto détail québec", "autre"), ("mécanique québec", "autre"),93 ("crypto québec", "finance-affaires"), ("bourse québec", "finance-affaires"),94 ("entrepreneur québécois", "finance-affaires"),95 ("développeur québécois", "tech"), ("programmation québec", "tech"),96 ("photographie québec", "arts"), ("dessin québécois", "arts"),97 ("couture québec", "arts"), ("tricot québec", "arts"),98 ("chasse orignal québec", "plein-air"), ("motoneige québec", "plein-air"),99 ("quatre-roues québec", "plein-air"), ("survie en forêt québec", "plein-air"),100 ("astronomie québec", "education"), ("histoire du québec", "education"),101 ("hockey canadiens de montréal", "sport-fitness"),102 ("pêche mouche québec", "plein-air"), ("kayak québec", "plein-air"),103 ("randonnée québec", "plein-air"), ("microbrasserie québec", "bouffe-resto"),104 ("bbq québec", "cuisine"), ("pâtisserie québec", "cuisine"),105 ("végane québec", "cuisine"), ("chef québécois", "cuisine"),106 ("critique resto montréal", "bouffe-resto"),107 ("maquilleuse québécoise", "beaute"), ("coiffure québec", "beaute"),108 ("soins de peau québec", "beaute"), ("friperie montréal", "mode"),109 ("streetwear montréal", "mode"), ("chanteur québécois", "musique"),110 ("chanteuse québécoise", "musique"), ("guitare québec", "musique"),111 ("dj québec", "musique"), ("country québécois", "musique"),112 ("télé-réalité québec", "lifestyle"), ("occupation double", "lifestyle"),113 ("humoriste relève québec", "humour"), ("stand up québec", "humour"),114 ("améliorer son français québec", "education"),115 ("prof québécois", "education"), ("garderie maman québec", "famille-parentalite"),116 ("grossesse québec", "famille-parentalite"),117 ("astuces parents québec", "famille-parentalite"),118 ("spiritualité québec", "sante-mieux-etre"),119 ("astrologie québec", "autre"), ("paranormal québec", "autre"),120 ("urbex québec", "autre"), ("faits divers québec", "actualite-opinion"),121 ("politique québécoise", "actualite-opinion"),122 ("nouvelles québec", "actualite-opinion"),123 # créateurs anglophones/bilingues (les marqueurs montreal/mtl/quebec124 # figurent dans _QC_MARKERS — même filtre conservateur)125 ("montreal vlog", "lifestyle"), ("montreal vlogger", "lifestyle"),126 ("montreal youtuber", "lifestyle"), ("living in montreal", "lifestyle"),127 ("montreal food", "bouffe-resto"), ("montreal foodie", "bouffe-resto"),128 ("montreal restaurants", "bouffe-resto"), ("montreal cooking", "cuisine"),129 ("montreal gaming", "gaming"), ("montreal streamer", "gaming"),130 ("montreal comedy", "humour"), ("montreal standup", "humour"),131 ("montreal music", "musique"), ("montreal rap", "musique"),132 ("mtl rap", "musique"), ("montreal producer", "musique"),133 ("montreal fitness", "sport-fitness"), ("montreal gym", "sport-fitness"),134 ("montreal makeup", "beaute"), ("montreal beauty", "beaute"),135 ("montreal fashion", "mode"), ("montreal streetwear", "mode"),136 ("montreal thrift", "mode"), ("montreal barber", "beaute"),137 ("montreal tech", "tech"), ("montreal developer", "tech"),138 ("montreal photography", "arts"), ("montreal artist", "arts"),139 ("montreal dance", "danse"), ("montreal travel", "voyage"),140 ("montreal real estate", "finance-affaires"),141 ("montreal finance", "finance-affaires"),142 ("montreal podcast", "actualite-opinion"),143 ("montreal family vlog", "famille-parentalite"),144 ("quebec city vlog", "lifestyle"), ("quebec city food", "bouffe-resto"),145 ("quebec gaming", "gaming"), ("quebec outdoors", "plein-air"),146 ("quebec fishing", "plein-air"), ("quebec hunting", "plein-air"),147 ("quebec snowboard", "plein-air"), ("quebec skiing", "plein-air"),148 ("quebec history", "education"), ("learn french quebec", "education"),149 ("quebec french lessons", "education"), ("anglo montreal", "lifestyle"),150 ("laval vlogger", "lifestyle"), ("gatineau ottawa vlog", "lifestyle"),151]152153# --- longue traîne : villes/MRC × créneaux (nano/micro créateurs locaux) --------154# On ne vise PAS que les mégastars : chaque ville a ses vloggeurs, gamers,155# chasseurs, cuisiniers… Le filtre _QC_MARKERS garde la prudence d'attribution.156_TOWNS = [157 "montréal", "ville de québec", "laval", "gatineau", "longueuil",158 "sherbrooke", "lévis", "saguenay", "trois-rivières", "terrebonne",159 "brossard", "repentigny", "drummondville", "saint-jérôme", "granby",160 "blainville", "saint-hyacinthe", "shawinigan", "dollard-des-ormeaux",161 "rimouski", "châteauguay", "saint-jean-sur-richelieu", "mascouche",162 "joliette", "victoriaville", "salaberry-de-valleyfield", "rouyn-noranda",163 "chicoutimi", "alma", "sept-îles", "baie-comeau", "val-d'or", "matane",164 "rivière-du-loup", "sorel-tracy", "thetford mines", "magog", "cowansville",165 "la tuque", "mont-laurier", "mont-tremblant", "sainte-agathe",166 "saint-georges beauce", "montmagny", "la malbaie", "baie-saint-paul",167 "gaspé", "chandler", "amqui", "dolbeau-mistassini", "roberval",168 "chibougamau", "lachute", "varennes", "chambly", "boucherville",169]170_TOWN_THEMES = [("vlog", "lifestyle"), ("youtubeur", "lifestyle"),171 ("gaming", "gaming"), ("chasse pêche", "plein-air"),172 ("humour", "humour"),173 # vague 4 : la longue traîne locale couvre aussi la bouffe, la174 # forme, la beauté, la musique, la famille et les balados175 ("cuisine", "cuisine"), ("resto", "bouffe-resto"),176 ("fitness", "sport-fitness"), ("beauté", "beaute"),177 ("musique", "musique"), ("famille", "famille-parentalite"),178 ("balado", "actualite-opinion"), ("plein air", "plein-air")]179for _t in _TOWNS:180 for _th, _n in _TOWN_THEMES:181 QUERIES.append((f"{_th} {_t}", _n))182183# --- longue traîne : passe-temps & métiers × marqueur QC ------------------------184_HOBBIES = [185 ("skateboard", "sport-fitness"), ("snowboard", "plein-air"),186 ("pickleball", "sport-fitness"), ("golf", "sport-fitness"),187 ("baseball", "sport-fitness"), ("soccer", "sport-fitness"),188 ("basketball", "sport-fitness"), ("boxe", "sport-fitness"),189 ("mma", "sport-fitness"), ("crossfit", "sport-fitness"),190 ("échecs", "gaming"), ("donjons et dragons", "gaming"),191 ("cartes pokémon", "gaming"), ("warhammer", "gaming"),192 ("speedrun", "gaming"), ("simracing", "gaming"),193 ("lego", "autre"), ("maquette", "arts"), ("bois", "arts"),194 ("forge", "arts"), ("céramique", "arts"), ("peinture", "arts"),195 ("aquarelle", "arts"), ("calligraphie", "arts"),196 ("astrophotographie", "education"), ("ornithologie", "plein-air"),197 ("apiculture", "plein-air"), ("permaculture", "plein-air"),198 ("homesteading", "plein-air"), ("érablière", "plein-air"),199 ("acériculture", "plein-air"), ("motocross", "autre"),200 ("vtt", "plein-air"), ("side-by-side", "plein-air"),201 ("camion", "autre"), ("remorquage", "autre"), ("soudure", "autre"),202 ("électricien", "autre"), ("plombier", "autre"), ("excavation", "autre"),203 ("déneigement", "autre"), ("paysagement", "autre"),204 ("courtier immobilier", "finance-affaires"), ("comptable", "finance-affaires"),205 ("avocat", "education"), ("infirmière", "sante-mieux-etre"),206 ("massothérapie", "sante-mieux-etre"), ("ostéopathie", "sante-mieux-etre"),207 ("naturopathie", "sante-mieux-etre"), ("psychologie", "sante-mieux-etre"),208 ("méditation", "sante-mieux-etre"), ("tarot", "autre"),209 ("généalogie", "education"), ("philosophie", "education"),210 ("booktube", "education"), ("lecture", "education"),211 ("cinéma", "actualite-opinion"), ("séries télé", "actualite-opinion"),212 ("anime", "gaming"), ("manga", "gaming"), ("cosplay", "arts"),213 ("drag", "arts"), ("magie", "arts"), ("cirque", "arts"),214 ("violon", "musique"), ("piano", "musique"), ("batterie", "musique"),215 ("chorale", "musique"), ("trad", "musique"), ("métal", "musique"),216 ("punk", "musique"), ("folk", "musique"),217 ("fromage", "bouffe-resto"), ("vin", "bouffe-resto"),218 ("café", "bouffe-resto"), ("boulangerie", "cuisine"),219 ("cabane à sucre", "bouffe-resto"), ("poutine", "bouffe-resto"),220 ("food truck", "bouffe-resto"), ("mixologie", "bouffe-resto"),221 ("couture", "arts"), ("crochet", "arts"), ("broderie", "arts"),222 ("thrift", "mode"), ("sneakers", "mode"), ("barbier", "beaute"),223 ("ongles", "beaute"), ("tatouage", "arts"), ("esthétique", "beaute"),224 ("organisation maison", "lifestyle"), ("minimalisme", "lifestyle"),225 ("van aménagé", "voyage"), ("chalet", "lifestyle"),226 ("astuces budget", "finance-affaires"), ("coupons", "finance-affaires"),227 ("frugal", "finance-affaires"), ("bourse dividendes", "finance-affaires"),228 ("intelligence artificielle", "tech"), ("cybersécurité", "tech"),229 ("impression 3d", "tech"), ("domotique", "tech"), ("drones", "tech"),230 # vague 4 : encore plus de passe-temps, sports, métiers et scènes231 ("escalade", "sport-fitness"), ("trail", "sport-fitness"),232 ("triathlon", "sport-fitness"), ("powerlifting", "sport-fitness"),233 ("arts martiaux", "sport-fitness"), ("jiu-jitsu", "sport-fitness"),234 ("karaté", "sport-fitness"), ("boxe thaï", "sport-fitness"),235 ("patinage", "sport-fitness"), ("ringuette", "sport-fitness"),236 ("curling", "sport-fitness"), ("cheerleading", "sport-fitness"),237 ("gymnastique", "sport-fitness"), ("natation", "sport-fitness"),238 ("plongée", "plein-air"), ("voile", "plein-air"),239 ("kayak de mer", "plein-air"), ("canot", "plein-air"),240 ("raquette", "plein-air"), ("ski de fond", "plein-air"),241 ("ski hors-piste", "plein-air"), ("escapade", "voyage"),242 ("roadtrip", "voyage"), ("croisière", "voyage"),243 ("aquariophilie", "autre"), ("reptiles", "autre"),244 ("chiens", "autre"), ("chats", "autre"), ("chevaux", "plein-air"),245 ("équitation", "plein-air"), ("fermette", "plein-air"),246 ("poules", "plein-air"), ("potager", "plein-air"),247 ("champignons", "plein-air"), ("mycologie", "plein-air"),248 ("herboristerie", "sante-mieux-etre"), ("zéro déchet", "lifestyle"),249 ("autosuffisance", "plein-air"), ("survivalisme", "plein-air"),250 ("vinyle", "musique"), ("audiophile", "tech"),251 ("home studio", "musique"), ("beatmaking", "musique"),252 ("production musicale", "musique"), ("ukulélé", "musique"),253 ("saxophone", "musique"), ("harmonica", "musique"),254 ("accordéon", "musique"), ("chant", "musique"),255 ("billard", "autre"), ("poker", "autre"), ("fléchettes", "autre"),256 ("airsoft", "autre"), ("paintball", "autre"),257 ("modélisme", "arts"), ("figurines", "arts"), ("bijoux", "arts"),258 ("savonnerie", "arts"), ("chandelles", "arts"),259 ("macramé", "arts"), ("scrapbooking", "arts"),260 ("philatélie", "education"), ("numismatique", "education"),261 ("antiquités", "autre"), ("brocante", "autre"),262 ("horlogerie", "arts"), ("restauration meubles", "arts"),263 ("ébénisterie", "arts"), ("charpenterie", "autre"),264 ("maçonnerie", "autre"), ("toiture", "autre"),265 ("mécanique diesel", "autre"), ("carrosserie", "autre"),266 ("détailing", "autre"), ("camion lourd", "autre"),267 ("agriculture laitière", "plein-air"), ("grandes cultures", "plein-air"),268 ("boucherie", "cuisine"), ("charcuterie", "cuisine"),269 ("fermentation", "cuisine"), ("kombucha", "cuisine"),270 ("cidre", "bouffe-resto"), ("hydromel", "bouffe-resto"),271 ("distillerie", "bouffe-resto"), ("torréfaction", "bouffe-resto"),272 ("sommelier", "bouffe-resto"), ("cocktails", "bouffe-resto"),273 ("orthopédagogie", "education"), ("orthophonie", "education"),274 ("tdah", "sante-mieux-etre"), ("autisme", "sante-mieux-etre"),275 ("kinésiologie", "sante-mieux-etre"), ("chiropratique", "sante-mieux-etre"),276 ("dermatologie", "beaute"), ("nutritionniste", "sante-mieux-etre"),277 ("sage-femme", "famille-parentalite"), ("doula", "famille-parentalite"),278 ("homeschooling", "famille-parentalite"), ("école à la maison", "famille-parentalite"),279 ("astuces ménage", "lifestyle"), ("décoration", "lifestyle"),280 ("design intérieur", "lifestyle"), ("aménagement paysager", "autre"),281 ("piscine", "autre"), ("spa", "autre"),282 ("courtier hypothécaire", "finance-affaires"),283 ("planificateur financier", "finance-affaires"),284 ("assurance", "finance-affaires"), ("fiscalité", "finance-affaires"),285 ("dropshipping", "finance-affaires"), ("commerce en ligne", "finance-affaires"),286 ("marketing numérique", "finance-affaires"), ("freelance", "finance-affaires"),287 ("linguistique", "education"), ("vulgarisation", "education"),288 ("archéologie", "education"), ("géographie", "education"),289 ("météo", "education"), ("aviation", "tech"),290 ("simulateur de vol", "gaming"), ("trains", "autre"),291 ("camping-car", "voyage"), ("vr roulotte", "voyage"),292 ("pêche blanche", "plein-air"), ("trappage", "plein-air"),293 ("taxidermie", "autre"), ("coutellerie", "arts"),294 ("armes à feu", "plein-air"), ("tir à l'arc", "plein-air"),295]296for _h, _n in _HOBBIES:297 QUERIES.append((f"{_h} québec", _n))298 QUERIES.append((f"{_h} québécois", _n))299300301def _walk_channels(node, out: list) -> None:302 if isinstance(node, dict):303 if "channelRenderer" in node:304 out.append(node["channelRenderer"])305 for v in node.values():306 _walk_channels(v, out)307 elif isinstance(node, list):308 for item in node:309 _walk_channels(item, out)310311312def _walk_token(node) -> str | None:313 """Jeton de continuation (page suivante) n'importe où dans l'arbre JSON."""314 if isinstance(node, dict):315 cmd = node.get("continuationCommand")316 if isinstance(cmd, dict) and cmd.get("token"):317 return cmd["token"]318 for v in node.values():319 tok = _walk_token(v)320 if tok:321 return tok322 elif isinstance(node, list):323 for item in node:324 tok = _walk_token(item)325 if tok:326 return tok327 return None328329330def parse_channels(html: str) -> list[dict]:331 """HTML de résultats → [{handle, channel_id, name, subs, description}]."""332 m = _YTID_RE.search(html)333 if not m:334 return []335 try:336 data = json.loads(m.group(1))337 except Exception:338 return []339 return channels_from_data(data)340341342def channels_from_data(data) -> list[dict]:343 """Arbre JSON (page 1 ou réponse de continuation) → liste de chaînes."""344 renderers: list[dict] = []345 _walk_channels(data, renderers)346 channels = []347 for r in renderers:348 try:349 handle = ((r.get("navigationEndpoint") or {}).get("browseEndpoint")350 or {}).get("canonicalBaseUrl", "").lstrip("/")351 name = ((r.get("title") or {}).get("simpleText")) or ""352 desc = "".join(run.get("text", "") for run in353 ((r.get("descriptionSnippet") or {}).get("runs") or []))354 subs_txt = ((r.get("videoCountText") or {}).get("simpleText")) or \355 ((r.get("subscriberCountText") or {}).get("simpleText")) or ""356 # avatar « gratuit » : le renderer embarque les miniatures du profil357 # → meilleure résolution (URLs souvent protocol-relative « //yt3… »)358 thumbs = (r.get("thumbnail") or {}).get("thumbnails") or []359 avatar = None360 if thumbs:361 avatar = max(thumbs,362 key=lambda t: t.get("width") or 0).get("url")363 if avatar and avatar.startswith("//"):364 avatar = "https:" + avatar365 channels.append({"handle": handle, "channel_id": r.get("channelId", ""),366 "name": name, "subs": parse_count(subs_txt),367 "description": desc, "avatar": avatar})368 except Exception:369 continue370 return channels371372373def is_quebec(text: str) -> bool:374 return bool(_QC_MARKERS.search(strip_accents(text or "").lower()))375376377class YouTubeRechercheConnector(BaseConnector):378 source_id = "youtube-recherche"379 kind = "discovery"380 request_delay = 1.0381 max_queries = 2500382 max_pages_per_query = 4 # PROFONDEUR : pages de continuation (~20 chaînes/page)383384 def _collect(self, channels: list[dict], niche: str, seen: set[str],385 creators: list[Creator]) -> None:386 for ch in channels:387 handle = ch["handle"].lstrip("@")388 key = handle or ch["channel_id"]389 if not key or key in seen:390 continue391 # filtre conservateur : marqueur QC requis dans titre+description392 if not is_quebec(f"{ch['name']} {ch['description']}"):393 continue394 seen.add(key)395 acc = account("youtube", handle or ch["channel_id"], "profil_source",396 followers=ch["subs"])397 creators.append(Creator(398 source=self.source_id,399 external_id=slugify(ch["name"]),400 display_name=ch["name"],401 bio=ch["description"][:1200],402 niches=[niche],403 creator_type="youtubeur",404 avatar_url=ch.get("avatar"),405 platforms=[acc],406 source_ids=[f"{self.source_id}:{key}"],407 ))408409 def fetch(self) -> list[Creator]:410 creators: list[Creator] = []411 seen: set[str] = set()412 for query, niche in QUERIES[:self.max_queries]:413 try:414 html = self.get(415 SEARCH_URL,416 params={"search_query": query, "sp": "EgIQAg=="},417 headers={"Accept-Language": "fr-CA,fr;q=0.9"}).text418 except Exception:419 continue420 m = _YTID_RE.search(html)421 if not m:422 continue423 try:424 data = json.loads(m.group(1))425 except Exception:426 continue427 key_m, ver_m = _KEY_RE.search(html), _VER_RE.search(html)428 for page in range(self.max_pages_per_query):429 self._collect(channels_from_data(data), niche, seen, creators)430 token = _walk_token(data)431 if not token or not key_m or not ver_m \432 or page == self.max_pages_per_query - 1:433 break434 try: # pages suivantes via l'API interne de la page publique435 data = self.post(436 INNERTUBE_URL, params={"key": key_m.group(1)},437 json={"context": {"client": {438 "clientName": "WEB",439 "clientVersion": ver_m.group(1),440 "hl": "fr", "gl": "CA"}},441 "continuation": token},442 headers={"Accept-Language": "fr-CA,fr;q=0.9"}).json()443 except Exception:444 break445 return creators446