Muneesh commited on
Commit
c286216
·
1 Parent(s): 88ca278

feat(providers): switch blinkit to unauthenticated Buyhatke endpoint with auth_key, fix zepto fallback

Browse files
Files changed (2) hide show
  1. providers/blinkit.py +79 -16
  2. providers/zepto.py +16 -4
providers/blinkit.py CHANGED
@@ -7,14 +7,47 @@ logger = logging.getLogger("qc.blinkit")
7
 
8
  class BlinkitProvider(BaseProvider):
9
  platform_name = "blinkit"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  def _get_headers(self, lat: float, lon: float) -> dict:
12
  return {
13
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
14
  "app_client": "consumer_web",
 
 
 
15
  "lat": f"{lat:.6f}",
16
  "lon": f"{lon:.6f}",
17
- "Accept": "application/json, text/plain, */*",
 
 
18
  "Accept-Language": "en-US,en;q=0.9",
19
  "Origin": "https://blinkit.com",
20
  "Referer": "https://blinkit.com/",
@@ -56,54 +89,84 @@ class BlinkitProvider(BaseProvider):
56
  lon: float,
57
  pincode: Optional[str] = None
58
  ) -> List[RawProduct]:
 
 
59
  headers = self._get_headers(lat, lon)
60
- url = f"https://blinkit.com/v1/search/products?q={query}&start=0&size=20"
 
 
 
61
  products: List[RawProduct] = []
62
 
63
  async with self.semaphore:
64
  try:
65
  async with AsyncSession(impersonate="chrome124") as s:
66
- resp = await s.get(url, headers=headers, timeout=10)
 
67
  if resp.status_code != 200:
68
  logger.warning(f"[Blinkit] Search status={resp.status_code}")
69
  return products
70
 
71
  data = resp.json()
