- Backend: ?force=true on /proxy bypasses cache for fresh link fetch - Frontend: parse `se` param for live expiry countdown on download links - Frontend: show Refresh button when link expires in <6h, force-fetches fresh URL - Frontend: CliCommand component replaces Aria2Tip — wget/curl/aria2 tabs, persists selection in localStorage - Frontend: RecentlyViewed component — localStorage, shows active countdown or expired state - Frontend: FileSize shown alongside arch if returned by Microsoft CDN Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
5.4 KiB
Python
158 lines
5.4 KiB
Python
import argparse
|
|
import json
|
|
import logging
|
|
import uuid
|
|
import time
|
|
import requests
|
|
|
|
# New v3 JSON Endpoints
|
|
BASE_URL = "https://www.microsoft.com/software-download-connector/api"
|
|
PROFILE = "606624d44113"
|
|
LOCALE = "en-US"
|
|
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
|
|
def setup_session():
|
|
"""Initializes a session with Microsoft tracking servers."""
|
|
session_id = str(uuid.uuid4())
|
|
return session_id
|
|
|
|
def get_product(product_id, session_id):
|
|
"""Fetches product/SKU info for a given ID."""
|
|
url = f"{BASE_URL}/getskuinformationbyproductedition"
|
|
params = {
|
|
"profile": PROFILE,
|
|
"productEditionId": product_id,
|
|
"SKU": "undefined",
|
|
"friendlyFileName": "undefined",
|
|
"Locale": LOCALE,
|
|
"sessionID": session_id
|
|
}
|
|
headers = {
|
|
"User-Agent": UA,
|
|
"Accept": "application/json",
|
|
"Referer": "https://www.microsoft.com/en-us/software-download/windows11"
|
|
}
|
|
|
|
try:
|
|
r = requests.get(url, params=params, headers=headers, timeout=10)
|
|
if not r.ok: return None
|
|
|
|
# Handle Microsoft's double-encoded JSON
|
|
raw_text = r.text
|
|
try:
|
|
# First pass: decode the wrapper JSON
|
|
data = json.loads(raw_text)
|
|
# Second pass: if the value is a string, it's the real JSON
|
|
if isinstance(data, str):
|
|
data = json.loads(data)
|
|
return data
|
|
except:
|
|
return None
|
|
except Exception as e:
|
|
logging.error(f"Error fetching ID {product_id}: {e}")
|
|
return None
|
|
|
|
def scan_id(product_id):
|
|
"""Checks if a product ID is valid and active."""
|
|
session_id = setup_session()
|
|
data = get_product(product_id, session_id)
|
|
|
|
if not data or "Skus" not in data or len(data["Skus"]) == 0:
|
|
return None
|
|
|
|
if "Errors" in data and data["Errors"] and len(data["Errors"]) > 0:
|
|
return None
|
|
|
|
# MS usually puts the release name in the first SKU
|
|
skus = data.get("Skus", [])
|
|
if skus:
|
|
# Check multiple possible name keys
|
|
s = skus[0]
|
|
# ReleaseName is often "Windows 10 Version 22H2 (Updated Oct 2025)"
|
|
name = s.get("EditionName") or s.get("ReleaseName") or s.get("FriendlyName")
|
|
if name:
|
|
return name
|
|
|
|
return f"Windows Product {product_id}"
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Scan for new Windows Product IDs (v3 JSON API)")
|
|
parser.add_argument("--first", required=True, type=int, help="First ID to check")
|
|
parser.add_argument("--last", required=True, type=int, help="Last ID to check")
|
|
parser.add_argument("--write", help="Output JSON file")
|
|
args = parser.parse_args()
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
|
products = {}
|
|
|
|
logging.info(f"Scanning range {args.first} to {args.last}...")
|
|
|
|
for i in range(args.first, args.last + 1):
|
|
name = scan_id(i)
|
|
if name:
|
|
logging.info(f"✅ FOUND: [{i}] {name}")
|
|
products[str(i)] = name
|
|
else:
|
|
# Only log skips for very small ranges to keep output clean
|
|
if (args.last - args.first) < 20:
|
|
logging.info(f"❌ SKIP: [{i}] Not found")
|
|
products[str(i)] = False
|
|
|
|
# Be gentle to the API
|
|
time.sleep(0.5)
|
|
|
|
if args.write:
|
|
import os
|
|
catalog = {}
|
|
if os.path.exists(args.write):
|
|
try:
|
|
with open(args.write, 'r', encoding='utf-8') as f:
|
|
catalog = json.load(f)
|
|
except Exception as e:
|
|
logging.error(f"Failed to read existing catalog: {e}")
|
|
|
|
# Merge new products, preserving existing extra metadata if present
|
|
for pid, name in products.items():
|
|
if pid in catalog:
|
|
if name is False:
|
|
if isinstance(catalog[pid], dict):
|
|
catalog[pid]["active"] = False
|
|
else:
|
|
catalog[pid] = {
|
|
"name": catalog[pid],
|
|
"badge": "",
|
|
"archs": [],
|
|
"related": [],
|
|
"active": False
|
|
}
|
|
continue
|
|
|
|
# Update name but keep other fields like badge, archs, etc.
|
|
if isinstance(catalog[pid], dict):
|
|
catalog[pid]["name"] = name
|
|
catalog[pid]["active"] = True
|
|
else:
|
|
# Upgrade from old flat format
|
|
catalog[pid] = {
|
|
"name": name,
|
|
"badge": "",
|
|
"archs": [],
|
|
"related": [],
|
|
"active": True
|
|
}
|
|
else:
|
|
if name is False:
|
|
continue # Do not add new dead entries to the catalog
|
|
catalog[pid] = {
|
|
"name": name,
|
|
"badge": "",
|
|
"archs": [],
|
|
"related": [],
|
|
"active": True
|
|
}
|
|
|
|
with open(args.write, 'w', encoding='utf-8') as f:
|
|
json.dump(catalog, f, indent=4)
|
|
logging.info(f"\nSaved {len(catalog)} products to {args.write} (merged with existing)")
|
|
|
|
logging.info("Done.")
|