fix scoresaber api url

This commit is contained in:
Lee 2023-10-17 23:32:20 +01:00
parent e6f510cec3
commit 47a23f0484

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