vlr-stats-scraper 1.5.0 → 2.0.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,189 +1,326 @@
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 --player <name \| link>` | Print the stats of every saved player whose name contains `name`, or of one player scraped live from a link |
28
- | `valstats --clear-event <event-name>` | Remove one event and its players |
29
- | `valstats --clear` | Wipe the whole database (asks for confirmation) |
30
-
31
- ### Scraping an event
32
-
33
- ```bash
34
- npx valstats <link> <year> <event-name>
35
- ```
36
-
37
- ```bash
38
- npx valstats https://www.vlr.gg/event/2977/vct-2026-americas-stage-2/group-stage 2026 "Stage 2"
39
- ```
40
-
41
- - `link` — the vlr.gg event/group-stage page to scrape.
42
- - `year` — the year the event took place.
43
- - `event-name` — a label for this tournament/stage (used to group players together later).
44
-
45
- What happens:
46
-
47
- 1. If the event name contains the word **"stage"** (case-insensitive), you're prompted to
48
- enter a region (e.g. `AMER`, `EMEA`). That region is saved on the event and on every
49
- player scraped. Otherwise region is left empty.
50
- 2. Every player profile linked on the event page is collected. Stats are pulled using
51
- vlr.gg's **all-time** timespan.
52
- 3. Each player is scraped, saved to `players.db` in your current directory, linked to the
53
- event, and printed to the console.
54
-
55
- Running it again with an event name and year that are already saved replaces that event's
56
- players with the freshly scraped ones. A player already in the database is updated with the
57
- new data rather than duplicated.
58
-
59
- ### Viewing a player's stats
60
-
61
- ```bash
62
- npx valstats --player tenz # every saved player with "tenz" in their name
63
- npx valstats --player ten # partial names work too: tenz, tenten, ...
64
- npx valstats --player https://www.vlr.gg/player/9/tenz # scrape a player live
65
- ```
66
-
67
- - Passing a **name** searches `players.db` and prints every player whose name contains
68
- those characters (case-insensitive). Names are the ones from the vlr.gg profile URL,
69
- e.g. `tenz`. Nothing is scraped.
70
- - Passing a **vlr.gg player link** scrapes that profile live using the all-time timespan.
71
- The result is printed but not saved to the database.
72
-
73
- For each player, their team, past teams, nationality, region, winnings, top agents and
74
- average K:D are printed, followed by a table of their stats on their top 3 agents.
75
-
76
- ### Clearing data
77
-
78
- ```bash
79
- npx valstats --clear-event "Stage 2" # remove one event and its players
80
- npx valstats --clear # wipe the whole database (type "yes" to confirm)
81
- ```
82
-
83
- Players that also belong to another event are kept when a single event is cleared.
84
-
85
- ## What gets scraped
86
-
87
- Each player is stored as an object like this:
88
-
89
- ```js
90
- {
91
- name: 'tenz', // from the profile URL
92
- team: 'Sentinels', // current team
93
- past_teams: ['Cloud9', ...], // previous teams
94
- winnings: '$123,456', // total event winnings
95
- most_played_agents: ['Jett', ...], // top 3 agents by usage
96
- avg_kda: 1.23, // average K:D across those top 3 agents
97
- nationality: 'Canada',
98
- region: 'AMER', // from the CLI prompt / argument, or null
99
- stats: [ // full agent stat rows for the top 3 agents,
100
- { Agent: 'Jett', 'K:D': '1.25', ... } // keyed by vlr.gg's column headers
101
- ]
102
- }
103
- ```
104
-
105
- ## Library usage
106
-
107
- ```js
108
- import {
109
- scrapePlayer,
110
- scrapeWebsite,
111
- savePlayer,
112
- saveEvent,
113
- addPlayerToEvent,
114
- getPlayersByEvent,
115
- getRandomPlayerFromEvent,
116
- } from 'vlr-stats-scraper';
117
-
118
- const eventId = saveEvent({ name: 'Stage 2', region: 'AMER', year: 2026 });
119
-
120
- const links = await scrapeWebsite('https://www.vlr.gg/event/.../group-stage');
121
- for (const link of links) {
122
- const player = await scrapePlayer(link, 'AMER');
123
- if (player) {
124
- savePlayer(player);
125
- addPlayerToEvent(eventId, player.name);
126
- }
127
- }
128
-
129
- const roster = getPlayersByEvent('Stage 2');
130
- const pick = getRandomPlayerFromEvent('Stage 2');
131
- ```
132
-
133
- ### Exports
134
-
135
- #### Scraping
136
-
137
- | Function | Returns | Description |
138
- | --- | --- | --- |
139
- | `scrapeWebsite(url)` | `Promise<string[]>` | Scrapes an event page and returns every player profile link on it (with `?timespan=all`). |
140
- | `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. |
141
-
142
- #### Players
143
-
144
- | Function | Returns | Description |
145
- | --- | --- | --- |
146
- | `savePlayer(player)` | — | Inserts a player, or updates them if one with the same name exists. |
147
- | `getPlayer(name)` | `Player \| null` | Looks up a player by name. |
148
- | `searchPlayers(query)` | `Player[]` | Every player whose name contains `query` (case-insensitive), sorted by name. |
149
- | `getAllPlayers()` | `Player[]` | Every player in the database. |
150
- | `getRandomPlayer()` | `Player \| null` | A random player from the whole database. |
151
-
152
- #### Events (tournaments / stages)
153
-
154
- | Function | Returns | Description |
155
- | --- | --- | --- |
156
- | `saveEvent({ name, url, region, year, stage })` | `number` | Creates or updates an event by name and returns its id. Only `name` is required. |
157
- | `addPlayerToEvent(eventId, playerName)` | — | Links a saved player to an event (ignored if already linked). |
158
- | `getEvents()` | `Event[]` | Every saved event. |
159
- | `getEventByName(name)` | `Event \| undefined` | Looks up an event by name. |
160
- | `getPlayersByEvent(eventName)` | `Player[]` | All players linked to an event. |
161
- | `getRandomPlayerFromEvent(eventName)` | `Player \| null` | A random player from one event. |
162
-
163
- #### Clearing
164
-
165
- | Function | Returns | Description |
166
- | --- | --- | --- |
167
- | `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. |
168
- | `clearEvent(eventName)` | `boolean` | Removes an event and its players. `false` if the event doesn't exist. |
169
- | `clearDatabase()` | — | Deletes every event and player. |
170
-
171
- Players that still belong to another event are never deleted by `emptyEvent` or
172
- `clearEvent`.
173
-
174
- ## Data storage
175
-
176
- Player and event data is stored in a `players.db` SQLite file created in whichever
177
- directory you run the tool from. It has three tables:
178
-
179
- - `players` — one row per player (`past_teams`, `most_played_agents` and `stats` are stored
180
- as JSON and parsed back automatically when read).
181
- - `events` — one row per tournament/stage (`name`, `url`, `region`, `year`, `stage`).
182
- - `event_players` — links players to the events they were scraped from.
183
-
184
- Databases created by older versions (with `tournaments` / `tournament_players` tables) are
185
- migrated automatically.
186
-
187
- ## License
188
-
189
- 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 --search <name \| link>` | Print the stats of every saved player whose name starts with `name`, or of one player scraped live from a link |
28
+ | `valstats --player <link> <event-name> <year> [region]` | Scrape one player and add them to an event that's already saved |
29
+ | `valstats --clear-event <event-name>` | Remove one event and its players |
30
+ | `valstats --clear` | Wipe the whole database (asks for confirmation) |
31
+
32
+ ### Scraping an event
33
+
34
+ ```bash
35
+ npx valstats <link> <year> <event-name>
36
+ ```
37
+
38
+ ```bash
39
+ npx valstats https://www.vlr.gg/event/2977/vct-2026-americas-stage-2/group-stage 2026 "Stage 2"
40
+ ```
41
+
42
+ - `link` — the vlr.gg event/group-stage page to scrape.
43
+ - `year` — the year the event took place.
44
+ - `event-name` — a label for this tournament/stage (used to group players together later).
45
+
46
+ What happens:
47
+
48
+ 1. The event page is scraped for its title and every player profile linked on it.
49
+ 2. The region is read from the event title and saved on every player (not on the event):
50
+
51
+ | Title contains | Region |
52
+ | --- | --- |
53
+ | `America` (e.g. "VCT 2026: Americas Stage 2") | `AMER` |
54
+ | `EMEA` | `EMEA` |
55
+ | `Pacific` | `PAC` |
56
+ | `China` | `CN` |
57
+ | none of these (e.g. Masters, Champions) | `null` |
58
+
59
+ 3. Each player is scraped using vlr.gg's **all-time** timespan, saved to `players.db` in your
60
+ current directory under that event, and printed to the console.
61
+
62
+ If the event page can't be scraped, nothing is saved or deleted.
63
+
64
+ Running it again with an event name and year that are already saved replaces that event's
65
+ players with the freshly scraped ones. A player who is already saved under a different event
66
+ gets a new, separate row for this event, so their stats from each event are kept apart (see
67
+ [Data storage](#data-storage)).
68
+
69
+ ### Viewing a player's stats
70
+
71
+ ```bash
72
+ npx valstats --search tenz # every saved player with "tenz" in their name
73
+ npx valstats --search ten # matches names starting with "ten": tenz, tenten, ... (not "kitten")
74
+ npx valstats --search https://www.vlr.gg/player/9/tenz # scrape a player live without saving
75
+ ```
76
+
77
+ - `--search <name>` searches `players.db` and prints every player whose name starts with
78
+ those characters (case-insensitive). Names are the ones from the vlr.gg profile URL,
79
+ e.g. `tenz`. Nothing is scraped. A player saved under several events is printed once per
80
+ event, labelled with the event name and year, e.g. `tenz (Champs, 2026)`.
81
+ - `--search <link>` scrapes that profile live using the all-time timespan. The result is
82
+ printed but not saved to the database.
83
+
84
+ For each player, their team, past teams, nationality, region, winnings, top agents and
85
+ average K:D are printed, followed by a table of their stats on their top 3 agents.
86
+
87
+ ### Adding a player to an event by hand
88
+
89
+ ```bash
90
+ npx valstats --player <link> <event-name> <year> [region]
91
+ ```
92
+
93
+ ```bash
94
+ npx valstats --player https://www.vlr.gg/player/9/tenz "Stage 2" 2026 # region copied from the event's other players
95
+ npx valstats --player https://www.vlr.gg/player/9/tenz "Stage 2" 2026 AMER # region set explicitly
96
+ ```
97
+
98
+ Scrapes one player and saves them under an event that's already in `players.db`, e.g. a player
99
+ the event page missed. The event is checked before scraping, so a mistyped name or year fails
100
+ straight away without using a scrape. If you leave out `region`, the player gets the same
101
+ region as the players already saved under that event.
102
+
103
+ ### Clearing data
104
+
105
+ ```bash
106
+ npx valstats --clear-event "Stage 2" # remove one event and its players
107
+ npx valstats --clear # wipe the whole database (type "yes" to confirm)
108
+ ```
109
+
110
+ Clearing one event only removes that event's player rows. The same player's saves under other
111
+ events are kept.
112
+
113
+ ## What gets scraped
114
+
115
+ Each player is stored as an object like this:
116
+
117
+ ```js
118
+ {
119
+ name: 'tenz', // from the profile URL
120
+ team: 'Sentinels', // current team
121
+ past_teams: ['Cloud9', ...], // previous teams
122
+ winnings: '$123,456', // total event winnings
123
+ most_played_agents: ['Jett', ...], // top 3 agents by usage
124
+ avg_kda: 1.23, // average K:D across those top 3 agents
125
+ nationality: 'Canada',
126
+ region: 'AMER', // from the event title, or null
127
+ stats: [ // full agent stat rows for the top 3 agents,
128
+ { Agent: 'Jett', 'K:D': '1.25', ... } // keyed by vlr.gg's column headers
129
+ ]
130
+ }
131
+ ```
132
+
133
+ ## Library usage
134
+
135
+ ```js
136
+ import {
137
+ scrapePlayer,
138
+ scrapeWebsite,
139
+ savePlayer,
140
+ saveEvent,
141
+ getPlayersByEvent,
142
+ getRandomPlayerFromEvent,
143
+ } from 'vlr-stats-scraper';
144
+
145
+ const { playerLinks, region } = await scrapeWebsite('https://www.vlr.gg/event/.../group-stage');
146
+ const eventId = saveEvent({ name: 'Stage 2', year: 2026 });
147
+
148
+ for (const link of playerLinks) {
149
+ const player = await scrapePlayer(link, region);
150
+ if (player) {
151
+ savePlayer(eventId, player);
152
+ }
153
+ }
154
+
155
+ const roster = getPlayersByEvent('Stage 2');
156
+ const pick = getRandomPlayerFromEvent('Stage 2');
157
+ ```
158
+
159
+ ### Upgrading from 1.x
160
+
161
+ 2.0.0 changes how players are stored and has breaking API changes:
162
+
163
+ | 1.x | 2.0.0 |
164
+ | --- | --- |
165
+ | `savePlayer(player)` then `addPlayerToEvent(eventId, player.name)` | `savePlayer(eventId, player)`. `addPlayerToEvent` is removed. |
166
+ | `scrapeWebsite(url)` returns `string[]` | Returns `{ playerLinks, region }` |
167
+ | One row per player, shared across events | One row per player **per event** (see [Data storage](#data-storage)) |
168
+ | `valstats --player <name>` | `valstats --search <name>`. `--player` now adds a player to an event. |
169
+ | Region typed in at a prompt and saved on the event | Region read from the event title and saved on each player only |
170
+ | `saveEvent({ name, url, region, year, stage })` | `saveEvent({ name, url, year, stage })` |
171
+
172
+ Existing `players.db` files are migrated automatically the first time they're opened.
173
+
174
+ ### Exports
175
+
176
+ #### Scraping
177
+
178
+ | Function | Returns | Description |
179
+ | --- | --- | --- |
180
+ | `scrapeWebsite(url)` | `Promise<{ playerLinks, region }>` | 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. |
181
+ | `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. |
182
+
183
+ #### Players
184
+
185
+ A player is saved separately for each event, so the same player can have different stats in
186
+ different events. Players read from the database also have `id`, `event_id`, `event_name` and
187
+ `event_year`.
188
+
189
+ | Function | Returns | Description |
190
+ | --- | --- | --- |
191
+ | `savePlayer(eventId, player)` | `number` | Saves a player's stats for one event and returns the row's id. Re-saving the same player in the same event updates them. |
192
+ | `getPlayerById(id)` | `Player \| null` | Looks up one saved player row by id. |
193
+ | `getPlayer(name)` | `Player \| null` | The player as they were in their most recent event. |
194
+ | `getPlayerHistory(name)` | `Player[]` | Every saved version of a player, one per event, newest first. |
195
+ | `searchPlayers(query)` | `Player[]` | Every player whose name starts with `query` (case-insensitive), sorted by name, one entry per event. |
196
+ | `getAllPlayers()` | `Player[]` | Every player row in the database. |
197
+ | `getRandomPlayer()` | `Player \| null` | A random player row from the whole database. |
198
+
199
+ #### Events (tournaments / stages)
200
+
201
+ | Function | Returns | Description |
202
+ | --- | --- | --- |
203
+ | `saveEvent({ name, url, year, stage })` | `number` | Creates or updates an event by name and returns its id. Only `name` is required. |
204
+ | `getEvents()` | `Event[]` | Every saved event. |
205
+ | `getEventByName(name)` | `Event \| undefined` | Looks up an event by name. |
206
+ | `getEventId(name, year)` | `number \| undefined` | The id of the event with that name and year. |
207
+ | `getPlayersByEvent(eventName)` | `Player[]` | Every player saved under an event. |
208
+ | `getRandomPlayerFromEvent(eventName)` | `Player \| null` | A random player from one event. |
209
+
210
+ #### Clearing
211
+
212
+ | Function | Returns | Description |
213
+ | --- | --- | --- |
214
+ | `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. |
215
+ | `clearEvent(eventName)` | `boolean` | Removes an event and its players. `false` if the event doesn't exist. |
216
+ | `clearDatabase()` | — | Deletes every event and player. |
217
+
218
+ `emptyEvent` and `clearEvent` only delete that event's player rows, so the same player's saves in
219
+ other events are kept.
220
+
221
+ ## Data storage
222
+
223
+ Player and event data is stored in a `players.db` SQLite file created in whichever
224
+ directory you run the tool from. It has two tables, and **every player row belongs to exactly
225
+ one event**:
226
+
227
+ ```
228
+ events players
229
+ ────────────── ────────────────────────────
230
+ id (PK) ◄───────────────────── event_id (which event this row is from)
231
+ name (unique) id (PK) (unique per row)
232
+ url name
233
+ year team, past_teams, winnings,
234
+ stage most_played_agents, avg_kda,
235
+ nationality, region, stats
236
+ ```
237
+
238
+ - `events` — one row per tournament/stage. Region isn't stored here; it's on each player.
239
+ - `players` — one row per player **per event**. `past_teams`, `most_played_agents` and `stats`
240
+ are stored as JSON and parsed back automatically when read.
241
+
242
+ A player's stats change over time, so the same player in two events is saved as two separate
243
+ rows, each with the stats they had when that event was scraped.
244
+
245
+ ### Example
246
+
247
+ **events**
248
+
249
+ | id | name | year |
250
+ |----|---------|------|
251
+ | 1 | Masters | 2025 |
252
+ | 2 | Champs | 2026 |
253
+
254
+ **players**
255
+
256
+ | id | event_id | name | avg_kda |
257
+ |----|----------|-------|---------|
258
+ | 1 | 1 | tenz | 1.10 |
259
+ | 2 | 2 | tenz | 1.35 |
260
+ | 3 | 2 | aspas | 1.40 |
261
+
262
+ `tenz` has two rows: one for how they played at Masters and one for Champs. Each has its own
263
+ `id` and stats.
264
+
265
+ ### Rules the database enforces
266
+
267
+ - **One copy of a player per event.** `(event_id, name)` is unique, so re-scraping Champs
268
+ updates tenz's Champs row instead of adding another one. Their Masters row isn't touched.
269
+ - **Every player row must point to a real event.** Foreign keys are enforced, so saving a
270
+ player under an event id that doesn't exist fails.
271
+ - **Deleting an event deletes its players.** Player rows are removed along with their event
272
+ (`ON DELETE CASCADE`).
273
+
274
+ ### Which function reads what
275
+
276
+ | You want… | Function | What it does |
277
+ |---|---|---|
278
+ | Save a scraped player | `savePlayer(eventId, player)` | Inserts or updates that player's row for the event and returns its `id` |
279
+ | An event's roster | `getPlayersByEvent('Champs')` | Every row whose `event_id` matches that event |
280
+ | A player's latest version | `getPlayer('tenz')` | Their row from the newest event (highest year) |
281
+ | Every version of a player | `getPlayerHistory('tenz')` | All their rows, newest first |
282
+ | One specific row | `getPlayerById(2)` | Looks a row up by `id` |
283
+ | Search by name | `searchPlayers('ten')` | Partial match, one result per player per event |
284
+ | Re-scrape an event | `emptyEvent('Champs')` | Deletes that event's players but keeps the event |
285
+ | Remove an event | `clearEvent('Champs')` | Deletes the event, and its players go with it |
286
+
287
+ Every player read from the database also has `event_name` and `event_year`, so you can tell
288
+ which event a row came from without a separate lookup.
289
+
290
+ ### What happens when you scrape an event
291
+
292
+ ```
293
+ valstats <link> <year> <event-name>
294
+ │
295
+ ▼
296
+ Already saved with this name + year? ──yes──► emptyEvent(name)
297
+ │ (delete its old player rows)
298
+ ▼ │
299
+ saveEvent(...) ◄─────────────────────────────────────┘
300
+ (create or update the event, get its id)
301
+ │
302
+ ▼
303
+ For each player on the event page:
304
+ scrapePlayer(link) ──► savePlayer(eventId, player)
305
+ ```
306
+
307
+ ### Migrating older databases
308
+
309
+ Older versions stored one shared row per player and linked players to events through an
310
+ `event_players` table (or `tournaments` / `tournament_players` before that). These are
311
+ migrated automatically the first time the database is opened:
312
+
313
+ ```
314
+ Before After
315
+ players: tenz players: id 1, tenz, event_id 1
316
+ event_players: (1, tenz), (2, tenz) ──► id 2, tenz, event_id 2
317
+ ```
318
+
319
+ Each player is copied into a row for every event they were linked to, and `event_players` is
320
+ removed. Older versions overwrote a player's stats on every scrape, so migrated rows for the
321
+ same player all start with the same (most recent) stats. Players who weren't linked to any
322
+ event are dropped.
323
+
324
+ ## License
325
+
326
+ ISC
package/index.mjs CHANGED
@@ -1,18 +1,20 @@
1
- export { default as scrapePlayer } from './scripts/player-info-scraper.mjs';
2
- export { scrapeWebsite } from './scripts/player-link-scraper.mjs';
3
- export {
4
- savePlayer,
5
- getPlayer,
6
- searchPlayers,
7
- getAllPlayers,
8
- getRandomPlayer,
9
- saveEvent,
10
- addPlayerToEvent,
11
- getEvents,
12
- getEventByName,
13
- getPlayersByEvent,
14
- getRandomPlayerFromEvent,
15
- emptyEvent,
16
- clearEvent,
17
- clearDatabase,
18
- } from './scripts/db.mjs';
1
+ export { default as scrapePlayer } from './scripts/player-info-scraper.mjs';
2
+ export { scrapeWebsite } from './scripts/player-link-scraper.mjs';
3
+ export {
4
+ savePlayer,
5
+ getPlayerById,
6
+ getPlayer,
7
+ getPlayerHistory,
8
+ searchPlayers,
9
+ getAllPlayers,
10
+ getRandomPlayer,
11
+ saveEvent,
12
+ getEvents,
13
+ getEventByName,
14
+ getEventId,
15
+ getPlayersByEvent,
16
+ getRandomPlayerFromEvent,
17
+ emptyEvent,
18
+ clearEvent,
19
+ clearDatabase,
20
+ } from './scripts/db.mjs';
package/main.mjs CHANGED
@@ -1,138 +1,166 @@
1
- #!/usr/bin/env node
2
- import promptSync from 'prompt-sync'
3
- import { scrapeWebsite } from './scripts/player-link-scraper.mjs'
4
- import scrapePlayer from './scripts/player-info-scraper.mjs'
5
- import {
6
- savePlayer,
7
- saveEvent,
8
- addPlayerToEvent,
9
- getEventByName,
10
- emptyEvent,
11
- clearEvent,
12
- clearDatabase,
13
- searchPlayers,
14
- } from './scripts/db.mjs'
15
-
16
- const args = process.argv.slice(2)
17
-
18
- function printPlayerStats(player) {
19
- console.log(`\n${player.name}`)
20
- console.log(` Team: ${player.team ?? 'Free agent'}`)
21
- console.log(` Past teams: ${player.past_teams?.length ? player.past_teams.join(', ') : 'None'}`)
22
- console.log(` Nationality: ${player.nationality || 'Unknown'}`)
23
- console.log(` Region: ${player.region ?? 'Unknown'}`)
24
- console.log(` Winnings: ${player.winnings ?? 'Unknown'}`)
25
- // Older saves stored agents as raw <img> tags, so pull the name out of the alt text.
26
- const agents = (player.most_played_agents ?? []).map(agent => agent?.match(/alt="([^"]*)"/)?.[1] ?? agent)
27
- console.log(` Top agents: ${agents.join(', ') || 'None'}`)
28
- console.log(` Avg K:D: ${Number(player.avg_kda).toFixed(2)}`)
29
- if (player.stats?.length) {
30
- console.table(player.stats)
31
- }
32
- }
33
-
34
- if (args[0] === '--player') {
35
- const query = args[1]
36
- if (!query) {
37
- console.error('Usage: valstats --player <name | vlr.gg player link>')
38
- process.exit(1)
39
- }
40
-
41
- // A vlr.gg link is scraped live anything else is searched for in players.db.
42
- if (query.startsWith('http')) {
43
- const url = query.includes('timespan=') ? query : query.replace(/\/?$/, '/?timespan=all')
44
- const player = await scrapePlayer(url, null)
45
- if (!player) {
46
- console.error(`Couldn't scrape a player from ${query}`)
47
- process.exit(1)
48
- }
49
- printPlayerStats(player)
50
- process.exit(0)
51
- }
52
-
53
- const players = searchPlayers(query)
54
- if (!players.length) {
55
- console.error(`No players matching "${query}" in players.db (pass a vlr.gg player link to scrape them live)`)
56
- process.exit(1)
57
- }
58
- console.log(`${players.length} player${players.length === 1 ? '' : 's'} matching "${query}":`)
59
- players.forEach(printPlayerStats)
60
- process.exit(0)
61
- }
62
-
63
- if (args[0] === '--clear') {
64
- const prompt = promptSync()
65
- const answer = prompt('This deletes every event and player in players.db. Type "yes" to continue: ')
66
- if (answer?.trim().toLowerCase() === 'yes') {
67
- clearDatabase()
68
- console.log('Cleared players.db')
69
- } else {
70
- console.log('Cancelled')
71
- }
72
- process.exit(0)
73
- }
74
-
75
- if (args[0] === '--clear-event') {
76
- const name = args[1]
77
- if (!name) {
78
- console.error('Usage: valstats --clear-event <event-name>')
79
- process.exit(1)
80
- }
81
- if (clearEvent(name)) {
82
- console.log(`Removed "${name}" and its players from players.db`)
83
- } else {
84
- console.error(`No event named "${name}" in players.db`)
85
- process.exit(1)
86
- }
87
- process.exit(0)
88
- }
89
-
90
- const [link, year, eventName] = args
91
-
92
- if (!link || !year || !eventName) {
93
- console.error('Usage: valstats <link> <year> <event-name>')
94
- console.error(' valstats --player <name | vlr.gg player link>')
95
- console.error(' valstats --clear-event <event-name>')
96
- console.error(' valstats --clear')
97
- process.exit(1)
98
- }
99
-
100
- const isStage = eventName.toLowerCase().includes('stage')
101
-
102
- let region = null
103
- if (isStage) {
104
- const prompt = promptSync()
105
- region = prompt('Enter the region: ')
106
- }
107
-
108
- // Re-adding an event + year that's already saved replaces its old players.
109
- const existing = getEventByName(eventName)
110
- if (existing && existing.year === parseInt(year, 10)) {
111
- emptyEvent(eventName)
112
- console.log(`"${eventName}" (${year}) is already in players.db, replacing it`)
113
- }
114
-
115
- const eventId = saveEvent({
116
- name: eventName,
117
- url: link,
118
- region,
119
- year: parseInt(year, 10),
120
- stage: null,
121
- })
122
-
123
- const links = await scrapeWebsite(link)
124
-
125
- if (!links) {
126
- console.error(`Couldn't get the player list from ${link}`)
127
- process.exit(1)
128
- }
129
-
130
- for (const playerLink of links) {
131
- const player = await scrapePlayer(playerLink, region)
132
- if (player) {
133
- savePlayer(player)
134
- addPlayerToEvent(eventId, player.name)
135
- console.log(`Saved ${player.name} to players.db under "${eventName}"`)
136
- console.log(player)
137
- }
138
- }
1
+ #!/usr/bin/env node
2
+ import promptSync from 'prompt-sync'
3
+ import { scrapeWebsite } from './scripts/player-link-scraper.mjs'
4
+ import scrapePlayer from './scripts/player-info-scraper.mjs'
5
+ import {
6
+ savePlayer,
7
+ saveEvent,
8
+ getEventByName,
9
+ getEventId,
10
+ getPlayersByEvent,
11
+ emptyEvent,
12
+ clearEvent,
13
+ clearDatabase,
14
+ searchPlayers,
15
+ } from './scripts/db.mjs'
16
+
17
+ const args = process.argv.slice(2)
18
+
19
+ function printPlayerStats(player) {
20
+ // Players saved from the database belong to an event; ones scraped live don't.
21
+ const event = player.event_name ? ` (${player.event_name}${player.event_year ? `, ${player.event_year}` : ''})` : ''
22
+ console.log(`\n${player.name}${event}`)
23
+ console.log(` Team: ${player.team ?? 'Free agent'}`)
24
+ console.log(` Past teams: ${player.past_teams?.length ? player.past_teams.join(', ') : 'None'}`)
25
+ console.log(` Nationality: ${player.nationality || 'Unknown'}`)
26
+ console.log(` Region: ${player.region ?? 'Unknown'}`)
27
+ console.log(` Winnings: ${player.winnings ?? 'Unknown'}`)
28
+ // Older saves stored agents as raw <img> tags, so pull the name out of the alt text.
29
+ const agents = (player.most_played_agents ?? []).map(agent => agent?.match(/alt="([^"]*)"/)?.[1] ?? agent)
30
+ console.log(` Top agents: ${agents.join(', ') || 'None'}`)
31
+ console.log(` Avg K:D: ${Number(player.avg_kda).toFixed(2)}`)
32
+ if (player.stats?.length) {
33
+ console.table(player.stats)
34
+ }
35
+ }
36
+
37
+ // Manually adds one player to an event that's already saved.
38
+ if (args[0] === '--player') {
39
+ const [, query, eventName, eventYear, regionArg] = args
40
+ if (!query?.startsWith('http') || !eventName || !eventYear) {
41
+ console.error('Usage: valstats --player <vlr.gg player link> <event-name> <year> [region]')
42
+ process.exit(1)
43
+ }
44
+
45
+ // Look the event up before scraping so a typo doesn't waste a scrape.
46
+ const eventId = getEventId(eventName, parseInt(eventYear, 10))
47
+ if (!eventId) {
48
+ console.error(`No event named "${eventName}" (${eventYear}) in players.db`)
49
+ process.exit(1)
50
+ }
51
+
52
+ // Without a region argument, use the region the event's other players were saved with.
53
+ const region = regionArg ?? getPlayersByEvent(eventName)[0]?.region ?? null
54
+
55
+ const url = query.includes('timespan=') ? query : query.replace(/\/?$/, '/?timespan=all')
56
+ const player = await scrapePlayer(url, region)
57
+ if (!player) {
58
+ console.error(`Couldn't scrape a player from ${query}`)
59
+ process.exit(1)
60
+ }
61
+ savePlayer(eventId, player)
62
+ console.log(`Saved ${player.name} to players.db under "${eventName}" (${eventYear})`)
63
+ printPlayerStats(player)
64
+ process.exit(0)
65
+ }
66
+
67
+ if (args[0] === '--search') {
68
+ const query = args[1]
69
+ if (!query) {
70
+ console.error('Usage: valstats --search <name | vlr.gg player link>')
71
+ process.exit(1)
72
+ }
73
+
74
+ // A vlr.gg link is scraped live anything else is searched for in players.db.
75
+ if (query.startsWith('http')) {
76
+ const url = query.includes('timespan=') ? query : query.replace(/\/?$/, '/?timespan=all')
77
+ const player = await scrapePlayer(url, null)
78
+ if (!player) {
79
+ console.error(`Couldn't scrape a player from ${query}`)
80
+ process.exit(1)
81
+ }
82
+ printPlayerStats(player)
83
+ process.exit(0)
84
+ }
85
+
86
+ const players = searchPlayers(query)
87
+ if (!players.length) {
88
+ console.error(`No players matching "${query}" in players.db (pass a vlr.gg player link to scrape them live)`)
89
+ process.exit(1)
90
+ }
91
+ console.log(`${players.length} player${players.length === 1 ? '' : 's'} matching "${query}":`)
92
+ players.forEach(printPlayerStats)
93
+ process.exit(0)
94
+ }
95
+
96
+ if (args[0] === '--clear') {
97
+ const prompt = promptSync()
98
+ const answer = prompt('This deletes every event and player in players.db. Type "yes" to continue: ')
99
+ if (answer?.trim().toLowerCase() === 'yes') {
100
+ clearDatabase()
101
+ console.log('Cleared players.db')
102
+ } else {
103
+ console.log('Cancelled')
104
+ }
105
+ process.exit(0)
106
+ }
107
+
108
+ if (args[0] === '--clear-event') {
109
+ const name = args[1]
110
+ if (!name) {
111
+ console.error('Usage: valstats --clear-event <event-name>')
112
+ process.exit(1)
113
+ }
114
+ if (clearEvent(name)) {
115
+ console.log(`Removed "${name}" and its players from players.db`)
116
+ } else {
117
+ console.error(`No event named "${name}" in players.db`)
118
+ process.exit(1)
119
+ }
120
+ process.exit(0)
121
+ }
122
+
123
+ const [link, year, eventName] = args
124
+
125
+ if (!link || !year || !eventName) {
126
+ console.error('Usage: valstats <link> <year> <event-name>')
127
+ console.error(' valstats --player <vlr.gg player link> <event-name> <year> [region]')
128
+ console.error(' valstats --search <name | vlr.gg player link>')
129
+ console.error(' valstats --clear-event <event-name>')
130
+ console.error(' valstats --clear')
131
+ process.exit(1)
132
+ }
133
+
134
+ // Scrape first so a failed scrape doesn't wipe an event that's already saved.
135
+ const scraped = await scrapeWebsite(link)
136
+
137
+ if (!scraped) {
138
+ console.error(`Couldn't get the player list from ${link}`)
139
+ process.exit(1)
140
+ }
141
+
142
+ const { playerLinks, region } = scraped
143
+ console.log(`Found ${playerLinks.length} players, region: ${region ?? 'unknown'}`)
144
+
145
+ // Re-adding an event + year that's already saved replaces its old players.
146
+ const existing = getEventByName(eventName)
147
+ if (existing && existing.year === parseInt(year, 10)) {
148
+ emptyEvent(eventName)
149
+ console.log(`"${eventName}" (${year}) is already in players.db, replacing it`)
150
+ }
151
+
152
+ const eventId = saveEvent({
153
+ name: eventName,
154
+ url: link,
155
+ year: parseInt(year, 10),
156
+ stage: null,
157
+ })
158
+
159
+ for (const playerLink of playerLinks) {
160
+ const player = await scrapePlayer(playerLink, region)
161
+ if (player) {
162
+ savePlayer(eventId, player)
163
+ console.log(`Saved ${player.name} to players.db under "${eventName}"`)
164
+ console.log(player)
165
+ }
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vlr-stats-scraper",
3
- "version": "1.5.0",
3
+ "version": "2.0.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
@@ -2,25 +2,7 @@ import Database from 'better-sqlite3'
2
2
  import path from 'path'
3
3
 
4
4
  const db = new Database(path.join(process.cwd(), 'players.db'))
5
-
6
- db.exec(`
7
- CREATE TABLE IF NOT EXISTS players (
8
- name TEXT PRIMARY KEY,
9
- team TEXT,
10
- past_teams TEXT,
11
- winnings TEXT,
12
- most_played_agents TEXT,
13
- avg_kda REAL,
14
- nationality TEXT,
15
- region TEXT,
16
- stats TEXT
17
- )
18
- `)
19
-
20
- const existingPlayerColumns = db.prepare('PRAGMA table_info(players)').all().map(col => col.name)
21
- if (!existingPlayerColumns.includes('stats')) {
22
- db.exec('ALTER TABLE players ADD COLUMN stats TEXT')
23
- }
5
+ db.pragma('foreign_keys = ON')
24
6
 
25
7
  const existingTableNames = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(t => t.name)
26
8
  if (existingTableNames.includes('tournaments') && !existingTableNames.includes('events')) {
@@ -36,7 +18,6 @@ db.exec(`
36
18
  id INTEGER PRIMARY KEY AUTOINCREMENT,
37
19
  name TEXT UNIQUE NOT NULL,
38
20
  url TEXT,
39
- region TEXT,
40
21
  year INTEGER,
41
22
  stage INTEGER
42
23
  )
@@ -49,19 +30,60 @@ if (!existingEventColumns.includes('year')) {
49
30
  if (!existingEventColumns.includes('stage')) {
50
31
  db.exec('ALTER TABLE events ADD COLUMN stage INTEGER')
51
32
  }
33
+ // Region is stored on each player, not on the event.
34
+ if (existingEventColumns.includes('region')) {
35
+ db.exec('ALTER TABLE events DROP COLUMN region')
36
+ }
52
37
 
53
- db.exec(`
54
- CREATE TABLE IF NOT EXISTS event_players (
55
- event_id INTEGER NOT NULL REFERENCES events(id),
56
- player_name TEXT NOT NULL REFERENCES players(name),
57
- PRIMARY KEY (event_id, player_name)
38
+ // Each row is a player as they were in one event, so the same player can have a row per event
39
+ // with different stats.
40
+ const createPlayersTableSql = (tableName) => `
41
+ CREATE TABLE IF NOT EXISTS ${tableName} (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
44
+ name TEXT NOT NULL,
45
+ team TEXT,
46
+ past_teams TEXT,
47
+ winnings TEXT,
48
+ most_played_agents TEXT,
49
+ avg_kda REAL,
50
+ nationality TEXT,
51
+ region TEXT,
52
+ stats TEXT,
53
+ UNIQUE (event_id, name)
58
54
  )
59
- `)
55
+ `
56
+
57
+ // Older databases kept one shared row per player and linked them to events through event_players.
58
+ // Copy each of those links into its own per-event player row.
59
+ const existingPlayerColumns = db.prepare('PRAGMA table_info(players)').all().map(col => col.name)
60
+ if (existingPlayerColumns.length && !existingPlayerColumns.includes('event_id')) {
61
+ db.transaction(() => {
62
+ if (!existingPlayerColumns.includes('stats')) {
63
+ db.exec('ALTER TABLE players ADD COLUMN stats TEXT')
64
+ }
65
+ db.exec(createPlayersTableSql('players_new'))
66
+ db.exec(`
67
+ INSERT INTO players_new (event_id, name, team, past_teams, winnings, most_played_agents, avg_kda, nationality, region, stats)
68
+ SELECT event_players.event_id, players.name, players.team, players.past_teams, players.winnings,
69
+ players.most_played_agents, players.avg_kda, players.nationality, players.region, players.stats
70
+ FROM event_players
71
+ JOIN players ON players.name = event_players.player_name
72
+ JOIN events ON events.id = event_players.event_id
73
+ `)
74
+ db.exec('DROP TABLE event_players')
75
+ db.exec('DROP TABLE players')
76
+ db.exec('ALTER TABLE players_new RENAME TO players')
77
+ })()
78
+ }
79
+
80
+ db.exec(createPlayersTableSql('players'))
81
+ db.exec('DROP TABLE IF EXISTS event_players')
60
82
 
61
83
  const upsertPlayerStmt = db.prepare(`
62
- INSERT INTO players (name, team, past_teams, winnings, most_played_agents, avg_kda, nationality, region, stats)
63
- VALUES (@name, @team, @past_teams, @winnings, @most_played_agents, @avg_kda, @nationality, @region, @stats)
64
- ON CONFLICT(name) DO UPDATE SET
84
+ INSERT INTO players (event_id, name, team, past_teams, winnings, most_played_agents, avg_kda, nationality, region, stats)
85
+ VALUES (@event_id, @name, @team, @past_teams, @winnings, @most_played_agents, @avg_kda, @nationality, @region, @stats)
86
+ ON CONFLICT(event_id, name) DO UPDATE SET
65
87
  team = excluded.team,
66
88
  past_teams = excluded.past_teams,
67
89
  winnings = excluded.winnings,
@@ -73,19 +95,20 @@ const upsertPlayerStmt = db.prepare(`
73
95
  `)
74
96
 
75
97
  const upsertEventStmt = db.prepare(`
76
- INSERT INTO events (name, url, region, year, stage)
77
- VALUES (@name, @url, @region, @year, @stage)
98
+ INSERT INTO events (name, url, year, stage)
99
+ VALUES (@name, @url, @year, @stage)
78
100
  ON CONFLICT(name) DO UPDATE SET
79
101
  url = excluded.url,
80
- region = excluded.region,
81
102
  year = excluded.year,
82
103
  stage = excluded.stage
83
104
  `)
84
105
 
85
- const linkPlayerToEventStmt = db.prepare(`
86
- INSERT OR IGNORE INTO event_players (event_id, player_name)
87
- VALUES (@eventId, @playerName)
88
- `)
106
+ // Every player query also pulls in the event's name and year, newest event first.
107
+ const selectPlayersSql = `
108
+ SELECT players.*, events.name AS event_name, events.year AS event_year FROM players
109
+ JOIN events ON events.id = players.event_id
110
+ `
111
+ const newestFirst = 'ORDER BY events.year DESC, players.id DESC'
89
112
 
90
113
  function rowToPlayer(row) {
91
114
  if (!row) return null
@@ -97,50 +120,60 @@ function rowToPlayer(row) {
97
120
  }
98
121
  }
99
122
 
100
- function savePlayer(player) {
123
+ // Saves a player's stats for one event, replacing any earlier save of them in that same event.
124
+ // Returns the player row's id.
125
+ function savePlayer(eventId, player) {
101
126
  upsertPlayerStmt.run({
102
127
  ...player,
128
+ event_id: eventId,
103
129
  past_teams: JSON.stringify(player.past_teams),
104
130
  most_played_agents: JSON.stringify(player.most_played_agents),
105
131
  stats: JSON.stringify(player.stats),
106
132
  })
133
+ return db.prepare('SELECT id FROM players WHERE event_id = ? AND name = ?').get(eventId, player.name).id
134
+ }
135
+
136
+ function getPlayerById(id) {
137
+ return rowToPlayer(db.prepare(`${selectPlayersSql} WHERE players.id = ?`).get(id))
107
138
  }
108
139
 
140
+ // The player as they were in their most recent event.
109
141
  function getPlayer(name) {
110
- return rowToPlayer(db.prepare('SELECT * FROM players WHERE name = ?').get(name))
142
+ return rowToPlayer(db.prepare(`${selectPlayersSql} WHERE players.name = ? ${newestFirst} LIMIT 1`).get(name))
111
143
  }
112
144
 
113
- // Case-insensitive partial match, e.g. "ten" finds "tenz" and "tenten".
145
+ // Every saved version of a player, one per event, newest first.
146
+ function getPlayerHistory(name) {
147
+ return db.prepare(`${selectPlayersSql} WHERE players.name = ? ${newestFirst}`).all(name).map(rowToPlayer)
148
+ }
149
+
150
+ // Case-insensitive prefix match, e.g. "nat" finds "nats" but not "gnat". Returns one row per event.
114
151
  function searchPlayers(query) {
115
- return db.prepare("SELECT * FROM players WHERE name LIKE ? ESCAPE '\\' ORDER BY name")
116
- .all(`%${query.replace(/[\\%_]/g, '\\$&')}%`)
152
+ return db.prepare(`${selectPlayersSql} WHERE players.name LIKE ? ESCAPE '\\' ORDER BY players.name, events.year DESC, players.id DESC`)
153
+ .all(`${query.replace(/[\\%_]/g, '\\$&')}%`)
117
154
  .map(rowToPlayer)
118
155
  }
119
156
 
120
157
  function getAllPlayers() {
121
- return db.prepare('SELECT * FROM players').all().map(rowToPlayer)
158
+ return db.prepare(selectPlayersSql).all().map(rowToPlayer)
122
159
  }
123
160
 
124
161
  function getRandomPlayer() {
125
- return rowToPlayer(db.prepare('SELECT * FROM players ORDER BY RANDOM() LIMIT 1').get())
162
+ return rowToPlayer(db.prepare(`${selectPlayersSql} ORDER BY RANDOM() LIMIT 1`).get())
126
163
  }
127
164
 
165
+
128
166
  // Creates the event (tournament, stage, etc.) if it doesn't exist yet, and returns its id either way.
129
- function saveEvent({ name, url, region, year, stage }) {
167
+ function saveEvent({ name, url, year, stage }) {
130
168
  upsertEventStmt.run({
131
169
  name,
132
170
  url: url ?? null,
133
- region: region ?? null,
134
171
  year: year ?? null,
135
172
  stage: stage ?? null,
136
173
  })
137
174
  return db.prepare('SELECT id FROM events WHERE name = ?').get(name).id
138
175
  }
139
176
 
140
- function addPlayerToEvent(eventId, playerName) {
141
- linkPlayerToEventStmt.run({ eventId, playerName })
142
- }
143
-
144
177
  function getEvents() {
145
178
  return db.prepare('SELECT * FROM events').all()
146
179
  }
@@ -149,62 +182,49 @@ function getEventByName(name) {
149
182
  return db.prepare('SELECT * FROM events WHERE name = ?').get(name)
150
183
  }
151
184
 
152
- function getPlayersByEvent(eventName) {
153
- return db.prepare(`
154
- SELECT players.* FROM players
155
- JOIN event_players ON event_players.player_name = players.name
156
- JOIN events ON events.id = event_players.event_id
157
- WHERE events.name = ?
158
- `).all(eventName).map(rowToPlayer)
185
+ function getEventId(name, year) {
186
+ return db.prepare('SELECT id FROM events WHERE name = ? AND year = ?').get(name, year)?.id
159
187
  }
160
188
 
161
- function getRandomPlayerFromEvent(eventName) {
162
- return rowToPlayer(db.prepare(`
163
- SELECT players.* FROM players
164
- JOIN event_players ON event_players.player_name = players.name
165
- JOIN events ON events.id = event_players.event_id
166
- WHERE events.name = ?
167
- ORDER BY RANDOM() LIMIT 1
168
- `).get(eventName))
189
+ function getPlayersByEvent(eventName) {
190
+ return db.prepare(`${selectPlayersSql} WHERE events.name = ?`).all(eventName).map(rowToPlayer)
169
191
  }
170
192
 
171
- // Deletes players that no longer belong to any event.
172
- function deleteOrphanedPlayers() {
173
- db.prepare('DELETE FROM players WHERE name NOT IN (SELECT player_name FROM event_players)').run()
193
+ function getRandomPlayerFromEvent(eventName) {
194
+ return rowToPlayer(db.prepare(`${selectPlayersSql} WHERE events.name = ? ORDER BY RANDOM() LIMIT 1`).get(eventName))
174
195
  }
175
196
 
176
197
  // Removes every player from an event but keeps the event itself, so it can be re-scraped.
177
- // Players that are still in other events are kept.
178
- const emptyEvent = db.transaction((eventName) => {
198
+ function emptyEvent(eventName) {
179
199
  const event = getEventByName(eventName)
180
200
  if (!event) return false
181
- db.prepare('DELETE FROM event_players WHERE event_id = ?').run(event.id)
182
- deleteOrphanedPlayers()
201
+ db.prepare('DELETE FROM players WHERE event_id = ?').run(event.id)
183
202
  return true
184
- })
203
+ }
185
204
 
186
- // Removes an event and its players. Players that are still in other events are kept.
187
- const clearEvent = db.transaction((eventName) => {
188
- if (!emptyEvent(eventName)) return false
189
- db.prepare('DELETE FROM events WHERE name = ?').run(eventName)
190
- return true
191
- })
205
+ // Removes an event and its players.
206
+ function clearEvent(eventName) {
207
+ // The event's players are deleted along with it (ON DELETE CASCADE).
208
+ return db.prepare('DELETE FROM events WHERE name = ?').run(eventName).changes > 0
209
+ }
192
210
 
193
211
  // Wipes every event and player from the database.
194
212
  const clearDatabase = db.transaction(() => {
195
- db.exec('DELETE FROM event_players DELETE FROM events DELETE FROM players')
213
+ db.exec('DELETE FROM players; DELETE FROM events;')
196
214
  })
197
215
 
198
216
  export {
199
217
  savePlayer,
218
+ getPlayerById,
200
219
  getPlayer,
220
+ getPlayerHistory,
201
221
  searchPlayers,
202
222
  getAllPlayers,
203
223
  getRandomPlayer,
204
224
  saveEvent,
205
- addPlayerToEvent,
206
225
  getEvents,
207
226
  getEventByName,
227
+ getEventId,
208
228
  getPlayersByEvent,
209
229
  getRandomPlayerFromEvent,
210
230
  emptyEvent,
@@ -24,7 +24,7 @@ async function scrapePlayer(url, region) {
24
24
  }
25
25
  })
26
26
 
27
- const $ = cheerio.load(scrapeData.data.html);
27
+ const $ = cheerio.load(scrapeData.data.html)
28
28
  const agentsPlayed = findAgents($)
29
29
 
30
30
  return {
@@ -1,3 +1,4 @@
1
+ import * as cheerio from 'cheerio'
1
2
  import { Hyperbrowser } from "@hyperbrowser/sdk"
2
3
  import { config } from "dotenv";
3
4
 
@@ -19,24 +20,50 @@ async function scrapeWebsite(url) {
19
20
  url: url,
20
21
  scrapeOptions: {
21
22
  formats: ["html"],
22
- includeTags: [".event-team-players-item"]
23
+ includeTags: [".event-header-main-title", ".event-team-players-item"]
23
24
  },
24
25
  })
26
+ const data = scrapeData.data.html
27
+ const $ = cheerio.load(data)
25
28
 
26
- const players = scrapeData.data.html.split('\n').filter(line => line.includes('event-team-players-item'))
27
- const playerLinks = players.map(player => {
28
- const match = player.match(/href="(https:\/\/www\.vlr\.gg\/player\/[^"]*)"/)
29
- return match ? match[1] + "/?timespan=all" : null;
30
- }).filter(link => link !== null);
31
-
32
- return playerLinks
29
+ /*
30
+ <h1 class="event-header-main-title">
31
+ VCT 2027: America Kickoff - North America Open Qualifier
32
+ </h1>
33
+ */
34
+ const playerLinks = $('.event-team-players-item')
35
+ .map((i, el) => $(el).attr('href'))
36
+ .get()
37
+ .filter(href => href.includes('/player/'))
38
+ .map(href => href + '/?timespan=all')
39
+
40
+ let region = $('.event-header-main-title').first().text().trim()
41
+ console.log(region)
42
+
43
+ if (region.includes('America')) {
44
+ region = 'AMER'
45
+ }
46
+ else if (region.includes('EMEA')) {
47
+ region = 'EMEA'
48
+ }
49
+ else if (region.includes('Pacific')) {
50
+ region = 'PAC'
51
+ }
52
+ else if (region.includes('China')) {
53
+ region = 'CN'
54
+ }
55
+ else {
56
+ region = null
57
+ }
58
+
59
+ return { playerLinks, region }
33
60
  }
34
61
  catch (error) {
35
62
  console.error("Error scraping website:", error)
36
63
  }
37
64
  }
38
65
 
39
- export { scrapeWebsite };
66
+ export { scrapeWebsite }
40
67
  /*
41
68
  node teamscraper.mjs
42
69
  */