Last active
January 24, 2026 03:32
-
-
Save BeepIsla/08007d9fe98f9baeb1f35a4714f54209 to your computer and use it in GitHub Desktop.
Dumps every community server into an SQLite database. Use something like TablePlus to view the database. Requires a recent NodeJS version to run using `node fetch_all_servers_from_steam.ts`. Fetching only CS2 (730 AppID) servers takes under 4 minutes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { DatabaseSync } from "node:sqlite"; | |
| const STEAM_API_KEY = "< STEAM API KEY FROM https://steamcommunity.com/dev/apikey >"; | |
| const TARGET_APP_ID = 730; // 0 = All | |
| // Other configuration you probably don't want to touch | |
| const SERVER_LIST_MAX_LIMIT = 10000; | |
| const INCLUDE_VALVE_SERVERS = false; | |
| const INVALID_CHARS = ["?".charCodeAt(0), "*".charCodeAt(0), "\\".charCodeAt(0)]; // These are used for filtering | |
| const SQLITE_MAX_SERVERS_PER_INSERT = 2000; | |
| const db = new DatabaseSync("db.sqlite"); | |
| let total = 0; | |
| db.prepare( | |
| ` | |
| CREATE TABLE IF NOT EXISTS servers ( | |
| addr TEXT PRIMARY KEY ON CONFLICT IGNORE, | |
| steamid TEXT, | |
| name TEXT, | |
| appid INTEGER, | |
| gamedir TEXT, | |
| version TEXT, | |
| players INTEGER, | |
| max_players INTEGER, | |
| bots INTEGER, | |
| map TEXT, | |
| gametype TEXT | |
| ) STRICT | |
| ` | |
| ).run(); | |
| async function getListInternal(filter: string): Promise< | |
| { | |
| addr: string; | |
| steamid: string; | |
| name: string; | |
| appid: number; | |
| gamedir: string; | |
| version: string; | |
| players: number; | |
| max_players: number; | |
| bots: number; | |
| map: string; | |
| gametype: string; | |
| }[] | |
| > { | |
| const uri = new URL("https://api.steampowered.com/"); | |
| uri.pathname = "IGameServersService/GetServerList/v1"; | |
| uri.searchParams.set("key", STEAM_API_KEY); | |
| uri.searchParams.set("limit", SERVER_LIST_MAX_LIMIT.toString()); | |
| uri.searchParams.set("filter", filter); | |
| const res = await fetch(uri); | |
| if (res.status !== 200) { | |
| throw new Error(`Expected 200 status code but got ${res.status}`); | |
| } | |
| const obj = await res.json(); | |
| return obj?.response?.servers ?? []; | |
| } | |
| async function getList(filter: string): ReturnType<typeof getListInternal> { | |
| filter = `\\name_match\\${filter}\\`; | |
| if (!INCLUDE_VALVE_SERVERS) { | |
| // Exclude Valve servers | |
| filter += "nor\\1\\white\\1\\"; | |
| } | |
| if (TARGET_APP_ID > 0) { | |
| filter += `appid\\${TARGET_APP_ID}\\`; | |
| } | |
| let lastErr; | |
| for (let i = 0; i < 5; i++) { | |
| try { | |
| return await getListInternal(filter); | |
| } catch (err) { | |
| lastErr = err; | |
| await new Promise((p) => setTimeout(p, 5 * 1000)); | |
| } | |
| } | |
| throw lastErr; | |
| } | |
| async function recursiveScan(prefix: string) { | |
| const currentFilter = prefix === "" ? "*" : `${prefix}*`; | |
| console.log(Buffer.from(currentFilter, "ascii").toString("hex")); | |
| const servers = await getList(currentFilter); | |
| if (servers.length <= 0) { | |
| return; | |
| } | |
| if (servers.length < SERVER_LIST_MAX_LIMIT - 100) { | |
| for (let i = 0; i < servers.length; i += SQLITE_MAX_SERVERS_PER_INSERT) { | |
| const chunk = servers.slice(i, i + SQLITE_MAX_SERVERS_PER_INSERT); | |
| if (chunk.length <= 0) { | |
| continue; | |
| } | |
| db.prepare( | |
| ` | |
| INSERT INTO servers ( | |
| addr, | |
| steamid, | |
| name, | |
| appid, | |
| gamedir, | |
| version, | |
| players, | |
| max_players, | |
| bots, | |
| map, | |
| gametype | |
| ) VALUES ${new Array(chunk.length).fill("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ")} | |
| ` | |
| ).run( | |
| ...chunk | |
| .map((s) => [ | |
| // I don't know which one of these can actually be optional | |
| s?.addr ?? "0.0.0.0:0", | |
| s?.steamid ?? "0", | |
| s?.name ?? "", | |
| s?.appid ?? 0, | |
| s?.gamedir ?? "", | |
| s?.version ?? "", | |
| s?.players ?? 0, | |
| s?.max_players ?? 0, | |
| s?.bots ?? 0, | |
| s?.map ?? "", | |
| s?.gametype ?? "" | |
| ]) | |
| .flat() | |
| ); | |
| } | |
| total += servers.length; | |
| console.log(`-> ${servers.length} (Total: ${total})`); | |
| return; | |
| } | |
| // The server browser allows basically anything that is a valid C string | |
| for (let i = 1; i <= 0xff; i++) { | |
| if (INVALID_CHARS.includes(i)) { | |
| continue; | |
| } | |
| await recursiveScan(prefix + String.fromCodePoint(i)); | |
| } | |
| } | |
| async function main() { | |
| const start = Date.now(); | |
| await recursiveScan(""); | |
| const end = Date.now(); | |
| const secs = Math.floor((end - start) / 1000); | |
| console.log(`Took ${secs}s`); | |
| } | |
| main().catch(console.error); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment