From 47a23f04846147d35d6ef1e090b461a0f69d2d3c Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 17 Oct 2023 23:32:20 +0100 Subject: [PATCH] fix scoresaber api url --- src/network/queues/scoresaber/page-queue.js | 865 +++++++++++++------- 1 file changed, 570 insertions(+), 295 deletions(-) diff --git a/src/network/queues/scoresaber/page-queue.js b/src/network/queues/scoresaber/page-queue.js index 15e70f1..ed097e5 100644 --- a/src/network/queues/scoresaber/page-queue.js +++ b/src/network/queues/scoresaber/page-queue.js @@ -1,33 +1,42 @@ -import {default as createQueue, PRIORITY} from '../http-queue'; -import {substituteVars} from '../../../utils/format' -import {extractDiffAndType} from '../../../utils/scoresaber/format' -import cfDecryptEmail from '../../../utils/cf-email-decrypt' -import {capitalize, getFirstRegexpMatch, opt} from '../../../utils/js' -import {dateFromString} from '../../../utils/date' -import {LEADERBOARD_SCORES_PER_PAGE} from '../../../utils/scoresaber/consts' +import ssrConfig from "../../../ssr-config"; +import cfDecryptEmail from "../../../utils/cf-email-decrypt"; +import { dateFromString } from "../../../utils/date"; +import { substituteVars } from "../../../utils/format"; +import { capitalize, getFirstRegexpMatch, opt } from "../../../utils/js"; +import { LEADERBOARD_SCORES_PER_PAGE } from "../../../utils/scoresaber/consts"; +import { extractDiffAndType } from "../../../utils/scoresaber/format"; +import { PRIORITY, default as createQueue } from "../http-queue"; -export const SS_HOST = 'https://scoresaber.com'; -const SS_CORS_HOST = '/cors/score-saber'; -const RANKEDS_URL = SS_CORS_HOST + '/api.php?function=get-leaderboards&cat=1&limit=5000&ranked=1&page=${page}'; -const PLAYER_PROFILE_URL = SS_CORS_HOST + '/u/${playerId}?page=1&sort=2' -const COUNTRY_RANKING_URL = SS_CORS_HOST + '/global/${page}?country=${country}' -const LEADERBOARD_URL = SS_CORS_HOST + '/leaderboard/${leaderboardId}?page=${page}' +export const SS_HOST = "https://scoresaber.com"; +const SS_CORS_HOST = `${ssrConfig.proxy}/${SS_HOST}`; +const RANKEDS_URL = + SS_CORS_HOST + + "/api.php?function=get-leaderboards&cat=1&limit=5000&ranked=1&page=${page}"; +const PLAYER_PROFILE_URL = SS_CORS_HOST + "/u/${playerId}?page=1&sort=2"; +const COUNTRY_RANKING_URL = SS_CORS_HOST + "/global/${page}?country=${country}"; +const LEADERBOARD_URL = + SS_CORS_HOST + "/leaderboard/${leaderboardId}?page=${page}"; -export const parseSsInt = text => { - const value = getFirstRegexpMatch(/(-?[0-9,]+)\s*$/, text) - return value ? parseInt(value.replace(/[^\d-]/g, '') , 10) : null; -} -export const parseSsFloat = text => text ? parseFloat(getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, ''))) : null; +export const parseSsInt = (text) => { + const value = getFirstRegexpMatch(/(-?[0-9,]+)\s*$/, text); + return value ? parseInt(value.replace(/[^\d-]/g, ""), 10) : null; +}; +export const parseSsFloat = (text) => + text + ? parseFloat( + getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, "")) + ) + : null; export default (options = {}) => { const queue = createQueue(options); - const {fetchJson, fetchHtml, ...queueToReturn} = queue; + const { fetchJson, fetchHtml, ...queueToReturn } = queue; const processRankeds = (data) => { if (!data || !data.songs || !Array.isArray(data.songs)) return null; - return data.songs.map(s => { + return data.songs.map((s) => { const { uid: leaderboardId, id: hash, @@ -37,205 +46,344 @@ export default (options = {}) => { levelAuthorName, stars, image: imageUrl, - diff + diff, } = s; const diffInfo = extractDiffAndType(diff); - return {leaderboardId, hash, name, subName, authorName, levelAuthorName, imageUrl, stars, diff, diffInfo}; - }) - } + return { + leaderboardId, + hash, + name, + subName, + authorName, + levelAuthorName, + imageUrl, + stars, + diff, + diffInfo, + }; + }); + }; - const getImgUrl = imgUrl => { + const getImgUrl = (imgUrl) => { try { const aUrl = new URL(imgUrl); return SS_HOST + aUrl.pathname; - } - catch(err) { + } catch (err) { return null; } - } + }; - const rankeds = async (page = 1, priority = PRIORITY.BG_NORMAL, options = {}) => fetchJson(substituteVars(RANKEDS_URL, {page}), options, priority) - .then(r => { - r.body = processRankeds(r.body); + const rankeds = async ( + page = 1, + priority = PRIORITY.BG_NORMAL, + options = {} + ) => + fetchJson(substituteVars(RANKEDS_URL, { page }), options, priority).then( + (r) => { + r.body = processRankeds(r.body); - return r; - }) + return r; + } + ); const processPlayerProfile = (playerId, doc) => { cfDecryptEmail(doc); - let avatar = getImgUrl(opt(doc.querySelector('.column.avatar img'), 'src', null)); + let avatar = getImgUrl( + opt(doc.querySelector(".column.avatar img"), "src", null) + ); - let playerName = opt(doc.querySelector('.content .column:not(.avatar) .title a'), 'innerText'); + let playerName = opt( + doc.querySelector(".content .column:not(.avatar) .title a"), + "innerText" + ); playerName = playerName ? playerName.trim() : null; - let country = getFirstRegexpMatch(/^.*?\/flags\/([^.]+)\..*$/, opt(doc.querySelector('.content .column .title img'), 'src')); + let country = getFirstRegexpMatch( + /^.*?\/flags\/([^.]+)\..*$/, + opt(doc.querySelector(".content .column .title img"), "src") + ); country = country ? country.toUpperCase() : null; - let pageNum = parseSsInt(opt(doc.querySelector('.pagination .pagination-list li a.is-current'), 'innerText', null)); - pageNum = !isNaN(pageNum) ? pageNum : null + let pageNum = parseSsInt( + opt( + doc.querySelector(".pagination .pagination-list li a.is-current"), + "innerText", + null + ) + ); + pageNum = !isNaN(pageNum) ? pageNum : null; - let pageQty = parseSsInt(opt(doc.querySelector('.pagination .pagination-list li:last-of-type'), 'innerText', null)); - pageQty = !isNaN(pageQty) ? pageQty : null + let pageQty = parseSsInt( + opt( + doc.querySelector(".pagination .pagination-list li:last-of-type"), + "innerText", + null + ) + ); + pageQty = !isNaN(pageQty) ? pageQty : null; - let totalItems = parseSsFloat(getFirstRegexpMatch(/^\s*(?:[^:]+)\s*:?\s*<\/strong>\s*(.*)$/, opt(doc.querySelector('.columns .column:not(.is-narrow) ul li:nth-of-type(3)'), 'innerHTML'))) + let totalItems = parseSsFloat( + getFirstRegexpMatch( + /^\s*(?:[^:]+)\s*:?\s*<\/strong>\s*(.*)$/, + opt( + doc.querySelector( + ".columns .column:not(.is-narrow) ul li:nth-of-type(3)" + ), + "innerHTML" + ) + ) + ); totalItems = !isNaN(totalItems) ? totalItems : 0; - let playerRank = parseSsInt(opt(doc.querySelector('.content .column ul li:first-of-type a:first-of-type'), 'innerText')); + let playerRank = parseSsInt( + opt( + doc.querySelector( + ".content .column ul li:first-of-type a:first-of-type" + ), + "innerText" + ) + ); playerRank = !isNaN(playerRank) ? playerRank : null; - let countryRank = parseSsInt(opt(doc.querySelector('.content .column ul li:first-of-type a[href^="/global?country="]'), 'innerText')) + let countryRank = parseSsInt( + opt( + doc.querySelector( + '.content .column ul li:first-of-type a[href^="/global?country="]' + ), + "innerText" + ) + ); countryRank = !isNaN(countryRank) ? countryRank : null; - const stats = [{key: 'Player ranking', type: 'rank', value: playerRank, countryRank: countryRank}] + const stats = [ + { + key: "Player ranking", + type: "rank", + value: playerRank, + countryRank: countryRank, + }, + ] .concat( - [...doc.querySelectorAll('.content .column ul li')] - .map(li => { - const matches = li.innerHTML.match(/^\s*([^:]+)\s*:?\s*<\/strong>\s*(.*)$/); + [...doc.querySelectorAll(".content .column ul li")] + .map((li) => { + const matches = li.innerHTML.match( + /^\s*([^:]+)\s*:?\s*<\/strong>\s*(.*)$/ + ); if (!matches) return null; const mapping = [ - {key: 'Performance Points', type: 'number', precision: 2, suffix: 'pp', number: true,}, - {key: 'Play Count', type: 'number', precision: 0, number: true, colorVar: 'selected',}, - {key: 'Total Score', type: 'number', precision: 0, number: true, colorVar: 'selected',}, { - key: 'Replays Watched by Others', - type: 'number', - precision: 0, - title: 'profile.stats.replays', + key: "Performance Points", + type: "number", + precision: 2, + suffix: "pp", number: true, - colorVar: 'dimmed', }, - {key: 'Role', number: false, colorVar: 'dimmed'}, - {key: 'Inactive Account', number: false, colorVar: 'decrease'}, - {key: 'Banned', number: false, colorVar: 'decrease'}, + { + key: "Play Count", + type: "number", + precision: 0, + number: true, + colorVar: "selected", + }, + { + key: "Total Score", + type: "number", + precision: 0, + number: true, + colorVar: "selected", + }, + { + key: "Replays Watched by Others", + type: "number", + precision: 0, + title: "profile.stats.replays", + number: true, + colorVar: "dimmed", + }, + { key: "Role", number: false, colorVar: "dimmed" }, + { key: "Inactive Account", number: false, colorVar: "decrease" }, + { key: "Banned", number: false, colorVar: "decrease" }, ]; - const value = mapping.filter(m => m.number).map(m => m.key).includes(matches[1]) + const value = mapping + .filter((m) => m.number) + .map((m) => m.key) + .includes(matches[1]) ? parseSsFloat(matches[2]) : matches[2]; - const item = mapping.find(m => m.key === matches[1]); - return item ? {...item, value} : {label: matches[1], value}; + const item = mapping.find((m) => m.key === matches[1]); + return item ? { ...item, value } : { label: matches[1], value }; }) - .filter(s => s) - ).reduce((cum, item) => { - if (item.key) - switch (item.key) { - case 'Player ranking': - cum.rank = item.value; - cum.countryRank = item.countryRank; + .filter((s) => s) + ) + .reduce( + (cum, item) => { + if (item.key) + switch (item.key) { + case "Player ranking": + cum.rank = item.value; + cum.countryRank = item.countryRank; + break; + + case "Performance Points": + cum.pp = item.value; + break; + case "Play Count": + cum.playCount = item.value; + break; + case "Total Score": + cum.totalScore = item.value; + break; + case "Replays Watched by Others": + cum.replays = item.value; + break; + case "Role": + cum.role = item.value; + break; + case "Inactive Account": + cum.inactiveAccount = true; + break; + case "Banned": + cum.bannedAccount = true; + break; + } + + return cum; + }, + { inactiveAccount: false, bannedAccount: false } + ); + + const scores = [...doc.querySelectorAll("table.ranking tbody tr")].map( + (tr) => { + let ret = { lastUpdated: new Date() }; + + const rank = tr.querySelector("th.rank"); + if (rank) { + const rankMatch = parseSsInt(rank.innerText); + ret.rank = !isNaN(rankMatch) ? rankMatch : null; + } else { + ret.rank = null; + } + + const song = tr.querySelector("th.song a"); + if (song) { + const leaderboardId = parseInt( + getFirstRegexpMatch(/leaderboard\/(\d+)/, song.href), + 10 + ); + ret.leaderboardId = leaderboardId ? leaderboardId : null; + } else { + ret.leaderboardId = null; + } + + const img = tr.querySelector("th.song img"); + const imgMatch = img + ? img.src.match(/([^\/]+)\.(jpg|jpeg|png)$/) + : null; + ret.songHash = imgMatch ? imgMatch[1] : null; + + const songPp = tr.querySelector("th.song a .songTop.pp"); + const songMatch = songPp + ? songPp.innerHTML + .replace(/&/g, "&") + .replace( + /\[email protected]<\/span>/g, + "" + ) + .match(/^(.*?)\s*]+>(.*?)<\/span>/) + : null; + if (songMatch) { + const songAuthorMatch = songMatch[1].match(/^(.*?)\s-\s(.*)$/); + if (songAuthorMatch) { + ret.songName = songAuthorMatch[2]; + ret.songSubName = ""; + ret.songAuthorName = songAuthorMatch[1]; + } else { + ret.songName = songMatch[1]; + ret.songSubName = ""; + ret.songAuthorName = ""; + } + ret.difficultyRaw = + "_" + + songMatch[2].replace("Expert+", "ExpertPlus") + + "_SoloStandard"; + } else { + ret = Object.assign(ret, { + songName: null, + songSubName: null, + songAuthorName: null, + difficultyRaw: null, + }); + } + + const songMapper = tr.querySelector("th.song a .songTop.mapper"); + ret.levelAuthorName = songMapper ? songMapper.innerText : null; + + const songDate = tr.querySelector("th.song span.songBottom.time"); + ret.timeSet = songDate ? dateFromString(songDate.title) : null; + + const pp = parseSsFloat( + opt(tr.querySelector("th.score .scoreTop.ppValue"), "innerText") + ); + ret.pp = !isNaN(pp) ? pp : null; + + const ppWeighted = parseSsFloat( + getFirstRegexpMatch( + /^\(([0-9.]+)pp\)$/, + opt( + tr.querySelector("th.score .scoreTop.ppWeightedValue"), + "innerText" + ) + ) + ); + ret.ppWeighted = !isNaN(ppWeighted) ? ppWeighted : null; + + const scoreInfo = tr.querySelector("th.score .scoreBottom"); + const scoreInfoMatch = scoreInfo + ? scoreInfo.innerText.match(/^([^:]+):\s*([0-9,.]+)(?:.*?\((.*?)\))?/) + : null; + if (scoreInfoMatch) { + switch (scoreInfoMatch[1]) { + case "score": + ret.acc = null; + scoreInfoMatch[3] = scoreInfoMatch[3] + ? scoreInfoMatch[3].replace("-", "").trim() + : null; + ret.mods = + scoreInfoMatch[3] && scoreInfoMatch[3].length + ? scoreInfoMatch[3] + .split(",") + .filter((m) => m && m.trim().length) + : null; + ret.score = parseSsFloat(scoreInfoMatch[2]); break; - case 'Performance Points': - cum.pp = item.value; - break; - case 'Play Count': - cum.playCount = item.value; - break; - case 'Total Score': - cum.totalScore = item.value; - break; - case 'Replays Watched by Others': - cum.replays = item.value; - break; - case 'Role': - cum.role = item.value; - break; - case 'Inactive Account': - cum.inactiveAccount = true; - break; - case 'Banned': - cum.bannedAccount = true; + case "accuracy": + ret.score = null; + scoreInfoMatch[3] = scoreInfoMatch[3] + ? scoreInfoMatch[3].replace("-", "").trim() + : null; + ret.mods = + scoreInfoMatch[3] && scoreInfoMatch[3].length + ? scoreInfoMatch[3] + .split(",") + .filter((m) => m && m.trim().length) + : null; + ret.acc = parseSsFloat(scoreInfoMatch[2]); break; } - - return cum; - }, {inactiveAccount: false, bannedAccount: false}); - - const scores = [...doc.querySelectorAll('table.ranking tbody tr')].map(tr => { - let ret = {lastUpdated: new Date()}; - - const rank = tr.querySelector('th.rank'); - if (rank) { - const rankMatch = parseSsInt(rank.innerText); - ret.rank = !isNaN(rankMatch) ? rankMatch : null; - } else { - ret.rank = null; - } - - const song = tr.querySelector('th.song a'); - if (song) { - const leaderboardId = parseInt(getFirstRegexpMatch(/leaderboard\/(\d+)/, song.href), 10); - ret.leaderboardId = leaderboardId ? leaderboardId : null; - } else { - ret.leaderboardId = null; - } - - const img = tr.querySelector('th.song img'); - const imgMatch = img ? img.src.match(/([^\/]+)\.(jpg|jpeg|png)$/) : null; - ret.songHash = imgMatch ? imgMatch[1] : null; - - const songPp = tr.querySelector('th.song a .songTop.pp'); - const songMatch = songPp - ? songPp.innerHTML - .replace(/&/g, '&') - .replace(/\[email protected]<\/span>/g, '') - .match(/^(.*?)\s*]+>(.*?)<\/span>/) - : null; - if (songMatch) { - const songAuthorMatch = songMatch[1].match(/^(.*?)\s-\s(.*)$/); - if (songAuthorMatch) { - ret.songName = songAuthorMatch[2]; - ret.songSubName = ''; - ret.songAuthorName = songAuthorMatch[1]; - } else { - ret.songName = songMatch[1]; - ret.songSubName = ''; - ret.songAuthorName = ''; } - ret.difficultyRaw = '_' + songMatch[2].replace('Expert+', 'ExpertPlus') + '_SoloStandard' - } else { - ret = Object.assign(ret, {songName: null, songSubName: null, songAuthorName: null, difficultyRaw: null}); + + return ret; } - - const songMapper = tr.querySelector('th.song a .songTop.mapper'); - ret.levelAuthorName = songMapper ? songMapper.innerText : null; - - const songDate = tr.querySelector('th.song span.songBottom.time'); - ret.timeSet = songDate ? dateFromString(songDate.title) : null; - - const pp = parseSsFloat(opt(tr.querySelector('th.score .scoreTop.ppValue'), 'innerText')); - ret.pp = !isNaN(pp) ? pp : null; - - const ppWeighted = parseSsFloat(getFirstRegexpMatch(/^\(([0-9.]+)pp\)$/, opt(tr.querySelector('th.score .scoreTop.ppWeightedValue'), 'innerText'))); - ret.ppWeighted = !isNaN(ppWeighted) ? ppWeighted : null; - - const scoreInfo = tr.querySelector('th.score .scoreBottom'); - const scoreInfoMatch = scoreInfo ? scoreInfo.innerText.match(/^([^:]+):\s*([0-9,.]+)(?:.*?\((.*?)\))?/) : null; - if (scoreInfoMatch) { - switch (scoreInfoMatch[1]) { - case "score": - ret.acc = null; - scoreInfoMatch[3] = scoreInfoMatch[3] ? scoreInfoMatch[3].replace('-','').trim() : null - ret.mods = scoreInfoMatch[3] && scoreInfoMatch[3].length ? scoreInfoMatch[3].split(',').filter(m => m && m.trim().length) : null; - ret.score = parseSsFloat(scoreInfoMatch[2]); - break; - - case "accuracy": - ret.score = null; - scoreInfoMatch[3] = scoreInfoMatch[3] ? scoreInfoMatch[3].replace('-','').trim() : null - ret.mods = scoreInfoMatch[3] && scoreInfoMatch[3].length ? scoreInfoMatch[3].split(',').filter(m => m && m.trim().length) : null; - ret.acc = parseSsFloat(scoreInfoMatch[2]); - break; - } - } - - return ret; - }); - const recentPlay = scores && scores.length && scores[0].timeSet ? scores[0].timeSet : null; + ); + const recentPlay = + scores && scores.length && scores[0].timeSet ? scores[0].timeSet : null; return { player: { @@ -243,19 +391,28 @@ export default (options = {}) => { playerId, playerName, avatar, - externalProfileUrl: opt(doc.querySelector('.content .column:not(.avatar) .title a'), 'href', null), - history: getFirstRegexpMatch(/data:\s*\[([0-9,]+)\]/, doc.body.innerHTML), + externalProfileUrl: opt( + doc.querySelector(".content .column:not(.avatar) .title a"), + "href", + null + ), + history: getFirstRegexpMatch( + /data:\s*\[([0-9,]+)\]/, + doc.body.innerHTML + ), country, - badges: [...doc.querySelectorAll('.column.avatar center img')].map(img => ({ - image: getImgUrl(img.src), - description: img.title - })), + badges: [...doc.querySelectorAll(".column.avatar center img")].map( + (img) => ({ + image: getImgUrl(img.src), + description: img.title, + }) + ), rank: stats.rank ? stats.rank : null, countryRank: stats.countryRank ? stats.countryRank : null, pp: stats.pp !== undefined ? stats.pp : null, inactive: stats.inactiveAccount ? 1 : 0, banned: stats.bannedAccount ? 1 : 0, - role: '', + role: "", }, scoreStats: { totalScore: stats.totalScore ? stats.totalScore : 0, @@ -270,41 +427,59 @@ export default (options = {}) => { pageNum, pageQty, totalItems, - } + }, }; - } + }; - const player = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => fetchHtml(substituteVars(PLAYER_PROFILE_URL, {playerId}), options, priority) - .then(r => { + const player = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => + fetchHtml( + substituteVars(PLAYER_PROFILE_URL, { playerId }), + options, + priority + ).then((r) => { r.body = processPlayerProfile(playerId, r.body); - return r - }) + return r; + }); const processCountryRanking = (country, doc) => { cfDecryptEmail(doc); - const data = [...doc.querySelectorAll('.ranking.global .player a')] - .map(a => { + const data = [...doc.querySelectorAll(".ranking.global .player a")].map( + (a) => { const tr = a.closest("tr"); - const id = getFirstRegexpMatch(/\/(\d+)$/, a.href) + const id = getFirstRegexpMatch(/\/(\d+)$/, a.href); - const avatar = getImgUrl(opt(tr.querySelector('td.picture img'), 'src', null)); + const avatar = getImgUrl( + opt(tr.querySelector("td.picture img"), "src", null) + ); - let country = getFirstRegexpMatch(/^.*?\/flags\/([^.]+)\..*$/, opt(tr.querySelector('td.player img'), 'src', null)); + let country = getFirstRegexpMatch( + /^.*?\/flags\/([^.]+)\..*$/, + opt(tr.querySelector("td.player img"), "src", null) + ); country = country ? country.toUpperCase() : null; - let difference = parseSsInt(opt(tr.querySelector('td.diff'), 'innerText', null)); - difference = !isNaN(difference) ? difference : null + let difference = parseSsInt( + opt(tr.querySelector("td.diff"), "innerText", null) + ); + difference = !isNaN(difference) ? difference : null; - let playerName = opt(a.querySelector('.songTop.pp'), 'innerText'); - playerName = playerName || playerName === '' ? playerName.trim() : null; + let playerName = opt(a.querySelector(".songTop.pp"), "innerText"); + playerName = playerName || playerName === "" ? playerName.trim() : null; - let pp = parseSsFloat(opt(tr.querySelector('td.pp .scoreTop.ppValue'), 'innerText')); + let pp = parseSsFloat( + opt(tr.querySelector("td.pp .scoreTop.ppValue"), "innerText") + ); pp = !isNaN(pp) ? pp : null; - let rank = parseSsInt(getFirstRegexpMatch(/^\s*#(\d+)\s*$/, opt(tr.querySelector('td.rank'), 'innerText', null))); - rank = !isNaN(rank) ? rank : null + let rank = parseSsInt( + getFirstRegexpMatch( + /^\s*#(\d+)\s*$/, + opt(tr.querySelector("td.rank"), "innerText", null) + ) + ); + rank = !isNaN(rank) ? rank : null; return { avatar, @@ -315,167 +490,258 @@ export default (options = {}) => { playerName, pp, rank, - } - }) + }; + } + ); - return {players: data}; - } + return { players: data }; + }; - const countryRanking = async (country, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchHtml(substituteVars(COUNTRY_RANKING_URL, {country, page}), options, priority) - .then(r => { - r.body = processCountryRanking(country, r.body) + const countryRanking = async ( + country, + page = 1, + priority = PRIORITY.FG_LOW, + options = {} + ) => + fetchHtml( + substituteVars(COUNTRY_RANKING_URL, { country, page }), + options, + priority + ).then((r) => { + r.body = processCountryRanking(country, r.body); return r; - }) + }); - const parseSsLeaderboardScores = doc => { + const parseSsLeaderboardScores = (doc) => { cfDecryptEmail(doc); - return [...doc.querySelectorAll('table.ranking tbody tr')].map(tr => { - let ret = {player: {playerInfo: {countries: []}}, score: {lastUpdated: new Date()}}; + return [...doc.querySelectorAll("table.ranking tbody tr")].map((tr) => { + let ret = { + player: { playerInfo: { countries: [] } }, + score: { lastUpdated: new Date() }, + }; - const parseValue = selector => { - const val = parseSsFloat(opt(tr.querySelector(selector), 'innerText')); + const parseValue = (selector) => { + const val = parseSsFloat(opt(tr.querySelector(selector), "innerText")); return !isNaN(val) ? val : null; - } + }; - ret.player.playerInfo.avatar = getImgUrl(opt(tr.querySelector('.picture img'), 'src', null)); + ret.player.playerInfo.avatar = getImgUrl( + opt(tr.querySelector(".picture img"), "src", null) + ); - ret.score.rank = parseSsInt(opt(tr.querySelector('td.rank'), 'innerText')); + ret.score.rank = parseSsInt( + opt(tr.querySelector("td.rank"), "innerText") + ); if (isNaN(ret.score.rank)) ret.score.rank = null; - const player = tr.querySelector('.player a'); + const player = tr.querySelector(".player a"); if (player) { - let country = getFirstRegexpMatch(/^.*?\/flags\/([^.]+)\..*$/, opt(player.querySelector('img'), 'src', '')); + let country = getFirstRegexpMatch( + /^.*?\/flags\/([^.]+)\..*$/, + opt(player.querySelector("img"), "src", "") + ); country = country ? country.toUpperCase() : null; if (country) { - ret.player.playerInfo.country = country - ret.player.playerInfo.countries.push({country, rank: null}); + ret.player.playerInfo.country = country; + ret.player.playerInfo.countries.push({ country, rank: null }); } - ret.player.name = opt(player.querySelector('span.songTop.pp'), 'innerText') - ret.player.name = ret.player.name ? ret.player.name.trim().replace(''', "'") : null; - ret.player.playerId = getFirstRegexpMatch(/\/u\/(\d+)((\?|&|#).*)?$/, opt(player, 'href', '')); - ret.player.playerId = ret.player.playerId ? ret.player.playerId.trim() : null; + ret.player.name = opt( + player.querySelector("span.songTop.pp"), + "innerText" + ); + ret.player.name = ret.player.name + ? ret.player.name.trim().replace("'", "'") + : null; + ret.player.playerId = getFirstRegexpMatch( + /\/u\/(\d+)((\?|&|#).*)?$/, + opt(player, "href", "") + ); + ret.player.playerId = ret.player.playerId + ? ret.player.playerId.trim() + : null; } else { ret.player.playerId = null; ret.player.name = null; ret.player.playerInfo.country = null; } - ret.score.score = parseValue('td.score'); + ret.score.score = parseValue("td.score"); - ret.score.timeSetString = opt(tr.querySelector('td.timeset'), 'innerText', null); - if (ret.score.timeSetString) ret.score.timeSetString = ret.score.timeSetString.trim(); + ret.score.timeSetString = opt( + tr.querySelector("td.timeset"), + "innerText", + null + ); + if (ret.score.timeSetString) + ret.score.timeSetString = ret.score.timeSetString.trim(); - ret.score.mods = opt(tr.querySelector('td.mods'), 'innerText'); - ret.score.mods = ret.score.mods ? ret.score.mods.replace('-','').trim() : null - ret.score.mods = ret.score.mods && ret.score.mods.length ? ret.score.mods.split(',').filter(m => m && m.trim().length) : null; + ret.score.mods = opt(tr.querySelector("td.mods"), "innerText"); + ret.score.mods = ret.score.mods + ? ret.score.mods.replace("-", "").trim() + : null; + ret.score.mods = + ret.score.mods && ret.score.mods.length + ? ret.score.mods.split(",").filter((m) => m && m.trim().length) + : null; - ret.score.pp = parseValue('td.pp .scoreTop.ppValue'); + ret.score.pp = parseValue("td.pp .scoreTop.ppValue"); - ret.score.percentage = parseValue('td.percentage'); + ret.score.percentage = parseValue("td.percentage"); return ret; }); - } + }; const processLeaderboard = (leaderboardId, page, doc) => { cfDecryptEmail(doc); - const diffs = [...doc.querySelectorAll('.tabs ul li a')].map(a => { - let leaderboardId = parseInt(getFirstRegexpMatch(/leaderboard\/(\d+)$/, a.href), 10); + const diffs = [...doc.querySelectorAll(".tabs ul li a")].map((a) => { + let leaderboardId = parseInt( + getFirstRegexpMatch(/leaderboard\/(\d+)$/, a.href), + 10 + ); if (isNaN(leaderboardId)) leaderboardId = null; - const span = a.querySelector('span'); + const span = a.querySelector("span"); const color = span ? span.style.color : null; - return {name: a.innerText, leaderboardId, color}; + return { name: a.innerText, leaderboardId, color }; }); - const currentDiffHuman = opt(doc.querySelector('.tabs li.is-active a span'), 'innerText', null); + const currentDiffHuman = opt( + doc.querySelector(".tabs li.is-active a span"), + "innerText", + null + ); let diff = null; let diffInfo = null; if (currentDiffHuman) { - const lowerCaseDiff = currentDiffHuman.toLowerCase().replace('+', 'Plus'); + const lowerCaseDiff = currentDiffHuman.toLowerCase().replace("+", "Plus"); diff = `_${capitalize(lowerCaseDiff)}_SoloStandard`; - diffInfo = {type: 'Standard', diff: lowerCaseDiff} + diffInfo = { type: "Standard", diff: lowerCaseDiff }; } - const songName = opt(doc.querySelector('.column.is-one-third-desktop .box:first-of-type .title a'), 'innerText', null); + const songName = opt( + doc.querySelector( + ".column.is-one-third-desktop .box:first-of-type .title a" + ), + "innerText", + null + ); - const imageUrl = getImgUrl(opt(doc.querySelector('.column.is-one-third-desktop .box:first-of-type .columns .column.is-one-quarter img'), 'src', null)); + const imageUrl = getImgUrl( + opt( + doc.querySelector( + ".column.is-one-third-desktop .box:first-of-type .columns .column.is-one-quarter img" + ), + "src", + null + ) + ); const songInfo = [ - {id: 'hash', label: 'ID', value: null}, - {id: 'scores', label: 'Scores', value: null}, - {id: 'status', label: 'Status', value: null}, - {id: 'totalScores', label: 'Total Scores', value: null}, - {id: 'notes', label: 'Note Count', value: null}, - {id: 'bpm', label: 'BPM', value: null}, - {id: 'stars', label: 'Star Difficulty', value: null}, - {id: 'levelAuthorName', label: 'Mapped by', value: null}, + { id: "hash", label: "ID", value: null }, + { id: "scores", label: "Scores", value: null }, + { id: "status", label: "Status", value: null }, + { id: "totalScores", label: "Total Scores", value: null }, + { id: "notes", label: "Note Count", value: null }, + { id: "bpm", label: "BPM", value: null }, + { id: "stars", label: "Star Difficulty", value: null }, + { id: "levelAuthorName", label: "Mapped by", value: null }, ] - .map(sid => { - let songInfoBox = doc.querySelector('.column.is-one-third-desktop .box:first-of-type') + .map((sid) => { + let songInfoBox = doc.querySelector( + ".column.is-one-third-desktop .box:first-of-type" + ); return { ...sid, - value: songInfoBox ? songInfoBox.innerHTML.match(new RegExp(sid.label + ':\\s*(.*?)', 'i')) : null, - } + value: songInfoBox + ? songInfoBox.innerHTML.match( + new RegExp(sid.label + ":\\s*(.*?)", "i") + ) + : null, + }; }) - .concat([{id: 'name', value: [null, songName]}]) - .reduce((cum, sid) => { - let value = Array.isArray(sid.value) ? sid.value[1] : null; + .concat([{ id: "name", value: [null, songName] }]) + .reduce( + (cum, sid) => { + let value = Array.isArray(sid.value) ? sid.value[1] : null; - if (value !== null && ['scores', 'totalScores', 'bpm', 'notes'].includes(sid.id)) { - value = parseSsFloat(value); + if ( + value !== null && + ["scores", "totalScores", "bpm", "notes"].includes(sid.id) + ) { + value = parseSsFloat(value); - if (value !== null) { + if (value !== null) { + cum.stats[sid.id] = value; + } + + return cum; + } + if (value !== null && sid.id === "stars") value = parseSsFloat(value); + if (value && sid.id === "name") { + const songAuthorMatch = value.match(/^(.*?)\s-\s(.*)$/); + if (songAuthorMatch) { + value = songAuthorMatch[2]; + cum.authorName = songAuthorMatch[1]; + } else { + cum.authorName = ""; + } + cum.subName = ""; + } + if (value && sid.id === "levelAuthorName") { + const el = doc.createElement("div"); + el.innerHTML = value; + value = el.innerText; + } + if (value && sid.id === "status") { cum.stats[sid.id] = value; + return cum; } + if (value !== null) cum[sid.id] = value; return cum; - } - if (value !== null && sid.id === 'stars') value = parseSsFloat(value); - if (value && sid.id === 'name') { - const songAuthorMatch = value.match(/^(.*?)\s-\s(.*)$/); - if (songAuthorMatch) { - value = songAuthorMatch[2]; - cum.authorName = songAuthorMatch[1]; - } else { - cum.authorName = ''; - } - cum.subName = ''; - } - if (value && sid.id === 'levelAuthorName') { - const el = doc.createElement('div'); - el.innerHTML = value; - value = el.innerText; - } - if (value && sid.id === 'status') { - cum.stats[sid.id] = value; - return cum; - } - if (value !== null) cum[sid.id] = value; + }, + { imageUrl, stats: {} } + ); - return cum; - }, {imageUrl, stats: {}}); + const { stats, ...song } = songInfo; + const leaderboard = { leaderboardId, song, diffInfo, stats }; - const {stats, ...song} = songInfo; - const leaderboard = {leaderboardId, song, diffInfo, stats}; - - let pageQty = parseInt(opt(doc.querySelector('.pagination .pagination-list li:last-of-type'), 'innerText', null), 10) + let pageQty = parseInt( + opt( + doc.querySelector(".pagination .pagination-list li:last-of-type"), + "innerText", + null + ), + 10 + ); if (isNaN(pageQty)) pageQty = null; - let scoresQty = opt(stats, 'scores', 0); + let scoresQty = opt(stats, "scores", 0); if (isNaN(scoresQty)) scoresQty = null; - const totalItems = pageQty && scoresQty ? (Math.ceil(scoresQty / LEADERBOARD_SCORES_PER_PAGE) > pageQty ? pageQty * LEADERBOARD_SCORES_PER_PAGE : scoresQty) : null; + const totalItems = + pageQty && scoresQty + ? Math.ceil(scoresQty / LEADERBOARD_SCORES_PER_PAGE) > pageQty + ? pageQty * LEADERBOARD_SCORES_PER_PAGE + : scoresQty + : null; - let diffChartText = getFirstRegexpMatch(/'difficulty',\s*([0-9.,\s]+)\s*\]/, doc.body.innerHTML) - let diffChart = (diffChartText ? diffChartText : '').split(',').map(i => parseFloat(i)).filter(i => i && !isNaN(i)); + let diffChartText = getFirstRegexpMatch( + /'difficulty',\s*([0-9.,\s]+)\s*\]/, + doc.body.innerHTML + ); + let diffChart = (diffChartText ? diffChartText : "") + .split(",") + .map((i) => parseFloat(i)) + .filter((i) => i && !isNaN(i)); return { diffs, @@ -485,15 +751,24 @@ export default (options = {}) => { pageQty, totalItems, scores: parseSsLeaderboardScores(doc), - } - } + }; + }; - const leaderboard = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchHtml(substituteVars(LEADERBOARD_URL, {leaderboardId, page}), options, priority) - .then(r => { + const leaderboard = async ( + leaderboardId, + page = 1, + priority = PRIORITY.FG_LOW, + options = {} + ) => + fetchHtml( + substituteVars(LEADERBOARD_URL, { leaderboardId, page }), + options, + priority + ).then((r) => { r.body = processLeaderboard(leaderboardId, page, r.body); return r; - }) + }); return { rankeds, @@ -501,5 +776,5 @@ export default (options = {}) => { countryRanking, leaderboard, ...queueToReturn, - } -} \ No newline at end of file + }; +};