vlr-stats-scraper 1.5.0 → 3.1.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 +228 -73
- package/index.mjs +10 -1
- package/main.mjs +108 -52
- package/package.json +1 -1
- package/scripts/client.mjs +27 -0
- package/scripts/db.mjs +315 -152
- package/scripts/event-stats-scraper.mjs +77 -0
- package/scripts/migrate.mjs +181 -0
- package/scripts/player-info-scraper.mjs +43 -87
- package/scripts/player-link-scraper.mjs +41 -42
- package/scripts/stats.mjs +111 -0
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# vlr-stats-scraper
|
|
2
2
|
|
|
3
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
|
|
4
|
+
database, grouped by tournament or stage, so you can pull a backlog of players or serve them
|
|
5
|
+
from a website.
|
|
5
6
|
|
|
6
7
|
## Setup
|
|
7
8
|
|
|
@@ -15,6 +16,7 @@ Create a `.env` file in the directory you run it from:
|
|
|
15
16
|
|
|
16
17
|
```
|
|
17
18
|
HYPERBROWSER_API_KEY=your_key_here
|
|
19
|
+
VLR_DB_PATH=/path/to/players.db # optional, defaults to ./players.db
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
## CLI usage
|
|
@@ -24,8 +26,9 @@ The package installs a `valstats` command.
|
|
|
24
26
|
| Command | What it does |
|
|
25
27
|
| --- | --- |
|
|
26
28
|
| `valstats <link> <year> <event-name>` | Scrape every player on an event page and save them under that event |
|
|
27
|
-
| `valstats --
|
|
28
|
-
| `valstats --
|
|
29
|
+
| `valstats --search <name \| link>` | Print the stats of every saved player whose name starts with `name`, or of one player scraped live from a link |
|
|
30
|
+
| `valstats --player <link> <event-name> <year> [region]` | Scrape one player and add them to an event that's already saved |
|
|
31
|
+
| `valstats --clear-event <event-name> [year]` | Remove one event and its players |
|
|
29
32
|
| `valstats --clear` | Wipe the whole database (asks for confirmation) |
|
|
30
33
|
|
|
31
34
|
### Scraping an event
|
|
@@ -40,149 +43,301 @@ npx valstats https://www.vlr.gg/event/2977/vct-2026-americas-stage-2/group-stage
|
|
|
40
43
|
|
|
41
44
|
- `link` — the vlr.gg event/group-stage page to scrape.
|
|
42
45
|
- `year` — the year the event took place.
|
|
43
|
-
- `event-name` — a label for this tournament/stage
|
|
46
|
+
- `event-name` — a label for this tournament/stage. Events are identified by name **and** year,
|
|
47
|
+
so "Stage 2" 2025 and "Stage 2" 2026 are kept apart.
|
|
44
48
|
|
|
45
49
|
What happens:
|
|
46
50
|
|
|
47
|
-
1.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
1. The event page is scraped for its title and every player profile linked on it.
|
|
52
|
+
2. The region is read from the event title and saved on every player (not on the event):
|
|
53
|
+
|
|
54
|
+
| Title contains | Region |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `America` (e.g. "VCT 2026: Americas Stage 2") | `AMER` |
|
|
57
|
+
| `EMEA` | `EMEA` |
|
|
58
|
+
| `Pacific` | `PAC` |
|
|
59
|
+
| `China` | `CN` |
|
|
60
|
+
| none of these (e.g. Masters, Champions) | `null` |
|
|
61
|
+
|
|
62
|
+
3. The event's stats page (`vlr.gg/event/stats/<id>`) is scraped once. It gives every player's
|
|
63
|
+
stats **in this event only**: rating, ACS, K:D, ADR, KAST, agents played, and so on. The stats
|
|
64
|
+
page covers the whole event, so a link to one stage of it still gets stats from every stage.
|
|
65
|
+
4. Each player's profile is scraped using vlr.gg's **all-time** timespan for their team, past
|
|
66
|
+
teams, nationality, winnings and career stats on their top 3 agents. Both sets of stats are
|
|
67
|
+
saved to the database under that event and printed to the console.
|
|
68
|
+
|
|
69
|
+
If the event page can't be scraped, nothing is saved or deleted. If only the stats page fails,
|
|
70
|
+
players are saved without event stats.
|
|
54
71
|
|
|
55
72
|
Running it again with an event name and year that are already saved replaces that event's
|
|
56
|
-
players with the freshly scraped ones.
|
|
57
|
-
new data rather than duplicated.
|
|
73
|
+
players with the freshly scraped ones.
|
|
58
74
|
|
|
59
75
|
### Viewing a player's stats
|
|
60
76
|
|
|
61
77
|
```bash
|
|
62
|
-
npx valstats --
|
|
63
|
-
npx valstats --
|
|
64
|
-
npx valstats --
|
|
78
|
+
npx valstats --search tenz # every saved player with a name starting "tenz"
|
|
79
|
+
npx valstats --search ten # tenz, tenten, ... (not "kitten")
|
|
80
|
+
npx valstats --search https://www.vlr.gg/player/9/tenz # scrape a player live without saving
|
|
65
81
|
```
|
|
66
82
|
|
|
67
|
-
-
|
|
68
|
-
those characters (case-insensitive).
|
|
69
|
-
e.g. `tenz
|
|
70
|
-
-
|
|
71
|
-
|
|
83
|
+
- `--search <name>` searches the database and prints every player whose name starts with
|
|
84
|
+
those characters (case-insensitive). Nothing is scraped. A player saved under several events
|
|
85
|
+
is printed once per event, labelled with the event name and year, e.g. `tenz (Champs, 2026)`.
|
|
86
|
+
- `--search <link>` scrapes that profile live using the all-time timespan. The result is
|
|
87
|
+
printed but not saved.
|
|
88
|
+
|
|
89
|
+
For each player, their team, past teams, nationality, region, winnings, top agents, career K:D
|
|
90
|
+
and their stats in that event are printed, followed by a table of their career stats on their
|
|
91
|
+
top 3 agents.
|
|
72
92
|
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
### Adding a player to an event by hand
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx valstats --player <link> <event-name> <year> [region]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx valstats --player https://www.vlr.gg/player/9/tenz "Stage 2" 2026 # region copied from the event's other players
|
|
101
|
+
npx valstats --player https://www.vlr.gg/player/9/tenz "Stage 2" 2026 AMER # region set explicitly
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Scrapes one player, plus the event's stats page for their event stats, and saves them under an
|
|
105
|
+
event that's already saved, e.g. a player the event page missed. The event is checked before
|
|
106
|
+
scraping, so a mistyped name or year fails straight away without using a scrape.
|
|
75
107
|
|
|
76
108
|
### Clearing data
|
|
77
109
|
|
|
78
110
|
```bash
|
|
79
|
-
npx valstats --clear-event "Stage 2" # remove one event and its players
|
|
80
|
-
npx valstats --clear
|
|
111
|
+
npx valstats --clear-event "Stage 2" 2026 # remove one event and its players
|
|
112
|
+
npx valstats --clear-event "Stage 2" # works when only one year has that name
|
|
113
|
+
npx valstats --clear # wipe the whole database (type "yes" to confirm)
|
|
81
114
|
```
|
|
82
115
|
|
|
83
|
-
|
|
116
|
+
If several years share the name, `--clear-event` asks you to give the year instead of guessing.
|
|
84
117
|
|
|
85
118
|
## What gets scraped
|
|
86
119
|
|
|
87
|
-
|
|
120
|
+
`scrapePlayer` returns an object like this:
|
|
88
121
|
|
|
89
122
|
```js
|
|
90
123
|
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
vlr_id: 9, // vlr.gg's player id, from the profile URL
|
|
125
|
+
name: 'tenz', // URL slug
|
|
126
|
+
url: 'https://www.vlr.gg/player/9/tenz/',
|
|
127
|
+
team: 'Sentinels', // current team, or null
|
|
128
|
+
past_teams: ['Cloud9', ...],
|
|
129
|
+
winnings: 183650, // total event winnings in dollars
|
|
130
|
+
most_played_agents: ['jett', 'omen', 'reyna'],
|
|
131
|
+
avg_kda: 1.31, // career K:D on those agents: total kills / total deaths
|
|
97
132
|
nationality: 'Canada',
|
|
98
|
-
region: 'AMER', // from the
|
|
99
|
-
|
|
100
|
-
|
|
133
|
+
region: 'AMER', // from the event title, or null
|
|
134
|
+
scraped_at: '2026-10-31T12:00:00.000Z',
|
|
135
|
+
stats: [ // career stats on the top 3 agents
|
|
136
|
+
{ agent: 'jett', maps: 207, use_pct: 45, rounds: 4328, rating: 1.17, acs: 262.4, kd: 1.32,
|
|
137
|
+
kast: 72, adr: 156.4, kpr: 0.92, apr: 0.2, fk_fd: 1.66, kills: 3989, deaths: 3028, ... },
|
|
138
|
+
...
|
|
101
139
|
]
|
|
102
140
|
}
|
|
103
141
|
```
|
|
104
142
|
|
|
143
|
+
Players read from the database also have `id`, `event_id`, `event_name`, `event_year` and
|
|
144
|
+
`event_stats` — their stats in that event, or `null`:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
event_stats: {
|
|
148
|
+
team: '100T',
|
|
149
|
+
agents: [{ agent: 'phoenix', use_pct: 95 }, { agent: 'omen', use_pct: 5 }],
|
|
150
|
+
maps: 21, rounds: 449, rating: 1.19, acs: 243, kd: 1.22, kast: 77, adr: 160.5,
|
|
151
|
+
kpr: 0.83, apr: 0.4, fk_fd: 1.12, fk_pct: 11, fd_pct: 10, hs_pct: 22, clutch_pct: 14,
|
|
152
|
+
clutches_won: 5, clutches_played: 35, kills_max: 28,
|
|
153
|
+
kills: 374, deaths: 307, assists: 180, first_kills: 48, first_deaths: 43,
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Every stat is a number. Percentages are stored as 0–100 (`kast: 77` means 77%).
|
|
158
|
+
|
|
105
159
|
## Library usage
|
|
106
160
|
|
|
107
161
|
```js
|
|
108
162
|
import {
|
|
109
|
-
scrapePlayer,
|
|
110
163
|
scrapeWebsite,
|
|
111
|
-
|
|
164
|
+
scrapeEventStats,
|
|
165
|
+
scrapePlayer,
|
|
112
166
|
saveEvent,
|
|
113
|
-
|
|
167
|
+
savePlayer,
|
|
114
168
|
getPlayersByEvent,
|
|
115
|
-
|
|
169
|
+
getEventLeaderboard,
|
|
116
170
|
} from 'vlr-stats-scraper';
|
|
117
171
|
|
|
118
|
-
const
|
|
172
|
+
const link = 'https://www.vlr.gg/event/2977/vct-2026-americas-stage-2/group-stage';
|
|
173
|
+
const { playerLinks, region } = await scrapeWebsite(link);
|
|
174
|
+
const eventStats = await scrapeEventStats(link);
|
|
175
|
+
const eventId = saveEvent({ name: 'Stage 2', year: 2026, url: link });
|
|
119
176
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const player = await scrapePlayer(link, 'AMER');
|
|
177
|
+
for (const playerLink of playerLinks) {
|
|
178
|
+
const player = await scrapePlayer(playerLink, region);
|
|
123
179
|
if (player) {
|
|
124
|
-
savePlayer(player);
|
|
125
|
-
addPlayerToEvent(eventId, player.name);
|
|
180
|
+
savePlayer(eventId, { ...player, event_stats: eventStats?.get(player.vlr_id) ?? null });
|
|
126
181
|
}
|
|
127
182
|
}
|
|
128
183
|
|
|
129
|
-
const roster = getPlayersByEvent('Stage 2');
|
|
130
|
-
const
|
|
184
|
+
const roster = getPlayersByEvent('Stage 2', 2026);
|
|
185
|
+
const topFive = getEventLeaderboard('Stage 2', 2026, { stat: 'acs', minRounds: 100, limit: 5 });
|
|
131
186
|
```
|
|
132
187
|
|
|
188
|
+
### Using it from a website
|
|
189
|
+
|
|
190
|
+
Scrape on a schedule (cron, a GitHub Action, ...) with the CLI or library, and have the web server
|
|
191
|
+
open the same file read-only:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
import { openDatabase, getPlayer } from 'vlr-stats-scraper';
|
|
195
|
+
|
|
196
|
+
openDatabase({ path: '/data/players.db', readonly: true });
|
|
197
|
+
const player = getPlayer('tenz');
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- The database uses WAL mode, so the site keeps reading while a scrape is writing.
|
|
201
|
+
- A read-only connection never migrates. If the file is from an older version, it throws and asks
|
|
202
|
+
you to open it once for writing (e.g. run `valstats`) to upgrade it.
|
|
203
|
+
- better-sqlite3 needs a real, persistent disk: a VPS, or Fly.io / Railway / Render with a
|
|
204
|
+
volume. Serverless hosts like Vercel and Netlify don't keep files between requests; for those,
|
|
205
|
+
copy the data into a hosted database such as Turso or Postgres.
|
|
206
|
+
|
|
207
|
+
### Upgrading from 2.x
|
|
208
|
+
|
|
209
|
+
3.0.0 changes the database layout and has breaking API changes. Existing `players.db` files are
|
|
210
|
+
migrated automatically the first time they're opened for writing.
|
|
211
|
+
|
|
212
|
+
| 2.x | 3.0.0 |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| Event names unique on their own; re-scraping "Stage 2" for a new year overwrote the old one | Events unique by name **and** year |
|
|
215
|
+
| `getEventByName(name)`, `getPlayersByEvent(name)`, `emptyEvent(name)`, `clearEvent(name)` | Take an optional `year`: `getPlayersByEvent('Stage 2', 2026)`. Without it, reads use the newest year and deletes refuse to guess if there are several. `getEvent` is the new name for `getEventByName`. |
|
|
216
|
+
| Per-event rows held all-time profile stats | Rows also get `event_stats`, scraped from the event's stats page |
|
|
217
|
+
| Players matched by URL slug | Players matched by `vlr_id`; `getPlayer(9)` also works |
|
|
218
|
+
| `winnings: '$123,456'` | `winnings: 123456` |
|
|
219
|
+
| `stats: [{ Agent: 'jett', 'K:D': '1.25', ... }]` | `stats: [{ agent: 'jett', kd: 1.25, ... }]`, all numbers |
|
|
220
|
+
| `avg_kda` was a plain average of each agent's K:D | Total kills / total deaths on those agents |
|
|
221
|
+
| `players.db` always in the current directory, opened on import | `VLR_DB_PATH` or `openDatabase({ path, readonly })`, opened on first use |
|
|
222
|
+
| `scrapeWebsite` returned `{ playerLinks, region }` | Also returns `title` and `vlrEventId` |
|
|
223
|
+
|
|
224
|
+
Rows migrated from 2.x have no `vlr_id`, `url`, `scraped_at` or `event_stats` until their event is
|
|
225
|
+
scraped again; re-scraping matches them up by name.
|
|
226
|
+
|
|
133
227
|
### Exports
|
|
134
228
|
|
|
135
229
|
#### Scraping
|
|
136
230
|
|
|
137
231
|
| Function | Returns | Description |
|
|
138
232
|
| --- | --- | --- |
|
|
139
|
-
| `scrapeWebsite(url)` | `Promise<
|
|
140
|
-
| `
|
|
233
|
+
| `scrapeWebsite(url)` | `Promise<{ playerLinks, region, title, vlrEventId }>` | Scrapes an event page. `playerLinks` is every player profile link on it (with `?timespan=all`); `region` is read from the event title (`'AMER'`, `'EMEA'`, `'PAC'`, `'CN'` or `null`). Returns `undefined` if scraping fails. |
|
|
234
|
+
| `scrapeEventStats(url)` | `Promise<Map<vlrId, EventStats>>` | Scrapes the stats page of the event `url` points at (any event or stage link works). Returns `undefined` if scraping fails. |
|
|
235
|
+
| `eventStatsUrl(url)` | `string \| null` | The stats page URL for an event link. |
|
|
236
|
+
| `scrapePlayer(url, region)` | `Promise<Player>` | Scrapes one player's profile page into a player object (see above). Returns `undefined` if scraping fails. |
|
|
237
|
+
|
|
238
|
+
#### Connection
|
|
239
|
+
|
|
240
|
+
| Function | Returns | Description |
|
|
241
|
+
| --- | --- | --- |
|
|
242
|
+
| `openDatabase({ path, readonly })` | `Database` | Opens the database file. Both options are optional. Without a call, the first query opens `$VLR_DB_PATH` or `./players.db` for writing. |
|
|
243
|
+
| `closeDatabase()` | — | Closes the connection. |
|
|
141
244
|
|
|
142
245
|
#### Players
|
|
143
246
|
|
|
247
|
+
A player is saved separately for each event. Functions that take a player accept a name
|
|
248
|
+
(`'tenz'`, case-insensitive) or a vlr id (`9`).
|
|
249
|
+
|
|
144
250
|
| Function | Returns | Description |
|
|
145
251
|
| --- | --- | --- |
|
|
146
|
-
| `savePlayer(player)` |
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
252
|
+
| `savePlayer(eventId, player)` | `number` | Saves a player for one event and returns the row's id. Re-saving the same player in the same event updates them. Leaving out `event_stats` keeps what's saved; `null` removes it. |
|
|
253
|
+
| `getPlayerById(id)` | `Player \| null` | One saved row by id. |
|
|
254
|
+
| `getPlayer(nameOrVlrId)` | `Player \| null` | The player as they were in their most recent event. |
|
|
255
|
+
| `getPlayerHistory(nameOrVlrId)` | `Player[]` | Every saved version of a player, one per event, newest first. |
|
|
256
|
+
| `searchPlayers(query)` | `Player[]` | Every player whose name starts with `query` (case-insensitive), one entry per event. |
|
|
257
|
+
| `getPlayersByTeam(team, { includePast })` | `Player[]` | Players currently on `team`, plus former players with `includePast: true`. Newest row per player. |
|
|
258
|
+
| `getAllPlayers()` | `Player[]` | Every player row. |
|
|
259
|
+
| `getRandomPlayer()` | `Player \| null` | A random player row. |
|
|
260
|
+
|
|
261
|
+
#### Leaderboards
|
|
262
|
+
|
|
263
|
+
| Function | Returns | Description |
|
|
264
|
+
| --- | --- | --- |
|
|
265
|
+
| `getEventLeaderboard(name, year, { stat, minRounds, limit, ascending })` | `Player[]` | Players in one event sorted by an `event_stats` column (default `rating`, 20 players). |
|
|
266
|
+
| `getAgentLeaderboard(agent, { stat, minRounds, limit, ascending })` | `Player[]` | Players sorted by their career stats on one agent, e.g. `getAgentLeaderboard('jett', { stat: 'kd', minRounds: 500 })`. Uses each player's newest row. |
|
|
151
267
|
|
|
152
268
|
#### Events (tournaments / stages)
|
|
153
269
|
|
|
154
270
|
| Function | Returns | Description |
|
|
155
271
|
| --- | --- | --- |
|
|
156
|
-
| `saveEvent({ name,
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
160
|
-
| `getPlayersByEvent(
|
|
161
|
-
| `getRandomPlayerFromEvent(
|
|
272
|
+
| `saveEvent({ name, year, url, stage })` | `number` | Creates or updates the event with this name and year and returns its id. Only `name` is required. |
|
|
273
|
+
| `getEvents()` | `Event[]` | Every saved event, newest first. |
|
|
274
|
+
| `getEvent(name, year?)` | `Event \| undefined` | Looks up an event. Without `year`, the newest event with that name. `getEventByName` is an alias. |
|
|
275
|
+
| `getEventId(name, year?)` | `number \| undefined` | The event's id. |
|
|
276
|
+
| `getPlayersByEvent(name, year?)` | `Player[]` | Every player saved under an event. |
|
|
277
|
+
| `getRandomPlayerFromEvent(name, year?)` | `Player \| null` | A random player from one event. |
|
|
162
278
|
|
|
163
279
|
#### Clearing
|
|
164
280
|
|
|
165
281
|
| Function | Returns | Description |
|
|
166
282
|
| --- | --- | --- |
|
|
167
|
-
| `emptyEvent(
|
|
168
|
-
| `clearEvent(
|
|
283
|
+
| `emptyEvent(name, year?)` | `boolean` | Removes an event's players but keeps the event so it can be re-scraped. |
|
|
284
|
+
| `clearEvent(name, year?)` | `boolean` | Removes an event and its players. |
|
|
169
285
|
| `clearDatabase()` | — | Deletes every event and player. |
|
|
170
286
|
|
|
171
|
-
|
|
172
|
-
|
|
287
|
+
Both return `false` if the event doesn't exist, and throw if `year` is left out and several years
|
|
288
|
+
share the name. They only delete that event's rows, so the same player's saves in other events
|
|
289
|
+
are kept.
|
|
173
290
|
|
|
174
291
|
## Data storage
|
|
175
292
|
|
|
176
|
-
|
|
177
|
-
|
|
293
|
+
```
|
|
294
|
+
events players agent_stats
|
|
295
|
+
─────────── ─────────────────── ─────────────────────────
|
|
296
|
+
id (PK) ◄──────────────── event_id ┌────── player_id
|
|
297
|
+
name ┐ unique together id (PK) ◄───────────────┤ agent, position
|
|
298
|
+
year ┘ vlr_id │ maps, use_pct, rounds, rating,
|
|
299
|
+
url, vlr_event_id name, url │ acs, kd, kast, adr, kills, deaths...
|
|
300
|
+
stage team, past_teams (JSON) │
|
|
301
|
+
winnings, avg_kda │ event_stats
|
|
302
|
+
most_played_agents │ ─────────────────────────
|
|
303
|
+
nationality, region └────── player_id (PK)
|
|
304
|
+
scraped_at team, agents (JSON), maps, rounds,
|
|
305
|
+
rating, acs, kd, kast, adr, hs_pct...
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
- `events` — one row per tournament/stage, unique by `(name, year)`.
|
|
309
|
+
- `players` — one row per player **per event**, unique by `(event_id, vlr_id)`.
|
|
310
|
+
- `agent_stats` — the player's career stats on their top 3 agents, as of `scraped_at`.
|
|
311
|
+
- `event_stats` — the player's stats in that event only.
|
|
312
|
+
|
|
313
|
+
Deleting an event deletes its players, and deleting a player deletes their stats
|
|
314
|
+
(`ON DELETE CASCADE`). The schema version is kept in SQLite's `user_version`; older files are
|
|
315
|
+
migrated automatically when opened for writing.
|
|
316
|
+
|
|
317
|
+
### Example
|
|
318
|
+
|
|
319
|
+
**events**
|
|
320
|
+
|
|
321
|
+
| id | name | year |
|
|
322
|
+
|----|---------|------|
|
|
323
|
+
| 1 | Stage 2 | 2025 |
|
|
324
|
+
| 2 | Stage 2 | 2026 |
|
|
325
|
+
|
|
326
|
+
**players**
|
|
327
|
+
|
|
328
|
+
| id | event_id | vlr_id | name |
|
|
329
|
+
|----|----------|--------|------|
|
|
330
|
+
| 1 | 1 | 9 | tenz |
|
|
331
|
+
| 2 | 2 | 9 | tenz |
|
|
332
|
+
|
|
333
|
+
**event_stats**
|
|
178
334
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
335
|
+
| player_id | rating | acs |
|
|
336
|
+
|-----------|--------|-----|
|
|
337
|
+
| 1 | 1.05 | 221 |
|
|
338
|
+
| 2 | 1.19 | 243 |
|
|
183
339
|
|
|
184
|
-
|
|
185
|
-
migrated automatically.
|
|
340
|
+
`tenz` has a row per event, each with how they played in that event.
|
|
186
341
|
|
|
187
342
|
## License
|
|
188
343
|
|
package/index.mjs
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
export { default as scrapePlayer } from './scripts/player-info-scraper.mjs';
|
|
2
2
|
export { scrapeWebsite } from './scripts/player-link-scraper.mjs';
|
|
3
|
+
export { scrapeEventStats, eventStatsUrl } from './scripts/event-stats-scraper.mjs';
|
|
3
4
|
export {
|
|
5
|
+
openDatabase,
|
|
6
|
+
closeDatabase,
|
|
4
7
|
savePlayer,
|
|
8
|
+
getPlayerById,
|
|
5
9
|
getPlayer,
|
|
10
|
+
getPlayerHistory,
|
|
6
11
|
searchPlayers,
|
|
7
12
|
getAllPlayers,
|
|
8
13
|
getRandomPlayer,
|
|
14
|
+
getPlayersByTeam,
|
|
15
|
+
getEventLeaderboard,
|
|
16
|
+
getAgentLeaderboard,
|
|
9
17
|
saveEvent,
|
|
10
|
-
addPlayerToEvent,
|
|
11
18
|
getEvents,
|
|
19
|
+
getEvent,
|
|
12
20
|
getEventByName,
|
|
21
|
+
getEventId,
|
|
13
22
|
getPlayersByEvent,
|
|
14
23
|
getRandomPlayerFromEvent,
|
|
15
24
|
emptyEvent,
|