Spaces:
Sleeping
Sleeping
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- providers/blinkit.py +79 -16
- 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 (
|
| 14 |
"app_client": "consumer_web",
|
|
|
|
|
|
|
|
|
|
| 15 |
"lat": f"{lat:.6f}",
|
| 16 |
"lon": f"{lon:.6f}",
|
| 17 |
-
"
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 61 |
products: List[RawProduct] = []
|
| 62 |
|
| 63 |
async with self.semaphore:
|
| 64 |
try:
|
| 65 |
async with AsyncSession(impersonate="chrome124") as s:
|
| 66 |
-
|
|
|
|
| 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 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
products: List[RawProduct] = []
|
| 71 |
|
| 72 |
async with self.semaphore:
|
| 73 |
try:
|
| 74 |
async with AsyncSession(impersonate="chrome124") as s:
|
| 75 |
-
resp =
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|