diff --git a/ikmmh.js b/ikmmh.js index f3ebfc1..f2a2dfe 100644 --- a/ikmmh.js +++ b/ikmmh.js @@ -1,452 +1,440 @@ -class Ikm extends ComicSource { - // 基础配置 - name = "爱看漫"; - key = "ikmmh"; - version = "1.0.3"; - minAppVersion = "1.0.0"; - url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/ikmmh.js"; - // 常量定义 - static baseUrl = "https://ymcdnyfqdapp.ikmmh.com"; - static Mobile_UA = "Mozilla/5.0 (Linux; Android) Mobile"; - static webHeaders = { - "User-Agent": Ikm.Mobile_UA, - Accept: - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - }; - static jsonHead = { - "User-Agent": Ikm.Mobile_UA, - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", - Accept: "application/json, text/javascript, */*; q=0.01", - "Accept-Encoding": "gzip", - "X-Requested-With": "XMLHttpRequest", - }; - // 统一缩略图加载配置 - static thumbConfig = (url) => ({ - headers: { - ...Ikm.webHeaders, - Referer: Ikm.baseUrl, - }, - }); - // 账号系统 - account = { - login: async (account, pwd) => { - try { - let res = await Network.post( - `${Ikm.baseUrl}/api/user/userarr/login`, - Ikm.jsonHead, - `user=${account}&pass=${pwd}` - ); - if (res.status !== 200) - throw new Error(`登录失败,状态码:${res.status}`); - let data = JSON.parse(res.body); - if (data.code !== 0) throw new Error(data.msg || "登录异常"); - return "ok"; - } catch (err) { - throw new Error(`登录失败:${err.message}`); - } - }, - logout: () => Network.deleteCookies("ymcdnyfqdapp.ikmmh.com"), - registerWebsite: `${Ikm.baseUrl}/user/register/`, - }; - // 探索页面 - explore = [ - { - title: this.name, - type: "singlePageWithMultiPart", - load: async () => { - try { - let res = await Network.get(`${Ikm.baseUrl}/`, Ikm.webHeaders); - if (res.status !== 200) - throw new Error(`加载探索页面失败,状态码:${res.status}`); - let document = new HtmlDocument(res.body); - let parseComic = (e) => { - let title = e.querySelector("div.title").text.split("~")[0]; - let cover = e.querySelector("div.thumb_img").attributes["data-src"]; - let link = `${Ikm.baseUrl}${ - e.querySelector("a").attributes["href"] - }`; - return { - title, - cover, - id: link, - }; - }; - return { - 本周推荐: document - .querySelectorAll("div.module-good-fir > div.item") - .map(parseComic), - 今日更新: document - .querySelectorAll("div.module-day-fir > div.item") - .map(parseComic), - }; - } catch (err) { - throw new Error(`探索页面加载失败:${err.message}`); - } - }, - onThumbnailLoad: Ikm.thumbConfig, - }, - ]; - // 分类页面 - category = { - title: "爱看漫", - parts: [ - { - name: "分类", - // fixed 或者 random - // random用于分类数量相当多时, 随机显示其中一部分 - type: "fixed", - // 如果类型为random, 需要提供此字段, 表示同时显示的数量 - // randomNumber: 5, - categories: [ - "全部", - "长条", - "大女主", - "百合", - "耽美", - "纯爱", - "後宫", - "韩漫", - "奇幻", - "轻小说", - "生活", - "悬疑", - "格斗", - "搞笑", - "伪娘", - "竞技", - "职场", - "萌系", - "冒险", - "治愈", - "都市", - "霸总", - "神鬼", - "侦探", - "爱情", - "古风", - "欢乐向", - "科幻", - "穿越", - "性转换", - "校园", - "美食", - "悬疑", - "剧情", - "热血", - "节操", - "励志", - "异世界", - "历史", - "战争", - "恐怖", - "霸总", - "全部", - "连载中", - "已完结", - "全部", - "日漫", - "港台", - "美漫", - "国漫", - "韩漫", - "未分类", - ], - // category或者search - // 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画 - // 如果为search, 将进入搜索页面 - itemType: "category", - }, - { - name: "更新", - type: "fixed", - categories: [ - "星期一", - "星期二", - "星期三", - "星期四", - "星期五", - "星期六", - "星期日", - ], - itemType: "category", - categoryParams: ["1", "2", "3", "4", "5", "6", "7"], - }, - ], - enableRankingPage: false, - }; - // 分类漫画加载 - categoryComics = { - load: async (category, param, options, page) => { - try { - let res; - if (param) { - res = await Network.get( - `${Ikm.baseUrl}/update/${param}.html`, - Ikm.webHeaders - ); - if (res.status !== 200) - throw new Error(`分类请求失败,状态码:${res.status}`); - let document = new HtmlDocument(res.body); - let comics = document.querySelectorAll("li.comic-item").map((e) => ({ - title: e.querySelector("p.title").text.split("~")[0], - cover: e.querySelector("img").attributes["src"], - id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`, - subTitle: e.querySelector("span.chapter").text, - })); - return { - comics, - maxPage: 1, - }; - } else { - res = await Network.post( - `${Ikm.baseUrl}/api/comic/index/lists`, - Ikm.jsonHead, - `area=${options[1]}&tags=${encodeURIComponent(category)}&full=${ - options[0] - }&page=${page}` - ); - let resData = JSON.parse(res.body); - return { - comics: resData.data.map((e) => ({ - id: `${Ikm.baseUrl}${e.info_url}`, - title: e.name.split("~")[0], - subTitle: e.author, - cover: e.cover, - tags: e.tags, - description: e.lastchapter, - })), - maxPage: resData.end || 1, - }; - } - } catch (err) { - throw new Error(`分类加载失败:${err.message}`); - } - }, - onThumbnailLoad: Ikm.thumbConfig, - optionList: [ - { - // 对于单个选项, 使用-分割, 左侧为用于数据加载的值, 即传给load函数的options参数; 右侧为显示给用户的文本 - - options: ["3-全部", "4-连载中", "1-已完结"], - notShowWhen: [ - "星期一", - "星期二", - "星期三", - "星期四", - "星期五", - "星期六", - "星期日", - ], - showWhen: null, - }, - { - options: [ - "9-全部", - "1-日漫", - "2-港台", - "3-美漫", - "4-国漫", - "5-韩漫", - "6-未分类", - ], - notShowWhen: [ - "星期一", - "星期二", - "星期三", - "星期四", - "星期五", - "星期六", - "星期日", - ], - showWhen: null, - }, - ], - }; - // 搜索功能 - search = { - load: async (keyword, options, page) => { - try { - let res = await Network.get( - `${Ikm.baseUrl}/search?searchkey=${encodeURIComponent(keyword)}`, - Ikm.webHeaders - ); - let document = new HtmlDocument(res.body); - return { - comics: document.querySelectorAll("li.comic-item").map((e) => ({ - title: e.querySelector("p.title").text.split("~")[0], - cover: e.querySelector("img").attributes["src"], - id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`, - subTitle: e.querySelector("span.chapter").text, - })), - maxPage: 1, - }; - } catch (err) { - throw new Error(`搜索失败:${err.message}`); - } - }, - onThumbnailLoad: Ikm.thumbConfig, - optionList: [], - }; - // 收藏功能 - favorites = { - multiFolder: false, - addOrDelFavorite: async (comicId, folderId, isAdding) => { - try { - let id = comicId.match(/\d+/)[0]; - if (isAdding) { - // 获取漫画信息 - let infoRes = await Network.get(comicId, Ikm.webHeaders); - let name = new HtmlDocument(infoRes.body).querySelector( - "meta[property='og:title']" - ).attributes["content"]; - // 添加收藏 - let res = await Network.post( - `${Ikm.baseUrl}/api/user/bookcase/add`, - Ikm.jsonHead, - `articleid=${id}&articlename=${encodeURIComponent(name)}` - ); - let data = JSON.parse(res.body); - if (data.code !== "0") throw new Error(data.msg || "收藏失败"); - return "ok"; - } else { - // 删除收藏 - let res = await Network.post( - `${Ikm.baseUrl}/api/user/bookcase/del`, - Ikm.jsonHead, - `articleid=${id}` - ); - let data = JSON.parse(res.body); - if (data.code !== "0") throw new Error(data.msg || "取消收藏失败"); - return "ok"; - } - } catch (err) { - throw new Error(`收藏操作失败:${err.message}`); - } - }, - //加载收藏 - loadComics: async (page, folder) => { - let res = await Network.get( - `${Ikm.baseUrl}/user/bookcase`, - Ikm.webHeaders - ); - if (res.status !== 200) { - throw "加载收藏失败:" + res.status; - } - let document = new HtmlDocument(res.body); - return { - comics: document.querySelectorAll("div.bookrack-item").map((e) => ({ - title: e.querySelector("h3").text.split("~")[0], - subTitle: e.querySelector("p.desc").text, - cover: e.querySelector("img").attributes["src"], - id: `${Ikm.baseUrl}/book/${e.attributes["data-id"]}/`, - })), - maxPage: 1, - }; - }, - onThumbnailLoad: Ikm.thumbConfig, - }; - // 漫画详情 - comic = { - loadInfo: async (id) => { - let res = await Network.get(id, Ikm.webHeaders); - let document = new HtmlDocument(res.body); - let comicId = id.match(/\d+/)[0]; - // 获取章节数据 - let epRes = await Network.get( - `${Ikm.baseUrl}/api/comic/zyz/chapterlink?id=${comicId}`, - { - ...Ikm.jsonHead, - Referer: id, - } - ); - let epData = JSON.parse(epRes.body); - let eps = new Map(); - if (epData.data && epData.data.length > 0 && epData.data[0].list) { - epData.data[0].list.forEach((e) => { - let title = e.name; - let id = `${Ikm.baseUrl}${e.url}`; - eps.set(id, title); - }); - } else { - throw new Error(`章节数据格式异常`); - } - - let title = document.querySelector( - "div.book-hero__detail > div.title" - ).text; - let escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - let thumb = - document - .querySelector("div.coverimg") - .attributes["style"].match(/\((.*?)\)/)?.[1] || ""; - let desc = document - .querySelector("article.book-container__detail") - .text.match( - new RegExp( - `漫画名:${escapedTitle}(?:(?:[^。]*?(?:简介|漫画简介)\\s*[::]?\\s*)|(?:[^。]*?))([\\s\\S]+?)\\.\\.\\.。` - ) - ); - let intro = desc?.[1]?.trim().replace(/\s+/g, " ") || ""; - - // 获取更新日期 - let fullDateStr = document - .querySelector('meta[property="og:cartoon:update_time"]') - .attributes["content"]; // "2025-07-18 08:37:02" - let date = new Date(fullDateStr); - let year = date.getFullYear(); - let month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始,要加1 - let day = String(date.getDate()).padStart(2, "0"); - let updateTime = `${year}-${month}-${day}`; - - return new ComicDetails({ - title: title.split("~")[0], - cover: thumb, - description: intro, - updateTime: updateTime, - tags: { - 作者: [ - document - .querySelector("div.book-container__author") - .text.split("作者:")[1], - ], - 最新章节: [document.querySelector("div.update > a > em").text], - 标签: document - .querySelectorAll("div.book-hero__detail > div.tags > a") - .map((e) => e.text.trim()) - .filter((text) => text), - }, - chapters: eps, - recommend: document - .querySelectorAll("div.module-guessu > div.item") - .map((e) => ({ - title: e.querySelector("div.title").text.split("~")[0], - cover: e.querySelector("div.thumb_img").attributes["data-src"], - id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`, - })), - }); - }, - onThumbnailLoad: Ikm.thumbConfig, - loadEp: async (comicId, epId) => { - try { - let res = await Network.get(epId, Ikm.webHeaders); - let document = new HtmlDocument(res.body); - return { - images: document - .querySelectorAll("img.lazy") - .map((e) => e.attributes["data-src"]), - }; - } catch (err) { - throw new Error(`加载章节失败:${err.message}`); - } - }, - onImageLoad: (url, comicId, epId) => { - return { - url, - headers: { - ...Ikm.webHeaders, - Referer: epId, - }, - }; - }, - }; -} +class Ikm extends ComicSource { + // 基础配置 + name = "爱看漫"; + key = "ikmmh"; + version = "1.0.4"; + minAppVersion = "1.0.0"; + url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/ikmmh.js"; + // 常量定义 + static baseUrl = "https://ymcdnyfqdapp.ikmmh.com"; + static Mobile_UA = "Mozilla/5.0 (Linux; Android) Mobile"; + static webHeaders = { + "User-Agent": Ikm.Mobile_UA, + "Accept": + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + }; + static jsonHead = { + "User-Agent": Ikm.Mobile_UA, + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "Accept": "application/json, text/javascript, */*; q=0.01", + "Accept-Encoding": "gzip", + "X-Requested-With": "XMLHttpRequest", + }; + // 统一缩略图加载配置 + static thumbConfig = (url) => ({ + headers: { + ...Ikm.webHeaders, + "referer": Ikm.baseUrl, + }, + }); + // 账号系统 + account = { + login: async (account, pwd) => { + try { + let res = await Network.post( + `${Ikm.baseUrl}/api/user/userarr/login`, + Ikm.jsonHead, + `user=${account}&pass=${pwd}` + ); + if (res.status !== 200) + throw new Error(`登录失败,状态码:${res.status}`); + let data = JSON.parse(res.body); + if (data.code !== 0) throw new Error(data.msg || "登录异常"); + return "ok"; + } catch (err) { + throw new Error(`登录失败:${err.message}`); + } + }, + logout: () => Network.deleteCookies("ymcdnyfqdapp.ikmmh.com"), + registerWebsite: `${Ikm.baseUrl}/user/register/`, + }; + // 探索页面 + explore = [ + { + title: this.name, + type: "singlePageWithMultiPart", + load: async () => { + try { + let res = await Network.get(`${Ikm.baseUrl}/`, Ikm.webHeaders); + if (res.status !== 200) + throw new Error(`加载探索页面失败,状态码:${res.status}`); + let document = new HtmlDocument(res.body); + let parseComic = (e) => { + let title = e.querySelector("div.title").text.split("~")[0]; + let cover = e.querySelector("div.thumb_img").attributes["data-src"]; + let link = `${Ikm.baseUrl}${ + e.querySelector("a").attributes["href"] + }`; + return { + title, + cover, + id: link, + }; + }; + return { + "本周推荐": document + .querySelectorAll("div.module-good-fir > div.item") + .map(parseComic), + "今日更新": document + .querySelectorAll("div.module-day-fir > div.item") + .map(parseComic), + }; + } catch (err) { + throw new Error(`探索页面加载失败:${err.message}`); + } + }, + onThumbnailLoad: Ikm.thumbConfig, + }, + ]; + // 分类页面 + category = { + title: "爱看漫", + parts: [ + { + name: "更新", + type: "fixed", + categories: [ + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日", + ], + itemType: "category", + categoryParams: ["1", "2", "3", "4", "5", "6", "7"], + }, + { + name: "分类", + // fixed 或者 random + // random用于分类数量相当多时, 随机显示其中一部分 + type: "fixed", + // 如果类型为random, 需要提供此字段, 表示同时显示的数量 + // randomNumber: 5, + categories: [ + "全部", + "长条", + "大女主", + "百合", + "耽美", + "纯爱", + "後宫", + "韩漫", + "奇幻", + "轻小说", + "生活", + "悬疑", + "格斗", + "搞笑", + "伪娘", + "竞技", + "职场", + "萌系", + "冒险", + "治愈", + "都市", + "霸总", + "神鬼", + "侦探", + "爱情", + "古风", + "欢乐向", + "科幻", + "穿越", + "性转换", + "校园", + "美食", + "悬疑", + "剧情", + "热血", + "节操", + "励志", + "异世界", + "历史", + "战争", + "恐怖", + "霸总" + ], + // category或者search + // 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画 + // 如果为search, 将进入搜索页面 + itemType: "category", + } + ], + enableRankingPage: false, + }; + // 分类漫画加载 + categoryComics = { + load: async (category, param, options, page) => { + try { + let res; + if (param) { + res = await Network.get( + `${Ikm.baseUrl}/update/${param}.html`, + Ikm.webHeaders + ); + if (res.status !== 200) + throw new Error(`分类请求失败,状态码:${res.status}`); + let document = new HtmlDocument(res.body); + let comics = document.querySelectorAll("li.comic-item").map((e) => ({ + title: e.querySelector("p.title").text.split("~")[0], + cover: e.querySelector("img").attributes["src"], + id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`, + subTitle: e.querySelector("span.chapter").text, + })); + return { + comics, + maxPage: 1 + }; + } else { + res = await Network.post( + `${Ikm.baseUrl}/api/comic/index/lists`, + Ikm.jsonHead, + `area=${options[1]}&tags=${encodeURIComponent(category)}&full=${ + options[0] + }&page=${page}` + ); + let resData = JSON.parse(res.body); + return { + comics: resData.data.map((e) => ({ + id: `${Ikm.baseUrl}${e.info_url}`, + title: e.name.split("~")[0], + subTitle: e.author, + cover: e.cover, + tags: e.tags, + description: e.lastchapter, + })), + maxPage: resData.end || 1, + }; + } + } catch (err) { + throw new Error(`分类加载失败:${err.message}`); + } + }, + onThumbnailLoad: Ikm.thumbConfig, + optionList: [ + { + // 对于单个选项, 使用-分割, 左侧为用于数据加载的值, 即传给load函数的options参数; 右侧为显示给用户的文本 + + options: ["3-全部", "4-连载中", "1-已完结"], + notShowWhen: [ + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日", + ], + showWhen: null, + }, + { + options: [ + "9-全部", + "1-日漫", + "2-港台", + "3-美漫", + "4-国漫", + "5-韩漫", + "6-未分类", + ], + notShowWhen: [ + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日", + ], + showWhen: null, + }, + ], + }; + // 搜索功能 + search = { + load: async (keyword, options, page) => { + try { + let res = await Network.get( + `${Ikm.baseUrl}/search?searchkey=${encodeURIComponent(keyword)}`, + Ikm.webHeaders + ); + let document = new HtmlDocument(res.body); + return { + comics: document.querySelectorAll("li.comic-item").map((e) => ({ + title: e.querySelector("p.title").text.split("~")[0], + cover: e.querySelector("img").attributes["src"], + id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`, + subTitle: e.querySelector("span.chapter").text, + })), + maxPage: 1, + }; + } catch (err) { + throw new Error(`搜索失败:${err.message}`); + } + }, + onThumbnailLoad: Ikm.thumbConfig, + optionList: [], + }; + // 收藏功能 + favorites = { + multiFolder: false, + addOrDelFavorite: async (comicId, folderId, isAdding) => { + try { + let id = comicId.match(/\d+/)[0]; + if (isAdding) { + // 获取漫画信息 + let infoRes = await Network.get(comicId, Ikm.webHeaders); + let name = new HtmlDocument(infoRes.body).querySelector( + "meta[property='og:title']" + ).attributes["content"]; + // 添加收藏 + let res = await Network.post( + `${Ikm.baseUrl}/api/user/bookcase/add`, + Ikm.jsonHead, + `articleid=${id}&articlename=${encodeURIComponent(name)}` + ); + let data = JSON.parse(res.body); + if (data.code !== "0") throw new Error(data.msg || "收藏失败"); + return "ok"; + } else { + // 删除收藏 + let res = await Network.post( + `${Ikm.baseUrl}/api/user/bookcase/del`, + Ikm.jsonHead, + `articleid=${id}` + ); + let data = JSON.parse(res.body); + if (data.code !== "0") throw new Error(data.msg || "取消收藏失败"); + return "ok"; + } + } catch (err) { + throw new Error(`收藏操作失败:${err.message}`); + } + }, + //加载收藏 + loadComics: async (page, folder) => { + let res = await Network.get( + `${Ikm.baseUrl}/user/bookcase`, + Ikm.webHeaders + ); + if (res.status !== 200) { + throw "加载收藏失败:" + res.status; + } + let document = new HtmlDocument(res.body); + return { + comics: document.querySelectorAll("div.bookrack-item").map((e) => ({ + title: e.querySelector("h3").text.split("~")[0], + subTitle: e.querySelector("p.desc").text, + cover: e.querySelector("img").attributes["src"], + id: `${Ikm.baseUrl}/book/${e.attributes["data-id"]}/`, + })), + maxPage: 1, + }; + }, + onThumbnailLoad: Ikm.thumbConfig, + }; + // 漫画详情 + comic = { + loadInfo: async (id) => { + // 加载收藏页并判断是否收藏 + let isFavorite = false; + try { + let favorites = await this.favorites.loadComics(1, null); + isFavorite = favorites.comics.some((comic) => comic.id === id); + } catch (error) { + console.error("加载收藏页失败:", error); + } + let res = await Network.get(id, Ikm.webHeaders); + let document = new HtmlDocument(res.body); + let comicId = id.match(/\d+/)[0]; + // 获取章节数据 + let epRes = await Network.get( + `${Ikm.baseUrl}/api/comic/zyz/chapterlink?id=${comicId}`, + { + ...Ikm.jsonHead, + "referer": id, + } + ); + let epData = JSON.parse(epRes.body); + let eps = new Map(); + if (epData.data && epData.data.length > 0 && epData.data[0].list) { + epData.data[0].list.forEach((e) => { + let title = e.name; + let id = `${Ikm.baseUrl}${e.url}`; + eps.set(id, title); + }); + } else { + throw new Error(`章节数据格式异常`); + } + + let title = document.querySelector( + "div.book-hero__detail > div.title" + ).text; + let escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + let thumb = + document + .querySelector("div.coverimg") + .attributes["style"].match(/\((.*?)\)/)?.[1] || ""; + let desc = document + .querySelector("article.book-container__detail") + .text.match( + new RegExp( + `漫画名:${escapedTitle}(?:(?:[^。]*?(?:简介|漫画简介)\\s*[::]?\\s*)|(?:[^。]*?))([\\s\\S]+?)\\.\\.\\.。` + ) + ); + let intro = desc?.[1]?.trim().replace(/\s+/g, " ") || ""; + + return { + title: title.split("~")[0], + cover: thumb, + description: intro, + tags: { + "作者": [ + document + .querySelector("div.book-container__author") + .text.split("作者:")[1], + ], + "更新": [document.querySelector("div.update > a > em").text], + "标签": document + .querySelectorAll("div.book-hero__detail > div.tags > a") + .map((e) => e.text.trim()) + .filter((text) => text), + }, + chapters: eps, + recommend: document + .querySelectorAll("div.module-guessu > div.item") + .map((e) => ({ + title: e.querySelector("div.title").text.split("~")[0], + cover: e.querySelector("div.thumb_img").attributes["data-src"], + id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`, + })), + isFavorite: isFavorite, + }; + }, + onThumbnailLoad: Ikm.thumbConfig, + loadEp: async (comicId, epId) => { + try { + let res = await Network.get(epId, Ikm.webHeaders); + let document = new HtmlDocument(res.body); + return { + images: document + .querySelectorAll("img.lazy") + .map((e) => e.attributes["data-src"]), + }; + } catch (err) { + throw new Error(`加载章节失败:${err.message}`); + } + }, + onImageLoad: (url, comicId, epId) => { + return { + url, + headers: { + ...Ikm.webHeaders, + "referer": epId, + }, + }; + }, + }; +} diff --git a/index.json b/index.json index 5a24b9b..0ac63db 100644 --- a/index.json +++ b/index.json @@ -60,7 +60,7 @@ "name": "爱看漫", "fileName": "ikmmh.js", "key": "ikmmh", - "version": "1.0.3" + "version": "1.0.4" }, { "name": "少年ジャンプ+", @@ -90,7 +90,7 @@ "name": "再漫画", "fileName": "zaimanhua.js", "key": "zaimanhua", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "漫画柜", diff --git a/zaimanhua.js b/zaimanhua.js index 809b8e5..c0c0508 100644 --- a/zaimanhua.js +++ b/zaimanhua.js @@ -1,418 +1,490 @@ -/** @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 +class Zaimanhua extends ComicSource { + // 基础信息 name = "再漫画"; - - // unique id of the source key = "zaimanhua"; + version = "1.0.1"; + minAppVersion = "1.0.0"; + url = + "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/zaimanhua.js"; - 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<{document:HtmlDocument}>} - */ - async fetchHtml(url, headers = {}) { - let res = await Network.get(url, headers); - 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<{data:object}>} - */ - async fetchJson(url, headers = {}) { - let res = await Network.get(url, headers); - return JSON.parse(res.body).data; - } - - /** - * 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: id.toString(), - title: title.toString(), - subtitle: e?.authors, - tags: e?.types?.split("/"), - cover: e?.cover, - description: e?.last_update_chapter_name.toString(), - }); - } - - /** - * [Optional] init function - */ + // 初始化请求头 init() { - this.domain = "https://www.zaimanhua.com"; - this.imgBase = "https://images.zaimanhua.com"; - this.baseUrl = "https://manhua.zaimanhua.com"; + this.headers = { + "User-Agent": "Mozilla/5.0 (Linux; Android) Mobile", + "authorization": `Bearer ${this.loadData("token") || ""}`, + }; + } + // 构建 URL + buildUrl(path) { + return `https://v4api.zaimanhua.com/app/v1/${path}`; } - // explore page list + //账户管理 + account = { + login: async (username, password) => { + try { + const encryptedPwd = Convert.hexEncode( + Convert.md5(Convert.encodeUtf8(password)) + ); + const res = await Network.post( + "https://account-api.zaimanhua.com/v1/login/passwd", + { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" }, + `username=${username}&passwd=${encryptedPwd}` + ); + + const data = JSON.parse(res.body); + if (data.errno !== 0) throw new Error(data.errmsg); + + this.saveData("token", data.data.user.token); + this.headers.authorization = `Bearer ${data.data.user.token}`; + return true; + } catch (e) { + UI.showMessage(`登录失败: ${e.message}`); + throw e; + } + }, + logout: () => { + this.deleteData("token"); + }, + }; + + // 状态检查 + checkResponseStatus(res) { + if (res.status === 401) { + throw new Error("登录失效"); + } + if (res.status !== 200) { + throw new Error(`请求失败: ${res.status}`); + } + } + + // 漫画解析 + parseComic(comic) { + // const safeString = (value) => (value || "").toString().trim(); + const safeString = (value) => (value != null ? value.toString() : ""); + const resolveId = () => + [comic.comic_id, comic.id].find((id) => id && id !== "0") || ""; + const resolveTags = () => + [comic.status, ...safeString(comic.types).split("/")].filter(Boolean); + const resolveDescription = () => { + const candidates = [ + comic.description, + comic.last_update_chapter_name, + comic.last_name, + ]; + return candidates.find((text) => text) || ""; + }; + + return { + id: safeString(resolveId()), + title: comic.title || comic.name, + subTitle: comic.authors, + cover: comic.cover, + tags: resolveTags(), + description: resolveDescription(), + }; + } + + //探索页面 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?} - */ + title: "再漫画 更新", + type: "multiPageComicList", load: async (page) => { - let result = {}; - // 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, + const res = await Network.get( + this.buildUrl(`comic/update/list/0/${page}`), + this.headers + ); + const data = JSON.parse(res.body).data; + return { + comics: data.map((item) => this.parseComic(item)), }; - 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; }, }, ]; - // categories - // categories + static categoryParamMap = { + "全部": "0", + "冒险": "4", + "欢乐向": "5", + "格斗": "6", + "科幻": "7", + "爱情": "8", + "侦探": "9", + "竞技": "10", + "魔法": "11", + "神鬼": "12", + "校园": "13", + "惊悚": "14", + "其他": "16", + "四格": "17", + "亲情": "3242", + "百合": "3243", + "秀吉": "3244", + "悬疑": "3245", + "纯爱": "3246", + "热血": "3248", + "泛爱": "3249", + "历史": "3250", + "战争": "3251", + "萌系": "3252", + "宅系": "3253", + "治愈": "3254", + "励志": "3255", + "武侠": "3324", + "机战": "3325", + "音乐舞蹈": "3326", + "美食": "3327", + "职场": "3328", + "西方魔幻": "3365", + "高清单行": "4459", + "TS": "4518", + "东方": "5077", + "魔幻": "5806", + "奇幻": "5848", + "节操": "6219", + "轻小说": "6316", + "颜艺": "6437", + "搞笑": "7568", + "仙侠": "23388", + "舰娘": "7900", + "动画": "13627", + "AA": "17192", + "福瑞": "18522", + "生存": "23323", + "日常": "23388", + "画集": "30788", + "C100": "31137", + }; + + //分类页面 category = { - /// title of the category page, used to identify the page, it should be unique - title: this.name, + title: "再漫画", parts: [ { - name: "类型", + name: "排行榜", type: "fixed", - categories: [ - "全部", - "冒险", - "搞笑", - "格斗", - "科幻", - "爱情", - "侦探", - "竞技", - "魔法", - "校园", - "百合", - "耽美", - "历史", - "战争", - "宅系", - "治愈", - "仙侠", - "武侠", - "职场", - "神鬼", - "奇幻", - "生活", - "其他", - ], + categories: ["日排行", "周排行", "月排行", "总排行"], + itemType: "category", + categoryParams: ["0", "1", "2", "3"], + }, + { + name: "分类", + type: "fixed", + categories: Object.keys(Zaimanhua.categoryParamMap), + categoryParams: Object.values(Zaimanhua.categoryParamMap), 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", - ], }, ], - // 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 fil = `${this.baseUrl}/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); - - 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, - }; + if (category.includes("排行")) { + let res = await Network.get( + this.buildUrl( + `comic/rank/list?page=${page}&rank_type=${options}&by_time=${param}` + ), + this.headers + ); + return { + comics: JSON.parse(res.body).data.map((item) => + this.parseComic(item) + ), + maxPage: 10, + }; + } else { + param = Zaimanhua.categoryParamMap[category] || "0"; + let res = await Network.get( + this.buildUrl( + `comic/filter/list?status=${options[2]}&theme=${param}&zone=${options[3]}&cate=${options[1]}&sortType=${options[0]}&page=${page}&size=20` + ), + this.headers + ); + const data = JSON.parse(res.body).data; + return { + comics: data.comicList.map((item) => this.parseComic(item)), + maxPage: Math.ceil(data.totalNum / 20), + }; + } }, - // provide options for category comic loading optionList: [ { - options: ["0-全部", "3262-少年", "3263-少女", "3264-青年"], + options: ["1-更新", "2-人气"], + notShowWhen: null, + showWhen: Object.keys(Zaimanhua.categoryParamMap), }, { - options: ["0-全部", "1-故事漫画", "2-四格多格"], + options: [ + "0-全部", + "3262-少年漫画", + "3263-少女漫画", + "3264-青年漫画", + "13626-女青漫画", + ], + notShowWhen: null, + showWhen: Object.keys(Zaimanhua.categoryParamMap), }, { - options: ["0-全部", "1-连载", "2-完结"], + options: ["0-全部", "2309-连载中", "2310-已完结", "29205-短篇"], + notShowWhen: null, + showWhen: Object.keys(Zaimanhua.categoryParamMap), + }, + { + options: [ + "0-全部", + "2304-日本", + "2305-韩国", + "2306-欧美", + "2307-港台", + "2308-内地", + "8435-其他", + ], + notShowWhen: null, + showWhen: Object.keys(Zaimanhua.categoryParamMap), + }, + { + options: ["0-人气", "1-吐槽", "2-订阅"], + notshowWhen: null, + showWhen: ["日排行", "周排行", "月排行", "总排行"], }, ], }; - /// 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 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); - // log("error", "再漫画", comics); + const res = await Network.get( + this.buildUrl( + `search/index?keyword=${encodeURIComponent( + keyword + )}&page=${page}&sort=0&size=20` + ), + this.headers + ); + const data = JSON.parse(res.body).data.list; return { - comics, - maxPage, + comics: data.map((item) => this.parseComic(item)), }; }, - - // provide options for search optionList: [], }; - /// single comic related + //收藏 + favorites = { + multiFolder: false, + addOrDelFavorite: async (comicId, folderId, isAdding) => { + const path = isAdding ? "add" : "del"; + const res = await Network.get( + this.buildUrl(`comic/sub/${path}?comic_id=${comicId}`), + this.headers + ); + const data = JSON.parse(res.body); + if (data.errno !== 0) { + throw new Error(data.errmsg || "操作失败"); + } + return "ok"; + }, + loadComics: async (page) => { + try { + const res = await Network.get( + this.buildUrl(`comic/sub/list?status=0&page=${page}&size=20`), + this.headers + ); + const data = JSON.parse(res.body).data; + return { + comics: data.subList.map((item) => this.parseComic(item)) ?? [], + maxPage: Math.ceil(data.total / 20), + }; + } catch (e) { + console.error("加载收藏失败:", e); + return { comics: [], maxPage: null }; + } + }, + }; + + // 时间戳转换 + formatTimestamp(ts) { + const date = new Date(ts * 1000); + return date.toISOString().split("T")[0]; + } + + //漫画详情 comic = { - /** - * load comic info - * @param id {string} - * @returns {Promise} - */ 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, + const getFavoriteStatus = async (id) => { + let res = await Network.get( + this.buildUrl(`comic/sub/checkIsSub?objId=${id}&source=1`), + this.headers + ); + this.checkResponseStatus(res); + return JSON.parse(res.body).data.isSub; }; - 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 results = await Promise.all([ + Network.get( + this.buildUrl(`comic/detail/${id}?channel=android`), + this.headers + ), + getFavoriteStatus.bind(this)(id), + ]); + const response = JSON.parse(results[0].body); + if (response.errno !== 0) throw new Error(response.errmsg || "加载失败"); + const data = response.data.data; - // 修复时间戳转换问题 - 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[0].data.forEach((e) => { - chapters.set(e.chapter_id.toString(), e.chapter_title); - }); - // chapters 按照key排序 - let chaptersSorted = new Map([...chapters].sort((a, b) => a[0] - b[0])); - - // 获取推荐漫画 - 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.data.comicList.map((e) => this.parseJsonComic(e)); - let tags = { - 状态: [info.status], - 类型: [info.readerGroup, ...info.types.split("/")], - 点击: [info.hitNumStr.toString()], - 订阅: [info.subNumStr], - }; - - return new ComicDetails({ - title, - subtitle: author, - cover, - description, - tags, - chapters: chaptersSorted, - recommend, - updateTime, - }); - }, - - /** - * load images of a chapter - * @param comicId {string} - * @param epId {string?} - * @returns {Promise<{images: string[]}>} - */ - loadEp: async (comicId, epId) => { - 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; + function processChapters(groups) { + return (groups || []).reduce((result, group) => { + const groupTitle = group.title || "默认"; + const chapters = (group.data || []) + .reverse() + .map((ch) => [ + String(ch.chapter_id), + `${ch.chapter_title.replace( + /^(?:连载版?)?(\d+\.?\d*)([话卷])?$/, + (_, n, t) => `第${n}${t || "话"}` + )}`, + ]); + result.set(groupTitle, new Map(chapters)); + return result; + }, new Map()); + } + // 分类标签 + const { authors, status, types } = data; + const tagMapper = (arr) => arr.map((t) => t.tag_name); return { - images: info.page_url, + title: data.title, + cover: data.cover, + description: data.description, + tags: { + "作者": tagMapper(authors), + "状态": [...tagMapper(status), data.last_update_chapter_name], + "标签": tagMapper(types), + }, + updateTime: this.formatTimestamp(data.last_updatetime), + chapters: processChapters(data.chapters), + isFavorite: results[1], + subId: id, }; }, - /** - * [Optional] provide configs for an image loading - * @param url - * @param comicId - * @param epId - * @returns {ImageLoadingConfig | Promise} - */ - onImageLoad: (url, comicId, epId) => { - return {}; + loadEp: async (comicId, epId) => { + const res = await Network.get( + this.buildUrl(`comic/chapter/${comicId}/${epId}`) + ); + const data = JSON.parse(res.body).data.data; + return { images: data.page_url_hd || data.page_url }; }, - /** - * [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 {}; + + loadComments: async (comicId, subId, page, replyTo) => { + try { + // 构建请求URL + const url = this.buildUrl( + `comment/list?page=${page}&size=30&type=4&objId=${ + subId || comicId + }&sortBy=1` + ); + const res = await Network.get(url, this.headers); + this.checkResponseStatus(res); + + const response = JSON.parse(res.body); + const data = response.data; + + /* 空数据检查 */ + if (!data || !data.commentIdList || !data.commentList) { + UI.showMessage("暂时没有评论,快来发表第一条吧~"); + return { comments: [], maxPage: 0 }; + } + + /* 处理评论ID列表 */ + // 标准化ID数组:处理null/字符串/数组等多种情况 + const rawIds = Array.isArray(data.commentIdList) + ? data.commentIdList + : []; + + // 展开所有ID并过滤无效值 + const allCommentIds = rawIds + .map((idStr) => `${idStr || ""}`.split(",")) // 转换为字符串再分割 + .flat() + .filter((id) => id.trim() !== ""); + + // 最终ID处理流程 + const processComments = () => { + // 去重并验证ID有效性 + const validIds = [...new Set(allCommentIds)].filter((id) => + data.commentList.hasOwnProperty(id) + ); + + // 过滤回复评论 + const filteredIds = replyTo + ? validIds.filter( + (id) => data.commentList[id]?.to_comment_id == replyTo + ) + : validIds; + + // 转换为评论对象 + return filteredIds.map((id) => { + const comment = data.commentList[id]; + return new Comment({ + userName: comment.nickname || "匿名用户", + avatar: comment.photo || "", + content: comment.content || "[内容已删除]", + time: this.formatTimestamp(comment.create_time), + replyCount: comment.reply_amount || 0, + score: comment.like_amount || 0, + id: String(id), + parentId: comment.to_comment_id || null, + }); + }); + }; + + // 当没有有效评论时显示提示 + const comments = processComments(); + if (comments.length === 0) { + UI.showMessage(replyTo ? "该评论暂无回复" : "这里还没有评论哦~"); + } + + return { + comments: comments, + maxPage: Math.ceil((data.total || 0) / 30), + }; + } catch (e) { + console.error("评论加载失败:", e); + UI.showMessage(`加载评论失败: ${e.message}`); + return { comments: [], maxPage: 0 }; + } + }, + + // 发送评论, 返回任意值表示成功. + sendComment: async (comicId, subId, content, replyTo) => { + if (!replyTo) { + replyTo = 0; + } + let res = await Network.post( + this.buildUrl(`comment/add`), + { + ...this.headers, + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", + }, + `obj_id=${subId}&content=${encodeURIComponent( + content + )}&to_comment_id=${replyTo}&type=4` + ); + this.checkResponseStatus(res); + let response = JSON.parse(res.body); + if (response.errno !== 0) throw new Error(response.errmsg || "加载失败"); + return "ok"; + }, + // 点赞 + likeComment: async (comicId, subId, commentId, isLike) => { + let res = await Network.post( + this.buildUrl(`comment/addLike`), + { + ...this.headers, + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", + }, + `commentId=${commentId}&type=4` + ); + this.checkResponseStatus(res); + return "ok"; }, }; }