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.
@@ -1,106 +1,109 @@
1
- const { request } = require('../utils/httpClient');
2
- const cheerio = require('cheerio');
1
+ const httpClient = require('../utils/httpClient')
2
+ const cheerio = require('cheerio')
3
3
 
4
4
  class TopdeckFetcher {
5
- async fetchById(handle) {
6
- const cleanHandle = handle.startsWith('@') ? handle : `@${handle}`;
7
- const url = `https://topdeck.gg/profile/${cleanHandle}`;
5
+ async fetchById (handle) {
6
+ const cleanHandle = handle.startsWith('@') ? handle : `@${handle}`
7
+ const url = `https://topdeck.gg/profile/${cleanHandle}`
8
8
  try {
9
- const { data } = await request(url);
10
- const playerInfo = this.parseHtml(data, url, cleanHandle);
9
+ const { data } = await httpClient.request(url)
10
+ const playerInfo = this.parseHtml(data, url, cleanHandle)
11
11
 
12
- // Try to fetch stats via XHR if internal ID is found
13
- const internalIdMatch = data.match(/https:\/\/topdeck\.gg\/profile\/([a-zA-Z0-9]+)\/stats/) || data.match(/const playerId = "([a-zA-Z0-9]+)";/);
14
- const internalId = internalIdMatch ? internalIdMatch[1] : null;
12
+ const internalIdMatch = data.match(/https:\/\/topdeck\.gg\/profile\/([a-zA-Z0-9]+)\/stats/) || data.match(/const playerId = "([a-zA-Z0-9]+)";/)
13
+ const internalId = internalIdMatch ? internalIdMatch[1] : null
15
14
 
16
15
  if (internalId) {
17
- try {
18
- const statsUrl = `https://topdeck.gg/profile/${internalId}/stats`;
19
- const statsResponse = await request(statsUrl);
20
- const statsJson = statsResponse.data;
21
- const stats = typeof statsJson === 'string' ? JSON.parse(statsJson) : statsJson;
16
+ await this.fetchStats(internalId, playerInfo)
17
+ }
18
+
19
+ return playerInfo
20
+ } catch (error) {
21
+ console.error(`Error fetching Topdeck profile ${handle}:`, error.message)
22
+ return null
23
+ }
24
+ }
22
25
 
23
- if (stats) {
24
- if (stats.yearlyStats) {
25
- let totalTournaments = 0;
26
- let wins = 0;
27
- let losses = 0;
28
- let draws = 0;
26
+ async fetchStats (internalId, playerInfo) {
27
+ try {
28
+ const statsUrl = `https://topdeck.gg/profile/${internalId}/stats`
29
+ const statsResponse = await httpClient.request(statsUrl)
30
+ const statsJson = statsResponse.data
31
+ const stats = typeof statsJson === 'string' ? JSON.parse(statsJson) : statsJson
29
32
 
30
- Object.values(stats.yearlyStats).forEach(yearData => {
31
- if (yearData.overall) {
32
- totalTournaments += yearData.overall.totalTournaments || 0;
33
- wins += yearData.overall.wins || 0;
34
- losses += yearData.overall.losses || 0;
35
- draws += yearData.overall.draws || 0;
36
- }
37
- });
33
+ if (stats) {
34
+ if (stats.yearlyStats) {
35
+ let totalTournaments = 0
36
+ let wins = 0
37
+ let losses = 0
38
+ let draws = 0
38
39
 
39
- if (totalTournaments > 0) {
40
- playerInfo['tournaments'] = totalTournaments.toString();
41
- playerInfo['record'] = `${wins}-${losses}-${draws}`;
42
- playerInfo['win rate'] = ((wins / (wins + losses + draws)) * 100).toFixed(2) + '%';
43
- }
44
- } else {
45
- playerInfo['tournaments'] = stats.totalTournaments || playerInfo['tournaments'] || '0';
46
- playerInfo['record'] = stats.overallRecord || playerInfo['record'] || '0-0-0';
47
- playerInfo['win rate'] = stats.overallWinRate || playerInfo['win rate'] || '0.00%';
48
- playerInfo['conversion'] = stats.conversionRate || playerInfo['conversion'] || '0.00%';
40
+ Object.values(stats.yearlyStats).forEach(yearData => {
41
+ if (yearData.overall) {
42
+ totalTournaments += yearData.overall.totalTournaments || 0
43
+ wins += yearData.overall.wins || 0
44
+ losses += yearData.overall.losses || 0
45
+ draws += yearData.overall.draws || 0
49
46
  }
47
+ })
48
+
49
+ if (totalTournaments > 0) {
50
+ playerInfo.tournaments = totalTournaments.toString()
51
+ playerInfo.record = `${wins}-${losses}-${draws}`
52
+ playerInfo['win rate'] = ((wins / (wins + losses + draws)) * 100).toFixed(2) + '%'
50
53
  }
51
- } catch (statsError) {
52
- console.error(`Error fetching Topdeck stats for ${handle}:`, statsError.message);
54
+ } else {
55
+ playerInfo.tournaments = stats.totalTournaments || playerInfo.tournaments || '0'
56
+ playerInfo.record = stats.overallRecord || playerInfo.record || '0-0-0'
57
+ playerInfo['win rate'] = stats.overallWinRate || playerInfo['win rate'] || '0.00%'
58
+ playerInfo.conversion = stats.conversionRate || playerInfo.conversion || '0.00%'
53
59
  }
54
60
  }
55
-
56
- return playerInfo;
57
- } catch (error) {
58
- console.error(`Error fetching Topdeck profile ${handle}:`, error.message);
59
- return null;
61
+ } catch (statsError) {
62
+ console.error(`Error fetching Topdeck stats for ${internalId}:`, statsError.message)
60
63
  }
61
64
  }
62
65
 
63
- parseHtml(html, url, handle) {
64
- const $ = cheerio.load(html);
65
- const name = $('h2.text-white.fw-bold.mb-1').first().text().trim() || $('h1').first().text().trim() || handle;
66
- const photo = $('img.rounded-circle.shadow-lg').first().attr('src') || $('img[src*="avatar"], img[src*="profile"]').first().attr('src');
66
+ parseHtml (html, url, handle) {
67
+ const $ = cheerio.load(html)
68
+ const name = $('h2.text-white.fw-bold.mb-1').first().text().trim() || $('h1').first().text().trim() || handle
69
+ const photo = $('img.rounded-circle.shadow-lg').first().attr('src') || $('img[src*="avatar"], img[src*="profile"]').first().attr('src')
67
70
 
68
71
  const data = {
69
72
  source: 'Topdeck',
70
73
  url,
71
74
  name,
72
75
  photo: photo ? (photo.startsWith('http') ? photo : `https://topdeck.gg${photo}`) : null
73
- };
76
+ }
74
77
 
75
78
  const statsMap = {
76
- 'totalTournaments': 'tournaments',
77
- 'overallRecord': 'record',
78
- 'overallWinRate': 'win rate',
79
- 'conversionRate': 'conversion'
80
- };
79
+ totalTournaments: 'tournaments',
80
+ overallRecord: 'record',
81
+ overallWinRate: 'win rate',
82
+ conversionRate: 'conversion'
83
+ }
81
84
 
82
85
  Object.entries(statsMap).forEach(([id, label]) => {
83
- const val = $(`#${id}`).text().trim();
86
+ const val = $(`#${id}`).text().trim()
84
87
  if (val) {
85
- data[label] = val;
88
+ data[label] = val
86
89
  }
87
- });
90
+ })
88
91
 
89
- const currentStats = Object.keys(data).filter(k => Object.values(statsMap).includes(k));
92
+ const currentStats = Object.keys(data).filter(k => Object.values(statsMap).includes(k))
90
93
  if (currentStats.length <= 1) {
91
94
  $('.stats-container, .player-stats').each((i, el) => {
92
95
  $(el).find('.stat').each((j, statEl) => {
93
- const label = $(statEl).find('.label').text().trim().toLowerCase();
94
- const value = $(statEl).find('.value').text().trim();
96
+ const label = $(statEl).find('.label').text().trim().toLowerCase()
97
+ const value = $(statEl).find('.value').text().trim()
95
98
  if (label && value) {
96
- data[label] = value;
99
+ data[label] = value
97
100
  }
98
- });
99
- });
101
+ })
102
+ })
100
103
  }
