UNPKG

@irfanshadikrishad/anilist

Version:

Minimalist unofficial AniList CLI for Anime and Manga Enthusiasts

456 lines (455 loc) • 19.2 kB
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, };