- Add 4 version-fetch strategies (UpToDateCheck API, Store Page Scrape, Community Hub Scrape, Store API Heuristic) — no Steam API key needed - Create orchestrator that tries all strategies, prefers named version over build ID - Add test page at /test-version-fetchers for comparing strategies - Add standalone /api/version-test endpoint (direct Steam App ID, no DB) - Update /api/games/:id/steamdb-version to use new strategies - Disable auto-fetch on submit page by default (NEXT_PUBLIC_VERSION_AUTO_FETCH=true to enable) - SteamDB scraping blocked by Cloudflare; strategies preserved for later enablement
141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
/**
|
|
* Strategy 3: Steam Community Hub Scraping
|
|
*
|
|
* Fetches steamcommunity.com/app/{appid} and extracts version/build info
|
|
* from update announcements, embedded data, and the app sidebar.
|
|
*
|
|
* Community hub is less aggressively rate-limited than the store.
|
|
*/
|
|
import type { VersionFetchResult } from "./types"
|
|
|
|
const COMMUNITY_URL = "https://steamcommunity.com/app"
|
|
|
|
export async function fetchCommunityHub(
|
|
steamAppId: number,
|
|
): Promise<VersionFetchResult> {
|
|
const source = "Community Hub Scrape"
|
|
|
|
try {
|
|
const res = await fetch(`${COMMUNITY_URL}/${steamAppId}`, {
|
|
headers: {
|
|
"User-Agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
Accept: "text/html",
|
|
},
|
|
signal: AbortSignal.timeout(10000),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
return {
|
|
versionString: null,
|
|
buildId: null,
|
|
source,
|
|
success: false,
|
|
error: `HTTP ${res.status}`,
|
|
}
|
|
}
|
|
|
|
const html = await res.text()
|
|
|
|
let versionString: string | null = null
|
|
let buildId: string | null = null
|
|
|
|
// ── Extract from embedded community data ──────────────────
|
|
// The community hub embeds JSON in data attributes
|
|
const communityDataMatch = html.match(
|
|
/data-community="([^"]+)"/i,
|
|
)
|
|
if (communityDataMatch) {
|
|
try {
|
|
const decoded = communityDataMatch[1]
|
|
.replace(/"/g, '"')
|
|
.replace(/&/g, "&")
|
|
const data = JSON.parse(decoded)
|
|
// Community data usually has CLANSTEAMID, APPID, IS_OGG, etc.
|
|
// Not version info, but checked for completeness
|
|
if (data.APPID && !buildId) {
|
|
// APPID is just the same app ID — not useful
|
|
}
|
|
} catch {
|
|
// Non-fatal
|
|
}
|
|
}
|
|
|
|
// ── Extract from "View Stats" link ────────────────────────
|
|
// Sometimes game stats pages have more info
|
|
|
|
// ── Parse update announcements for version numbers ────────
|
|
// Look for patterns like "Counter-Strike 2 Update" or "Game Update 1.4.2"
|
|
const updateTitlePatterns = [
|
|
// "Game Name Update" — generic, skip
|
|
// "Update 1.4.2" pattern
|
|
/(?:Update|Patch)\s+(\d+\.\d+(?:\.\d+)?(?:[a-z]\d*)?)/gi,
|
|
// "Version 1.4.2" pattern
|
|
/Version\s+(\d+\.\d+(?:\.\d+)?(?:[a-z]\d*)?)/gi,
|
|
// "Build 12345678" pattern
|
|
/Build\s+(\d{5,10})/gi,
|
|
]
|
|
|
|
for (const pattern of updateTitlePatterns) {
|
|
const match = pattern.exec(html)
|
|
if (match) {
|
|
const val = match[1]
|
|
if (/^\d{5,10}$/.test(val)) {
|
|
if (!buildId) buildId = val
|
|
} else if (/^\d+\.\d+/.test(val)) {
|
|
if (!versionString) versionString = val
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Extract build ID from JS globals ──────────────────────
|
|
const buildTimestampMatch = html.match(
|
|
/"BUILD_TIMESTAMP"[:\s]+(\d{9,10})/,
|
|
)
|
|
if (buildTimestampMatch && !buildId) {
|
|
// BUILD_TIMESTAMP is the page build time, not game build
|
|
// But if we have nothing else, it's a signal
|
|
// Skipping — too noisy
|
|
}
|
|
|
|
// ── Try apphub sidebar for build/version info ──────────────
|
|
const apphubMatch = html.match(
|
|
/apphub_AppInfo[^>]*>([\s\S]*?)<\/div>/i,
|
|
)
|
|
if (apphubMatch) {
|
|
const apphubHtml = apphubMatch[1]
|
|
// Look for "Build ID" or "Current version" labels
|
|
const buildLabelMatch = apphubHtml.match(
|
|
/Build\s*ID[:\s]*<\/span>\s*([\d,]+)/i,
|
|
)
|
|
if (buildLabelMatch && !buildId) {
|
|
buildId = buildLabelMatch[1].replace(/,/g, "")
|
|
}
|
|
|
|
const versionLabelMatch = apphubHtml.match(
|
|
/(?:Current|Latest)\s*Version[:\s]*<\/span>\s*([\d.]+)/i,
|
|
)
|
|
if (versionLabelMatch && !versionString) {
|
|
versionString = versionLabelMatch[1]
|
|
}
|
|
}
|
|
|
|
const success = !!(versionString || buildId)
|
|
return {
|
|
versionString,
|
|
buildId,
|
|
source,
|
|
success,
|
|
error: success ? undefined : "No version data found on community hub",
|
|
}
|
|
} catch (err) {
|
|
return {
|
|
versionString: null,
|
|
buildId: null,
|
|
source,
|
|
success: false,
|
|
error: err instanceof Error ? err.message : "Unknown error",
|
|
}
|
|
}
|
|
}
|