index.js

const activity = require("./activity"),
	lists = require("./lists"),
	media = require("./media"),
	people = require("./people"),
	recommendation = require("./recommendation"),
	searchEntry = require("./search"),
	Thread = require("./thread"),
	User = require("./user"),
	util = require("./utilities");

/**
 * The main class for AniList-Node
 * @since 1.0.0
 */
class AniList {
	/**
	 * @constructor
	 * @param {String} [accessKey] - The AniList API token. If no key is provided,
	 *      the user will not be able to access private information such as
	 *      the authorized user's profile (if set to private).
	 * @param {InitOptions} [options] - Optional options to use when getting info from AniList
	 */
	constructor(accessKey, options = {}) {
		if (!accessKey) {
			accessKey = null;
		}

		if (options.timeout) {
			if (typeof options.timeout !== "number") throw new TypeError("ERROR: 'options.timeout' should be a number");
		} else {
			options.timeout = 15000;
		}

		// Import utilities for the classes.
		this.__util = new util(accessKey, options);

		/**
		 * @augments User
		 * @see {@Link AniList.User}
		 * @since 1.0.0
		 */
		this.user = new User(this.__util);

		/**
		 * @augments lists
		 * @see {@Link AniList.Lists}
		 * @since 1.1.0
		 */
		this.lists = new lists(this.__util);

		/**
		 * @augments media
		 * @see {@Link AniList.Media}
		 * @since 1.0.0
		 */
		this.media = new media(this.__util);

		/**
		 * @augments people
		 * @see {@Link AniList.People}
		 * @since 1.0.0
		 */
		this.people = new people(this.__util);

		/**
		 * @augments Activity
		 * @see {@Link AniList.Activity}
		 * @since 1.7.0
		 */
		this.activity = new activity(this.__util);

		/**
		 * @augments Search
		 * @see {@Link AniList.Search}
		 * @since 1.7.0
		 */
		this.searchEntry = new searchEntry(this.__util);

		/**
		 * @augments Recommendation
		 * @see {@Link AniList.Recommendation}
		 * @since 1.8.0
		 */
		this.recommendation = new recommendation(this.__util);

		/**
		 * @augments Thread
		 * @see {@Link AniList.Thread}
		 * @since 1.11.0
		 */
		this.thread = new Thread(this.__util);
	}

	/**
	 * Grabs data on a studio
	 * @param {String | Number} studio - The studio ID or name on AniList.
	 * @return { StudioEntry }
	 * @since 1.0.0
	 */
	studio(studio) {
		const queryVars = this.__util.generateQueryHeaders("Studio", studio);

		return this.__util.send(
			queryVars[1] +
				`id name isAnimationStudio siteUrl isFavourite favourites 
            media { nodes { id title { romaji english native userPreferred } } } } }`,
			queryVars[0]
		);
	}

	/**
	 * [Requires Login] Favourite/Unfavourite a studio
	 * @param {Number} id - Required. The ID tied to the AniList entry.
	 * @returns {Boolean} Returns true if added, false otherwise.
	 * @since 1.12.0
	 */
	async favouriteStudio(id) {
		if (!id || typeof id !== "number") {
			throw new Error("AniList ID is not provided!");
		}

		const data = await this.util.send(
			`mutation ($studioID: Int) {
				ToggleFavourite(studioId: $studioID) {
					studios (page: 1, perPage: 25) {
					nodes { id }
			} } }`,
			{ studioID: id }
		);

		return data.ToggleFavourite.studios.nodes.some((e) => {
			if (e.id === id) {
				return true;
			}
		});
	}

	/**
	 * Searches AniList based on a specific term.
	 * @param {String} type - Required. Either anime, manga, character, staff, studio, or user.
	 * @param {String} term - Required. The term to lookup. (ie: "Honzuki no Gekokujou" or "AurelicButter")
	 * @param {Number} page - Which page of the results to look at. Will default to 1 if not provided.
	 * @param {Number} amount - The amount of results per page. AniList will cap this at 25 and function will default to 5 if not provided.
	 * @return { SearchEntry }
	 * @since 1.0.0
	 * @deprecated Please use {@link AniList.Search} class via `AniList.searchEntry` for updated searching. {@link AniList.Search} will replace
	 * this function in the next major update (v2.0.0).
	 */
	search(type, term, page = 1, amount = 5) {
		if (!type) {
			throw new Error("Type of search not defined!");
		} else if (!term) {
			throw new Error("Search term was not provided!");
		}

		//Validate all type conditions.
		if (typeof type !== "string") {
			throw new Error("Type is not a string.");
		}
		if (typeof term !== "string") {
			throw new Error("Term is not a string");
		}
		if (typeof page !== "number") {
			throw new Error("Page number is not a number");
		}
		if (typeof amount !== "number") {
			throw new Error("Amount is not a number");
		}

		const search = {
			anime: "media (type: ANIME, search: $search) { id title { romaji english native userPreferred } }",
			manga: "media (type: MANGA, search: $search) { id title { romaji english native userPreferred } }",
			char: "characters (search: $search) { id name { english: full } }",
			staff: "staff (search: $search) { id name { english: full } }",
			studio: "studios (search: $search) { id name }",
			user: "users (search: $search) { id name }"
		};

		let query = search[type.toLowerCase()];
		if (!query) {
			throw new Error("Type not supported.");
		}

		return this.__util.send(
			`query ($page: Int, $perPage: Int, $search: String) {
        Page (page: $page, perPage: $perPage) { pageInfo { total currentPage lastPage hasNextPage perPage } ${query} } }`,
			{ search: term, page: page, perPage: amount }
		);
	}

	/**
	 * Grabs all possible genres
	 * @return { String[] }
	 * @since 1.12.0
	 */
	genres() {
		return this.__util.send("query { GenreCollection }", null).then((data) => {
			return data.GenreCollection;
		});
	}

	/**
	 * Grabs all possible media tags
	 * @return { MediaTag[] }
	 * @since 1.12.0
	 */
	mediaTags() {
		return this.__util
			.send(
				`query { MediaTagCollection {
				id name description category isAdult
			} }`,
				null
			)
			.then((data) => {
				return data.MediaTagCollection;
			});
	}

	/**
	 * Grabs the site's statistics over the last seven days
	 * @return { AniListStats }
	 * @since 1.14.0
	 */
	siteStatistics() {
		return this.__util
			.send(
				`query { SiteStatistics {
			users (sort: DATE_DESC, perPage: 7) { nodes { date count change } }
			anime (sort: DATE_DESC, perPage: 7) { nodes { date count change } }
			manga (sort: DATE_DESC, perPage: 7) { nodes { date count change } }
			characters (sort: DATE_DESC, perPage: 7) { nodes { date count change } }
			staff (sort: DATE_DESC, perPage: 7) { nodes { date count change } }
			studios (sort: DATE_DESC, perPage: 7) { nodes { date count change } }
			reviews (sort: DATE_DESC, perPage: 7) { nodes { date count change } }
		} }`,
				null
			)
			.then((data) => {
				return data.SiteStatistics;
			});
	}
}

module.exports = AniList;