koneko-cli
Version:
Your CLI for reading manga from the terminal
325 lines (324 loc) ⢠15.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadEnabledPluginServers = loadEnabledPluginServers;
const inquirer_1 = __importDefault(require("inquirer"));
const ora_1 = __importDefault(require("ora"));
const open_1 = __importDefault(require("open"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = __importDefault(require("path"));
const SettingsConfig_1 = __importDefault(require("../SettingsConfig"));
const DebugLogger_1 = require("./DebugLogger");
const Loader_1 = __importDefault(require("../../modules/mangakakalot/Loader"));
const child_process_1 = require("child_process");
async function loadEnabledPluginServers() {
const pluginFolder = path_1.default.join(process.env.HOME || process.env.USERPROFILE || '.', '.config', 'koneko', 'plugins');
DebugLogger_1.logger.debug('š Loading plugins from:', pluginFolder);
if (!(await fs_extra_1.default.pathExists(pluginFolder))) {
DebugLogger_1.logger.debug('ā ļø Plugin folder does not exist.');
return {};
}
const pluginDirs = await fs_extra_1.default.readdir(pluginFolder);
DebugLogger_1.logger.debug('š Plugin directories found:', pluginDirs);
const servers = {};
for (const pluginName of pluginDirs) {
try {
const pluginMainPath = path_1.default.join(pluginFolder, pluginName, 'index.js');
const packageJsonPath = path_1.default.join(pluginFolder, pluginName, 'package.json');
if (!(await fs_extra_1.default.pathExists(pluginMainPath))) {
DebugLogger_1.logger.debug(`ā ļø Skipping plugin "${pluginName}": index.js not found.`);
continue;
}
if (await fs_extra_1.default.pathExists(packageJsonPath)) {
DebugLogger_1.logger.debug(`š Found package.json for plugin "${pluginName}". Checking for main entry...`);
DebugLogger_1.logger.debug(`š Loading package.json for plugin "${pluginName}"`);
try {
(0, child_process_1.execSync)(`npm install`, { cwd: path_1.default.join(pluginFolder, pluginName), stdio: 'ignore' });
DebugLogger_1.logger.debug(`ā
Successfully installed dependencies for plugin "${pluginName}".`);
}
catch (error) {
DebugLogger_1.logger.debug(`ā Failed to install dependencies for plugin "${pluginName}":`, error);
continue;
}
}
DebugLogger_1.logger.debug(`š Importing plugin: ${pluginName}`);
const pluginModule = await Promise.resolve(`${pluginMainPath}`).then(s => __importStar(require(s)));
if (pluginModule) {
servers[pluginName] = {
name: pluginName,
searchManga: (query) => {
var _a, _b, _c;
return pluginModule.search
? pluginModule.search(query, (_c = (_b = (_a = SettingsConfig_1.default.data) === null || _a === void 0 ? void 0 : _a.Plugins) === null || _b === void 0 ? void 0 : _b[pluginName]) !== null && _c !== void 0 ? _c : {})
: Promise.resolve([]);
},
getMangaDetail: (id) => pluginModule.getMangaDetail
? pluginModule.getMangaDetail(id)
: Promise.resolve(null),
getChapterList: (id) => pluginModule.getChapters
? pluginModule.getChapters(id)
: Promise.resolve([]),
getImagesAndCreatePDF: (url, mangaTitle, chapterTitle) => pluginModule.getImagesAndCreatePDF
? pluginModule.getImagesAndCreatePDF(url, mangaTitle, chapterTitle)
: Promise.reject(new Error("Not implemented")),
};
DebugLogger_1.logger.debug(`ā
Plugin "${pluginName}" loaded.`);
}
else {
DebugLogger_1.logger.debug(`ā ļø Plugin "${pluginName}" did not export anything.`);
}
}
catch (error) {
DebugLogger_1.logger.debug(`ā Failed to load plugin "${pluginName}":`, error);
}
}
return servers;
}
class MangaEventHandler {
constructor() {
this.lastDownloadedFolder = null;
this.currentServer = null;
this.allServers = {
MangaKonekuto: Loader_1.default,
};
}
parseChapterNumber(title) {
const match = title.match(/chapter\s*(\d+(\.\d+)?)/i);
if (match && match[1])
return parseFloat(match[1]);
return Number.MAX_SAFE_INTEGER;
}
async cleanupPreviousDownload() {
var _a;
const shouldDelete = (_a = (await SettingsConfig_1.default.get("Settings.DeletePrevious"))) !== null && _a !== void 0 ? _a : false;
if (shouldDelete && this.lastDownloadedFolder) {
try {
await fs_extra_1.default.remove(this.lastDownloadedFolder);
}
catch (_b) {
console.log(`ā ļø Failed to delete ${this.lastDownloadedFolder}`);
}
}
}
async setupServer() {
const enabledServers = (await SettingsConfig_1.default.get("Settings.Server")) || [];
DebugLogger_1.logger.debug('š Enabled servers from settings:', enabledServers);
const pluginServers = await loadEnabledPluginServers();
DebugLogger_1.logger.debug('š Plugin servers loaded:', Object.keys(pluginServers));
const allServers = Object.assign(Object.assign({}, this.allServers), pluginServers);
DebugLogger_1.logger.debug('š All servers after merging built-ins and plugins:', Object.keys(allServers));
// Normalize keys for case-insensitive match
const allServersLowerCaseMap = new Map();
for (const key of Object.keys(allServers)) {
allServersLowerCaseMap.set(key.toLowerCase(), key);
}
for (const serverName of enabledServers) {
DebugLogger_1.logger.debug(`š Checking if server "${serverName}" exists in allServers...`);
const lower = serverName.toLowerCase();
if (allServersLowerCaseMap.has(lower)) {
const realKey = allServersLowerCaseMap.get(lower);
DebugLogger_1.logger.debug(`ā
Server found: "${realKey}". Setting as current server.`);
this.currentServer = allServers[realKey];
return true;
}
else {
DebugLogger_1.logger.debug(`ā Server "${serverName}" not found in loaded servers.`);
}
}
console.log('\nā ļø No supported servers enabled in your settings.');
console.log('Please enable a server in Settings -> Enable Server.');
return false;
}
async manage() {
var _a, _b;
if (!(await this.setupServer()))
return;
console.log(`\nš Using ${(_b = (_a = this.currentServer) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : "unknown"} as your manga source.`);
const searchAnswer = await inquirer_1.default.prompt([
{
type: 'input',
name: 'query',
message: 'Enter the manga title to search for:',
},
]);
const searchSpinner = (0, ora_1.default)('Searching...').start();
const searchResults = await this.currentServer.searchManga(searchAnswer.query);
searchSpinner.stop();
if (searchResults.length === 0) {
console.log('No results found.');
return;
}
const mangaChoices = searchResults.map((manga) => ({
name: `${manga.title} - Author: ${manga.author}`,
value: manga,
}));
const selectAnswer = await inquirer_1.default.prompt([
{
type: 'rawlist',
name: 'selectedManga',
message: 'Select a manga:',
choices: mangaChoices,
},
]);
const selectedManga = selectAnswer.selectedManga;
const infoSpinner = (0, ora_1.default)('Loading manga info...').start();
const mangaInfo = await this.currentServer.getMangaDetail(selectedManga.id);
infoSpinner.stop();
if (!mangaInfo) {
console.log('Failed to load manga details.');
return;
}
console.log(`\n=== Manga Information ===`);
console.log(`Title: ${mangaInfo.title}`);
console.log(`Image URL: ${mangaInfo.imageUrl}`);
console.log(`Summary:\n${mangaInfo.summary}\n`);
const actionAnswer = await inquirer_1.default.prompt([
{
type: 'list',
name: 'action',
message: `What would you like to do?`,
choices: [
{ name: 'Select Chapter To Read', value: 'read' },
{ name: 'Back to Search', value: 'back' },
],
},
]);
if (actionAnswer.action === 'read') {
await this.readChapters(selectedManga.id, mangaInfo.title);
}
else {
await this.manage();
}
}
async readChapters(mangaId, mangaTitle) {
const chapterSpinner = (0, ora_1.default)(`Loading chapters for "${mangaTitle}"...`).start();
let chapters = await this.currentServer.getChapterList(mangaId);
chapterSpinner.stop();
if (chapters.length === 0) {
console.log('No chapters found.');
return;
}
chapters.sort((a, b) => this.parseChapterNumber(a.title) - this.parseChapterNumber(b.title));
let currentIndex;
while (true) {
if (typeof currentIndex !== 'number') {
const chapterChoices = chapters.map((ch, idx) => ({
name: `${ch.title} (${ch.date})`,
value: idx,
}));
const chapterAnswer = await inquirer_1.default.prompt([
{
type: 'list',
name: 'chapterIndex',
message: `Select a chapter to read:`,
choices: chapterChoices,
},
]);
currentIndex = chapterAnswer.chapterIndex;
}
if (typeof currentIndex === 'number' && currentIndex < chapters.length) {
const selectedChapter = chapters[currentIndex];
console.log(`\nš Now reading: ${selectedChapter.title} (${selectedChapter.date}) [${currentIndex + 1} / ${chapters.length}]`);
const safeMangaTitle = mangaTitle.replace(/[^a-z0-9]/gi, '_').toLowerCase();
const safeChapterTitle = selectedChapter.title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
await this.cleanupPreviousDownload();
const pdfPath = await this.currentServer.getImagesAndCreatePDF(selectedChapter.url, safeMangaTitle, safeChapterTitle);
this.lastDownloadedFolder = path_1.default.dirname(pdfPath);
await (0, open_1.default)(pdfPath);
const nextAction = await inquirer_1.default.prompt([
{
type: 'list',
name: 'action',
message: `What do you want to do now?`,
choices: [
{ name: 'Read Next Chapter', value: 'next' },
{ name: 'Back to Chapter List', value: 'back' },
{ name: 'Back to Manga Search', value: 'search' },
],
},
]);
if (nextAction.action === 'next') {
currentIndex++;
if (currentIndex >= chapters.length) {
console.log('\nš No more chapters available.');
const backAnswer = await inquirer_1.default.prompt([
{
type: 'confirm',
name: 'goBack',
message: 'Would you like to go back to the chapter list?',
default: true,
},
]);
if (backAnswer.goBack) {
currentIndex = undefined;
continue;
}
else {
break;
}
}
}
else if (nextAction.action === 'back') {
currentIndex = undefined;
}
else {
break;
}
}
else {
console.log('\nš No more chapters available.');
const backAnswer = await inquirer_1.default.prompt([
{
type: 'confirm',
name: 'goBack',
message: 'Would you like to go back to the chapter list?',
default: true,
},
]);
if (backAnswer.goBack) {
currentIndex = undefined;
continue;
}
else {
break;
}
}
}
}
}
exports.default = MangaEventHandler;