vlr-stats-scraper 1.4.1 → 1.5.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
@@ -24,6 +24,7 @@ The package installs a `valstats` command.
24
24
  | Command | What it does |
25
25
  | --- | --- |
26
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 |
27
28
  | `valstats --clear-event <event-name>` | Remove one event and its players |
28
29
  | `valstats --clear` | Wipe the whole database (asks for confirmation) |
29
30
 
@@ -55,6 +56,23 @@ Running it again with an event name and year that are already saved replaces tha
55
56
  players with the freshly scraped ones. A player already in the database is updated with the
56
57
  new data rather than duplicated.
57
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
+
58
76
  ### Clearing data
59
77
 
60
78
  ```bash
@@ -127,6 +145,7 @@ const pick = getRandomPlayerFromEvent('Stage 2');
127
145
  | --- | --- | --- |
128
146
  | `savePlayer(player)` | — | Inserts a player, or updates them if one with the same name exists. |
129
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. |
130
149
  | `getAllPlayers()` | `Player[]` | Every player in the database. |
131
150
  | `getRandomPlayer()` | `Player \| null` | A random player from the whole database. |
132
151
 
package/index.mjs CHANGED
@@ -3,6 +3,7 @@ export { scrapeWebsite } from './scripts/player-link-scraper.mjs';
3
3
  export {
4
4
  savePlayer,
5
5
  getPlayer,
6
+ searchPlayers,
6
7
  getAllPlayers,
7
8
  getRandomPlayer,
8
9
  saveEvent,
package/main.mjs CHANGED
@@ -1,91 +1,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
- addPlayerToEvent,
9
- getEventByName,
10
- emptyEvent,
11
- clearEvent,
12
- clearDatabase,
13
- } from './scripts/db.mjs';
14
-
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;
45
-
46
- if (!link || !year || !eventName) {
47
- console.error('Usage: valstats <link> <year> <event-name>');
48
- console.error(' valstats --clear-event <event-name>');
49
- console.error(' valstats --clear');
50
- process.exit(1);
51
- }
52
-
53
- const isStage = eventName.toLowerCase().includes('stage');
54
-
55
- let region = null;
56
- if (isStage) {
57
- const prompt = promptSync();
58
- region = prompt('Enter the region: ');
59
- }
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
-
68
- const eventId = saveEvent({
69
- name: eventName,
70
- url: link,
71
- region,
72
- year: parseInt(year, 10),
73
- stage: null,
74
- });
75
-
76
- const links = await scrapeWebsite(link);
77
-
78
- if (!links) {
79
- console.error(`Couldn't get the player list from ${link}`);
80
- process.exit(1);
81
- }
82
-
83
- for (const playerLink of links) {
84
- const player = await scrapePlayer(playerLink, region);
85
- if (player) {
86
- savePlayer(player);
87
- addPlayerToEvent(eventId, player.name);
88
- console.log(`Saved ${player.name} to players.db under "${eventName}"`);
89
- console.log(player);
90
- }
91
- }
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vlr-stats-scraper",
3
- "version": "1.4.1",
3
+ "version": "1.5.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
@@ -1,7 +1,7 @@
1
- import Database from 'better-sqlite3';
2
- import path from 'path';
1
+ import Database from 'better-sqlite3'
2
+ import path from 'path'
3
3
 
4
- const db = new Database(path.join(process.cwd(), 'players.db'));
4
+ const db = new Database(path.join(process.cwd(), 'players.db'))
5
5
 
6
6
  db.exec(`
7
7
  CREATE TABLE IF NOT EXISTS players (
@@ -15,20 +15,20 @@ db.exec(`
15
15
  region TEXT,
16
16
  stats TEXT
17
17
  )
18
- `);
18
+ `)
19
19
 
20
- const existingPlayerColumns = db.prepare('PRAGMA table_info(players)').all().map(col => col.name);
20
+ const existingPlayerColumns = db.prepare('PRAGMA table_info(players)').all().map(col => col.name)
21
21
  if (!existingPlayerColumns.includes('stats')) {
22
- db.exec('ALTER TABLE players ADD COLUMN stats TEXT');
22
+ db.exec('ALTER TABLE players ADD COLUMN stats TEXT')
23
23
  }
24
24
 
25
- const existingTableNames = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(t => t.name);
25
+ const existingTableNames = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(t => t.name)
26
26
  if (existingTableNames.includes('tournaments') && !existingTableNames.includes('events')) {
27
- db.exec('ALTER TABLE tournaments RENAME TO events');
27
+ db.exec('ALTER TABLE tournaments RENAME TO events')
28
28
  }
