mirror of
https://github.com/venera-app/venera-configs.git
synced 2025-09-27 00:27:23 +00:00
Compare commits
13 Commits
8bfb3fdf2e
...
7dce35fd5a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7dce35fd5a | ||
![]() |
ad91da8e0f | ||
![]() |
4a18a7de3a | ||
![]() |
5ff8254dd5 | ||
![]() |
fb20c68024 | ||
![]() |
631298ce1b | ||
![]() |
f812964e55 | ||
![]() |
2e13f5fce9 | ||
![]() |
0976105138 | ||
![]() |
b1b8b8cab9 | ||
![]() |
fd59c132a2 | ||
![]() |
a5b1fd6ca2 | ||
![]() |
2174c13e16 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
test/
|
836
baozi.js
836
baozi.js
@@ -1,386 +1,494 @@
|
|||||||
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: {
|
||||||
title: "简繁切换",
|
title: "简繁切换",
|
||||||
type: "select",
|
type: "select",
|
||||||
options: [
|
options: [
|
||||||
{ value: "cn", text: "简体" },
|
{ value: "cn", text: "简体" },
|
||||||
{ value: "tw", text: "繁體" }
|
{ value: "tw", text: "繁體" },
|
||||||
],
|
],
|
||||||
default: "cn"
|
default: "cn",
|
||||||
|
},
|
||||||
|
domains: {
|
||||||
|
title: "主域名",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ value: "baozimhcn.com" },
|
||||||
|
{ value: "webmota.com" },
|
||||||
|
{ value: "kukuc.co" },
|
||||||
|
{ value: "twmanga.com" },
|
||||||
|
{ value: "dinnerku.com" },
|
||||||
|
],
|
||||||
|
default: "baozimhcn.com",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 动态生成完整域名
|
||||||
|
get lang() {
|
||||||
|
return this.loadSetting("language") || this.settings.language.default;
|
||||||
|
}
|
||||||
|
get baseUrl() {
|
||||||
|
let domain = this.loadSetting("domains") || this.settings.domains.default;
|
||||||
|
return `https://${this.lang}.${domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 账号
|
||||||
|
/// 设置为null禁用账号功能
|
||||||
|
account = {
|
||||||
|
/// 登录
|
||||||
|
/// 返回任意值表示登录成功
|
||||||
|
login: async (account, pwd) => {
|
||||||
|
let res = await Network.post(
|
||||||
|
`${this.baseUrl}/api/bui/signin`,
|
||||||
|
{
|
||||||
|
"content-type":
|
||||||
|
"multipart/form-data; boundary=----WebKitFormBoundaryFUNUxpOwyUaDop8s",
|
||||||
},
|
},
|
||||||
domains: {
|
'------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name="username"\r\n\r\n' +
|
||||||
title: "主域名",
|
account +
|
||||||
type: "select",
|
'\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name="password"\r\n\r\n' +
|
||||||
options: [
|
pwd +
|
||||||
{ value: "baozimhcn.com" },
|
"\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s--\r\n"
|
||||||
{ value: "webmota.com" },
|
);
|
||||||
{ value: "kukuc.co" },
|
if (res.status !== 200) {
|
||||||
{ value: "twmanga.com" },
|
throw "Invalid status code: " + res.status;
|
||||||
{ value: "dinnerku.com" }
|
}
|
||||||
],
|
let json = JSON.parse(res.body);
|
||||||
default: "baozimhcn.com"
|
let token = json.data;
|
||||||
|
Network.setCookies(this.baseUrl, [
|
||||||
|
new Cookie({
|
||||||
|
name: "TSID",
|
||||||
|
value: token,
|
||||||
|
domain: this.loadSetting("domains") || this.settings.domains.default,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
return "ok";
|
||||||
|
},
|
||||||
|
|
||||||
|
// 退出登录时将会调用此函数
|
||||||
|
logout: function () {
|
||||||
|
Network.deleteCookies(
|
||||||
|
this.loadSetting("domains") || this.settings.domains.default
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
get registerWebsite() {
|
||||||
|
return `${this.baseUrl}/user/signup`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 解析漫画列表
|
||||||
|
parseComic(e) {
|
||||||
|
let url = e.querySelector("a").attributes["href"];
|
||||||
|
let id = url.split("/").pop();
|
||||||
|
let title = e.querySelector("h3").text.trim();
|
||||||
|
let cover = e.querySelector("a > amp-img").attributes["src"];
|
||||||
|
let tags = e.querySelectorAll("div.tabs > span").map((e) => e.text.trim());
|
||||||
|
let description = e.querySelector("small").text.trim();
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
cover: cover,
|
||||||
|
tags: tags,
|
||||||
|
description: description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parseJsonComic(e) {
|
||||||
|
return {
|
||||||
|
id: e.comic_id,
|
||||||
|
title: e.name,
|
||||||
|
subTitle: e.author,
|
||||||
|
cover: `https://static-tw.baozimh.com/cover/${e.topic_img}?w=285&h=375&q=100`,
|
||||||
|
tags: e.type_names,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 探索页面
|
||||||
|
/// 一个漫画源可以有多个探索页面
|
||||||
|
explore = [
|
||||||
|
{
|
||||||
|
/// 标题
|
||||||
|
/// 标题同时用作标识符, 不能重复
|
||||||
|
title: "包子漫画",
|
||||||
|
|
||||||
|
/// singlePageWithMultiPart 或者 multiPageComicList
|
||||||
|
type: "singlePageWithMultiPart",
|
||||||
|
|
||||||
|
load: async () => {
|
||||||
|
var res = await Network.get(this.baseUrl);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw "Invalid status code: " + res.status;
|
||||||
}
|
}
|
||||||
}
|
let document = new HtmlDocument(res.body);
|
||||||
|
let parts = document.querySelectorAll("div.index-recommend-items");
|
||||||
// 动态生成完整域名
|
let result = {};
|
||||||
get lang() {
|
for (let part of parts) {
|
||||||
return this.loadSetting('language') || this.settings.language.default;
|
let title = part.querySelector("div.catalog-title").text.trim();
|
||||||
}
|
let comics = part
|
||||||
get baseUrl() {
|
.querySelectorAll("div.comics-card")
|
||||||
let domain = this.loadSetting('domains') || this.settings.domains.default;
|
.map((e) => this.parseComic(e));
|
||||||
return `https://${this.lang}.${domain}`;
|
if (comics.length > 0) {
|
||||||
}
|
result[title] = comics;
|
||||||
|
}
|
||||||
/// 账号
|
|
||||||
/// 设置为null禁用账号功能
|
|
||||||
account = {
|
|
||||||
/// 登录
|
|
||||||
/// 返回任意值表示登录成功
|
|
||||||
login: async (account, pwd) => {
|
|
||||||
let res = await Network.post(`${this.baseUrl}/api/bui/signin`, {
|
|
||||||
'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) {
|
|
||||||
throw "Invalid status code: " + res.status
|
|
||||||
}
|
|
||||||
let json = JSON.parse(res.body)
|
|
||||||
let token = json.data
|
|
||||||
Network.setCookies(this.baseUrl, [
|
|
||||||
new Cookie({
|
|
||||||
name: 'TSID',
|
|
||||||
value: token,
|
|
||||||
domain: this.loadSetting('domains') || this.settings.domains.default
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
return 'ok'
|
|
||||||
},
|
|
||||||
|
|
||||||
// 退出登录时将会调用此函数
|
|
||||||
logout: function () {
|
|
||||||
Network.deleteCookies(this.loadSetting('domains') || this.settings.domains.default)
|
|
||||||
},
|
|
||||||
|
|
||||||
get registerWebsite() {
|
|
||||||
return `${this.baseUrl}/user/signup`
|
|
||||||
}
|
}
|
||||||
}
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 分类页面
|
||||||
|
/// 一个漫画源只能有一个分类页面, 也可以没有, 设置为null禁用分类页面
|
||||||
|
category = {
|
||||||
|
/// 标题, 同时为标识符, 不能与其他漫画源的分类页面重复
|
||||||
|
title: "包子漫画",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
name: "类型",
|
||||||
|
|
||||||
|
// fixed 或者 random
|
||||||
|
// random用于分类数量相当多时, 随机显示其中一部分
|
||||||
|
type: "fixed",
|
||||||
|
|
||||||
|
// 如果类型为random, 需要提供此字段, 表示同时显示的数量
|
||||||
|
// randomNumber: 5,
|
||||||
|
|
||||||
|
categories: [
|
||||||
|
"全部",
|
||||||
|
"恋爱",
|
||||||
|
"纯爱",
|
||||||
|
"古风",
|
||||||
|
"异能",
|
||||||
|
"悬疑",
|
||||||
|
"剧情",
|
||||||
|
"科幻",
|
||||||
|
"奇幻",
|
||||||
|
"玄幻",
|
||||||
|
"穿越",
|
||||||
|
"冒险",
|
||||||
|
"推理",
|
||||||
|
"武侠",
|
||||||
|
"格斗",
|
||||||
|
"战争",
|
||||||
|
"热血",
|
||||||
|
"搞笑",
|
||||||
|
"大女主",
|
||||||
|
"都市",
|
||||||
|
"总裁",
|
||||||
|
"后宫",
|
||||||
|
"日常",
|
||||||
|
"韩漫",
|
||||||
|
"少年",
|
||||||
|
"其它",
|
||||||
|
],
|
||||||
|
|
||||||
|
// category或者search
|
||||||
|
// 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画
|
||||||
|
// 如果为search, 将进入搜索页面
|
||||||
|
itemType: "category",
|
||||||
|
|
||||||
|
// 若提供, 数量需要和`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",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
enableRankingPage: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 分类漫画页面, 即点击分类标签后进入的页面
|
||||||
|
categoryComics = {
|
||||||
|
load: async (category, param, options, page) => {
|
||||||
|
let res = await Network.get(
|
||||||
|
`${this.baseUrl}/api/bzmhq/amp_comic_list?type=${param}®ion=${options[0]}&state=${options[1]}&filter=%2a&page=${page}&limit=36&language=${this.lang}&__amp_source_origin=${this.baseUrl}`
|
||||||
|
);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw "Invalid status code: " + res.status;
|
||||||
|
}
|
||||||
|
let maxPage = null;
|
||||||
|
let json = JSON.parse(res.body);
|
||||||
|
if (!json.next) {
|
||||||
|
maxPage = page;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
comics: json.items.map((e) => this.parseJsonComic(e)),
|
||||||
|
maxPage: maxPage,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// 提供选项
|
||||||
|
optionList: [
|
||||||
|
{
|
||||||
|
options: ["all-全部", "cn-国漫", "jp-日本", "kr-韩国", "en-欧美"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
options: ["all-全部", "serial-连载中", "pub-已完结"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 搜索
|
||||||
|
search = {
|
||||||
|
load: async (keyword, options, page) => {
|
||||||
|
let res = await Network.get(`${this.baseUrl}/search?q=${keyword}`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw "Invalid status code: " + res.status;
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body);
|
||||||
|
let comics = document
|
||||||
|
.querySelectorAll("div.comics-card")
|
||||||
|
.map((e) => this.parseComic(e));
|
||||||
|
return {
|
||||||
|
comics: comics,
|
||||||
|
maxPage: 1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// 提供选项
|
||||||
|
optionList: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 收藏
|
||||||
|
favorites = {
|
||||||
|
/// 是否为多收藏夹
|
||||||
|
multiFolder: false,
|
||||||
|
/// 添加或者删除收藏
|
||||||
|
addOrDelFavorite: async (comicId, folderId, isAdding) => {
|
||||||
|
if (!isAdding) {
|
||||||
|
let res = await Network.post(
|
||||||
|
`${this.baseUrl}/user/operation_v2?op=del_bookmark&comic_id=${comicId}`
|
||||||
|
);
|
||||||
|
if (!res.status || res.status >= 400) {
|
||||||
|
throw "Invalid status code: " + res.status;
|
||||||
|
}
|
||||||
|
return "ok";
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
throw "Invalid status code: " + res.status;
|
||||||
|
}
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 加载收藏夹, 仅当multiFolder为true时有效
|
||||||
|
// 当comicId不为null时, 需要同时返回包含该漫画的收藏夹
|
||||||
|
loadFolders: null,
|
||||||
|
/// 加载漫画
|
||||||
|
loadComics: async (page, folder) => {
|
||||||
|
let res = await Network.get(`${this.baseUrl}/user/my_bookshelf`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw "Invalid status code: " + res.status;
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body);
|
||||||
|
function parseComic(e) {
|
||||||
|
let title = e.querySelector("h4 > a").text.trim();
|
||||||
|
let url = e.querySelector("h4 > a").attributes["href"];
|
||||||
|
let id = url.split("/").pop();
|
||||||
|
let author = e
|
||||||
|
.querySelector("div.info > ul")
|
||||||
|
.children[1].text.split(":")[1]
|
||||||
|
.trim();
|
||||||
|
let description = e
|
||||||
|
.querySelector("div.info > ul")
|
||||||
|
.children[4].children[0].text.trim();
|
||||||
|
|
||||||
/// 解析漫画列表
|
|
||||||
parseComic(e) {
|
|
||||||
let url = e.querySelector("a").attributes['href']
|
|
||||||
let id = url.split("/").pop()
|
|
||||||
let title = e.querySelector("h3").text.trim()
|
|
||||||
let cover = e.querySelector("a > amp-img").attributes["src"]
|
|
||||||
let tags = e.querySelectorAll("div.tabs > span").map(e => e.text.trim())
|
|
||||||
let description = e.querySelector("small").text.trim()
|
|
||||||
return {
|
return {
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
subTitle: author,
|
||||||
|
description: description,
|
||||||
|
cover: e.querySelector("amp-img").attributes["src"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let comics = document
|
||||||
|
.querySelectorAll("div.bookshelf-items")
|
||||||
|
.map((e) => parseComic(e));
|
||||||
|
return {
|
||||||
|
comics: comics,
|
||||||
|
maxPage: 1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 单个漫画相关
|
||||||
|
comic = {
|
||||||
|
// 加载漫画信息
|
||||||
|
loadInfo: async (id) => {
|
||||||
|
let res = await Network.get(`${this.baseUrl}/comic/${id}`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw "Invalid status code: " + res.status;
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body);
|
||||||
|
|
||||||
|
let title = document.querySelector("h1.comics-detail__title").text.trim();
|
||||||
|
let cover = document.querySelector("div.l-content > div > div > amp-img")
|
||||||
|
.attributes["src"];
|
||||||
|
let author = document
|
||||||
|
.querySelector("h2.comics-detail__author")
|
||||||
|
.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) {
|
||||||
|
const getLastChapterText = () => {
|
||||||
|
// 合并所有章节容器(处理可能存在多个列表的情况)
|
||||||
|
const containers = [
|
||||||
|
...document.querySelectorAll(
|
||||||
|
"#chapter-items, #chapters_other_list"
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let allChapters = [];
|
||||||
|
containers.forEach((container) => {
|
||||||
|
const chapters = container.querySelectorAll(".comics-chapters > a");
|
||||||
|
allChapters.push(...Array.from(chapters));
|
||||||
|
});
|
||||||
|
const lastChapter = allChapters[allChapters.length - 1];
|
||||||
|
return (
|
||||||
|
lastChapter?.querySelector("div > span")?.text.trim() ||
|
||||||
|
"暂无更新信息"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
updateTime = getLastChapterText();
|
||||||
|
}
|
||||||
|
let description = document
|
||||||
|
.querySelector("p.comics-detail__desc")
|
||||||
|
.text.trim();
|
||||||
|
let chapters = new Map();
|
||||||
|
let i = 0;
|
||||||
|
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"
|
||||||
|
)) {
|
||||||
|
chapters.set(i.toString(), c.text.trim());
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i === 0) {
|
||||||
|
// 将倒序的最新章节反转
|
||||||
|
const spans = Array.from(
|
||||||
|
document.querySelectorAll("div.comics-chapters > a > div > span")
|
||||||
|
).reverse();
|
||||||
|
for (let c of spans) {
|
||||||
|
chapters.set(i.toString(), c.text.trim());
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let recommend = [];
|
||||||
|
for (let c of document.querySelectorAll("div.recommend--item")) {
|
||||||
|
if (c.querySelectorAll("div.tag-comic").length > 0) {
|
||||||
|
let title = c.querySelector("span").text.trim();
|
||||||
|
let cover = c.querySelector("amp-img").attributes["src"];
|
||||||
|
let url = c.querySelector("a").attributes["href"];
|
||||||
|
let id = url.split("/").pop();
|
||||||
|
recommend.push({
|
||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: title,
|
||||||
cover: cover,
|
cover: cover,
|
||||||
tags: tags,
|
});
|
||||||
description: description
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// updateTime 将 Y年 M月 D日 转化为 Y-M-D
|
||||||
|
let updateDate = updateTime
|
||||||
|
.replace(/年/g, "-")
|
||||||
|
.replace(/月/g, "-")
|
||||||
|
.replace(/日/g, "");
|
||||||
|
|
||||||
parseJsonComic(e) {
|
return new ComicDetails({
|
||||||
return {
|
title: title,
|
||||||
id: e.comic_id,
|
cover: cover,
|
||||||
title: e.name,
|
description: description,
|
||||||
subTitle: e.author,
|
tags: {
|
||||||
cover: `https://static-tw.baozimh.com/cover/${e.topic_img}?w=285&h=375&q=100`,
|
作者: [author],
|
||||||
tags: e.type_names,
|
标签: tags,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 探索页面
|
|
||||||
/// 一个漫画源可以有多个探索页面
|
|
||||||
explore = [{
|
|
||||||
/// 标题
|
|
||||||
/// 标题同时用作标识符, 不能重复
|
|
||||||
title: "包子漫画",
|
|
||||||
|
|
||||||
/// singlePageWithMultiPart 或者 multiPageComicList
|
|
||||||
type: "singlePageWithMultiPart",
|
|
||||||
|
|
||||||
load: async () => {
|
|
||||||
var res = await Network.get(this.baseUrl)
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw "Invalid status code: " + res.status
|
|
||||||
}
|
|
||||||
let document = new HtmlDocument(res.body)
|
|
||||||
let parts = document.querySelectorAll("div.index-recommend-items")
|
|
||||||
let result = {}
|
|
||||||
for (let part of parts) {
|
|
||||||
let title = part.querySelector("div.catalog-title").text.trim()
|
|
||||||
let comics = part.querySelectorAll("div.comics-card").map(e => this.parseComic(e))
|
|
||||||
if (comics.length > 0) {
|
|
||||||
result[title] = comics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
/// 分类页面
|
|
||||||
/// 一个漫画源只能有一个分类页面, 也可以没有, 设置为null禁用分类页面
|
|
||||||
category = {
|
|
||||||
/// 标题, 同时为标识符, 不能与其他漫画源的分类页面重复
|
|
||||||
title: "包子漫画",
|
|
||||||
parts: [{
|
|
||||||
name: "类型",
|
|
||||||
|
|
||||||
// fixed 或者 random
|
|
||||||
// random用于分类数量相当多时, 随机显示其中一部分
|
|
||||||
type: "fixed",
|
|
||||||
|
|
||||||
// 如果类型为random, 需要提供此字段, 表示同时显示的数量
|
|
||||||
// randomNumber: 5,
|
|
||||||
|
|
||||||
categories: ['全部', '恋爱', '纯爱', '古风', '异能', '悬疑', '剧情', '科幻', '奇幻', '玄幻', '穿越', '冒险', '推理', '武侠', '格斗', '战争', '热血', '搞笑', '大女主', '都市', '总裁', '后宫', '日常', '韩漫', '少年', '其它'],
|
|
||||||
|
|
||||||
// category或者search
|
|
||||||
// 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画
|
|
||||||
// 如果为search, 将进入搜索页面
|
|
||||||
itemType: "category",
|
|
||||||
|
|
||||||
// 若提供, 数量需要和`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']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
enableRankingPage: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 分类漫画页面, 即点击分类标签后进入的页面
|
|
||||||
categoryComics = {
|
|
||||||
load: async (category, param, options, page) => {
|
|
||||||
let res = await Network.get(`${this.baseUrl}/api/bzmhq/amp_comic_list?type=${param}®ion=${options[0]}&state=${options[1]}&filter=%2a&page=${page}&limit=36&language=${this.lang}&__amp_source_origin=${this.baseUrl}`)
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw "Invalid status code: " + res.status
|
|
||||||
}
|
|
||||||
let maxPage = null
|
|
||||||
let json = JSON.parse(res.body)
|
|
||||||
if (!json.next) {
|
|
||||||
maxPage = page
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
comics: json.items.map(e => this.parseJsonComic(e)),
|
|
||||||
maxPage: maxPage
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// 提供选项
|
chapters: chapters,
|
||||||
optionList: [{
|
recommend: recommend,
|
||||||
options: [
|
updateTime: updateDate,
|
||||||
"all-全部",
|
});
|
||||||
"cn-国漫",
|
},
|
||||||
"jp-日本",
|
loadEp: async (comicId, epId) => {
|
||||||
"kr-韩国",
|
const images = [];
|
||||||
"en-欧美",
|
let currentPageUrl = `${this.baseUrl}/comic/chapter/${comicId}/0_${epId}.html`;
|
||||||
],
|
let maxAttempts = 100;
|
||||||
}, {
|
|
||||||
options: [
|
|
||||||
"all-全部",
|
|
||||||
"serial-连载中",
|
|
||||||
"pub-已完结",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 搜索
|
while (maxAttempts > 0) {
|
||||||
search = {
|
const res = await Network.get(currentPageUrl);
|
||||||
load: async (keyword, options, page) => {
|
if (res.status !== 200) break;
|
||||||
let res = await Network.get(`${this.baseUrl}/search?q=${keyword}`)
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw "Invalid status code: " + res.status
|
|
||||||
}
|
|
||||||
let document = new HtmlDocument(res.body)
|
|
||||||
let comics = document.querySelectorAll("div.comics-card").map(e => this.parseComic(e))
|
|
||||||
return {
|
|
||||||
comics: comics,
|
|
||||||
maxPage: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 提供选项
|
// 解析当前页图片
|
||||||
optionList: []
|
const doc = new HtmlDocument(res.body);
|
||||||
}
|
doc
|
||||||
|
.querySelectorAll("ul.comic-contain > div > amp-img")
|
||||||
|
.forEach((img) => {
|
||||||
|
const src = img?.attributes?.["src"];
|
||||||
|
if (typeof src === "string") images.push(src);
|
||||||
|
});
|
||||||
|
|
||||||
/// 收藏
|
// 查找下一页链接
|
||||||
favorites = {
|
const nextLink = doc.querySelector("a#next-chapter");
|
||||||
/// 是否为多收藏夹
|
if (nextLink?.text?.match(/下一页|下一頁/)) {
|
||||||
multiFolder: false,
|
currentPageUrl = nextLink.attributes["href"];
|
||||||
/// 添加或者删除收藏
|
} else {
|
||||||
addOrDelFavorite: async (comicId, folderId, isAdding) => {
|
break;
|
||||||
if (!isAdding) {
|
|
||||||
let res = await Network.post(`${this.baseUrl}/user/operation_v2?op=del_bookmark&comic_id=${comicId}`)
|
|
||||||
if (!res.status || res.status >= 400) {
|
|
||||||
throw "Invalid status code: " + res.status
|
|
||||||
}
|
|
||||||
return 'ok'
|
|
||||||
} else {
|
|
||||||
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) {
|
|
||||||
throw "Invalid status code: " + res.status
|
|
||||||
}
|
|
||||||
return 'ok'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 加载收藏夹, 仅当multiFolder为true时有效
|
|
||||||
// 当comicId不为null时, 需要同时返回包含该漫画的收藏夹
|
|
||||||
loadFolders: null,
|
|
||||||
/// 加载漫画
|
|
||||||
loadComics: async (page, folder) => {
|
|
||||||
let res = await Network.get(`${this.baseUrl}/user/my_bookshelf`)
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw "Invalid status code: " + res.status
|
|
||||||
}
|
|
||||||
let document = new HtmlDocument(res.body)
|
|
||||||
function parseComic(e) {
|
|
||||||
let title = e.querySelector("h4 > a").text.trim()
|
|
||||||
let url = e.querySelector("h4 > a").attributes['href']
|
|
||||||
let id = url.split("/").pop()
|
|
||||||
let author = e.querySelector("div.info > ul").children[1].text.split(":")[1].trim()
|
|
||||||
let description = e.querySelector("div.info > ul").children[4].children[0].text.trim()
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
title: title,
|
|
||||||
subTitle: author,
|
|
||||||
description: description,
|
|
||||||
cover: e.querySelector("amp-img").attributes['src']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let comics = document.querySelectorAll("div.bookshelf-items").map(e => parseComic(e))
|
|
||||||
return {
|
|
||||||
comics: comics,
|
|
||||||
maxPage: 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
maxAttempts--;
|
||||||
|
}
|
||||||
/// 单个漫画相关
|
// 代理后图片水印更少
|
||||||
comic = {
|
let mobileImages = images.map((e) => {
|
||||||
// 加载漫画信息
|
const regex = /scomic\/.*/;
|
||||||
loadInfo: async (id) => {
|
const match = e.match(regex);
|
||||||
let res = await Network.get(`${this.baseUrl}/comic/${id}`)
|
return `https://as-rsa1-usla.baozicdn.com/w640/${match[0]}`;
|
||||||
if (res.status !== 200) {
|
});
|
||||||
throw "Invalid status code: " + res.status
|
return { images: mobileImages };
|
||||||
}
|
},
|
||||||
let document = new HtmlDocument(res.body)
|
};
|
||||||
|
|
||||||
let title = document.querySelector("h1.comics-detail__title").text.trim()
|
|
||||||
let cover = document.querySelector("div.l-content > div > div > amp-img").attributes['src']
|
|
||||||
let author = document.querySelector("h2.comics-detail__author").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) {
|
|
||||||
const getLastChapterText = () => {
|
|
||||||
// 合并所有章节容器(处理可能存在多个列表的情况)
|
|
||||||
const containers = [
|
|
||||||
...document.querySelectorAll("#chapter-items, #chapters_other_list")
|
|
||||||
];
|
|
||||||
let allChapters = [];
|
|
||||||
containers.forEach(container => {
|
|
||||||
const chapters = container.querySelectorAll(".comics-chapters > a");
|
|
||||||
allChapters.push(...Array.from(chapters));
|
|
||||||
});
|
|
||||||
const lastChapter = allChapters[allChapters.length - 1];
|
|
||||||
return lastChapter?.querySelector("div > span")?.text.trim() || "暂无更新信息";
|
|
||||||
};
|
|
||||||
updateTime = getLastChapterText();
|
|
||||||
}
|
|
||||||
let description = document.querySelector("p.comics-detail__desc").text.trim()
|
|
||||||
let chapters = new Map()
|
|
||||||
let i = 0
|
|
||||||
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")) {
|
|
||||||
chapters.set(i.toString(), c.text.trim())
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if (i === 0) {
|
|
||||||
// 将倒序的最新章节反转
|
|
||||||
const spans = Array.from(document.querySelectorAll("div.comics-chapters > a > div > span")).reverse();
|
|
||||||
for (let c of spans) {
|
|
||||||
chapters.set(i.toString(), c.text.trim());
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let recommend = []
|
|
||||||
for (let c of document.querySelectorAll("div.recommend--item")) {
|
|
||||||
if (c.querySelectorAll("div.tag-comic").length > 0) {
|
|
||||||
let title = c.querySelector("span").text.trim()
|
|
||||||
let cover = c.querySelector("amp-img").attributes['src']
|
|
||||||
let url = c.querySelector("a").attributes['href']
|
|
||||||
let id = url.split("/").pop()
|
|
||||||
recommend.push({
|
|
||||||
id: id,
|
|
||||||
title: title,
|
|
||||||
cover: cover
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// updateTime 将 Y年 M月 D日 转化为 Y-M-D
|
|
||||||
let updateDate = updateTime.replace(/年/g, '-').replace(/月/g, '-').replace(/日/g, '');
|
|
||||||
|
|
||||||
return new ComicDetails({
|
|
||||||
title: title,
|
|
||||||
cover: cover,
|
|
||||||
description: description,
|
|
||||||
tags: {
|
|
||||||
"作者": [author],
|
|
||||||
"标签": tags
|
|
||||||
},
|
|
||||||
chapters: chapters,
|
|
||||||
recommend: recommend,
|
|
||||||
updateTime: updateDate,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadEp: async (comicId, epId) => {
|
|
||||||
const images = [];
|
|
||||||
let currentPageUrl = `${this.baseUrl}/comic/chapter/${comicId}/0_${epId}.html`;
|
|
||||||
let maxAttempts = 100;
|
|
||||||
|
|
||||||
while (maxAttempts > 0) {
|
|
||||||
const res = await Network.get(currentPageUrl);
|
|
||||||
if (res.status !== 200) break;
|
|
||||||
|
|
||||||
// 解析当前页图片
|
|
||||||
const doc = new HtmlDocument(res.body);
|
|
||||||
doc.querySelectorAll("ul.comic-contain > div > amp-img").forEach(img => {
|
|
||||||
const src = img?.attributes?.['src'];
|
|
||||||
if (typeof src === 'string') images.push(src);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 查找下一页链接
|
|
||||||
const nextLink = doc.querySelector("a#next-chapter");
|
|
||||||
if (nextLink?.text?.match(/下一页|下一頁/)) {
|
|
||||||
currentPageUrl = nextLink.attributes['href'];
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
maxAttempts--;
|
|
||||||
}
|
|
||||||
// 代理后图片水印更少
|
|
||||||
return { images };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
22
index.json
22
index.json
@@ -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",
|
||||||
@@ -85,5 +85,23 @@
|
|||||||
"fileName": "ykmh.js",
|
"fileName": "ykmh.js",
|
||||||
"key": "ykmh",
|
"key": "ykmh",
|
||||||
"version": "1.0.0"
|
"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": "优酷漫画",
|
||||||
|
"fileName": "ykmh.js",
|
||||||
|
"key": "ykmh",
|
||||||
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
862
manhuagui.js
Normal file
862
manhuagui.js
Normal 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
418
zaimanhua.js
Normal 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×tamp=1753547675981&uid=0
|
||||||
|
let api = `${this.baseUrl}/api/v1/comic1/recommend/list`;
|
||||||
|
let params = {
|
||||||
|
channel: "pc",
|
||||||
|
app_name: "zmh",
|
||||||
|
version: "1.0.0",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
uid: 0,
|
||||||
|
};
|
||||||
|
let params_str = Object.keys(params)
|
||||||
|
.map((key) => `${key}=${params[key]}`)
|
||||||
|
.join("&");
|
||||||
|
let url = `${api}?${params_str}`;
|
||||||
|
const json = await this.fetchJson(url);
|
||||||
|
let data = json.list;
|
||||||
|
data.shift(); // 去掉第一个
|
||||||
|
data.pop(); // 去掉最后一个
|
||||||
|
data.map((arr) => {
|
||||||
|
let title = arr.name;
|
||||||
|
let comic_list = arr.list.map((item) => this.parseJsonComic(item));
|
||||||
|
result[title] = comic_list;
|
||||||
|
});
|
||||||
|
|
||||||
|
log("error", "再看漫画", result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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 {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user