|
# /// script |
|
# requires-python = ">=3.13" |
|
# dependencies = [ |
|
# "playwright>=1.55.0", |
|
# "undetected-playwright>=0.3.0", |
|
# ] |
|
# /// |
|
|
|
import asyncio |
|
|
|
from playwright.async_api import Browser, Page, async_playwright |
|
|
|
LOCATION = "Miami, FL" |
|
IPHONES = { |
|
"iPhone 17 Pro - Cosmic Orange - 256GB": "MG7L4LL/A", |
|
"iPhone 17 Pro - Cosmic Orange - 512GB": "MG7P4LL/A", |
|
"iPhone 17 Pro - Deep Blue - 256GB": "MG7M4LL/A", |
|
"iPhone 17 Pro - Deep Blue - 512GB": "MG7Q4LL/A", |
|
"iPhone 17 Pro - Deep Blue - 1TB": "MG7U4LL/A", |
|
} |
|
|
|
|
|
async def start_browser() -> tuple[Browser, Page]: |
|
playwright = await async_playwright().__aenter__() |
|
browser = await playwright.chromium.launch(headless=True) |
|
context = await browser.new_context( |
|
locale="en-US,en;q=0.9,it;q=0.8", no_viewport=True |
|
) |
|
page = await context.new_page() |
|
page.set_default_timeout(15000) |
|
page.set_default_navigation_timeout(15000) |
|
await add_stealth(page) |
|
return browser, page |
|
|
|
|
|
async def add_stealth(page: Page): |
|
await page.add_init_script(r""" |
|
// remove webdriver from prototype (works in many cases) |
|
try { |
|
delete Object.getPrototypeOf(navigator).webdriver; |
|
} catch (e) {} |
|
|
|
// and also define it explicitly to be safe |
|
Object.defineProperty(navigator, 'webdriver', { |
|
get: () => false, |
|
configurable: true |
|
}); |
|
""") |
|
|
|
|
|
def parse_availability(availability: dict, product_id: str): |
|
objs = [] |
|
for store in availability["body"]["content"]["pickupMessage"]["stores"]: |
|
availability = store["partsAvailability"][product_id] |
|
is_buyable = availability["buyability"]["isBuyable"] |
|
quantity = availability["buyability"]["inventory"] |
|
store_name = store["storeName"] |
|
store_city = store["city"] |
|
store_state = store["state"] |
|
distance = store["storedistance"] |
|
|
|
obj = { |
|
"store_name": store_name, |
|
"city": store_city, |
|
"state": store_state, |
|
"is_buyable": is_buyable, |
|
"quantity": quantity, |
|
"distance": distance, |
|
} |
|
objs.append(obj) |
|
objs = sorted(objs, key=lambda x: (x["is_buyable"], -x["distance"])) |
|
return objs |
|
|
|
|
|
def print_stock(objs: list[dict]): |
|
for item in objs: |
|
msg = f"{item['quantity']} in stock @ {item['store_name']} - {item['city']}, {item['state']} - {item['distance']} miles away" |
|
if item["is_buyable"]: |
|
print(f"✅ {msg} ✅") |
|
else: |
|
print(f"❌ {msg} ❌") |
|
|
|
|
|
async def run_stock_check(page: Page): |
|
await page.goto("https://www.apple.com/iphone/", wait_until="networkidle") |
|
assert await page.evaluate("navigator.webdriver") is False |
|
await page.goto( |
|
"https://www.apple.com/shop/buy-iphone/iphone-17-pro/6.3-inch-display-256gb-deep-blue-unlocked", |
|
wait_until="networkidle", |
|
) |
|
|
|
for name, product_id in IPHONES.items(): |
|
print(f"➡️ Checking stock for: {name} ➡️") |
|
response = await get_fulfillment_messages(page, product_id) |
|
|
|
objs = parse_availability(response, product_id) |
|
objs = [x for x in objs if x["is_buyable"] or x["quantity"] > 0] |
|
if not objs: |
|
print(f"❌ No stock found for: {name} ❌") |
|
continue |
|
else: |
|
print_stock(objs) |
|
|
|
|
|
async def main(): |
|
browser, page = await start_browser() |
|
await run_stock_check(page) |
|
await browser.close() |
|
|
|
|
|
async def get_fulfillment_messages(page: Page, product_id: str): |
|
response = await page.request.get( |
|
"https://www.apple.com/shop/fulfillment-messages", |
|
params={ |
|
"fae": "true", |
|
"pl": "true", |
|
"mts.0": "regular", |
|
"mts.1": "compact", |
|
"cppart": "UNLOCKED/US", |
|
"parts.0": product_id, |
|
"location": LOCATION, |
|
}, |
|
) |
|
assert response.status == 200 |
|
return await response.json() |
|
|
|
|
|
if __name__ == "__main__": |
|
asyncio.run(main()) |