29
29
  if (existingTableNames.includes('tournament_players') && !existingTableNames.includes('event_players')) {
30
- db.exec('ALTER TABLE tournament_players RENAME TO event_players');
31
- db.exec('ALTER TABLE event_players RENAME COLUMN tournament_id TO event_id');
30
+ db.exec('ALTER TABLE tournament_players RENAME TO event_players')
31
+ db.exec('ALTER TABLE event_players RENAME COLUMN tournament_id TO event_id')
32
32
  }
33
33
 
34
34
  db.exec(`
@@ -40,14 +40,14 @@ db.exec(`
40
40
  year INTEGER,
41
41
  stage INTEGER
42
42
  )
43
- `);
43
+ `)
44
44
 
45
- const existingEventColumns = db.prepare('PRAGMA table_info(events)').all().map(col => col.name);
45
+ const existingEventColumns = db.prepare('PRAGMA table_info(events)').all().map(col => col.name)
46
46
  if (!existingEventColumns.includes('year')) {
47
- db.exec('ALTER TABLE events ADD COLUMN year INTEGER');
47
+ db.exec('ALTER TABLE events ADD COLUMN year INTEGER')
48
48
  }
49
49
  if (!existingEventColumns.includes('stage')) {
50
- db.exec('ALTER TABLE events ADD COLUMN stage INTEGER');
50
+ db.exec('ALTER TABLE events ADD COLUMN stage INTEGER')
51
51
  }
52
52
 
53
53
  db.exec(`
@@ -56,7 +56,7 @@ db.exec(`
56
56
  player_name TEXT NOT NULL REFERENCES players(name),
57
57
  PRIMARY KEY (event_id, player_name)
58
58
  )
59
- `);
59
+ `)
60
60
 
61
61
  const upsertPlayerStmt = db.prepare(`
62
62
  INSERT INTO players (name, team, past_teams, winnings, most_played_agents, avg_kda, nationality, region, stats)
@@ -70,7 +70,7 @@ const upsertPlayerStmt = db.prepare(`
70
70
  nationality = excluded.nationality,
71
71
  region = excluded.region,
72
72
  stats = excluded.stats
73
- `);
73
+ `)
74
74
 
75
75
  const upsertEventStmt = db.prepare(`
76
76
  INSERT INTO events (name, url, region, year, stage)
@@ -80,21 +80,21 @@ const upsertEventStmt = db.prepare(`
80
80
  region = excluded.region,
81
81
  year = excluded.year,
82
82
  stage = excluded.stage
83
- `);
83
+ `)
84
84
 
