UNPKG

@wahaha216/koishi-plugin-jmcomic

Version:

下载JM本子,无需python。支持pdf、zip加密。

1,531 lines (1,504 loc) 68.4 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/locales/zh-CN.yml var require_zh_CN = __commonJS({ "src/locales/zh-CN.yml"(exports2, module2) { module2.exports = { commands: { jm: { description: "下载JM漫画,无需python!", examples: "jm album 本子数字ID\njm album info 本子数字ID\njm photo 本子章节数字ID", album: { examples: "jm album 数字ID", messages: { addedToQueue: "已将 {id} 添加到处理队列,请稍候。", queueFirst: "已将 {id} 添加到处理队列,即将开始处理", queuePosition: "已将 {id} 添加到处理队列\n前面还有 {ahead} 个任务等待或正在处理", queueProcessing: "已将 {id} 添加到处理队列\n当前任务状态:{status}", notExistError: "找不到该ID对应的本子", mysqlError: "已尝试所有备用地址,但是JM坏掉了" }, info: { examples: "jm album info 本子数字ID", messages: { notExistError: "找不到该ID对应的本子", mysqlError: "已尝试所有备用地址,但是JM坏掉了" } } }, photo: { examples: "jm photo 本子章节数字ID", messages: { addedToQueue: "已将 {id} 添加到处理队列,请稍候。", queueFirst: "已将 {id} 添加到处理队列,即将开始处理", queuePosition: "已将 {id} 添加到处理队列\n前面还有 {ahead} 个任务等待或正在处理", queueProcessing: "已将 {id} 添加到处理队列\n当前任务状态:{status}", notExistError: "找不到该ID对应的章节", mysqlError: "已尝试所有备用地址,但是JM坏掉了" } }, queue: { examples: "jm queue", messages: { emptyQueue: "当前没有正在处理或者等待处理的任务", msgFormat: "ID: {id}, 类型: {type}, 状态: {status}\n", task: { pending: "等待中...", processing: "处理中...", failed: "发生未知错误", completed: "已完成", unknown: "未定义状态" }, type: { album: "本子", photo: "章节" } } }, blog: { examples: "jm album 数字ID", messages: { addedToQueue: "已将 {id} 添加到处理队列,请稍候。", queueFirst: "已将 {id} 添加到处理队列,即将开始处理", queuePosition: "已将 {id} 添加到处理队列\n前面还有 {ahead} 个任务等待或正在处理", queueProcessing: "已将 {id} 添加到处理队列\n当前任务状态:{status}", notExistError: "找不到该ID对应的本子", mysqlError: "已尝试所有备用地址,但是JM坏掉了" } }, search: { example: "jm search <关键词>", messages: { emptyKeywordError: "请输入搜索关键词", id: "JMID", name: "名称", author: "作者", category: "分类", description: "描述", pagination: "共 {total} 条, 当前第 {page} 页, 每页 {pageSize} 条" } } } }, _config: [{ $desc: "基础设置", sendMethod: "发送方式", fileMethod: "文件获取方式<br>`buffer`: 读取成buffer后发送给bot实现端<br>`file`: 以`file:///` 本地路径形式发送,如docker环境,请在bot实现端同时映射/koishi目录", password: "密码,留空则不加密", fileName: "文件名定义<br>`{{name}}`:标题<br>`{{id}}`:章节或者本子ID<br>`{{index}}`:多章节本子自动填充`1` 、 `2`" }, { level: "压缩级别,0~9,0为仅存储" }, { listeningJMId: "监听 `JM ID` 关键词,当出现JMxxxxx的关键词时,发送第一张图片" }, { $desc: "限制相关设置", retryCount: "重试次数限制", concurrentDownloadLimit: "同时下载数量限制", concurrentDecodeLimit: "同时解密数量限制", concurrentQueueLimit: "同时处理数量限制" }, { $desc: "缓存设置", cache: "缓存文件" }, { autoDelete: "自动删除缓存,**依赖cron服务**" }, { cron: "5位cron表达式", deleteInStart: "启动时检测", keepDays: "缓存保留时间(天)" }, { $desc: "开发者选项", debug: "调试模式,输出更多信息" }] }; } }); // src/locales/en-US.yml var require_en_US = __commonJS({ "src/locales/en-US.yml"(exports2, module2) { module2.exports = { commands: { jm: { description: "Download JM comics without python!", examples: "jm album albumID\njm album info albumID\njm photo photoID", album: { examples: "jm album albumID", messages: { addedToQueue: "Album {id} has been added to the processing queue. Please wait.", queueFirst: "Added {id} to the processing queue, starting shortly.", queuePosition: "Added {id} to the processing queue.\nThere are {ahead} tasks ahead or currently processing.", queueProcessing: "Added {id} to the processing queue.\nCurrent task status: {status}", notExistError: "albumID not found", mysqlError: "All alternate addresses have been tried. But no response." }, info: { examples: "jm album info albumID", messages: { notExistError: "albumID not found", mysqlError: "All alternate addresses have been tried. But no response." } } }, photo: { examples: "jm photo photoID", messages: { addedToQueue: "Photo {id} has been added to the processing queue. Please wait.", queueFirst: "Added {id} to the processing queue, starting shortly.", queuePosition: "Added {id} to the processing queue.\nThere are {ahead} tasks ahead or currently processing.", queueProcessing: "Added {id} to the processing queue.\nCurrent task status: {status}", notExistError: "photoID not found", mysqlError: "All alternate addresses have been tried. But no response." } }, queue: { examples: "jm queue", messages: { emptyQueue: "There are currently no tasks being processed or waiting.", msgFormat: "ID: {id}, type: {type}, status: {status}\n", task: { Pending: "Pending...", Processing: "Processing...", failed: "Failed (Unknown Error)", completed: "Completed", unknown: "Unknown Status" }, type: { album: "Album", photo: "Photo" } } }, search: { example: "jm search <keyword>", messages: { emptyKeywordError: "Please enter search keywords", id: "JMID", name: "name", author: "author", category: "category", description: "description", pagination: "Total {total}, current {page} page, each page {pageSize}" } }, blog: { examples: "jm blog blogID", messages: { addedToQueue: "Blog {id} has been added to the processing queue. Please wait.", queueFirst: "Added {id} to the processing queue, starting shortly.", queuePosition: "Added {id} to the processing queue.\nThere are {ahead} tasks ahead or currently processing.", queueProcessing: "Added {id} to the processing queue.\nCurrent task status: {status}", notExistError: "blogID not found", mysqlError: "All alternate addresses have been tried. But no response." } } } }, _config: [{ $desc: "Basic settings", sendMethod: "Send method", fileMethod: "File acquisition method<br>`buffer`: Read as buffer and send it to the bot implementation.<br>`file`: Send it in the local path of `file:///`. For example, if in the docker environment, please map the `/koishi` directory at the bot implementation.", password: "Password, leave blank without encryption", fileName: "File name definition<br>`{{name}}`: Title<br>`{{id}}`: Chapter or Book ID<br>`{{index}}`: Multi-chapter book auto-filling `_1` `_2`" }, { level: "Compression level, 0~9, 0 is only stores" }, { listeningJMId: "Monitors for the `JM ID` keyword. When the 'JMxxxxx' keyword is detected, the first image will be sent." }, { $desc: "Limit settings", retryCount: "Retry limit", concurrentDownloadLimit: "Concurrent Download Limit", concurrentDecodeLimit: "Concurrent Decode Limit", concurrentQueueLimit: "Concurrent Queue Limit" }, { $desc: "Cache settings", cache: "Cache files" }, { autoDelete: "Automatically delete cache, **need cron services**" }, { cron: "5-bit cron expression", deleteInStart: "Detection on startup", keepDays: "Cache retention time (days)" }, { $desc: "Developer Options", debug: "Debug mode, output more information" }] }; } }); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, inject: () => inject, name: () => name }); module.exports = __toCommonJS(src_exports); var import_koishi2 = require("koishi"); var import_path4 = require("path"); // src/utils/Utils.ts var import_fs = require("fs"); var import_promises = require("fs/promises"); // src/utils/Regexp.ts var JM_SCRAMBLE_ID = /var scramble_id = (\d+);/; // src/utils/Const.ts var JM_CLIENT_URL_LIST = [ "www.cdnaspa.vip", "www.cdnaspa.club", "www.cdnplaystation6.vip", "www.cdnplaystation6.cc" ]; var JM_IMAGE_URL_LIST = [ "cdn-msp.jmapiproxy1.cc", "cdn-msp.jmapiproxy2.cc", "cdn-msp2.jmapiproxy2.cc", "cdn-msp3.jmapiproxy2.cc", "cdn-msp.jmapinodeudzn.net", "cdn-msp3.jmapinodeudzn.net" ]; var JM_API_URL_DOMAIN_SERVER_LIST = [ "https://rup4a04-c01.tos-ap-southeast-1.bytepluses.com/newsvr-2025.txt", "https://rup4a04-c02.tos-cn-hongkong.bytepluses.com/newsvr-2025.txt" ]; var JM_APP_VERSION = "1.7.9"; var JM_APP_TOKEN_SECRET = "18comicAPP"; var JM_APP_TOKEN_SECRET_2 = "18comicAPPContent"; var JM_APP_DATA_SECRET = "185Hcomic3PAPP7R"; var JM_API_DOMAIN_SERVER_SECRET = "diosfjckwpqpdfjkvnqQjsik"; // src/utils/Utils.ts var import_path = require("path"); // src/error/albumNotExist.error.ts var AlbumNotExistError = class extends Error { static { __name(this, "AlbumNotExistError"); } constructor(message) { super(message); this.name = "AlbumNotExistError"; } }; // src/error/photoNotExist.error.ts var PhotoNotExistError = class extends Error { static { __name(this, "PhotoNotExistError"); } constructor(message) { super(message); this.name = "PhotoNotExistError"; } }; // src/error/mysql.error.ts var MySqlError = class extends Error { static { __name(this, "MySqlError"); } constructor(message) { super(message); this.name = "MySqlError"; } }; // src/error/emptybuffer.error.ts var EmptyBufferError = class extends Error { static { __name(this, "EmptyBufferError"); } constructor(message) { super(message); this.name = "EmptyBufferError"; } }; // src/error/overRetry.error.ts var OverRetryError = class extends Error { static { __name(this, "OverRetryError"); } constructor(message) { super(message); this.name = "OverRetryError"; } }; // src/error/allDomainFailed.error.ts var AllDomainFailedError = class extends Error { static { __name(this, "AllDomainFailedError"); } constructor(message) { super(message); this.name = "AllDomainFailedError"; } }; // src/utils/Utils.ts var import_crypto = require("crypto"); function fileExistsAsync(path) { try { (0, import_fs.accessSync)(path, import_fs.constants.F_OK); return true; } catch (err) { return false; } } __name(fileExistsAsync, "fileExistsAsync"); function fileSizeAsync(path) { try { const stats = (0, import_fs.statSync)(path); return stats.size; } catch (err) { return 0; } } __name(fileSizeAsync, "fileSizeAsync"); function sanitizeFileName(fileName) { const forbiddenAndShellSpecialChars = /[<>:"/\\|?*()[\]{}!#$&;'`~]/g; let sanitizedFileName = fileName.replace(forbiddenAndShellSpecialChars, "_"); sanitizedFileName = sanitizedFileName.replace(/\s+/g, "_"); sanitizedFileName = sanitizedFileName.replace(/_+/g, "_"); sanitizedFileName = sanitizedFileName.replace(/^[._]+|[._]+$/g, ""); let byteLength = Buffer.byteLength(sanitizedFileName, "utf-8"); while (byteLength > 200) { const length = sanitizedFileName.length; sanitizedFileName = sanitizedFileName.substring(0, length - 1); byteLength = Buffer.byteLength(sanitizedFileName, "utf8"); } if (sanitizedFileName.trim() === "") { return "untitled_document"; } return sanitizedFileName; } __name(sanitizeFileName, "sanitizeFileName"); async function limitPromiseAll(promises, limit) { const results = new Array(promises.length); const executing = []; for (let i = 0; i < promises.length; i++) { const promiseFn = promises[i]; while (executing.length >= limit) { await Promise.race(executing); } const p = promiseFn().then((res) => { results[i] = res; }).finally(() => { const index = executing.indexOf(p); if (index !== -1) { executing.splice(index, 1); } }); executing.push(p); } await Promise.all(executing); return results; } __name(limitPromiseAll, "limitPromiseAll"); async function requestWithRetry(url, method, config = {}, http, pluginsConfig, logger, retryIndex = 0) { try { const res = await http(method, url, config); if (typeof res.data === "string" && res.data.includes("Could not connect to mysql")) { throw new MySqlError(); } return res.data; } catch (error) { if (error instanceof MySqlError) { throw new MySqlError(); } else if (retryIndex < pluginsConfig.retryCount) { logger.info( `${url} 请求失败,正在重试... ${retryIndex + 1}/${pluginsConfig.retryCount}` ); return await requestWithRetry( url, method, config, http, pluginsConfig, logger, retryIndex + 1 ); } else { throw new OverRetryError(`请求失败,超过最大重试次数: ${url}`); } } } __name(requestWithRetry, "requestWithRetry"); async function requestWithUrlSwitch(url, method, config = {}, http, pluginsConfig, logger, type = "CLIENT", urlIndex = 0, isUpdate = false) { const list = type === "CLIENT" ? JM_CLIENT_URL_LIST : JM_IMAGE_URL_LIST; console.log(list); const urlCount = list.length; const url_bak = url; if (url.startsWith("/")) { url = `https://${list[urlIndex]}${url}`; } try { if (urlIndex < urlCount) { const res = await requestWithRetry( url, method, config, http, pluginsConfig, logger ); if (res instanceof ArrayBuffer && res.byteLength === 0) { throw new EmptyBufferError(); } return res; } else { throw new AllDomainFailedError(); } } catch (error) { const isMysqlError = error instanceof MySqlError; const isEmptyBuffer = error instanceof EmptyBufferError; const isOverRetryError = error instanceof OverRetryError; const isAllDomainFailedError = error instanceof AllDomainFailedError; if (isMysqlError || isEmptyBuffer || isOverRetryError) { logger.info(`请求失败,尝试切换域名... ${urlIndex + 1}/${urlCount}`); return await requestWithUrlSwitch( url_bak, method, config, http, pluginsConfig, logger, type, urlIndex + 1, isUpdate ); } else if (isAllDomainFailedError && !isUpdate) { logger.info(`请求失败,尝试更新域名...`); const res = await updateApiDomain(http, pluginsConfig, logger); if (!res) throw new AllDomainFailedError(); return await requestWithUrlSwitch( url_bak, method, config, http, pluginsConfig, logger, type, 0, true ); } throw new Error(error); } } __name(requestWithUrlSwitch, "requestWithUrlSwitch"); function getFileInfo(filePath) { const normalizedPath = (0, import_path.normalize)(filePath); const parsePath = (0, import_path.parse)(normalizedPath); const ext = parsePath.ext.slice(1); const fileName = parsePath.name; const dir = parsePath.dir; return { fileName, ext, dir }; } __name(getFileInfo, "getFileInfo"); async function deleteFewDaysAgoFolders(path, days) { const dirEntries = await (0, import_promises.readdir)(path, { withFileTypes: true }); const subfolderNames = dirEntries.filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name); for (const folder of subfolderNames) { const s = await (0, import_promises.stat)(`${path}/${folder}`); const creationTime = s.birthtime || s.ctime; const now = /* @__PURE__ */ new Date(); const diffTime = Math.abs(now.getTime() - creationTime.getTime()); const diffDays = Math.floor(diffTime / (1e3 * 3600 * 24)); if (diffDays >= days) { (0, import_promises.rm)(`${path}/${folder}`, { recursive: true }); } } } __name(deleteFewDaysAgoFolders, "deleteFewDaysAgoFolders"); function formatFileName(originName, name2, id, index) { return originName.replaceAll("{{name}}", name2).replaceAll("{{id}}", id).replaceAll("{{index}}", index ? `${index}` : "").trim(); } __name(formatFileName, "formatFileName"); function md5Hex(key, inputEncoding = "utf-8") { return (0, import_crypto.createHash)("md5").update(key, inputEncoding).digest("hex"); } __name(md5Hex, "md5Hex"); function decodeBase64(base64, timestamp, secret = JM_APP_DATA_SECRET) { const dataB64 = Buffer.from(base64, "base64"); const md5 = md5Hex(`${timestamp}${secret}`); const key = Buffer.from(md5); const decipher = (0, import_crypto.createDecipheriv)("aes-256-ecb", key, null); let dataAES = decipher.update(dataB64); let decrypted = Buffer.concat([dataAES, decipher.final()]); const decodedString = decrypted.toString("utf-8"); return JSON.parse(decodedString); } __name(decodeBase64, "decodeBase64"); async function updateApiDomain(http, config, logger) { for (const url of JM_API_URL_DOMAIN_SERVER_LIST) { try { const encodeText = await requestWithRetry( url, "GET", { responseType: "text" }, http, config, logger ); const domainJson = decodeBase64( encodeText, "", JM_API_DOMAIN_SERVER_SECRET ); JM_CLIENT_URL_LIST.splice(0, JM_CLIENT_URL_LIST.length); JM_CLIENT_URL_LIST.push(...domainJson.Server); console.log(domainJson); console.log(JM_CLIENT_URL_LIST); return true; } catch (error) { console.log(error); } } return false; } __name(updateApiDomain, "updateApiDomain"); // src/entity/JMAppClient.ts var import_crypto3 = require("crypto"); var import_form_data = __toESM(require("form-data")); // src/abstract/JMClientAbstract.ts var JMClientAbstract = class { static { __name(this, "JMClientAbstract"); } root; constructor(root) { this.root = root; } setRoot(root) { this.root = root; } getRoot() { return this.root; } }; // src/abstract/JMPhotoAbstract.ts var import_crypto2 = __toESM(require("crypto")); var JMPhotoAbstract = class _JMPhotoAbstract { static { __name(this, "JMPhotoAbstract"); } static SCRAMBLE_268850 = 268850; static SCRAMBLE_421926 = 421926; id; images; image_names; splitNumbers; setId(id) { this.id = id; } getId() { return this.id; } setImages(images) { this.images = images; } getImages() { return this.images; } setImageNames(imageNames) { this.image_names = imageNames; } getImageNames() { return this.image_names; } setSplitNumbers(splitNumbers) { this.splitNumbers = splitNumbers; } getSplitNumbers() { return this.splitNumbers; } /** * 生成图片分割数 */ generateSplitNumbers(scramble_id) { const splitNumbers = this.image_names.map((name2) => { if (this.id < scramble_id) { return 0; } else if (this.id < _JMPhotoAbstract.SCRAMBLE_268850) { return 10; } else { const x = this.id < _JMPhotoAbstract.SCRAMBLE_421926 ? 10 : 8; const s = `${this.id}${name2}`; const md5 = import_crypto2.default.createHash("md5"); const hash = md5.update(s).digest("hex"); const lastChar = hash[hash.length - 1]; let num = lastChar.charCodeAt(0) % x; num = num * 2 + 2; return num; } }); this.splitNumbers = splitNumbers; } }; // src/entity/JMAppPhoto.ts var JMAppPhoto = class _JMAppPhoto extends JMPhotoAbstract { static { __name(this, "JMAppPhoto"); } series; tags; name; addtime; series_id; is_favorite; liked; constructor(json) { super(); for (const key in json) { if (Object.prototype.hasOwnProperty.call(this, key)) { this[key] = json[key]; } } } setSeries(series) { this.series = series; } getSeries() { return this.series; } setTags(tags) { this.tags = tags; } getTags() { return this.tags; } setName(name2) { this.name = name2; } getName() { return this.name; } setAddtime(addtime) { this.addtime = addtime; } getAddtime() { return this.addtime; } setSeriesId(seriesId) { this.series_id = seriesId; } getSeriesId() { return this.series_id; } setIsFavorite(isFavorite) { this.is_favorite = isFavorite; } getIsFavorite() { return this.is_favorite; } setLiked(liked) { this.liked = liked; } getLiked() { return this.liked; } /** * 从JSON数据返回JMPhoto实体类 * @param json JMPhoto JSON数据 * @returns */ static fromJson(json) { return new _JMAppPhoto(json); } }; // src/abstract/JMAlbumAbstract.ts var JMAlbumAbstract = class { static { __name(this, "JMAlbumAbstract"); } /** * 本子ID */ id; /** * 名称 */ name; /** * 章节列表 */ series = []; /** * 作品 */ works; /** * 登场人物 */ actors; /** * 标签 */ tags; /** * 作者 */ authors; /** * 描述 */ description; /** * 点赞数 */ likes; /** * 观看次数 */ total_views; /** * 章节信息 */ photos; setId(id) { this.id = id; } getId() { return this.id; } setName(name2) { this.name = name2; } getName() { return this.name; } setSeries(series) { this.series = series; } getSeries() { return this.series; } setWorks(works) { this.works = works; } getWorks() { return this.works; } setActors(actors) { this.actors = actors; } getActors() { return this.actors; } setTags(tags) { this.tags = tags; } getTags() { return this.tags; } setAuthors(authors) { this.authors = authors; } getAuthors() { return this.authors; } setDescription(description) { this.description = description; } getDescription() { return this.description; } setLikes(likes) { this.likes = likes; } getLikes() { return this.likes; } setTotalViews(totalViews) { this.total_views = totalViews; } getTotalViews() { return this.total_views; } setPhotos(photos) { this.photos = photos; } getPhotos() { return this.photos; } }; // src/entity/JMAppAlbum.ts var JMAppAlbum = class _JMAppAlbum extends JMAlbumAbstract { static { __name(this, "JMAppAlbum"); } images; addtime; series_id; comment_total; related_list; liked; is_favorite; is_aids; price; purchased; constructor(json) { super(); for (const key in json) { if (Object.prototype.hasOwnProperty.call(this, key)) { this[key] = json[key]; } } } setImages(images) { this.images = images; } getImages() { return this.images; } setAddtime(addtime) { this.addtime = addtime; } getAddtime() { return this.addtime; } setSeriesId(seriesId) { this.series_id = seriesId; } getSeriesId() { return this.series_id; } setCommentTotal(commentTotal) { this.comment_total = commentTotal; } getCommentTotal() { return this.comment_total; } setRelatedList(relatedList) { this.related_list = relatedList; } getRelatedList() { return this.related_list; } setLiked(liked) { this.liked = liked; } getLiked() { return this.liked; } setIsFavorite(isFavorite) { this.is_favorite = isFavorite; } getIsFavorite() { return this.is_favorite; } setIsAids(isAids) { this.is_aids = isAids; } getIsAids() { return this.is_aids; } setPrice(price) { this.price = price; } getPrice() { return this.price; } setPurchased(purchased) { this.purchased = purchased; } getPurchased() { return this.purchased; } getPhotos() { return super.getPhotos(); } static fromJson(json) { return new _JMAppAlbum(json); } }; // src/entity/JMAppClient.ts var import_promises2 = require("fs/promises"); // src/utils/Image.ts var import_fs2 = __toESM(require("fs")); var import_sharp = __toESM(require("sharp")); var import_archiver = __toESM(require("archiver")); var import_archiver_zip_encrypted = __toESM(require("archiver-zip-encrypted")); var import_path2 = require("path"); async function decodeImage(imageBuffer, num, path) { if (num > 0) { const metadata = await (0, import_sharp.default)(Buffer.from(imageBuffer)).metadata(); const height = metadata.height || 0; const width = metadata.width || 0; if (height < num) { await (0, import_sharp.default)(Buffer.from(imageBuffer)).toFile(path); return; } const over = height % num; const move = Math.floor(height / num); let decodedImageInstance = (0, import_sharp.default)({ create: { width, height, channels: 3, background: { r: 0, g: 0, b: 0 } } }).webp(); const croppeds = []; for (let i = 0; i < num; i++) { let currentMove = move; let ySrc = height - move * (i + 1) - over; let yDst = move * i; if (i === 0) { currentMove += over; } else { yDst += over; } const cropped = await (0, import_sharp.default)(imageBuffer).extract({ left: 0, top: ySrc, width, height: currentMove }).toBuffer(); croppeds.push({ top: yDst, cropped }); } decodedImageInstance = decodedImageInstance.composite( croppeds.map((c) => ({ input: c.cropped, top: c.top, left: 0 })) ); await decodedImageInstance.toFile(path); } else { await (0, import_sharp.default)(Buffer.from(imageBuffer)).toFile(path); } } __name(decodeImage, "decodeImage"); async function saveImage(imageBuffer, path) { if (!imageBuffer.byteLength) return; const buffer = Buffer.from(imageBuffer); await (0, import_sharp.default)(buffer).toFile(path); } __name(saveImage, "saveImage"); async function archiverImage(directorys, outputPath, password, level = 9) { if (!import_archiver.default.isRegisteredFormat("zip-encrypted")) { import_archiver.default.registerFormat("zip-encrypted", import_archiver_zip_encrypted.default); } const output = import_fs2.default.createWriteStream(outputPath); const options = { zlib: { level } // 压缩级别 }; if (password) { options["encryptionMethod"] = "aes256"; options["password"] = password; } const archive = import_archiver.default.create(password ? "zip-encrypted" : "zip", options); archive.pipe(output); directorys.forEach(({ directory, destpath }) => { archive.directory(directory, destpath); }); await archive.finalize(); } __name(archiverImage, "archiverImage"); async function archiverFile(path, outputPath, password, level = 9) { if (!import_archiver.default.isRegisteredFormat("zip-encrypted")) { import_archiver.default.registerFormat("zip-encrypted", import_archiver_zip_encrypted.default); } const output = import_fs2.default.createWriteStream(outputPath); const options = { zlib: { level } // 压缩级别 }; if (password) { options["encryptionMethod"] = "aes256"; options["password"] = password; } const archive = import_archiver.default.create(password ? "zip-encrypted" : "zip", options); archive.pipe(output); archive.file(path, { name: (0, import_path2.basename)(path) }); await archive.finalize(); } __name(archiverFile, "archiverFile"); // src/entity/JMAppClient.ts var import_path3 = require("path"); var import_sharp2 = __toESM(require("sharp")); var import_muhammara = require("muhammara"); // src/abstract/JMBlogAbstract.ts var JMBlogAbstract = class { static { __name(this, "JMBlogAbstract"); } /** * 文库信息 */ info; /** * 关联的本子 */ related_comics; /** * 相关文库 */ related_blogs; // --- Info 的 Getter 和 Setter --- get Info() { return this.info; } set Info(value) { this.info = value; } // --- RelatedComics 的 Getter 和 Setter --- get RelatedComics() { return this.related_comics; } set RelatedComics(value) { this.related_comics = value; } // --- RelatedBlogs 的 Getter 和 Setter --- get RelatedBlogs() { return this.related_blogs; } set RelatedBlogs(value) { this.related_blogs = value; } }; // src/entity/JMAppBlog.ts var JMAppBlog = class _JMAppBlog extends JMBlogAbstract { static { __name(this, "JMAppBlog"); } constructor(json) { super(); for (const key in json) { if (Object.prototype.hasOwnProperty.call(this, key)) { this[key] = json[key]; } } } static fromJson(json) { return new _JMAppBlog(json); } }; // src/entity/JMAppClient.ts var JMAppClient = class extends JMClientAbstract { static { __name(this, "JMAppClient"); } /** * koishi 配置项 */ config; /** * koishi 日志 */ logger; /** * koishi http */ http; puppeteer; constructor(root, http, config, logger, puppeteer) { super(root); this.config = config; this.logger = logger; this.http = http; this.puppeteer = puppeteer; } /** * 登录,未完成 * @param username 用户名 * @param password 密码 * @returns 用户信息 */ async login(username, password) { const timestamp = this.getTimeStamp(); const { token, tokenparam } = this.getTokenAndTokenParam(timestamp); const headers = { "Accept-Encoding": "gzip, deflate", "User-Agent": "Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.11211812; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36", token, tokenparam }; const formData = new import_form_data.default(); formData.append("username", username); formData.append("password", password); const res = await this.http.post( "https://www.cdnmhws.cc/login", formData, { headers, responseType: "json" } ); return decodeBase64(res.data, timestamp); } async search(keyword) { if (this.config.debug) this.logger.info(`搜索本子: ${keyword}`); const timestamp = this.getTimeStamp(); const { token, tokenparam } = this.getTokenAndTokenParam(timestamp); const searchRes = await requestWithUrlSwitch( "/search", "POST", { params: { search_query: keyword }, headers: { token, tokenparam }, responseType: "json" }, this.http, this.config, this.logger ); return decodeBase64(searchRes.data, timestamp); } async getAlbumById(id) { if (this.config.debug) this.logger.info(`获取本子(${id})信息`); const timestamp = this.getTimeStamp(); const { token, tokenparam } = this.getTokenAndTokenParam(timestamp); const res = await requestWithUrlSwitch( "/album", "POST", { params: { id }, headers: { token, tokenparam }, responseType: "json" }, this.http, this.config, this.logger ); const album_json = decodeBase64(res.data, timestamp); if (!album_json.name) throw new AlbumNotExistError(); const album = JMAppAlbum.fromJson(album_json); const series = album.getSeries(); const photos = []; if (series.length) { for (const s of series) { const photo = await this.getPhotoById(s.id); photos.push(photo); } } else { const photo = await this.getPhotoById(id); photos.push(photo); } if (this.config.debug) this.logger.info(`本子 ${id} 章节数:${photos.length}`); album.setPhotos(photos); return album; } async getPhotoById(id) { if (this.config.debug) this.logger.info(`获取章节(${id})信息`); const timestamp = this.getTimeStamp(); const { token, tokenparam } = this.getTokenAndTokenParam(timestamp); const res = await requestWithUrlSwitch( "/chapter", "POST", { params: { id }, headers: { token, tokenparam }, responseType: "json" }, this.http, this.config, this.logger ); const photo_json = decodeBase64(res.data, timestamp); if (!photo_json.name) throw new PhotoNotExistError(); const photo = JMAppPhoto.fromJson(photo_json); const images = photo.getImages(); const image_ids = images.map((image) => image.split(".")[0]); photo.setImageNames(image_ids); return photo; } async getBlogById(id) { if (this.config.debug) this.logger.info(`获取文库(${id})信息`); const timestamp = this.getTimeStamp(); const { token, tokenparam } = this.getTokenAndTokenParam(timestamp); const res = await requestWithUrlSwitch( "/blog", "POST", { params: { id }, headers: { token, tokenparam }, responseType: "json" }, this.http, this.config, this.logger ); const blog_json = decodeBase64(res.data, timestamp); if (!blog_json?.info?.title) throw new AlbumNotExistError(); return JMAppBlog.fromJson(blog_json); } async downloadByAlbum(album) { const id = album.getId(); const path = `${this.root}/album/${id}`; await (0, import_promises2.mkdir)(path, { recursive: true }); const photos = album.getPhotos(); for (const photo of photos) { await this.downloadByPhoto(photo, "album", id, photos.length === 1); } } async downloadByPhoto(photo, type = "photo", albumId = "", single = false) { const images = photo.getImages(); const id = photo.getId(); let path = `${this.root}/${type}/${id}/origin`; if (this.config.debug) { this.logger.info(`开始下载: ${id}`); if (type === "album") { this.logger.info(`单章节: ${single ? "是" : "否"}`); this.logger.info(`子章节: ${albumId ? "是" : "否"}`); this.logger.info(`本子ID: ${albumId}`); } } if (type === "album") { if (single) { path = `${this.root}/${type}/${albumId}/origin`; } else { path = `${this.root}/${type}/${albumId}/origin/${id}`; } } if (this.config.debug) this.logger.info(`存储目录:${path}`); await (0, import_promises2.mkdir)(path, { recursive: true }); await limitPromiseAll( images.filter((image) => { const imagePath = `${path}/${image}`; const fileExists = fileExistsAsync(imagePath); const fileSize = fileSizeAsync(imagePath); return !fileExists || !fileSize; }).map((image) => async () => { const url = `/media/photos/${id}/${image}`; if (this.config.debug) this.logger.info(`下载图片:${url}`); const res = await requestWithUrlSwitch( url, "GET", { responseType: "arraybuffer" }, this.http, this.config, this.logger, "IMAGE" ); await saveImage(res, `${path}/${image}`); }), this.config.concurrentDownloadLimit ); if (this.config.debug) this.logger.info(`${id} 下载完成,开始解密图片`); await this.decodeByPhoto(photo, type, albumId, single); } async downloadFirstImageByAlbum(album) { const id = album.getId(); const path = `${this.root}/album/${id}`; await (0, import_promises2.mkdir)(path, { recursive: true }); const photos = album.getPhotos(); return await this.downloadAndDecodeFirstImageByPhoto( photos[0], "album", id, photos.length === 1 ); } async downloadAndDecodeFirstImageByPhoto(photo, type = "photo", albumId = "", single = false) { const image = photo.getImages()[0]; const id = photo.getId(); let path = `${this.root}/${type}/${id}/origin`; if (type === "album") { if (single) { path = `${this.root}/${type}/${albumId}/origin`; } else { path = `${this.root}/${type}/${albumId}/origin/${id}`; } } if (this.config.debug) this.logger.info(`存储目录:${path}`); await (0, import_promises2.mkdir)(path, { recursive: true }); const url = `/media/photos/${id}/${image}`; if (this.config.debug) this.logger.info(`下载图片:${url}`); const res = await requestWithUrlSwitch( url, "GET", { responseType: "arraybuffer" }, this.http, this.config, this.logger, "IMAGE" ); await saveImage(res, `${path}/${image}`); if (this.config.debug) this.logger.info(`${id} 下载完成,开始解密图片`); let decodedPath = `${this.root}/${type}/${id}/decoded`; if (type === "album" && !single) { path = `${this.root}/${type}/${albumId}/origin/${id}`; decodedPath = `${this.root}/${type}/${albumId}/decoded/${id}`; } await (0, import_promises2.mkdir)(decodedPath, { recursive: true }); const scramble_id = await this.requestScrambleId(id); photo.generateSplitNumbers(scramble_id); const splitNumber = photo.getSplitNumbers()[0]; const imagePath = `${path}/${image}`; if (this.config.debug) this.logger.info(`解密图片:${imagePath}`); const decodedImagePath = `${decodedPath}/${image}`; const imageBuffer = await (0, import_promises2.readFile)(imagePath); await decodeImage(imageBuffer, splitNumber, decodedImagePath); return decodedImagePath; } async decodeByPhoto(photo, type = "photo", albumId = "", single = false) { const images = photo.getImages(); const id = photo.getId(); const scramble_id = await this.requestScrambleId(id); photo.generateSplitNumbers(scramble_id); const splitNumbers = photo.getSplitNumbers(); let path = `${this.root}/${type}/${id}/origin`; let decodedPath = `${this.root}/${type}/${id}/decoded`; if (type === "album" && !single) { path = `${this.root}/${type}/${albumId}/origin/${id}`; decodedPath = `${this.root}/${type}/${albumId}/decoded/${id}`; } await (0, import_promises2.mkdir)(path, { recursive: true }); await (0, import_promises2.mkdir)(decodedPath, { recursive: true }); await limitPromiseAll( images.filter((image) => { const imagePath = `${decodedPath}/${image}`; const fileExists = fileExistsAsync(imagePath); const fileSize = fileSizeAsync(imagePath); return !fileExists || !fileSize; }).map((image, index) => async () => { const imagePath = `${path}/${image}`; if (this.config.debug) this.logger.info(`解密图片:${imagePath}`); const decodedImagePath = `${decodedPath}/${image}`; const imageBuffer = await (0, import_promises2.readFile)(imagePath); await decodeImage(imageBuffer, splitNumbers[index], decodedImagePath); }), this.config.concurrentDecodeLimit ); this.logger.info(`${id} 解密完成`); } async albumToPdf(album, password) { const id = album.getId(); const photos = album.getPhotos(); if (photos.length === 1) { const photo = photos[0]; return await this.photoToPdf( photo, `${photo.getName()}`, "album", id, true, password ); } else { let paths = []; for (const [i, photo] of photos.entries()) { const path = await this.photoToPdf( photo, `${photo.getName()}_${i + 1}`, "album", id, false, password ); paths.push(path); } return paths; } } async photoToPdf(photo, pdfName, type = "photo", albumId = "", single = false, password) { const images = photo.getImages(); const id = photo.getId(); pdfName = sanitizeFileName(pdfName); if (this.config.debug) this.logger.info(`开始生成PDF ${pdfName}.pdf`); let path = (0, import_path3.join)(this.root, type, `${id}`); if (type === "album") { path = (0, import_path3.join)(this.root, type, `${albumId}`); } const pdfPath = (0, import_path3.join)(path, `${pdfName}.pdf`); let pdfDoc; try { pdfDoc = new import_muhammara.Recipe("new", pdfPath, { version: 1.6 }); } catch (error) { throw new Error(error); } const tempPath = (0, import_path3.join)(path, `temp_${id}`); await (0, import_promises2.mkdir)(tempPath); for (const image of images) { let imagePath = (0, import_path3.join)(path, "decoded", image); if (type === "album" && !single) { imagePath = (0, import_path3.join)(path, "decoded", `${id}`, image); } const buffer = await (0, import_promises2.readFile)(imagePath); const ext = (0, import_path3.extname)(imagePath); const jpgName = image.replace(ext, ".jpg"); const jpgPath = (0, import_path3.join)(tempPath, jpgName); const sharpInstance = (0, import_sharp2.default)(buffer); await sharpInstance.jpeg().toFile(jpgPath); const metadata = await sharpInstance.metadata(); pdfDoc.createPage(metadata.width, metadata.height).image(jpgPath, 0, 0).endPage(); } if (password) { pdfDoc.encrypt({ userPassword: password, ownerPassword: password, userProtectionFlag: 4 }); } try { pdfDoc.endPDF(() => { if (this.config.debug) this.logger.info(`PDF ${pdfName}.pdf 生成完成`); }); } catch (error) { throw new Error(error); } finally { await (0, import_promises2.rm)(tempPath, { recursive: true }); } return pdfPath; } async blogToPdf(blog, password) { const pdfName = sanitizeFileName(blog.Info.title); if (this.config.debug) this.logger.info(`开始生成PDF ${pdfName}.pdf`); const path = (0, import_path3.join)(this.root, "blog", `${blog.Info.id}`); await (0, import_promises2.mkdir)(path, { recursive: true }); const pdfPath = (0, import_path3.join)(path, `${pdfName}.pdf`); const pdfTempPath = (0, import_path3.join)(path, `temp.pdf`); await this.writeBlogPdf(blog, pdfTempPath); try { const recipe = new import_muhammara.Recipe(pdfTempPath, pdfPath); if (password) { recipe.encrypt({ userPassword: password, ownerPassword: password, userProtectionFlag: 4 }); } recipe.endPDF(() => { if (this.config.debug) this.logger.info(`PDF ${pdfName}.pdf 生成完成`); }); } catch (error) { throw new Error(error); } finally { await (0, import_promises2.rm)(pdfTempPath, { recursive: true, force: true }); } return pdfPath; } async writeBlogPdf(blog, pdfPath) { const context = await this.puppeteer.browser.createBrowserContext(); const page = await context.newPage(); await page.setViewport({ width: 0, height: 0, deviceScaleFactor: 1 }); await page.evaluate((title) => { document.title = title; }, blog.Info.title); await page.setContent(blog.Info.content, { waitUntil: "networkidle0", timeout: 60 * 1e3 }); const pdfBuffer = await page.pdf({ format: "A4", printBackground: true, // 确保背景颜色和图片被打印 margin: { top: "25mm", right: "25mm", bottom: "25mm", left: "25mm" } }); await (0, import_promises2.writeFile)(pdfPath, pdfBuffer); context.close(); page.close(); } async albumToZip(album, password, level = 6) { const id = album.getId(); const series = album.getSeries(); const zipName = sanitizeFileName(album.getName()); if (this.config.debug) this.logger.info(`开始生成ZIP ${zipName}.zip`); const path = (0, import_path3.join)(this.root, "album", `${id}`); const directorys = []; if (series.length > 1) { for (const s of series) { directorys.push({ directory: (0, import_path3.join)(path, "decoded", s.id), destpath: `第${s.sort}章` }); } } else { directorys.push({ directory: (0, import_path3.join)(path, "decoded"), destpath: false }); } const zipPath = (0, import_path3.join)(path, `${zipName}.zip`); await archiverImage(directorys, zipPath, password, level); if (this.config.debug) this.logger.info(`ZIP ${zipName}.zip 生成完成`); return zipPath; } async photoToZip(photo, zipName, password, level = 6) { const id = photo.getId(); zipName = sanitizeFileName(zipName); if (this.config.debug) this.logger.info(`开始生成ZIP ${zipName}.zip`); const path = (0, import_path3.join)(this.root, "photo", `${id}`); const zipPath = (0, import_path3.join)(path, `${zipName}.zip`); await archiverImage( [{ directory: (0, import_path3.join)(path, "decoded"), destpath: false }], zipPath, password, level ); if (this.config.debug) this.logger.info(`ZIP ${zipName}.zip 生成完成`); return zipPath; } async blogToZip(blog, password, level = 6) { const zipName = sanitizeFileName(blog.Info.title); if (this.config.debug) this.logger.info(`开始生成ZIP ${zipName}.zip`); const path = (0, import_path3.join)(this.root, "blog", `${blog.Info.id}`); await (0, import_promises2.mkdir)(path, { recursive: true }); const pdfPath = (0, import_path3.join)(path, `${zipName}.pdf`); const zipPath = (0, import_path3.join)(path, `${zipName}.zip`); await this.writeBlogPdf(blog, pdfPath); await archiverFile(pdfPath, zipPath, password, level); if (this.config.debug) this.logger.info(`ZIP ${zipName}.zip 生成完成`); return zipPath; } /** * 获取时间戳 * @returns 时间戳 */ getTimeStamp() { const date = /* @__PURE__ */ new Date(); return date.getTime(); } /** * 获取Scramble ID * @param id JM本子ID */ async requestScrambleId(id) { const timestamp = this.getTimeStamp(); const { token, tokenparam } = this.getTokenAndTokenParam( timestamp, JM_APP_TOKEN_SECRET_2 ); const html = await requestWithUrlSwitch( "/chapter_view_template", "POST", { params: { id }, headers: { token, tokenparam }, responseType: "text" }, this.http, this.config, this.logger ); return parseInt(html.match(JM_SCRAMBLE_ID)[1]); } /** * 获取请求时所需的token和tokenparam * @param timestamp 时间戳 * @param version APP版本 * @param secret 密钥 * @returns 请求时所需的token和tokenparam */ getTokenAndTokenParam(timestamp, secret = JM_APP_TOKEN_SECRET, version = JM_APP_VERSION) { const key = `${timestamp}${secret}`; const token = (0, import_crypto3.createHash)("md5").update(key).digest("hex"); const tokenparam = `${timestamp},${version}`; return { token, tokenparam }; } }; // src/processors/jmProcessor.ts var import_koishi = require("koishi"); var import_promises3 = require("node:fs/promises"); var createJmProcessor = /* @__PURE__ */ __name((processorConfig, http, config, logger, puppeteer) => { const { root, sendMethod, password, level, fileName, fileMethod, cache, debug } = processorConfig; return async (payload) => { const { id, session, messageId, scope } = payload; try { const jmClient = new JMAppClient(root, http, config, logger, puppeteer); let filePath; switch (payload.type) { case "album": { const album = await jmClient.getAlbumById(id); await jmClient.downloadByAlbum(album); if (sendMethod === "zip") { filePath = await jmClient.albumToZip(album, password, level); } else { filePath = await jmClient.albumToPdf(album, password); } break; } case "photo": { const photo = await jmClient.getPhotoById(id); await jmClient.downloadByPhoto(photo); const photoName = photo.getName(); if (sendMethod === "zip") { filePath = await jmClient.photoToZip( photo, photoName, password, level ); } else { filePath = await jmClient.photoToPdf(photo, photoName); } break; } case "blog": { const blog = await jmClient.getBlogById(id); if (sendMethod === "zip") { filePath = await jmClient.blogToZip(blog, password, level); } else { filePath = await jmClient.blogToPdf(blog, password); } return; } // 理论上不会走到这里,但为了类型安全和健壮性,可以抛出错误 default: throw new Error(`未知任务类型: ${payload.type}`); } if (typeof filePath === "string") { const { fileName: baseFileName, ext: fileExt, dir: fileDir } = getFileInfo(filePath); const finalName = formatFileName(fileName, baseFileName, id); if (debug) logger.info(`文件名:${finalName}.${fileExt}`); if (fileMethod === "buffer") { const buffer = await (0, import_promises3.readFile)(filePath); await session.send([ import_koishi.h.file(buffer, fileExt, { title: `${finalName}.${fileExt}` }) ]); } else { await session.send([ import_koishi.h.file(`file:///${filePath}`, { title: `${finalName}.${fileExt}` }) ]); } if (!cache) (0, import_promises3.rm)(fileDir, { recursive: true }); } else { let currentFileDir = ""; for (const [index, p] of filePath.entries()) { const { fileName: baseFileName, ext: fileExt, dir: fileDir } = getFileInfo(p); const finalName = formatFileName( fileName, baseFileName, id, index + 1 ); if (debug) logger.info(`文件名:${finalName}.${fileExt}`); if (fileMethod === "buffer") { const buffer = await (0, import_promises3.readFile)(p); await session.send([ import_koishi.h.file(buffer, fileExt, { title: `${finalName}.${fileExt}` }) ]); } else { await session.send([ import_koishi.h.file(`file:///${p}`, { title: `${finalName}.${fileExt}` }) ]); } currentFileDir = fileDir; } if (!cache) { if (currentFileDir) { (0, import_promises3.rm)(currentFileDir, { recu