Compare commits

...

13 Commits

Author SHA1 Message Date
Brooklyn Bartly
7dce35fd5a 包子漫画水印去除 (#114)
* refactor(baozi): 格式化代码并优化漫画章节解析逻辑

* feat: 更新包子漫画图片代理地址以优化水印问题

将图片地址从s1.baozicdn.com替换为as-rsa1-usla.baozicdn.com/w640,减少代理后的图片水印

* chore: 更新包子漫画插件版本至1.1.0

* feat(图片加载): 添加图片加载时的自定义请求头

为漫画图片加载添加自定义请求头,包括User-Agent等信息,以适配服务器要求

* refactor(图片代理): 优化移动端图片代理逻辑并移除无用代码

- 使用正则匹配简化图片URL替换逻辑
- 移除不再使用的onImageLoad方法
2025-07-27 15:55:55 +08:00
nyne
ad91da8e0f Merge pull request #115 from morning-start/zaimanhua
Zaimanhua
2025-07-27 15:55:20 +08:00
nyne
4a18a7de3a Merge branch 'main' into zaimanhua 2025-07-27 15:54:47 +08:00
Brooklyn Bartly
5ff8254dd5 Manhuagui (#102)
* feat: 添加漫画柜漫画源实现

实现漫画柜(ManHuaGui)漫画源的完整功能,包括:
- 首页探索页面的多分区加载
- 分类浏览功能支持多种筛选条件
- 搜索功能支持按时间和人气排序
- 漫画详情页面的完整信息展示
- 章节图片的加载功能

* feat: 添加漫画柜扩展支持

* chore: 在.gitignore中添加test目录

避免将测试生成的临时文件提交到版本控制

* feat(漫画源): 实现图片信息提取逻辑并优化图片URL生成

* fix(manhuagui): 修复封面图片加载和章节列表获取问题

- 删除别名信息,可能为空
- 当封面图片src属性不存在时,尝试使用data-src属性
- 处理章节列表可能存在于不同DOM节点的情况
- 移除部分调试日志输出
- 为缩略图请求添加必要的headers

* refactor: 将类名从NewComicSource重命名为ManHuaGui
2025-07-27 15:52:55 +08:00
morning-start
fb20c68024 feat: 添加再漫画源配置文件 2025-07-27 01:21:53 +08:00
morning-start
631298ce1b refactor(zaimanhua): 使用API接口替代HTML解析获取漫画数据
移除parseCoverComic方法,改为通过API接口获取漫画数据并重构parseJsonComic方法处理返回的JSON数据。同时修改首页加载逻辑,直接调用API接口获取推荐漫画列表,提高数据获取的稳定性和效率。
2025-07-27 01:20:28 +08:00
morning-start
f812964e55 fix: 修复章节ID和点击数转换为字符串的问题
确保章节ID和点击数字段始终作为字符串处理,避免潜在的类型错误。同时修正URL参数拼接中的变量名错误。
2025-07-27 00:23:29 +08:00
morning-start
2e13f5fce9 fix(漫画源): 修复时间戳转换和章节排序问题,实现章节图片加载
- 修复时间戳需要乘以1000的问题
- 对章节按照ID进行排序
- 实现章节图片加载功能
- 完善漫画详情页的标签信息
2025-07-27 00:05:57 +08:00
morning-start
0976105138 refactor(zaimanhua): 简化 parseJsonComic 方法中的对象创建逻辑
直接使用对象属性初始化 Comic 对象,避免不必要的中间变量
2025-07-26 23:00:52 +08:00
morning-start
b1b8b8cab9 feat(漫画详情): 实现漫画详情页的加载功能
添加从API获取漫画详细信息的实现,包括标题、作者、封面、描述、章节列表和推荐漫画
使用baseUrl代替硬编码的域名,提高代码可维护性
移除未使用的parseListComic方法
2025-07-26 22:58:19 +08:00
morning-start
fd59c132a2 fix: 修复再漫画搜索功能返回结果问题
搜索功能未正确返回漫画列表和最大页数,添加缺失的返回数据逻辑
2025-07-26 22:14:43 +08:00
morning-start
a5b1fd6ca2 refactor(zaimanhua): 重构漫画源接口实现和数据结构
- 修改fetchHtml和fetchJson返回类型,增加错误处理
- 简化漫画信息解析逻辑,移除冗余字段
- 重构分类页面实现,使用固定分类选项
- 实现分类漫画加载接口,支持分页和筛选
2025-07-26 22:14:14 +08:00
morning-start
2174c13e16 feat: 添加再漫画源配置文件并更新.gitignore
添加zaimanhua.js作为新的漫画源配置文件,包含完整的漫画源实现
在.gitignore中新增test/目录忽略规则
2025-07-26 20:27:08 +08:00
5 changed files with 1774 additions and 367 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea .idea
.vscode .vscode
test/

400
baozi.js
View File

@@ -1,16 +1,16 @@
class Baozi extends ComicSource { class Baozi extends ComicSource {
// 此漫画源的名称 // 此漫画源的名称
name = "包子漫画" name = "包子漫画";
// 唯一标识符 // 唯一标识符
key = "baozi" key = "baozi";
version = "1.0.5" version = "1.1.0";
minAppVersion = "1.0.0" minAppVersion = "1.0.0";
// 更新链接 // 更新链接
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/baozi.js" url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/baozi.js";
settings = { settings = {
language: { language: {
@@ -18,9 +18,9 @@ class Baozi extends ComicSource {
type: "select", type: "select",
options: [ options: [
{ value: "cn", text: "简体" }, { value: "cn", text: "简体" },
{ value: "tw", text: "繁體" } { value: "tw", text: "繁體" },
], ],
default: "cn" default: "cn",
}, },
domains: { domains: {
title: "主域名", title: "主域名",
@@ -30,18 +30,18 @@ class Baozi extends ComicSource {
{ value: "webmota.com" }, { value: "webmota.com" },
{ value: "kukuc.co" }, { value: "kukuc.co" },
{ value: "twmanga.com" }, { value: "twmanga.com" },
{ value: "dinnerku.com" } { value: "dinnerku.com" },
], ],
default: "baozimhcn.com" default: "baozimhcn.com",
} },
} };
// 动态生成完整域名 // 动态生成完整域名
get lang() { get lang() {
return this.loadSetting('language') || this.settings.language.default; return this.loadSetting("language") || this.settings.language.default;
} }
get baseUrl() { get baseUrl() {
let domain = this.loadSetting('domains') || this.settings.domains.default; let domain = this.loadSetting("domains") || this.settings.domains.default;
return `https://${this.lang}.${domain}`; return `https://${this.lang}.${domain}`;
} }
@@ -51,49 +51,60 @@ class Baozi extends ComicSource {
/// 登录 /// 登录
/// 返回任意值表示登录成功 /// 返回任意值表示登录成功
login: async (account, pwd) => { login: async (account, pwd) => {
let res = await Network.post(`${this.baseUrl}/api/bui/signin`, { let res = await Network.post(
'content-type': 'multipart/form-data; boundary=----WebKitFormBoundaryFUNUxpOwyUaDop8s' `${this.baseUrl}/api/bui/signin`,
}, "------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\n" + account + "\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name=\"password\"\r\n\r\n" + pwd + "\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s--\r\n") {
"content-type":
"multipart/form-data; boundary=----WebKitFormBoundaryFUNUxpOwyUaDop8s",
},
'------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name="username"\r\n\r\n' +
account +
'\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name="password"\r\n\r\n' +
pwd +
"\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s--\r\n"
);
if (res.status !== 200) { if (res.status !== 200) {
throw "Invalid status code: " + res.status throw "Invalid status code: " + res.status;
} }
let json = JSON.parse(res.body) let json = JSON.parse(res.body);
let token = json.data let token = json.data;
Network.setCookies(this.baseUrl, [ Network.setCookies(this.baseUrl, [
new Cookie({ new Cookie({
name: 'TSID', name: "TSID",
value: token, value: token,
domain: this.loadSetting('domains') || this.settings.domains.default domain: this.loadSetting("domains") || this.settings.domains.default,
}), }),
]) ]);
return 'ok' return "ok";
}, },
// 退出登录时将会调用此函数 // 退出登录时将会调用此函数
logout: function () { logout: function () {
Network.deleteCookies(this.loadSetting('domains') || this.settings.domains.default) Network.deleteCookies(
this.loadSetting("domains") || this.settings.domains.default
);
}, },
get registerWebsite() { get registerWebsite() {
return `${this.baseUrl}/user/signup` return `${this.baseUrl}/user/signup`;
} },
} };
/// 解析漫画列表 /// 解析漫画列表
parseComic(e) { parseComic(e) {
let url = e.querySelector("a").attributes['href'] let url = e.querySelector("a").attributes["href"];
let id = url.split("/").pop() let id = url.split("/").pop();
let title = e.querySelector("h3").text.trim() let title = e.querySelector("h3").text.trim();
let cover = e.querySelector("a > amp-img").attributes["src"] let cover = e.querySelector("a > amp-img").attributes["src"];
let tags = e.querySelectorAll("div.tabs > span").map(e => e.text.trim()) let tags = e.querySelectorAll("div.tabs > span").map((e) => e.text.trim());
let description = e.querySelector("small").text.trim() let description = e.querySelector("small").text.trim();
return { return {
id: id, id: id,
title: title, title: title,
cover: cover, cover: cover,
tags: tags, tags: tags,
description: description description: description,
} };
} }
parseJsonComic(e) { parseJsonComic(e) {
@@ -103,12 +114,13 @@ class Baozi extends ComicSource {
subTitle: e.author, subTitle: e.author,
cover: `https://static-tw.baozimh.com/cover/${e.topic_img}?w=285&h=375&q=100`, cover: `https://static-tw.baozimh.com/cover/${e.topic_img}?w=285&h=375&q=100`,
tags: e.type_names, tags: e.type_names,
} };
} }
/// 探索页面 /// 探索页面
/// 一个漫画源可以有多个探索页面 /// 一个漫画源可以有多个探索页面
explore = [{ explore = [
{
/// 标题 /// 标题
/// 标题同时用作标识符, 不能重复 /// 标题同时用作标识符, 不能重复
title: "包子漫画", title: "包子漫画",
@@ -117,31 +129,34 @@ class Baozi extends ComicSource {
type: "singlePageWithMultiPart", type: "singlePageWithMultiPart",
load: async () => { load: async () => {
var res = await Network.get(this.baseUrl) var res = await Network.get(this.baseUrl);
if (res.status !== 200) { if (res.status !== 200) {
throw "Invalid status code: " + res.status throw "Invalid status code: " + res.status;
} }
let document = new HtmlDocument(res.body) let document = new HtmlDocument(res.body);
let parts = document.querySelectorAll("div.index-recommend-items") let parts = document.querySelectorAll("div.index-recommend-items");
let result = {} let result = {};
for (let part of parts) { for (let part of parts) {
let title = part.querySelector("div.catalog-title").text.trim() let title = part.querySelector("div.catalog-title").text.trim();
let comics = part.querySelectorAll("div.comics-card").map(e => this.parseComic(e)) let comics = part
.querySelectorAll("div.comics-card")
.map((e) => this.parseComic(e));
if (comics.length > 0) { if (comics.length > 0) {
result[title] = comics result[title] = comics;
} }
} }
return result return result;
} },
} },
] ];
/// 分类页面 /// 分类页面
/// 一个漫画源只能有一个分类页面, 也可以没有, 设置为null禁用分类页面 /// 一个漫画源只能有一个分类页面, 也可以没有, 设置为null禁用分类页面
category = { category = {
/// 标题, 同时为标识符, 不能与其他漫画源的分类页面重复 /// 标题, 同时为标识符, 不能与其他漫画源的分类页面重复
title: "包子漫画", title: "包子漫画",
parts: [{ parts: [
{
name: "类型", name: "类型",
// fixed 或者 random // fixed 或者 random
@@ -151,7 +166,34 @@ class Baozi extends ComicSource {
// 如果类型为random, 需要提供此字段, 表示同时显示的数量 // 如果类型为random, 需要提供此字段, 表示同时显示的数量
// randomNumber: 5, // randomNumber: 5,
categories: ['全部', '恋爱', '纯爱', '古风', '异能', '悬疑', '剧情', '科幻', '奇幻', '玄幻', '穿越', '冒险', '推理', '武侠', '格斗', '战争', '热血', '搞笑', '大女主', '都市', '总裁', '后宫', '日常', '韩漫', '少年', '其它'], categories: [
"全部",
"恋爱",
"纯爱",
"古风",
"异能",
"悬疑",
"剧情",
"科幻",
"奇幻",
"玄幻",
"穿越",
"冒险",
"推理",
"武侠",
"格斗",
"战争",
"热血",
"搞笑",
"大女主",
"都市",
"总裁",
"后宫",
"日常",
"韩漫",
"少年",
"其它",
],
// category或者search // category或者search
// 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画 // 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画
@@ -159,66 +201,89 @@ class Baozi extends ComicSource {
itemType: "category", itemType: "category",
// 若提供, 数量需要和`categories`一致, `categoryComics.load`方法将会收到此参数 // 若提供, 数量需要和`categories`一致, `categoryComics.load`方法将会收到此参数
categoryParams: ['all', 'lianai', 'chunai', 'gufeng', 'yineng', 'xuanyi', 'juqing', 'kehuan', 'qihuan', 'xuanhuan', 'chuanyue', 'mouxian', 'tuili', 'wuxia', 'gedou', 'zhanzheng', 'rexie', 'gaoxiao', 'danuzhu', 'dushi', 'zongcai', 'hougong', 'richang', 'hanman', 'shaonian', 'qita'] categoryParams: [
} "all",
"lianai",
"chunai",
"gufeng",
"yineng",
"xuanyi",
"juqing",
"kehuan",
"qihuan",
"xuanhuan",
"chuanyue",
"mouxian",
"tuili",
"wuxia",
"gedou",
"zhanzheng",
"rexie",
"gaoxiao",
"danuzhu",
"dushi",
"zongcai",
"hougong",
"richang",
"hanman",
"shaonian",
"qita",
],
},
], ],
enableRankingPage: false, enableRankingPage: false,
} };
/// 分类漫画页面, 即点击分类标签后进入的页面 /// 分类漫画页面, 即点击分类标签后进入的页面
categoryComics = { categoryComics = {
load: async (category, param, options, page) => { load: async (category, param, options, page) => {
let res = await Network.get(`${this.baseUrl}/api/bzmhq/amp_comic_list?type=${param}&region=${options[0]}&state=${options[1]}&filter=%2a&page=${page}&limit=36&language=${this.lang}&__amp_source_origin=${this.baseUrl}`) let res = await Network.get(
`${this.baseUrl}/api/bzmhq/amp_comic_list?type=${param}&region=${options[0]}&state=${options[1]}&filter=%2a&page=${page}&limit=36&language=${this.lang}&__amp_source_origin=${this.baseUrl}`
);
if (res.status !== 200) { if (res.status !== 200) {
throw "Invalid status code: " + res.status throw "Invalid status code: " + res.status;
} }
let maxPage = null let maxPage = null;
let json = JSON.parse(res.body) let json = JSON.parse(res.body);
if (!json.next) { if (!json.next) {
maxPage = page maxPage = page;
} }
return { return {
comics: json.items.map(e => this.parseJsonComic(e)), comics: json.items.map((e) => this.parseJsonComic(e)),
maxPage: maxPage maxPage: maxPage,
} };
}, },
// 提供选项 // 提供选项
optionList: [{ optionList: [
options: [ {
"all-全部", options: ["all-全部", "cn-国漫", "jp-日本", "kr-韩国", "en-欧美"],
"cn-国漫", },
"jp-日本", {
"kr-韩国", options: ["all-全部", "serial-连载中", "pub-已完结"],
"en-欧美",
],
}, {
options: [
"all-全部",
"serial-连载中",
"pub-已完结",
],
}, },
], ],
} };
/// 搜索 /// 搜索
search = { search = {
load: async (keyword, options, page) => { load: async (keyword, options, page) => {
let res = await Network.get(`${this.baseUrl}/search?q=${keyword}`) let res = await Network.get(`${this.baseUrl}/search?q=${keyword}`);
if (res.status !== 200) { if (res.status !== 200) {
throw "Invalid status code: " + res.status throw "Invalid status code: " + res.status;
} }
let document = new HtmlDocument(res.body) let document = new HtmlDocument(res.body);
let comics = document.querySelectorAll("div.comics-card").map(e => this.parseComic(e)) let comics = document
.querySelectorAll("div.comics-card")
.map((e) => this.parseComic(e));
return { return {
comics: comics, comics: comics,
maxPage: 1 maxPage: 1,
} };
}, },
// 提供选项 // 提供选项
optionList: [] optionList: [],
} };
/// 收藏 /// 收藏
favorites = { favorites = {
@@ -227,17 +292,21 @@ class Baozi extends ComicSource {
/// 添加或者删除收藏 /// 添加或者删除收藏
addOrDelFavorite: async (comicId, folderId, isAdding) => { addOrDelFavorite: async (comicId, folderId, isAdding) => {
if (!isAdding) { if (!isAdding) {
let res = await Network.post(`${this.baseUrl}/user/operation_v2?op=del_bookmark&comic_id=${comicId}`) let res = await Network.post(
`${this.baseUrl}/user/operation_v2?op=del_bookmark&comic_id=${comicId}`
);
if (!res.status || res.status >= 400) { if (!res.status || res.status >= 400) {
throw "Invalid status code: " + res.status throw "Invalid status code: " + res.status;
} }
return 'ok' return "ok";
} else { } else {
let res = await Network.post(`${this.baseUrl}/user/operation_v2?op=set_bookmark&comic_id=${comicId}&chapter_slot=0`) let res = await Network.post(
`${this.baseUrl}/user/operation_v2?op=set_bookmark&comic_id=${comicId}&chapter_slot=0`
);
if (!res.status || res.status >= 400) { if (!res.status || res.status >= 400) {
throw "Invalid status code: " + res.status throw "Invalid status code: " + res.status;
} }
return 'ok' return "ok";
} }
}, },
// 加载收藏夹, 仅当multiFolder为true时有效 // 加载收藏夹, 仅当multiFolder为true时有效
@@ -245,114 +314,146 @@ class Baozi extends ComicSource {
loadFolders: null, loadFolders: null,
/// 加载漫画 /// 加载漫画
loadComics: async (page, folder) => { loadComics: async (page, folder) => {
let res = await Network.get(`${this.baseUrl}/user/my_bookshelf`) let res = await Network.get(`${this.baseUrl}/user/my_bookshelf`);
if (res.status !== 200) { if (res.status !== 200) {
throw "Invalid status code: " + res.status throw "Invalid status code: " + res.status;
} }
let document = new HtmlDocument(res.body) let document = new HtmlDocument(res.body);
function parseComic(e) { function parseComic(e) {
let title = e.querySelector("h4 > a").text.trim() let title = e.querySelector("h4 > a").text.trim();
let url = e.querySelector("h4 > a").attributes['href'] let url = e.querySelector("h4 > a").attributes["href"];
let id = url.split("/").pop() let id = url.split("/").pop();
let author = e.querySelector("div.info > ul").children[1].text.split("")[1].trim() let author = e
let description = e.querySelector("div.info > ul").children[4].children[0].text.trim() .querySelector("div.info > ul")
.children[1].text.split("")[1]
.trim();
let description = e
.querySelector("div.info > ul")
.children[4].children[0].text.trim();
return { return {
id: id, id: id,
title: title, title: title,
subTitle: author, subTitle: author,
description: description, description: description,
cover: e.querySelector("amp-img").attributes['src'] cover: e.querySelector("amp-img").attributes["src"],
};
} }
} let comics = document
let comics = document.querySelectorAll("div.bookshelf-items").map(e => parseComic(e)) .querySelectorAll("div.bookshelf-items")
.map((e) => parseComic(e));
return { return {
comics: comics, comics: comics,
maxPage: 1 maxPage: 1,
} };
} },
} };
/// 单个漫画相关 /// 单个漫画相关
comic = { comic = {
// 加载漫画信息 // 加载漫画信息
loadInfo: async (id) => { loadInfo: async (id) => {
let res = await Network.get(`${this.baseUrl}/comic/${id}`) let res = await Network.get(`${this.baseUrl}/comic/${id}`);
if (res.status !== 200) { if (res.status !== 200) {
throw "Invalid status code: " + res.status throw "Invalid status code: " + res.status;
} }
let document = new HtmlDocument(res.body) let document = new HtmlDocument(res.body);
let title = document.querySelector("h1.comics-detail__title").text.trim() let title = document.querySelector("h1.comics-detail__title").text.trim();
let cover = document.querySelector("div.l-content > div > div > amp-img").attributes['src'] let cover = document.querySelector("div.l-content > div > div > amp-img")
let author = document.querySelector("h2.comics-detail__author").text.trim() .attributes["src"];
let tags = document.querySelectorAll("div.tag-list > span").map(e => e.text.trim()) let author = document
tags = [...tags.filter(e => e !== "")] .querySelector("h2.comics-detail__author")
let updateTime = document.querySelector("div.supporting-text > div > span > em")?.text.trim().replace('(', '').replace(')', '') .text.trim();
let tags = document
.querySelectorAll("div.tag-list > span")
.map((e) => e.text.trim());
tags = [...tags.filter((e) => e !== "")];
let updateTime = document
.querySelector("div.supporting-text > div > span > em")
?.text.trim()
.replace("(", "")
.replace(")", "");
if (!updateTime) { if (!updateTime) {
const getLastChapterText = () => { const getLastChapterText = () => {
// 合并所有章节容器(处理可能存在多个列表的情况) // 合并所有章节容器(处理可能存在多个列表的情况)
const containers = [ const containers = [
...document.querySelectorAll("#chapter-items, #chapters_other_list") ...document.querySelectorAll(
"#chapter-items, #chapters_other_list"
),
]; ];
let allChapters = []; let allChapters = [];
containers.forEach(container => { containers.forEach((container) => {
const chapters = container.querySelectorAll(".comics-chapters > a"); const chapters = container.querySelectorAll(".comics-chapters > a");
allChapters.push(...Array.from(chapters)); allChapters.push(...Array.from(chapters));
}); });
const lastChapter = allChapters[allChapters.length - 1]; const lastChapter = allChapters[allChapters.length - 1];
return lastChapter?.querySelector("div > span")?.text.trim() || "暂无更新信息"; return (
lastChapter?.querySelector("div > span")?.text.trim() ||
"暂无更新信息"
);
}; };
updateTime = getLastChapterText(); updateTime = getLastChapterText();
} }
let description = document.querySelector("p.comics-detail__desc").text.trim() let description = document
let chapters = new Map() .querySelector("p.comics-detail__desc")
let i = 0 .text.trim();
for (let c of document.querySelectorAll("div#chapter-items > div.comics-chapters > a > div > span")) { let chapters = new Map();
chapters.set(i.toString(), c.text.trim()) let i = 0;
i++ for (let c of document.querySelectorAll(
"div#chapter-items > div.comics-chapters > a > div > span"
)) {
chapters.set(i.toString(), c.text.trim());
i++;
} }
for (let c of document.querySelectorAll("div#chapters_other_list > div.comics-chapters > a > div > span")) { for (let c of document.querySelectorAll(
chapters.set(i.toString(), c.text.trim()) "div#chapters_other_list > div.comics-chapters > a > div > span"
i++ )) {
chapters.set(i.toString(), c.text.trim());
i++;
} }
if (i === 0) { if (i === 0) {
// 将倒序的最新章节反转 // 将倒序的最新章节反转
const spans = Array.from(document.querySelectorAll("div.comics-chapters > a > div > span")).reverse(); const spans = Array.from(
document.querySelectorAll("div.comics-chapters > a > div > span")
).reverse();
for (let c of spans) { for (let c of spans) {
chapters.set(i.toString(), c.text.trim()); chapters.set(i.toString(), c.text.trim());
i++; i++;
} }
} }
let recommend = [] let recommend = [];
for (let c of document.querySelectorAll("div.recommend--item")) { for (let c of document.querySelectorAll("div.recommend--item")) {
if (c.querySelectorAll("div.tag-comic").length > 0) { if (c.querySelectorAll("div.tag-comic").length > 0) {
let title = c.querySelector("span").text.trim() let title = c.querySelector("span").text.trim();
let cover = c.querySelector("amp-img").attributes['src'] let cover = c.querySelector("amp-img").attributes["src"];
let url = c.querySelector("a").attributes['href'] let url = c.querySelector("a").attributes["href"];
let id = url.split("/").pop() let id = url.split("/").pop();
recommend.push({ recommend.push({
id: id, id: id,
title: title, title: title,
cover: cover cover: cover,
}) });
} }
} }
// updateTime 将 Y年 M月 D日 转化为 Y-M-D // updateTime 将 Y年 M月 D日 转化为 Y-M-D
let updateDate = updateTime.replace(/年/g, '-').replace(/月/g, '-').replace(/日/g, ''); let updateDate = updateTime
.replace(/年/g, "-")
.replace(/月/g, "-")
.replace(/日/g, "");
return new ComicDetails({ return new ComicDetails({
title: title, title: title,
cover: cover, cover: cover,
description: description, description: description,
tags: { tags: {
"作者": [author], 作者: [author],
"标签": tags 标签: tags,
}, },
chapters: chapters, chapters: chapters,
recommend: recommend, recommend: recommend,
updateTime: updateDate, updateTime: updateDate,
}) });
}, },
loadEp: async (comicId, epId) => { loadEp: async (comicId, epId) => {
const images = []; const images = [];
@@ -365,22 +466,29 @@ class Baozi extends ComicSource {
// 解析当前页图片 // 解析当前页图片
const doc = new HtmlDocument(res.body); const doc = new HtmlDocument(res.body);
doc.querySelectorAll("ul.comic-contain > div > amp-img").forEach(img => { doc
const src = img?.attributes?.['src']; .querySelectorAll("ul.comic-contain > div > amp-img")
if (typeof src === 'string') images.push(src); .forEach((img) => {
const src = img?.attributes?.["src"];
if (typeof src === "string") images.push(src);
}); });
// 查找下一页链接 // 查找下一页链接
const nextLink = doc.querySelector("a#next-chapter"); const nextLink = doc.querySelector("a#next-chapter");
if (nextLink?.text?.match(/下一页|下一頁/)) { if (nextLink?.text?.match(/下一页|下一頁/)) {
currentPageUrl = nextLink.attributes['href']; currentPageUrl = nextLink.attributes["href"];
} else { } else {
break; break;
} }
maxAttempts--; maxAttempts--;
} }
// 代理后图片水印更少 // 代理后图片水印更少
return { images }; let mobileImages = images.map((e) => {
} const regex = /scomic\/.*/;
} const match = e.match(regex);
return `https://as-rsa1-usla.baozicdn.com/w640/${match[0]}`;
});
return { images: mobileImages };
},
};
} }

View File

@@ -15,7 +15,7 @@
"name": "包子漫画", "name": "包子漫画",
"fileName": "baozi.js", "fileName": "baozi.js",
"key": "baozi", "key": "baozi",
"version": "1.0.5" "version": "1.1.0"
}, },
{ {
"name": "Picacg", "name": "Picacg",
@@ -80,6 +80,24 @@
"key": "comick", "key": "comick",
"version": "1.1.1" "version": "1.1.1"
}, },
{
"name": "优酷漫画",
"fileName": "ykmh.js",
"key": "ykmh",
"version": "1.0.0"
},
{
"name": "再漫画",
"fileName": "zaimanhua.js",
"key": "zaimanhua",
"version": "1.0.0"
},
{
"name": "漫画柜",
"fileName": "manhuagui.js",
"key": "manhuagui",
"version": "1.0.0"
},
{ {
"name": "优酷漫画", "name": "优酷漫画",
"fileName": "ykmh.js", "fileName": "ykmh.js",

862
manhuagui.js Normal file
View File

@@ -0,0 +1,862 @@
/** @type {import('./_venera_.js')} */
class ManHuaGui 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 = "ManHuaGui";
version = "1.0.0";
minAppVersion = "1.4.0";
// update url
url =
"https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/manhuagui.js";
baseUrl = "https://www.manhuagui.com";
async getHtml(url) {
let headers = {
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",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "no-cache",
pragma: "no-cache",
priority: "u=0, i",
"sec-ch-ua":
'"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
cookie: "country=US",
Referer: "https://www.manhuagui.com/",
"Referrer-Policy": "strict-origin-when-cross-origin",
};
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;
}
parseSimpleComic(e) {
let url = e.querySelector(".ell > a").attributes["href"];
let id = url.split("/")[2];
let title = e.querySelector(".ell > a").text.trim();
let cover = e.querySelector("img").attributes["src"];
if (!cover) {
cover = e.querySelector("img").attributes["data-src"];
}
cover = `https:${cover}`;
let description = e.querySelector(".tt").text.trim();
return new Comic({
id,
title,
cover,
description,
});
}
parseComic(e) {
let simple = this.parseSimpleComic(e);
let sl = e.querySelector(".sl");
let status = sl ? "连载" : "完结";
// 如果能够找到 <span class="updateon">更新于2020-03-31<em>3.9</em></span> 解析 更新和评分
let tmp = e.querySelector(".updateon").childNodes;
let update = tmp[0].replace("更新于:", "").trim();
let tags = [status, update];
return new Comic({
id: simple.id,
title: simple.title,
cover: simple.cover,
description: simple.description,
tags,
author,
});
}
/**
* [Optional] init function
*/
init() {
var LZString = (function () {
var f = String.fromCharCode;
var keyStrBase64 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var baseReverseDic = {};
function getBaseValue(alphabet, character) {
if (!baseReverseDic[alphabet]) {
baseReverseDic[alphabet] = {};
for (var i = 0; i < alphabet.length; i++) {
baseReverseDic[alphabet][alphabet.charAt(i)] = i;
}
}
return baseReverseDic[alphabet][character];
}
var LZString = {
decompressFromBase64: function (input) {
if (input == null) return "";
if (input == "") return null;
return LZString._0(input.length, 32, function (index) {
return getBaseValue(keyStrBase64, input.charAt(index));
});
},
_0: function (length, resetValue, getNextValue) {
var dictionary = [],
next,
enlargeIn = 4,
dictSize = 4,
numBits = 3,
entry = "",
result = [],
i,
w,
bits,
resb,
maxpower,
power,
c,
data = {
val: getNextValue(0),
position: resetValue,
index: 1,
};
for (i = 0; i < 3; i += 1) {
dictionary[i] = i;
}
bits = 0;
maxpower = Math.pow(2, 2);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
switch ((next = bits)) {
case 0:
bits = 0;
maxpower = Math.pow(2, 8);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
c = f(bits);
break;
case 1:
bits = 0;
maxpower = Math.pow(2, 16);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
c = f(bits);
break;
case 2:
return "";
}
dictionary[3] = c;
w = c;
result.push(c);
while (true) {
if (data.index > length) {
return "";
}
bits = 0;
maxpower = Math.pow(2, numBits);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
switch ((c = bits)) {
case 0:
bits = 0;
maxpower = Math.pow(2, 8);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
dictionary[dictSize++] = f(bits);
c = dictSize - 1;
enlargeIn--;
break;
case 1:
bits = 0;
maxpower = Math.pow(2, 16);
power = 1;
while (power != maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position == 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
dictionary[dictSize++] = f(bits);
c = dictSize - 1;
enlargeIn--;
break;
case 2:
return result.join("");
}
if (enlargeIn == 0) {
enlargeIn = Math.pow(2, numBits);
numBits++;
}
if (dictionary[c]) {
entry = dictionary[c];
} else {
if (c === dictSize) {
entry = w + w.charAt(0);
} else {
return null;
}
}
result.push(entry);
dictionary[dictSize++] = w + entry.charAt(0);
enlargeIn--;
w = entry;
if (enlargeIn == 0) {
enlargeIn = Math.pow(2, numBits);
numBits++;
}
}
},
};
return LZString;
})();
function splitParams(str) {
let params = [];
let currentParam = "";
let stack = [];
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (char === "(" || char === "[" || char === "{") {
stack.push(char);
currentParam += char;
} else if (char === ")" && stack[stack.length - 1] === "(") {
stack.pop();
currentParam += char;
} else if (char === "]" && stack[stack.length - 1] === "[") {
stack.pop();
currentParam += char;
} else if (char === "}" && stack[stack.length - 1] === "{") {
stack.pop();
currentParam += char;
} else if (char === "," && stack.length === 0) {
params.push(currentParam.trim());
currentParam = "";
} else {
currentParam += char;
}
}
if (currentParam) {
params.push(currentParam.trim());
}
return params;
}
function extractParams(str) {
let params_part = str.split("}(")[1].split("))")[0];
let params = splitParams(params_part);
params[5] = {};
params[3] = LZString.decompressFromBase64(params[3].split("'")[1]).split(
"|"
);
return params;
}
function formatData(p, a, c, k, e, d) {
e = function (c) {
return (
(c < a ? "" : e(parseInt(c / a))) +
((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
);
};
if (!"".replace(/^/, String)) {
while (c--) d[e(c)] = k[c] || e(c);
k = [
function (e) {
return d[e];
},
];
e = function () {
return "\\w+";
};
c = 1;
}
while (c--)
if (k[c]) p = p.replace(new RegExp("\\b" + e(c) + "\\b", "g"), k[c]);
return p;
}
function extractFields(text) {
// 创建一个对象存储提取的结果
const result = {};
// 提取files数组
const filesMatch = text.match(/"files":\s*\[(.*?)\]/);
if (filesMatch && filesMatch[1]) {
// 提取所有文件名并去除引号和空格
result.files = filesMatch[1]
.split(",")
.map((file) => file.trim().replace(/"/g, ""));
}
// 提取path
const pathMatch = text.match(/"path":\s*"([^"]+)"/);
if (pathMatch && pathMatch[1]) {
result.path = pathMatch[1];
}
// 提取len
const lenMatch = text.match(/"len":\s*(\d+)/);
if (lenMatch && lenMatch[1]) {
result.len = parseInt(lenMatch[1], 10);
}
// 提取sl对象
const slMatch = text.match(/"sl":\s*({[^}]+})/);
if (slMatch && slMatch[1]) {
try {
// 将提取的字符串转换为对象
result.sl = JSON.parse(slMatch[1].replace(/(\w+):/g, '"$1":'));
} catch (e) {
console.error("解析sl字段失败:", e);
result.sl = null;
}
}
return result;
}
this.getImgInfos = function (script) {
let params = extractParams(script);
let imgData = formatData(...params);
let imgInfos = extractFields(imgData);
return imgInfos;
};
}
// explore page list
explore = [
{
// title of the page.
// title is used to identify the page, it should be unique
title: "漫画柜",
/// multiPartPage or multiPageComicList or mixed
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 document = await this.getHtml(this.baseUrl);
// log("info", this.name, `获取主页成功`);
let tabs = document.querySelectorAll("#cmt-tab li");
// log("info", this.name, tabs);
let parts = document.querySelectorAll("#cmt-cont ul");
// log("info", this.name, parts);
let result = {};
// tabs len = parts len
for (let i = 0; i < tabs.length; i++) {
let title = tabs[i].text.trim();
let comics = parts[i]
.querySelectorAll("li")
.map((e) => this.parseSimpleComic(e));
result[title] = comics;
}
// log("info", this.name, result);
return result;
},
/**
* Only use for `multiPageComicList` type.
* `loadNext` would be ignored if `load` function is implemented.
* @param next {string | null} - next page token, null if first page
* @returns {Promise<{comics: Comic[], next: string?}>} - next is null if no next page.
*/
loadNext(next) {},
},
];
// categories
category = {
/// title of the category page, used to identify the page, it should be unique
title: "漫画柜",
parts: [
{
name: "类型",
type: "fixed",
itemType: "category",
categories: [
"全部",
"热血",
"冒险",
"魔幻",
"神鬼",
"搞笑",
"萌系",
"爱情",
"科幻",
"魔法",
"格斗",
"武侠",
"机战",
"战争",
"竞技",
"体育",
"校园",
"生活",
"励志",
"历史",
"伪娘",
"宅男",
"腐女",
"耽美",
"百合",
"后宫",
"治愈",
"美食",
"推理",
"悬疑",
"恐怖",
"四格",
"职场",
"侦探",
"社会",
"音乐",
"舞蹈",
"杂志",
"黑道",
],
categoryParams: [
"",
"rexue",
"maoxian",
"mohuan",
"shengui",
"gaoxiao",
"mengxi",
"aiqing",
"kehuan",
"mofa",
"gedou",
"wuxia",
"jizhan",
"zhanzheng",
"jingji",
"tiyu",
"xiaoyuan",
"shenghuo",
"lizhi",
"lishi",
"weiniang",
"zhainan",
"funv",
"danmei",
"baihe",
"hougong",
"zhiyu",
"meishi",
"tuili",
"xuanyi",
"kongbu",
"sige",
"zhichang",
"zhentan",
"shehui",
"yinyue",
"wudao",
"zazhi",
"heidao",
],
},
],
// 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 area = options[0];
let genre = param;
let age = options[1];
let status = options[2];
// log(
// "info",
// this.name,
// ` 加载分类漫画: ${area} | ${genre} | ${age} | ${status}`
// );
// 字符串之间用“_”连接空字符串除外
let params = [area, genre, age, status].filter((e) => e != "").join("_");
let url = `${this.baseUrl}/list/${params}/index_p${page}.html`;
let document = await this.getHtml(url);
let maxPage = document
.querySelector(".result-count")
.querySelectorAll("strong")[1].text;
maxPage = parseInt(maxPage);
let comics = document
.querySelectorAll("#contList > li")
.map((e) => this.parseSimpleComic(e));
return {
comics,
maxPage,
};
},
// provide options for category comic loading
optionList: [
{
options: [
"-全部",
"japan-日本",
"hongkong-港台",
"other-其它",
"europe-欧美",
"china-内地",
"korea-韩国",
],
},
{
options: [
"-全部",
"shaonv-少女",
"shaonian-少年",
"qingnian-青年",
"ertong-儿童",
"tongyong-通用",
],
},
{
options: ["-全部", "lianzai-连载", "wanjie-完结"],
},
],
ranking: {
// 对于单个选项,使用“-”分隔值和文本,左侧为值,右侧为文本
options: [
"-最新发布",
"update-最新更新",
"view-人气最旺",
"rate-评分最高",
],
/**
* 加载排行榜漫画
* @param option {string} - 来自optionList的选项
* @param page {number} - 页码
* @returns {Promise<{comics: Comic[], maxPage: number}>}
*/
load: async (option, page) => {
let url = `${this.baseUrl}/list/${option}_p${page}.html`;
let document = await this.getHtml(url);
let maxPage = document
.querySelector(".result-count")
.querySelectorAll("strong")[1].text;
maxPage = parseInt(maxPage);
let comics = document
.querySelector("#contList")
.querySelectorAll("li")
.map((e) => this.parseComic(e));
return {
comics,
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 url = `${this.baseUrl}/s/${keyword}_p${page}.html`;
let document = await this.getHtml(url);
let comicNum = document
.querySelector(".result-count")
.querySelectorAll("strong")[1].text;
comicNum = parseInt(comicNum);
// 每页10个
let maxPage = Math.ceil(comicNum / 10);
let bookshelf = document
.querySelector("#contList")
.querySelectorAll("li");
let comics = bookshelf.map((e) => this.parseComic(e));
return {
comics,
maxPage,
};
},
// 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,
};
/// single comic related
comic = {
/**
* load comic info
* @param id {string}
* @returns {Promise<ComicDetails>}
*/
loadInfo: async (id) => {
let url = `${this.baseUrl}/comic/${id}/`;
let document = await this.getHtml(url);
// ANCHOR 基本信息
let book = document.querySelector(".book-cont");
let title = book
.querySelector(".book-title")
.querySelector("h1")
.text.trim();
let subtitle = book
.querySelector(".book-title")
.querySelector("h2")
.text.trim();
let cover = book.querySelector(".hcover").querySelector("img").attributes[
"src"
];
cover = `https:${cover}`;
let description = book
.querySelector("#intro-all")
.querySelectorAll("p")
.map((e) => e.text.trim())
.join("\n");
// log("warn", this.name, { title, subtitle, cover, description });
let detail_list = book.querySelectorAll(".detail-list span");
function parseDetail(idx) {
let ele = detail_list[idx].querySelectorAll("a");
if (ele.length > 0) {
return ele.map((e) => e.text.trim());
}
return [""];
}
let createYear = parseDetail(0);
let area = parseDetail(1);
let genre = parseDetail(3);
let author = parseDetail(4);
// let alias = parseDetail(5);
// let lastChapter = parseDetail(6);
let status = detail_list[7].text.trim();
let tags = {
年代: createYear,
状态: [status],
作者: author,
地区: area,
类型: genre,
};
let updateTime = detail_list[8].text.trim();
// ANCHOR 章节信息
let chapters = new Map();
let chapter_list = document.querySelector("#chapter-list-1");
if (!chapter_list) {
chapter_list = document.querySelector("#chapter-list-0");
}
let lis = chapter_list.querySelectorAll("li");
for (let li of lis) {
let a = li.querySelector("a");
let i = a.attributes["href"].split("/").pop().replace(".html", "");
let title = a.querySelector("span").text.trim();
chapters.set(i, title);
}
// chapters 升序
chapters = new Map([...chapters].sort((a, b) => a[0] - b[0]));
//ANCHOR - 推荐
let recommend = [];
let similar = document.querySelector(".similar-list");
if (similar) {
let similar_list = similar.querySelectorAll("li");
for (let li of similar_list) {
let comic = this.parseSimpleComic(li);
recommend.push(comic);
}
}
return new ComicDetails({
title,
subtitle,
cover,
description,
tags,
updateTime,
chapters,
recommend,
});
},
/**
* load images of a chapter
* @param comicId {string}
* @param epId {string?}
* @returns {Promise<{images: string[]}>}
*/
loadEp: async (comicId, epId) => {
let url = `${this.baseUrl}/comic/${comicId}/${epId}.html`;
let document = await this.getHtml(url);
let script = document.querySelectorAll("script")[4].innerHTML;
let infos = this.getImgInfos(script);
// https://us.hamreus.com/ps3/y/yiquanchaoren/第190话重制版/003.jpg.webp?e=1754143606&m=DPpelwkhr-pS3OXJpS6VkQ
let imgDomain = `https://us.hamreus.com`;
let images = [];
for (let f of infos.files) {
let imgUrl =
imgDomain + infos.path + f + `?e=${infos.sl.e}&m=${infos.sl.m}`;
images.push(imgUrl);
}
// log("warning", this.name, images);
return {
images,
};
},
/**
* [Optional] provide configs for an image loading
* @param url
* @param comicId
* @param epId
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>}
*/
onImageLoad: (url, comicId, epId) => {
return {
headers: {
accept:
"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "no-cache",
pragma: "no-cache",
priority: "i",
"sec-ch-ua":
'"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "image",
"sec-fetch-mode": "no-cors",
"sec-fetch-site": "cross-site",
"sec-fetch-storage-access": "active",
Referer: "https://www.manhuagui.com/",
"Referrer-Policy": "strict-origin-when-cross-origin",
},
};
},
/**
* [Optional] provide configs for a thumbnail loading
* @param url {string}
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>}
*
* `ImageLoadingConfig.modifyImage` and `ImageLoadingConfig.onLoadFailed` will be ignored.
* They are not supported for thumbnails.
*/
onThumbnailLoad: (url) => {
let headers = {
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",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "no-cache",
pragma: "no-cache",
priority: "u=0, i",
"sec-ch-ua":
'"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"Referrer-Policy": "strict-origin-when-cross-origin",
};
return {
headers,
};
},
};
}

418
zaimanhua.js Normal file
View File

@@ -0,0 +1,418 @@
/** @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<{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";
}
// 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 = {};
// https://manhua.zaimanhua.com/api/v1/comic1/recommend/list?
// channel=pc&app_name=zmh&version=1.0.0&timestamp=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;
},
},
];
// 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: [
"全部",
"冒险",
"搞笑",
"格斗",
"科幻",
"爱情",
"侦探",
"竞技",
"魔法",
"校园",
"百合",
"耽美",
"历史",
"战争",
"宅系",
"治愈",
"仙侠",
"武侠",
"职场",
"神鬼",
"奇幻",
"生活",
"其他",
],
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,
};
},
// provide options for category comic loading
optionList: [
{
options: ["0-全部", "3262-少年", "3263-少女", "3264-青年"],
},
{
options: ["0-全部", "1-故事漫画", "2-四格多格"],
},
{
options: ["0-全部", "1-连载", "2-完结"],
},
],
};
/// 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);
return {
comics,
maxPage,
};
},
// provide options for search
optionList: [],
};
/// single comic related
comic = {
/**
* load comic info
* @param id {string}
* @returns {Promise<ComicDetails>}
*/
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 * 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;
return {
images: info.page_url,
};
},
/**
* [Optional] provide configs for an image loading
* @param url
* @param comicId
* @param epId
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>}
*/
onImageLoad: (url, comicId, epId) => {
return {};
},
/**
* [Optional] provide configs for a thumbnail loading
* @param url {string}
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>}
*
* `ImageLoadingConfig.modifyImage` and `ImageLoadingConfig.onLoadFailed` will be ignored.
* They are not supported for thumbnails.
*/
onThumbnailLoad: (url) => {
return {};
},
};
}