@irfanshadikrishad/anilist
Version:
Minimalist unofficial AniList CLI for Anime and Manga Enthusiasts
457 lines (456 loc) ⢠19.3 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 __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import fs from "fs";
import { readdir, writeFile } from "fs/promises";
import inquirer from "inquirer";
import { createRequire } from "module";
import open from "open";
import { homedir } from "os";
import Papa from "papaparse";
import { join } from "path";
import process from "process";
import Spinner from "tiny-spinner";
import { Auth } from "./auth.js";
import { fetcher } from "./fetcher.js";
import { animeSearchQuery } from "./queries.js";
import { MALAnimeStatus, MALMangaStatus, } from "./types.js";
const aniListEndpoint = `https://graphql.anilist.co`;
const redirectUri = "https://anilist.co/api/v2/oauth/pin";
const spinner = new Spinner();
function getTitle(title) {
return (title === null || title === void 0 ? void 0 : title.english) || (title === null || title === void 0 ? void 0 : title.romaji) || "???";
}
function formatDateObject(dateObj) {
if (!dateObj)
return "null";
return ([dateObj.day, dateObj.month, dateObj.year].filter(Boolean).join("/") ||
"null");
}
function getNextSeasonAndYear() {
const currentMonth = new Date().getMonth() + 1;
const currentYear = new Date().getFullYear();
let nextSeason;
let nextYear;
// Determine the current season
if (currentMonth >= 12 || currentMonth <= 2) {
nextSeason = "SPRING";
nextYear = currentMonth === 12 ? currentYear + 1 : currentYear;
}
else if (currentMonth >= 3 && currentMonth <= 5) {
nextSeason = "SUMMER";
nextYear = currentYear;
}
else if (currentMonth >= 6 && currentMonth <= 8) {
nextSeason = "FALL";
nextYear = currentYear;
}
else if (currentMonth >= 9 && currentMonth <= 11) {
nextSeason = "WINTER";
nextYear = currentYear + 1;
}
return { nextSeason, nextYear };
}
function removeHtmlAndMarkdown(input) {
if (input) {
input = input.replace(/<\/?[^>]+(>|$)/g, "");
input = input.replace(/(^|\n)#{1,6}\s+(.+?)(\n|$)/g, "$2 ");
input = input.replace(/(\*\*|__)(.*?)\1/g, "$2");
input = input.replace(/(\*|_)(.*?)\1/g, "$2");
input = input.replace(/`(.+?)`/g, "$1");
input = input.replace(/\[(.*?)\]\(.*?\)/g, "$1");
input = input.replace(/!\[(.*?)\]\(.*?\)/g, "$1");
input = input.replace(/(^|\n)>\s+(.+?)(\n|$)/g, "$2 ");
input = input.replace(/(^|\n)-\s+(.+?)(\n|$)/g, "$2 ");
input = input.replace(/(^|\n)\d+\.\s+(.+?)(\n|$)/g, "$2 ");
input = input.replace(/(^|\n)\s*([-*_]){3,}\s*(\n|$)/g, "$1");
input = input.replace(/~~(.*?)~~/g, "$1");
input = input.replace(/\s+/g, " ").trim();
}
return input;
}
function getDownloadFolderPath() {
const homeDirectory = homedir();
// Determine the Downloads folder path based on the platform
if (process.platform === "win32") {
return join(homeDirectory, "Downloads");
}
else if (process.platform === "darwin" || process.platform === "linux") {
return join(homeDirectory, "Downloads");
}
return homeDirectory;
}
function getFormattedDate() {
const date = new Date();
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
// Format as DD-MM-YYYY-HH-MM
return `${day}-${month}-${year}-${hours}-${minutes}`;
}
/**
* Export JSON as JSON
* @param js0n
* @param dataType (eg: anime|manga)
*/
function saveJSONasJSON(js0n, dataType) {
return __awaiter(this, void 0, void 0, function* () {
try {
const jsonData = JSON.stringify(js0n, null, 2);
const path = yield saveToPath(dataType, ".json");
yield writeFile(path, jsonData, "utf8");
console.log(`\nSaved as JSON successfully.`);
open(getDownloadFolderPath());
}
catch (error) {
console.error("\nError saving JSON data:", error);
}
});
}
/**
* Export JSON as CSV
* @param js0n
* @param dataType (eg: anime|manga)
*/
function saveJSONasCSV(js0n, dataType) {
return __awaiter(this, void 0, void 0, function* () {
try {
const js0n_WTAS = js0n.map((_a) => {
var { title } = _a, rest = __rest(_a, ["title"]);
return (Object.assign(Object.assign({}, rest), { title: getTitle(title) }));
});
const csvData = Papa.unparse(js0n_WTAS);
const path = yield saveToPath(dataType, ".csv");
yield writeFile(path, csvData, "utf8");
console.log(`\nSaved as CSV successfully.`);
open(getDownloadFolderPath());
}
catch (error) {
console.error("\nError saving CSV data:", error);
}
});
}
function saveJSONasXML(js0n, data_type) {
return __awaiter(this, void 0, void 0, function* () {
try {
const xmlContent = data_type === 0 ? createAnimeListXML(js0n) : createMangaListXML(js0n);
const path = yield saveToPath(data_type === 0 ? "anime" : "manga", ".xml");
yield writeFile(path, yield xmlContent, "utf8");
console.log(`\nGenerated XML for MyAnimeList.`);
open(getDownloadFolderPath());
}
catch (error) {
console.error(`Error saving XML data:`, error);
}
});
}
function listFilesInDownloadFolder() {
return __awaiter(this, void 0, void 0, function* () {
const downloadFolderPath = getDownloadFolderPath();
const files = yield readdir(downloadFolderPath);
return files;
});
}
function selectFile(fileType) {
return __awaiter(this, void 0, void 0, function* () {
try {
const files = yield listFilesInDownloadFolder();
console.log(getDownloadFolderPath());
// Filter to include only files, not directories, with the specified extension
const onlyFiles = files.filter((file) => {
const filePath = `${getDownloadFolderPath()}/${file}`; // Adjust this to the correct path
const isFile = fs.lstatSync(filePath).isFile(); // Check if it's a file
return isFile && file.endsWith(fileType);
});
if (onlyFiles.length > 0) {
const answers = yield inquirer.prompt([
{
type: "list",
name: "fileName",
message: "Select a file to import:",
choices: onlyFiles,
},
]);
return answers.fileName;
}
else {
console.error(`\nNo importable ${fileType} file(s) found in download folder.`);
return null;
}
}
catch (error) {
console.error("\nError selecting file:", error);
return null;
}
});
}
function createAnimeXML(malId, progress, status, episodes, title, format) {
return `
<anime>
<series_animedb_id>${malId}</series_animedb_id>
<series_title><![CDATA[${title}]]></series_title>
<series_type>${format}</series_type>
<series_episodes>${episodes}</series_episodes>
<my_id>0</my_id>
<my_watched_episodes>${progress}</my_watched_episodes>
<my_start_date>0000-00-00</my_start_date>
<my_finish_date>0000-00-00</my_finish_date>
<my_score>0</my_score>
<my_storage_value>0.00</my_storage_value>
<my_status>${status}</my_status>
<my_comments><![CDATA[]]></my_comments>
<my_times_watched>0</my_times_watched>
<my_rewatch_value></my_rewatch_value>
<my_priority>LOW</my_priority>
<my_tags><![CDATA[]]></my_tags>
<my_rewatching>0</my_rewatching>
<my_rewatching_ep>0</my_rewatching_ep>
<my_discuss>0</my_discuss>
<my_sns>default</my_sns>
<update_on_import>1</update_on_import>
</anime>`;
}
function createMangaXML(malId, progress, status, chapters, title) {
return `
<manga>
<manga_mangadb_id>${malId}</manga_mangadb_id>
<manga_title><![CDATA[${title ? title : "unknown"}]]></manga_title>
<manga_volumes>0</manga_volumes>
<manga_chapters>${chapters ? chapters : 0}</manga_chapters>
<my_id>0</my_id>
<my_read_chapters>${progress}</my_read_chapters>
<my_start_date>0000-00-00</my_start_date>
<my_finish_date>0000-00-00</my_finish_date>
<my_score>0</my_score>
<my_status>${status}</my_status>
<my_reread_value></my_reread_value>
<my_priority>LOW</my_priority>
<my_rereading>0</my_rereading>
<my_discuss>0</my_discuss>
<update_on_import>1</update_on_import>
</manga>`;
}
function createAnimeListXML(mediaWithProgress) {
return __awaiter(this, void 0, void 0, function* () {
const statusMap = {
PLANNING: MALAnimeStatus.PLAN_TO_WATCH,
COMPLETED: MALAnimeStatus.COMPLETED,
CURRENT: MALAnimeStatus.WATCHING,
PAUSED: MALAnimeStatus.ON_HOLD,
DROPPED: MALAnimeStatus.DROPPED,
};
// Filter out anime without malId
const filteredMedia = mediaWithProgress.filter((anime) => anime.malId);
const xmlEntries = filteredMedia.map((anime) => {
console.log(anime);
const malId = anime.malId;
const progress = anime.progress;
const episodes = anime.episodes;
const title = getTitle(anime.title);
const status = statusMap[anime.status];
const format = anime.format ? anime.format : "";
return createAnimeXML(malId, progress, status, episodes, title, format);
});
return `<myanimelist>
<myinfo>
<user_id/>
<user_name>${yield Auth.MyUserName()}</user_name>
<user_export_type>1</user_export_type>
<user_total_anime>0</user_total_anime>
<user_total_watching>0</user_total_watching>
<user_total_completed>0</user_total_completed>
<user_total_onhold>0</user_total_onhold>
<user_total_dropped>0</user_total_dropped>
<user_total_plantowatch>0</user_total_plantowatch>
</myinfo>
\n${xmlEntries.join("\n")}\n
</myanimelist>`;
});
}
function createMangaListXML(mediaWithProgress) {
return __awaiter(this, void 0, void 0, function* () {
const statusMap = {
PLANNING: MALMangaStatus.PLAN_TO_READ,
COMPLETED: MALMangaStatus.COMPLETED,
CURRENT: MALMangaStatus.READING,
PAUSED: MALMangaStatus.ON_HOLD,
DROPPED: MALMangaStatus.DROPPED,
};
// Filter out manga without malId
const filteredMedia = mediaWithProgress.filter((manga) => manga.malId);
const xmlEntries = filteredMedia.map((manga) => {
const malId = manga.malId;
const progress = manga.progress;
const chapters = manga.chapters;
const title = getTitle(manga.title);
const status = statusMap[manga.status];
return createMangaXML(malId, progress, status, chapters, title);
});
return `<myanimelist>
<myinfo>
<user_id/>
<user_name>${yield Auth.MyUserName()}</user_name>
<user_export_type>2</user_export_type>
<user_total_manga>5</user_total_manga>
<user_total_reading>1</user_total_reading>
<user_total_completed>1</user_total_completed>
<user_total_onhold>1</user_total_onhold>
<user_total_dropped>1</user_total_dropped>
<user_total_plantoread>1</user_total_plantoread>
</myinfo>
\n${xmlEntries.join("\n")}\n
</myanimelist>`;
});
}
function getCurrentPackageVersion() {
const require = createRequire(import.meta.url);
const packageJson = require("../../package.json");
const version = packageJson.version;
return version || null;
}
function timestampToTimeAgo(timestamp) {
const now = Math.floor(Date.now() / 1000);
const elapsed = now - timestamp;
if (elapsed < 60) {
return `${elapsed} second${elapsed === 1 ? "" : "s"} ago`;
}
else if (elapsed < 3600) {
const minutes = Math.floor(elapsed / 60);
return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
}
else if (elapsed < 86400) {
const hours = Math.floor(elapsed / 3600);
return `${hours} hour${hours === 1 ? "" : "s"} ago`;
}
else if (elapsed < 2592000) {
const days = Math.floor(elapsed / 86400);
return `${days} day${days === 1 ? "" : "s"} ago`;
}
else if (elapsed < 31536000) {
const months = Math.floor(elapsed / 2592000);
return `${months} month${months === 1 ? "" : "s"} ago`;
}
else {
const years = Math.floor(elapsed / 31536000);
return `${years} year${years === 1 ? "" : "s"} ago`;
}
}
const anidbToanilistMapper = (romanjiName, year, englishName) => __awaiter(void 0, void 0, void 0, function* () {
const fetchAnime = (search) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
try {
const response = yield fetcher(animeSearchQuery, {
search,
perPage: 50,
});
return ((_a = response.data) === null || _a === void 0 ? void 0 : _a.Page.media) || [];
}
catch (error) {
console.error("Error fetching AniList data:", error);
return [];
}
});
// Search using romanjiName first
let results = yield fetchAnime(romanjiName);
// If no results, fallback to englishName
if (!results.length && englishName) {
results = yield fetchAnime(englishName);
}
// Match using year
for (const anime of results) {
if (anime.startDate.year === year) {
return anime.id;
}
}
return null;
});
/**
* Extract the save file path
* @param data_type - anime|manga
* @param file_format - save format (eg: .json|.csv)
* @returns string of file path
*/
function saveToPath(data_type, file_format) {
return __awaiter(this, void 0, void 0, function* () {
return join(getDownloadFolderPath(), `${yield Auth.MyUserName()}@irfanshadikrishad-anilist-${data_type}-${getFormattedDate()}.${file_format}`);
});
}
function simpleDateFormat(date) {
if (!date.day && !date.month && !date.year) {
return `null`;
}
return `${date === null || date === void 0 ? void 0 : date.day}/${date === null || date === void 0 ? void 0 : date.month}/${date === null || date === void 0 ? void 0 : date.year}`;
}
function handleRateLimitRetry(retryCount) {
return __awaiter(this, void 0, void 0, function* () {
let seconds = Math.pow(2, retryCount) * 1000;
const maxWait = 60 * 1000;
seconds = Math.min(seconds, maxWait);
spinner.start(`Rate limit reached. Retrying in ${seconds / 1000} sec...`);
let remainingTime = seconds / 1000;
const interval = setInterval(() => {
remainingTime--;
spinner.update(`Rate limit reached. Retrying in ${remainingTime} sec...`);
if (remainingTime <= 0)
clearInterval(interval);
}, 1000);
yield new Promise((resolve) => setTimeout(resolve, seconds));
clearInterval(interval);
spinner.stop();
});
}
function formatDate(timestamp) {
return timestamp ? new Date(timestamp * 1000).toUTCString() : "N/A";
}
function logUserDetails(user, followersCount, followingCount) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
console.log("\nš User Information:");
console.table({
"ID": user.id,
"Name": user.name,
"Site URL": user.siteUrl,
"Donator Tier": user.donatorTier,
"Donator Badge": user.donatorBadge,
"Account Created": formatDate(user.createdAt),
"Account Updated": formatDate(user.updatedAt),
"Blocked": user.isBlocked,
"isFollower": user.isFollower,
"isFollowing": user.isFollowing,
"Profile Color": ((_a = user.options) === null || _a === void 0 ? void 0 : _a.profileColor) || "N/A",
"Timezone": ((_b = user.options) === null || _b === void 0 ? void 0 : _b.timezone) || "N/A",
"Followers": followersCount,
"Following": followingCount,
});
console.log("\nš Anime Statistics:");
console.table({
"Count": ((_d = (_c = user.statistics) === null || _c === void 0 ? void 0 : _c.anime) === null || _d === void 0 ? void 0 : _d.count) || 0,
"Episodes Watched": ((_f = (_e = user.statistics) === null || _e === void 0 ? void 0 : _e.anime) === null || _f === void 0 ? void 0 : _f.episodesWatched) || 0,
"Minutes Watched": ((_h = (_g = user.statistics) === null || _g === void 0 ? void 0 : _g.anime) === null || _h === void 0 ? void 0 : _h.minutesWatched) || 0,
});
console.log("\nš Manga Statistics:");
console.table({
"Count": ((_k = (_j = user.statistics) === null || _j === void 0 ? void 0 : _j.manga) === null || _k === void 0 ? void 0 : _k.count) || 0,
"Chapters Read": ((_m = (_l = user.statistics) === null || _l === void 0 ? void 0 : _l.manga) === null || _m === void 0 ? void 0 : _m.chaptersRead) || 0,
"Volumes Read": ((_p = (_o = user.statistics) === null || _o === void 0 ? void 0 : _o.manga) === null || _p === void 0 ? void 0 : _p.volumesRead) || 0,
});
}
export { anidbToanilistMapper, aniListEndpoint, createAnimeListXML, createAnimeXML, createMangaListXML, createMangaXML, formatDateObject, getCurrentPackageVersion, getDownloadFolderPath, getFormattedDate, getNextSeasonAndYear, getTitle, handleRateLimitRetry, logUserDetails, redirectUri, removeHtmlAndMarkdown, saveJSONasCSV, saveJSONasJSON, saveJSONasXML, saveToPath, selectFile, simpleDateFormat, timestampToTimeAgo, };