101
104
 
102
- return data;
105
+ return data
103
106
  }
104
107
  }
105
108
 
106
- module.exports = TopdeckFetcher;
109
+ module.exports = TopdeckFetcher
@@ -1,24 +1,24 @@
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 UnityLeagueFetcher {
5
- async fetchById(id) {
6
- const url = `https://unityleague.gg/player/${id}/`;
5
+ async fetchById (id) {
6
+ const url = `https://unityleague.gg/player/${id}/`
7
7
  try {
8
- const { data } = await request(url);
9
- return this.parseHtml(data, url);
8
+ const { data } = await request(url)
9
+ return this.parseHtml(data, url)
10
10
  } catch (error) {
11
- console.error(`Error fetching Unity League player ${id}:`, error.message);
12
- return null;
11
+ console.error(`Error fetching Unity League player ${id}:`, error.message)
12
+ return null
13
13
  }
14
14
  }
15
15
 
16
- parseHtml(html, url) {
17
- const $ = cheerio.load(html);
18
- const name = $('h1.d-inline').text().trim();
19
- let photo = $('.card-body img.img-fluid').first().attr('src');
16
+ parseHtml (html, url) {
17
+ const $ = cheerio.load(html)
18
+ const name = $('h1.d-inline').text().trim()
19
+ let photo = $('.card-body img.img-fluid').first().attr('src')
20
20
  if (photo && !photo.includes('player_profile')) {
21
- photo = null;
21
+ photo = null
22
22
  }
23
23
 
24
24
  const data = {
@@ -26,74 +26,74 @@ class UnityLeagueFetcher {
26
26
  url,
27
27
  name,
28
28
  photo: photo ? (photo.startsWith('http') ? photo : `https://unityleague.gg${photo}`) : null
29
- };
29
+ }
30
30
 
31
- const headerFlag = $('.card-body i.fi').first();
31
+ const headerFlag = $('.card-body i.fi').first()
32
32
  if (headerFlag.length > 0) {
33
- const classes = headerFlag.attr('class').split(' ');
34
- const countryClass = classes.find(c => c.startsWith('fi-'));
33
+ const classes = headerFlag.attr('class').split(' ')
34
+ const countryClass = classes.find(c => c.startsWith('fi-'))
35
35
  if (countryClass) {
36
- data.country = countryClass.replace('fi-', '');
36
+ data.country = countryClass.replace('fi-', '')
37
37
  }
38
38
  }
39
39
 
40
40
  $('dt.small.text-muted').each((i, el) => {
41
- const key = $(el).text().trim().replace(/:$/, '').toLowerCase();
42
- const dd = $(el).next('dd');
43
- let value = dd.text().trim();
41
+ const key = $(el).text().trim().replace(/:$/, '').toLowerCase()
42
+ const dd = $(el).next('dd')
43
+ let value = dd.text().trim()
44
44
 
45
45
  if (key === 'country') {
46
- const flagIcon = dd.find('i.fi');
46
+ const flagIcon = dd.find('i.fi')
47
47
  if (flagIcon.length > 0) {
48
- const classes = flagIcon.attr('class').split(' ');
49
- const countryClass = classes.find(c => c.startsWith('fi-'));
48
+ const classes = flagIcon.attr('class').split(' ')
49
+ const countryClass = classes.find(c => c.startsWith('fi-'))
50
50
  if (countryClass) {
51
- value = countryClass.replace('fi-', '');
51
+ value = countryClass.replace('fi-', '')
52
52
  }
53
53
  }
54
54
  }
55
55
 
56
- data[key] = value;
57
- });
56
+ data[key] = value
57
+ })
58
58
 
59
- const bioElement = $('.card-body > small.mt-2').first();
59
+ const bioElement = $('.card-body > small.mt-2').first()
60
60
  if (bioElement.length > 0) {
61
- data.bio = bioElement.text().trim();
61
+ data.bio = bioElement.text().trim()
62
62
  }
63
63
 
64
- const rankingTable = $('table.table-sm').first();
64
+ const rankingTable = $('table.table-sm').first()
65
65
  if (rankingTable.length) {
66
- const headers = rankingTable.find('th').map((i, el) => $(el).text().trim()).get();
67
- const values = rankingTable.find('tbody td').map((i, el) => $(el).text().trim()).get();
66
+ const headers = rankingTable.find('th').map((i, el) => $(el).text().trim()).get()
67
+ const values = rankingTable.find('tbody td').map((i, el) => $(el).text().trim()).get()
68
68
 
69
69
  headers.forEach((header, i) => {
70
70
  if (header && values[i]) {
71
- let val = values[i];
72
- val = val.replace(/\D/g, ''); // remove non-numeric characters to support #23->23, 1st->1, and 42nd->42 etc.
71
+ let val = values[i]
72
+ val = val.replace(/\D/g, '') // remove non-numeric characters to support #23->23, 1st->1, and 42nd->42 etc.
73
73
  if (val) {
74
- data[`rank ${header.toLowerCase()}`] = val;
74
+ data[`rank ${header.toLowerCase()}`] = val
75
75
  }
76
76
  }
77
- });
77
+ })
78
78
  }
