| 44 | ACTOR_OWNER = "gorgeous_thistle" |
44 | ACTOR_OWNER = "gorgeous_thistle" |
| 45 | RESIDENTIAL = {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]} |
45 | RESIDENTIAL = {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]} |
| 46 | |
46 | |
| |
47 | +# erreurs DÉFINITIVES des acteurs (handle mort, profil privé, mur de login…) : |
| |
48 | +# on tamponne le compte quand même pour qu'il ne soit re-sondé qu'à la rotation |
| |
49 | +# `revisit_days`, au lieu d'être re-payé chaque jour en pure perte. Les erreurs |
| |
50 | +# transitoires (429, 5xx, shell, proxy) ne sont PAS tamponnées → re-tentées. |
| |
51 | +MISS_PREFIXES = ("user_not_found", "private", "not_found", "no_profile", |
| |
52 | + "no_channel", "login_wall", "invite_invalide", "http_404", |
| |
53 | + "wall_") |
| |
54 | + |
| 47 | # --- parallélisme : on dispose de 256 Go de RAM Apify → au lieu d'UN run à |
55 | # --- parallélisme : on dispose de 256 Go de RAM Apify → au lieu d'UN run à |
| 48 | # 1 Go, on éclate le lot en tranches lancées EN PARALLÈLE, chacune avec plus de |
56 | # 1 Go, on éclate le lot en tranches lancées EN PARALLÈLE, chacune avec plus de |
| 49 | # mémoire (donc plus de CPU/vCPU côté Apify) et une concurrence interne accrue. |
57 | # mémoire (donc plus de CPU/vCPU côté Apify) et une concurrence interne accrue. |
| 57 | # rafraîchissement : un compte est re-sondé s'il n'a pas été enrichi depuis |
65 | # rafraîchissement : un compte est re-sondé s'il n'a pas été enrichi depuis |
| 58 | # REVISIT_DAYS jours. Le watch quotidien couvre ainsi TOUT le bassin en ≤ N j. |
66 | # REVISIT_DAYS jours. Le watch quotidien couvre ainsi TOUT le bassin en ≤ N j. |
| 59 | REVISIT_DAYS = int(os.environ.get("APIFY_REVISIT_DAYS", "7")) |
67 | REVISIT_DAYS = int(os.environ.get("APIFY_REVISIT_DAYS", "7")) |
| |
68 | +# passage FORCÉ (APIFY_FORCE=1) : ignore fraîcheur ET caps — couvre TOUT le |
| |
69 | +# bassin de chaque plateforme en un seul passage (rattrapage de couverture) |
| |
70 | +FORCE_PASS = os.environ.get("APIFY_FORCE", "") == "1" |
| 60 | |
71 | |
| 61 | |
72 | |
| 62 | def _token() -> str: |
73 | def _token() -> str: |
| 178 | return f"{self.source_id}_at" |
189 | return f"{self.source_id}_at" |
| 179 | |
190 | |
| 180 | def _stale(self, acc) -> bool: |
191 | def _stale(self, acc) -> bool: |
| |
192 | + if FORCE_PASS: |
| |
193 | + return True |
| 181 | stamp = (acc.metrics or {}).get(self._stamp_key) |
194 | stamp = (acc.metrics or {}).get(self._stamp_key) |
| 182 | if not stamp: |
195 | if not stamp: |
| 183 | return True |
196 | return True |
| 209 | if acc is None or not self._stale(acc): |
222 | if acc is None or not self._stale(acc): |
| 210 | continue |
223 | continue |
| 211 | targets.setdefault(self.target_of(acc), []).append((cr, acc)) |
224 | targets.setdefault(self.target_of(acc), []).append((cr, acc)) |
| 212 | − if len(targets) >= self.cap: |
225 | + if not FORCE_PASS and len(targets) >= self.cap: |
| 213 | break |
226 | break |
| 214 | if not targets: |
227 | if not targets: |
| 215 | return [] |
228 | return [] |
| 218 | {"proxyConfiguration": RESIDENTIAL, |
231 | {"proxyConfiguration": RESIDENTIAL, |
| 219 | "concurrency": RUN_CONCURRENCY, |
232 | "concurrency": RUN_CONCURRENCY, |
| 220 | **self.extra_input()}, |
233 | **self.extra_input()}, |
| 221 | − sorted(targets)) |
234 | + sorted(targets), |
| |
235 | + # passage forcé : tranches plus grosses (bassin entier / 32 runs) |
| |
236 | + # → délai par run élargi en proportion |
| |
237 | + timeout_s=7200 if FORCE_PASS else 2400) |
| 222 | by_handle = {str(it.get("username") or "").lower(): it |
238 | by_handle = {str(it.get("username") or "").lower(): it |
| 223 | for it in items if it.get("kind") == "profile"} |
239 | for it in items if it.get("kind") == "profile"} |
| 224 | enriched: list[Creator] = [] |
240 | enriched: list[Creator] = [] |
| 228 | if it is None: |
244 | if it is None: |
| 229 | continue |
245 | continue |
| 230 | if not it.get("found"): |
246 | if not it.get("found"): |
| 231 | − if str(it.get("error", "")).startswith(("http_5", "shell")): |
247 | + err = str(it.get("error", "")) |
| |
248 | + if err.startswith(("http_5", "shell")): |
| 232 | self.errors += 1 |
249 | self.errors += 1 |
| |
250 | + elif err.startswith(MISS_PREFIXES): |
| |
251 | + for cr, acc in pairs: # miss définitif → rotation douce |
| |
252 | + acc.metrics[self._stamp_key] = now_iso() |
| |
253 | + acc.metrics[f"{self.source_id}_miss"] = err |
| |
254 | + enriched.append(cr) |
| 233 | continue |
255 | continue |
| 234 | for cr, acc in pairs: |
256 | for cr, acc in pairs: |
| 235 | self.apply(cr, acc, it) |
257 | self.apply(cr, acc, it) |
| 252 | if it.get(img_key): |
274 | if it.get(img_key): |
| 253 | metrics[img_key] = it[img_key] |
275 | metrics[img_key] = it[img_key] |
| 254 | metrics[self._stamp_key] = now_iso() # tampon d'enrichissement propre |
276 | metrics[self._stamp_key] = now_iso() # tampon d'enrichissement propre |
| |
277 | + acc.metrics.pop(f"{self.source_id}_miss", None) # ressuscité |
| 255 | acc.metrics.update(metrics) |
278 | acc.metrics.update(metrics) |
| 256 | # avatar/bannière de la FICHE : rafraîchis à chaque passage depuis la |
279 | # avatar/bannière de la FICHE : rafraîchis à chaque passage depuis la |
| 257 | # plateforme principale (les URLs CDN signées expirent — ex. Instagram) ; |
280 | # plateforme principale (les URLs CDN signées expirent — ex. Instagram) ; |
| 362 | "top_hashtags": "top_hashtags", "top_tweet": "top_tweet"} |
385 | "top_hashtags": "top_hashtags", "top_tweet": "top_tweet"} |
| 363 | |
386 | |
| 364 | def extra_input(self) -> dict: |
387 | def extra_input(self) -> dict: |
| 365 | − return {"maxTweets": 20} |
388 | + # concurrence réduite : l'endpoint syndication de X rend des 429 en |
| |
389 | + # rafale au-delà de ~4 requêtes simultanées par run (constat 2026-08-21) |
| |
390 | + return {"maxTweets": 20, "concurrency": 4} |
| 366 | |
391 | |
| 367 | def bio_urls(self, it: dict) -> list[str]: |
392 | def bio_urls(self, it: dict) -> list[str]: |
| 368 | return [it.get("website") or ""] |
393 | return [it.get("website") or ""] |
| 369 | |
394 | |