@irfanshadikrishad/anilist
Version:
Minimalist unofficial AniList CLI for Anime and Manga Enthusiasts
456 lines (455 loc) ⢠19.2 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();
// Filter to include only files, not directories, with the specified extension
const onlyFiles = files.filter((file) => {
const filePath = `${getDownloadFolderPath()}/${file}`;
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: 'select',
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, };