79
79
 
80
80
  // Extract tournament record and win rate
81
81
  const overallRow = $('table.table tr').filter((i, el) => {
82
- return $(el).find('td').first().text().trim() === 'Overall';
83
- });
82
+ return $(el).find('td').first().text().trim() === 'Overall'
83
+ })
84
84
 
85
85
  if (overallRow.length > 0) {
86
- const cells = overallRow.find('td');
86
+ const cells = overallRow.find('td')
87
87
  if (cells.length >= 3) {
88
- const record = $(cells[1]).text().trim().replace(/\s+/g, '');
89
- const winRate = $(cells[2]).text().trim();
90
- data.record = record;
91
- data['win rate'] = winRate;
88
+ const record = $(cells[1]).text().trim().replace(/\s+/g, '')
89
+ const winRate = $(cells[2]).text().trim()
90
+ data.record = record
91
+ data['win rate'] = winRate
92
92
  }
93
93
  }
94
94
 
95
- return data;
95
+ return data
96
96
  }
97
97
  }
98
98
 
99
- module.exports = UnityLeagueFetcher;
99
+ module.exports = UnityLeagueFetcher
@@ -0,0 +1,54 @@
1
+ const { request } = require('../utils/httpClient')
2
+
3
+ class UntappedFetcher {
4
+ async fetchById (id) {
5
+ // Split the two-part ID
6
+ const parts = id.split('/')
7
+ if (parts.length !== 2) {
8
+ console.error('Untapped ID must be in format "userId/playerCode"')
9
+ return null
10
+ }
11
+
12
+ const userId = parts[0]
13
+ const playerCode = parts[1]
14
+ const url = `https://mtga.untapped.gg/profile/${userId}/${playerCode}`
15
+ const apiUrl = `https://api.mtga.untapped.gg/api/v1/games/users/${userId}/players/${playerCode}/?card_set=ECL`
16
+
17
+ try {
18
+ const { data } = await request(apiUrl)
19
+ const matches = typeof data === 'string' ? JSON.parse(data) : data
20
+
21
+ if (!Array.isArray(matches) || matches.length === 0) {
22
+ console.warn(`No matches found for Untapped player ${id}`)
23
+ return null
24
+ }
25
+
26
+ const mostRecentMatch = matches.reduce((latest, current) => {
27
+ return (current.match_start > latest.match_start) ? current : latest
28
+ })
29
+
30
+ return this.parseMatch(mostRecentMatch, url)
31
+ } catch (error) {
32
+ console.error(`Error fetching Untapped profile ${id}:`, error.message)
33
+ return null
34
+ }
35
+ }
36
+
37
+ parseMatch (match, url) {
38
+ const data = {
39
+ source: 'Untapped.gg',
40
+ url,
41
+ mtga_rank: null
42
+ }
43
+
44
+ if (match.friendly_ranking_class_after && match.friendly_ranking_tier_after !== null && match.friendly_ranking_tier_after !== undefined) {
45
+ data.mtga_rank = `${match.friendly_ranking_class_after} ${match.friendly_ranking_tier_after}`
46
+ }
47
+
48
+ return data
49
+ }
50
+ }
51
+
52
+ module.exports = UntappedFetcher
53
+
54
+
package/src/index.js CHANGED
@@ -1,89 +1,122 @@
1
- const UnityLeagueFetcher = require('./fetchers/unityLeague');
2
- const MtgEloFetcher = require('./fetchers/mtgElo');
3
- const MeleeFetcher = require('./fetchers/melee');
4
- const TopdeckFetcher = require('./fetchers/topdeck');
1
+ const UnityLeagueFetcher = require('./fetchers/unityLeague')
2
+ const MtgEloFetcher = require('./fetchers/mtgElo')
3
+ const MeleeFetcher = require('./fetchers/melee')
4
+ const TopdeckFetcher = require('./fetchers/topdeck')
5
+ const UntappedFetcher = require('./fetchers/untapped')
5
6
 
