mtg-playerinfo 1.2.0 → 1.3.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/.github/workflows/ci.yml +29 -3
- package/.github/workflows/update-test-data.yml +33 -0
- package/README.md +16 -11
- package/cli.js +30 -13
- package/package.json +21 -2
- package/scripts/update-test-data.js +29 -0
- package/src/fetchers/melee.js +26 -35
- package/src/fetchers/mtgElo.js +40 -40
- package/src/fetchers/topdeck.js +70 -67
- package/src/fetchers/unityLeague.js +45 -45
- package/src/fetchers/untapped.js +54 -0
- package/src/index.js +75 -42
- package/src/utils/httpClient.js +30 -30
- package/test/data/melee.html +77 -62
- package/test/data/mtgElo.html +8 -8
- package/test/data/topdeck.html +778 -662
- package/test/data/topdeck.json +1 -0
- package/test/data/unityLeague.html +1445 -1340
- package/test/data/untapped.json +104 -0
- package/test/edgeCases.test.js +128 -0
- package/test/melee.test.js +23 -23
- package/test/meleeEdgeCases.test.js +53 -0
- package/test/mtgElo.test.js +21 -21
- package/test/mtgEloEdgeCases.test.js +92 -0
- package/test/playerInfoManager.test.js +312 -0
- package/test/topdeck.test.js +42 -29
- package/test/unityLeague.test.js +24 -24
- package/test/unityLeagueEdgeCases.test.js +123 -0
- package/test/untapped.test.js +58 -0
- package/test/verboseLogging.test.js +215 -0
- package/test/winRatePrecision.test.js +25 -0
- package/.github/workflows/pull-player-data.yml +0 -27
package/.github/workflows/ci.yml
CHANGED
|
@@ -7,11 +7,12 @@ on:
|
|
|
7
7
|
branches: [ main ]
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
|
-
|
|
11
|
-
runs-on:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
12
|
|
|
13
13
|
strategy:
|
|
14
14
|
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
15
16
|
node-version: [20, 22, 24]
|
|
16
17
|
|
|
17
18
|
steps:
|
|
@@ -27,4 +28,29 @@ jobs:
|
|
|
27
28
|
run: npm install
|
|
28
29
|
|
|
29
30
|
- name: Run tests
|
|
30
|
-
run: npm test
|
|
31
|
+
run: npm run test:coverage
|
|
32
|
+
|
|
33
|
+
- name: Upload test coverage data
|
|
34
|
+
if: github.repository == 'bkimminich/mtg-playerinfo' && github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24'
|
|
35
|
+
uses: actions/upload-artifact@v6
|
|
36
|
+
with:
|
|
37
|
+
name: test-lcov
|
|
38
|
+
path: |
|
|
39
|
+
coverage/lcov.info
|
|
40
|
+
|
|
41
|
+
coverage-report:
|
|
42
|
+
needs: [test]
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
if: github.repository == 'bkimminich/mtg-playerinfo' && github.event_name == 'push'
|
|
45
|
+
steps:
|
|
46
|
+
- name: "Check out Git repository"
|
|
47
|
+
uses: actions/checkout@v4
|
|
48
|
+
- name: "Download test coverage data"
|
|
49
|
+
uses: actions/download-artifact@v7
|
|
50
|
+
with:
|
|
51
|
+
name: test-lcov
|
|
52
|
+
- name: "Publish coverage to Coveralls"
|
|
53
|
+
uses: coverallsapp/github-action@v2
|
|
54
|
+
with:
|
|
55
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
56
|
+
files: lcov.info
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Update Test Data
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 2 * * 0' # Sundays 2:00 AM
|
|
6
|
+
workflow_dispatch: # Allow manual run
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
update:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout repository
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Use Node.js
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 24
|
|
21
|
+
cache: 'npm'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm install
|
|
25
|
+
|
|
26
|
+
- name: Fetch new test data
|
|
27
|
+
run: node scripts/update-test-data.js
|
|
28
|
+
|
|
29
|
+
- name: Commit and push changes
|
|
30
|
+
uses: stefanzweifel/git-auto-commit-action@v5
|
|
31
|
+
with:
|
|
32
|
+
commit_message: "🤖 Update test data files"
|
|
33
|
+
file_pattern: 'test/data/*.*'
|
package/README.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# MTG Player Info
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-

