vlr-stats-scraper 0.0.0-stage → 1.0.1
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/LICENSE +15 -0
- package/README.md +73 -3
- package/index.mjs +14 -0
- package/main.mjs +40 -0
- package/package.json +47 -4
- package/scripts/db.mjs +175 -0
- package/scripts/player-info-scraper.mjs +122 -0
- package/scripts/player-link-scraper.mjs +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, OverYander
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# vlr-stats-scraper
|
|
2
|
+
|
|
3
|
+
Scrapes player stats from [vlr.gg](https://www.vlr.gg) and stores them in a local SQLite
|
|
4
|
+
database, grouped by tournament or stage, so you can pull a backlog of players.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
Requires a [Hyperbrowser](https://hyperbrowser.ai) API key for scraping.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install vlr-stats-scraper
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Create a `.env` file in the directory you run it from:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
HYPERBROWSER_API_KEY=your_key_here
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## CLI usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx valstats <link> <year> <event-name>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx valstats https://www.vlr.gg/event/2977/vct-2026-americas-stage-2/group-stage 2026 "Stage 2"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `link` — the vlr.gg event/group-stage page to scrape.
|
|
31
|
+
- `year` — the year the event took place.
|
|
32
|
+
- `event-name` — a label for this tournament/stage (used to group players together later).
|
|
33
|
+
|
|
34
|
+
Each player on that event page gets scraped and saved to `players.db` in your current
|
|
35
|
+
directory, linked to that event name.
|
|
36
|
+
|
|
37
|
+
## Library usage
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import {
|
|
41
|
+
scrapePlayer,
|
|
42
|
+
scrapeWebsite,
|
|
43
|
+
savePlayer,
|
|
44
|
+
saveEvent,
|
|
45
|
+
addPlayerToEvent,
|
|
46
|
+
getPlayersByEvent,
|
|
47
|
+
getRandomPlayerFromEvent,
|
|
48
|
+
} from 'vlr-stats-scraper';
|
|
49
|
+
|
|
50
|
+
const links = await scrapeWebsite('https://www.vlr.gg/event/.../group-stage');
|
|
51
|
+
const player = await scrapePlayer(links[0], 'AMER');
|
|
52
|
+
savePlayer(player);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Exports
|
|
56
|
+
|
|
57
|
+
- `scrapePlayer(url, region)` — scrapes one player's profile page.
|
|
58
|
+
- `scrapeWebsite(url)` — scrapes an event page for player profile links.
|
|
59
|
+
- `savePlayer(player)` / `getPlayer(name)` / `getAllPlayers()` / `getRandomPlayer()`
|
|
60
|
+
- `saveEvent({ name, url, region, year, stage })` — creates/updates a tournament or stage,
|
|
61
|
+
returns its id.
|
|
62
|
+
- `addPlayerToEvent(eventId, playerName)`
|
|
63
|
+
- `getEvents()` / `getEventByName(name)`
|
|
64
|
+
- `getPlayersByEvent(eventName)` / `getRandomPlayerFromEvent(eventName)`
|
|
65
|
+
|
|
66
|
+
## Data storage
|
|
67
|
+
|
|
68
|
+
Player and event data is stored in a `players.db` SQLite file created in whichever
|
|
69
|
+
directory you run the tool from.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
ISC
|
package/index.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
getAllPlayers,
|
|
7
|
+
getRandomPlayer,
|
|
8
|
+
saveEvent,
|
|
9
|
+
addPlayerToEvent,
|
|
10
|
+
getEvents,
|
|
11
|
+
getEventByName,
|
|
12
|
+
getPlayersByEvent,
|
|
13
|
+
getRandomPlayerFromEvent,
|
|
14
|
+
} from './scripts/db.mjs';
|
package/main.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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 { savePlayer, saveEvent, addPlayerToEvent } from './scripts/db.mjs';
|
|
6
|
+
|
|
7
|
+
const [link, year, eventName] = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
if (!link || !year || !eventName) {
|
|
10
|
+
console.error('Usage: valstats <link> <year> <event-name>');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isStage = eventName.toLowerCase().includes('stage');
|
|
15
|
+
|
|
16
|
+
let region = null;
|
|
17
|
+
if (isStage) {
|
|
18
|
+
const prompt = promptSync();
|
|
19
|
+
region = prompt('Enter the region: ');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const eventId = saveEvent({
|
|
23
|
+
name: eventName,
|
|
24
|
+
url: link,
|
|
25
|
+
region,
|
|
26
|
+
year: parseInt(year, 10),
|
|
27
|
+
stage: null,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const links = await scrapeWebsite(link);
|
|
31
|
+
|
|
32
|
+
for (const playerLink of links) {
|
|
33
|
+
const player = await scrapePlayer(playerLink, region);
|
|
34
|
+
if (player) {
|
|
35
|
+
savePlayer(player);
|
|
36
|
+
addPlayerToEvent(eventId, player.name);
|
|
37
|
+
console.log(`Saved ${player.name} to players.db under "${eventName}"`);
|
|
38
|
+
console.log(player);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vlr-stats-scraper",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Scrapes player stats from vlr.gg and stores them in a local SQLite database, grouped by tournament or stage.",
|
|
5
|
+
"homepage": "https://github.com/OverYander/VLR-Webscraper#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/OverYander/VLR-Webscraper/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/OverYander/VLR-Webscraper.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "OverYander <gavinchen8706@gmail.com>",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./index.mjs",
|
|
17
|
+
"exports": "./index.mjs",
|
|
18
|
+
"bin": {
|
|
19
|
+
"valstats": "main.mjs"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"index.mjs",
|
|
23
|
+
"main.mjs",
|
|
24
|
+
"scripts/",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"vlr",
|
|
30
|
+
"valorant",
|
|
31
|
+
"esports",
|
|
32
|
+
"scraper",
|
|
33
|
+
"sqlite",
|
|
34
|
+
"stats"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@hyperbrowser/sdk": "^0.92.3",
|
|
44
|
+
"better-sqlite3": "^13.0.3",
|
|
45
|
+
"cheerio": "^1.2.0",
|
|
46
|
+
"dotenv": "^17.4.2",
|
|
47
|
+
"prompt-sync": "^4.2.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/scripts/db.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
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
|
+
}
|
|
24
|
+
|
|
25
|
+
const existingTableNames = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(t => t.name);
|
|
26
|
+
if (existingTableNames.includes('tournaments') && !existingTableNames.includes('events')) {
|
|
27
|
+
db.exec('ALTER TABLE tournaments RENAME TO events');
|
|
28
|
+
}
|
|
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');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
db.exec(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
name TEXT UNIQUE NOT NULL,
|
|
38
|
+
url TEXT,
|
|
39
|
+
region TEXT,
|
|
40
|
+
year INTEGER,
|
|
41
|
+
stage INTEGER
|
|
42
|
+
)
|
|
43
|
+
`);
|
|
44
|
+
|
|
45
|
+
const existingEventColumns = db.prepare('PRAGMA table_info(events)').all().map(col => col.name);
|
|
46
|
+
if (!existingEventColumns.includes('year')) {
|
|
47
|
+
db.exec('ALTER TABLE events ADD COLUMN year INTEGER');
|
|
48
|
+
}
|
|
49
|
+
if (!existingEventColumns.includes('stage')) {
|
|
50
|
+
db.exec('ALTER TABLE events ADD COLUMN stage INTEGER');
|
|
51
|
+
}
|
|
52
|
+
|
|
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)
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
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
|
|
65
|
+
team = excluded.team,
|
|
66
|
+
past_teams = excluded.past_teams,
|
|
67
|
+
winnings = excluded.winnings,
|
|
68
|
+
most_played_agents = excluded.most_played_agents,
|
|
69
|
+
avg_kda = excluded.avg_kda,
|
|
70
|
+
nationality = excluded.nationality,
|
|
71
|
+
region = excluded.region,
|
|
72
|
+
stats = excluded.stats
|
|
73
|
+
`);
|
|
74
|
+
|
|
75
|
+
const upsertEventStmt = db.prepare(`
|
|
76
|
+
INSERT INTO events (name, url, region, year, stage)
|
|
77
|
+
VALUES (@name, @url, @region, @year, @stage)
|
|
78
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
79
|
+
url = excluded.url,
|
|
80
|
+
region = excluded.region,
|
|
81
|
+
year = excluded.year,
|
|
82
|
+
stage = excluded.stage
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
const linkPlayerToEventStmt = db.prepare(`
|
|
86
|
+
INSERT OR IGNORE INTO event_players (event_id, player_name)
|
|
87
|
+
VALUES (@eventId, @playerName)
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
function rowToPlayer(row) {
|
|
91
|
+
if (!row) return null;
|
|
92
|
+
return {
|
|
93
|
+
...row,
|
|
94
|
+
past_teams: JSON.parse(row.past_teams),
|
|
95
|
+
most_played_agents: JSON.parse(row.most_played_agents),
|
|
96
|
+
stats: JSON.parse(row.stats),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function savePlayer(player) {
|
|
101
|
+
upsertPlayerStmt.run({
|
|
102
|
+
...player,
|
|
103
|
+
past_teams: JSON.stringify(player.past_teams),
|
|
104
|
+
most_played_agents: JSON.stringify(player.most_played_agents),
|
|
105
|
+
stats: JSON.stringify(player.stats),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getPlayer(name) {
|
|
110
|
+
return rowToPlayer(db.prepare('SELECT * FROM players WHERE name = ?').get(name));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getAllPlayers() {
|
|
114
|
+
return db.prepare('SELECT * FROM players').all().map(rowToPlayer);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getRandomPlayer() {
|
|
118
|
+
return rowToPlayer(db.prepare('SELECT * FROM players ORDER BY RANDOM() LIMIT 1').get());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Creates the event (tournament, stage, etc.) if it doesn't exist yet, and returns its id either way.
|
|
122
|
+
function saveEvent({ name, url, region, year, stage }) {
|
|
123
|
+
upsertEventStmt.run({
|
|
124
|
+
name,
|
|
125
|
+
url: url ?? null,
|
|
126
|
+
region: region ?? null,
|
|
127
|
+
year: year ?? null,
|
|
128
|
+
stage: stage ?? null,
|
|
129
|
+
});
|
|
130
|
+
return db.prepare('SELECT id FROM events WHERE name = ?').get(name).id;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function addPlayerToEvent(eventId, playerName) {
|
|
134
|
+
linkPlayerToEventStmt.run({ eventId, playerName });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getEvents() {
|
|
138
|
+
return db.prepare('SELECT * FROM events').all();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getEventByName(name) {
|
|
142
|
+
return db.prepare('SELECT * FROM events WHERE name = ?').get(name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getPlayersByEvent(eventName) {
|
|
146
|
+
return db.prepare(`
|
|
147
|
+
SELECT players.* FROM players
|
|
148
|
+
JOIN event_players ON event_players.player_name = players.name
|
|
149
|
+
JOIN events ON events.id = event_players.event_id
|
|
150
|
+
WHERE events.name = ?
|
|
151
|
+
`).all(eventName).map(rowToPlayer);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getRandomPlayerFromEvent(eventName) {
|
|
155
|
+
return rowToPlayer(db.prepare(`
|
|
156
|
+
SELECT players.* FROM players
|
|
157
|
+
JOIN event_players ON event_players.player_name = players.name
|
|
158
|
+
JOIN events ON events.id = event_players.event_id
|
|
159
|
+
WHERE events.name = ?
|
|
160
|
+
ORDER BY RANDOM() LIMIT 1
|
|
161
|
+
`).get(eventName));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
savePlayer,
|
|
166
|
+
getPlayer,
|
|
167
|
+
getAllPlayers,
|
|
168
|
+
getRandomPlayer,
|
|
169
|
+
saveEvent,
|
|
170
|
+
addPlayerToEvent,
|
|
171
|
+
getEvents,
|
|
172
|
+
getEventByName,
|
|
173
|
+
getPlayersByEvent,
|
|
174
|
+
getRandomPlayerFromEvent,
|
|
175
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
|
|
29
|
+
const agentsPlayed = findAgents($)
|
|
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),
|
|
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
|
+
export default scrapePlayer;
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
node scripts/player-info-scraper.mjs
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
function getPlayerName(link) {
|
|
54
|
+
console.log('getPlayerName ran')
|
|
55
|
+
return link.substring(link.lastIndexOf('/') + 1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findAgents($) {
|
|
59
|
+
console.log('findAgents ran')
|
|
60
|
+
const header = $('.mod-agent-rows').first().children().first()
|
|
61
|
+
|
|
62
|
+
const statCategories = header.text().split('\n').map(ele => ele.trim()).filter(ele => ele !== '')
|
|
63
|
+
statCategories.unshift('Agent')
|
|
64
|
+
|
|
65
|
+
const agentStats = header.next().children()
|
|
66
|
+
const agents = agentStats.map((i, el) => {
|
|
67
|
+
el = $(el)
|
|
68
|
+
return {
|
|
69
|
+
agent: el.children().first().html().trim(),
|
|
70
|
+
stats: el.text().split('\t').map(ele => ele.trim()).filter(ele => ele !== '')
|
|
71
|
+
}
|
|
72
|
+
}).get()
|
|
73
|
+
|
|
74
|
+
const agentData = agents.map(agent => {
|
|
75
|
+
return statCategories.reduce((acc, category, index) => {
|
|
76
|
+
if(index == 0) {
|
|
77
|
+
acc[category] = agent.agent
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
acc[category] = agent.stats[index-1]
|
|
81
|
+
}
|
|
82
|
+
return acc
|
|
83
|
+
}, {})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return agentData.length<3 ? agentData : agentData.slice(0,3)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getCurTeam($) {
|
|
90
|
+
console.log('getCurTeam ran')
|
|
91
|
+
const currentTeam = $('h2:contains("Current Teams")').first().next().text().trim()
|
|
92
|
+
const currentTeamName = currentTeam.split('\t').filter(ele => ele.trim() !== '')
|
|
93
|
+
return currentTeamName[0].trim()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getPastTeam($) {
|
|
97
|
+
console.log('getPastTeam ran')
|
|
98
|
+
const pastTeams = $('h2:contains("Past Teams")').first().next().text().trim()
|
|
99
|
+
const pastTeamNames = pastTeams.split('\t').filter(ele => ele.trim() !== ''
|
|
100
|
+
&& !ele.includes('joined')
|
|
101
|
+
&& !ele.includes(' – ')
|
|
102
|
+
&& !ele.includes('left'))
|
|
103
|
+
.map(ele => ele.trim())
|
|
104
|
+
return pastTeamNames
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getWinnings($) {
|
|
108
|
+
console.log('getWinnings ran')
|
|
109
|
+
const events = $('h2:contains("Event Placements")').first().next()
|
|
110
|
+
const winnings = events.children().first().text().trim()
|
|
111
|
+
// console.log(winnings.split('\t').map(ele => ele.trim()).filter(ele => ele !== '')[1])
|
|
112
|
+
return winnings.split('\t').map(ele => ele.trim()).filter(ele => ele !== '')[1]
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getNationality($) {
|
|
117
|
+
console.log('getNationality ran')
|
|
118
|
+
const header = $('.flag').first()
|
|
119
|
+
console.log('flags found:', $('.flag').length);
|
|
120
|
+
console.log(header.parent().text().trim())
|
|
121
|
+
return header.parent().text().trim()
|
|
122
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
async function scrapeWebsite(url) {
|
|
17
|
+
try {
|
|
18
|
+
const scrapeData = await getClient().scrape.startAndWait({
|
|
19
|
+
url: url,
|
|
20
|
+
scrapeOptions: {
|
|
21
|
+
formats: ["html"],
|
|
22
|
+
includeTags: [".event-team-players-item"]
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
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
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error("Error scraping website:", error)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { scrapeWebsite };
|
|
40
|
+
/*
|
|
41
|
+
node teamscraper.mjs
|
|
42
|
+
*/
|