6
7
  class PlayerInfoManager {
7
- constructor() {
8
+ constructor () {
8
9
  this.fetchers = {
9
10
  unity: new UnityLeagueFetcher(),
10
11
  mtgelo: new MtgEloFetcher(),
11
12
  melee: new MeleeFetcher(),
12
- topdeck: new TopdeckFetcher()
13
- };
13
+ topdeck: new TopdeckFetcher(),
14
+ untapped: new UntappedFetcher()
15
+ }
14
16
  }
15
17
 
16
- async getPlayerInfo(options) {
17
- const results = [];
18
+ async getPlayerInfo (options, priorityOrder = []) {
19
+ const fetcherMap = {
20
+ unity: { option: 'unityId', fetcher: this.fetchers.unity },
21
+ mtgelo: { option: 'mtgeloId', fetcher: this.fetchers.mtgelo },
22
+ melee: { option: 'meleeUser', fetcher: this.fetchers.melee },
23
+ topdeck: { option: 'topdeckHandle', fetcher: this.fetchers.topdeck },
24
+ untapped: { option: 'untappedId', fetcher: this.fetchers.untapped }
25
+ }
26
+
27
+ // Default order if no priority specified
28
+ const defaultOrder = ['unity', 'mtgelo', 'melee', 'topdeck', 'untapped']
29
+ const order = priorityOrder.length > 0 ? priorityOrder : defaultOrder
18
30
 
19
- if (options.unityId) results.push(await this.fetchers.unity.fetchById(options.unityId));
20
- if (options.mtgeloId) results.push(await this.fetchers.mtgelo.fetchById(options.mtgeloId));
21
- if (options.meleeUser) results.push(await this.fetchers.melee.fetchById(options.meleeUser));
22
- if (options.topdeckHandle) results.push(await this.fetchers.topdeck.fetchById(options.topdeckHandle));
31
+ const results = []
32
+
33
+ for (const source of order) {
34
+ const config = fetcherMap[source]
35
+ if (config && options[config.option]) {
36
+ const result = await config.fetcher.fetchById(options[config.option])
37
+ results.push(result)
38
+ }
39
+ }
23
40
 
24
- const filteredResults = results.filter(r => r !== null);
25
- return this.mergeData(filteredResults, options.verbose);
41
+ const filteredResults = results.filter(r => r !== null)
42
+ return this.mergeData(filteredResults, options.verbose)
26
43
  }
27
44
 
28
- mergeData(results, verbose = false) {
45
+ mergeData (results, verbose = false) {
29
46
  const player = {
30
47
  general: {},
31
48
  sources: {}
32
- };
49
+ }
33
50
 
34
- const seenUrls = new Set();
35
- const firstSeenValues = {};
51
+ const seenUrls = new Set()
52
+ const firstSeenValues = {}
36
53
 
37
- const winRates = [];
54
+ const winRates = []
55
+ let totalWins = 0
56
+ let totalLosses = 0
57
+ let totalDraws = 0
38
58
 
39
59
  results.forEach(res => {
40
- if (seenUrls.has(res.url)) return;
41
- seenUrls.add(res.url);
60
+ if (seenUrls.has(res.url)) return
61
+ seenUrls.add(res.url)
42
62
 
43
- const generalProps = ['name', 'photo', 'age', 'bio', 'team', 'country', 'hometown', 'pronouns', 'facebook', 'twitch', 'youtube'];
63
+ const generalProps = ['name', 'photo', 'age', 'bio', 'team', 'country', 'hometown', 'pronouns', 'facebook', 'twitch', 'youtube', 'mtga_rank']
44
64
  generalProps.forEach(prop => {
45
65
  if (res[prop]) {
46
66
  if (!player.general[prop]) {
47
- player.general[prop] = res[prop];
48
- firstSeenValues[prop] = res[prop];
67
+ player.general[prop] = res[prop]
68
+ firstSeenValues[prop] = res[prop]
49
69
  if (verbose) {
50
- console.log(`⬆️: Promoted '${prop}' from ${res.source} to general information: ${res[prop]}`);
70
+ console.log(`⬆️: Promoted '${prop}' from ${res.source} to general information: ${res[prop]}`)
51
71
  }
52
72
  } else if (verbose) {
53
73
  if (res[prop] === firstSeenValues[prop]) {
54
- console.log(`🆗: ${res.source} has the same '${prop}' as seen before: ${res[prop]}`);
74
+ console.log(`🆗: ${res.source} has the same '${prop}' as seen before: ${res[prop]}`)
55
75
  } else {
56
- console.log(`${prop === 'photo' ? '🆕': '🆚'}️: ${res.source} has different '${prop}' than seen before: ${res[prop]} (instead of ${firstSeenValues[prop]})`);
76
+ console.log(`${prop === 'photo' ? '🆕' : '🆚'}️: ${res.source} has different '${prop}' than seen before: ${res[prop]} (instead of ${firstSeenValues[prop]})`)
57
77
  }
58
78
  }
59
79
  }
60
- });
80
+ })
61
81
 
62
- const winRateStr = res['win rate'] || res.winRate;
82
+ const winRateStr = res['win rate'] || res.winRate
63
83
  if (winRateStr && typeof winRateStr === 'string') {
64
- const winRate = parseFloat(winRateStr.replace('%', ''));
84
+ const winRate = parseFloat(winRateStr.replace('%', ''))
65
85
  if (!isNaN(winRate)) {
66
- winRates.push(winRate);
86
+ winRates.push(winRate)
67
87
  }
68
88
  }
69
89
 
70
- const sourceData = { ...res };
71
- delete sourceData.source;
72
- delete sourceData.url;
90
+ if (res.record && typeof res.record === 'string' && res.record.includes('-')) {
91
+ const parts = res.record.split('-').map(Number)
92
+ if (parts.length >= 2 && parts.every(p => !isNaN(p))) {
93
+ totalWins += parts[0]
94
+ totalLosses += parts[1]
95
+ totalDraws += parts[2] || 0
96
+ }
97
+ }
98
+
99
+ const sourceData = { ...res }
100
+ delete sourceData.source
101
+ delete sourceData.url
73
102
 
74
103
  player.sources[res.source] = {
75
104
  url: res.url,
76
105
  data: sourceData
77
- };
78
- });
106
+ }
107
+ })
79
108
 
