ittf-pingpong
Version:
Unofficial API to retrieve player rankings and statistics from ITTF (International Table Tennis Federation) affiliated members and events.
281 lines (280 loc) • 16.5 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ittfPingPong = void 0;
const jsdom_1 = __importDefault(require("jsdom"));
const getWeek_1 = __importDefault(require("./getWeek.cjs"));
class ittfPingPong {
constructor() {
this.currentRankingsUrl = 'https://www.ittf.com/wp-content/uploads';
this.historicalRankingsUrl = 'https://ranking.ittf.com/public/s/ranking/list?category=SEN&typeGender=M;SINGLES&year=2020&week=49&offset=0&size=10000';
this.allPlayersUrl = 'https://wttcmsapigateway-new.azure-api.net/ttu/Players/GetPlayers?limit=100000';
this.allCountriesUrl = 'https://ranking.ittf.com/public/s/countries/list';
this.playerProfileUrl = 'https://ranking.ittf.com/public/s/player/profile/';
this.playerMatchesUrl = 'https://ranking.ittf.com/public/s/player/matches/ittfId?offset=0&size=valueSize&ind=valueInd&dbl=valueDouble';
}
static isValidGender(value) {
//Ensures that only 'M', 'W', 'X' is passed as an input
return this.currentGender.includes(value);
}
static isValidCategory(value) {
//Ensures that only 'S', 'D', 'DI' is passed as an input
return this.currentCategory.includes(value);
}
static isValidType(value) {
//Ensures that only 'YOU' or 'SEN' is passed as an input
return this.currentType.includes(value);
}
isPositiveInteger(n) {
//Ensures that only positive integers are passed as an input
return Number.isInteger(n) && n > 0;
}
isAlphabetic(str) {
//Ensures that only Latin Alphabet characters (upper and lower case) and spaces are passed as an input
return /^[A-Za-z\s]+$/.test(str);
}
/**
* Fetch the current rankings.
* @param {string} type - The type of rankings ('YOU' | 'SEN') All 3 youth competition types lumped together (U15, U18, U21), Seniors
* @param {string} gender - Gender ('M' | 'W' | 'X') Man, woman, mixed (doubles only)
* @param {string} category - Category ('S' | 'D' | 'DI') Singles, doubles ranking (pairs), doubles ranking (individual)
* @param {number | string} topN - Only positive integers (e.g., 1, 2, 3, ...) or 'all'. Defaults to 'all'.
*/
currentRankings(type_1, gender_1, category_1) {
return __awaiter(this, arguments, void 0, function* (type, gender, category, topN = 'all') {
try {
// Validate type
if (!ittfPingPong.isValidType(type)) {
throw new Error(`Invalid type: ${type}. Valid options are: ${ittfPingPong.currentType.join(', ')}`);
}
// Validate gender
if (!ittfPingPong.isValidGender(gender)) {
throw new Error(`Invalid gender: ${gender}. Valid options are: ${ittfPingPong.currentGender.join(', ')}`);
}
// Validate category
if (!ittfPingPong.isValidCategory(category)) {
throw new Error(`Invalid category: ${category}. Valid options are: ${ittfPingPong.currentCategory.join(', ')}`);
}
// Validate topN
if (topN !== 'all') {
if (!this.isPositiveInteger(topN)) {
throw new Error('topN must be a positive integer or "all"');
}
}
// Check for invalid gender and category combination
if (gender === 'X' && !['D', 'DI'].includes(category)) {
throw new Error("Mixed gender ('X') requires a doubles category ('D' or 'DI').");
}
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = (currentDate.getMonth() + 1);
const monthStr = (String(month).length == 1 ? '0' + month : String(month));
const week = (0, getWeek_1.default)();
let endpointString = `/${year}/${monthStr}/${year}_${week}_${type}_${gender}${category}.html`;
let response = yield fetch(this.currentRankingsUrl + endpointString);
let htmlText = yield response.text();
if (htmlText.includes("Not Found")) {
// Not found, subtract 1 from month
const lastMonth = month - 1;
const lastMonthStr = (String(lastMonth).length == 1 ? '0' + lastMonth : String(lastMonth));
endpointString = `/${year}/${lastMonthStr}/${year}_${week}_${type}_${gender}${category}.html`;
response = yield fetch(this.currentRankingsUrl + endpointString);
htmlText = yield response.text();
if (htmlText.includes("Not Found")) {
// Still not found, subtract 1 from week
const lastWeek = week - 1;
endpointString = `/${year}/${monthStr}/${year}_${lastWeek}_${type}_${gender}${category}.html`;
response = yield fetch(this.currentRankingsUrl + endpointString);
htmlText = yield response.text();
if (htmlText.includes("Not Found")) {
// Still not found, subtract 1 from week and 1 from month
endpointString = `/${year}/${lastMonthStr}/${year}_${lastWeek}_${type}_${gender}${category}.html`;
response = yield fetch(this.currentRankingsUrl + endpointString);
htmlText = yield response.text();
}
}
}
const dom = new jsdom_1.default.JSDOM(htmlText);
const table = dom.window.document.getElementsByTagName("tbody");
const rows = Array.from(table[0].getElementsByClassName("rank"));
const ranks = rows.map(td => {
var _a;
return { rank: td.textContent.trim(),
name: td.parentElement.nextElementSibling.textContent.trim(),
assoc: (_a = td.parentElement.parentElement) === null || _a === void 0 ? void 0 : _a.getElementsByClassName("rcellc assoc")[0].textContent.trim(),
points: td.parentElement.nextElementSibling.nextElementSibling.nextElementSibling.textContent.trim() };
});
// Determine the maximum number of results
const maxResults = ranks.length;
// Set the limit
const takeCount = topN === 'all' ? maxResults : Math.min(topN, maxResults);
// Slice the results to top n
const topRanks = ranks.slice(0, takeCount);
return topRanks;
}
catch (err) {
throw err;
}
});
}
;
/**
* Fetch the player's ittfId given their name.
* @param playerFullName - Player's family name and given name. Ensure that the family name is capitalized and placed before the given name. (e.g. "FAN Zhendong", "CALDERANO Hugo")
* @param playerGivenName - Player's given name(s) only.
* @param playerFamilyName - Player's family name.
* @param {Object} searchName - The name parameters, all mutually exclusive. I.E., you can only enter playerFullName, playerGivenName, or playerFamilyName.
* @param {string} [searchName.playerFullName] - The full name, mutually exclusive with givenName and familyName.
* @param {string} [searchName.playerGivenName] - Given name, mutually exclusive with fullName.
* @param {string} [searchName.playerFamilyName] - Family name, mutually exclusive with fullName.
*
* @throws {Error} When multiple search methods are used together.
*
* @example
* // Valid:
* playerIttfId({ playerFullName: 'FAN Zhendong' });
* // output: 121404
* playerIttfId({ playerFamilyName: 'Lebrun'});
* // output: [{IttfId:"132992", PlayerGivenName:"Alexis",...},{IttfId:, PlayerGivenName:"Felix",...} ,...]
*
*
* // Invalid:
* playerIttfId({ fullName: 'FAN Zhendong', givenName: 'Zhendong' });
* Input the full name with the family name in all caps first, then the given name with the first letter capitalized. (e.g. "FAN Zhendong", "LEBRUN Alexis")
* If playerFullName is inputted and found, returns ittfId as a string; the same applies for GivenName and FamilyNambe, but if multiple players share the GivenName or FamilyName searched for, an array is returned instead.
*/
playerIttfId(searchName) {
return __awaiter(this, void 0, void 0, function* () {
try {
const hasFullName = !!searchName.playerFullName;
const hasGivenName = !!searchName.playerGivenName;
const hasFamilyName = !!searchName.playerFamilyName;
const count = [hasFullName, hasGivenName, hasFamilyName].filter(Boolean).length;
//Ensures that only a single search parameter is used at a time
if (count > 1) {
throw new Error("Only one of playerFullName, playerGivenName, or playerFamilyName should be provided");
}
//Ensures that only Latin alphabet characters and spaces are input for name searches
if (!this.isAlphabetic(Object.values(searchName))) {
throw new Error("Input cannot contain any numbers or special characters, only roman alphabet characters A-Z/a-z.");
}
let response = yield fetch(this.allPlayersUrl);
let output = yield response.json();
//No players will have the exact same Full Name (Double check this is not allowed e.g. John Smith?)
if (hasFullName) {
try {
let searchResult = output.Result.filter((entries) => entries.PlayerFamilyNameFirst === searchName.playerFullName)[0];
return Number(searchResult.IttfId);
}
catch (err) {
console.log(err.message);
throw new Error(`Cannot find player's full name in the database. Ensure that family name comes first, followed by given name (e.g. "ARUNA Quadri", and not "QUADRI Aruna")`);
}
}
else if (hasGivenName) {
try {
let searchResult = output.Result.filter((entries) => { var _a; return entries.PlayerGivenName === ((_a = searchName.playerGivenName) === null || _a === void 0 ? void 0 : _a.charAt(0).toUpperCase()) + searchName.playerGivenName.slice(1).toLowerCase(); });
if (searchResult.length == 1) {
return Number(searchResult[0].IttfId);
}
else {
return searchResult;
}
}
catch (err) {
console.log(err.message);
throw new Error(`Cannot find given name in the database. Ensure that the spelling is correct and that there is a ranked ITTF player with this given name (e.g. "Hugo", for Hugo Calderano, Hugo Hanashiro, etc.)`);
}
}
else {
try {
let searchResult = output.Result.filter((entries) => { var _a; return entries.PlayerFamilyName === ((_a = searchName.playerFamilyName) === null || _a === void 0 ? void 0 : _a.toUpperCase()); });
if (searchResult.length == 1) {
return Number(searchResult[0].IttfId);
}
else {
return searchResult;
}
}
catch (err) {
console.log(err.message);
throw new Error(`Cannot find family name in the database. Ensure that the spelling is correct and that there is a ranked ITTF player with this family name (e.g. "Lebrun", for Alexis Lebrun, Félix Lebrun, etc.)`);
}
}
}
catch (err) {
throw err;
}
});
}
/**
* Fetch the player's yearly match totals.
* @param playerFullName - Player's family name and given name. Ensure that the family name is capitalized and placed before the given name. (e.g. "FAN Zhendong", "CALDERANO Hugo")
* @param playerIttfId - 0-6 digit integer. (e.g. 121404 - FAN Zhendong)
*
* @throws {Error} When multiple search methods are used together. Only input the playerFullName or the playerIttfId - not both.
*
* @example
* // Valid:
* playerProfile({ playerFullName: 'FAN Zhendong' });
* // output: {player: {IttfId:"121404", Org:"CHN", Gender:"M",...}, ranking: {LastPos:[{...}], BestPos:[{...},{...},...]}, stats: {total:{[...]}, indiv:{[...]}, doubles:{[...]}}}
* playerProfile({ playerIttfId: 121404});
* // output: {player: {IttfId:"121404", Org:"CHN", Gender:"M",...}, ranking: {LastPos:[{...}], BestPos:[{...},{...},...]}, stats: {total:{[...]}, indiv:{[...]}, doubles:{[...]}}}
*
*/
playerProfile(searchMethod) {
return __awaiter(this, void 0, void 0, function* () {
try {
const hasFullName = !!searchMethod.playerFullName;
const hasIttfId = !!searchMethod.playerIttfId;
const count = [hasFullName, hasIttfId].filter(Boolean).length;
let profileUrl = this.playerProfileUrl;
//Ensures that only a single search parameter is used at a time
if (count > 1) {
throw new Error("Only one of playerFullName or IttfId should be provided");
}
//Ensures that only Latin alphabet characters and spaces are input for name searches
if (hasFullName) {
if (!this.isAlphabetic(searchMethod.playerFullName)) {
throw new Error("Name cannot contain any numbers or special characters, only roman alphabet characters A-Z/a-z.");
}
const playerId = yield this.playerIttfId({ playerFullName: searchMethod.playerFullName });
profileUrl += playerId;
}
//Ensures that only positive integers are input for ID searches
if (hasIttfId) {
if (!this.isPositiveInteger(searchMethod.playerIttfId)) {
throw new Error('Invalid ittfID! Format must be 0-6 digit integer.');
}
profileUrl += searchMethod.playerIttfId;
}
let response = yield fetch(profileUrl);
let output = yield response.json();
// console.log(output.Result[0].Age);
// console.log(filteredOutput.length);
// let player = this.playerProfileUrl +
console.log(output);
return output;
}
catch (err) {
throw err;
}
});
}
}
exports.ittfPingPong = ittfPingPong;
ittfPingPong.currentGender = ['M', 'W', 'X']; // Man, woman, mixed (doubles only)
ittfPingPong.currentCategory = ['S', 'D', 'DI']; // Singles, doubles ranking (pairs), doubles ranking (individual)
ittfPingPong.currentType = ['YOU', 'SEN']; // All 3 youth competition types lumped together, Seniors
;