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