UNPKG

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
"use strict"; 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 ;