mtg-playerinfo 1.1.0 → 1.2.2
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 +61 -36
- package/cli.js +28 -12
- package/package.json +21 -2
- package/scripts/update-test-data.js +28 -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/index.js +80 -37
- package/src/utils/httpClient.js +30 -30
- package/test/data/melee.html +548 -572
- package/test/data/mtgElo.html +4 -5
- package/test/data/topdeck.html +266 -152
- package/test/data/topdeck.json +1 -0
- package/test/data/unityLeague.html +983 -1076
- package/test/integration/playerInfoManager.test.js +312 -0
- package/test/melee.test.js +23 -23
- package/test/mtgElo.test.js +21 -21
- package/test/topdeck.test.js +42 -29
- package/test/unityLeague.test.js +24 -24
- 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 HTML files"
|
|
33
|
+
file_pattern: 'test/data/*.html'
|
package/README.md
CHANGED
|
@@ -1,84 +1,109 @@
|
|
|
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
10
|
A simple NPM module and CLI tool to pull Magic: The Gathering player data from various sources (Unity League, MTG Elo Project, Melee, and Topdeck).
|
|
7
11
|
|
|
8
12
|
## Installation
|
|
9
13
|
|
|
10
14
|
```bash
|
|
11
|
-
npm i -g mtg-
|
|
15
|
+
npm i -g mtg-playerinfo
|
|
12
16
|
```
|
|
13
17
|
|
|
14
18
|
## CLI Usage
|
|
15
19
|
|
|
16
20
|
```bash
|
|
17
|
-
mtg-
|
|
21
|
+
mtg-playerinfo --unity-id 16215 --mtgelo-id 3irvwtmk --melee-user k0shiii --topdeck-handle k0shiii
|
|
18
22
|
```
|
|
19
23
|
|
|
20
24
|
or without previous installation
|
|
21
25
|
|
|
22
26
|
```bash
|
|
23
|
-
npx mtg-
|
|
27
|
+
npx mtg-playerinfo --unity-id 16215 --mtgelo-id 3irvwtmk --melee-user k0shiii --topdeck-handle k0shiii
|
|
24
28
|
```
|
|
25
29
|
|
|
26
30
|
## Output Format
|
|
27
31
|
|
|
28
|
-
The tool returns a JSON object representing the player and their combined metadata. Redundant information like
|
|
32
|
+
The tool returns a JSON object representing the player and their combined metadata. Redundant information like name, photo, country, age, and hometown is merged into a `general` section, while source-specific data is kept in the `sources` section.
|
|
29
33
|
|
|
30
|
-
###
|
|
34
|
+
### General meta-data and merging priority
|
|
31
35
|
|
|
32
|
-
-
|
|
33
|
-
- **Deduplication**: If multiple IDs point to the exact same profile URL, the profile is only processed once to avoid redundant data in the `sources` section.
|
|
34
|
-
- **General Metadata**: Fields like `Age`, `Country`, and `Hometown` are extracted from the first source that provides them and placed in the `general` section.
|
|
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`.
|
|
35
37
|
|
|
36
|
-
|
|
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.
|
|
39
|
+
|
|
40
|
+
### Example output
|
|
37
41
|
|
|
38
42
|
```json
|
|
39
43
|
{
|
|
40
|
-
"name": "Björn Kimminich",
|
|
41
|
-
"photo": "https://unityleague.gg/media/player_profile/1000023225.jpg",
|
|
42
44
|
"general": {
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
45
|
+
"name": "Björn Kimminich",
|
|
46
|
+
"photo": "https://unityleague.gg/media/player_profile/1000023225.jpg",
|
|
47
|
+
"age": "45",
|
|
48
|
+
"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.",
|
|
49
|
+
"team": "Mull to Five",
|
|
50
|
+
"country": "de",
|
|
51
|
+
"hometown": "Hamburg",
|
|
52
|
+
"pronouns": "He/Him",
|
|
53
|
+
"facebook": "bjoern.kimminich",
|
|
54
|
+
"twitch": "koshiii",
|
|
55
|
+
"youtube": "@BjörnKimminich",
|
|
56
|
+
"win rate": "42.49%"
|
|
49
57
|
},
|
|
50
58
|
"sources": {
|
|
51
59
|
"Unity League": {
|
|
52
60
|
"url": "https://unityleague.gg/player/16215/",
|
|
53
61
|
"data": {
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
62
|
+
"name": "Björn Kimminich",
|
|
63
|
+
"photo": "https://unityleague.gg/media/player_profile/1000023225.jpg",
|
|
64
|
+
"country": "de",
|
|
65
|
+
"age": "45",
|
|
66
|
+
"hometown": "Hamburg",
|
|
67
|
+
"local organizer": "Mulligan TCG Shop",
|
|
68
|
+
"team": "Mull to Five",
|
|
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.",
|
|
70
|
+
"rank germany": "63",
|
|
71
|
+
"rank europe": "547",
|
|
72
|
+
"rank points": "304",
|
|
73
|
+
"record": "45-43-5",
|
|
74
|
+
"win rate": "50.2%"
|
|
60
75
|
}
|
|
61
76
|
},
|
|
62
77
|
"MTG Elo Project": {
|
|
63
78
|
"url": "https://mtgeloproject.net/profile/3irvwtmk",
|
|
64
79
|
"data": {
|
|
80
|
+
"name": "Bjoern Kimminich",
|
|
65
81
|
"player_id": "3irvwtmk",
|
|
66
82
|
"current_rating": "1466",
|
|
67
83
|
"record": "9-12-1",
|
|
68
|
-
"
|
|
84
|
+
"win rate": "40.91%"
|
|
69
85
|
}
|
|
70
86
|
},
|
|
71
87
|
"Melee": {
|
|
72
88
|
"url": "https://melee.gg/Profile/Index/k0shiii",
|
|
73
|
-
"data": {
|
|
89
|
+
"data": {
|
|
90
|
+
"name": "Björn Kimminich",
|
|
91
|
+
"pronouns": "He/Him",
|
|
92
|
+
"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.",
|
|
93
|
+
"facebook": "bjoern.kimminich",
|
|
94
|
+
"twitch": "koshiii",
|
|
95
|
+
"youtube": "@BjörnKimminich"
|
|
96
|
+
}
|
|
74
97
|
},
|
|
75
98
|
"Topdeck": {
|
|
76
99
|
"url": "https://topdeck.gg/profile/@k0shiii",
|
|
77
100
|
"data": {
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
101
|
+
"name": "Björn Kimminich",
|
|
102
|
+
"photo": "https://imagedelivery.net/kN_u_RUfFF6xsGMKYWhO1g/2a7b8d12-5924-4a58-5f9c-c0bf55766800/square",
|
|
103
|
+
"tournaments": "2",
|
|
104
|
+
"record": "4-6-1",
|
|
105
|
+
"win rate": "36.36%",
|
|
106
|
+
"conversion": "0%"
|
|
82
107
|
}
|
|
83
108
|
}
|
|
84
109
|
}
|
|
@@ -89,12 +114,12 @@ Example output:
|
|
|
89
114
|
|
|
90
115
|
The following sites are currently supported based on HTML scraping and/or API calls. In general, API calls are preferred over scraping due to their higher reliability and independence from site structure changes.
|
|
91
116
|
|
|
92
|
-
| Site | Method
|
|
93
|
-
|
|
94
|
-
| Unity League | ✅Scraping
|
|
95
|
-
| MTG Elo Project | ✅Scraping
|
|
96
|
-
| Topdeck | ✅Scraping
|
|
97
|
-
| Melee |
|
|
117
|
+
| Site | Method |
|
|
118
|
+
|-----------------|---------------------------------------------------------------------------------|
|
|
119
|
+
| Unity League | ✅Scraping |
|
|
120
|
+
| MTG Elo Project | ✅Scraping |
|
|
121
|
+
| Topdeck | ✅Scraping / ✅API |
|
|
122
|
+
| Melee | ✅Scraping / 🚧API ([#1](https://github.com/bkimminich/mtg-playerinfo/issues/1)) |
|
|
98
123
|
|
|
99
124
|
_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._
|
|
100
125
|
|
package/cli.js
CHANGED
|
@@ -1,31 +1,47 @@
|
|
|
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('-v, --verbose', 'Print consistency check information to console')
|
|
15
16
|
.action(async (options) => {
|
|
16
17
|
if (!options.unityId && !options.mtgeloId && !options.meleeUser && !options.topdeckHandle) {
|
|
17
|
-
console.error('Error: Please provide at least one search option (unity-id, mtgelo-id, melee-user, or topdeck-handle).')
|
|
18
|
-
process.exit(1)
|
|
18
|
+
console.error('Error: Please provide at least one search option (unity-id, mtgelo-id, melee-user, or topdeck-handle).')
|
|
19
|
+
process.exit(1)
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// Determine priority order based on CLI argument order
|
|
23
|
+
const argOrder = []
|
|
24
|
+
const optionMap = {
|
|
25
|
+
'--unity-id': 'unity',
|
|
26
|
+
'--mtgelo-id': 'mtgelo',
|
|
27
|
+
'--melee-user': 'melee',
|
|
28
|
+
'--topdeck-handle': 'topdeck'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.argv.forEach(arg => {
|
|
32
|
+
if (optionMap[arg]) {
|
|
33
|
+
argOrder.push(optionMap[arg])
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const manager = new PlayerInfoManager()
|
|
22
38
|
try {
|
|
23
|
-
const playerInfo = await manager.getPlayerInfo(options)
|
|
24
|
-
console.log(JSON.stringify(playerInfo, null, 2))
|
|
39
|
+
const playerInfo = await manager.getPlayerInfo(options, argOrder)
|
|
40
|
+
console.log(JSON.stringify(playerInfo, null, 2))
|
|
25
41
|
} catch (error) {
|
|
26
|
-
console.error('An error occurred:', error.message)
|
|
27
|
-
process.exit(1)
|
|
42
|
+
console.error('An error occurred:', error.message)
|
|
43
|
+
process.exit(1)
|
|
28
44
|
}
|
|
29
|
-
})
|
|
45
|
+
})
|
|
30
46
|
|
|
31
|
-
program.parse(process.argv)
|
|
47
|
+
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.2.2",
|
|
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,28 @@
|
|
|
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
|
+
]
|
|
12
|
+
|
|
13
|
+
async function updateTestData () {
|
|
14
|
+
for (const target of targets) {
|
|
15
|
+
console.log(`Fetching ${target.url}...`)
|
|
16
|
+
try {
|
|
17
|
+
const { data } = await request(target.url, { maxRedirects: 10 })
|
|
18
|
+
const filePath = path.join(__dirname, '..', 'test', 'data', target.file)
|
|
19
|
+
fs.writeFileSync(filePath, data)
|
|
20
|
+
console.log(`Updated ${filePath}`)
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(`Error updating ${target.file}:`, error.message)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
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
|