Files
AntiCoco/tools/probe_menu_capture.py
jerem 03e281a810 Sélection courante + favoris + images servables (sortie prête Hermes)
Deux outils MCP pour qu'Hermes n'ait plus de scripts à écrire :
- hf_next_delivery() : prochaine box RÉELLEMENT sélectionnée (≈4 recettes,
  pas le menu complet) + date/cutoff ; erreur stricte si introuvable
  (jamais de repli propose). Saute les semaines PAUSED via next_delivery.
- hf_favorites() : recettes favorites du compte. Champ is_favorite ajouté
  partout (hf_get_menu inclus).

Endpoints découverts (probe CDP) :
- sélection : GET /gw/my-deliveries/menu -> meals[].selection.quantity>0
- favoris   : GET /gw/cfs/v2/favorites/recipe -> items[].object_id
(GET /gw/v1/carts/{week} renvoie 404 : pas la lecture de sélection.)

Images : URLs recettes CloudFront (502) réécrites vers
img.hellofresh.com/.../hellofresh_s3/... (hellofresh/images.py),
appliqué dans Recipe.summary() -> profite à tous les outils.

README : procédure de ré-auth CDP clarifiée (refresh tokens rotatifs,
backups inutiles, page /login, profil Chrome dédié).

Outils de re-découverte : tools/probe_selection.py, tools/probe_menu_capture.py
2026-06-18 14:18:40 +02:00

105 lines
4.1 KiB
Python

"""Capture ciblée de l'appel « sélection courante » de la page menu (via CDP).
Contrairement à attach_capture (passif), ce script PILOTE la page : il navigue lui-même
vers la page menu de la semaine cible, ce qui force le rechargement et fait partir l'appel
qui porte la sélection (probablement GET /gw/cart/{uuid}). Il isole les appels pertinents,
puis, s'il voit un GET /gw/cart/{uuid}, le rejoue côté client authentifié et dump le corps.
Prérequis : Chrome lancé en debug + connecté (cf. README §2) :
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \\
--remote-debugging-port=9222 --user-data-dir="$HOME/.hf-chrome-debug" \\
https://www.hellofresh.fr/my-account/deliveries/menu
Usage :
python tools/probe_menu_capture.py [WEEK] # WEEK ex. 2026-W27 (défaut: prochaine livraison)
"""
from __future__ import annotations
import json
import re
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from playwright.sync_api import sync_playwright # noqa: E402
from hellofresh import api, auth # noqa: E402
CDP_URL = "http://localhost:9222"
OUT = auth.SESSION_DIR / "_probe"
RELEVANT = ("/cart", "/v1/carts", "menus-service", "/meal", "selection", "/box")
UUID_RE = re.compile(r"/gw/cart/([0-9a-f]{8}-[0-9a-f-]{27,})")
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
week = sys.argv[1] if len(sys.argv) > 1 else None
if not week:
with api.HelloFreshClient() as c:
week = (c.account_info().get("next_delivery") or {}).get("week") or api.current_week()
menu_url = f"{auth.BASE_URL}/my-account/deliveries/menu?weekId={week}"
print(f"[probe] semaine cible = {week}")
hits: list[dict] = []
def on_request(req):
u = req.url
if not auth._is_gateway_request(u):
return
if any(k in u for k in RELEVANT):
hits.append({"method": req.method, "url": u,
"auth": bool(req.headers.get("authorization"))})
print(f" [{req.method}] {u[:150]}")
with sync_playwright() as pw:
print(f"[probe] connexion CDP {CDP_URL}")
browser = pw.chromium.connect_over_cdp(CDP_URL)
ctx = browser.contexts[0]
for p in ctx.pages:
p.on("request", on_request)
ctx.on("page", lambda p: p.on("request", on_request))
page = ctx.pages[0] if ctx.pages else ctx.new_page()
print(f"[probe] navigation -> {menu_url}")
try:
page.goto(menu_url, wait_until="domcontentloaded", timeout=45000)
except Exception as e: # noqa: BLE001
print(" (goto a levé, on continue d'écouter)", e)
for _ in range(20):
page.wait_for_timeout(1000)
print(f"\n[probe] {len(hits)} appels pertinents capturés.")
cart_uuids = []
for h in hits:
m = UUID_RE.search(h["url"])
if m and h["method"] == "GET":
cart_uuids.append(m.group(1))
cart_uuids = list(dict.fromkeys(cart_uuids))
(OUT / "menu_capture_hits.json").write_text(
json.dumps(hits, ensure_ascii=False, indent=2), encoding="utf-8")
if cart_uuids:
print(f"[probe] cart UUID(s) détecté(s): {cart_uuids}")
with api.HelloFreshClient() as c:
for uid in cart_uuids:
for suffix in ("", "/items"):
url = f"{auth.BASE_URL}/gw/cart/{uid}{suffix}"
try:
r = c._request("GET", url, params={"country": "FR", "locale": "fr-FR"})
name = f"cart_{uid[:8]}{suffix.replace('/', '_')}"
(OUT / f"{name}.json").write_text(
json.dumps(r.json(), ensure_ascii=False, indent=2), encoding="utf-8")
print(f" {r.status_code} {url} -> {name}.json")
except Exception as e: # noqa: BLE001
print(f" ERR {url}: {e}")
else:
print("[probe] aucun GET /gw/cart/{uuid} vu. Voir menu_capture_hits.json pour les autres pistes.")
if __name__ == "__main__":
main()