diff --git a/baihehui.js b/baihehui.js
new file mode 100644
index 0000000..71c1b25
--- /dev/null
+++ b/baihehui.js
@@ -0,0 +1,673 @@
+/** @type {import('./_venera_.js')} */
+class Baihehui 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 = "baihehui"
+
+ version = "1.0.0"
+
+ minAppVersion = "1.4.0"
+
+ // update url
+ url = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/baihehui.js"
+
+ settings = {
+ domains: {
+ title: "主页源",
+ type: "select",
+ options: [
+ { value: "yamibo.com" },
+ ],
+ default: "yamibo.com"
+ },
+ }
+
+ get baseUrl() {
+ return `https://www.${this.loadSetting('domains')}`;
+ }
+
+ /**
+ * [Optional] init function
+ */
+ init() {
+
+ }
+
+ account = {
+ login: async (username, password) => {
+ Network.deleteCookies("https://www.yamibo.com");
+ // 1. GET 登录页,保存 PHPSESSID 和 _csrf-frontend
+ let resGet = await Network.get("https://www.yamibo.com/user/login", {
+ headers: { "User-Agent": "Mozilla/5.0" }
+ });
+ if (resGet.status !== 200) throw "无法打开登录页";
+
+ // 1.1 提取并保存 GET 返回的 Set-Cookie
+ let sc1 = resGet.headers["set-cookie"] || resGet.headers["Set-Cookie"] || [];
+ let initialCookies = [];
+ for (let line of Array.isArray(sc1) ? sc1 : [sc1]) {
+ let [pair] = line.split(";");
+ let [name, value] = pair.split("=");
+ name = name.trim(); value = value.trim();
+ if (name === "PHPSESSID" || name === "_csrf-frontend") {
+ initialCookies.push(new Cookie({ name, value, domain: "www.yamibo.com" }));
+ }
+ }
+ Network.setCookies("https://www.yamibo.com", initialCookies);
+
+ // 2. 解析 CSRF token
+ let doc = new HtmlDocument(resGet.body);
+ let csrf = doc
+ .querySelector('meta[name="csrf-token"]')
+ .attributes.content;
+ doc.dispose();
+
+ // 3. 构造编码后的表单
+ let form = [
+ `_csrf-frontend=${encodeURIComponent(csrf)}`,
+ `LoginForm%5Busername%5D=${encodeURIComponent(username)}`,
+ `LoginForm%5Bpassword%5D=${encodeURIComponent(password)}`,
+ 'LoginForm%5BrememberMe%5D=0',
+ 'LoginForm%5BrememberMe%5D=1',
+ `login-button=${encodeURIComponent("登录")}`
+ ].join("&");
+
+ // 4. POST 登录(会自动带上刚才的 Cookie)
+ let resPost = await Network.post(
+ "https://www.yamibo.com/user/login",
+ {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Referer": "https://www.yamibo.com/user/login",
+ "User-Agent": "Mozilla/5.0"
+ },
+ form
+ );
+ if (resPost.status === 400) throw "登录失败";
+ Network.deleteCookies("https://www.yamibo.com");
+
+ // …account.login 中 POST 后提取 Cookie 部分…
+ let raw = resPost.headers["set-cookie"] || resPost.headers["Set-Cookie"];
+ if (!raw) throw "未收到任何 Cookie";
+
+ // 1. 将单条字符串按“逗号+Cookie名=”拆分
+ let parts = Array.isArray(raw)
+ ? raw
+ : raw.split(/,(?=\s*(?:PHPSESSID|_identity-frontend|_csrf-frontend)=)/);
+
+ // 2. 提取目标 Cookie
+ const names = ["PHPSESSID", "_identity-frontend", "_csrf-frontend"];
+ let cookies = parts.map(line => {
+ let [pair] = line.split(";");
+ let [k, v] = pair.split("=");
+ k = k.trim(); v = v.trim();
+ if (names.includes(k)) return new Cookie({ name: k, value: v, domain: "www.yamibo.com" });
+ }).filter(Boolean);
+
+ // 3. 验证并保存
+ if (cookies.length !== names.length) {
+ throw "登录未返回完整 Cookie,实际:" + cookies.map(c => c.name).join(",");
+ }
+ Network.setCookies("https://www.yamibo.com", cookies);
+
+ return true;
+ },
+
+ logout: () => {
+ Network.deleteCookies("https://www.yamibo.com");
+ },
+
+ registerWebsite: "https://www.yamibo.com/user/signup"
+ }
+
+
+ static category_types = {
+ "全部作品": "manga/list@a@?",
+ "原创": "manga/list?q=4@a@&",
+ "同人": "manga/list?q=6@a@&",
+ }
+
+ static article_types = {
+ "翻页漫画": "search/type?type=3&tag=@b@翻页漫画",
+ "条漫": "search/type?type=3&tag=@b@条漫",
+ "四格": "search/type?type=3&tag=@b@四格",
+ "绘本": "search/type?type=3&tag=@b@绘本",
+ "杂志": "search/type?type=3&tag=@b@杂志",
+ "合志": "search/type?type=3&tag=@b@合志",
+ }
+
+ static relate_types = {
+ "编辑推荐": "manga/rcmds?type=3012@c@&",
+ "最近更新": "manga/latest@c@?",
+ "原创推荐": "manga/rcmds?type=3014@c@&",
+ "同人推荐": "manga/rcmds?type=3015@c@&",
+ }
+
+
+
+
+// explore page list
+explore = [
+ {
+ title: "百合会",
+ type: "singlePageWithMultiPart",
+ load: async (page) => {
+ // 1. 拿到 HTML
+ let res = await Network.get("https://www.yamibo.com/site/manga");
+ if (res.status !== 200) {
+ throw `Invalid status code: ${res.status}`;
+ }
+
+ // 2. 解析文档
+ let doc = new HtmlDocument(res.body);
+
+ // 3. 通用解析单元函数
+ function parseItem(el) {
+ let a = el.querySelector(".media-img") || el.querySelector("a.media-img");
+ let href = a.attributes.href;
+ let id = href.match(/\/manga\/(\d+)/)[1];
+ // 从 style 中提取 url
+ let style = a.attributes.style || "";
+ let cover = `https://www.yamibo.com/coverm/000/000/${id}.jpg`;
+ let title = el.querySelector("h3 a").text.trim();
+ return new Comic({ id, title, cover });
+ }
+
+ // 4. 抓「编辑推荐」
+ let editor = [];
+ let editorEls = doc.querySelectorAll(".recommend-list .media-cell.horizontal");
+ for (let el of editorEls) {
+ editor.push(parseItem(el));
+ }
+
+ // 5. 抓「最近更新」
+ let latest = [];
+ // 找到标题元素,再拿其后面的
下的 .media-cell.vertical
+ let latestTitle = doc.querySelectorAll("h2.module-title")
+ .find(e => e.text.includes("最近更新"));
+ if (latestTitle) {
+ let ul = latestTitle.nextElementSibling;
+ if (ul) {
+ let items = ul.querySelectorAll(".media-cell.vertical");
+ for (let el of items) latest.push(parseItem(el));
+ }
+ }
+
+ // 原创推荐
+ let original = [];
+ let originalTitle = doc.querySelectorAll("h2.module-title")
+ .find(e => e.text.includes("原创推荐"));
+ if (originalTitle) {
+ let ul = originalTitle.nextElementSibling;
+ if (ul) {
+ let items = ul.querySelectorAll(".media-cell.vertical");
+ for (let el of items) original.push(parseItem(el));
+ }
+ }
+
+ // 6. 抓「同人推荐」
+ let fan = [];
+ let fanTitle = doc.querySelectorAll("h2.module-title")
+ .find(e => e.text.includes("同人推荐"));
+ if (fanTitle) {
+ let ul = fanTitle.nextElementSibling;
+ if (ul) {
+ let items = ul.querySelectorAll(".media-cell.vertical");
+ for (let el of items) fan.push(parseItem(el));
+ }
+ }
+
+ // 7. 清理并返回
+ doc.dispose();
+ return {
+ "编辑推荐": editor,
+ "最近更新": latest,
+ "原创推荐": original,
+ "同人推荐": fan
+ };
+ }
+ }
+];
+
+ // categories
+ category = {
+ /// title of the category page, used to identify the page, it should be unique
+ title: "百合会",
+ parts: [
+ {
+ name: "分类",
+ type: "fixed",
+ categories: Object.keys(Baihehui.category_types),
+ itemType: "category",
+ categoryParams: Object.values(Baihehui.category_types),
+ },
+ {
+ name: "作品类型(需要登陆)",
+ type: "fixed",
+ categories: Object.keys(Baihehui.article_types),
+ itemType: "category",
+ categoryParams: Object.values(Baihehui.article_types),
+ },
+ {
+ name: "更多推荐",
+ type: "fixed",
+ categories: Object.keys(Baihehui.relate_types),
+ itemType: "category",
+ categoryParams: Object.values(Baihehui.relate_types),
+ },
+ ],
+ // enable ranking page
+ enableRankingPage: false,
+ }
+
+ /// category comic loading related
+ categoryComics = {
+ load: async (category, params, options, page) => {
+ let param = params.split('@')[0];
+ let type = params.split('@')[1];
+ let type_options = params.split('@')[2];
+ let url = ""
+ if (type == "b") {
+ url = `${this.baseUrl}/${param}${encodeURIComponent(type_options)}&sort=updated_at`;
+ url += `&page=${page}&per-page=50`;
+ } else {
+ url = `${this.baseUrl}/${param}${type_options}sort=updated_at`;
+ url += `&page=${page}&per-page=50`;
+ }
+
+ // 发起请求
+ let res = await Network.get(url, {
+ headers: { "User-Agent": "Mozilla/5.0" }
+ });
+ if (res.status !== 200) {
+ throw `Invalid status code: ${res.status}`;
+ }
+
+
+ // 解析 HTML
+ let document = new HtmlDocument(res.body);
+
+ // 获取最大页数
+ let lastPageElement = document.querySelector('li.last > a');
+ let maxPage = lastPageElement ? parseInt(lastPageElement.attributes['data-page']) + 1 : 1;
+
+
+ // 分类解析、
+ if (type == "a") {
+ let mangaList = [];
+ // 获取所有漫画行
+ let rows = document.querySelectorAll('tr[data-key]');
+
+ rows.forEach(row => {
+ // 提取信息
+ let href = row.querySelector('a').attributes['href'];
+ // 提取最后的数字作为 id
+ let rawId = href.match(/\/manga\/(\d+)$/)[1];
+
+ // 补零处理 - 确保id是3位数
+ let id = rawId.padStart(3, '0');
+ let title = row.querySelector('a').text;
+ let author = row.querySelectorAll('td')[2].text;
+
+ // 获取标签
+ let tags = [
+ row.querySelectorAll('td')[4].text, // 作品分类(原创/同人)
+ row.querySelectorAll('td')[5].text // 连载状态
+ ];
+
+ // 获取更新时间作为描述
+ let updateTime = row.querySelectorAll('td')[8].text;
+
+ // 构建漫画对象
+ let manga = {
+ id: id,
+ title: title,
+ cover: `https://www.yamibo.com/coverm/000/000/${id}.jpg`, // 默认封面
+ tags: tags,
+ description: `更新于: ${updateTime}`
+ };
+
+ mangaList.push(manga);
+ });
+
+ return {
+ comics: mangaList,
+ maxPage: maxPage // 从分页信息可以看出总共5页
+ };
+ } else if (type == "b") {
+ let mangaList = [];
+ // 获取所有漫画行
+ let rows = document.querySelectorAll('tr[data-key]');
+ rows.forEach(row => {
+ // 提取信息
+ let href = row.querySelector('a').attributes['href'];
+ // 提取最后的数字作为 id
+ let rawId = href.match(/\/manga\/(\d+)$/)[1];
+ // 补零处理 - 确保id是3位数
+ let id = rawId.padStart(3, '0');
+ let title = row.querySelector('a').text;
+ let author = row.querySelectorAll('td')[2].text;
+
+ // 获取标签
+ let tags = [
+ row.querySelectorAll('td')[3].text.replace(/\[|\]/g, ''), // 作品分类 (去掉方括号)
+ row.querySelectorAll('td')[4].text // 连载状态
+ ];
+
+ // 获取更新时间作为描述
+ let updateTime = row.querySelectorAll('td')[6].text;
+
+ // 构建封面 URL
+ let cover = `https://www.yamibo.com/coverm/000/000/${id}.jpg`;
+
+ // 构建漫画对象
+ let manga = {
+ id: id,
+ title: title,
+ cover: cover, // 使用有效封面或默认封面
+ tags: tags,
+ description: `${updateTime}`
+ };
+
+ mangaList.push(manga);
+ });
+
+ return {
+ comics: mangaList,
+ maxPage: maxPage // 从分页信息可以看出总共8页
+ };
+ } else {
+ let mangaList = [];
+ // 获取所有漫画行
+ let rows = document.querySelectorAll('tr[data-key]');
+ rows.forEach(row => {
+ // 提取信息
+ let href = row.querySelector('a').attributes['href'];
+ // 提取最后的数字作为 id
+ let rawId = href.match(/\/manga\/(\d+)$/)[1];
+ // 补零处理 - 确保id是3位数
+ let id = rawId.padStart(3, '0');
+ let title = row.querySelector('a').text;
+
+ // 获取更新时间作为描述
+ let updateTime = row.querySelector('td:last-child').text.trim();
+
+ // 构建封面 URL
+ let cover = `https://www.yamibo.com/coverm/000/000/${id}.jpg`;
+
+ // 构建漫画对象
+ let manga = {
+ id: id,
+ title: title,
+ cover: cover, // 使用有效封面或默认封面
+ tags: [],
+ description: `更新于: ${updateTime}`
+ };
+
+ mangaList.push(manga);
+ });
+
+ return {
+ comics: mangaList,
+ maxPage: maxPage // 从分页信息可以看出总共8页
+ };
+ }
+ }
+ }
+
+ /// 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 = `https://www.yamibo.com/search/manga?SearchForm%5Bkeyword%5D=${encodeURIComponent(keyword)}&page=${page}`;
+ let res = await Network.get(url, {
+ headers: {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0"
+ }
+ });
+
+ if (res.status !== 200) {
+ throw `Invalid status code: ${res.status}`;
+ }
+
+ let document = new HtmlDocument(res.body);
+ // 获取最大页数
+ let lastPageElement = document.querySelector('li.last > a');
+ let maxPage = lastPageElement ? parseInt(lastPageElement.attributes['data-page']) + 1 : 1;
+ // 提取漫画列表
+ let mangaList = [];
+ // 获取所有漫画行
+ let rows = document.querySelectorAll('tr[data-key]');
+ rows.forEach(row => {
+ // 提取信息
+ let href = row.querySelector('a').attributes['href'];
+ // 提取最后的数字作为 id
+ let rawId = href.match(/\/manga\/(\d+)$/)[1];
+ // 补零处理 - 确保id是3位数
+ let id = rawId.padStart(3, '0');
+ let title = row.querySelector('a').text;
+
+ // 获取更新时间作为描述
+ let updateTime = row.querySelector('td:last-child').text.trim();
+
+ // 构建封面 URL
+ let cover = `https://www.yamibo.com/coverm/000/000/${id}.jpg`;
+
+ // 构建漫画对象
+ let manga = {
+ id: id,
+ title: title,
+ cover: cover, // 使用有效封面或默认封面
+ tags: [],
+ description: `更新于: ${updateTime}`
+ };
+
+ mangaList.push(manga);
+ });
+
+ return {
+ comics: mangaList,
+ maxPage: maxPage // 从分页信息可以看出总共8页
+ };
+ },
+
+ /**
+ * 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: [],
+
+ // enable tags suggestions
+ enableTagsSuggestions: false,
+ }
+
+ /// single comic related
+ comic = {
+ /**
+ * load comic info
+ * @param id {string}
+ * @returns {Promise}
+ */
+ loadInfo: async (id) => {
+ let res = await Network.get(`${this.baseUrl}/manga/${id}`);
+ if (res.status !== 200) {
+ throw `Invalid status code: ${res.status}`;
+ }
+
+ let document = new HtmlDocument(res.body);
+
+ // 提取漫画标题
+ let title = document.querySelector("h3.col-md-12").text.trim();
+
+ // 提取封面图片
+ let cover = "https://www.yamibo.com/coverm/000/000/" + id + ".jpg";
+
+ // 提取作者信息
+ let author = "";
+ document.querySelectorAll("p").forEach(p => {
+ if (p.text.includes("作者:")) {
+ author = p.text.replace("作者:", "").trim();
+ }
+ });
+
+ // 提取标签
+ let tags = [];
+ document.querySelectorAll("a.label.label-ntype").forEach(tag => {
+ tags.push(tag.text.trim());
+ });
+
+ // 提取更新时间
+ let updateTime = "";
+ document.querySelectorAll("p").forEach(p => {
+ if (p.text.includes("更新时间:")) {
+ updateTime = p.text.replace("更新时间:", "").trim();
+ }
+ });
+
+ // 提取简介
+ //let description = document.querySelector("div.panel-body > div.panel-collapse > div.panel-body").text.trim();
+ let description = "";
+
+ // 提取章节信息
+ let chapters = new Map();
+ document.querySelectorAll("div[data-key]").forEach(chapter => {
+ let chapterKey = chapter.attributes['data-key']; // 获取 data-key 值
+ let chapterTitle = chapter.querySelector("a").text.trim(); // 获取章节标题
+ chapters.set(chapterKey, chapterTitle); // 将 data-key 和章节标题存入 Map
+ });
+
+ return {
+ title: title,
+ cover: cover,
+ description: description,
+ tags: {
+ "作者": [author],
+ "更新": [updateTime],
+ "标签": tags
+ },
+ chapters: chapters
+ };
+
+ },
+ loadComments: async (comicId, subId, page, replyTo) => {
+ let url = `${this.baseUrl}/manga/${comicId}?dp-1-page=${page}`;
+ let res = await Network.get(url, {
+ headers: {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0"
+ }
+ });
+
+ if (res.status !== 200) {
+ throw `Invalid status code: ${res.status}`;
+ }
+
+ let document = new HtmlDocument(res.body);
+
+ // 提取评论总数
+ let totalCommentsMatch = document.querySelector("div.panel-body").text.match(/共(\d+)篇/);
+ let totalComments = totalCommentsMatch ? parseInt(totalCommentsMatch[1]) : 0;
+
+ // 提取评论列表
+ let comments = [];
+ document.querySelectorAll("div.post.row").forEach(post => {
+ let userName = post.querySelector("span.cmt-username > a").text.trim();
+ let avatar = "https://www.yamibo.com/" + post.querySelector("a > img.cmt-avatar").attributes['src'];
+ let content = post.querySelector("div.row > p").text.trim();
+ let time = post.querySelector("span.description").text.replace("在 ", "").trim();
+ let replyCountMatch = post.querySelector("a.btn.btn-sm").text.match(/(\d+) 条回复/);
+ let replyCount = replyCountMatch ? parseInt(replyCountMatch[1]) : 0;
+ let id = post.querySelector("button.btn_reply").attributes['pid'];
+
+ comments.push({
+ userName: userName,
+ avatar: avatar,
+ content: content,
+ time: time,
+ replyCount: replyCount,
+ id: id
+ });
+ });
+
+ // 计算最大页数
+ let maxPageElement = document.querySelector("li.last > a");
+ let maxPage = maxPageElement ? parseInt(maxPageElement.attributes['data-page']) + 1 : 1;
+
+ return {
+ comments: comments,
+ totalComments: totalComments,
+ maxPage: maxPage
+ };
+ },
+
+ loadEp: async (comicId, epId) => {
+ let baseUrl = `https://www.yamibo.com/manga/view-chapter?id=${epId}`;
+ let res = await Network.get(`${baseUrl}&page=1`, {
+ headers: {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0"
+ }
+ });
+
+ if (res.status !== 200) {
+ throw `Invalid status code: ${res.status}`;
+ }
+
+ let document = new HtmlDocument(res.body);
+
+ // 提取最大页数
+ let lastPageElement = document.querySelector("li.last > a");
+ let maxPage = lastPageElement ? parseInt(lastPageElement.attributes['data-page']) + 1 : 1;
+
+ let images = [];
+
+ // 循环抓取所有页面的图片
+ for (let page = 1; page <= maxPage; page++) {
+ let pageUrl = `${baseUrl}&page=${page}`;
+ let pageRes = await Network.get(pageUrl, {
+ headers: {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0"
+ }
+ });
+
+ if (pageRes.status !== 200) {
+ throw `Invalid status code on page ${page}: ${pageRes.status}`;
+ }
+
+ let pageDocument = new HtmlDocument(pageRes.body);
+
+ // 提取图片 URL
+ let imageElement = pageDocument.querySelector("img#imgPic");
+ if (!imageElement) {
+ throw `Image not found on page ${page}.`;
+ }
+ let imageUrl = imageElement.attributes['src'];
+ images.push(imageUrl);
+ }
+
+ return {
+ images: images, // 所有页面的图片 URL
+ maxPage: maxPage
+ };
+ },
+
+ // enable tags translate
+ enableTagsTranslate: false,
+ }
+}
\ No newline at end of file
diff --git a/comick.js b/comick.js
index e6010b4..a58a597 100644
--- a/comick.js
+++ b/comick.js
@@ -1,7 +1,7 @@
class Comick extends ComicSource {
name = "comick"
key = "comick"
- version = "1.0.0"
+ version = "1.0.1"
minAppVersion = "1.4.0"
// update url
url = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/comick.js"
@@ -16,25 +16,22 @@ class Comick extends ComicSource {
],
default: "preview.comick.io"
},
- // language: {
- // title: "标题语言",
- // type: "select",
- // options: [
- // {
- // value: '中文',
- // text: 'zh',
- // },
- // {
- // value: '韩文',
- // text: 'ko',
- // },
- // {
- // value: '英文',
- // text: 'en',
- // },
- // ],
- // default: 'en',
- // },
+ lang_len: {
+ title: "最大语言数量(不建议大于5)",
+ type: "select",
+ options: [
+ {value: "1"},
+ {value: "2"},
+ {value: "3"},
+ {value: "4"},
+ {value: "5"},
+ {value: "8"},
+ {value: "10"},
+ {value: "15"},
+ {value: "20"},
+ ],
+ default: "3"
+ },
}
get baseUrl() {
@@ -129,6 +126,229 @@ class Comick extends ComicSource {
"sexual-violence": "性暴力",
"smut": "肉欲",
}
+ static reversed_category_param_dict = {
+ "浪漫": "romance",
+ "喜剧": "comedy",
+ "剧情": "drama",
+ "奇幻": "fantasy",
+ "日常": "slice-of-life",
+ "动作": "action",
+ "冒险": "adventure",
+ "心理": "psychological",
+ "悬疑": "mystery",
+ "历史": "historical",
+ "悲剧": "tragedy",
+ "科幻": "sci-fi",
+ "恐怖": "horror",
+ "异世界": "isekai",
+ "运动": "sports",
+ "惊悚": "thriller",
+ "机甲": "mecha",
+ "哲学": "philosophical",
+ "武侠": "wuxia",
+ "医疗": "medical",
+ "魔法少女": "magical-girls",
+ "超级英雄": "superhero",
+ "少年爱": "shounen-ai",
+ "成年": "mature",
+ "性转": "gender-bender",
+ "少女爱": "shoujo-ai",
+ "单篇": "oneshot",
+ "网络漫画": "web-comic",
+ "同人志": "doujinshi",
+ "全彩": "full-color",
+ "长条": "long-strip",
+ "改编": "adaptation",
+ "选集": "anthology",
+ "四格": "4-koma",
+ "用户创作": "user-created",
+ "获奖": "award-winning",
+ "官方上色": "official-colored",
+ "粉丝上色": "fan-colored",
+ "校园生活": "school-life",
+ "超自然": "supernatural",
+ "魔法": "magic",
+ "怪物": "monsters",
+ "武术": "martial-arts",
+ "动物": "animals",
+ "恶魔": "demons",
+ "后宫": "harem",
+ "转生": "reincarnation",
+ "上班族": "office-workers",
+ "生存": "survival",
+ "军事": "military",
+ "女装": "crossdressing",
+ "萝莉": "loli",
+ "正太": "shota",
+ "百合": "yuri",
+ "耽美": "yaoi",
+ "电子游戏": "video-games",
+ "魔物娘": "monster-girls",
+ "不良少年": "delinquents",
+ "幽灵": "ghosts",
+ "时间旅行": "time-travel",
+ "烹饪": "cooking",
+ "警察": "police",
+ "外星人": "aliens",
+ "音乐": "music",
+ "黑帮": "mafia",
+ "吸血鬼": "vampires",
+ "武士": "samurai",
+ "后末日": "post-apocalyptic",
+ "辣妹": "gyaru",
+ "恶役千金": "villainess",
+ "逆后宫": "reverse-harem",
+ "忍者": "ninja",
+ "僵尸": "zombies",
+ "传统游戏": "traditional-games",
+ "虚拟现实": "virtual-reality",
+ "成人": "adult",
+ "情色": "ecchi",
+ "性暴力": "sexual-violence",
+ "肉欲": "smut"
+ }
+ static language_dict = {
+ 'en': '英文',
+ 'pt-br': '巴西葡萄牙文',
+ 'es-419': '拉丁美洲西班牙文',
+ 'ru': '俄文',
+ 'vi': '越南文',
+ 'fr': '法文',
+ 'pl': '波兰文',
+ 'id': '印度尼西亚文',
+ 'tr': '土耳其文',
+ 'it': '意大利文',
+ 'es': '西班牙文',
+ 'uk': '乌克兰文',
+ 'ar': '阿拉伯文',
+ 'zh-hk': '繁体中文',
+ 'hu': '匈牙利文',
+ 'zh': '中文',
+ 'de': '德文',
+ 'ko': '韩文',
+ 'th': '泰文',
+ 'bg': '保加利亚文',
+ 'ca': '加泰罗尼亚文',
+ 'fa': '波斯文',
+ 'ro': '罗马尼亚文',
+ 'cs': '捷克文',
+ 'mn': '蒙古文',
+ 'he': '希伯来文',
+ 'pt': '葡萄牙文',
+ 'hi': '印地文',
+ 'tl': '他加禄文',
+ 'fi': '芬兰文',
+ 'ms': '马来文',
+ 'eu': '巴斯克文',
+ 'kk': '哈萨克文',
+ 'sr': '塞尔维亚文',
+ 'my': '缅甸文',
+ 'el': '希腊文',
+ 'nl': '荷兰文',
+ 'ja': '日文',
+ 'uz': '乌兹别克文',
+ 'eo': '世界语',
+ 'bn': '孟加拉文',
+ 'lt': '立陶宛文',
+ 'ka': '格鲁吉亚文',
+ 'da': '丹麦文',
+ 'ta': '泰米尔文',
+ 'sv': '瑞典文',
+ 'be': '白俄罗斯文',
+ 'cv': '楚瓦什文',
+ 'hr': '克罗地亚文',
+ 'la': '拉丁文',
+ 'ne': '尼泊尔文',
+ 'ur': '乌尔都文',
+ 'gl': '加利西亚文',
+ 'no': '挪威文',
+ 'sq': '阿尔巴尼亚文',
+ 'ga': '爱尔兰文',
+ 'te': '泰卢固文',
+ 'jv': '爪哇文',
+ 'sl': '斯洛文尼亚文',
+ 'et': '爱沙尼亚文',
+ 'az': '阿塞拜疆文',
+ 'sk': '斯洛伐克文',
+ 'af': '南非荷兰文',
+ 'lv': '拉脱维亚文'
+ }
+ static reversed_language_dict = {
+ '英文': 'en',
+ '巴西葡萄牙文': 'pt-br',
+ '拉丁美洲西班牙文': 'es-419',
+ '俄文': 'ru',
+ '越南文': 'vi',
+ '法文': 'fr',
+ '波兰文': 'pl',
+ '印度尼西亚文': 'id',
+ '土耳其文': 'tr',
+ '意大利文': 'it',
+ '西班牙文': 'es',
+ '乌克兰文': 'uk',
+ '阿拉伯文': 'ar',
+ '香港繁体中文': 'zh-hk',
+ '匈牙利文': 'hu',
+ '中文': 'zh',
+ '德文': 'de',
+ '韩文': 'ko',
+ '泰文': 'th',
+ '保加利亚文': 'bg',
+ '加泰罗尼亚文': 'ca',
+ '波斯文': 'fa',
+ '罗马尼亚文': 'ro',
+ '捷克文': 'cs',
+ '蒙古文': 'mn',
+ '希伯来文': 'he',
+ '葡萄牙文': 'pt',
+ '印地文': 'hi',
+ '菲律宾文/他加禄文': 'tl',
+ '芬兰文': 'fi',
+ '马来文': 'ms',
+ '巴斯克文': 'eu',
+ '哈萨克文': 'kk',
+ '塞尔维亚文': 'sr',
+ '缅甸文': 'my',
+ '希腊文': 'el',
+ '荷兰文': 'nl',
+ '日文': 'ja',
+ '乌兹别克文': 'uz',
+ '世界语': 'eo',
+ '孟加拉文': 'bn',
+ '立陶宛文': 'lt',
+ '格鲁吉亚文': 'ka',
+ '丹麦文': 'da',
+ '泰米尔文': 'ta',
+ '瑞典文': 'sv',
+ '白俄罗斯文': 'be',
+ '楚瓦什文': 'cv',
+ '克罗地亚文': 'hr',
+ '拉丁文': 'la',
+ '尼泊尔文': 'ne',
+ '乌尔都文': 'ur',
+ '加利西亚文': 'gl',
+ '挪威文': 'no',
+ '阿尔巴尼亚文': 'sq',
+ '爱尔兰文': 'ga',
+ '泰卢固文': 'te',
+ '爪哇文': 'jv',
+ '斯洛文尼亚文': 'sl',
+ '爱沙尼亚文': 'et',
+ '阿塞拜疆文': 'az',
+ '斯洛伐克文': 'sk',
+ '南非荷兰文': 'af',
+ '拉脱维亚文': 'lv'
+ }
+
+ transReformBookList(bookList, descriptionPrefix = "更新至:") {
+ return bookList.map(book => ({
+ id: `${book.relates.slug}//${book.relates.title}`,
+ title: book.relates.title,
+ cover: book.relates.md_covers?.[0]?.b2key
+ ? `https://meo.comick.pictures/${book.relates.md_covers[0].b2key}`
+ : 'w7xqzd.jpg',
+ }));
+ }
transformBookList(bookList, descriptionPrefix = "更新至:") {
return bookList.map(book => ({
@@ -171,11 +391,11 @@ class Comick extends ComicSource {
// 使用统一函数转换数据
const result = {
+ "最近更新": this.transformBookList(mangaData.extendedNews),
+ "最近上传": this.transformBookList(mangaData.news),
"最近热门": this.transformBookList(mangaData.recentRank),
"总热门": this.transformBookList(mangaData.rank),
- "最近上传": this.transformBookList(mangaData.news),
- "最近更新": this.transformBookList(mangaData.extendedNews),
- "完结": this.transformBookList(mangaData.completions)
+ "完结": this.transformBookList(mangaData.completions),
};
return result;
@@ -249,138 +469,11 @@ class Comick extends ComicSource {
optionList: []
}
- // 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 = {
- // 加载漫画信息
+ id: null,
+ buildId: null,
+
loadInfo: async (id) => {
const [cId, cTitle] = id.split("//");
if (!cId) {
@@ -392,6 +485,90 @@ class Comick extends ComicSource {
throw "Invalid status code: " + res.status
}
+
+ // 加载漫画信息
+ let load_chapter = async (firstChapters, comicData, buildId, id) => {
+ // 1. 按 lang 聚合首个有效 {hid,vol,chap}
+ const langMap = firstChapters.reduce((map, chapter) => {
+ const { lang, hid, vol, chap } = chapter;
+ if (!map[lang]) {
+ // 第一次见该语言,先记录
+ map[lang] = { hid, vol, chap };
+ } else if (
+ // 如果当前已记录的 vol/chap 都为 null,且新的有任意一个不为 null,则用新记录替换
+ map[lang].vol == null && map[lang].chap == null &&
+ (vol != null || chap != null)
+ ) {
+ map[lang] = { hid, vol, chap };
+ }
+ return map;
+ }, {});
+
+ let lang_min_len = Math.min(firstChapters.length, parseInt(this.loadSetting("lang_len"))|| parseInt(this.settings.lang_len.default));
+
+ // 2. 取前 lang_min_len 个语言
+ const langs = Object.keys(langMap).slice(0, lang_min_len);
+ const result = {};
+ let updateTime = "";
+ let i = 1;
+ for (const lang of langs) {
+ let first = langMap[lang];
+ if (first.vol == null && first.chap == null) {
+ const chapters = new Map();
+ chapters.set(`${first.hid}//no//-1//${first.lang}`, '无标卷');
+ result[Comick.language_dict[lang] || lang] = chapters;
+ continue;
+ }
+
+ // 3. 构造章节请求 URL
+ const url =
+ `${this.baseUrl}/_next/data/${buildId}/comic/${id}/${first.hid}` +
+ (first.chap != null
+ ? `-chapter-${first.chap}`
+ : `-volume-${first.vol}`) +
+ `-${lang}.json`;
+
+ const res = await Network.get(url);
+ if (res.status !== 200) {
+ throw `Invalid status code: ${res.status}`;
+ }
+ const raw = JSON.parse(res.body);
+ if(i==1){
+ //获得更新时间:
+ updateTime = raw.pageProps.chapter.updated_at
+ ? raw.pageProps.chapter.updated_at.split('T')[0] : comicData.last_chapter
+ ? `第${comicData.last_chapter}话`: " ";
+ }
+ i++;
+ const list = (raw.pageProps.chapters || []).reverse();
+
+
+ // 4. 构建章节 Map
+ const chapters = new Map();
+ list.forEach(ch => {
+ let key, label;
+ if (ch.chap == null && ch.vol == null) {
+ key = `${ch.hid}//no//-1//${first.lang}`;
+ label = '无标卷';
+ } else if (ch.chap != null) {
+ key = `${ch.hid}//chapter//${ch.chap}//${first.lang}`;
+ label = `第${ch.chap}话`;
+ } else {
+ key = `${ch.hid}//volume//${ch.vol}//${first.lang}`;
+ label = `第${ch.vol}卷`;
+ }
+ chapters.set(key, label);
+ });
+
+ result[Comick.language_dict[lang] || lang] = chapters;
+
+ }
+ // 5. 返回 Map<语言, Map<章节Key, 章节名称>>
+ return [new Map(Object.entries(result)), updateTime];
+ };
+
+ //填充文章id:
+ this.comic.id = id;
let document = new HtmlDocument(res.body)
let jsonData = JSON.parse(document.getElementById('__NEXT_DATA__').text);
let comicData = jsonData.props.pageProps.comic;
@@ -399,7 +576,6 @@ class Comick extends ComicSource {
let title = cTitle? cTitle:comicData?.title|| "未知标题";
let status = comicData?.status || "1"; // 默认连载
let cover = comicData.md_covers?.[0]?.b2key ? `https://meo.comick.pictures/${comicData.md_covers[0].b2key}` : 'w7xqzd.jpg';
-
let author = authorData[0]?.name || "未知作者";
// 提取标签的slug数组的代码
@@ -421,13 +597,23 @@ class Comick extends ComicSource {
return Comick.category_param_dict[tag] || tag; // 如果字典里没有,就返回原值
});
let description = comicData.desc || "暂无描述";
- if(comicData.chapter_count == 0){
+
+ //处理推荐列表
+ let recommends = this.transReformBookList(comicData.recommendations!=null?comicData.recommendations:[]);
+ //只要recommends数组前面十个,不够十个则就是recommends的长度
+ recommends = recommends.slice(0, Math.min(recommends.length, 10));
+
+ //处理空漫画
+ let firstChapters = jsonData.props.pageProps.firstChapters;
+
+ if(comicData.chapter_count == 0 && (firstChapters==null||firstChapters.length==0)){
let chapters = new Map()
return {
title: title,
cover: cover,
description: description,
tags: {
+ "语言": [],
"作者": [author],
"更新": ["暂无更新"],
"标签": translatedTags,
@@ -441,8 +627,7 @@ class Comick extends ComicSource {
let buildId = jsonData.buildId;
let slug = jsonData.query.slug;
let firstChapter = jsonData.props.pageProps.firstChapters[0];
- let firstChapters = jsonData.props.pageProps.firstChapters;
-
+
// 处理无标卷和无标话的情况
if(firstChapter.vol == null && firstChapter.chap == null){
for(let i = 0; i < firstChapters.length; i++) {
@@ -454,7 +639,7 @@ class Comick extends ComicSource {
// 如果处理完成之后依然章节没有卷和话信息,直接返回无标卷
if(firstChapter.vol == null && firstChapter.chap == null){
let chapters = new Map()
- chapters.set(firstChapter.hid + "//no//-1", "无标卷")
+ chapters.set(firstChapter.hid + "//no//-1//" + firstChapter.lang, "无标卷")
return {
title: title,
cover: cover,
@@ -470,39 +655,10 @@ class Comick extends ComicSource {
}
}
- let chapters_url = `${this.baseUrl}/_next/data/${buildId}/comic/${slug}/${firstChapter.hid}${
- firstChapter.chap != null
- ? `-chapter-${firstChapter.chap}`
- : `-volume-${firstChapter.vol}`
- }-${firstChapter.lang}.json`;
- let list_res = await Network.get(chapters_url)
- if (list_res.status !== 200) {
- throw "Invalid status code: " + res.status
- }
- let chapters_raw = JSON.parse(list_res.body);
- let chapters = new Map()
- // 剩余解析章节信息
- //获得更新时间:
- let updateTime = chapters_raw.pageProps.chapter.updated_at
- ? chapters_raw.pageProps.chapter.updated_at.split('T')[0] : comicData.last_chapter
- ? `第${comicData.last_chapter}话`: " ";
- let chaptersList = chapters_raw.pageProps.chapters || [];
- let chapters_next = chaptersList.reverse();
- chapters_next.forEach((chapter, index) => {
- if(chapter.chap==null && chapter.vol==null) {
- let chapNum = "无标卷";
- chapters.set(chapter.hid + "//no//-1", chapNum);
- }else if(chapter.chap!=null && chapter.vol==null){
- let chapNum = "第" + chapter.chap + "话" ;
- chapters.set(chapter.hid + "//chapter//" + chapter.chap + "//" + firstChapter.lang, chapNum);
- }else if(chapter.chap==null && chapter.vol!==null){
- let chapNum = "第" + chapter.vol + "卷" ;
- chapters.set(chapter.hid + "//volume//" + chapter.vol + "//" + firstChapter.lang, chapNum);
- }else{
- let chapNum = "第" + chapter.chap + "话" ;
- chapters.set(chapter.hid + "//chapter//" + chapter.chap + "//" + firstChapter.lang, chapNum);
- }
- });
+ //获取章节
+ let temp = await load_chapter(firstChapters, comicData, buildId, cId);
+ let chapters = temp[0];
+ let updateTime = temp[1];
return {
title: title,
@@ -512,9 +668,10 @@ class Comick extends ComicSource {
"作者": [author],
"更新": [updateTime],
"标签": translatedTags,
- "状态": [Comick.comic_status[status]]
+ "状态": [Comick.comic_status[status]],
},
chapters: chapters,
+ recommend: recommends!=null?recommends:[]
}
},
loadEp: async (comicId, epId) => {
@@ -571,32 +728,14 @@ class Comick extends ComicSource {
},
onClickTag: (namespace, tag) => {
if (namespace === "标签") {
+ let r_tag = Comick.reversed_category_param_dict[tag] || tag;
return {
action: 'category',
keyword: `${tag}`,
- param: null,
+ param: r_tag,
}
}
throw "Click Tag Error"
},
- /**
- * [Optional] Handle links
- */
- link: {
- /**
- * set accepted domains
- */
- domains: [
- 'example.com'
- ],
- /**
- * parse url to comic id
- * @param url {string}
- * @returns {string | null}
- */
- linkToId: (url) => {
-
- }
- },
}
}
\ No newline at end of file
diff --git a/index.json b/index.json
index d93f0ee..fffade5 100644
--- a/index.json
+++ b/index.json
@@ -78,6 +78,6 @@
"name": "comick",
"fileName": "comick.js",
"key": "comick",
- "version": "1.0.0"
+ "version": "1.0.1"
}
]