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 +19 -0
- package/index.mjs +1 -0
- package/main.mjs +138 -91
- package/package.json +1 -1
- package/scripts/db.mjs +53 -45
- package/scripts/player-info-scraper.mjs +126 -127
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
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
&& !ele.includes('
|
|
105
|
-
&& !ele.includes('
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// console.log(
|
|
124
|
-
|
|
125
|
-
|
|
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
|