"""gta6-listing-audit.py - re-check what Take-Two and the two console stores say about Grand Theft Auto VI before its November 2026 launch. The article that ships with this file was written on 9 October 2026 from Take-Two Interactive's own releases and from the PlayStation Store and Microsoft Store listings: the launch date, the platforms, the two editions and their prices, and the age labels the stores display. Pre-order listings change, so this script re-reads the same public pages, prints the same tables, converts each store's release timestamp into the local clock of its market, and marks every figure that no longer matches the one the article printed. What it reads, all public and none of it needing a key: Take-Two www.take2games.com/ir/news/: the 24 June 2026 pre-order release and the 7 August 2026 results release, whose "future lineup" table has one row for the game PlayStation store.playstation.com concept page 10000730 (en-us and en-gb); the release timestamp, edition prices and displayed age label sit in the page's embedded Apollo cache, read with the json module Microsoft displaycatalog.mp.microsoft.com, the catalogue behind the Microsoft Store: 9P3H4968GRSM (the game), 9NNZSNHLR63L (Ultimate Edition) and 9PN4LLBR8RCH (the Ultimate Edition Upgrade sold on its own), in the US, UK, German and Japanese markets Every request names this script in its User-Agent header. Standard library only (urllib, json, re, html, datetime, zoneinfo). Python 3.9 or newer; on Windows the local-time column needs the tzdata package and is left blank without it. python gta6-listing-audit.py Exit code is 0 when every page answered, 1 when any of them did not. """ from __future__ import annotations import datetime import html import json import re import sys import time import urllib.error import urllib.request UA = {"User-Agent": "gta6-listing-audit/1.0 (python-urllib; https://blastproc.com/code/gta6-listing-audit.py)", "Accept-Language": "en-US,en;q=0.9"} TAKE_TWO = "https://www.take2games.com/ir/news/" PREORDER_RELEASE = "rockstar-games-announces-pre-orders-grand-theft-auto-vi" RESULTS_RELEASE = "take-two-interactive-software-inc-reports-results-fiscal-first-6" PS_CONCEPT = 10000730 PS_LOCALES = (("US", "en-us"), ("UK", "en-gb")) MS_PRODUCTS = (("standard", "9P3H4968GRSM"), ("ultimate", "9NNZSNHLR63L"), ("upgrade", "9PN4LLBR8RCH")) MS_MARKETS = (("US", "US", "en-us", "ESRB"), ("UK", "GB", "en-gb", "PEGI"), ("DE", "DE", "de-de", "USK"), ("JP", "JP", "ja-jp", "CERO")) CLOCKS = {"US": ("New York", "America/New_York"), "UK": ("London", "Europe/London"), "DE": ("Berlin", "Europe/Berlin"), "JP": ("Tokyo", "Asia/Tokyo")} # what the article printed, read on 9 October 2026 PRINTED = { "take_two_date": "November 19, 2026", "take_two_prices": ["$79.99", "$99.99"], "lineup_row": "PS5, Xbox Series X|S November 19, 2026", ("PlayStation", "US"): {"release": "2026-11-19T05:00:00Z", "standard": "79.99", "ultimate": "99.99", "label": "ESRB Mature 17+"}, ("PlayStation", "UK"): {"release": "2026-11-19T00:00:00Z", "standard": "69.99", "ultimate": "89.99", "label": "PEGI 18"}, ("Microsoft", "US"): {"release": "2026-11-19T05:00:00Z", "standard": "79.99", "ultimate": "99.99", "upgrade": "20.00", "label": "ESRB:M"}, ("Microsoft", "UK"): {"release": "2026-11-19T00:00:00Z", "standard": "69.99", "ultimate": "89.99", "upgrade": "20.00", "label": "PEGI:18"}, ("Microsoft", "DE"): {"release": "2026-11-18T23:00:00Z", "standard": "79.99", "ultimate": "99.99", "upgrade": "20.00", "label": "USK:RP"}, ("Microsoft", "JP"): {"release": "2026-11-18T15:00:00Z", "standard": "9800", "ultimate": "12280", "upgrade": "2480", "label": "CERO:RP"}, } FAILED = [] CHANGED = [] def get(url: str) -> str | None: time.sleep(1.5) # one request at a time, spaced out try: req = urllib.request.Request(url, headers=UA) with urllib.request.urlopen(req, timeout=60) as r: return r.read().decode("utf-8", "replace") except (urllib.error.URLError, TimeoutError) as e: FAILED.append(f"{url}: {e}") return None def plain(page: str) -> str: page = re.sub(r"||", " ", page, flags=re.S) return re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", " ", page))) def take_two() -> dict: out = {} page = get(TAKE_TWO + PREORDER_RELEASE) if page: t = plain(page) m = re.search(r"Launching ([A-Z][a-z]+ \d{1,2}, \d{4})", t) out["date"] = m.group(1) if m else None out["prices"] = sorted(set(re.findall(r"\$\d+\.\d\d", t))) out["not_yet_rated"] = "is not yet rated" in t page = get(TAKE_TWO + RESULTS_RELEASE) if page: t = plain(page) m = re.search(r"Grand Theft Auto VI (PS5, Xbox Series X\|S [A-Z][a-z]+ \d{1,2}, \d{4})", t) out["lineup_row"] = m.group(1) if m else None return out def apollo(page: str) -> dict: """Merge every Apollo cache found in the page's JSON script blocks.""" merged = {} for block in re.findall(r']*type="application/json"[^>]*>(.*?)', page, flags=re.S): try: stack = [json.loads(block)] except ValueError: continue while stack: o = stack.pop() if isinstance(o, dict): if isinstance(o.get("cache"), dict): for k, v in o["cache"].items(): if isinstance(v, dict) and isinstance(merged.get(k), dict): merged[k].update(v) else: merged[k] = v stack.extend(o.values()) elif isinstance(o, list): stack.extend(o) return merged def playstation(locale: str) -> dict | None: page = get(f"https://store.playstation.com/{locale}/concept/{PS_CONCEPT}") if not page: return None c = apollo(page) concept = c.get(f"Concept:{PS_CONCEPT}", {}) out = {"release": (concept.get("releaseDate") or {}).get("value"), "label": (concept.get("contentRating") or {}).get("description"), "descriptors": [d.get("description") for d in (concept.get("contentRating") or {}).get("descriptors") or []]} for key, v in c.items(): if key.startswith("GameCTA:") and v.get("price"): price = re.sub(r"[^\d.]", "", v["price"].get("basePrice") or "") if "GTAVISTANDARD001" in key: out["standard"] = price elif "GTAVIULTIMATE001" in key: out["ultimate"] = price return out def microsoft(pid: str, market: str, lang: str, board: str) -> dict | None: page = get(f"https://displaycatalog.mp.microsoft.com/v7.0/products/{pid}?market={market}" f"&languages={lang}&fieldsTemplate=details") if not page: return None p = json.loads(page)["Product"] mp = p["MarketProperties"][0] out = {"release": mp.get("OriginalReleaseDate"), "label": None, "price": None} for r in mp.get("ContentRatings", []): if r.get("RatingSystem") == board: out["label"] = r.get("RatingId") for sku in p.get("DisplaySkuAvailabilities", []): for a in sku.get("Availabilities", []): if "Purchase" in a.get("Actions", []): pr = a["OrderManagementData"]["Price"] out["price"] = f"{pr['ListPrice']:.0f}" if pr["CurrencyCode"] == "JPY" else f"{pr['ListPrice']:.2f}" out["currency"] = pr["CurrencyCode"] return out def utc(stamp: str | None) -> datetime.datetime | None: if not stamp: return None s = re.sub(r"\.\d+Z$", "Z", stamp).replace("Z", "+00:00") return datetime.datetime.fromisoformat(s) def local(stamp: str | None, mkt: str) -> str: when = utc(stamp) if when is None: return "" try: from zoneinfo import ZoneInfo city, zone = CLOCKS[mkt] t = when.astimezone(ZoneInfo(zone)) return f"{t:%a %d %b %H:%M} {city}" except Exception: # noqa: BLE001 (no tzdata on this machine) return "" def same_moment(a: str | None, b: str | None) -> bool: return utc(a) is not None and utc(a) == utc(b) def check(name: str, got, want): if got != want: CHANGED.append(f"{name}: now {got!r}, the article printed {want!r}") return "CHANGED" return "same" def main() -> int: print("Grand Theft Auto VI: Take-Two and the console stores, re-read", datetime.date.today().isoformat()) print() t2 = take_two() print(f"Take-Two, 24 June 2026 release: launching {t2.get('date')}, prices {', '.join(t2.get('prices') or [])}" f" ({check('Take-Two date', t2.get('date'), PRINTED['take_two_date'])}," f" {check('Take-Two prices', t2.get('prices'), PRINTED['take_two_prices'])})") print(f"Take-Two, 7 August 2026 lineup row: {t2.get('lineup_row')}" f" ({check('lineup row', t2.get('lineup_row'), PRINTED['lineup_row'])})") print() print(f"{'Store':<12} {'Mkt':<4} {'Release (UTC)':<22} {'Local time':<26} {'Game':>8} {'Ultimate':>9} {'Upgrade':>8} Age label") for mkt, locale in PS_LOCALES: ps = playstation(locale) if ps is None: continue want = PRINTED[("PlayStation", mkt)] print(f"{'PlayStation':<12} {mkt:<4} {ps['release'] or '':<22} {local(ps['release'], mkt):<26} " f"{ps.get('standard', ''):>8} {ps.get('ultimate', ''):>9} {'':>8} {ps['label']}: {', '.join(ps['descriptors'])}") check(f"PlayStation {mkt} release", same_moment(ps["release"], want["release"]), True) for k in ("standard", "ultimate", "label"): check(f"PlayStation {mkt} {k}", ps.get(k), want[k]) for mkt, market, lang, board in MS_MARKETS: rows = {k: microsoft(pid, market, lang, board) for k, pid in MS_PRODUCTS} if any(v is None for v in rows.values()): continue want = PRINTED[("Microsoft", mkt)] rel = rows["standard"]["release"] print(f"{'Microsoft':<12} {mkt:<4} {(utc(rel).strftime('%Y-%m-%dT%H:%M:%SZ') if rel else ''):<22} {local(rel, mkt):<26} " f"{rows['standard']['price'] or '':>8} {rows['ultimate']['price'] or '':>9} {rows['upgrade']['price'] or '':>8} " f"{rows['standard']['label']} ({rows['standard'].get('currency', '')})") check(f"Microsoft {mkt} release", same_moment(rel, want["release"]), True) for k in ("standard", "ultimate", "upgrade"): check(f"Microsoft {mkt} {k}", rows[k]["price"], want[k]) check(f"Microsoft {mkt} label", rows["standard"]["label"], want["label"]) print() if CHANGED: print("Changed since the article was written:") for c in CHANGED: print(" " + c) else: print("Every figure above matches the article.") for f in FAILED: print("FAILED:", f) return 1 if FAILED else 0 if __name__ == "__main__": sys.exit(main())