85
85
  const linkPlayerToEventStmt = db.prepare(`
86
86
  INSERT OR IGNORE INTO event_players (event_id, player_name)
87
87
  VALUES (@eventId, @playerName)
88
- `);
88
+ `)
89
89
 
90
90
  function rowToPlayer(row) {
91
- if (!row) return null;
91
+ if (!row) return null
92
92
  return {
93
93
  ...row,
94
94
  past_teams: JSON.parse(row.past_teams),
95
95
  most_played_agents: JSON.parse(row.most_played_agents),
96
96
  stats: JSON.parse(row.stats),
97
- };
97
+ }
98
98
  }
99
99
 
100
100
  function savePlayer(player) {
@@ -103,19 +103,26 @@ function savePlayer(player) {
103
103
  past_teams: JSON.stringify(player.past_teams),
104
104
  most_played_agents: JSON.stringify(player.most_played_agents),
105
105
  stats: JSON.stringify(player.stats),
106
- });
106
+ })
107
107
  }
108
108
 
109
109
  function getPlayer(name) {
110
- return rowToPlayer(db.prepare('SELECT * FROM players WHERE name = ?').get(name));
110
+ return rowToPlayer(db.prepare('SELECT * FROM players WHERE name = ?').get(name))
111
+ }
112
+
113
+ // Case-insensitive partial match, e.g. "ten" finds "tenz" and "tenten".
114
+ function searchPlayers(query) {
115
+ return db.prepare("SELECT * FROM players WHERE name LIKE ? ESCAPE '\\' ORDER BY name")
116
+ .all(`%${query.replace(/[\\%_]/g, '\\$&')}%`)
117
+ .map(rowToPlayer)
111
118
  }
112
119
 
113
120
  function getAllPlayers() {
114
- return db.prepare('SELECT * FROM players').all().map(rowToPlayer);
121
+ return db.prepare('SELECT * FROM players').all().map(rowToPlayer)
115
122
  }
116
123
 
117
124
  function getRandomPlayer() {
118
- return rowToPlayer(db.prepare('SELECT * FROM players ORDER BY RANDOM() LIMIT 1').get());
125
+ return rowToPlayer(db.prepare('SELECT * FROM players ORDER BY RANDOM() LIMIT 1').get())
119
126
  }
120
127
 
121
128
  // Creates the event (tournament, stage, etc.) if it doesn't exist yet, and returns its id either way.
@@ -126,20 +133,20 @@ function saveEvent({ name, url, region, year, stage }) {
126
133
  region: region ?? null,
127
134
  year: year ?? null,
128
135
  stage: stage ?? null,
129
- });
130
- return db.prepare('SELECT id FROM events WHERE name = ?').get(name).id;
136
+ })
137
+ return db.prepare('SELECT id FROM events WHERE name = ?').get(name).id
131
138
  }
132
139
 
133
140
  function addPlayerToEvent(eventId, playerName) {
134
- linkPlayerToEventStmt.run({ eventId, playerName });
141
+ linkPlayerToEventStmt.run({ eventId, playerName })
135
142
  }
136
143
 
137
144
  function getEvents() {
138
- return db.prepare('SELECT * FROM events').all();
145
+ return db.prepare('SELECT * FROM events').all()
139
146
  }
140
147
 
141
148
  function getEventByName(name) {
142
- return db.prepare('SELECT * FROM events WHERE name = ?').get(name);
149
+ return db.prepare('SELECT * FROM events WHERE name = ?').get(name)
143
150
  }
144
151
 
145
152
  function getPlayersByEvent(eventName) {
@@ -148,7 +155,7 @@ function getPlayersByEvent(eventName) {
148
155
  JOIN event_players ON event_players.player_name = players.name
149
156
  JOIN events ON events.id = event_players.event_id
150
157
  WHERE events.name = ?
151
- `).all(eventName).map(rowToPlayer);
158
+ `).all(eventName).map(rowToPlayer)
152
159
  }
153
160
 
154
161
  function getRandomPlayerFromEvent(eventName) {
@@ -158,39 +165,40 @@ function getRandomPlayerFromEvent(eventName) {
158
165
  JOIN events ON events.id = event_players.event_id
159
166
  WHERE events.name = ?
160
167
  ORDER BY RANDOM() LIMIT 1
161
- `).get(eventName));
168
+ `).get(eventName))
162
169
  }
163
170
 
164
171
  // Deletes players that no longer belong to any event.
165
172
  function deleteOrphanedPlayers() {
166
- db.prepare('DELETE FROM players WHERE name NOT IN (SELECT player_name FROM event_players)').run();
173
+ db.prepare('DELETE FROM players WHERE name NOT IN (SELECT player_name FROM event_players)').run()
167
174
  }
168
175
 
169
176
  // Removes every player from an event but keeps the event itself, so it can be re-scraped.
170
177
  // Players that are still in other events are kept.
171
178
  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
- });
179
+ const event = getEventByName(eventName)
180
+ if (!event) return false
181
+ db.prepare('DELETE FROM event_players WHERE event_id = ?').run(event.id)
182
+ deleteOrphanedPlayers()
183
+ return true
184
+ })
178
185
 
179
186
  // Removes an event and its players. Players that are still in other events are kept.
180
187
  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
- });
188
+ if (!emptyEvent(eventName)) return false
189
+ db.prepare('DELETE FROM events WHERE name = ?').run(eventName)
190
+ return true
191
+ })
185
192
 
186
193
  // Wipes every event and player from the database.
187
194
  const clearDatabase = db.transaction(() => {
188
- db.exec('DELETE FROM event_players; DELETE FROM events; DELETE FROM players;');
189
- });
195
+ db.exec('DELETE FROM event_players DELETE FROM events DELETE FROM players')
196
+ })
190
197
 
191
198
  export {
192
199
  savePlayer,
193
200
  getPlayer,
201
+ searchPlayers,
194
202
  getAllPlayers,
195
203
  getRandomPlayer,
196
204
  saveEvent,
@@ -202,4 +210,4 @@ export {
202
210
  emptyEvent,
203
211
  clearEvent,
204
212
  clearDatabase,
205
- };
213
+ }
@@ -1,128 +1,127 @@
1
- import * as cheerio from 'cheerio'
2
- import { Hyperbrowser } from "@hyperbrowser/sdk"
3
- import { config } from "dotenv"
4
-
5
- config()
6
-
7
- let client;
8
- function getClient() {
9
- if (!client) {
10
- client = new Hyperbrowser({
11
- apiKey: process.env.HYPERBROWSER_API_KEY,
12
- })
13
- }
14
- return client;
15
- }
16
-
17
- async function scrapePlayer(url, region) {
18
- // console.log('scrapePlayer ran')
19
- try {
20
- const scrapeData = await getClient().scrape.startAndWait({
21
- url: url,
22
- scrapeOptions: {
23
- formats: ["html"]
24
- }
25
- })
26
-
27
- const $ = cheerio.load(scrapeData.data.html);
28
- const agentsPlayed = findAgents($)
29
-
30
- return {
31
- name: getPlayerName(url),
32
- team: getCurTeam($),
33
- past_teams: getPastTeam($),
34
- winnings: getWinnings($),
35
- most_played_agents: agentsPlayed.map(data => data.Agent),
36
- avg_kda: agentsPlayed.reduce((acc, data) => acc + (parseFloat(data['K:D'])) / agentsPlayed.length, 0).toFixed(2),
37
- nationality: getNationality($),
38
- region: region,
39
- stats: agentsPlayed
40
- }
41
- }
42
- catch (error) {
43
- console.error("Error scraping website:", error);
44
- }
45
- }
46
-
47
- // testing
48
- /*
49
- node scripts/player-info-scraper.mjs
50
- */
51
-
52
- scrapePlayer("https://www.vlr.gg/player/939/mwzera/?timespan=all", "AMER")
53
-
54
-
55
- function getPlayerName(link) {
56
- let name = new URL(link).pathname.split('/').filter(ele => ele !== '').pop()
57
- return name
58
- }
59
-
60
- function findAgents($) {
61
- // console.log('findAgents ran')
62
- const header = $('.mod-agent-rows').first().children().first()
63
-
64
- const statCategories = header.text().split('\n').map(ele => ele.trim()).filter(ele => ele !== '')
65
- statCategories.unshift('Agent')
66
-
67
- const agentStats = header.next().children()
68
- const agents = agentStats.map((i, el) => {
69
- el = $(el)
70
- // console.log('agent found:', el.children().first().find('img').attr('alt'))?.trim()
71
- return {
72
- agent: el.children().first().find('img').attr('alt')?.trim(),
73
- stats: el.text().split('\t').map(ele => ele.trim()).filter(ele => ele !== '')
74
- }
75
- }).get()
76
-
77
- const agentData = agents.map(agent => {
78
- return statCategories.reduce((acc, category, index) => {
79
- if(index == 0) {
80
- acc[category] = agent.agent
81
- }
82
- else {
83
- acc[category] = agent.stats[index-1]
84
- }
85
- return acc
86
- }, {})
87
- })
88
-
89
- return agentData.length<3 ? agentData : agentData.slice(0,3)
90
- }
91
-
92
- function getCurTeam($) {
93
- // console.log('getCurTeam ran')
94
- const currentTeam = $('h2:contains("Current Teams")').first().next().text().trim()
95
- const currentTeamName = currentTeam.split('\t').filter(ele => ele.trim() !== '')
96
- // Free agents have no current team
97
- return currentTeamName.length ? currentTeamName[0].trim() : null
98
- }
99
-
100
- function getPastTeam($) {
101
- // console.log('getPastTeam ran')
102
- const pastTeams = $('h2:contains("Past Teams")').first().next().text().trim()
103
- const pastTeamNames = pastTeams.split('\t').filter(ele => ele.trim() !== ''
104
- && !ele.includes('joined')
105
- && !ele.includes(' – ')
106
- && !ele.includes('left'))
107
- .map(ele => ele.trim())
108
- return pastTeamNames
109
- }
110
-
111
- function getWinnings($) {
112
- // console.log('getWinnings ran')
113
- const events = $('h2:contains("Event Placements")').first().next()
114
- const winnings = events.children().first().text().trim()
115
- // // console.log(winnings.split('\t').map(ele => ele.trim()).filter(ele => ele !== '')[1])
116
- return winnings.split('\t').map(ele => ele.trim()).filter(ele => ele !== '')[1]
117
-
118
- }
119
-
120
- function getNationality($) {
121
- // console.log('getNationality ran')
122
- const header = $('.flag').first()
123
- // console.log('flags found:', $('.flag').length);
124
- // console.log(header.parent().text().trim())
125
- return header.parent().text().trim()
126
- }
127
-
1
+ import * as cheerio from 'cheerio'
2
+ import { Hyperbrowser } from "@hyperbrowser/sdk"
3
+ import { config } from "dotenv"
4
+
5
+ config()
6
+
7
+ let client;
8
+ function getClient() {
9
+ if (!client) {
10
+ client = new Hyperbrowser({
11
+ apiKey: process.env.HYPERBROWSER_API_KEY,
12
+ })
13
+ }
14
+ return client;
15
+ }
16
+
17
+ async function scrapePlayer(url, region) {
18
+ // console.log('scrapePlayer ran')
19
+ try {
20
+ const scrapeData = await getClient().scrape.startAndWait({
21
+ url: url,
22
+ scrapeOptions: {
23
+ formats: ["html"]
24
+ }
25
+ })
26
+
27
+ const $ = cheerio.load(scrapeData.data.html);
28
+ const agentsPlayed = findAgents($)
29
+
30
+ return {
31
+ name: getPlayerName(url),
32
+ team: getCurTeam($),
33
+ past_teams: getPastTeam($),
34
+ winnings: getWinnings($),
35
+ most_played_agents: agentsPlayed.map(data => data.Agent),
36
+ avg_kda: agentsPlayed.reduce((acc, data) => acc + (parseFloat(data['K:D'])) / agentsPlayed.length, 0).toFixed(2),
37
+ nationality: getNationality($),
38
+ region: region,
39
+ stats: agentsPlayed
40
+ }
41
+ }
42
+ catch (error) {
43
+ console.error("Error scraping website:", error);
44
+ }
45
+ }
46
+
47
+ // testing
48
+ /*
49
+ node scripts/player-info-scraper.mjs
50
+ scrapePlayer("https://www.vlr.gg/player/939/mwzera/?timespan=all", "AMER")
51
+ */
52
+
53
+
54
+ function getPlayerName(link) {
55
+ let name = new URL(link).pathname.split('/').filter(ele => ele !== '').pop()
56
+ return name
57
+ }
58
+
59
+ function findAgents($) {
60
+ // console.log('findAgents ran')
61
+ const header = $('.mod-agent-rows').first().children().first()
62
+
63
+ const statCategories = header.text().split('\n').map(ele => ele.trim()).filter(ele => ele !== '')
64
+ statCategories.unshift('Agent')
65
+
66
+ const agentStats = header.next().children()
67
+ const agents = agentStats.map((i, el) => {
68
+ el = $(el)
69
+ // console.log('agent found:', el.children().first().find('img').attr('alt'))?.trim()
70
+ return {
71
+ agent: el.children().first().find('img').attr('alt')?.trim(),
72
+ stats: el.text().split('\t').map(ele => ele.trim()).filter(ele => ele !== '')
73
+ }
74
+ }).get()
75
+
76
+ const agentData = agents.map(agent => {
77
+ return statCategories.reduce((acc, category, index) => {
78
+ if(index == 0) {
79
+ acc[category] = agent.agent
80
+ }
81
+ else {
82
+ acc[category] = agent.stats[index-1]
83
+ }
84
+ return acc
85
+ }, {})
86
+ })
87
+
88
+ return agentData.length<3 ? agentData : agentData.slice(0,3)
89
+ }
90
+
91
+ function getCurTeam($) {
92
+ // console.log('getCurTeam ran')
93
+ const currentTeam = $('h2:contains("Current Teams")').first().next().text().trim()
94
+ const currentTeamName = currentTeam.split('\t').filter(ele => ele.trim() !== '')
95
+ // Free agents have no current team
96
+ return currentTeamName.length ? currentTeamName[0].trim() : null
97
+ }
98
+
99
+ function getPastTeam($) {
100
+ // console.log('getPastTeam ran')
101
+ const pastTeams = $('h2:contains("Past Teams")').first().next().text().trim()
102
+ const pastTeamNames = pastTeams.split('\t').filter(ele => ele.trim() !== ''
103
+ && !ele.includes('joined')
104
+ && !ele.includes(' – ')
105
+ && !ele.includes('left'))
106
+ .map(ele => ele.trim())
107
+ return pastTeamNames
108
+ }
109
+
110
+ function getWinnings($) {
111
+ // console.log('getWinnings ran')
112
+ const events = $('h2:contains("Event Placements")').first().next()
113
+ const winnings = events.children().first().text().trim()
114
+ // // console.log(winnings.split('\t').map(ele => ele.trim()).filter(ele => ele !== '')[1])
115
+ return winnings.split('\t').map(ele => ele.trim()).filter(ele => ele !== '')[1]
116
+
117
+ }
118
+
119
+ function getNationality($) {
120
+ // console.log('getNationality ran')
121
+ const header = $('.flag').first()
122
+ // console.log('flags found:', $('.flag').length);
123
+ // console.log(header.parent().text().trim())
124
+ return header.parent().text().trim()
125
+ }
126
+
128
127
  export default scrapePlayer