const fetch = require("node-fetch");
const { AbortController } = require("abort-controller");
/**
* Converts date object into null if year, month, and day are missing
* @private
* @param { Object } obj
* @returns { Object | null }
*/
function convertPossibleDateNull(obj) {
if (obj.year === null && obj.month === null && obj.day === null) {
return null;
}
return obj;
}
/**
* Moves data up levels in the object for better use.
* @private
* @param { Object } obj - Required. The object to edit.
* @returns { Object } Returns the edited object.
*/
function edgeRemove(obj) {
let list = [];
for (let x = 0; x < obj.length; x++) {
if (obj[x].name) {
obj[x].name = obj[x].name.english || obj[x].name.full;
}
if (obj[x].node) {
list.push(obj[x].node);
} else if (obj[x].id && obj[x].length === 1) {
list.push(obj[x].id);
} else if (obj[x].url) {
list.push(obj[x].url);
} else {
list.push(obj[x]);
}
}
if (list.length < 1) {
list = null;
}
return list;
}
/**
* Converts a fuzzyDate into a Javascript Date
* @private
* @param { fuzzyDate } fuzzyDate - Date provided by AniList's API.
* @returns { Date } Returns a date object of the data provided.
*/
function convertFuzzyDate(fuzzyDate) {
if (Object.values(fuzzyDate).some((d) => d === null)) return null;
return new Date(fuzzyDate.year, fuzzyDate.month - 1, fuzzyDate.day);
}
/**
* Formats the media data to read better.
* @private
* @param { Object } media
*/
function formatMedia(media) {
media.reviews = media.reviews.nodes.length === 0 ? null : media.reviews.nodes;
media.externalLinks = edgeRemove(media.externalLinks);
media.characters = edgeRemove(media.characters.nodes);
media.staff = edgeRemove(media.staff.nodes);
if (media.airingSchedule) {
media.airingSchedule = media.airingSchedule.nodes;
}
if (media.studios) {
media.studios = media.studios.nodes;
}
media.recommendations = media.recommendations.nodes;
media.relations = media.relations.nodes;
media.trends = media.trends.nodes;
if (media.synonyms.length < 1) {
media.synonyms = null;
}
if (media.trailer) {
switch (media.trailer.site) {
case "youtube":
media.trailer = `https://www.youtube.com/watch?v=${media.trailer.id}`;
break;
case "dailymotion":
media.trailer = `https://www.dailymotion.com/video/${media.trailer.id}`;
break;
case undefined:
media.trailer = null;
break;
default:
break;
}
}
return media;
}
module.exports = {
/**
* Send a call to the AniList API with a query and variables.
* @param { String } query
* @param { Object } variables
* @returns { Object } Returns a customized object containing all of the data fetched.
*/
send: async function (query, variables) {
if (!query) {
throw new Error("Query is not given!");
}
if (query.startsWith("mutation") && this.key === null) {
throw new Error("Function requires authenciation but no authorization found.");
}
const controller = new AbortController();
const requestTimeout = setTimeout(() => {
controller.abort();
}, this.options.timeout);
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
signal: controller.signal
};
if (this.key) {
options.headers.Authorization = `Bearer ${this.key}`;
}
if (variables) {
options.body = JSON.stringify({ query: query, variables: variables });
} else {
options.body = JSON.stringify({ query: query });
}
const response = await fetch("https://graphql.anilist.co", options)
.catch((error) => {
if (error.name === "AbortError") {
throw new Error(`ERROR: Request timed out after ${this.options.timeout}ms, is AniList up?`);
}
})
.finally(() => {
clearTimeout(requestTimeout);
});
if (response.status !== 200) {
if (response.statusText) {
throw new Error(
`ERROR: AniList API returned with a ${response.status} error code. Message: ${response.statusText}`
);
}
throw new Error(`ERROR: AniList API returned with a ${response.status} error code.`);
}
const json = await response.json();
if (Object.keys(json).length < 0) {
throw new Error("ERROR: AniList API is down. Please refer to official channels for more information.");
}
if (json.errors) {
return json.errors;
}
if (json.data.Media) {
return formatMedia(json.data.Media);
}
if (json.data.Character) {
json.data.Character.media = json.data.Character.media.nodes;
json.data.Character.dateOfBirth = convertPossibleDateNull(json.data.Character.dateOfBirth);
return json.data.Character;
}
if (json.data.Staff) {
if (json.data.Staff.description.length < 1) {
json.data.Staff.description = null;
}
json.data.Staff.dateOfBirth = convertPossibleDateNull(json.data.Staff.dateOfBirth);
json.data.Staff.dateOfDeath = convertPossibleDateNull(json.data.Staff.dateOfDeath);
json.data.Staff.staffMedia = json.data.Staff.staffMedia.nodes;
json.data.Staff.characters = json.data.Staff.characters.nodes;
json.data.Staff.characterMedia = json.data.Staff.characterMedia.nodes;
return json.data.Staff;
}
if (json.data.Page) {
if (json.data.Page.activities) {
// For list of recent activities with getRecentActivity.
return json.data.Page.activities;
}
if (json.data.Recommendation) {
// For recommendation lists.
json.data.Recommendation.recommendations = json.data.Page.recommendations;
return json.data.Recommendation;
}
return json.data.Page; // For general searching
}
if (json.data.Studio) {
json.data.Studio.media = edgeRemove(json.data.Studio.media.nodes);
return json.data.Studio;
}
if (json.data.User || json.data.Viewer) {
let userObj = json.data.User || json.data.Viewer;
if (userObj.statistics) {
//Move all names up a level.
userObj.statistics.anime.staff.forEach((e) => {
e.staff.name = e.staff.name.english;
});
userObj.statistics.anime.voiceActors.forEach((e) => {
e.voiceActor.name = e.voiceActor.name.english;
});
userObj.statistics.manga.staff.forEach((e) => {
e.staff.name = e.staff.name.english;
});
}
if (userObj.statistics && !userObj.avatar) {
return userObj.statistics;
}
//Move all node objects up one level.
userObj.favourites.anime = userObj.favourites.anime.nodes;
userObj.favourites.manga = userObj.favourites.manga.nodes;
userObj.favourites.characters = edgeRemove(userObj.favourites.characters.nodes);
userObj.favourites.staff = edgeRemove(userObj.favourites.staff.nodes);
userObj.favourites.studios = userObj.favourites.studios.nodes;
return userObj;
}
if (json.data.MediaListCollection) {
json.data.MediaListCollection.lists.forEach((list) => {
list.entries.map((entry) => {
//Media does not need to be formatted in a list query.
entry.dates = {
startedAt: convertFuzzyDate(entry.startedAt),
completedAt: convertFuzzyDate(entry.completedAt),
updatedAt: new Date(entry.updatedAt * 1000),
createdAt: entry.createdAt === 0 ? null : new Date(entry.createdAt * 1000)
};
["startedAt", "completedAt", "updatedAt", "createdAt"].forEach((e) => delete entry[e]);
});
});
return json.data.MediaListCollection.lists;
}
if (json.data.SiteStatistics) {
for (const key in json.data.SiteStatistics) {
json.data.SiteStatistics[key] = json.data.SiteStatistics[key].nodes;
for (const entry in json.data.SiteStatistics[key]) {
// Date is given in epoch time. x1000 with UTC seconds for date
json.data.SiteStatistics[key][entry].date = new Date(
json.data.SiteStatistics[key][entry].date * 1000
);
}
}
}
return json.data; //If nothing matches, return collected data
}
};