diff --git a/docs/EN.md b/docs/EN.md index ce57197..dc342ef 100644 --- a/docs/EN.md +++ b/docs/EN.md @@ -162,6 +162,10 @@ Get skins from a specific tab of the site nameMc.getSkins({ tab: "new", page: 2 }) .then((skins) => console.log(skins)) .catch((error) => console.log(error)); + +nameMc.getSkins({ tab: "tag" }) + .then((skins) => console.log(skins)) + .catch((error) => console.log(error)); ``` diff --git a/docs/RU.md b/docs/RU.md index ba1997a..660f406 100644 --- a/docs/RU.md +++ b/docs/RU.md @@ -161,6 +161,10 @@ nameMc.skinHistory({ nickname: "MrZillaGold", page: 2 }) nameMc.getSkins({ tab: "new", page: 2 }) .then((skins) => console.log(skins)) .catch((error) => console.log(error)); + +nameMc.getSkins({ tab: "tag" }) + .then((skins) => console.log(skins)) + .catch((error) => console.log(error)); ``` diff --git a/package-lock.json b/package-lock.json index e2ff840..2c36842 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "namemcwrapper", - "version": "1.7.2", + "version": "1.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "namemcwrapper", - "version": "1.7.2", + "version": "1.8.0", "license": "MIT", "dependencies": { "axios": "^0.21.1", @@ -33,6 +33,7 @@ "integrity": "sha512-lYSBC7B4B9hJ7sv0Ojx1BrGhuzCoOIYfLjd+Xpd4rOzdS+a47yi8voV8vFkfjlZR1N5qZO7ixOCbobUdT304PQ==", "dev": true, "dependencies": { + "chokidar": "^3.4.0", "commander": "^4.0.1", "convert-source-map": "^1.1.0", "fs-readdir-recursive": "^1.1.0", @@ -47,11 +48,7 @@ "babel-external-helpers": "bin/babel-external-helpers.js" }, "optionalDependencies": { - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents", - "chokidar": "^3.4.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents" } }, "node_modules/@babel/code-frame": { @@ -94,10 +91,6 @@ }, "engines": { "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" } }, "node_modules/@babel/core/node_modules/semver": { @@ -149,9 +142,6 @@ "@babel/helper-validator-option": "^7.12.17", "browserslist": "^4.14.5", "semver": "^6.3.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { @@ -500,9 +490,6 @@ "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", "@babel/plugin-proposal-optional-chaining": "^7.13.12" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" } }, "node_modules/@babel/plugin-proposal-async-generator-functions": { @@ -618,9 +605,6 @@ "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-private-methods": { @@ -743,9 +727,6 @@ "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-top-level-await": { @@ -1163,9 +1144,6 @@ "babel-plugin-polyfill-regenerator": "^0.1.2", "core-js-compat": "^3.9.0", "semver": "^6.3.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/preset-env/node_modules/semver": { @@ -1555,19 +1533,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^4.0.0", - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { @@ -1581,10 +1546,6 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { @@ -1594,10 +1555,6 @@ "dev": true, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { @@ -1611,10 +1568,6 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { @@ -1647,13 +1600,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" } }, "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/scope-manager": { @@ -1667,10 +1613,6 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": { @@ -1680,10 +1622,6 @@ "dev": true, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": { @@ -1702,15 +1640,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": { @@ -1724,10 +1653,6 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/experimental-utils/node_modules/semver": { @@ -1758,18 +1683,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { @@ -1783,10 +1696,6 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { @@ -1796,10 +1705,6 @@ "dev": true, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { @@ -1818,15 +1723,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { @@ -1840,10 +1736,6 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/parser/node_modules/semver": { @@ -2342,7 +2234,13 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", "integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, "node_modules/chokidar/node_modules/readdirp": { "version": "3.5.0", @@ -4309,10 +4207,6 @@ }, "engines": { "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" } }, "node_modules/mocha/node_modules/debug": { diff --git a/package.json b/package.json index 31d78e3..d5e7247 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "namemcwrapper", - "version": "1.7.2", + "version": "1.8.0", "description": "ES6 Promise based wrapper for NameMC.com", "main": "./dist/NameMC.js", "exports": { diff --git a/src/DataParser.ts b/src/DataParser.ts index 8987166..702a758 100644 --- a/src/DataParser.ts +++ b/src/DataParser.ts @@ -2,9 +2,9 @@ import cheerio from "cheerio"; import { WrapperError } from "./WrapperError"; -import { profileSkinsRegExp, escapeColorsClasses, escapeHtml } from "./utils"; +import { profileSkinsRegExp, escapeColorsClasses, escapeHtml, skinRegExp } from "./utils"; -import { ISkin, ICape, ICapeResponse, IRender, IGetEndpointOptions, IGetRendersOptions, ISkinResponse, ICapeInfo, IServerPreview, IServer, Hash, BasePlayerInfo, IExtendedSkin, Model } from "./interfaces"; +import { ISkin, IExtendedSkin, INamedSkin, ICape, ICapeResponse, IRender, IGetEndpointOptions, IGetRendersOptions, ISkinResponse, ICapeInfo, IServerPreview, IServer, Hash, BasePlayerInfo, Model } from "./interfaces"; import TagElement = cheerio.TagElement; import Root = cheerio.Root; @@ -14,25 +14,31 @@ export abstract class DataParser { abstract getRenders(options: IGetRendersOptions): IRender; abstract getCapeInfo(hash: Hash): ICapeInfo; - protected parseSkins(data: string): ISkin[] { + protected parseSkins(data: string): (ISkin | INamedSkin)[] { const $ = cheerio.load(data); const skins = $("div.card-body.position-relative.text-center.checkered.p-1") .map((index, card) => { + const cardLinkHash = (card.parent as TagElement).attribs.href.replace(skinRegExp, "$1"); + const cardHeader = ((card.parent as TagElement).children as TagElement[]).filter(({ name, attribs: { class: className = "" } = {} }) => name === "div" && className.includes("card-header"))[0]; + const cardName = cardHeader ? cardHeader.children[0]?.data : null; + const $ = cheerio.load(card); const [skin] = $("div > img.drop-shadow") // @ts-ignore .map((index, { attribs: { src } }) => { const isValidSkin = this.checkSkinLink(src); - if (isValidSkin) { - return isValidSkin; - } + return { + ...isValidSkin, + hash: cardLinkHash + }; }) .get(); return { ...skin, + name: cardName, rating: this.parseSkinRating($) }; }) @@ -326,12 +332,14 @@ export abstract class DataParser { switch(type) { case "skin": { - const model = (response as ISkinResponse).model; + const name = (response as ISkinResponse).name || null; + const model = (response as ISkinResponse).model || "unknown"; const rating = (response as ISkinResponse).rating ?? 0; return { url, hash, + name, model, rating, renders: this.getRenders({ @@ -367,10 +375,19 @@ export abstract class DataParser { } private parseSkinRating = ($: Root): number => { - const { children: [{ data: rating }] } = $(".position-absolute.bottom-0.right-0.text-muted") - .get(0); + const ratingElement = $(".position-absolute.bottom-0.right-0.text-muted") + .get(0) + .children; + + const rating = ( + ratingElement.length > 1 ? + ratingElement[1] + : + ratingElement[0] + ) + .data; - return Number(rating.slice(1)); + return Number(rating.replace(/(?:[^\d]+)([\d]+)/, "$1")); }; private parseSkinTime = ($: Root): number => { diff --git a/src/NameMC.ts b/src/NameMC.ts index ce2d719..0dc3e87 100644 --- a/src/NameMC.ts +++ b/src/NameMC.ts @@ -6,7 +6,7 @@ import { WrapperError } from "./WrapperError"; import { nameRegExp, profileRegExp, skinRegExp, capes, getUUID } from "./utils"; -import { IRender, IOptions, ISkin, IExtendedSkin, ICape, ICapeInfo, Transformation, ITransformSkinOptions, ICheckServerLikeOptions, IFriend, IGetSkinsOptions, IServerPreview, IGetEndpointOptions, IPlayer, IGetSkinHistoryOptions, IGetRendersOptions, IServer, Tab, Section, Nickname, CapeHash, BasePlayerInfo } from "./interfaces"; +import { IRender, IOptions, ISkin, IExtendedSkin, ICape, ICapeInfo, Transformation, ITransformSkinOptions, ICheckServerLikeOptions, IFriend, IGetSkinsOptions, IServerPreview, IGetEndpointOptions, IPlayer, IGetSkinHistoryOptions, IGetRendersOptions, IServer, Tab, Section, Nickname, CapeHash, BasePlayerInfo, INamedSkin } from "./interfaces"; export class NameMC extends DataParser { @@ -243,7 +243,9 @@ export class NameMC extends DataParser { /** * Get skins from a specific tab of the site */ - getSkins({ tab = "trending", page = 1, section = "weekly" }: IGetSkinsOptions = {}): Promise { + getSkins(options: IGetSkinsOptions & ({ tab: "trending" | "tag" | "new" })): Promise + getSkins(options: IGetSkinsOptions & ({ tab: "trending"; section: "top"; } | { tab: "random" })): Promise + getSkins({ tab = "trending", page = 1, section = "weekly" }: IGetSkinsOptions = {}): Promise { const tabs: Tab[] = ["trending", "new", "random", "tag"]; const sections: Section[] = ["daily", "weekly", "monthly", "top"]; diff --git a/src/interfaces/NameMC/response.ts b/src/interfaces/NameMC/response.ts index c1b2946..60221e4 100644 --- a/src/interfaces/NameMC/response.ts +++ b/src/interfaces/NameMC/response.ts @@ -5,9 +5,10 @@ export interface IResponse { } export interface ISkinResponse extends IResponse { + type: "skin"; model: Model; + name?: string; rating?: number; - type: "skin"; } export interface ICapeResponse extends IResponse { diff --git a/src/interfaces/NameMC/skin.ts b/src/interfaces/NameMC/skin.ts index 37d1fdf..b864105 100644 --- a/src/interfaces/NameMC/skin.ts +++ b/src/interfaces/NameMC/skin.ts @@ -1,7 +1,7 @@ import { IRender } from "./render"; import { Nickname } from "./nickname"; -export type Model = "classic" | "slim"; +export type Model = "classic" | "slim" | "unknown"; export type Hash = string; export interface ISkin { @@ -11,6 +11,11 @@ export interface ISkin { hash: Hash; model: Model; rating: number; + name: string | null; +} + +export interface INamedSkin extends ISkin { + name: string; } export interface IExtendedSkin extends ISkin { diff --git a/src/utils.ts b/src/utils.ts index 71ac689..a2d50bc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,7 +9,7 @@ import Element = cheerio.Element; export const nameRegExp = /^(?:(?[A-Za-z0-9_]{1,16})|(?[0-9a-f]{8}-?[0-9a-f]{4}-?[0-5][0-9a-f]{3}-?[089ab][0-9a-f]{3}-?[0-9a-f]{12}))$/; export const profileRegExp = /[^]+\/profile\/[^]+/; export const profileSkinsRegExp = /\/minecraft-skins\/profile\/([^]+)/; -export const skinRegExp = /[^]+\/skin\/([^]+)/; +export const skinRegExp = /(?:[^]+)?\/skin\/([^]+)/; export const capes: CapesMap = new Map([ ["1981aad373fa9754", "MineCon 2016"], diff --git a/test/tests.mjs b/test/tests.mjs index 53865ac..c7496a1 100644 --- a/test/tests.mjs +++ b/test/tests.mjs @@ -44,19 +44,35 @@ describe("Skins", () => { describe("getSkins();", () => { it("Get skins and check array size", async () => { const skins = await nameMc.getSkins(); - + + assert.strictEqual(skins.length, 30); + }); + + it("Get skins tags and check array size", async () => { + const skins = await nameMc.getSkins({ + tab: "tag" + }); + assert.strictEqual(skins.length, 30); }); - + it("Get skins with custom tag and check array size", async () => { const skins = await nameMc.getSkins({ tab: "tag", section: "girl" }); - + assert.strictEqual(skins.length, 30); }); }); + + describe("getSkins();", () => { + it("Get skins and check tags array size", async () => { + const skin = await nameMc.getSkin("a4eaf5f46753cf75"); + + assert.ok(skin.tags.length > 1); + }); + }); }); describe("Capes", () => {