vlr-stats-scraper 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -170
- package/index.mjs +20 -17
- package/main.mjs +122 -47
- package/package.json +1 -1
- package/scripts/db.mjs +130 -102
- package/scripts/player-info-scraper.mjs +126 -127
- package/scripts/player-link-scraper.mjs +36 -9
package/main.mjs
CHANGED
|
@@ -1,91 +1,166 @@
|
|
|
1
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'
|
|
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
5
|
import {
|
|
6
6
|
savePlayer,
|
|
7
7
|
saveEvent,
|
|
8
|
-
addPlayerToEvent,
|
|
9
8
|
getEventByName,
|
|
9
|
+
getEventId,
|
|
10
|
+
getPlayersByEvent,
|
|
10
11
|
emptyEvent,
|
|
11
12
|
clearEvent,
|
|
12
13
|
clearDatabase,
|
|
13
|
-
|
|
14
|
+
searchPlayers,
|
|
15
|
+
} from './scripts/db.mjs'
|
|
14
16
|
|
|
15
|
-
const args = process.argv.slice(2)
|
|
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
|
+
}
|
|
16
95
|
|
|
17
96
|
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: ')
|
|
97
|
+
const prompt = promptSync()
|
|
98
|
+
const answer = prompt('This deletes every event and player in players.db. Type "yes" to continue: ')
|
|
20
99
|
if (answer?.trim().toLowerCase() === 'yes') {
|
|
21
|
-
clearDatabase()
|
|
22
|
-
console.log('Cleared players.db')
|
|
100
|
+
clearDatabase()
|
|
101
|
+
console.log('Cleared players.db')
|
|
23
102
|
} else {
|
|
24
|
-
console.log('Cancelled')
|
|
103
|
+
console.log('Cancelled')
|
|
25
104
|
}
|
|
26
|
-
process.exit(0)
|
|
105
|
+
process.exit(0)
|
|
27
106
|
}
|
|
28
107
|
|
|
29
108
|
if (args[0] === '--clear-event') {
|
|
30
|
-
const name = args[1]
|
|
109
|
+
const name = args[1]
|
|
31
110
|
if (!name) {
|
|
32
|
-
console.error('Usage: valstats --clear-event <event-name>')
|
|
33
|
-
process.exit(1)
|
|
111
|
+
console.error('Usage: valstats --clear-event <event-name>')
|
|
112
|
+
process.exit(1)
|
|
34
113
|
}
|
|
35
114
|
if (clearEvent(name)) {
|
|
36
|
-
console.log(`Removed "${name}" and its players from players.db`)
|
|
115
|
+
console.log(`Removed "${name}" and its players from players.db`)
|
|
37
116
|
} else {
|
|
38
|
-
console.error(`No event named "${name}" in players.db`)
|
|
39
|
-
process.exit(1)
|
|
117
|
+
console.error(`No event named "${name}" in players.db`)
|
|
118
|
+
process.exit(1)
|
|
40
119
|
}
|
|
41
|
-
process.exit(0)
|
|
120
|
+
process.exit(0)
|
|
42
121
|
}
|
|
43
122
|
|
|
44
|
-
const [link, year, eventName] = args
|
|
123
|
+
const [link, year, eventName] = args
|
|
45
124
|
|
|
46
125
|
if (!link || !year || !eventName) {
|
|
47
|
-
console.error('Usage: valstats <link> <year> <event-name>')
|
|
48
|
-
console.error(' valstats --
|
|
49
|
-
console.error(' valstats --
|
|
50
|
-
|
|
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)
|
|
51
132
|
}
|
|
52
133
|
|
|
53
|
-
|
|
134
|
+
// Scrape first so a failed scrape doesn't wipe an event that's already saved.
|
|
135
|
+
const scraped = await scrapeWebsite(link)
|
|
54
136
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
region = prompt('Enter the region: ');
|
|
137
|
+
if (!scraped) {
|
|
138
|
+
console.error(`Couldn't get the player list from ${link}`)
|
|
139
|
+
process.exit(1)
|
|
59
140
|
}
|
|
60
141
|
|
|
142
|
+
const { playerLinks, region } = scraped
|
|
143
|
+
console.log(`Found ${playerLinks.length} players, region: ${region ?? 'unknown'}`)
|
|
144
|
+
|
|
61
145
|
// Re-adding an event + year that's already saved replaces its old players.
|
|
62
|
-
const existing = getEventByName(eventName)
|
|
146
|
+
const existing = getEventByName(eventName)
|
|
63
147
|
if (existing && existing.year === parseInt(year, 10)) {
|
|
64
|
-
emptyEvent(eventName)
|
|
65
|
-
console.log(`"${eventName}" (${year}) is already in players.db, replacing it`)
|
|
148
|
+
emptyEvent(eventName)
|
|
149
|
+
console.log(`"${eventName}" (${year}) is already in players.db, replacing it`)
|
|
66
150
|
}
|
|
67
151
|
|
|
68
152
|
const eventId = saveEvent({
|
|
69
153
|
name: eventName,
|
|
70
154
|
url: link,
|
|
71
|
-
region,
|
|
72
155
|
year: parseInt(year, 10),
|
|
73
156
|
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
|
-
}
|
|
157
|
+
})
|
|
82
158
|
|
|
83
|
-
for (const playerLink of
|
|
84
|
-
const player = await scrapePlayer(playerLink, region)
|
|
159
|
+
for (const playerLink of playerLinks) {
|
|
160
|
+
const player = await scrapePlayer(playerLink, region)
|
|
85
161
|
if (player) {
|
|
86
|
-
savePlayer(player)
|
|
87
|
-
|
|
88
|
-
console.log(
|
|
89
|
-
console.log(player);
|
|
162
|
+
savePlayer(eventId, player)
|
|
163
|
+
console.log(`Saved ${player.name} to players.db under "${eventName}"`)
|
|
164
|
+
console.log(player)
|
|
90
165
|
}
|
|
91
166
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vlr-stats-scraper",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Scrapes player stats from vlr.gg and stores them in a local SQLite database, grouped by tournament or stage.",
|
|
5
5
|
"homepage": "https://github.com/OverYander/VLR-Webscraper#readme",
|
|
6
6
|
"bugs": {
|
package/scripts/db.mjs
CHANGED
|
@@ -1,34 +1,16 @@
|
|
|
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
|
+
db.pragma('foreign_keys = ON')
|
|
5
6
|
|
|
6
|
-
db.
|
|
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);
|
|
7
|
+
const existingTableNames = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(t => t.name)
|
|
26
8
|
if (existingTableNames.includes('tournaments') && !existingTableNames.includes('events')) {
|
|
27
|
-
db.exec('ALTER TABLE tournaments RENAME TO events')
|
|
9
|
+
db.exec('ALTER TABLE tournaments RENAME TO events')
|
|
28
10
|
}
|
|
29
11
|
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')
|
|
12
|
+
db.exec('ALTER TABLE tournament_players RENAME TO event_players')
|
|
13
|
+
db.exec('ALTER TABLE event_players RENAME COLUMN tournament_id TO event_id')
|
|
32
14
|
}
|
|
33
15
|
|
|
34
16
|
db.exec(`
|
|
@@ -36,32 +18,72 @@ db.exec(`
|
|
|
36
18
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
19
|
name TEXT UNIQUE NOT NULL,
|
|
38
20
|
url TEXT,
|
|
39
|
-
region TEXT,
|
|
40
21
|
year INTEGER,
|
|
41
22
|
stage INTEGER
|
|
42
23
|
)
|
|
43
|
-
`)
|
|
24
|
+
`)
|
|
44
25
|
|
|
45
|
-
const existingEventColumns = db.prepare('PRAGMA table_info(events)').all().map(col => col.name)
|
|
26
|
+
const existingEventColumns = db.prepare('PRAGMA table_info(events)').all().map(col => col.name)
|
|
46
27
|
if (!existingEventColumns.includes('year')) {
|
|
47
|
-
db.exec('ALTER TABLE events ADD COLUMN year INTEGER')
|
|
28
|
+
db.exec('ALTER TABLE events ADD COLUMN year INTEGER')
|
|
48
29
|
}
|
|
49
30
|
if (!existingEventColumns.includes('stage')) {
|
|
50
|
-
db.exec('ALTER TABLE events ADD COLUMN stage INTEGER')
|
|
31
|
+
db.exec('ALTER TABLE events ADD COLUMN stage INTEGER')
|
|
32
|
+
}
|
|
33
|
+
// Region is stored on each player, not on the event.
|
|
34
|
+
if (existingEventColumns.includes('region')) {
|
|
35
|
+
db.exec('ALTER TABLE events DROP COLUMN region')
|
|
51
36
|
}
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
PRIMARY KEY
|
|
38
|
+
// Each row is a player as they were in one event, so the same player can have a row per event
|
|
39
|
+
// with different stats.
|
|
40
|
+
const createPlayersTableSql = (tableName) => `
|
|
41
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
|
44
|
+
name TEXT NOT NULL,
|
|
45
|
+
team TEXT,
|
|
46
|
+
past_teams TEXT,
|
|
47
|
+
winnings TEXT,
|
|
48
|
+
most_played_agents TEXT,
|
|
49
|
+
avg_kda REAL,
|
|
50
|
+
nationality TEXT,
|
|
51
|
+
region TEXT,
|
|
52
|
+
stats TEXT,
|
|
53
|
+
UNIQUE (event_id, name)
|
|
58
54
|
)
|
|
59
|
-
`
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
// Older databases kept one shared row per player and linked them to events through event_players.
|
|
58
|
+
// Copy each of those links into its own per-event player row.
|
|
59
|
+
const existingPlayerColumns = db.prepare('PRAGMA table_info(players)').all().map(col => col.name)
|
|
60
|
+
if (existingPlayerColumns.length && !existingPlayerColumns.includes('event_id')) {
|
|
61
|
+
db.transaction(() => {
|
|
62
|
+
if (!existingPlayerColumns.includes('stats')) {
|
|
63
|
+
db.exec('ALTER TABLE players ADD COLUMN stats TEXT')
|
|
64
|
+
}
|
|
65
|
+
db.exec(createPlayersTableSql('players_new'))
|
|
66
|
+
db.exec(`
|
|
67
|
+
INSERT INTO players_new (event_id, name, team, past_teams, winnings, most_played_agents, avg_kda, nationality, region, stats)
|
|
68
|
+
SELECT event_players.event_id, players.name, players.team, players.past_teams, players.winnings,
|
|
69
|
+
players.most_played_agents, players.avg_kda, players.nationality, players.region, players.stats
|
|
70
|
+
FROM event_players
|
|
71
|
+
JOIN players ON players.name = event_players.player_name
|
|
72
|
+
JOIN events ON events.id = event_players.event_id
|
|
73
|
+
`)
|
|
74
|
+
db.exec('DROP TABLE event_players')
|
|
75
|
+
db.exec('DROP TABLE players')
|
|
76
|
+
db.exec('ALTER TABLE players_new RENAME TO players')
|
|
77
|
+
})()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
db.exec(createPlayersTableSql('players'))
|
|
81
|
+
db.exec('DROP TABLE IF EXISTS event_players')
|
|
60
82
|
|
|
61
83
|
const upsertPlayerStmt = db.prepare(`
|
|
62
|
-
INSERT INTO players (name, team, past_teams, winnings, most_played_agents, avg_kda, nationality, region, stats)
|
|
63
|
-
VALUES (@name, @team, @past_teams, @winnings, @most_played_agents, @avg_kda, @nationality, @region, @stats)
|
|
64
|
-
ON CONFLICT(name) DO UPDATE SET
|
|
84
|
+
INSERT INTO players (event_id, name, team, past_teams, winnings, most_played_agents, avg_kda, nationality, region, stats)
|
|
85
|
+
VALUES (@event_id, @name, @team, @past_teams, @winnings, @most_played_agents, @avg_kda, @nationality, @region, @stats)
|
|
86
|
+
ON CONFLICT(event_id, name) DO UPDATE SET
|
|
65
87
|
team = excluded.team,
|
|
66
88
|
past_teams = excluded.past_teams,
|
|
67
89
|
winnings = excluded.winnings,
|
|
@@ -70,136 +92,142 @@ const upsertPlayerStmt = db.prepare(`
|
|
|
70
92
|
nationality = excluded.nationality,
|
|
71
93
|
region = excluded.region,
|
|
72
94
|
stats = excluded.stats
|
|
73
|
-
`)
|
|
95
|
+
`)
|
|
74
96
|
|
|
75
97
|
const upsertEventStmt = db.prepare(`
|
|
76
|
-
INSERT INTO events (name, url,
|
|
77
|
-
VALUES (@name, @url, @
|
|
98
|
+
INSERT INTO events (name, url, year, stage)
|
|
99
|
+
VALUES (@name, @url, @year, @stage)
|
|
78
100
|
ON CONFLICT(name) DO UPDATE SET
|
|
79
101
|
url = excluded.url,
|
|
80
|
-
region = excluded.region,
|
|
81
102
|
year = excluded.year,
|
|
82
103
|
stage = excluded.stage
|
|
83
|
-
`)
|
|
104
|
+
`)
|
|
84
105
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
// Every player query also pulls in the event's name and year, newest event first.
|
|
107
|
+
const selectPlayersSql = `
|
|
108
|
+
SELECT players.*, events.name AS event_name, events.year AS event_year FROM players
|
|
109
|
+
JOIN events ON events.id = players.event_id
|
|
110
|
+
`
|
|
111
|
+
const newestFirst = 'ORDER BY events.year DESC, players.id DESC'
|
|
89
112
|
|
|
90
113
|
function rowToPlayer(row) {
|
|
91
|
-
if (!row) return null
|
|
114
|
+
if (!row) return null
|
|
92
115
|
return {
|
|
93
116
|
...row,
|
|
94
117
|
past_teams: JSON.parse(row.past_teams),
|
|
95
118
|
most_played_agents: JSON.parse(row.most_played_agents),
|
|
96
119
|
stats: JSON.parse(row.stats),
|
|
97
|
-
}
|
|
120
|
+
}
|
|
98
121
|
}
|
|
99
122
|
|
|
100
|
-
|
|
123
|
+
// Saves a player's stats for one event, replacing any earlier save of them in that same event.
|
|
124
|
+
// Returns the player row's id.
|
|
125
|
+
function savePlayer(eventId, player) {
|
|
101
126
|
upsertPlayerStmt.run({
|
|
102
127
|
...player,
|
|
128
|
+
event_id: eventId,
|
|
103
129
|
past_teams: JSON.stringify(player.past_teams),
|
|
104
130
|
most_played_agents: JSON.stringify(player.most_played_agents),
|
|
105
131
|
stats: JSON.stringify(player.stats),
|
|
106
|
-
})
|
|
132
|
+
})
|
|
133
|
+
return db.prepare('SELECT id FROM players WHERE event_id = ? AND name = ?').get(eventId, player.name).id
|
|
107
134
|
}
|
|
108
135
|
|
|
136
|
+
function getPlayerById(id) {
|
|
137
|
+
return rowToPlayer(db.prepare(`${selectPlayersSql} WHERE players.id = ?`).get(id))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The player as they were in their most recent event.
|
|
109
141
|
function getPlayer(name) {
|
|
110
|
-
return rowToPlayer(db.prepare(
|
|
142
|
+
return rowToPlayer(db.prepare(`${selectPlayersSql} WHERE players.name = ? ${newestFirst} LIMIT 1`).get(name))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Every saved version of a player, one per event, newest first.
|
|
146
|
+
function getPlayerHistory(name) {
|
|
147
|
+
return db.prepare(`${selectPlayersSql} WHERE players.name = ? ${newestFirst}`).all(name).map(rowToPlayer)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Case-insensitive prefix match, e.g. "nat" finds "nats" but not "gnat". Returns one row per event.
|
|
151
|
+
function searchPlayers(query) {
|
|
152
|
+
return db.prepare(`${selectPlayersSql} WHERE players.name LIKE ? ESCAPE '\\' ORDER BY players.name, events.year DESC, players.id DESC`)
|
|
153
|
+
.all(`${query.replace(/[\\%_]/g, '\\$&')}%`)
|
|
154
|
+
.map(rowToPlayer)
|
|
111
155
|
}
|
|
112
156
|
|
|
113
157
|
function getAllPlayers() {
|
|
114
|
-
return db.prepare(
|
|
158
|
+
return db.prepare(selectPlayersSql).all().map(rowToPlayer)
|
|
115
159
|
}
|
|
116
160
|
|
|
117
161
|
function getRandomPlayer() {
|
|
118
|
-
return rowToPlayer(db.prepare(
|
|
162
|
+
return rowToPlayer(db.prepare(`${selectPlayersSql} ORDER BY RANDOM() LIMIT 1`).get())
|
|
119
163
|
}
|
|
120
164
|
|
|
165
|
+
|
|
121
166
|
// Creates the event (tournament, stage, etc.) if it doesn't exist yet, and returns its id either way.
|
|
122
|
-
function saveEvent({ name, url,
|
|
167
|
+
function saveEvent({ name, url, year, stage }) {
|
|
123
168
|
upsertEventStmt.run({
|
|
124
169
|
name,
|
|
125
170
|
url: url ?? null,
|
|
126
|
-
region: region ?? null,
|
|
127
171
|
year: year ?? null,
|
|
128
172
|
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 });
|
|
173
|
+
})
|
|
174
|
+
return db.prepare('SELECT id FROM events WHERE name = ?').get(name).id
|
|
135
175
|
}
|
|
136
176
|
|
|
137
177
|
function getEvents() {
|
|
138
|
-
return db.prepare('SELECT * FROM events').all()
|
|
178
|
+
return db.prepare('SELECT * FROM events').all()
|
|
139
179
|
}
|
|
140
180
|
|
|
141
181
|
function getEventByName(name) {
|
|
142
|
-
return db.prepare('SELECT * FROM events WHERE name = ?').get(name)
|
|
182
|
+
return db.prepare('SELECT * FROM events WHERE name = ?').get(name)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getEventId(name, year) {
|
|
186
|
+
return db.prepare('SELECT id FROM events WHERE name = ? AND year = ?').get(name, year)?.id
|
|
143
187
|
}
|
|
144
188
|
|
|
145
189
|
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);
|
|
190
|
+
return db.prepare(`${selectPlayersSql} WHERE events.name = ?`).all(eventName).map(rowToPlayer)
|
|
152
191
|
}
|
|
153
192
|
|
|
154
193
|
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));
|
|
194
|
+
return rowToPlayer(db.prepare(`${selectPlayersSql} WHERE events.name = ? ORDER BY RANDOM() LIMIT 1`).get(eventName))
|
|
162
195
|
}
|
|
163
196
|
|
|
164
|
-
//
|
|
165
|
-
function
|
|
166
|
-
|
|
197
|
+
// Removes every player from an event but keeps the event itself, so it can be re-scraped.
|
|
198
|
+
function emptyEvent(eventName) {
|
|
199
|
+
const event = getEventByName(eventName)
|
|
200
|
+
if (!event) return false
|
|
201
|
+
db.prepare('DELETE FROM players WHERE event_id = ?').run(event.id)
|
|
202
|
+
return true
|
|
167
203
|
}
|
|
168
204
|
|
|
169
|
-
// Removes
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
db.prepare('DELETE FROM event_players WHERE event_id = ?').run(event.id);
|
|
175
|
-
deleteOrphanedPlayers();
|
|
176
|
-
return true;
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Removes an event and its players. Players that are still in other events are kept.
|
|
180
|
-
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
|
-
});
|
|
205
|
+
// Removes an event and its players.
|
|
206
|
+
function clearEvent(eventName) {
|
|
207
|
+
// The event's players are deleted along with it (ON DELETE CASCADE).
|
|
208
|
+
return db.prepare('DELETE FROM events WHERE name = ?').run(eventName).changes > 0
|
|
209
|
+
}
|
|
185
210
|
|
|
186
211
|
// Wipes every event and player from the database.
|
|
187
212
|
const clearDatabase = db.transaction(() => {
|
|
188
|
-
db.exec('DELETE FROM
|
|
189
|
-
})
|
|
213
|
+
db.exec('DELETE FROM players; DELETE FROM events;')
|
|
214
|
+
})
|
|
190
215
|
|
|
191
216
|
export {
|
|
192
217
|
savePlayer,
|
|
218
|
+
getPlayerById,
|
|
193
219
|
getPlayer,
|
|
220
|
+
getPlayerHistory,
|
|
221
|
+
searchPlayers,
|
|
194
222
|
getAllPlayers,
|
|
195
223
|
getRandomPlayer,
|
|
196
224
|
saveEvent,
|
|
197
|
-
addPlayerToEvent,
|
|
198
225
|
getEvents,
|
|
199
226
|
getEventByName,
|
|
227
|
+
getEventId,
|
|
200
228
|
getPlayersByEvent,
|
|
201
229
|
getRandomPlayerFromEvent,
|
|
202
230
|
emptyEvent,
|
|
203
231
|
clearEvent,
|
|
204
232
|
clearDatabase,
|
|
205
|
-
}
|
|
233
|
+
}
|