|
|
4
|
+

|
|
5
|
+
[](https://www.npmjs.com/package/mtg-playerinfo)
|
|
6
|
+
[](https://www.npmjs.com/package/mtg-playerinfo)
|
|
7
|
+
[](https://coveralls.io/github/bkimminich/mtg-playerinfo?branch=main)
|
|
8
|
+
[](https://standardjs.com)
|
|
5
9
|
|
|
6
|
-
A simple NPM module and CLI tool to pull Magic: The Gathering player data from various sources (Unity League, MTG Elo Project, Melee, and
|
|
10
|
+
A simple NPM module and CLI tool to pull Magic: The Gathering player data from various sources (Unity League, MTG Elo Project, Melee, Topdeck, and Untapped.gg).
|
|
7
11
|
|
|
8
12
|
## Installation
|
|
9
13
|
|
|
@@ -14,13 +18,13 @@ npm i -g mtg-playerinfo
|
|
|
14
18
|
## CLI Usage
|
|
15
19
|
|
|
16
20
|
```bash
|
|
17
|
-
mtg-playerinfo --unity-id 16215 --mtgelo-id 3irvwtmk --melee-user k0shiii --topdeck-handle k0shiii
|
|
21
|
+
mtg-playerinfo --unity-id 16215 --mtgelo-id 3irvwtmk --melee-user k0shiii --topdeck-handle k0shiii --untapped-id 7de50700-c3f6-48e4-a38d-2add5b0d9b71/76DCDWCZS5FX5PIEEMUVY6GV74
|
|
18
22
|
```
|
|
19
23
|
|
|
20
24
|
or without previous installation
|
|
21
25
|
|
|
22
26
|
```bash
|
|
23
|
-
npx mtg-playerinfo --unity-id 16215 --mtgelo-id 3irvwtmk --melee-user k0shiii --topdeck-handle k0shiii
|
|
27
|
+
npx mtg-playerinfo --unity-id 16215 --mtgelo-id 3irvwtmk --melee-user k0shiii --topdeck-handle k0shiii --untapped-id 7de50700-c3f6-48e4-a38d-2add5b0d9b71/76DCDWCZS5FX5PIEEMUVY6GV74
|
|
24
28
|
```
|
|
25
29
|
|
|
26
30
|
## Output Format
|
|
@@ -29,7 +33,7 @@ The tool returns a JSON object representing the player and their combined metada
|
|
|
29
33
|
|
|
30
34
|
### General meta-data and merging priority
|
|
31
35
|
|
|
32
|
-
General meta-data fields like `name`, `photo`, `age`, `country`, and `hometown` are extracted from the first source that provides them and placed in the `general` section. Merging follows a "first-come, first-served" approach based on the order of sources provided in the command line or processed by the manager. In the [CLI usage example](#cli-usage) above, the source priority is `Unity League` > `MTG Elo Project` > `Melee` > `Topdeck`.
|
|
36
|
+
General meta-data fields like `name`, `photo`, `age`, `country`, and `hometown` are extracted from the first source that provides them and placed in the `general` section. Merging follows a "first-come, first-served" approach based on the order of sources provided in the command line or processed by the manager. In the [CLI usage example](#cli-usage) above, the source priority is `Unity League` > `MTG Elo Project` > `Melee` > `Topdeck` > `Untapped.gg`.
|
|
33
37
|
|
|
34
38
|
> If you notice any inconsistencies or unexpected fields values, you can run the tool with the `-v` or `--verbose` flag to see the full list of extracted fields and if they were promoted to the `general` section or deviated from a previous source.
|
|
35
39
|
|
|
@@ -49,7 +53,7 @@ General meta-data fields like `name`, `photo`, `age`, `country`, and `hometown`
|
|
|
49
53
|
"facebook": "bjoern.kimminich",
|
|
50
54
|
"twitch": "koshiii",
|
|
51
55
|
"youtube": "@BjörnKimminich",
|
|
52
|
-
"win rate": "42.
|
|
56
|
+
"win rate": "42.49%"
|
|
53
57
|
},
|
|
54
58
|
"sources": {
|
|
55
59
|
"Unity League": {
|
|
@@ -63,11 +67,11 @@ General meta-data fields like `name`, `photo`, `age`, `country`, and `hometown`
|
|
|
63
67
|
"local organizer": "Mulligan TCG Shop",
|
|
64
68
|
"team": "Mull to Five",
|
|
65
69
|
"bio": "Smugly held back on an Untimely Malfunction against a Storm player going off, being totally sure that you can redirect the summed-up damage of their Grapeshots back to their face with its \"Change the target of target spell or ability with a single target\" mode.",
|
|
66
|
-
"rank germany": "
|
|
67
|
-
"rank europe": "
|
|
68
|
-
"rank points": "
|
|
69
|
-
"record": "
|
|
70
|
-
"win rate": "
|
|
70
|
+
"rank germany": "63",
|
|
71
|
+
"rank europe": "547",
|
|
72
|
+
"rank points": "304",
|
|
73
|
+
"record": "45-43-5",
|
|
74
|
+
"win rate": "50.2%"
|
|
71
75
|
}
|
|
72
76
|
},
|
|
73
77
|
"MTG Elo Project": {
|
|
@@ -116,6 +120,7 @@ The following sites are currently supported based on HTML scraping and/or API ca
|
|
|
116
120
|
| MTG Elo Project | ✅Scraping |
|
|
117
121
|
| Topdeck | ✅Scraping / ✅API |
|
|
118
122
|
| Melee | ✅Scraping / 🚧API ([#1](https://github.com/bkimminich/mtg-playerinfo/issues/1)) |
|
|
123
|
+
| Untapped.gg | ✅API |
|
|
119
124
|
|
|
120
125
|
_Note: Some sites may have anti-bot protections that can lead to "Maximum number of redirects exceeded" or "403 Forbidden" errors depending on the execution environment._
|
|
121
126
|
|
package/cli.js
CHANGED
|
@@ -1,32 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const { program } = require('commander')
|
|
3
|
-
const PlayerInfoManager = require('./src/index')
|
|
2
|
+
const { program } = require('commander')
|
|
3
|
+
const PlayerInfoManager = require('./src/index')
|
|
4
4
|
|
|
5
5
|
program
|
|
6
6
|
.name('mtg-playerinfo')
|
|
7
7
|
.description('CLI to pull MTG player data from various sources')
|
|
8
|
-
.version('1.0.0')
|
|
8
|
+
.version('1.0.0')
|
|
9
9
|
|
|
10
10
|
program
|
|
11
11
|
.option('--unity-id <id>', 'Unity League Player ID')
|
|
12
12
|
.option('--mtgelo-id <id>', 'MTG Elo Project Player ID')
|
|
13
13
|
.option('--melee-user <username>', 'Melee Username')
|
|
14
14
|
.option('--topdeck-handle <handle>', 'Topdeck Handle')
|
|
15
|
+
.option('--untapped-id <id>', 'Untapped.gg Player ID (format: userId/playerCode)')
|
|
15
16
|
.option('-v, --verbose', 'Print consistency check information to console')
|
|
16
17
|
.action(async (options) => {
|
|
17
|
-
if (!options.unityId && !options.mtgeloId && !options.meleeUser && !options.topdeckHandle) {
|
|
18
|
-
console.error('Error: Please provide at least one search option (unity-id, mtgelo-id, melee-user, or
|
|
19
|
-
process.exit(1)
|
|
18
|
+
if (!options.unityId && !options.mtgeloId && !options.meleeUser && !options.topdeckHandle && !options.untappedId) {
|
|
19
|
+
console.error('Error: Please provide at least one search option (unity-id, mtgelo-id, melee-user, topdeck-handle, or untapped-id).')
|
|
20
|
+
process.exit(1)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
// Determine priority order based on CLI argument order
|
|
24
|
+
const argOrder = []
|
|
25
|
+
const optionMap = {
|
|
26
|
+
'--unity-id': 'unity',
|
|
27
|
+
'--mtgelo-id': 'mtgelo',
|
|
28
|
+
'--melee-user': 'melee',
|
|
29
|
+
'--topdeck-handle': 'topdeck',
|
|
30
|
+
'--untapped-id': 'untapped'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
process.argv.forEach(arg => {
|
|
34
|
+
if (optionMap[arg]) {
|
|
35
|
+
argOrder.push(optionMap[arg])
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const manager = new PlayerInfoManager()
|
|
23
40
|
try {
|
|
24
|
-
const playerInfo = await manager.getPlayerInfo(options)
|
|
25
|
-
console.log(JSON.stringify(playerInfo, null, 2))
|
|
41
|
+
const playerInfo = await manager.getPlayerInfo(options, argOrder)
|
|
42
|
+
console.log(JSON.stringify(playerInfo, null, 2))
|
|
26
43
|
} catch (error) {
|
|
27
|
-
console.error('An error occurred:', error.message)
|
|
28
|
-
process.exit(1)
|
|
44
|
+
console.error('An error occurred:', error.message)
|
|
45
|
+
process.exit(1)
|
|
29
46
|
}
|
|
30
|
-
})
|
|
47
|
+
})
|
|
31
48
|
|
|
32
|
-
program.parse(process.argv)
|
|
49
|
+
program.parse(process.argv)
|
package/package.json
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtg-playerinfo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A simple NPM module and CLI tool to pull Magic: The Gathering player data from various sources",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mtg-playerinfo": "./cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node --test"
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"test:coverage": "nyc --reporter=lcov --produce-source-map node --test",
|
|
12
|
+
"lint": "standard",
|
|
13
|
+
"lint:fix": "standard --fix"
|
|
14
|
+
},
|
|
15
|
+
"nyc": {
|
|
16
|
+
"extension": [
|
|
17
|
+
".js"
|
|
18
|
+
],
|
|
19
|
+
"include": [
|
|
20
|
+
"src/fetchers/*.js",
|
|
21
|
+
"src/index.js",
|
|
22
|
+
"cli.js"
|
|
23
|
+
],
|
|
24
|
+
"sourceMap": true,
|
|
25
|
+
"instrument": true
|
|
11
26
|
},
|
|
12
27
|
"keywords": [],
|
|
13
28
|
"author": "Björn Kimminich",
|
|
@@ -16,6 +31,10 @@
|
|
|
16
31
|
"cheerio": "^1.2.0",
|
|
17
32
|
"commander": "^14.0.3"
|
|
18
33
|
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"nyc": "^17.1.0",
|
|
36
|
+
"standard": "^17.1.2"
|
|
37
|
+
},
|
|
19
38
|
"engines": {
|
|
20
39
|
"node": "20 - 24"
|
|
21
40
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { request } = require('../src/utils/httpClient')
|
|
4
|
+
|
|
5
|
+
const targets = [
|
|
6
|
+
{ id: '16215', url: 'https://unityleague.gg/player/16215/', file: 'unityLeague.html' },
|
|
7
|
+
{ id: '3irvwtmk', url: 'https://mtgeloproject.net/profile/3irvwtmk', file: 'mtgElo.html' },
|
|
8
|
+
{ id: 'k0shiii', url: 'https://melee.gg/Profile/Index/k0shiii', file: 'melee.html' },
|
|
9
|
+
{ id: 'k0shiii', url: 'https://topdeck.gg/profile/@k0shiii', file: 'topdeck.html' },
|
|
10
|
+
{ id: 'm4VSTJShiXR1PCSCWaM9TBY0rcg1', url: 'https://topdeck.gg/profile/m4VSTJShiXR1PCSCWaM9TBY0rcg1/stats', file: 'topdeck.json' },
|
|
11
|
+
{ id: '7de50700-c3f6-48e4-a38d-2add5b0d9b71/76DCDWCZS5FX5PIEEMUVY6GV74', url: 'https://api.mtga.untapped.gg/api/v1/games/users/7de50700-c3f6-48e4-a38d-2add5b0d9b71/players/76DCDWCZS5FX5PIEEMUVY6GV74/?card_set=ECL', file: 'untapped.json' }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
async function updateTestData () {
|
|
15
|
+
for (const target of targets) {
|
|
16
|
+
console.log(`Fetching ${target.url}...`)
|
|
17
|
+
try {
|
|
18
|
+
const { data } = await request(target.url, { maxRedirects: 10 })
|
|
19
|
+
const filePath = path.join(__dirname, '..', 'test', 'data', target.file)
|
|
20
|
+
fs.writeFileSync(filePath, data)
|
|
21
|
+
console.log(`Updated ${filePath}`)
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(`Error updating ${target.file}:`, error.message)
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
updateTestData()
|
package/src/fetchers/melee.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
const { request } = require('../utils/httpClient')
|
|
2
|
-
const cheerio = require('cheerio')
|
|
1
|
+
const { request } = require('../utils/httpClient')
|
|
2
|
+
const cheerio = require('cheerio')
|
|
3
3
|
|
|
4
4
|
class MeleeFetcher {
|
|
5
|
-
async fetchById(username) {
|
|
6
|
-
const url = `https://melee.gg/Profile/Index/${username}
|
|
5
|
+
async fetchById (username) {
|
|
6
|
+
const url = `https://melee.gg/Profile/Index/${username}`
|
|
7
7
|
try {
|
|
8
|
-
const { data } = await request(url)
|
|
9
|
-
return this.parseHtml(data, url, username)
|
|
8
|
+
const { data } = await request(url)
|
|
9
|
+
return this.parseHtml(data, url, username)
|
|
10
10
|
} catch (error) {
|
|
11
|
-
console.error(`Error fetching Melee profile ${username}:`, error.message)
|
|
12
|
-
return null
|
|
11
|
+
console.error(`Error fetching Melee profile ${username}:`, error.message)
|
|
12
|
+
return null
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
parseHtml(html, url, username) {
|
|
17
|
-
const $ = cheerio.load(html)
|
|
18
|
-
const name = $('span[style*="font-size: xx-large"]').first().text().trim() || username
|
|
19
|
-
const pronouns = $('.profile-details span.text-muted.mr-2').filter((i, el) => $(el).text().includes('/')).first().text().trim()
|
|
20
|
-
const bio = $('.profile-details div[style*="max-width: 75%"]').first().text().trim()
|
|
16
|
+
parseHtml (html, url, username) {
|
|
17
|
+
const $ = cheerio.load(html)
|
|
18
|
+
const name = $('span[style*="font-size: xx-large"]').first().text().trim() || username
|
|
19
|
+
const pronouns = $('.profile-details span.text-muted.mr-2').filter((i, el) => $(el).text().includes('/')).first().text().trim()
|
|
20
|
+
const bio = $('.profile-details div[style*="max-width: 75%"]').first().text().trim()
|
|
21
21
|
// FIXME Photos cannot be loaded with unauthenticated requests from Melee.gg
|
|
22
22
|
// const photo = $('.profile-button-column img').first().attr('src') || $('img.m-auto').attr('src');
|
|
23
23
|
|
|
@@ -26,39 +26,30 @@ class MeleeFetcher {
|
|
|
26
26
|
url,
|
|
27
27
|
name,
|
|
28
28
|
pronouns: pronouns || null,
|
|
29
|
-
bio: bio || null
|
|
29
|
+
bio: bio || null
|
|
30
30
|
// photo: photo ? (photo.startsWith('http') ? photo : `https://melee.gg${photo}`) : null
|
|
31
|
-
}
|
|
31
|
+
}
|
|
32
32
|
|
|
33
33
|
$('.social-link').each((i, el) => {
|
|
34
|
-
const href = $(el).attr('href')
|
|
34
|
+
const href = $(el).attr('href')
|
|
35
35
|
if (href) {
|
|
36
36
|
try {
|
|
37
|
-
const urlObj = new URL(href)
|
|
38
|
-
const platform = urlObj.hostname.replace('www.', '').split('.')[0]
|
|
39
|
-
let handle = urlObj.pathname.split('/').filter(Boolean).pop()
|
|
40
|
-
if (handle) {
|
|
41
|
-
handle = decodeURIComponent(handle);
|
|
42
|
-
}
|
|
43
|
-
if (platform === 'youtube' && handle.startsWith('@')) {
|
|
44
|
-
// keep @ for youtube
|
|
45
|
-
} else if (platform === 'facebook') {
|
|
46
|
-
// handle is correct
|
|
47
|
-
} else if (platform === 'twitch') {
|
|
48
|
-
// handle is correct
|
|
49
|
-
}
|
|
37
|
+
const urlObj = new URL(href)
|
|
38
|
+
const platform = urlObj.hostname.replace('www.', '').split('.')[0]
|
|
39
|
+
let handle = urlObj.pathname.split('/').filter(Boolean).pop()
|
|
50
40
|
if (handle) {
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
handle = decodeURIComponent(handle)
|
|
42
|
+
const label = platform.charAt(0).toLowerCase() + platform.slice(1)
|
|
43
|
+
data[label] = handle
|
|
53
44
|
}
|
|
54
45
|
} catch (e) {
|
|
55
|
-
|
|
46
|
+
console.log('Invalid URL in social link ' + href + ': ' + e.message)
|
|
56
47
|
}
|
|
57
48
|
}
|
|
58
|
-
})
|
|
49
|
+
})
|
|
59
50
|
|
|
60
|
-
return data
|
|
51
|
+
return data
|
|
61
52
|
}
|
|
62
53
|
}
|
|
63
54
|
|
|
64
|
-
module.exports = MeleeFetcher
|
|
55
|
+
module.exports = MeleeFetcher
|
package/src/fetchers/mtgElo.js
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
const { request } = require('../utils/httpClient')
|
|
1
|
+
const { request } = require('../utils/httpClient')
|
|
2
2
|
|
|
3
3
|
class MtgEloFetcher {
|
|
4
|
-
async fetchById(id) {
|
|
5
|
-
const url = `https://mtgeloproject.net/profile/${id}
|
|
4
|
+
async fetchById (id) {
|
|
5
|
+
const url = `https://mtgeloproject.net/profile/${id}`
|
|
6
6
|
try {
|
|
7
7
|
const { data: html } = await request(url, {
|
|
8
8
|
maxRedirects: 10
|
|
9
|
-
})
|
|
9
|
+
})
|
|
10
10
|
|
|
11
|
-
return this.parseHtml(html, url, id)
|
|
11
|
+
return this.parseHtml(html, url, id)
|
|
12
12
|
} catch (error) {
|
|
13
|
-
console.error(`Error fetching MTG Elo Project profile ${id}:`, error.message)
|
|
14
|
-
return null
|
|
13
|
+
console.error(`Error fetching MTG Elo Project profile ${id}:`, error.message)
|
|
14
|
+
return null
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
parseHtml(html, url, id) {
|
|
19
|
-
const cheerio = require('cheerio')
|
|
20
|
-
const $ = cheerio.load(html)
|
|
18
|
+
parseHtml (html, url, id) {
|
|
19
|
+
const cheerio = require('cheerio')
|
|
20
|
+
const $ = cheerio.load(html)
|
|
21
21
|
|
|
22
|
-
let name = ''
|
|
23
|
-
let currentRating = ''
|
|
24
|
-
let record = ''
|
|
22
|
+
let name = ''
|
|
23
|
+
let currentRating = ''
|
|
24
|
+
let record = ''
|
|
25
25
|
|
|
26
|
-
const astroIsland = $('astro-island[component-url*="Profile"]')
|
|
26
|
+
const astroIsland = $('astro-island[component-url*="Profile"]')
|
|
27
27
|
if (astroIsland.length > 0) {
|
|
28
28
|
try {
|
|
29
|
-
const props = JSON.parse(astroIsland.attr('props'))
|
|
30
|
-
const info = props.info[1]
|
|
31
|
-
name = `${info.first_name[1]} ${info.last_name[1]}
|
|
32
|
-
currentRating = Math.round(info.current_rating[1]).toString()
|
|
33
|
-
const r = info.record[1]
|
|
34
|
-
record = `${r[0][1]}-${r[1][1]}-${r[2][1]}
|
|
29
|
+
const props = JSON.parse(astroIsland.attr('props'))
|
|
30
|
+
const info = props.info[1]
|
|
31
|
+
name = `${info.first_name[1]} ${info.last_name[1]}`
|
|
32
|
+
currentRating = Math.round(info.current_rating[1]).toString()
|
|
33
|
+
const r = info.record[1]
|
|
34
|
+
record = `${r[0][1]}-${r[1][1]}-${r[2][1]}`
|
|
35
35
|
} catch (e) {
|
|
36
|
-
console.error('Error parsing MTG Elo props:', e.message)
|
|
36
|
+
console.error('Error parsing MTG Elo props:', e.message)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (!name) {
|
|
41
|
-
name = $('.text-\\[22pt\\]').text().trim()
|
|
41
|
+
name = $('.text-\\[22pt\\]').text().trim()
|
|
42
42
|
if (name.includes(',')) {
|
|
43
|
-
const parts = name.split(',').map(s => s.trim())
|
|
44
|
-
name = `${parts[1]} ${parts[0]}
|
|
43
|
+
const parts = name.split(',').map(s => s.trim())
|
|
44
|
+
name = `${parts[1]} ${parts[0]}`
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
if (!currentRating) {
|
|
49
|
-
currentRating = $('.text-\\[18pt\\]:contains("Current rating")').find('.font-bold').text().trim()
|
|
49
|
+
currentRating = $('.text-\\[18pt\\]:contains("Current rating")').find('.font-bold').text().trim()
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (!record) {
|
|
53
|
-
const recordText = $('.text-\\[18pt\\]:contains("Record")').text()
|
|
54
|
-
record = recordText.replace('Record:', '').trim()
|
|
53
|
+
const recordText = $('.text-\\[18pt\\]:contains("Record")').text()
|
|
54
|
+
record = recordText.replace('Record:', '').trim()
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
if (!name) return null
|
|
57
|
+
if (!name) return null
|
|
58
58
|
|
|
59
59
|
const data = {
|
|
60
60
|
source: 'MTG Elo Project',
|
|
61
|
-
url
|
|
62
|
-
name
|
|
61
|
+
url,
|
|
62
|
+
name,
|
|
63
63
|
player_id: id,
|
|
64
64
|
current_rating: currentRating,
|
|
65
|
-
record
|
|
66
|
-
}
|
|
65
|
+
record
|
|
66
|
+
}
|
|
67
67
|
|
|
68
68
|
if (record && record.includes('-')) {
|
|
69
|
-
const [w, l, d] = record.split('-').map(Number)
|
|
69
|
+
const [w, l, d] = record.split('-').map(Number)
|
|
70
70
|
if (!isNaN(w) && !isNaN(l)) {
|
|
71
|
-
const wins = w
|
|
72
|
-
const losses = l
|
|
73
|
-
const draws = isNaN(d) ? 0 : d
|
|
74
|
-
const total = wins + losses + draws
|
|
71
|
+
const wins = w
|
|
72
|
+
const losses = l
|
|
73
|
+
const draws = isNaN(d) ? 0 : d
|
|
74
|
+
const total = wins + losses + draws
|
|
75
75
|
if (total > 0) {
|
|
76
|
-
data['win rate'] = ((wins / total) * 100).toFixed(2) + '%'
|
|
76
|
+
data['win rate'] = ((wins / total) * 100).toFixed(2) + '%'
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
return data
|
|
81
|
+
return data
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
module.exports = MtgEloFetcher
|
|
85
|
+
module.exports = MtgEloFetcher
|