72
- items = (
73
- data.get("products", [])
74
- or data.get("objects", [])
75
- or data.get("items", [])
76
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  for item in items:
79
- name = item.get("name") or item.get("title") or item.get("product_name")
80
  if not name:
81
  continue
82
 
83
- price = float(item.get("price") or item.get("selling_price") or 0)
84
- mrp = float(item.get("mrp") or price)
85
  if price <= 0:
86
  continue
87
 
88
  discount = int(round(((mrp - price) / mrp) * 100)) if mrp > price else 0
89
 
90
- images = item.get("images") or item.get("image_url") or []
91
  img_url = images[0] if isinstance(images, list) and images else (images if isinstance(images, str) else None)
 
 
92
 
93
  pid = str(item.get("product_id") or item.get("id") or name)
94
  brand = item.get("brand") or item.get("brand_name")
95
- unit = item.get("unit") or item.get("pack_size") or item.get("weight")
96
 
97
  products.append(RawProduct(
98
  platform=self.platform_name,
99
  product_id=pid,
100
  name=name.strip(),
101
  brand=brand,
102
- pack_size=unit,
103
  price=price,
104
  mrp=mrp,
105
  discount_percentage=discount,
106
- available=item.get("inventory", 1) > 0 if "inventory" in item else True,
107
  image_url=img_url,
108
  deeplink=f"https://blinkit.com/s/?q={query}",
109
  eta_minutes=12
 
7
 
8
  class BlinkitProvider(BaseProvider):
9
  platform_name = "blinkit"
10
+ REQ_KEY = "c0e6868e-1180-400c-be51-f473479f1f0a" # Buyhatke/Comparify unauthenticated constant (blinkit-mcp)
11
+
12
+ def __init__(self, *args, **kwargs):
13
+ super().__init__(*args, **kwargs)
14
+ self._auth_key = None
15
+ self._device_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
16
+ self._session_uuid = "11111111-2222-3333-4444-555555555555"
17
+
18
+ async def _ensure_auth_key(self) -> str:
19
+ if self._auth_key:
20
+ return self._auth_key
21
+ try:
22
+ async with AsyncSession(impersonate="chrome124") as s:
23
+ headers = {
24
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
25
+ "app_client": "consumer_web",
26
+ "platform": "desktop_web",
27
+ "req_key": self.REQ_KEY,
28
+ "Accept": "*/*",
29
+ }
30
+ resp = await s.get("https://blinkit.com/v2/accounts/auth_key/", headers=headers, timeout=8)
31
+ if resp.status_code == 200:
32
+ data = resp.json()
33
+ self._auth_key = data.get("auth_key") or data.get("authKey")
34
+ return self._auth_key or ""
35
+ except Exception as e:
36
+ logger.warning(f"[Blinkit] auth_key fetch failed: {e}")
37
+ return ""
38
 
39
  def _get_headers(self, lat: float, lon: float) -> dict:
40
  return {
41
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
42
  "app_client": "consumer_web",
43
+ "platform": "desktop_web",
44
+ "web_app_version": "1008010016",
45
+ "rn_bundle_version": "1009003012",
46
  "lat": f"{lat:.6f}",
47
  "lon": f"{lon:.6f}",
48
+ "device_id": self._device_id,
49
+ "session_uuid": self._session_uuid,
50
+ "Accept": "*/*",
51
  "Accept-Language": "en-US,en;q=0.9",
52
  "Origin": "https://blinkit.com",
53
  "Referer": "https://blinkit.com/",
 
89
  lon: float,
90
  pincode: Optional[str] = None
91
  ) -> List[RawProduct]:
92
+ # Buyhatke/Comparify use POST /v1/layout/search with auth_key — unauthenticated, no OTP
93
+ auth_key = await self._ensure_auth_key()
94
  headers = self._get_headers(lat, lon)
95
+ if auth_key:
96
+ headers["auth_key"] = auth_key
97
+ # Use Buyhatke-style endpoint: POST /v1/layout/search
98
+ url = f"https://blinkit.com/v1/layout/search?q={query}&search_type=type_to_search"
99
  products: List[RawProduct] = []
100
 
101
  async with self.semaphore:
102
  try:
103
  async with AsyncSession(impersonate="chrome124") as s:
104
+ # Blinkit expects POST with JSON body {applied_filters:null, sort:"", previous_search_query}
105
+ resp = await s.post(url, headers=headers, json={"applied_filters": None, "sort": "", "previous_search_query": query}, timeout=10)
106
  if resp.status_code != 200:
107
  logger.warning(f"[Blinkit] Search status={resp.status_code}")
108
  return products
109
 
110
  data = resp.json()
111
+ # Server-driven UI tree — extract via walk, dedupe, filter by query
112
+ raw_items = []
113
+ def walk(node):
114
+ if not isinstance(node, dict):
115
+ return
116
+ if "product_id" in node and "price" in node:
117
+ raw_items.append(node)
118
+ for v in node.values():
119
+ if isinstance(v, dict):
120
+ walk(v)
121
+ elif isinstance(v, list):
122
+ for el in v:
123
+ walk(el)
124
+ walk(data)
125
+ if not raw_items:
126
+ raw_items = data.get("products", []) or data.get("objects", []) or data.get("items", [])
127
+ # Dedupe by product_id
128
+ seen = set()
129
+ items = []
130
+ for it in raw_items:
131
+ pid = str(it.get("product_id") or it.get("id") or "")
132
+ if pid in seen:
133
+ continue
134
+ seen.add(pid)
135
+ items.append(it)
136
+ if len(items) >= 20:
137
+ break
138
 
139
  for item in items:
140
+ name = item.get("name") or item.get("title") or item.get("product_name") or item.get("display_name")
141
  if not name:
142
  continue
143
 
144
+ price = float(item.get("price") or item.get("selling_price") or item.get("offer_price") or 0)
145
+ mrp = float(item.get("mrp") or item.get("store_price") or price)
146
  if price <= 0:
147
  continue
148
 
149
  discount = int(round(((mrp - price) / mrp) * 100)) if mrp > price else 0
150
 
151
+ images = item.get("images") or item.get("image_url") or item.get("imageUrl") or []
152
  img_url = images[0] if isinstance(images, list) and images else (images if isinstance(images, str) else None)
153
+ if isinstance(img_url, dict):
154
+ img_url = img_url.get("url") or img_url.get("image_url")
155
 
156
  pid = str(item.get("product_id") or item.get("id") or name)
157
  brand = item.get("brand") or item.get("brand_name")
158
+ unit = item.get("unit") or item.get("pack_size") or item.get("weight") or item.get("variant")
159
 
160
  products.append(RawProduct(
161
  platform=self.platform_name,
162
  product_id=pid,
163
  name=name.strip(),
164
  brand=brand,
165
+ pack_size=str(unit) if unit else None,
166
  price=price,
167
  mrp=mrp,
168
  discount_percentage=discount,
169
+ available=item.get("inventory", 1) > 0 if "inventory" in item else item.get("in_stock", True),
170
  image_url=img_url,
171
  deeplink=f"https://blinkit.com/s/?q={query}",
172
  eta_minutes=12
providers/zepto.py CHANGED
@@ -66,15 +66,27 @@ class ZeptoProvider(BaseProvider):
66
  if store_id:
67
  headers["x-store-id"] = str(store_id)
68
 
69
- url = f"https://api.zeptonow.com/api/v1/search?query={query}&pageNumber=1&mode=SEARCH"
 
 
 
 
70
  products: List[RawProduct] = []
71
 
72
  async with self.semaphore:
73
  try:
74
  async with AsyncSession(impersonate="chrome124") as s:
75
- resp = await s.get(url, headers=headers, timeout=10)
76
- if resp.status_code != 200:
77
- logger.warning(f"[Zepto] Search failed status={resp.status_code}")
 
 
 
 
 
 
 
 
78
  return products
79
 
80
  data = resp.json()
 
66
  if store_id:
67
  headers["x-store-id"] = str(store_id)
68
 
69
+ # Try primary host, fallback to www host (api. DNS sometimes fails locally)
70
+ urls = [
71
+ f"https://api.zeptonow.com/api/v1/search?query={query}&pageNumber=1&mode=SEARCH",
72
+ f"https://www.zeptonow.com/api/v1/search?query={query}&pageNumber=1&mode=SEARCH",
73
+ ]
74
  products: List[RawProduct] = []
75
 
76
  async with self.semaphore:
77
  try:
78
  async with AsyncSession(impersonate="chrome124") as s:
79
+ resp = None
80
+ for u in urls:
81
+ try:
82
+ resp = await s.get(u, headers=headers, timeout=10)
83
+ if resp.status_code == 200:
84
+ url = u
85
+ break
86
+ except Exception:
87
+ continue
88
+ if resp is None or resp.status_code != 200:
89
+ logger.warning(f"[Zepto] Search failed status={resp.status_code if resp else 'no response'}")
90
  return products
91
 
92
  data = resp.json()