vlr-stats-scraper 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,73 +1,170 @@
1
- # vlr-stats-scraper
2
-
3
- Scrapes player stats from [vlr.gg](https://www.vlr.gg) and stores them in a local SQLite
4
- database, grouped by tournament or stage, so you can pull a backlog of players.
5
-
6
- ## Setup
7
-
8
- Requires a [Hyperbrowser](https://hyperbrowser.ai) API key for scraping.
9
-
10
- ```bash
11
- npm install vlr-stats-scraper
12
- ```
13
-
14
- Create a `.env` file in the directory you run it from:
15
-
16
- ```
17
- HYPERBROWSER_API_KEY=your_key_here
18
- ```
19
-
20
- ## CLI usage
21
-
22
- ```bash
23
- npx valstats <link> <year> <event-name>
24
- ```
25
-
26
- ```bash
27
- npx valstats https://www.vlr.gg/event/2977/vct-2026-americas-stage-2/group-stage 2026 "Stage 2"
28
- ```
29
-
30
- - `link` — the vlr.gg event/group-stage page to scrape.
31
- - `year` — the year the event took place.
32
- - `event-name` — a label for this tournament/stage (used to group players together later).
33
-
34
- Each player on that event page gets scraped and saved to `players.db` in your current
35
- directory, linked to that event name.
36
-
37
- ## Library usage
38
-
39
- ```js
40
- import {
41
- scrapePlayer,
42
- scrapeWebsite,
43
- savePlayer,
44
- saveEvent,
45
- addPlayerToEvent,
46
- getPlayersByEvent,
47
- getRandomPlayerFromEvent,
48
- } from 'vlr-stats-scraper';
49
-
50
- const links = await scrapeWebsite('https://www.vlr.gg/event/.../group-stage');
51
- const player = await scrapePlayer(links[0], 'AMER');
52
- savePlayer(player);
53
- ```
54
-
55
- ### Exports
56
-
57
- - `scrapePlayer(url, region)` — scrapes one player's profile page.
58
- - `scrapeWebsite(url)` — scrapes an event page for player profile links.
59
- - `savePlayer(player)` / `getPlayer(name)` / `getAllPlayers()` / `getRandomPlayer()`
60
- - `saveEvent({ name, url, region, year, stage })` — creates/updates a tournament or stage,
61
- returns its id.
62
- - `addPlayerToEvent(eventId, playerName)`
63
- - `getEvents()` / `getEventByName(name)`
64
- - `getPlayersByEvent(eventName)` / `getRandomPlayerFromEvent(eventName)`
65
-
66
- ## Data storage
67
-
68
- Player and event data is stored in a `players.db` SQLite file created in whichever
69
- directory you run the tool from.
70
-
71
- ## License
72
-
73
- ISC
1
+ # vlr-stats-scraper
2
+
3
+ Scrapes player stats from [vlr.gg](https://www.vlr.gg) and stores them in a local SQLite
4
+ database, grouped by tournament or stage, so you can pull a backlog of players.
5
+
6
+ ## Setup
7
+
8
+ Requires Node.js 18+ and a [Hyperbrowser](https://hyperbrowser.ai) API key for scraping.
9
+
10
+ ```bash
11
+ npm install vlr-stats-scraper
12
+ ```
13
+
14
+ Create a `.env` file in the directory you run it from:
15
+
16
+ ```
17
+ HYPERBROWSER_API_KEY=your_key_here
18
+ ```
19
+
20
+ ## CLI usage
21
+
22
+ The package installs a `valstats` command.
23
+
24
+ | Command | What it does |
25
+ | --- | --- |
26
+ | `valstats <link> <year> <event-name>` | Scrape every player on an event page and save them under that event |
27
+ | `valstats --clear-event <event-name>` | Remove one event and its players |
28
+ | `valstats --clear` | Wipe the whole database (asks for confirmation) |
29
+
30
+ ### Scraping an event
31
+
32
+ ```bash
33
+ npx valstats <link> <year> <event-name>
34
+ ```
35
+
36
+ ```bash
37
+ npx valstats https://www.vlr.gg/event/2977/vct-2026-americas-stage-2/group-stage 2026 "Stage 2"
38
+ ```
39
+
40
+ - `link` — the vlr.gg event/group-stage page to scrape.
41
+ - `year` — the year the event took place.
42
+ - `event-name` — a label for this tournament/stage (used to group players together later).
43
+
44
+ What happens:
45
+
46
+ 1. If the event name contains the word **"stage"** (case-insensitive), you're prompted to
47
+ enter a region (e.g. `AMER`, `EMEA`). That region is saved on the event and on every
48
+ player scraped. Otherwise region is left empty.
49
+ 2. Every player profile linked on the event page is collected. Stats are pulled using
50
+ vlr.gg's **all-time** timespan.
51
+ 3. Each player is scraped, saved to `players.db` in your current directory, linked to the
52
+ event, and printed to the console.
53
+
54
+ Running it again with an event name and year that are already saved replaces that event's
55
+ players with the freshly scraped ones. A player already in the database is updated with the
56
+ new data rather than duplicated.
57
+
58
+ ### Clearing data
59
+
60
+ ```bash
61
+ npx valstats --clear-event "Stage 2" # remove one event and its players
62
+ npx valstats --clear # wipe the whole database (type "yes" to confirm)
63
+ ```
64
+
65
+ Players that also belong to another event are kept when a single event is cleared.
66
+
67
+ ## What gets scraped
68
+
69
+ Each player is stored as an object like this:
70
+
71
+ ```js
72
+ {
73
+ name: 'tenz', // from the profile URL
74
+ team: 'Sentinels', // current team
75
+ past_teams: ['Cloud9', ...], // previous teams
76
+ winnings: '$123,456', // total event winnings
77
+ most_played_agents: ['Jett', ...], // top 3 agents by usage
78
+ avg_kda: 1.23, // average K:D across those top 3 agents
79
+ nationality: 'Canada',
80
+ region: 'AMER', // from the CLI prompt / argument, or null
81
+ stats: [ // full agent stat rows for the top 3 agents,
82
+ { Agent: 'Jett', 'K:D': '1.25', ... } // keyed by vlr.gg's column headers
83
+ ]
84
+ }
85
+ ```
86
+
87
+ ## Library usage
88
+
89
+ ```js
90
+ import {
91
+ scrapePlayer,
92
+ scrapeWebsite,
93
+ savePlayer,
94
+ saveEvent,
95
+ addPlayerToEvent,
96
+ getPlayersByEvent,
97
+ getRandomPlayerFromEvent,
98
+ } from 'vlr-stats-scraper';
99
+
100
+ const eventId = saveEvent({ name: 'Stage 2', region: 'AMER', year: 2026 });
101
+
102
+ const links = await scrapeWebsite('https://www.vlr.gg/event/.../group-stage');
103
+ for (const link of links) {
104
+ const player = await scrapePlayer(link, 'AMER');
105
+ if (player) {
106
+ savePlayer(player);
107
+ addPlayerToEvent(eventId, player.name);
108
+ }
109
+ }
110
+
111
+ const roster = getPlayersByEvent('Stage 2');
112
+ const pick = getRandomPlayerFromEvent('Stage 2');
113
+ ```
114
+
115
+ ### Exports
116
+
117
+ #### Scraping
118
+
119
+ | Function | Returns | Description |
120
+ | --- | --- | --- |
121
+ | `scrapeWebsite(url)` | `Promise<string[]>` | Scrapes an event page and returns every player profile link on it (with `?timespan=all`). |
122
+ | `scrapePlayer(url, region)` | `Promise<Player>` | Scrapes one player's profile page into a player object (see above). `region` is stored as-is. Returns `undefined` if scraping fails. |
123
+
124
+ #### Players
125
+
126
+ | Function | Returns | Description |
127
+ | --- | --- | --- |
128
+ | `savePlayer(player)` | — | Inserts a player, or updates them if one with the same name exists. |
129
+ | `getPlayer(name)` | `Player \| null` | Looks up a player by name. |
130
+ | `getAllPlayers()` | `Player[]` | Every player in the database. |
131
+ | `getRandomPlayer()` | `Player \| null` | A random player from the whole database. |
132
+
133
+ #### Events (tournaments / stages)
134
+
135
+ | Function | Returns | Description |
136
+ | --- | --- | --- |
137
+ | `saveEvent({ name, url, region, year, stage })` | `number` | Creates or updates an event by name and returns its id. Only `name` is required. |
138
+ | `addPlayerToEvent(eventId, playerName)` | — | Links a saved player to an event (ignored if already linked). |
139
+ | `getEvents()` | `Event[]` | Every saved event. |
140
+ | `getEventByName(name)` | `Event \| undefined` | Looks up an event by name. |
141
+ | `getPlayersByEvent(eventName)` | `Player[]` | All players linked to an event. |
142
+ | `getRandomPlayerFromEvent(eventName)` | `Player \| null` | A random player from one event. |
143
+
144
+ #### Clearing
145
+
146
+ | Function | Returns | Description |
147
+ | --- | --- | --- |
148
+ | `emptyEvent(eventName)` | `boolean` | Removes an event's players but keeps the event so it can be re-scraped. `false` if the event doesn't exist. |
149
+ | `clearEvent(eventName)` | `boolean` | Removes an event and its players. `false` if the event doesn't exist. |
150
+ | `clearDatabase()` | — | Deletes every event and player. |
151
+
152
+ Players that still belong to another event are never deleted by `emptyEvent` or
153
+ `clearEvent`.
154
+
155
+ ## Data storage
156
+
157
+ Player and event data is stored in a `players.db` SQLite file created in whichever
158
+ directory you run the tool from. It has three tables:
159
+
160
+ - `players` — one row per player (`past_teams`, `most_played_agents` and `stats` are stored
161
+ as JSON and parsed back automatically when read).
162
+ - `events` — one row per tournament/stage (`name`, `url`, `region`, `year`, `stage`).
163
+ - `event_players` — links players to the events they were scraped from.
164
+
165
+ Databases created by older versions (with `tournaments` / `tournament_players` tables) are
166
+ migrated automatically.
167
+
168
+ ## License
169
+
170
+ ISC
package/index.mjs CHANGED
@@ -11,4 +11,7 @@ export {
11
11
  getEventByName,
12
12
  getPlayersByEvent,
13
13
  getRandomPlayerFromEvent,
14
+ emptyEvent,
15
+ clearEvent,
16
+ clearDatabase,
14
17
  } from './scripts/db.mjs';
package/main.mjs CHANGED
@@ -2,12 +2,51 @@
2
2
  import promptSync from 'prompt-sync';
3
3
  import { scrapeWebsite } from './scripts/player-link-scraper.mjs';
4
4
  import scrapePlayer from './scripts/player-info-scraper.mjs';
5
- import { savePlayer, saveEvent, addPlayerToEvent } from './scripts/db.mjs';
5
+ import {
6
+ savePlayer,
7
+ saveEvent,
8
+ addPlayerToEvent,
9
+ getEventByName,
10
+ emptyEvent,
11
+ clearEvent,
12
+ clearDatabase,
13
+ } from './scripts/db.mjs';
6
14
 
7
- const [link, year, eventName] = process.argv.slice(2);
15
+ const args = process.argv.slice(2);
16
+
17
+ if (args[0] === '--clear') {
18
+ const prompt = promptSync();
19
+ const answer = prompt('This deletes every event and player in players.db. Type "yes" to continue: ');
20
+ if (answer?.trim().toLowerCase() === 'yes') {
21
+ clearDatabase();
22
+ console.log('Cleared players.db');
23
+ } else {
24
+ console.log('Cancelled');
25
+ }
26
+ process.exit(0);
27
+ }
28
+
29
+ if (args[0] === '--clear-event') {
30
+ const name = args[1];
31
+ if (!name) {
32
+ console.error('Usage: valstats --clear-event <event-name>');
33
+ process.exit(1);
34
+ }
35
+ if (clearEvent(name)) {
36
+ console.log(`Removed "${name}" and its players from players.db`);
37
+ } else {
38
+ console.error(`No event named "${name}" in players.db`);
39
+ process.exit(1);
40
+ }
41
+ process.exit(0);
42
+ }
43
+
44
+ const [link, year, eventName] = args;
8
45
 
9
46
  if (!link || !year || !eventName) {
10
47
  console.error('Usage: valstats <link> <year> <event-name>');
48
+ console.error(' valstats --clear-event <event-name>');
49
+ console.error(' valstats --clear');
11
50
  process.exit(1);
12
51
  }
13
52
 
@@ -19,6 +58,13 @@ if (isStage) {
19
58
  region = prompt('Enter the region: ');
20
59
  }
21
60
 
61
+ // Re-adding an event + year that's already saved replaces its old players.
62
+ const existing = getEventByName(eventName);
63
+ if (existing && existing.year === parseInt(year, 10)) {
64
+ emptyEvent(eventName);
65
+ console.log(`"${eventName}" (${year}) is already in players.db, replacing it`);
66
+ }
67
+
22
68
  const eventId = saveEvent({
23
69
  name: eventName,
24
70
  url: link,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vlr-stats-scraper",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Scrapes player stats from vlr.gg and stores them in a local SQLite database, grouped by tournament or stage.",
5
5
  "homepage": "https://github.com/OverYander/VLR-Webscraper#readme",
6
6
  "bugs": {
package/scripts/db.mjs CHANGED
@@ -161,6 +161,33 @@ function getRandomPlayerFromEvent(eventName) {
161
161
  `).get(eventName));
162
162
  }
163
163
 
164
+ // Deletes players that no longer belong to any event.
165
+ function deleteOrphanedPlayers() {
166
+ db.prepare('DELETE FROM players WHERE name NOT IN (SELECT player_name FROM event_players)').run();
167
+ }
168
+
169
+ // Removes every player from an event but keeps the event itself, so it can be re-scraped.
170
+ // Players that are still in other events are kept.
171
+ const emptyEvent = db.transaction((eventName) => {
172
+ const event = getEventByName(eventName);
173
+ if (!event) return false;
174
+ db.prepare('DELETE FROM event_players WHERE event_id = ?').run(event.id);
175
+ deleteOrphanedPlayers();
176
+ return true;
177
+ });
178
+
179
+ // Removes an event and its players. Players that are still in other events are kept.
180
+ const clearEvent = db.transaction((eventName) => {
181
+ if (!emptyEvent(eventName)) return false;
182
+ db.prepare('DELETE FROM events WHERE name = ?').run(eventName);
183
+ return true;
184
+ });
185
+
186
+ // Wipes every event and player from the database.
187
+ const clearDatabase = db.transaction(() => {
188
+ db.exec('DELETE FROM event_players; DELETE FROM events; DELETE FROM players;');
189
+ });
190
+
164
191
  export {
165
192
  savePlayer,
166
193
  getPlayer,
@@ -172,4 +199,7 @@ export {
172
199
  getEventByName,
173
200
  getPlayersByEvent,
174
201
  getRandomPlayerFromEvent,
202
+ emptyEvent,
203
+ clearEvent,
204
+ clearDatabase,
175
205
  };