80
- if (winRates.length > 0) {
81
- const avgWinRate = winRates.reduce((a, b) => a + b, 0) / winRates.length;
82
- player.general['win rate'] = avgWinRate.toFixed(2) + '%';
109
+ const totalGames = totalWins + totalLosses + totalDraws
110
+ if (totalGames > 0) {
111
+ const overall = (totalWins / totalGames) * 100
112
+ player.general['win rate'] = overall.toFixed(2) + '%'
113
+ } else if (winRates.length > 0) {
114
+ const avgWinRate = winRates.reduce((a, b) => a + b, 0) / winRates.length
115
+ player.general['win rate'] = avgWinRate.toFixed(2) + '%'
83
116
  }
84
117
 
85
- return player;
118
+ return player
86
119
  }
87
120
  }
88
121
 
89
- module.exports = PlayerInfoManager;
122
+ module.exports = PlayerInfoManager
@@ -1,41 +1,41 @@
1
- const https = require('https');
2
- const http = require('http');
3
- const { URL } = require('url');
1
+ const https = require('https')
2
+ const http = require('http')
3
+ const { URL } = require('url')
4
4
 
5
- function request(url, options = {}) {
5
+ function request (url, options = {}) {
6
6
  return new Promise((resolve, reject) => {
7
- const parsedUrl = new URL(url);
8
- const protocol = parsedUrl.protocol === 'https:' ? https : http;
9
-
7
+ const parsedUrl = new URL(url)
8
+ const protocol = parsedUrl.protocol === 'https:' ? https : http
9
+
10
10
  const headers = {
11
11
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
12
12
  ...options.headers
13
- };
13
+ }
14
14
 
15
15
  const requestOptions = {
16
16
  method: options.method || 'GET',
17
- headers: headers,
17
+ headers,
18
18
  timeout: 30000,
19
19
  ...options
20
- };
20
+ }
21
21
 
22
22
  const req = protocol.request(url, requestOptions, (res) => {
23
23
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
24
- const redirectUrl = new URL(res.headers.location, url).href;
25
- const maxRedirects = options.maxRedirects || 5;
24
+ const redirectUrl = new URL(res.headers.location, url).href
25
+ const maxRedirects = options.maxRedirects || 5
26
26
  if (maxRedirects > 0) {
27
27
  return request(redirectUrl, { ...options, maxRedirects: maxRedirects - 1 })
28
28
  .then(resolve)
29
- .catch(reject);
29
+ .catch(reject)
30
30
  } else {
31
- return reject(new Error('Too many redirects'));
31
+ return reject(new Error('Too many redirects'))
32
32
  }
33
33
  }
34
34
 
35
- let data = '';
35
+ let data = ''
36
36
  res.on('data', (chunk) => {
37
- data += chunk;
38
- });
37
+ data += chunk
38
+ })
39
39
 
40
40
  res.on('end', () => {
41
41
  if (res.statusCode >= 200 && res.statusCode < 300) {
@@ -43,27 +43,27 @@ function request(url, options = {}) {
43
43
  data,
44
44
  status: res.statusCode,
45
45
  headers: res.headers
46
- });
46
+ })
47
47
  } else {
48
- reject(new Error(`Request failed with status ${res.statusCode}`));
48
+ reject(new Error(`Request failed with status ${res.statusCode}`))
49
49
  }
50
- });
51
- });
50
+ })
51
+ })
52
52
 
53
53
  req.on('error', (err) => {
54
- reject(err);
55
- });
54
+ reject(err)
55
+ })
56
56
 
57
57
  req.on('timeout', () => {
58
- req.destroy();
59
- reject(new Error('Request timed out'));
60
- });
58
+ req.destroy()
59
+ reject(new Error('Request timed out'))
60
+ })
61
61
 
62
62
  if (options.body) {
63
- req.write(options.body);
63
+ req.write(options.body)
64
64
  }
65
- req.end();
66
- });
65
+ req.end()
66
+ })
67
67
  }
68
68
 
69
- module.exports = { request };
69
+ module.exports = { request }