From 37337c630c3beb08cf6d8174cb646668ba04f1bc Mon Sep 17 00:00:00 2001 From: seanbetts Date: Sun, 10 May 2026 21:26:46 +0100 Subject: [PATCH] Monitor Valve reservation packages --- scripts/check_valve_endpoints.sh | 133 +++++++++++++++++++++++++++- tests/test_check_valve_endpoints.py | 123 +++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 4 deletions(-) create mode 100644 tests/test_check_valve_endpoints.py diff --git a/scripts/check_valve_endpoints.sh b/scripts/check_valve_endpoints.sh index e528595..d74e8ac 100755 --- a/scripts/check_valve_endpoints.sh +++ b/scripts/check_valve_endpoints.sh @@ -14,6 +14,7 @@ mkdir -p "$OUT_DIR" "$REPORT_DIR" ERROR_FILE="$REPORT_DIR/valve-errors.txt" : > "$ERROR_FILE" : > "$REPORT_DIR/valve-key-lines.txt" +: > "$REPORT_DIR/valve-reservation-packages.tsv" fetch_page() { name="$1" @@ -25,15 +26,139 @@ fetch_page() { fi } +fetch_json() { + name="$1" + url="$2" + if ! curl -A 'Mozilla/5.0' --retry 2 --retry-delay 2 --max-time 45 -fsSL "$url" > "$OUT_DIR/$name.json" 2>/dev/null; then + printf '%s\n' "failed to fetch $url" >> "$ERROR_FILE" + rm -f "$OUT_DIR/$name.json" + return 1 + fi +} + fetch_page "steamdeck-site" "https://www.steamdeck.com/" || true fetch_page "steam-support-deck" "https://help.steampowered.com/en/wizard/HelpWithSteamDeck" || true fetch_page "steam-controller-store" "https://store.steampowered.com/app/4165870/" || true fetch_page "steam-hardware-root" "https://steampowered.com/hardware" || true -rg -n "Steam Controller|Steam Machine|Steam Frame|coming soon|manual|safety|warranty|preorder|release" \ - "$OUT_DIR" \ - > "$REPORT_DIR/valve-key-lines.txt" || true +for appid in 4165870 4165890 4165910; do + fetch_json "appdetails-$appid-us" "https://store.steampowered.com/api/appdetails?appids=$appid&cc=us&l=en" || true +done + +for packageid in 1558609 1629446 1629447 1629458 1629460 1629484 1629486; do + fetch_json "packagedetails-$packageid-us" "https://store.steampowered.com/api/packagedetails?packageids=$packageid&cc=us&l=en" || true +done + +python3 - "$OUT_DIR" "$REPORT_DIR/valve-reservation-packages.tsv" <<'PY' +import json +import sys +from pathlib import Path + + +out_dir = Path(sys.argv[1]) +report_path = Path(sys.argv[2]) + +apps = { + "4165870": "Steam Controller", + "4165890": "Steam Frame", + "4165910": "Steam Machine", +} +packages = { + "1558609": "Steam Controller", + "1629446": "Steam Machine", + "1629447": "Steam Machine", + "1629458": "Steam Machine", + "1629460": "Steam Machine", + "1629484": "Steam Frame", + "1629486": "Steam Frame", +} + + +def clean(value): + if value is None: + return "" + return str(value).replace("\t", " ").replace("\n", " ").strip() + + +def read_json(path): + if not path.exists(): + return None, "missing" + try: + return json.loads(path.read_text(encoding="utf-8")), "" + except Exception as exc: + return None, f"invalid json: {exc}" + + +rows = [["type", "product", "id", "status", "name", "release_or_price", "details"]] + +for appid, product in apps.items(): + payload, error = read_json(out_dir / f"appdetails-{appid}-us.json") + entry = payload.get(appid, {}) if isinstance(payload, dict) else {} + success = entry.get("success") is True + data = entry.get("data") if isinstance(entry.get("data"), dict) else {} + status = "public" if success else "unavailable" + if error: + status = error + name = data.get("name", "") + price = "" + price_overview = data.get("price_overview") + if isinstance(price_overview, dict): + price = price_overview.get("final_formatted", "") + release_date = data.get("release_date") + release_or_price = price + coming_soon = "" + if isinstance(release_date, dict): + release_or_price = release_or_price or release_date.get("date", "") + coming_soon = release_date.get("coming_soon", "") + package_ids = data.get("packages") if isinstance(data.get("packages"), list) else [] + package_groups = data.get("package_groups") if isinstance(data.get("package_groups"), list) else [] + details = ( + f"packages={','.join(str(package_id) for package_id in package_ids)}; " + f"package_groups={len(package_groups)}; " + f"coming_soon={coming_soon}; " + f"price={price}" + ) + rows.append(["app", product, appid, status, name, release_or_price, details]) + +for packageid, product in packages.items(): + payload, error = read_json(out_dir / f"packagedetails-{packageid}-us.json") + entry = payload.get(packageid, {}) if isinstance(payload, dict) else {} + success = entry.get("success") is True + status = "public" if success else "private" + if error: + status = error + data = entry.get("data") if isinstance(entry.get("data"), dict) else {} + name = data.get("name", "") + price = "" + package_price = data.get("price") + if isinstance(package_price, dict): + price = package_price.get("final_formatted", "") + apps_detail = [] + for app in data.get("apps", []) if isinstance(data.get("apps"), list) else []: + if isinstance(app, dict): + app_id = app.get("id", "") + app_name = app.get("name", "") + apps_detail.append(f"{app_id}:{app_name}") + details = f"apps={','.join(apps_detail)}" if success else "packagedetails success:false" + rows.append(["package", product, packageid, status, name, price, details]) + +report_path.write_text( + "\n".join("\t".join(clean(value) for value in row) for row in rows) + "\n", + encoding="utf-8", +) +PY + +{ + printf '%s\n' "Reservation package API snapshot:" + sed -n '1,40p' "$REPORT_DIR/valve-reservation-packages.tsv" + printf '\n%s\n' "HTML key matches:" +} > "$REPORT_DIR/valve-key-lines.txt" + +rg -n -o -g '*.html' "Steam Controller|Steam Machine|Steam Frame|coming soon|manual|safety|warranty|preorder|release" \ + "$OUT_DIR" | sort -u \ + >> "$REPORT_DIR/valve-key-lines.txt" || true printf '%s\n' \ "Saved Valve pages to $OUT_DIR" \ - "Saved matching lines to $REPORT_DIR/valve-key-lines.txt" + "Saved matching lines to $REPORT_DIR/valve-key-lines.txt" \ + "Saved reservation package report to $REPORT_DIR/valve-reservation-packages.tsv" diff --git a/tests/test_check_valve_endpoints.py b/tests/test_check_valve_endpoints.py new file mode 100644 index 0000000..335da34 --- /dev/null +++ b/tests/test_check_valve_endpoints.py @@ -0,0 +1,123 @@ +import os +import stat +import subprocess +import textwrap +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory + + +ROOT = Path(__file__).resolve().parents[1] + + +def write_executable(path, content): + path.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8") + path.chmod(path.stat().st_mode | stat.S_IXUSR) + + +class CheckValveEndpointsTests(unittest.TestCase): + def test_fetches_hardware_app_and_reservation_package_details(self): + tmp = TemporaryDirectory() + self.addCleanup(tmp.cleanup) + tmp_path = Path(tmp.name) + run_dir = tmp_path / "run" + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + + write_executable( + fake_bin / "curl", + """ + #!/bin/sh + for arg in "$@"; do + url="$arg" + done + + case "$url" in + *appdetails*appids=4165870*) + printf '%s\\n' '{"4165870":{"success":true,"data":{"name":"Steam Controller","release_date":{"date":"May 4, 2026"},"packages":[1558609],"price_overview":{"final_formatted":"$99.00"},"package_groups":[{"name":"default","subs":[{"packageid":1558609}]}]}}}' + ;; + *appdetails*appids=4165890*) + printf '%s\\n' '{"4165890":{"success":true,"data":{"name":"Steam Frame","release_date":{"coming_soon":true,"date":"Coming soon"},"package_groups":[]}}}' + ;; + *appdetails*appids=4165910*) + printf '%s\\n' '{"4165910":{"success":true,"data":{"name":"Steam Machine","release_date":{"coming_soon":true,"date":"Coming soon"},"package_groups":[]}}}' + ;; + *packagedetails*packageids=1558609*) + printf '%s\\n' '{"1558609":{"success":true,"data":{"name":"Steam Controller","apps":[{"id":4165870,"name":"Steam Controller"}],"price":{"final_formatted":"$99.00"}}}}' + ;; + *packagedetails*packageids=*) + package="${url##*=}" + printf '{"%s":{"success":false}}\\n' "$package" + ;; + *) + printf '%s\\n' 'Steam Frame Steam Machine Steam Controller coming soon' + ;; + esac + """, + ) + + write_executable( + fake_bin / "rg", + """ + #!/bin/sh + exit 1 + """, + ) + + env = os.environ.copy() + env.update({"PATH": f"{fake_bin}:{env['PATH']}"}) + + result = subprocess.run( + [str(ROOT / "scripts" / "check_valve_endpoints.sh"), str(run_dir)], + cwd=ROOT, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + self.assertEqual("", result.stderr) + self.assertEqual(0, result.returncode) + + valve_dir = run_dir / "api" / "valve" + for appid in ("4165870", "4165890", "4165910"): + self.assertTrue((valve_dir / f"appdetails-{appid}-us.json").exists()) + for packageid in ( + "1558609", + "1629446", + "1629447", + "1629458", + "1629460", + "1629484", + "1629486", + ): + self.assertTrue((valve_dir / f"packagedetails-{packageid}-us.json").exists()) + + report = (run_dir / "reports" / "valve-reservation-packages.tsv").read_text( + encoding="utf-8" + ) + self.assertIn( + "package\tSteam Controller\t1558609\tpublic\tSteam Controller\t$99.00", + report, + ) + self.assertIn( + "package\tSteam Machine\t1629446\tprivate\t\t\tpackagedetails success:false", + report, + ) + self.assertIn( + "package\tSteam Frame\t1629484\tprivate\t\t\tpackagedetails success:false", + report, + ) + self.assertIn("app\tSteam Machine\t4165910\tpublic\tSteam Machine\tComing soon", report) + self.assertIn("app\tSteam Frame\t4165890\tpublic\tSteam Frame\tComing soon", report) + + key_lines = (run_dir / "reports" / "valve-key-lines.txt").read_text( + encoding="utf-8" + ) + self.assertIn("Reservation package API snapshot:", key_lines) + self.assertIn("package\tSteam Frame\t1629486\tprivate", key_lines) + + +if __name__ == "__main__": + unittest.main()