From 2174c13e1661da801fe43bd52e51106dc85a7888 Mon Sep 17 00:00:00 2001 From: morning-start Date: Sat, 26 Jul 2025 20:27:08 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=86=8D?= =?UTF-8?q?=E6=BC=AB=E7=94=BB=E6=BA=90=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加zaimanhua.js作为新的漫画源配置文件,包含完整的漫画源实现 在.gitignore中新增test/目录忽略规则 --- .gitignore | 3 +- zaimanhua.js | 583 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 zaimanhua.js diff --git a/.gitignore b/.gitignore index d48c759..231a0fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -.vscode \ No newline at end of file +.vscode +test/ \ No newline at end of file diff --git a/zaimanhua.js b/zaimanhua.js new file mode 100644 index 0000000..9e88a3c --- /dev/null +++ b/zaimanhua.js @@ -0,0 +1,583 @@ +/** @type {import('./_venera_.js')} */ +class ZaiManHua extends ComicSource { + // Note: The fields which are marked as [Optional] should be removed if not used + + // name of the source + name = "再漫画"; + + // unique id of the source + key = "zaimanhua"; + + version = "1.0.0"; + + minAppVersion = "1.4.0"; + + // update url + url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/zaimanhua.js"; + + /** + * fetch html content + * @param url {string} + * @param headers {object?} + * @returns {Promise<{body: string, status: number, headers: object}>} + */ + async fetchHtml(url, headers = {}) { + let res = await Network.get(url, headers); + return res; + } + + /** + * fetch json content + * @param url {string} + * @param headers {object?} + * @returns {Promise<{errno:number,errmsg:string,data:object}>} + */ + async fetchJson(url, headers = {}) { + let res = await Network.get(url, headers); + return JSON.parse(res.body); + } + + /** + * parse comic from html element + * @param comic {HtmlElement} + * @returns {Comic} + */ + parseCoverComic(comic) { + let title = comic.querySelector("p > a").text.trim(); + let url = comic.querySelector("p > a").attributes["href"]; + let id = url.split("/").pop().split(".")[0]; + let cover = comic.querySelector("img").attributes["src"]; + let subtitle = comic.querySelector(".auth")?.text.trim(); + if (!subtitle) { + subtitle = comic + .querySelector(".con_author") + ?.text.replace("作者:", "") + .trim(); + } + let description = comic.querySelector(".tip")?.text.trim(); + + return new Comic({ title, id, subtitle, url, cover, description }); + } + /** + * parse comic from html element + * @param comic {HtmlElement} + * @returns {Comic} + */ + parseListComic(comic) { + let cover = comic.querySelector("img").attributes["src"]; + let title = comic.querySelector("h3 > a").text.trim(); + let url = comic.querySelector("h3 > a").attributes["href"]; + let id = url.split("/").pop().split(".")[0]; + + let infos = comic.querySelectorAll("p"); + + let subtitle = infos[0]?.text.replace("作者:", "").trim(); + let classify = infos[1]?.text.replace("类型:", "").trim().split("/"); + let status = infos[2]?.text.replace("状态:", "").trim(); + let description = infos[3]?.text.replace("最新:", "").trim(); + let tags = { + 类型: classify, + 状态: status, + }; + + return new Comic({ + title, + id, + subtitle, + tags, + url, + cover, + description, + }); + } + + /** + * parse json content + * @param e object + * @returns {Comic} + */ + parseJsonComic(e) { + let cover = e.cover; + let title = e.name; + let id = e.comic_py; + let url = `https://www.zaimanhua.com/info/${e.id}.html`; + + let subtitle = e.authors; + + let classify = e.types; + let status = e.status; + let description = e.last_update_chapter_name; + let tags = { + 类型: classify, + 状态: status, + }; + + return new Comic({ + title, + id, + subtitle, + tags, + url, + cover, + description, + }); + } + + /** + * [Optional] init function + */ + init() { + this.domain = "https://www.zaimanhua.com"; + this.imgBase = "https://images.zaimanhua.com"; + this.baseUrl = "https://manhua.zaimanhua.com"; + } + + // explore page list + explore = [ + { + // title of the page. + // title is used to identify the page, it should be unique + title: this.name, + + /// TODO multiPartPage + type: "singlePageWithMultiPart", + + /** + * load function + * @param page {number | null} - page number, null for `singlePageWithMultiPart` type + * @returns {{}} + * - for `multiPartPage` type, return [{title: string, comics: Comic[], viewMore: PageJumpTarget}] + * - for `multiPageComicList` type, for each page(1-based), return {comics: Comic[], maxPage: number} + * - for `mixed` type, use param `page` as index. for each index(0-based), return {data: [], maxPage: number?}, data is an array contains Comic[] or {title: string, comics: Comic[], viewMore: string?} + */ + load: async (page) => { + let result = {}; + let res = await this.fetchHtml(this.domain); + if (res.status !== 200) { + throw `Invalid status code: ${res.status}`; + } + let document = new HtmlDocument(res.body); + // 推荐 + let recommend_title = document.querySelector( + ".new_recommend_l h2" + )?.text; + let recommend_comics = document + .querySelectorAll(".new_recommend_l li") + .map(this.parseCoverComic); + result[recommend_title] = recommend_comics; + // 更新 + let update_title = document.querySelector(".new_update_l h2")?.text; + let update_comics = document + .querySelectorAll(".new_update_l li") + .map(this.parseCoverComic); + result[update_title] = update_comics; + // 少男漫画 + // 少女漫画 + // 冒险,搞笑,奇幻 + return result; + }, + }, + ]; + + // categories + category = { + /// title of the category page, used to identify the page, it should be unique + title: "", + parts: [ + { + // title of the part + name: "Theme", + + // fixed or random or dynamic + // if random, need to provide `randomNumber` field, which indicates the number of comics to display at the same time + // if dynamic, need to provide `loader` field, which indicates the function to load comics + type: "fixed", + + // Remove this if type is dynamic + categories: [ + { + label: "Category1", + /** + * @type {PageJumpTarget} + */ + target: { + page: "category", + attributes: { + category: "category1", + param: null, + }, + }, + }, + ] + + // number of comics to display at the same time + // randomNumber: 5, + + // load function for dynamic type + // loader: async () => { + // return [ + // // ... + // ] + // } + } + ], + // enable ranking page + enableRankingPage: false, + } + + /// category comic loading related + categoryComics = { + /** + * load comics of a category + * @param category {string} - category name + * @param param {string?} - category param + * @param options {string[]} - options from optionList + * @param page {number} - page number + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + load: async (category, param, options, page) => { + /* + ``` + let data = JSON.parse((await Network.get('...')).body) + let maxPage = data.maxPage + + function parseComic(comic) { + // ... + + return new Comic({ + id: id, + title: title, + subTitle: author, + cover: cover, + tags: tags, + description: description + }) + } + + return { + comics: data.list.map(parseComic), + maxPage: maxPage + } + ``` + */ + }, + // provide options for category comic loading + optionList: [ + { + // For a single option, use `-` to separate the value and text, left for value, right for text + options: [ + "newToOld-New to Old", + "oldToNew-Old to New" + ], + // [Optional] {string[]} - show this option only when the value not in the list + notShowWhen: null, + // [Optional] {string[]} - show this option only when the value in the list + showWhen: null + } + ], + ranking: { + // For a single option, use `-` to separate the value and text, left for value, right for text + options: [ + "day-Day", + "week-Week" + ], + /** + * load ranking comics + * @param option {string} - option from optionList + * @param page {number} - page number + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + load: async (option, page) => { + /* + ``` + let data = JSON.parse((await Network.get('...')).body) + let maxPage = data.maxPage + + function parseComic(comic) { + // ... + + return new Comic({ + id: id, + title: title, + subTitle: author, + cover: cover, + tags: tags, + description: description + }) + } + + return { + comics: data.list.map(parseComic), + maxPage: maxPage + } + ``` + */ + } + } + } + + /// search related + search = { + /** + * load search result + * @param keyword {string} + * @param options {string[]} - options from optionList + * @param page {number} + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + load: async (keyword, options, page) => { + /* + ``` + let data = JSON.parse((await Network.get('...')).body) + let maxPage = data.maxPage + + function parseComic(comic) { + // ... + + return new Comic({ + id: id, + title: title, + subTitle: author, + cover: cover, + tags: tags, + description: description + }) + } + + return { + comics: data.list.map(parseComic), + maxPage: maxPage + } + ``` + */ + }, + + /** + * load search result with next page token. + * The field will be ignored if `load` function is implemented. + * @param keyword {string} + * @param options {(string)[]} - options from optionList + * @param next {string | null} + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + loadNext: async (keyword, options, next) => {}, + + // provide options for search + optionList: [ + { + // [Optional] default is `select` + // type: select, multi-select, dropdown + // For select, there is only one selected value + // For multi-select, there are multiple selected values or none. The `load` function will receive a json string which is an array of selected values + // For dropdown, there is one selected value at most. If no selected value, the `load` function will receive a null + type: "select", + // For a single option, use `-` to separate the value and text, left for value, right for text + options: ["0-time", "1-popular"], + // option label + label: "sort", + // default selected options. If not set, use the first option as default + default: null, + }, + ], + + // enable tags suggestions + enableTagsSuggestions: false, + }; + + // favorite related + favorites = { + // whether support multi folders + multiFolder: false, + /** + * add or delete favorite. + * throw `Login expired` to indicate login expired, App will automatically re-login and re-add/delete favorite + * @param comicId {string} + * @param folderId {string} + * @param isAdding {boolean} - true for add, false for delete + * @param favoriteId {string?} - [Comic.favoriteId] + * @returns {Promise} - return any value to indicate success + */ + addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => { + /* + ``` + let res = await Network.post('...') + if (res.status === 401) { + throw `Login expired`; + } + return 'ok' + ``` + */ + }, + /** + * load favorite folders. + * throw `Login expired` to indicate login expired, App will automatically re-login retry. + * if comicId is not null, return favorite folders which contains the comic. + * @param comicId {string?} + * @returns {Promise<{folders: {[p: string]: string}, favorited: string[]}>} - `folders` is a map of folder id to folder name, `favorited` is a list of folder id which contains the comic + */ + loadFolders: async (comicId) => { + /* + ``` + let data = JSON.parse((await Network.get('...')).body) + + let folders = {} + + data.folders.forEach((f) => { + folders[f.id] = f.name + }) + + return { + folders: folders, + favorited: data.favorited + } + ``` + */ + }, + /** + * add a folder + * @param name {string} + * @returns {Promise} - return any value to indicate success + */ + addFolder: async (name) => { + /* + ``` + let res = await Network.post('...') + if (res.status === 401) { + throw `Login expired`; + } + return 'ok' + ``` + */ + }, + /** + * delete a folder + * @param folderId {string} + * @returns {Promise} - return any value to indicate success + */ + deleteFolder: async (folderId) => { + /* + ``` + let res = await Network.delete('...') + if (res.status === 401) { + throw `Login expired`; + } + return 'ok' + ``` + */ + }, + /** + * load comics in a folder + * throw `Login expired` to indicate login expired, App will automatically re-login retry. + * @param page {number} + * @param folder {string?} - folder id, null for non-multi-folder + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + loadComics: async (page, folder) => { + /* + ``` + let data = JSON.parse((await Network.get('...')).body) + let maxPage = data.maxPage + + function parseComic(comic) { + // ... + + return new Comic{ + id: id, + title: title, + subTitle: author, + cover: cover, + tags: tags, + description: description + } + } + + return { + comics: data.list.map(parseComic), + maxPage: maxPage + } + ``` + */ + }, + /** + * load comics with next page token + * @param next {string | null} - next page token, null for first page + * @param folder {string} + * @returns {Promise<{comics: Comic[], next: string?}>} + */ + loadNext: async (next, folder) => {}, + /** + * If the comic source only allows one comic in one folder, set this to true. + */ + singleFolderForSingleComic: false, + }; + + /// single comic related + comic = { + /** + * load comic info + * @param id {string} + * @returns {Promise} + */ + loadInfo: async (id) => {}, + /** + * [Optional] load thumbnails of a comic + * + * To render a part of an image as thumbnail, return `${url}@x=${start}-${end}&y=${start}-${end}` + * - If width is not provided, use full width + * - If height is not provided, use full height + * @param id {string} + * @param next {string?} - next page token, null for first page + * @returns {Promise<{thumbnails: string[], next: string?}>} - `next` is next page token, null for no more + */ + loadThumbnails: async (id, next) => { + /* + ``` + let data = JSON.parse((await Network.get('...')).body) + + return { + thumbnails: data.list, + next: next, + } + ``` + */ + }, + + /** + * load images of a chapter + * @param comicId {string} + * @param epId {string?} + * @returns {Promise<{images: string[]}>} + */ + loadEp: async (comicId, epId) => { + /* + ``` + return { + // string[] + images: images + } + ``` + */ + }, + /** + * [Optional] provide configs for an image loading + * @param url + * @param comicId + * @param epId + * @returns {ImageLoadingConfig | Promise} + */ + onImageLoad: (url, comicId, epId) => { + return {}; + }, + /** + * [Optional] provide configs for a thumbnail loading + * @param url {string} + * @returns {ImageLoadingConfig | Promise} + * + * `ImageLoadingConfig.modifyImage` and `ImageLoadingConfig.onLoadFailed` will be ignored. + * They are not supported for thumbnails. + */ + onThumbnailLoad: (url) => { + return {}; + }, + }; +} From a5b1fd6ca2d4d8a1b68ff9e553de34287987cab5 Mon Sep 17 00:00:00 2001 From: morning-start Date: Sat, 26 Jul 2025 22:14:14 +0800 Subject: [PATCH 2/9] =?UTF-8?q?refactor(zaimanhua):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=BC=AB=E7=94=BB=E6=BA=90=E6=8E=A5=E5=8F=A3=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=92=8C=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改fetchHtml和fetchJson返回类型,增加错误处理 - 简化漫画信息解析逻辑,移除冗余字段 - 重构分类页面实现,使用固定分类选项 - 实现分类漫画加载接口,支持分页和筛选 --- zaimanhua.js | 457 +++++++++++++++------------------------------------ 1 file changed, 129 insertions(+), 328 deletions(-) diff --git a/zaimanhua.js b/zaimanhua.js index 9e88a3c..7822cd0 100644 --- a/zaimanhua.js +++ b/zaimanhua.js @@ -19,22 +19,27 @@ class ZaiManHua extends ComicSource { * fetch html content * @param url {string} * @param headers {object?} - * @returns {Promise<{body: string, status: number, headers: object}>} + * @returns {Promise<{document:HtmlDocument}>} */ async fetchHtml(url, headers = {}) { let res = await Network.get(url, headers); - return res; + if (res.status !== 200) { + throw "Invalid status code: " + res.status; + } + let document = new HtmlDocument(res.body); + + return document; } /** * fetch json content * @param url {string} * @param headers {object?} - * @returns {Promise<{errno:number,errmsg:string,data:object}>} + * @returns {Promise<{data:object}>} */ async fetchJson(url, headers = {}) { let res = await Network.get(url, headers); - return JSON.parse(res.body); + return JSON.parse(res.body).data; } /** @@ -85,7 +90,6 @@ class ZaiManHua extends ComicSource { id, subtitle, tags, - url, cover, description, }); @@ -100,24 +104,17 @@ class ZaiManHua extends ComicSource { let cover = e.cover; let title = e.name; let id = e.comic_py; - let url = `https://www.zaimanhua.com/info/${e.id}.html`; let subtitle = e.authors; - let classify = e.types; - let status = e.status; + let classify = e.types.split("/"); let description = e.last_update_chapter_name; - let tags = { - 类型: classify, - 状态: status, - }; return new Comic({ title, id, subtitle, - tags, - url, + tags: classify, cover, description, }); @@ -152,11 +149,7 @@ class ZaiManHua extends ComicSource { */ load: async (page) => { let result = {}; - let res = await this.fetchHtml(this.domain); - if (res.status !== 200) { - throw `Invalid status code: ${res.status}`; - } - let document = new HtmlDocument(res.body); + let document = await this.fetchHtml(this.domain); // 推荐 let recommend_title = document.querySelector( ".new_recommend_l h2" @@ -179,142 +172,124 @@ class ZaiManHua extends ComicSource { }, ]; - // categories - category = { - /// title of the category page, used to identify the page, it should be unique - title: "", - parts: [ - { - // title of the part - name: "Theme", - - // fixed or random or dynamic - // if random, need to provide `randomNumber` field, which indicates the number of comics to display at the same time - // if dynamic, need to provide `loader` field, which indicates the function to load comics - type: "fixed", - - // Remove this if type is dynamic - categories: [ - { - label: "Category1", - /** - * @type {PageJumpTarget} - */ - target: { - page: "category", - attributes: { - category: "category1", - param: null, - }, - }, - }, - ] - - // number of comics to display at the same time - // randomNumber: 5, - - // load function for dynamic type - // loader: async () => { - // return [ - // // ... - // ] - // } - } + // categories + // categories + category = { + /// title of the category page, used to identify the page, it should be unique + title: this.name, + parts: [ + { + name: "类型", + type: "fixed", + categories: [ + "全部", + "冒险", + "搞笑", + "格斗", + "科幻", + "爱情", + "侦探", + "竞技", + "魔法", + "校园", + "百合", + "耽美", + "历史", + "战争", + "宅系", + "治愈", + "仙侠", + "武侠", + "职场", + "神鬼", + "奇幻", + "生活", + "其他", ], - // enable ranking page - enableRankingPage: false, - } - - /// category comic loading related - categoryComics = { - /** - * load comics of a category - * @param category {string} - category name - * @param param {string?} - category param - * @param options {string[]} - options from optionList - * @param page {number} - page number - * @returns {Promise<{comics: Comic[], maxPage: number}>} - */ - load: async (category, param, options, page) => { - /* - ``` - let data = JSON.parse((await Network.get('...')).body) - let maxPage = data.maxPage - - function parseComic(comic) { - // ... - - return new Comic({ - id: id, - title: title, - subTitle: author, - cover: cover, - tags: tags, - description: description - }) - } - - return { - comics: data.list.map(parseComic), - maxPage: maxPage - } - ``` - */ - }, - // provide options for category comic loading - optionList: [ - { - // For a single option, use `-` to separate the value and text, left for value, right for text - options: [ - "newToOld-New to Old", - "oldToNew-Old to New" - ], - // [Optional] {string[]} - show this option only when the value not in the list - notShowWhen: null, - // [Optional] {string[]} - show this option only when the value in the list - showWhen: null - } + itemType: "category", + categoryParams: [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "11", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", ], - ranking: { - // For a single option, use `-` to separate the value and text, left for value, right for text - options: [ - "day-Day", - "week-Week" - ], - /** - * load ranking comics - * @param option {string} - option from optionList - * @param page {number} - page number - * @returns {Promise<{comics: Comic[], maxPage: number}>} - */ - load: async (option, page) => { - /* - ``` - let data = JSON.parse((await Network.get('...')).body) - let maxPage = data.maxPage + }, + ], + // enable ranking page + enableRankingPage: false, + }; - function parseComic(comic) { - // ... + /// category comic loading related + categoryComics = { + /** + * load comics of a category + * @param category {string} - category name + * @param param {string?} - category param + * @param options {string[]} - options from optionList + * @param page {number} - page number + * @returns {Promise<{comics: Comic[], maxPage: number}>} + */ + load: async (category, param, options, page) => { + let fil = "https://manhua.zaimanhua.com/api/v1/comic1/filter"; + let params = { + timestamp: Date.now(), + sortType: 0, + page: page, + size: 20, + status: options[1], + audience: options[0], + theme: param, + cate: options[2], + }; + // 拼接url + let params_str = Object.keys(params) + .map((key) => `${key}=${params[key]}`) + .join("&"); + // log("error", "再漫画", params_str); + let url = `${fil}?${params_str}&firstLetter`; + // log("error", "再漫画", url); - return new Comic({ - id: id, - title: title, - subTitle: author, - cover: cover, - tags: tags, - description: description - }) - } - - return { - comics: data.list.map(parseComic), - maxPage: maxPage - } - ``` - */ - } - } - } + const json = await this.fetchJson(url); + let comics = json.comicList.map((e) => this.parseJsonComic(e)); + let maxPage = Math.ceil(json.totalNum / params.size); + // log("error", "再漫画", comics); + return { + comics, + maxPage, + }; + }, + // provide options for category comic loading + optionList: [ + { + options: ["0-全部", "3262-少年", "3263-少女", "3264-青年"], + }, + { + options: ["0-全部", "1-故事漫画", "2-四格多格"], + }, + { + options: ["0-全部", "1-连载", "2-完结"], + }, + ], + }; /// search related search = { @@ -326,191 +301,17 @@ class ZaiManHua extends ComicSource { * @returns {Promise<{comics: Comic[], maxPage: number}>} */ load: async (keyword, options, page) => { - /* - ``` - let data = JSON.parse((await Network.get('...')).body) - let maxPage = data.maxPage - - function parseComic(comic) { - // ... - - return new Comic({ - id: id, - title: title, - subTitle: author, - cover: cover, - tags: tags, - description: description - }) - } - - return { - comics: data.list.map(parseComic), - maxPage: maxPage - } - ``` - */ + let url = `https://manhua.zaimanhua.com/app/v1/search/index?keyword=${keyword}&source=0&page=${page}&size=20`; + const json = await this.fetchJson(url); }, - /** - * load search result with next page token. - * The field will be ignored if `load` function is implemented. - * @param keyword {string} - * @param options {(string)[]} - options from optionList - * @param next {string | null} - * @returns {Promise<{comics: Comic[], maxPage: number}>} - */ - loadNext: async (keyword, options, next) => {}, - // provide options for search - optionList: [ - { - // [Optional] default is `select` - // type: select, multi-select, dropdown - // For select, there is only one selected value - // For multi-select, there are multiple selected values or none. The `load` function will receive a json string which is an array of selected values - // For dropdown, there is one selected value at most. If no selected value, the `load` function will receive a null - type: "select", - // For a single option, use `-` to separate the value and text, left for value, right for text - options: ["0-time", "1-popular"], - // option label - label: "sort", - // default selected options. If not set, use the first option as default - default: null, - }, - ], + optionList: [], // enable tags suggestions enableTagsSuggestions: false, }; - // favorite related - favorites = { - // whether support multi folders - multiFolder: false, - /** - * add or delete favorite. - * throw `Login expired` to indicate login expired, App will automatically re-login and re-add/delete favorite - * @param comicId {string} - * @param folderId {string} - * @param isAdding {boolean} - true for add, false for delete - * @param favoriteId {string?} - [Comic.favoriteId] - * @returns {Promise} - return any value to indicate success - */ - addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => { - /* - ``` - let res = await Network.post('...') - if (res.status === 401) { - throw `Login expired`; - } - return 'ok' - ``` - */ - }, - /** - * load favorite folders. - * throw `Login expired` to indicate login expired, App will automatically re-login retry. - * if comicId is not null, return favorite folders which contains the comic. - * @param comicId {string?} - * @returns {Promise<{folders: {[p: string]: string}, favorited: string[]}>} - `folders` is a map of folder id to folder name, `favorited` is a list of folder id which contains the comic - */ - loadFolders: async (comicId) => { - /* - ``` - let data = JSON.parse((await Network.get('...')).body) - - let folders = {} - - data.folders.forEach((f) => { - folders[f.id] = f.name - }) - - return { - folders: folders, - favorited: data.favorited - } - ``` - */ - }, - /** - * add a folder - * @param name {string} - * @returns {Promise} - return any value to indicate success - */ - addFolder: async (name) => { - /* - ``` - let res = await Network.post('...') - if (res.status === 401) { - throw `Login expired`; - } - return 'ok' - ``` - */ - }, - /** - * delete a folder - * @param folderId {string} - * @returns {Promise} - return any value to indicate success - */ - deleteFolder: async (folderId) => { - /* - ``` - let res = await Network.delete('...') - if (res.status === 401) { - throw `Login expired`; - } - return 'ok' - ``` - */ - }, - /** - * load comics in a folder - * throw `Login expired` to indicate login expired, App will automatically re-login retry. - * @param page {number} - * @param folder {string?} - folder id, null for non-multi-folder - * @returns {Promise<{comics: Comic[], maxPage: number}>} - */ - loadComics: async (page, folder) => { - /* - ``` - let data = JSON.parse((await Network.get('...')).body) - let maxPage = data.maxPage - - function parseComic(comic) { - // ... - - return new Comic{ - id: id, - title: title, - subTitle: author, - cover: cover, - tags: tags, - description: description - } - } - - return { - comics: data.list.map(parseComic), - maxPage: maxPage - } - ``` - */ - }, - /** - * load comics with next page token - * @param next {string | null} - next page token, null for first page - * @param folder {string} - * @returns {Promise<{comics: Comic[], next: string?}>} - */ - loadNext: async (next, folder) => {}, - /** - * If the comic source only allows one comic in one folder, set this to true. - */ - singleFolderForSingleComic: false, - }; - /// single comic related comic = { /** From fd59c132a218e12b31726f8e635efa3be72be9ab Mon Sep 17 00:00:00 2001 From: morning-start Date: Sat, 26 Jul 2025 22:14:43 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=86=8D=E6=BC=AB?= =?UTF-8?q?=E7=94=BB=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 搜索功能未正确返回漫画列表和最大页数,添加缺失的返回数据逻辑 --- zaimanhua.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/zaimanhua.js b/zaimanhua.js index 7822cd0..6efa8e2 100644 --- a/zaimanhua.js +++ b/zaimanhua.js @@ -303,13 +303,18 @@ class ZaiManHua extends ComicSource { load: async (keyword, options, page) => { let url = `https://manhua.zaimanhua.com/app/v1/search/index?keyword=${keyword}&source=0&page=${page}&size=20`; const json = await this.fetchJson(url); + let comics = json.comicList.map((e) => this.parseJsonComic(e)); + let maxPage = Math.ceil(json.totalNum / params.size); + // log("error", "再漫画", comics); + return { + comics, + maxPage, + }; }, // provide options for search optionList: [], - // enable tags suggestions - enableTagsSuggestions: false, }; /// single comic related From b1b8b8cab90c0b3138de71f2a0c0f9b784b05c00 Mon Sep 17 00:00:00 2001 From: morning-start Date: Sat, 26 Jul 2025 22:58:19 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat(=E6=BC=AB=E7=94=BB=E8=AF=A6=E6=83=85):?= =?UTF-8?q?=20=E5=AE=9E=E7=8E=B0=E6=BC=AB=E7=94=BB=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=A1=B5=E7=9A=84=E5=8A=A0=E8=BD=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加从API获取漫画详细信息的实现,包括标题、作者、封面、描述、章节列表和推荐漫画 使用baseUrl代替硬编码的域名,提高代码可维护性 移除未使用的parseListComic方法 --- zaimanhua.js | 98 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/zaimanhua.js b/zaimanhua.js index 6efa8e2..3383a56 100644 --- a/zaimanhua.js +++ b/zaimanhua.js @@ -63,37 +63,6 @@ class ZaiManHua extends ComicSource { return new Comic({ title, id, subtitle, url, cover, description }); } - /** - * parse comic from html element - * @param comic {HtmlElement} - * @returns {Comic} - */ - parseListComic(comic) { - let cover = comic.querySelector("img").attributes["src"]; - let title = comic.querySelector("h3 > a").text.trim(); - let url = comic.querySelector("h3 > a").attributes["href"]; - let id = url.split("/").pop().split(".")[0]; - - let infos = comic.querySelectorAll("p"); - - let subtitle = infos[0]?.text.replace("作者:", "").trim(); - let classify = infos[1]?.text.replace("类型:", "").trim().split("/"); - let status = infos[2]?.text.replace("状态:", "").trim(); - let description = infos[3]?.text.replace("最新:", "").trim(); - let tags = { - 类型: classify, - 状态: status, - }; - - return new Comic({ - title, - id, - subtitle, - tags, - cover, - description, - }); - } /** * parse json content @@ -249,7 +218,7 @@ class ZaiManHua extends ComicSource { * @returns {Promise<{comics: Comic[], maxPage: number}>} */ load: async (category, param, options, page) => { - let fil = "https://manhua.zaimanhua.com/api/v1/comic1/filter"; + let fil = `${this.baseUrl}/api/v1/comic1/filter`; let params = { timestamp: Date.now(), sortType: 0, @@ -301,7 +270,7 @@ class ZaiManHua extends ComicSource { * @returns {Promise<{comics: Comic[], maxPage: number}>} */ load: async (keyword, options, page) => { - let url = `https://manhua.zaimanhua.com/app/v1/search/index?keyword=${keyword}&source=0&page=${page}&size=20`; + let url = `${this.baseUrl}/app/v1/search/index?keyword=${keyword}&source=0&page=${page}&size=20`; const json = await this.fetchJson(url); let comics = json.comicList.map((e) => this.parseJsonComic(e)); let maxPage = Math.ceil(json.totalNum / params.size); @@ -314,7 +283,6 @@ class ZaiManHua extends ComicSource { // provide options for search optionList: [], - }; /// single comic related @@ -324,7 +292,67 @@ class ZaiManHua extends ComicSource { * @param id {string} * @returns {Promise} */ - loadInfo: async (id) => {}, + loadInfo: async (id) => { + const api = `${this.domain}/api/v1/comic1/comic/detail`; + let params = { + channel: "pc", + app_name: "zmh", + version: "1.0.0", + timestamp: Date.now(), + uid: 0, + comic_py: id, + }; + let params_str = Object.keys(params) + .map((key) => `${key}=${params[key]}`) + .join("&"); + let url = `${api}?${params_str}`; + const json = await this.fetchJson(url); + const info = json.comicInfo; + const comic_id = info.id; + let title = info.title; + let author = info.authorInfo.authorName; + let lastUpdateTime = new Date(info.lastUpdateTime); + let updateTime = `${lastUpdateTime.getFullYear()}-${ + lastUpdateTime.getMonth() + 1 + }-${lastUpdateTime.getDate()}`; + let description = info.description; + let cover = info.cover; + + let chapters = new Map(); + info.chapterList.data.forEach((e) => { + chapters.set(e.chapter_id, e.chapter_title); + }); + + // &uid=0&comic_id=69500 + const api2 = `${this.baseUrl}/api/v1/comic1/comic/same_list`; + let params2 = { + channel: "pc", + app_name: "zmh", + version: "1.0.0", + timestamp: Date.now(), + uid: 0, + comic_id: comic_id, + }; + let params2_str = Object.keys(params2) + .map((key) => `${key}=${params2[key]}`) + .join("&"); + let url2 = `${api2}?${params2_str}`; + const json2 = await this.fetchJson(url2); + let recommend = json2.comicList.map((e) => this.parseJsonComic(e)); + return new ComicDetails({ + title, + subtitle: author, + cover, + description, + tags: { + 状态: [info.status], + 类型: [info.readerGroup, ...info.types.split("/")], + }, + chapters, + recommend, + updateTime, + }); + }, /** * [Optional] load thumbnails of a comic * From 0976105138ccb335f573375c17a80912f9826d0e Mon Sep 17 00:00:00 2001 From: morning-start Date: Sat, 26 Jul 2025 23:00:52 +0800 Subject: [PATCH 5/9] =?UTF-8?q?refactor(zaimanhua):=20=E7=AE=80=E5=8C=96?= =?UTF-8?q?=20parseJsonComic=20=E6=96=B9=E6=B3=95=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 直接使用对象属性初始化 Comic 对象,避免不必要的中间变量 --- zaimanhua.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/zaimanhua.js b/zaimanhua.js index 3383a56..72353d1 100644 --- a/zaimanhua.js +++ b/zaimanhua.js @@ -70,22 +70,13 @@ class ZaiManHua extends ComicSource { * @returns {Comic} */ parseJsonComic(e) { - let cover = e.cover; - let title = e.name; - let id = e.comic_py; - - let subtitle = e.authors; - - let classify = e.types.split("/"); - let description = e.last_update_chapter_name; - return new Comic({ - title, - id, - subtitle, - tags: classify, - cover, - description, + id: e.comic_py, + title: e.name, + subtitle: e.authors, + tags: e.types.split("/"), + cover: e.cover, + description: e.last_update_chapter_name, }); } From 2e13f5fce9b54c67a209f381bf7ffcba99b28aec Mon Sep 17 00:00:00 2001 From: morning-start Date: Sun, 27 Jul 2025 00:05:57 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix(=E6=BC=AB=E7=94=BB=E6=BA=90):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=B6=E9=97=B4=E6=88=B3=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E5=92=8C=E7=AB=A0=E8=8A=82=E6=8E=92=E5=BA=8F=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E5=AE=9E=E7=8E=B0=E7=AB=A0=E8=8A=82=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复时间戳需要乘以1000的问题 - 对章节按照ID进行排序 - 实现章节图片加载功能 - 完善漫画详情页的标签信息 --- zaimanhua.js | 95 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/zaimanhua.js b/zaimanhua.js index 72353d1..a716a46 100644 --- a/zaimanhua.js +++ b/zaimanhua.js @@ -302,19 +302,24 @@ class ZaiManHua extends ComicSource { const comic_id = info.id; let title = info.title; let author = info.authorInfo.authorName; - let lastUpdateTime = new Date(info.lastUpdateTime); + + // 修复时间戳转换问题 + let lastUpdateTime = new Date(info.lastUpdateTime * 1000); let updateTime = `${lastUpdateTime.getFullYear()}-${ lastUpdateTime.getMonth() + 1 }-${lastUpdateTime.getDate()}`; + let description = info.description; let cover = info.cover; let chapters = new Map(); - info.chapterList.data.forEach((e) => { + info.chapterList[0].data.forEach((e) => { chapters.set(e.chapter_id, e.chapter_title); }); + // chapters 按照key排序 + let chaptersSorted = new Map([...chapters].sort((a, b) => a[0] - b[0])); - // &uid=0&comic_id=69500 + // 获取推荐漫画 const api2 = `${this.baseUrl}/api/v1/comic1/comic/same_list`; let params2 = { channel: "pc", @@ -329,43 +334,25 @@ class ZaiManHua extends ComicSource { .join("&"); let url2 = `${api2}?${params2_str}`; const json2 = await this.fetchJson(url2); - let recommend = json2.comicList.map((e) => this.parseJsonComic(e)); + let recommend = json2.data.comicList.map((e) => this.parseJsonComic(e)); + let tags = { + 状态: [info.status], + 类型: [info.readerGroup, ...info.types.split("/")], + 点击: [info.hitNumStr], + 订阅: [info.subNumStr], + }; + return new ComicDetails({ title, subtitle: author, cover, description, - tags: { - 状态: [info.status], - 类型: [info.readerGroup, ...info.types.split("/")], - }, - chapters, + tags, + chapters: chaptersSorted, recommend, updateTime, }); }, - /** - * [Optional] load thumbnails of a comic - * - * To render a part of an image as thumbnail, return `${url}@x=${start}-${end}&y=${start}-${end}` - * - If width is not provided, use full width - * - If height is not provided, use full height - * @param id {string} - * @param next {string?} - next page token, null for first page - * @returns {Promise<{thumbnails: string[], next: string?}>} - `next` is next page token, null for no more - */ - loadThumbnails: async (id, next) => { - /* - ``` - let data = JSON.parse((await Network.get('...')).body) - - return { - thumbnails: data.list, - next: next, - } - ``` - */ - }, /** * load images of a chapter @@ -374,14 +361,44 @@ class ZaiManHua extends ComicSource { * @returns {Promise<{images: string[]}>} */ loadEp: async (comicId, epId) => { - /* - ``` - return { - // string[] - images: images - } - ``` - */ + const api_ = `${this.domain}/api/v1/comic1/comic/detail`; + // log("error", "再漫画", id); + let params_ = { + channel: "pc", + app_name: "zmh", + version: "1.0.0", + timestamp: Date.now(), + uid: 0, + comic_py: comicId, + }; + let params_str_ = Object.keys(params_) + .map((key) => `${key}=${params[key]}`) + .join("&"); + let url_ = `${api_}?${params_str_}`; + const json_ = await this.fetchJson(url_); + const info_ = json_.comicInfo; + const comic_id = info_.id; + + const api = `${this.baseUrl}/api/v1/comic1/chapter/detail`; + // comic_id=18114&chapter_id=36227 + let params = { + channel: "pc", + app_name: "zmh", + version: "1.0.0", + timestamp: Date.now(), + uid: 0, + comic_id: comic_id, + chapter_id: epId, + }; + let params_str = Object.keys(params) + .map((key) => `${key}=${params[key]}`) + .join("&"); + let url = `${api}?${params_str}`; + const json = await this.fetchJson(url); + const info = json.chapterInfo; + return { + images: info.page_url, + }; }, /** * [Optional] provide configs for an image loading From f812964e551b8031b7ed932789ec34c752c793ea Mon Sep 17 00:00:00 2001 From: morning-start Date: Sun, 27 Jul 2025 00:23:29 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=AB=A0=E8=8A=82?= =?UTF-8?q?ID=E5=92=8C=E7=82=B9=E5=87=BB=E6=95=B0=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 确保章节ID和点击数字段始终作为字符串处理,避免潜在的类型错误。同时修正URL参数拼接中的变量名错误。 --- zaimanhua.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zaimanhua.js b/zaimanhua.js index a716a46..b454d65 100644 --- a/zaimanhua.js +++ b/zaimanhua.js @@ -314,7 +314,7 @@ class ZaiManHua extends ComicSource { let chapters = new Map(); info.chapterList[0].data.forEach((e) => { - chapters.set(e.chapter_id, e.chapter_title); + chapters.set(e.chapter_id.toString(), e.chapter_title); }); // chapters 按照key排序 let chaptersSorted = new Map([...chapters].sort((a, b) => a[0] - b[0])); @@ -338,7 +338,7 @@ class ZaiManHua extends ComicSource { let tags = { 状态: [info.status], 类型: [info.readerGroup, ...info.types.split("/")], - 点击: [info.hitNumStr], + 点击: [info.hitNumStr.toString()], 订阅: [info.subNumStr], }; @@ -372,7 +372,7 @@ class ZaiManHua extends ComicSource { comic_py: comicId, }; let params_str_ = Object.keys(params_) - .map((key) => `${key}=${params[key]}`) + .map((key) => `${key}=${params_[key]}`) .join("&"); let url_ = `${api_}?${params_str_}`; const json_ = await this.fetchJson(url_); From 631298ce1b377afbd21207812646af5ff9743c4f Mon Sep 17 00:00:00 2001 From: morning-start Date: Sun, 27 Jul 2025 01:20:28 +0800 Subject: [PATCH 8/9] =?UTF-8?q?refactor(zaimanhua):=20=E4=BD=BF=E7=94=A8AP?= =?UTF-8?q?I=E6=8E=A5=E5=8F=A3=E6=9B=BF=E4=BB=A3HTML=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=BC=AB=E7=94=BB=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除parseCoverComic方法,改为通过API接口获取漫画数据并重构parseJsonComic方法处理返回的JSON数据。同时修改首页加载逻辑,直接调用API接口获取推荐漫画列表,提高数据获取的稳定性和效率。 --- zaimanhua.js | 85 ++++++++++++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/zaimanhua.js b/zaimanhua.js index b454d65..809b8e5 100644 --- a/zaimanhua.js +++ b/zaimanhua.js @@ -42,41 +42,27 @@ class ZaiManHua extends ComicSource { return JSON.parse(res.body).data; } - /** - * parse comic from html element - * @param comic {HtmlElement} - * @returns {Comic} - */ - parseCoverComic(comic) { - let title = comic.querySelector("p > a").text.trim(); - let url = comic.querySelector("p > a").attributes["href"]; - let id = url.split("/").pop().split(".")[0]; - let cover = comic.querySelector("img").attributes["src"]; - let subtitle = comic.querySelector(".auth")?.text.trim(); - if (!subtitle) { - subtitle = comic - .querySelector(".con_author") - ?.text.replace("作者:", "") - .trim(); - } - let description = comic.querySelector(".tip")?.text.trim(); - - return new Comic({ title, id, subtitle, url, cover, description }); - } - /** * parse json content * @param e object * @returns {Comic} */ parseJsonComic(e) { + let id = e.comic_py; + if (!id) { + id = id.comicPy; + } + let title = e?.name; + if (!title) { + title = e?.title; + } return new Comic({ - id: e.comic_py, - title: e.name, - subtitle: e.authors, - tags: e.types.split("/"), - cover: e.cover, - description: e.last_update_chapter_name, + id: id.toString(), + title: title.toString(), + subtitle: e?.authors, + tags: e?.types?.split("/"), + cover: e?.cover, + description: e?.last_update_chapter_name.toString(), }); } @@ -109,24 +95,31 @@ class ZaiManHua extends ComicSource { */ load: async (page) => { let result = {}; - let document = await this.fetchHtml(this.domain); - // 推荐 - let recommend_title = document.querySelector( - ".new_recommend_l h2" - )?.text; - let recommend_comics = document - .querySelectorAll(".new_recommend_l li") - .map(this.parseCoverComic); - result[recommend_title] = recommend_comics; - // 更新 - let update_title = document.querySelector(".new_update_l h2")?.text; - let update_comics = document - .querySelectorAll(".new_update_l li") - .map(this.parseCoverComic); - result[update_title] = update_comics; - // 少男漫画 - // 少女漫画 - // 冒险,搞笑,奇幻 + // https://manhua.zaimanhua.com/api/v1/comic1/recommend/list? + // channel=pc&app_name=zmh&version=1.0.0×tamp=1753547675981&uid=0 + let api = `${this.baseUrl}/api/v1/comic1/recommend/list`; + let params = { + channel: "pc", + app_name: "zmh", + version: "1.0.0", + timestamp: Date.now(), + uid: 0, + }; + let params_str = Object.keys(params) + .map((key) => `${key}=${params[key]}`) + .join("&"); + let url = `${api}?${params_str}`; + const json = await this.fetchJson(url); + let data = json.list; + data.shift(); // 去掉第一个 + data.pop(); // 去掉最后一个 + data.map((arr) => { + let title = arr.name; + let comic_list = arr.list.map((item) => this.parseJsonComic(item)); + result[title] = comic_list; + }); + + log("error", "再看漫画", result); return result; }, }, From fb20c6802496e0114cf34516c6dc216bac271910 Mon Sep 17 00:00:00 2001 From: morning-start Date: Sun, 27 Jul 2025 01:21:53 +0800 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=86=8D?= =?UTF-8?q?=E6=BC=AB=E7=94=BB=E6=BA=90=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/index.json b/index.json index 748768c..32ce99d 100644 --- a/index.json +++ b/index.json @@ -85,5 +85,11 @@ "fileName": "ykmh.js", "key": "ykmh", "version": "1.0.0" + }, + { + "name": "再漫画", + "fileName": "zaimanhua.js", + "key": "zaimanhua", + "version": "1.0.0" } ] \ No newline at end of file