vlr-stats-scraper 2.0.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/index.mjs CHANGED
@@ -1,20 +1,27 @@
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';
1
+ export { default as scrapePlayer } from './scripts/player-info-scraper.mjs';
2
+ export { scrapeWebsite } from './scripts/player-link-scraper.mjs';
3
+ export { scrapeEventStats, eventStatsUrl } from './scripts/event-stats-scraper.mjs';
4
+ export {
5
+ openDatabase,
6
+ closeDatabase,
7
+ savePlayer,
8
+ getPlayerById,
9
+ getPlayer,
10
+ getPlayerHistory,
11
+ searchPlayers,
12
+ getAllPlayers,
13
+ getRandomPlayer,
14
+ getPlayersByTeam,
15
+ getEventLeaderboard,
16
+ getAgentLeaderboard,
17
+ saveEvent,
18
+ getEvents,
19
+ getEvent,
20
+ getEventByName,
21
+ getEventId,
22
+ getPlayersByEvent,
23
+ getRandomPlayerFromEvent,
24
+ emptyEvent,
25
+ clearEvent,
26
+ clearDatabase,
27
+ } from './scripts/db.mjs';
package/main.mjs CHANGED
@@ -1,166 +1,194 @@
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
- }
1
+ #!/usr/bin/env node
2
+ import promptSync from 'prompt-sync'
3
+ import { scrapeWebsite } from './scripts/player-link-scraper.mjs'
4
+ import { scrapeEventStats } from './scripts/event-stats-scraper.mjs'
5
+ import scrapePlayer from './scripts/player-info-scraper.mjs'
6
+ import {
7
+ savePlayer,
8
+ saveEvent,
9
+ getEvent,
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
+ const USAGE = [
20
+ 'Usage: valstats <link> <year> <event-name>',
21
+ ' valstats --player <vlr.gg player link> <event-name> <year> [region]',
22
+ ' valstats --search <name | vlr.gg player link>',
23
+ ' valstats --clear-event <event-name> [year]',
24
+ ' valstats --clear',
25
+ ].join('\n')
26
+
27
+ const fmt = (value, digits = 2) => (value === null || value === undefined ? '-' : Number(value).toFixed(digits))
28
+
29
+ function printPlayerStats(player) {
30
+ // Players saved from the database belong to an event; ones scraped live don't.
31
+ const event = player.event_name ? ` (${player.event_name}${player.event_year ? `, ${player.event_year}` : ''})` : ''
32
+ console.log(`\n${player.name}${event}`)
33
+ console.log(` Team: ${player.team ?? 'Free agent'}`)
34
+ console.log(` Past teams: ${player.past_teams?.length ? player.past_teams.join(', ') : 'None'}`)
35
+ console.log(` Nationality: ${player.nationality || 'Unknown'}`)
36
+ console.log(` Region: ${player.region ?? 'Unknown'}`)
37
+ console.log(` Winnings: ${player.winnings === null || player.winnings === undefined ? 'Unknown' : `$${player.winnings.toLocaleString('en-US')}`}`)
38
+ console.log(` Top agents: ${player.most_played_agents?.join(', ') || 'None'}`)
39
+ console.log(` Career K:D: ${fmt(player.avg_kda)} (top agents)`)
40
+ if (player.scraped_at) console.log(` Scraped: ${player.scraped_at.slice(0, 10)}`)
41
+
42
+ const es = player.event_stats
43
+ if (es) {
44
+ const agents = es.agents.map(a => `${a.agent}${a.use_pct !== null ? ` ${a.use_pct}%` : ''}`).join(', ')
45
+ console.log(` This event: ${es.maps ?? '-'} maps, ${es.rounds ?? '-'} rounds on ${agents || 'unknown agents'}`)
46
+ console.log(` Rating ${fmt(es.rating)} | ACS ${fmt(es.acs, 0)} | K:D ${fmt(es.kd)} | ADR ${fmt(es.adr, 1)} | KAST ${fmt(es.kast, 0)}% | HS ${fmt(es.hs_pct, 0)}%`)
47
+ } else if (player.event_name) {
48
+ console.log(' This event: no stats saved')
49
+ }
50
+
51
+ if (player.stats?.length) {
52
+ console.log(' Career stats on top agents:')
53
+ console.table(player.stats.map(({ agent, maps, rounds, rating, acs, kd, kast, adr, kpr, apr, kills, deaths }) =>
54
+ ({ agent, maps, rounds, rating, acs, kd, kast, adr, kpr, apr, kills, deaths })))
55
+ }
56
+ }
57
+
58
+ const withTimespan = url => (url.includes('timespan=') ? url : url.replace(/\/?$/, '/?timespan=all'))
59
+
60
+ // Manually adds one player to an event that's already saved.
61
+ if (args[0] === '--player') {
62
+ const [, query, eventName, eventYear, regionArg] = args
63
+ if (!query?.startsWith('http') || !eventName || !eventYear) {
64
+ console.error('Usage: valstats --player <vlr.gg player link> <event-name> <year> [region]')
65
+ process.exit(1)
66
+ }
67
+
68
+ // Look the event up before scraping so a typo doesn't waste a scrape.
69
+ const event = getEvent(eventName, parseInt(eventYear, 10))
70
+ if (!event) {
71
+ console.error(`No event named "${eventName}" (${eventYear}) in players.db`)
72
+ process.exit(1)
73
+ }
74
+
75
+ // Without a region argument, use the region the event's other players were saved with.
76
+ const region = regionArg ?? getPlayersByEvent(event.name, event.year)[0]?.region ?? null
77
+
78
+ const player = await scrapePlayer(withTimespan(query), region)
79
+ if (!player) {
80
+ console.error(`Couldn't scrape a player from ${query}`)
81
+ process.exit(1)
82
+ }
83
+ if (event.url) {
84
+ const eventStats = await scrapeEventStats(event.url)
85
+ player.event_stats = eventStats?.get(player.vlr_id) ?? null
86
+ }
87
+ savePlayer(event.id, player)
88
+ console.log(`Saved ${player.name} to players.db under "${eventName}" (${eventYear})`)
89
+ printPlayerStats({ ...player, event_name: event.name, event_year: event.year })
90
+ process.exit(0)
91
+ }
92
+
93
+ if (args[0] === '--search') {
94
+ const query = args[1]
95
+ if (!query) {
96
+ console.error('Usage: valstats --search <name | vlr.gg player link>')
97
+ process.exit(1)
98
+ }
99
+
100
+ // A vlr.gg link is scraped live; anything else is searched for in players.db.
101
+ if (query.startsWith('http')) {
102
+ const player = await scrapePlayer(withTimespan(query), null)
103
+ if (!player) {
104
+ console.error(`Couldn't scrape a player from ${query}`)
105
+ process.exit(1)
106
+ }
107
+ printPlayerStats(player)
108
+ process.exit(0)
109
+ }
110
+
111
+ const players = searchPlayers(query)
112
+ if (!players.length) {
113
+ console.error(`No players matching "${query}" in players.db (pass a vlr.gg player link to scrape them live)`)
114
+ process.exit(1)
115
+ }
116
+ console.log(`${players.length} player${players.length === 1 ? '' : 's'} matching "${query}":`)
117
+ players.forEach(printPlayerStats)
118
+ process.exit(0)
119
+ }
120
+
121
+ if (args[0] === '--clear') {
122
+ const prompt = promptSync()
123
+ const answer = prompt('This deletes every event and player in players.db. Type "yes" to continue: ')
124
+ if (answer?.trim().toLowerCase() === 'yes') {
125
+ clearDatabase()
126
+ console.log('Cleared players.db')
127
+ } else {
128
+ console.log('Cancelled')
129
+ }
130
+ process.exit(0)
131
+ }
132
+
133
+ if (args[0] === '--clear-event') {
134
+ const [, name, year] = args
135
+ if (!name) {
136
+ console.error('Usage: valstats --clear-event <event-name> [year]')
137
+ process.exit(1)
138
+ }
139
+ try {
140
+ if (clearEvent(name, year ? parseInt(year, 10) : undefined)) {
141
+ console.log(`Removed "${name}"${year ? ` (${year})` : ''} and its players from players.db`)
142
+ } else {
143
+ console.error(`No event named "${name}"${year ? ` (${year})` : ''} in players.db`)
144
+ process.exit(1)
145
+ }
146
+ } catch (error) {
147
+ console.error(error.message)
148
+ process.exit(1)
149
+ }
150
+ process.exit(0)
151
+ }
152
+
153
+ const [link, yearArg, eventName] = args
154
+ const year = parseInt(yearArg, 10)
155
+
156
+ if (!link || !yearArg || !eventName || Number.isNaN(year)) {
157
+ console.error(USAGE)
158
+ process.exit(1)
159
+ }
160
+
161
+ // Scrape first so a failed scrape doesn't wipe an event that's already saved.
162
+ const scraped = await scrapeWebsite(link)
163
+
164
+ if (!scraped) {
165
+ console.error(`Couldn't get the player list from ${link}`)
166
+ process.exit(1)
167
+ }
168
+
169
+ const { playerLinks, region } = scraped
170
+ console.log(`Found ${playerLinks.length} players, region: ${region ?? 'unknown'}`)
171
+
172
+ // One scrape of the event's stats page gives every player's stats in this event only.
173
+ const eventStats = await scrapeEventStats(link)
174
+ if (!eventStats) {
175
+ console.warn('Couldn\'t get this event\'s stats page; players will be saved without event stats')
176
+ }
177
+
178
+ // Re-adding an event + year that's already saved replaces its old players.
179
+ if (getEvent(eventName, year)) {
180
+ emptyEvent(eventName, year)
181
+ console.log(`"${eventName}" (${year}) is already in players.db, replacing it`)
182
+ }
183
+
184
+ const eventId = saveEvent({ name: eventName, url: link, year, stage: null })
185
+
186
+ for (const playerLink of playerLinks) {
187
+ const player = await scrapePlayer(playerLink, region)
188
+ if (player) {
189
+ player.event_stats = eventStats?.get(player.vlr_id) ?? null
190
+ savePlayer(eventId, player)
191
+ console.log(`Saved ${player.name} to players.db under "${eventName}" (${year})`)
192
+ printPlayerStats({ ...player, event_name: eventName, event_year: year })
193
+ }
194
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vlr-stats-scraper",
3
- "version": "2.0.0",
3
+ "version": "3.1.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": {
@@ -0,0 +1,27 @@
1
+ import { Hyperbrowser } from '@hyperbrowser/sdk'
2
+ import { config } from 'dotenv'
3
+
4
+ config()
5
+
6
+ let client
7
+ function getClient() {
8
+ if (!client) {
9
+ client = new Hyperbrowser({
10
+ apiKey: process.env.HYPERBROWSER_API_KEY,
11
+ })
12
+ }
13
+ return client
14
+ }
15
+
16
+ // Fetches a page's HTML through Hyperbrowser. Throws if the scrape fails.
17
+ async function fetchHtml(url, scrapeOptions = {}) {
18
+ const scrapeData = await getClient().scrape.startAndWait({
19
+ url,
20
+ scrapeOptions: { formats: ['html'], ...scrapeOptions },
21
+ })
22
+ const html = scrapeData?.data?.html
23
+ if (!html) throw new Error(`No HTML returned for ${url}`)
24
+ return html
25
+ }
26
+
27
+ export { fetchHtml }