steam appid */ public const APP_IDS = [ 'age-of-empires-iv' => 1466860, 'alan-wake-2' => 2379780, 'balan-wonderworld' => 1336480, 'baldurs-gate-3' => 1086940, 'cities-skylines-ii' => 949230, 'control' => 870780, 'counter-strike-2' => 730, 'cuphead' => 268910, 'cyberpunk-2077' => 1091500, 'dark-souls-iii' => 374320, 'dead-cells' => 588650, 'death-stranding' => 1190460, 'deep-rock-galactic' => 548430, 'diablo-iv' => 2344520, 'disco-elysium' => 632470, 'dishonored-2' => 403640, 'divinity-original-sin-2' => 435150, 'doom-eternal' => 782330, 'ea-sports-fc-24' => 2195250, 'efootball-2024' => 2349380, 'elden-ring' => 1245620, 'enshrouded' => 1203620, 'factorio' => 427520, 'forza-horizon-5' => 1551360, 'god-of-war' => 1593500, 'grand-theft-auto-v' => 271590, 'hades-ii' => 1145350, 'helldivers-2' => 553850, 'hogwarts-legacy' => 990080, 'hollow-knight' => 367520, 'horizon-forbidden-west' => 2420110, 'it-takes-two' => 1426210, 'kingdom-come-deliverance' => 379430, 'lethal-company' => 1966720, 'manor-lords' => 1363080, 'marvels-spider-man-2' => 2651290, 'marvels-spider-man-remastered' => 1817070, 'metro-exodus' => 412020, 'microsoft-flight-simulator' => 1250410, 'monster-hunter-rise' => 1446780, 'nier-automata' => 524220, 'ori-and-the-will-of-the-wisps' => 1057090, 'outer-wilds' => 753640, 'palworld' => 1623730, 'persona-5-royal' => 1687950, 'ready-or-not' => 1144200, 'red-dead-redemption-2' => 1174180, 'resident-evil-4' => 2050650, 'rimworld' => 294100, 'sekiro-shadows-die-twice' => 814380, 'slay-the-spire' => 646570, 'sonic-frontiers' => 1237320, 'stardew-valley' => 413150, 'starfield' => 1716740, 'street-fighter-6' => 1364780, 'terraria' => 105600, 'the-last-of-us-part-i' => 1888930, 'the-witcher-3-wild-hunt' => 292030, 'total-war-warhammer-iii' => 1142710, 'valheim' => 892970, 'warhammer-40000-darktide' => 1361210, ]; private const CDN = 'https://cdn.cloudflare.steamstatic.com/steam/apps/%d/'; private const OUT_DIR = WRITEPATH . '../public/img/covers/'; private const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'; public function run(array $params): void { $games = Mongo::collection('games')->find()->toArray(); if (! is_dir(self::OUT_DIR)) { mkdir(self::OUT_DIR, 0777, true); } $ok = 0; $fail = []; $skipped = 0; foreach ($games as $game) { $slug = $game['slug']; $appid = self::APP_IDS[$slug] ?? null; if ($appid === null) { $skipped++; continue; } $filename = $slug . '.jpg'; $dest = self::OUT_DIR . $filename; $downloaded = $this->download( sprintf(self::CDN, $appid) . 'header.jpg', $dest ); if (! $downloaded) { // try the capsule variant as a fallback $downloaded = $this->download( sprintf(self::CDN, $appid) . 'capsule_616x353.jpg', $dest ); } if ($downloaded) { Mongo::collection('games')->updateOne( ['slug' => $slug], ['$set' => ['cover' => ['file' => '/img/covers/' . $filename, 'from' => '#000000', 'to' => '#000000']]] ); $ok++; CLI::write(' ✓ ' . $slug . ' (' . $appid . ')', 'green'); } else { $fail[] = $slug; CLI::write(' ✗ ' . $slug . ' (' . $appid . ')', 'red'); } usleep(120000); // be gentle with the CDN } CLI::write(''); CLI::write(sprintf('Downloaded %d covers, skipped %d (no appid), failed %d.', $ok, $skipped, count($fail)), 'cyan'); if ($fail !== []) { CLI::write('Failed: ' . implode(', ', $fail), 'yellow'); } } private function download(string $url, string $dest): bool { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_FOLLOWLOCATION => true, CURLOPT_USERAGENT => self::UA, CURLOPT_SSL_VERIFYPEER => true, ]); $body = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $type = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); if ($code !== 200 || $body === false || strlen($body) < 1000) { return false; } // ensure it's an image if (! str_starts_with($type, 'image/')) { return false; } return file_put_contents($dest, $body) !== false; } }