mirror of
https://github.com/venera-app/venera-configs.git
synced 2025-12-16 17:31:16 +00:00
Compare commits
8 Commits
a14e7552a5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d469bce4d | ||
|
|
0e36a2a97c | ||
|
|
5a323c416b | ||
|
|
c8de505945 | ||
|
|
6a0e667775 | ||
|
|
a184ea975d | ||
|
|
504766de0e | ||
|
|
89d9573f65 |
28
ccc.js
28
ccc.js
@@ -8,7 +8,7 @@ class CCC extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "ccc"
|
||||
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
|
||||
minAppVersion = "1.6.0"
|
||||
|
||||
@@ -40,7 +40,16 @@ class CCC extends ComicSource {
|
||||
"client_secret": "9eAhsCX3VWtyqTmkUo5EEaoH4MNPxrn6ZRwse7tE",
|
||||
"refresh_token": this.loadData("refreshToken")
|
||||
});
|
||||
this.processToken(res.body);
|
||||
if (res.body.search("Token has been revoked") == -1) {
|
||||
this.processToken(res.body);
|
||||
} else {
|
||||
const accountData = this.loadData("account");
|
||||
if (accountData) {
|
||||
await this.account.login(accountData[0], accountData[1]);
|
||||
} else {
|
||||
throw "請重新登錄";
|
||||
}
|
||||
}
|
||||
token = this.loadData("token");
|
||||
}
|
||||
return {
|
||||
@@ -76,7 +85,7 @@ class CCC extends ComicSource {
|
||||
title: c["name"],
|
||||
subtitle: c["brief"],
|
||||
description: c["description"],
|
||||
cover: c["image1"],
|
||||
cover: c["image1"]??c["image2"]??c["image3"],
|
||||
tags: tags
|
||||
});
|
||||
}
|
||||
@@ -185,7 +194,7 @@ class CCC extends ComicSource {
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
let curTitle = null;
|
||||
for (let data of jsonData["templates"]) {
|
||||
if (data["type"] == 4) {
|
||||
if ([4, 5].indexOf(data["type"]) != -1) {
|
||||
continue;
|
||||
}
|
||||
const comics = [];
|
||||
@@ -193,7 +202,7 @@ class CCC extends ComicSource {
|
||||
comics.push({
|
||||
id: c["value"],
|
||||
title: c["name"],
|
||||
cover: c["image1"],
|
||||
cover: c["image1"]??c["image2"]??c["image3"],
|
||||
tags: [c["book_type"]["name"]],
|
||||
subtitle: c["brief"]
|
||||
});
|
||||
@@ -539,7 +548,7 @@ class CCC extends ComicSource {
|
||||
for (let r of recommendData["hot"]) {
|
||||
recommends.push({
|
||||
title: r["name"],
|
||||
cover: r["image1"],
|
||||
cover: r["image1"]??r["image2"]??r["image3"],
|
||||
id: r["id"].toString(),
|
||||
subtitle: r["brief"]
|
||||
});
|
||||
@@ -547,21 +556,21 @@ class CCC extends ComicSource {
|
||||
for (let r of recommendData["history"]) {
|
||||
recommends.push({
|
||||
title: r["name"],
|
||||
cover: r["image1"],
|
||||
cover: r["image1"]??r["image2"]??r["image3"],
|
||||
id: r["id"].toString()
|
||||
});
|
||||
}
|
||||
for (let r of recommendData["also_buy"]) {
|
||||
recommends.push({
|
||||
title: r["name"],
|
||||
cover: r["image1"],
|
||||
cover: r["image1"]??r["image2"]??r["image3"],
|
||||
id: r["id"].toString()
|
||||
});
|
||||
}
|
||||
return new ComicDetails({
|
||||
title: jsonData["name"],
|
||||
subtitle: jsonData["brief"],
|
||||
cover: jsonData["image1"],
|
||||
cover: jsonData["image1"]??jsonData["image2"]??jsonData["image3"],
|
||||
description: jsonData["description"],
|
||||
likesCount: jsonData["like_count_only_uuid"],
|
||||
chapters: chapters,
|
||||
@@ -755,4 +764,5 @@ class CCC extends ComicSource {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
11
ehentai.js
11
ehentai.js
@@ -7,7 +7,7 @@ class Ehentai extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "ehentai"
|
||||
|
||||
version = "1.1.7"
|
||||
version = "1.1.8"
|
||||
|
||||
minAppVersion = "1.5.3"
|
||||
|
||||
@@ -738,6 +738,10 @@ class Ehentai extends ComicSource {
|
||||
let category = document.querySelector("div.cs").text;
|
||||
tags.set("Category", [category])
|
||||
|
||||
if (uploader) {
|
||||
tags.set("uploader", [uploader]);
|
||||
}
|
||||
|
||||
let time = document.querySelector("div#gdd > table > tbody > tr > td.gdt2").text
|
||||
|
||||
let script = document.querySelectorAll("script").find((e) => e.text.includes("var token"));
|
||||
@@ -763,7 +767,7 @@ class Ehentai extends ComicSource {
|
||||
stars: stars,
|
||||
maxPage: Number(maxPage),
|
||||
isFavorite: isFavorited,
|
||||
uploader: uploader,
|
||||
// uploader: uploader,
|
||||
uploadTime: time,
|
||||
url: id,
|
||||
comments: comments.comments,
|
||||
@@ -1450,6 +1454,7 @@ class Ehentai extends ComicSource {
|
||||
"group": "团队",
|
||||
"cosplayer": "Coser",
|
||||
"reclass": "重新分类",
|
||||
"uploader": "上传者",
|
||||
"Languages": "语言",
|
||||
"Artists": "画师",
|
||||
"Characters": "角色",
|
||||
@@ -1486,6 +1491,7 @@ class Ehentai extends ComicSource {
|
||||
"group": "團隊",
|
||||
"cosplayer": "Coser",
|
||||
"reclass": "重新分類",
|
||||
"uploader": "上傳者",
|
||||
"Languages": "語言",
|
||||
"Artists": "畫師",
|
||||
"Characters": "角色",
|
||||
@@ -1522,6 +1528,7 @@ class Ehentai extends ComicSource {
|
||||
"group": "Group",
|
||||
"cosplayer": "Cosplayer",
|
||||
"reclass": "Reclass",
|
||||
"uploader": "Uploader",
|
||||
"Languages": "Languages",
|
||||
"Artists": "Artists",
|
||||
"Characters": "Characters",
|
||||
|
||||
28
index.json
28
index.json
@@ -9,7 +9,7 @@
|
||||
"name": "Komiic",
|
||||
"fileName": "komiic.js",
|
||||
"key": "Komiic",
|
||||
"version": "1.0.2"
|
||||
"version": "1.0.3"
|
||||
},
|
||||
{
|
||||
"name": "包子漫画",
|
||||
@@ -33,14 +33,14 @@
|
||||
"name": "紳士漫畫",
|
||||
"fileName": "wnacg.js",
|
||||
"key": "wnacg",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"description": "紳士漫畫漫畫源, 不能使用時請嘗試更換URL"
|
||||
},
|
||||
{
|
||||
"name": "ehentai",
|
||||
"fileName": "ehentai.js",
|
||||
"key": "ehentai",
|
||||
"version": "1.1.7"
|
||||
"version": "1.1.8"
|
||||
},
|
||||
{
|
||||
"name": "禁漫天堂",
|
||||
@@ -93,10 +93,10 @@
|
||||
"version": "1.0.2"
|
||||
},
|
||||
{
|
||||
"name": "漫画柜",
|
||||
"fileName": "manhuagui.js",
|
||||
"key": "ManHuaGui",
|
||||
"version": "1.2.0"
|
||||
"name": "漫画柜",
|
||||
"fileName": "manhuagui.js",
|
||||
"key": "ManHuaGui",
|
||||
"version": "1.2.1"
|
||||
},
|
||||
{
|
||||
"name": "漫蛙吧",
|
||||
@@ -132,7 +132,7 @@
|
||||
"name": "CCC追漫台",
|
||||
"fileName": "ccc.js",
|
||||
"key": "ccc",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.1"
|
||||
},
|
||||
{
|
||||
"name": "GoDa漫画",
|
||||
@@ -145,5 +145,17 @@
|
||||
"fileName": "mh18.js",
|
||||
"key": "mh18",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "漫小肆",
|
||||
"fileName": "mxs.js",
|
||||
"key": "mxs",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "漫画人",
|
||||
"fileName": "manhuaren.js",
|
||||
"key": "manhuaren",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
|
||||
11
komiic.js
11
komiic.js
@@ -6,7 +6,7 @@ class Komiic extends ComicSource {
|
||||
// 唯一标识符
|
||||
key = "Komiic"
|
||||
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
|
||||
minAppVersion = "1.0.0"
|
||||
|
||||
@@ -27,8 +27,6 @@ class Komiic extends ComicSource {
|
||||
}
|
||||
|
||||
async queryJson(query) {
|
||||
let operationName = query["operationName"]
|
||||
|
||||
let res = await Network.post(
|
||||
'https://komiic.com/api/query',
|
||||
this.headers,
|
||||
@@ -42,8 +40,11 @@ class Komiic extends ComicSource {
|
||||
let json = JSON.parse(res.body)
|
||||
|
||||
if (json.errors != undefined) {
|
||||
if(json.errors[0].message.toString().indexOf('token is expired') >= 0){
|
||||
throw 'Login expired'
|
||||
const errorInfo = json.errors[0].message.toString();
|
||||
if ((errorInfo.indexOf('token is expired') >= 0) || (errorInfo.indexOf('no token') >= 0)) {
|
||||
const accountData = this.loadData("account");
|
||||
await this.account.login(accountData[0], accountData[1]);
|
||||
return await this.queryJson(query);
|
||||
}
|
||||
throw json.errors[0].message
|
||||
}
|
||||
|
||||
66
manhuagui.js
66
manhuagui.js
@@ -4,7 +4,7 @@ class ManHuaGui extends ComicSource {
|
||||
|
||||
key = "ManHuaGui";
|
||||
|
||||
version = "1.2.0";
|
||||
version = "1.2.1";
|
||||
|
||||
minAppVersion = "1.4.0";
|
||||
|
||||
@@ -934,27 +934,59 @@ class ManHuaGui extends ComicSource {
|
||||
// 查找所有章节分组标题
|
||||
let chapterGroups = chapterDocument.querySelectorAll(".chapter h4 span");
|
||||
if (chapterGroups.length === 0) {
|
||||
chapterDocument = document;
|
||||
chapterGroups = chapterDocument.querySelectorAll(".chapter h4 span");
|
||||
let docGroups = document.querySelectorAll(".chapter h4 span");
|
||||
if (docGroups.length > 0) {
|
||||
chapterDocument = document;
|
||||
chapterGroups = docGroups;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理每个分组
|
||||
for (let i = 0; i < chapterGroups.length; i++) {
|
||||
let groupName = chapterGroups[i].text.trim();
|
||||
let groupChapters = new Map();
|
||||
|
||||
let chapterList = chapterDocument.querySelectorAll(".chapter-list")[i];
|
||||
if (chapterList) {
|
||||
let lis = chapterList.querySelectorAll("li");
|
||||
for (let li of lis) {
|
||||
let a = li.querySelector("a");
|
||||
let id = a.attributes["href"].split("/").pop().replace(".html", "");
|
||||
let title = a.querySelector("span").text.trim();
|
||||
groupChapters.set(id, title);
|
||||
if (chapterGroups.length > 0) {
|
||||
// 处理每个分组
|
||||
for (let i = 0; i < chapterGroups.length; i++) {
|
||||
let groupName = chapterGroups[i].text.trim();
|
||||
let groupChapters = new Map();
|
||||
|
||||
let chapterList = chapterDocument.querySelectorAll(".chapter-list")[i];
|
||||
if (chapterList) {
|
||||
let lis = chapterList.querySelectorAll("li");
|
||||
for (let li of lis) {
|
||||
let a = li.querySelector("a");
|
||||
let id = a.attributes["href"].split("/").pop().replace(".html", "");
|
||||
let title = a.querySelector("span").text.trim();
|
||||
groupChapters.set(id, title);
|
||||
}
|
||||
|
||||
groupChapters = new Map([...groupChapters].sort((a, b) => a[0] - b[0]));
|
||||
|
||||
chaptersMap.set(groupName, groupChapters);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有分组标题的情况,直接查找章节列表
|
||||
let chapterLists = chapterDocument.querySelectorAll(".chapter-list");
|
||||
if (chapterLists.length === 0 && chapterDocument !== document) {
|
||||
chapterDocument = document;
|
||||
chapterLists = chapterDocument.querySelectorAll(".chapter-list");
|
||||
}
|
||||
|
||||
if (chapterLists.length > 0) {
|
||||
let groupName = "连载";
|
||||
let groupChapters = new Map();
|
||||
|
||||
for (let chapterList of chapterLists) {
|
||||
let lis = chapterList.querySelectorAll("li");
|
||||
for (let li of lis) {
|
||||
let a = li.querySelector("a");
|
||||
if (a) {
|
||||
let id = a.attributes["href"].split("/").pop().replace(".html", "");
|
||||
let title = a.querySelector("span").text.trim();
|
||||
groupChapters.set(id, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupChapters = new Map([...groupChapters].sort((a, b) => a[0] - b[0]));
|
||||
|
||||
chaptersMap.set(groupName, groupChapters);
|
||||
}
|
||||
}
|
||||
|
||||
979
manhuaren.js
Normal file
979
manhuaren.js
Normal file
@@ -0,0 +1,979 @@
|
||||
/** @type {import('../_venera_.js')} */
|
||||
class ManHuaRen extends ComicSource {
|
||||
name = "漫画人"
|
||||
|
||||
key = "manhuaren"
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
minAppVersion = "1.6.0"
|
||||
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/manhuaren.js"
|
||||
|
||||
|
||||
init() {
|
||||
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return "https://www.manhuaren.com";
|
||||
}
|
||||
|
||||
// helper to build common request headers
|
||||
_buildHeaders() {
|
||||
return {
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36',
|
||||
'accept': '*/*',
|
||||
'accept-encoding': 'gzip, deflate, br, zstd',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'cache-control': 'no-cache',
|
||||
'pragma': 'no-cache',
|
||||
'sec-ch-ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
|
||||
'sec-ch-ua-mobile': '?1',
|
||||
'sec-ch-ua-platform': '"Android"',
|
||||
'host': 'www.manhuaren.com'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_buildImageHeaders(imageUrl, referer) {
|
||||
let host = '';
|
||||
try {
|
||||
let u = new URL(imageUrl);
|
||||
host = u.host;
|
||||
} catch (e) {
|
||||
// fallback: try to extract host from string
|
||||
let m = imageUrl.match(/^https?:\/\/([^\/]+)/i);
|
||||
host = m ? m[1] : '';
|
||||
}
|
||||
|
||||
return {
|
||||
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
//'Host': host || '',
|
||||
'Pragma': 'no-cache',
|
||||
'Referer': referer || (this.baseUrl + '/'),
|
||||
'Sec-Fetch-Dest': 'image',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'Sec-Fetch-Storage-Access': 'active',
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36',
|
||||
'sec-ch-ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
|
||||
'sec-ch-ua-mobile': '?1',
|
||||
'sec-ch-ua-platform': '"Android"'
|
||||
}
|
||||
}
|
||||
|
||||
// explore page list
|
||||
explore = [
|
||||
{
|
||||
title: "漫画人",
|
||||
type: "multiPartPage",
|
||||
load: async (page) => {
|
||||
let url = this.baseUrl + '/';
|
||||
let res = await Network.get(
|
||||
url,
|
||||
this._buildHeaders()
|
||||
);
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`
|
||||
}
|
||||
|
||||
let html = res.body || '';
|
||||
let doc = new HtmlDocument(html);
|
||||
let parts = [];
|
||||
|
||||
// Banner
|
||||
let banner = doc.querySelector('.index-banner');
|
||||
if (banner) {
|
||||
let comics = [];
|
||||
let items = banner.querySelectorAll('li');
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let item = items[i];
|
||||
let a = item.querySelector('a');
|
||||
if (!a) continue;
|
||||
let img = item.querySelector('img');
|
||||
|
||||
let href = a.attributes['href'];
|
||||
let title = a.attributes['title'];
|
||||
let cover = img ? (img.attributes['src'] || img.attributes['data-src']) : '';
|
||||
|
||||
if (href) {
|
||||
if (!href.startsWith('http')) href = this.baseUrl + href;
|
||||
if (cover && !cover.startsWith('http')) {
|
||||
if (cover.startsWith('//')) cover = 'https:' + cover;
|
||||
else cover = this.baseUrl + cover;
|
||||
}
|
||||
comics.push(new Comic({
|
||||
id: href,
|
||||
title: title || '',
|
||||
cover: cover || '',
|
||||
description: ''
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (comics.length > 0) {
|
||||
parts.push({ title: '热门推荐', comics: comics });
|
||||
}
|
||||
}
|
||||
|
||||
// Lists
|
||||
let lists = doc.querySelectorAll('.manga-list');
|
||||
for (let i = 0; i < lists.length; i++) {
|
||||
let list = lists[i];
|
||||
let titleNode = list.querySelector('.manga-list-title');
|
||||
let title = titleNode ? titleNode.text.trim() : '';
|
||||
|
||||
let viewMore = null;
|
||||
if (titleNode) {
|
||||
let moreNode = titleNode.querySelector('a');
|
||||
if (moreNode) {
|
||||
let href = moreNode.attributes['href'];
|
||||
if (href) {
|
||||
if (!href.startsWith('http')) href = this.baseUrl + href;
|
||||
viewMore = href;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let comics = [];
|
||||
let items = list.querySelectorAll('li');
|
||||
for (let j = 0; j < items.length; j++) {
|
||||
let item = items[j];
|
||||
let a = item.querySelector('a');
|
||||
if (!a) continue;
|
||||
|
||||
let href = a.attributes['href'];
|
||||
let comicTitle = a.attributes['title'];
|
||||
|
||||
if (!comicTitle) {
|
||||
let t = item.querySelector('.manga-list-2-title');
|
||||
if (t) comicTitle = t.text.trim();
|
||||
}
|
||||
|
||||
let img = item.querySelector('img');
|
||||
let cover = img ? (img.attributes['data-src'] || img.attributes['src']) : '';
|
||||
|
||||
let tip = item.querySelector('.manga-list-1-tip') || item.querySelector('.manga-list-2-tip');
|
||||
let desc = tip ? tip.text.trim() : '';
|
||||
|
||||
let badgeNode = item.querySelector('.manga-list-1-cover-logo-font');
|
||||
let badge = badgeNode ? badgeNode.text.trim() : '';
|
||||
|
||||
if (href) {
|
||||
if (!href.startsWith('http')) href = this.baseUrl + href;
|
||||
if (cover && !cover.startsWith('http')) {
|
||||
if (cover.startsWith('//')) cover = 'https:' + cover;
|
||||
else cover = this.baseUrl + cover;
|
||||
}
|
||||
|
||||
comics.push(new Comic({
|
||||
id: href,
|
||||
title: comicTitle || '',
|
||||
cover: cover || '',
|
||||
description: desc,
|
||||
tags: badge ? [badge] : []
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (comics.length > 0) {
|
||||
if (!title) {
|
||||
if (comics[0].tags && comics[0].tags.length > 0) {
|
||||
title = comics[0].tags[0];
|
||||
} else {
|
||||
title = '漫画列表';
|
||||
}
|
||||
}
|
||||
let part = { title: title, comics: comics };
|
||||
if (viewMore) part.viewMore = viewMore;
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
},
|
||||
loadNext(next) { }
|
||||
}
|
||||
]
|
||||
|
||||
// categories
|
||||
category = {
|
||||
/// title of the category page, used to identify the page, it should be unique
|
||||
title: "漫画人",
|
||||
parts: [
|
||||
{
|
||||
// title of the part
|
||||
name: "类型",
|
||||
|
||||
// fixed list of categories
|
||||
type: "fixed",
|
||||
itemType: "category",
|
||||
|
||||
// human readable categories and params mapped in categoryParams
|
||||
categories: [
|
||||
"全部",
|
||||
"热血",
|
||||
"恋爱",
|
||||
"校园",
|
||||
"伪娘",
|
||||
"冒险",
|
||||
"职场",
|
||||
"后宫",
|
||||
"治愈",
|
||||
"科幻",
|
||||
"轻小说",
|
||||
"励志",
|
||||
"生活",
|
||||
"战争",
|
||||
"悬疑",
|
||||
"推理",
|
||||
"搞笑",
|
||||
"奇幻",
|
||||
"魔法",
|
||||
"神鬼",
|
||||
"萌系",
|
||||
"历史",
|
||||
"美食",
|
||||
"同人",
|
||||
"运动",
|
||||
"绅士",
|
||||
"机甲",
|
||||
"百合",
|
||||
],
|
||||
// corresponding params (tag ids). Keep order aligned with `categories` above.
|
||||
categoryParams: [
|
||||
"", // 全部
|
||||
"31", // 热血
|
||||
"26", // 恋爱
|
||||
"1", // 校园
|
||||
"5", // 伪娘
|
||||
"2", // 冒险
|
||||
"6", // 职场
|
||||
"8", // 后宫
|
||||
"9", // 治愈
|
||||
"25", // 科幻
|
||||
"156", // 轻小说
|
||||
"10", // 励志
|
||||
"11", // 生活
|
||||
"12", // 战争
|
||||
"17", // 悬疑
|
||||
"33", // 推理
|
||||
"37", // 搞笑
|
||||
"14", // 奇幻
|
||||
"15", // 魔法
|
||||
"20", // 神鬼
|
||||
"21", // 萌系
|
||||
"4", // 历史
|
||||
"7", // 美食
|
||||
"30", // 同人
|
||||
"34", // 运动
|
||||
"36", // 绅士
|
||||
"40", // 机甲
|
||||
"3", // 百合
|
||||
],
|
||||
}
|
||||
],
|
||||
// enable ranking page
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
categoryComics = {
|
||||
load: async (category, param, options, page) => {
|
||||
// param is expected to be the tag id (e.g. "31").
|
||||
let tag = param || '';
|
||||
|
||||
// options: [statusOption, sortOption]
|
||||
// option values use left side before '-' (e.g. 'st1-连载' -> 'st1')
|
||||
let statusOpt = (options && options[0]) ? options[0].split('-')[0] : '';
|
||||
let sortOpt = (options && options[1]) ? options[1].split('-')[0] : '';
|
||||
|
||||
// Build path like: manhua-list(-tag{tag})?(-{status})?(-{sort})?/dm5.ashx
|
||||
let path = 'manhua-list';
|
||||
if (tag) path += `-tag${tag}`;
|
||||
if (statusOpt) path += `-${statusOpt}`;
|
||||
if (sortOpt) path += `-${sortOpt}`;
|
||||
|
||||
let url = `${this.baseUrl}/${path}/dm5.ashx`;
|
||||
// POST body: use site form-data fields
|
||||
// action=getclasscomics&pageindex=3&pagesize=21&categoryid=0&tagid=0&status=1&usergroup=0&pay=-1&areaid=0&sort=2&iscopyright=0
|
||||
let pageIndex = Math.max(0, (parseInt(page) || 1));
|
||||
let pageSize = 21;
|
||||
// map status option like 'st1' -> 1, 'st2' -> 2
|
||||
let statusNum = 0;
|
||||
if (statusOpt && statusOpt.startsWith('st')) {
|
||||
let m = statusOpt.match(/st(\d+)/);
|
||||
if (m) statusNum = parseInt(m[1]);
|
||||
}
|
||||
// map sort option like 's2' -> 2, 's18' -> 18
|
||||
let sortNum = 0;
|
||||
if (sortOpt && sortOpt.startsWith('s')) {
|
||||
let m = sortOpt.match(/s(\d+)/);
|
||||
if (m) sortNum = parseInt(m[1]);
|
||||
}
|
||||
// tag id (tag param) - if empty use 0
|
||||
let tagId = tag && tag.length > 0 ? tag : '0';
|
||||
|
||||
let body = `action=getclasscomics&pageindex=${pageIndex}&pagesize=${pageSize}&categoryid=0&tagid=${encodeURIComponent(tagId)}&status=${statusNum}&usergroup=0&pay=-1&areaid=0&sort=${sortNum}&iscopyright=0`;
|
||||
|
||||
// 使用站点期望的 AJAX 请求头(不包含 cookie)
|
||||
let categoryHeaders = {
|
||||
'accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'accept-encoding': 'gzip, deflate, br, zstd',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'cache-control': 'no-cache',
|
||||
'connection': 'keep-alive',
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'host': 'www.manhuaren.com',
|
||||
'origin': this.baseUrl,
|
||||
'pragma': 'no-cache',
|
||||
'referer': `${this.baseUrl}/${path}/`,
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1',
|
||||
'x-requested-with': 'XMLHttpRequest'
|
||||
};
|
||||
|
||||
let res = await Network.post(url, categoryHeaders, body);
|
||||
if (res.status !== 200) {
|
||||
throw `加载分类漫画失败: ${res.status}`;
|
||||
}
|
||||
|
||||
let data = {};
|
||||
try {
|
||||
data = JSON.parse(res.body || '{}');
|
||||
} catch (e) {
|
||||
throw '解析分类返回数据失败';
|
||||
}
|
||||
|
||||
let items = data.UpdateComicItems || [];
|
||||
let comics = items.map(it => {
|
||||
// UrlKey already contains path like "manhua-xxxx"
|
||||
let id = it.UrlKey ? `/${it.UrlKey}/` : (it.ID ? `/m${it.ID}/` : '');
|
||||
let cover = it.ShowPicUrlB || it.ShowConver || '';
|
||||
if (cover && cover.startsWith('//')) cover = 'https:' + cover;
|
||||
if (cover && !cover.startsWith('http')) cover = this.baseUrl + cover;
|
||||
|
||||
let tags = [];
|
||||
if (it.Author && Array.isArray(it.Author)) tags = it.Author.slice(0,3);
|
||||
|
||||
return new Comic({
|
||||
id: id,
|
||||
title: it.Title,
|
||||
cover: cover,
|
||||
description: it.Content || '',
|
||||
tags: tags
|
||||
});
|
||||
});
|
||||
|
||||
let perPage = items.length || 20;
|
||||
let total = data.Count || 0;
|
||||
let maxPage = perPage > 0 ? Math.max(1, Math.ceil(total / perPage)) : (comics.length > 0 ? page + 1 : page);
|
||||
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: maxPage+1
|
||||
};
|
||||
},
|
||||
|
||||
// provide options for category comic loading: status and sort
|
||||
optionList: [
|
||||
{
|
||||
type: 'select',
|
||||
label: '状态',
|
||||
options: [
|
||||
'st0-全部',
|
||||
'st1-连载',
|
||||
'st2-已完结'
|
||||
],
|
||||
default: 'st0'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '排序',
|
||||
options: [
|
||||
's2-最近更新',
|
||||
's10-人气最旺',
|
||||
's18-最近上架'
|
||||
],
|
||||
default: 's2'
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
/// 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}/search?title=${encodeURIComponent(keyword)}&language=1&page=${page}`;
|
||||
|
||||
let res = await Network.get(url, this._buildHeaders());
|
||||
if (res.status !== 200) {
|
||||
throw `Search failed: ${res.status}`;
|
||||
}
|
||||
|
||||
let doc = new HtmlDocument(res.body);
|
||||
let comics = [];
|
||||
let list = doc.querySelectorAll('.book-list > li');
|
||||
|
||||
for (let item of list) {
|
||||
let link = item.querySelector('.book-list-info > a');
|
||||
let href = link?.attributes['href'];
|
||||
if (!href) continue;
|
||||
|
||||
if (!href.startsWith('http')) {
|
||||
href = this.baseUrl + href;
|
||||
}
|
||||
|
||||
let title = item.querySelector('.book-list-info-title')?.text?.trim();
|
||||
let coverEl = item.querySelector('.book-list-cover-img');
|
||||
let cover = coverEl?.attributes['src'];
|
||||
if (cover) {
|
||||
if (cover.startsWith('//')) cover = 'https:' + cover;
|
||||
else if (!cover.startsWith('http')) cover = this.baseUrl + cover;
|
||||
}
|
||||
|
||||
let desc = item.querySelector('.book-list-info-desc')?.text?.trim();
|
||||
|
||||
let tags = [];
|
||||
let tagEls = item.querySelectorAll('.book-list-info-bottom-item');
|
||||
for (let t of tagEls) {
|
||||
tags.push(t.text.trim());
|
||||
}
|
||||
|
||||
let status = item.querySelector('.book-list-info-bottom-right-font')?.text?.trim();
|
||||
if (status) tags.push(status);
|
||||
|
||||
comics.push(new Comic({
|
||||
id: href,
|
||||
title: title,
|
||||
cover: cover,
|
||||
description: desc,
|
||||
tags: tags
|
||||
}));
|
||||
}
|
||||
|
||||
let maxPage = comics.length > 0 ? page + 1 : page;
|
||||
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: maxPage
|
||||
};
|
||||
},
|
||||
|
||||
optionList: [],
|
||||
|
||||
enableTagsSuggestions: false,
|
||||
}
|
||||
|
||||
/// single comic related
|
||||
comic = {
|
||||
/**
|
||||
* load comic info
|
||||
* @param id {string}
|
||||
* @returns {Promise<ComicDetails>}
|
||||
*/
|
||||
loadInfo: async (id) => {
|
||||
if (!id || typeof id !== 'string') {
|
||||
throw "ID不能为空";
|
||||
}
|
||||
|
||||
let targetUrl = id;
|
||||
if (!targetUrl.startsWith('http')) {
|
||||
if (targetUrl.startsWith('/')) targetUrl = this.baseUrl + targetUrl;
|
||||
else targetUrl = this.baseUrl + '/' + targetUrl;
|
||||
}
|
||||
|
||||
let res = await Network.get(
|
||||
targetUrl,
|
||||
this._buildHeaders()
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
throw `请求失败,状态码: ${res.status},URL: ${targetUrl}`;
|
||||
}
|
||||
|
||||
let html = res.body || '';
|
||||
this.comic.id = id;
|
||||
|
||||
let toAbsUrl = (value) => {
|
||||
if (!value) return '';
|
||||
let trimmed = value.trim();
|
||||
if (trimmed.startsWith('http')) return trimmed;
|
||||
if (trimmed.startsWith('//')) return 'https:' + trimmed;
|
||||
if (trimmed.startsWith('/')) return this.baseUrl + trimmed;
|
||||
return this.baseUrl + '/' + trimmed;
|
||||
};
|
||||
|
||||
let doc = new HtmlDocument(html);
|
||||
|
||||
let title = doc.querySelector('p.detail-main-info-title')?.text?.trim()
|
||||
|| doc.querySelector('span.normal-top-title')?.text?.trim()
|
||||
|| doc.querySelector('title')?.text?.trim()?.replace(/漫画.*$/i, '')
|
||||
|| '未知标题';
|
||||
|
||||
let coverEl = doc.querySelector('.detail-main-cover img')
|
||||
|| doc.querySelector('.detail-main-cover .cover-img img');
|
||||
let cover = toAbsUrl(coverEl?.attributes?.src || coverEl?.attributes?.['data-src'] || '');
|
||||
|
||||
let authorContainer = doc.querySelector('.detail-main-info-author');
|
||||
let author = '未知作者';
|
||||
if (authorContainer) {
|
||||
let authors = [];
|
||||
let links = authorContainer.querySelectorAll('a') || [];
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
let text = links[i].text?.trim();
|
||||
if (text) authors.push(text);
|
||||
}
|
||||
if (authors.length > 0) {
|
||||
author = authors.join(',');
|
||||
} else {
|
||||
let raw = authorContainer.text?.replace(/作者[::]/, '').trim();
|
||||
if (raw) author = raw;
|
||||
}
|
||||
} else {
|
||||
let metaAuthor = doc.querySelector('meta[name="Author"]')?.attributes?.content;
|
||||
if (metaAuthor) {
|
||||
author = metaAuthor.includes(':') ? metaAuthor.split(':').pop().trim() : metaAuthor.trim();
|
||||
}
|
||||
}
|
||||
|
||||
let status = doc.querySelector('.detail-list-title-1')?.text?.trim() || '未知状态';
|
||||
|
||||
let descriptionEl = doc.querySelector('.detail-desc');
|
||||
let description = descriptionEl?.text?.trim() || '';
|
||||
if (!description) {
|
||||
description = doc.querySelector('meta[name="Description"]')?.attributes?.content || '';
|
||||
}
|
||||
|
||||
let tags = [];
|
||||
let tagElements = doc.querySelectorAll('.detail-main-info-class a') || [];
|
||||
for (let i = 0; i < tagElements.length; i++) {
|
||||
let tagText = tagElements[i].text?.trim();
|
||||
if (tagText) tags.push(tagText);
|
||||
}
|
||||
|
||||
let updateTime = doc.querySelector('.detail-list-title-3')?.text?.trim() || '';
|
||||
|
||||
let starValue = null;
|
||||
let starElement = doc.querySelector('.detail-main-info-star');
|
||||
if (starElement && starElement.attributes && starElement.attributes['class']) {
|
||||
let starClass = starElement.attributes['class'];
|
||||
let match = starClass.match(/star-(\d+)/i);
|
||||
if (match && match[1]) {
|
||||
let num = parseInt(match[1], 10);
|
||||
if (!isNaN(num)) {
|
||||
starValue = num;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let chapters = new Map();
|
||||
let selectorItems = doc.querySelectorAll('.detail-selector .detail-selector-item');
|
||||
|
||||
if (selectorItems.length > 0) {
|
||||
for (let item of selectorItems) {
|
||||
let groupName = item.text?.trim();
|
||||
if (!groupName || groupName.includes('评论')) continue;
|
||||
|
||||
let onclick = item.attributes['onclick'];
|
||||
let listId = null;
|
||||
if (onclick) {
|
||||
let match = onclick.match(/titleSelect\(.*?,.*?, *['"](.*?)['"]\)/);
|
||||
if (match) {
|
||||
listId = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (listId) {
|
||||
let listEl = doc.getElementById(listId);
|
||||
if (listEl) {
|
||||
let groupChapters = new Map();
|
||||
let links = listEl.querySelectorAll('a.chapteritem');
|
||||
for (let link of links) {
|
||||
let href = link.attributes['href'];
|
||||
let title = link.text?.trim() || link.attributes['title']?.trim();
|
||||
if (href && title) {
|
||||
if (!href.startsWith('http')) href = toAbsUrl(href);
|
||||
groupChapters.set(href, title);
|
||||
}
|
||||
}
|
||||
if (groupChapters.size > 0) {
|
||||
chapters.set(groupName, groupChapters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chapters.size === 0) {
|
||||
let groupChapters = new Map();
|
||||
let links = doc.querySelectorAll('a.chapteritem');
|
||||
for (let link of links) {
|
||||
let href = link.attributes['href'];
|
||||
let title = link.text?.trim() || link.attributes['title']?.trim();
|
||||
if (href && title) {
|
||||
if (!href.startsWith('http')) href = toAbsUrl(href);
|
||||
groupChapters.set(href, title);
|
||||
}
|
||||
}
|
||||
if (groupChapters.size > 0) {
|
||||
chapters.set('连载', groupChapters);
|
||||
}
|
||||
}
|
||||
|
||||
let parseRecommends = (htmlContent) => {
|
||||
let recs = [];
|
||||
let recPattern = /<li[^>]*class=["'][^"']*(?:list-comic|rec|recommend)[^"']*["'][^>]*>[\s\S]*?<a[^>]*href=["']([^"']+)["'][^>]*>[\s\S]*?<img[^>]*src=["']([^"']+)["'][^>]*>[^<]*<\/a>[\s\S]*?<a[^>]*>\s*([^<]+)\s*<\/a>/gi;
|
||||
let m;
|
||||
let count = 0;
|
||||
while ((m = recPattern.exec(htmlContent)) !== null && count < 12) {
|
||||
let url = m[1];
|
||||
let cover = m[2];
|
||||
let titleText = (m[3] || '').trim();
|
||||
if (!url || !titleText) continue;
|
||||
if (!url.startsWith('http')) url = toAbsUrl(url);
|
||||
if (cover && !cover.startsWith('http')) cover = toAbsUrl(cover);
|
||||
recs.push(new Comic({ id: url, title: titleText, cover: cover }));
|
||||
count++;
|
||||
}
|
||||
return recs;
|
||||
};
|
||||
|
||||
let recommends = parseRecommends(html);
|
||||
|
||||
// 提取 mid
|
||||
let midMatch = html.match(/mid["\s:]*(\d+)/i) || html.match(/var mid = (\d+)/i) || html.match(/mid=(\d+)/i) || html.match(/var DM5_MID = (\d+)/i) || html.match(/var COMIC_MID=(\d+)/i);
|
||||
if (midMatch) {
|
||||
this.comic.mid = parseInt(midMatch[1]);
|
||||
}
|
||||
|
||||
return new ComicDetails({
|
||||
title,
|
||||
cover,
|
||||
description: description || '暂无描述',
|
||||
tags: {
|
||||
'作者': [author || '未知作者'],
|
||||
'状态': [status || '未知状态'],
|
||||
'标签': tags
|
||||
},
|
||||
chapters: chapters,
|
||||
recommend: recommends,
|
||||
updateTime: updateTime,
|
||||
stars: starValue,
|
||||
subId: this.comic.mid ? this.comic.mid.toString() : '73225'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* load images of a chapter
|
||||
* @param comicId {string}
|
||||
* @param epId {string?}
|
||||
* @returns {Promise<{images: string[]}>}
|
||||
*/
|
||||
loadEp: async (comicId, epId) => {
|
||||
let url = `${epId}/`;
|
||||
let res = await Network.get(url, this._buildHeaders());
|
||||
if (res.status !== 200) throw new Error('获取章节内容失败: ' + res.status);
|
||||
let html = res.body;
|
||||
let document = new HtmlDocument(html);
|
||||
let scripts = document.querySelectorAll("script");
|
||||
let script = null;
|
||||
for (let s of scripts) {
|
||||
if (s.innerHTML.includes('eval(function(p,a,c,k,e,d)')) {
|
||||
script = s.innerHTML;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!script) throw ('无法显示付费内容/章节不存在');
|
||||
|
||||
let pStart = script.indexOf("}('") + 3;
|
||||
let boundaryMatch = script.substring(pStart).match(/',(\d+),(\d+),'/);
|
||||
if (!boundaryMatch) throw new Error('无法解析脚本参数边界');
|
||||
|
||||
let boundaryIndex = boundaryMatch.index + pStart;
|
||||
let rawP = script.substring(pStart, boundaryIndex);
|
||||
let a = parseInt(boundaryMatch[1]);
|
||||
let c = parseInt(boundaryMatch[2]);
|
||||
|
||||
let kContentStart = boundaryIndex + boundaryMatch[0].length;
|
||||
let kEnd = script.indexOf("'.split", kContentStart);
|
||||
let rawK = script.substring(kContentStart, kEnd);
|
||||
let dict = rawK.split('|');
|
||||
|
||||
let decrypt = (p, a, c, k) => {
|
||||
let e = (c) => (c < a ? '' : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36));
|
||||
let d = {};
|
||||
while (c--) d[e(c)] = k[c] || e(c);
|
||||
return p.replace(/\b\w+\b/g, w => d[w] || w);
|
||||
};
|
||||
|
||||
let decrypted = decrypt(rawP, a, c, dict);
|
||||
|
||||
let arrayMatch = decrypted.match(/\[(.*?)\]/);
|
||||
if (!arrayMatch) throw new Error('无法从解密后的脚本中提取图片数组');
|
||||
|
||||
let arrayContent = arrayMatch[1];
|
||||
let images = arrayContent.split(',').map(item => {
|
||||
// 去除引号和反斜杠
|
||||
return item.trim().replace(/^\\?['"]|\\?['"]$/g, '');
|
||||
}).filter(url => url && url.startsWith('http'));
|
||||
|
||||
return { images };
|
||||
},
|
||||
/**
|
||||
* [Optional] provide configs for an image loading
|
||||
* @param url
|
||||
* @param comicId
|
||||
* @param epId
|
||||
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>}
|
||||
*/
|
||||
onImageLoad: (url, comicId, epId) => {
|
||||
let referer = '';
|
||||
if (epId && typeof epId === 'string') {
|
||||
if (!epId.startsWith('http')) {
|
||||
referer = this.baseUrl + epId;
|
||||
} else {
|
||||
referer = epId;
|
||||
}
|
||||
} else {
|
||||
referer = this.baseUrl + '/';
|
||||
}
|
||||
|
||||
return {
|
||||
headers: this._buildImageHeaders(url, referer)
|
||||
};
|
||||
},
|
||||
/**
|
||||
* [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 {
|
||||
headers: this._buildImageHeaders(url, this.baseUrl + '/')
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [Optional] like or unlike a comic
|
||||
* @param id {string}
|
||||
* @param isLike {boolean} - true for like, false for unlike
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
likeComic: async (id, isLike) => {
|
||||
|
||||
},
|
||||
/**
|
||||
* [Optional] load comments
|
||||
*
|
||||
* Since app version 1.0.6, rich text is supported in comments.
|
||||
* Following html tags are supported: ['a', 'b', 'i', 'u', 's', 'br', 'span', 'img'].
|
||||
* span tag supports style attribute, but only support font-weight, font-style, text-decoration.
|
||||
* All images will be placed at the end of the comment.
|
||||
* Auto link detection is enabled, but only http/https links are supported.
|
||||
* @param comicId {string}
|
||||
* @param subId {string?} - ComicDetails.subId
|
||||
* @param page {number}
|
||||
* @param replyTo {string?} - commentId to reply, not null when reply to a comment
|
||||
* @returns {Promise<{comments: Comment[], maxPage: number?}>}
|
||||
*/
|
||||
loadComments: async (comicId, subId, page, replyTo) => {
|
||||
if (!subId) {
|
||||
throw new Error('漫画ID未找到,无法加载评论');
|
||||
}
|
||||
|
||||
let requestPage = page;
|
||||
let targetCommentId = null;
|
||||
if (replyTo) {
|
||||
let parts = replyTo.split('//');
|
||||
targetCommentId = parts[0];
|
||||
requestPage = parseInt(parts[1]);
|
||||
}
|
||||
|
||||
let url = `${this.baseUrl}/manhua-${comicId}/pagerdata.ashx`;
|
||||
let params = {
|
||||
d: Date.now(),
|
||||
pageindex: (requestPage - 1),
|
||||
pagesize: 767,
|
||||
mid: subId,
|
||||
t: 4
|
||||
};
|
||||
let query = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&');
|
||||
url += '?' + query;
|
||||
|
||||
let headers = {
|
||||
'accept': '*/*',
|
||||
'accept-encoding': 'gzip, deflate, br, zstd',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'cache-control': 'no-cache',
|
||||
'connection': 'keep-alive',
|
||||
'host': 'www.manhuaren.com',
|
||||
'pragma': 'no-cache',
|
||||
'referer': `${this.baseUrl}/manhua-${comicId}/`,
|
||||
'sec-ch-ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
|
||||
'sec-ch-ua-mobile': '?1',
|
||||
'sec-ch-ua-platform': '"Android"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36',
|
||||
'x-requested-with': 'XMLHttpRequest'
|
||||
};
|
||||
|
||||
let res = await Network.get(url, headers);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`加载评论失败,状态码: ${res.status}`);
|
||||
}
|
||||
|
||||
let data = JSON.parse(res.body);
|
||||
let comments = [];
|
||||
|
||||
let maxPage = 0
|
||||
if (replyTo) {
|
||||
let target = data.find(item => item.Id.toString() === targetCommentId);
|
||||
if (target && target.ToPostShowDataItems) {
|
||||
comments = target.ToPostShowDataItems.map(item => new Comment({
|
||||
id: item.Id.toString(),
|
||||
userName: item.Poster,
|
||||
content: item.PostContent,
|
||||
time: item.PostTime,
|
||||
avatar: item.HeadUrl,
|
||||
likeCount: item.PraiseCount,
|
||||
isLiked: item.IsPraise,
|
||||
replyCount: 0
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
comments = data.map(item => new Comment({
|
||||
id: `${item.Id}//${page}`,
|
||||
userName: item.Poster,
|
||||
content: item.PostContent,
|
||||
time: item.PostTime,
|
||||
avatar: item.HeadUrl,
|
||||
likeCount: item.PraiseCount,
|
||||
isLiked: item.IsPraise,
|
||||
replyCount: item.ToPostShowDataItems ? item.ToPostShowDataItems.length : 0
|
||||
}));
|
||||
if (comments == []){
|
||||
maxPage = page;
|
||||
}else{
|
||||
maxPage = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
comments: comments,
|
||||
maxPage: replyTo? 1 : maxPage
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* load chapter comments
|
||||
* @param comicId {string}
|
||||
* @param epId {string}
|
||||
* @param page {number}
|
||||
* @param replyTo {string?}
|
||||
* @returns {Promise<{comments: Comment[], maxPage: number}>}
|
||||
*/
|
||||
loadChapterComments: async (comicId, epId, page, replyTo) => {
|
||||
let cidMatch = epId.match(/m(\d+)/);
|
||||
let cid = cidMatch ? cidMatch[1] : null;
|
||||
if (!cid) {
|
||||
let match = epId.match(/(\d+)\/?$/);
|
||||
if (match) cid = match[1];
|
||||
}
|
||||
|
||||
if (!cid) return { comments: [], maxPage: page };
|
||||
|
||||
let requestPage = page;
|
||||
let targetCommentId = null;
|
||||
if (replyTo) {
|
||||
let parts = replyTo.split('//');
|
||||
targetCommentId = parts[0];
|
||||
requestPage = parseInt(parts[1]);
|
||||
}
|
||||
|
||||
let pageSize = 20;
|
||||
let url = `https://www.manhuaren.com/showcomment/pagerdata.ashx?d=${Date.now()}&pageindex=${requestPage}&pagesize=${pageSize}&cid=${cid}&t=9`;
|
||||
|
||||
let headers = {
|
||||
'accept': '*/*',
|
||||
'accept-encoding': 'gzip, deflate, br, zstd',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'cache-control': 'no-cache',
|
||||
'connection': 'keep-alive',
|
||||
'host': 'www.manhuaren.com',
|
||||
'pragma': 'no-cache',
|
||||
'referer': `https://www.manhuaren.com/showcomment/?cid=${cid}`,
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1',
|
||||
'x-requested-with': 'XMLHttpRequest'
|
||||
};
|
||||
|
||||
let res = await Network.get(url, headers);
|
||||
if (res.status !== 200) return { comments: [], maxPage: page };
|
||||
|
||||
let data = [];
|
||||
try {
|
||||
data = JSON.parse(res.body);
|
||||
} catch (e) {}
|
||||
|
||||
if (!Array.isArray(data)) return { comments: [], maxPage: page };
|
||||
|
||||
let comments = [];
|
||||
let maxPage = 0
|
||||
if (replyTo) {
|
||||
let target = data.find(item => item.Id.toString() === targetCommentId);
|
||||
if (target && target.ToPostShowDataItems) {
|
||||
comments = target.ToPostShowDataItems.map(item => new Comment({
|
||||
id: item.Id.toString(),
|
||||
userName: item.Poster,
|
||||
content: item.PostContent,
|
||||
time: item.PostTime,
|
||||
avatar: item.HeadUrl,
|
||||
likeCount: item.PraiseCount,
|
||||
isLiked: item.IsPraise,
|
||||
replyCount: 0
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
comments = data.map(item => new Comment({
|
||||
id: `${item.Id}//${page}`,
|
||||
userName: item.Poster,
|
||||
content: item.PostContent,
|
||||
time: item.PostTime,
|
||||
avatar: item.HeadUrl,
|
||||
likeCount: item.PraiseCount,
|
||||
isLiked: item.IsPraise,
|
||||
replyCount: item.ToPostShowDataItems ? item.ToPostShowDataItems.length : 0
|
||||
}));
|
||||
if (comments == []){
|
||||
maxPage = page;
|
||||
}else{
|
||||
maxPage = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
comments: comments,
|
||||
maxPage: replyTo? 1 : maxPage
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
523
mxs.js
Normal file
523
mxs.js
Normal file
@@ -0,0 +1,523 @@
|
||||
class MXS extends ComicSource {
|
||||
// 漫画源基本信息
|
||||
name = "漫小肆";
|
||||
key = "mxs";
|
||||
version = "1.0.0";
|
||||
minAppVersion = "1.5.0";
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/mxs.js";
|
||||
|
||||
// 漫画源设置项
|
||||
settings = {
|
||||
// 域名选择功能
|
||||
domains: {
|
||||
title: "选择域名",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "https://www.mxshm.top", text: "mxshm.top" },
|
||||
{ value: "https://www.jjmhw1.top", text: "jjmhw1.top" },
|
||||
{ value: "https://www.jjmh.top", text: "jjmh.top" },
|
||||
{ value: "https://www.jjmh.cc", text: "jjmh.cc" },
|
||||
{ value: "https://www.wzd1.cc", text: "wzd1.cc" },
|
||||
{ value: "https://www.wzdhm1.cc", text: "wzdhm1.cc" },
|
||||
{ value: "https://www.ikanwzd.cc", text: "ikanwzd.cc" }
|
||||
],
|
||||
default: "https://www.mxshm.top"
|
||||
},
|
||||
|
||||
// 域名检测功能
|
||||
domainCheck: {
|
||||
title: "检测当前域名",
|
||||
type: "callback",
|
||||
buttonText: "检测",
|
||||
callback: () => {
|
||||
const currentDomain = this.loadSetting("domains");
|
||||
const startTime = Date.now();
|
||||
let isCompleted = false;
|
||||
|
||||
// 显示加载对话框
|
||||
const loadingId = UI.showLoading(() => {
|
||||
UI.showMessage("检测已取消");
|
||||
isCompleted = true;
|
||||
});
|
||||
|
||||
// 10秒超时检测
|
||||
setTimeout(() => {
|
||||
if (!isCompleted) {
|
||||
UI.cancelLoading(loadingId);
|
||||
UI.showMessage("❌ 连接超时,可能需要 🚀");
|
||||
isCompleted = true;
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// 测试网络连接
|
||||
Network.get(currentDomain).then(res => {
|
||||
if (isCompleted) return;
|
||||
const delay = Date.now() - startTime;
|
||||
UI.cancelLoading(loadingId);
|
||||
UI.showMessage(`✅ 连接正常,延迟: ${delay}ms`);
|
||||
isCompleted = true;
|
||||
}).catch(() => {
|
||||
if (isCompleted) return;
|
||||
UI.cancelLoading(loadingId);
|
||||
UI.showMessage("❌ 连接失败,可能需要 🚀");
|
||||
isCompleted = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取基础URL
|
||||
get baseUrl() {
|
||||
return this.loadSetting("domains");
|
||||
}
|
||||
|
||||
// 解析普通漫画列表
|
||||
parseComicList(items) {
|
||||
const comics = [];
|
||||
|
||||
for (let item of items) {
|
||||
// 提取漫画ID
|
||||
const linkElem = item.querySelector("a[href^='/book/']");
|
||||
const id = linkElem.attributes.href.split("/").pop();
|
||||
|
||||
// 提取标题和作者
|
||||
const title = item.querySelector(".title a")?.text?.trim();
|
||||
const author = item.querySelector("span a")?.text?.trim();
|
||||
|
||||
// 提取描述信息
|
||||
const description = item.querySelector(".chapter")?.text?.replace(/^更新/, "")?.replace(/\s+/g, " ")?.trim() || item.querySelector(".zl")?.text?.trim();
|
||||
|
||||
// 验证必要字段并创建漫画对象
|
||||
if (id && title) {
|
||||
comics.push(new Comic({
|
||||
id: id,
|
||||
title: title,
|
||||
subTitle: author,
|
||||
cover: `${this.baseUrl}/static/upload/book/${id}/cover.jpg`,
|
||||
description: description
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return comics;
|
||||
}
|
||||
|
||||
// 解析热门漫画列表
|
||||
parseHotComicList(items) {
|
||||
const comics = [];
|
||||
|
||||
for (let item of items) {
|
||||
// 提取漫画ID
|
||||
const linkElem = item.querySelector(".cover a[href^='/book/']");
|
||||
const id = linkElem.attributes.href.split("/").pop();
|
||||
|
||||
// 提取标题、作者和点击量
|
||||
const title = item.querySelector(".info .title a")?.text?.trim();
|
||||
const author = item.querySelector(".info .desc")?.text?.trim();
|
||||
const clickCount = item.querySelector(".info .subtitle span a")?.text?.trim();
|
||||
|
||||
// 提取标签信息
|
||||
const tags = [];
|
||||
const tagElems = item.querySelectorAll(".info .tag a");
|
||||
for (let tagElem of tagElems) {
|
||||
if (tagElem.text) tags.push(tagElem.text.trim());
|
||||
}
|
||||
|
||||
// 验证必要字段并创建漫画对象
|
||||
if (id && title) {
|
||||
comics.push(new Comic({
|
||||
id: id,
|
||||
title: title,
|
||||
subTitle: author,
|
||||
cover: `${this.baseUrl}/static/upload/book/${id}/cover.jpg`,
|
||||
tags: tags,
|
||||
description: `热度: 🔥${clickCount}`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return comics;
|
||||
}
|
||||
|
||||
// 解析评论列表
|
||||
parseCommentList(items) {
|
||||
const comments = [];
|
||||
|
||||
for (let item of items) {
|
||||
// 提取评论信息
|
||||
const userName = item.querySelector(".title")?.text?.trim();
|
||||
const content = item.querySelector(".content")?.text?.trim();
|
||||
const time = item.querySelector(".bottom")?.text?.match(/\d{4}-\d{2}-\d{2}/)?.[0]?.trim();
|
||||
const avatar = item.querySelector(".cover img")?.attributes?.src;
|
||||
|
||||
// 验证必要字段并创建评论对象
|
||||
if (userName && content) {
|
||||
comments.push(new Comment({
|
||||
userName: userName,
|
||||
avatar: `${this.baseUrl}${avatar}`,
|
||||
content: content,
|
||||
time: time
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return comments;
|
||||
}
|
||||
|
||||
// 执行网络请求并返回HTML文档对象
|
||||
async fetchDocument(url) {
|
||||
const res = await Network.get(url, {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw `请求失败: ${res.status}`;
|
||||
}
|
||||
|
||||
return new HtmlDocument(res.body);
|
||||
}
|
||||
|
||||
// === 探索页面配置 ===
|
||||
explore = [
|
||||
{
|
||||
title: "漫小肆",
|
||||
type: "multiPartPage",
|
||||
load: async (page) => {
|
||||
const doc = await this.fetchDocument(this.baseUrl);
|
||||
|
||||
// 最近更新部分
|
||||
const updateSection = {
|
||||
title: "最近更新",
|
||||
comics: this.parseComicList(doc.querySelectorAll(".index-manga .mh-item")),
|
||||
viewMore: {
|
||||
page: "category",
|
||||
attributes: { category: "最近更新" }
|
||||
}
|
||||
};
|
||||
|
||||
// 热门漫画部分
|
||||
const hotSection = {
|
||||
title: "热门漫画",
|
||||
comics: this.parseHotComicList(doc.querySelectorAll(".index-original .index-original-list li")),
|
||||
viewMore: {
|
||||
page: "category",
|
||||
attributes: { category: "排行榜" }
|
||||
}
|
||||
};
|
||||
|
||||
// 完结优选部分
|
||||
const endSection = {
|
||||
title: "完结优选",
|
||||
comics: this.parseComicList(doc.querySelectorAll(".box-body .mh-item")),
|
||||
viewMore: {
|
||||
page: "category",
|
||||
attributes: { category: "全部漫画" }
|
||||
}
|
||||
};
|
||||
|
||||
doc.dispose();
|
||||
return [updateSection, hotSection, endSection];
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// === 分类页面配置 ===
|
||||
category = {
|
||||
title: "漫小肆",
|
||||
parts: [
|
||||
{
|
||||
name: "推荐",
|
||||
type: "fixed",
|
||||
categories: ["最近更新", "排行榜", "全部漫画"],
|
||||
itemType: "category"
|
||||
},
|
||||
{
|
||||
name: "题材",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"都市", "校园", "青春", "性感", "长腿", "多人", "御姐", "巨乳",
|
||||
"新婚", "媳妇", "暧昧", "清纯", "调教", "少妇", "风骚", "同居",
|
||||
"淫乱", "好友", "女神", "诱惑", "偷情", "出轨", "正妹", "家教"
|
||||
],
|
||||
itemType: "category"
|
||||
}
|
||||
],
|
||||
enableRankingPage: false
|
||||
};
|
||||
|
||||
// === 分类漫画加载配置 ===
|
||||
categoryComics = {
|
||||
// 加载分类漫画
|
||||
load: async (category, param, options, page) => {
|
||||
// 根据分类构建不同的请求URL
|
||||
let url;
|
||||
if (category === "最近更新") {
|
||||
url = `${this.baseUrl}/update?page=${page}`;
|
||||
} else if (category === "排行榜") {
|
||||
url = `${this.baseUrl}/rank`;
|
||||
} else {
|
||||
const tag = (category !== "全部漫画") ? category : "全部";
|
||||
const area = options[0] || "-1";
|
||||
const end = options[1] || "-1";
|
||||
url = `${this.baseUrl}/booklist?tag=${encodeURIComponent(tag)}&area=${area}&end=${end}&page=${page}`;
|
||||
}
|
||||
|
||||
const doc = await this.fetchDocument(url);
|
||||
let comics = [];
|
||||
|
||||
// 排行榜特殊处理
|
||||
if (category === "排行榜") {
|
||||
const selectedRank = options[0] || "new";
|
||||
const rankMapping = {
|
||||
"new": "新书榜",
|
||||
"popular": "人气榜",
|
||||
"end": "完结榜",
|
||||
"recommend": "推荐榜"
|
||||
};
|
||||
|
||||
// 查找对应的排行榜列表
|
||||
const rankLists = doc.querySelectorAll(".mh-list.col3.top-cat li");
|
||||
let targetList = null;
|
||||
|
||||
for (let list of rankLists) {
|
||||
const titleElem = list.querySelector(".title");
|
||||
if (titleElem) {
|
||||
const title = titleElem.text.trim();
|
||||
if (title === rankMapping[selectedRank]) {
|
||||
targetList = list;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetList) {
|
||||
doc.dispose();
|
||||
throw "未找到对应的排行榜";
|
||||
}
|
||||
|
||||
comics = this.parseComicList(targetList.querySelectorAll(".mh-item.horizontal, .mh-itme-top"));
|
||||
} else {
|
||||
// 普通分类处理
|
||||
comics = this.parseComicList(doc.querySelectorAll(".mh-list.col7 .mh-item"));
|
||||
}
|
||||
|
||||
// 解析最大页数(排行榜不分页)
|
||||
let maxPage = 1;
|
||||
if (category !== "排行榜") {
|
||||
const pageLinks = doc.querySelectorAll(".pagination a[href*='page=']");
|
||||
for (let link of pageLinks) {
|
||||
const match = link.attributes.href.match(/page=(\d+)/);
|
||||
if (match) {
|
||||
const pageNum = parseInt(match[1]);
|
||||
if (!isNaN(pageNum) && pageNum > maxPage) {
|
||||
maxPage = pageNum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc.dispose();
|
||||
return { comics, maxPage };
|
||||
},
|
||||
|
||||
// 动态加载分类选项
|
||||
optionLoader: async (category, param) => {
|
||||
if (category === "最近更新") {
|
||||
return [];
|
||||
} else if (category === "排行榜") {
|
||||
return [{
|
||||
options: [
|
||||
"new-新书榜",
|
||||
"popular-人气榜",
|
||||
"end-完结榜",
|
||||
"recommend-推荐榜"
|
||||
]
|
||||
}];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
label: "地区",
|
||||
options: [
|
||||
"-全部",
|
||||
"1-韩国",
|
||||
"2-日本",
|
||||
"3-台湾"
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "状态",
|
||||
options: [
|
||||
"-全部",
|
||||
"0-连载",
|
||||
"1-完结"
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// === 搜索功能配置 ===
|
||||
search = {
|
||||
// 搜索漫画
|
||||
load: async (keyword, options, page) => {
|
||||
const url = `${this.baseUrl}/search?keyword=${encodeURIComponent(keyword)}`;
|
||||
const doc = await this.fetchDocument(url);
|
||||
const comics = this.parseComicList(doc.querySelectorAll(".mh-item"));
|
||||
|
||||
doc.dispose();
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: 1
|
||||
};
|
||||
},
|
||||
enableTagsSuggestions: false
|
||||
};
|
||||
|
||||
// === 漫画详情和阅读功能配置 ===
|
||||
comic = {
|
||||
// 加载漫画详情
|
||||
loadInfo: async (id) => {
|
||||
const url = `${this.baseUrl}/book/${id}`;
|
||||
const doc = await this.fetchDocument(url);
|
||||
|
||||
// 提取标题信息
|
||||
const title = doc.querySelector(".info h1")?.text?.trim();
|
||||
|
||||
// 提取副标题信息(别名和作者)
|
||||
let author = "";
|
||||
let subTitle = "";
|
||||
const subTitleElems = doc.querySelectorAll(".info .subtitle");
|
||||
for (let elem of subTitleElems) {
|
||||
const text = elem.text;
|
||||
if (text.includes("别名:")) subTitle = text.replace("别名:", "").trim();
|
||||
if (text.includes("作者:")) author = text.replace("作者:", "").trim();
|
||||
}
|
||||
const authors = author ? author.split("&").map(a => a.trim()).filter(a => a) : [];
|
||||
|
||||
// 提取其他信息(状态、地区、更新时间、点击量和描述信息)
|
||||
let status = "";
|
||||
let area = "";
|
||||
let updateTime = "";
|
||||
let clickCount = "";
|
||||
const tipElems = doc.querySelectorAll(".info .tip span");
|
||||
for (let elem of tipElems) {
|
||||
const text = elem.text;
|
||||
if (text.includes("状态:")) status = elem.querySelector("span")?.text?.trim();
|
||||
if (text.includes("地区:")) area = elem.querySelector("a")?.text?.trim();
|
||||
if (text.includes("更新时间:")) updateTime = elem.text.replace("更新时间:", "").trim();
|
||||
if (text.includes("点击:")) clickCount = elem.text.replace("点击:", "").trim();
|
||||
}
|
||||
const description = doc.querySelector(".info .content")?.text?.trim();
|
||||
|
||||
// 提取标签信息
|
||||
const tagList = [];
|
||||
const tagElems = doc.querySelectorAll(".info .tip a[href*='tag=']");
|
||||
for (let elem of tagElems) {
|
||||
const tagName = elem.text?.trim();
|
||||
if (tagName) tagList.push(tagName);
|
||||
}
|
||||
|
||||
// 提取章节列表
|
||||
const chapters = {};
|
||||
const chapterElems = doc.querySelectorAll("#detail-list-select li a");
|
||||
for (let elem of chapterElems) {
|
||||
const chapterUrl = elem.attributes?.href;
|
||||
const chapterTitle = elem.text?.trim();
|
||||
if (chapterUrl && chapterTitle) {
|
||||
const chapterId = chapterUrl.split("/").pop();
|
||||
if (chapterId) chapters[chapterId] = chapterTitle;
|
||||
}
|
||||
}
|
||||
|
||||
// 提取评论和推荐漫画
|
||||
const comments = this.parseCommentList(doc.querySelectorAll(".view-comment-main .postlist li.dashed"));
|
||||
const recommend = this.parseComicList(doc.querySelectorAll(".index-manga .mh-item"));
|
||||
|
||||
doc.dispose();
|
||||
|
||||
// 创建并返回漫画详情对象
|
||||
return new ComicDetails({
|
||||
title: title,
|
||||
subTitle: subTitle,
|
||||
cover: `${this.baseUrl}/static/upload/book/${id}/cover.jpg`,
|
||||
description: description,
|
||||
tags: {
|
||||
"作者": authors,
|
||||
"题材": tagList,
|
||||
"地区": [area],
|
||||
"状态": [status],
|
||||
"热度": [`🔥${clickCount}`]
|
||||
},
|
||||
chapters: chapters,
|
||||
recommend: recommend,
|
||||
commentCount: comments.length,
|
||||
updateTime: updateTime,
|
||||
url: url,
|
||||
comments: comments
|
||||
});
|
||||
},
|
||||
|
||||
// 加载章节图片
|
||||
loadEp: async (comicId, epId) => {
|
||||
const url = `${this.baseUrl}/chapter/${epId}`;
|
||||
const doc = await this.fetchDocument(url);
|
||||
|
||||
// 提取懒加载图片
|
||||
const images = [];
|
||||
const imageElems = doc.querySelectorAll("img.lazy");
|
||||
for (let img of imageElems) {
|
||||
const src = img.attributes?.["data-original"];
|
||||
const image = src.replace(/https?:\/\/[^\/]+/, this.baseUrl);
|
||||
if (image) images.push(image);
|
||||
}
|
||||
|
||||
if (images.length === 0) {
|
||||
doc.dispose();
|
||||
throw "本章中未找到图片";
|
||||
}
|
||||
|
||||
doc.dispose();
|
||||
return {
|
||||
images: images
|
||||
};
|
||||
},
|
||||
|
||||
// 加载评论列表
|
||||
loadComments: async (comicId, subId, page, replyTo) => {
|
||||
const url = `${this.baseUrl}/book/${comicId}`;
|
||||
const doc = await this.fetchDocument(url);
|
||||
|
||||
const comments = this.parseCommentList(doc.querySelectorAll(".view-comment-main .postlist li.dashed"));
|
||||
|
||||
doc.dispose();
|
||||
return {
|
||||
comments: comments,
|
||||
maxPage: 1
|
||||
};
|
||||
},
|
||||
|
||||
// 处理标签点击事件
|
||||
onClickTag: (namespace, tag) => {
|
||||
// 作者标签跳转到搜索页面
|
||||
if (namespace === "作者") {
|
||||
return {
|
||||
page: "search",
|
||||
attributes: {
|
||||
keyword: tag
|
||||
}
|
||||
};
|
||||
}
|
||||
// 题材标签跳转到分类页面
|
||||
else if (namespace === "题材") {
|
||||
return {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: tag
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
enableTagsTranslate: false
|
||||
};
|
||||
}
|
||||
259
wnacg.js
259
wnacg.js
@@ -7,15 +7,39 @@ class Wnacg extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "wnacg"
|
||||
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
|
||||
minAppVersion = "1.0.0"
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/wnacg.js"
|
||||
|
||||
static domains = [];
|
||||
|
||||
get baseUrl() {
|
||||
return `https://${this.loadSetting('domain')}`
|
||||
let selection = this.loadSetting('domainSelection')
|
||||
if (selection === undefined || selection === null) selection = 0
|
||||
selection = parseInt(selection)
|
||||
|
||||
if (selection === 0) {
|
||||
// 选择自定义域名
|
||||
let domain0 = this.loadSetting('domain0')
|
||||
if (!domain0 || domain0.trim() === '') {
|
||||
throw 'Custom domain is not set'
|
||||
}
|
||||
return `https://${domain0.trim()}`
|
||||
} else {
|
||||
// 选择获取的域名 (Domain 1-3)
|
||||
let index = selection - 1
|
||||
if (index >= Wnacg.domains.length) {
|
||||
throw 'Selected domain is unavailable'
|
||||
}
|
||||
return `https://${Wnacg.domains[index]}`
|
||||
}
|
||||
}
|
||||
|
||||
overwriteDomains(domains) {
|
||||
if (domains.length != 0) Wnacg.domains = domains
|
||||
}
|
||||
|
||||
// [Optional] account related
|
||||
@@ -34,11 +58,11 @@ class Wnacg extends ComicSource {
|
||||
},
|
||||
`login_name=${encodeURIComponent(account)}&login_pass=${encodeURIComponent(pwd)}`
|
||||
)
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw 'Login failed'
|
||||
}
|
||||
let json = JSON.parse(res.body)
|
||||
if(json['html'].includes('登錄成功')) {
|
||||
if (json['html'].includes('登錄成功')) {
|
||||
return 'ok'
|
||||
}
|
||||
throw 'Login failed'
|
||||
@@ -55,6 +79,91 @@ class Wnacg extends ComicSource {
|
||||
registerWebsite: null
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.loadSetting('refreshDomainsOnStart')) await this.refreshDomains(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新域名列表
|
||||
* @param showConfirmDialog {boolean}
|
||||
*/
|
||||
async refreshDomains(showConfirmDialog) {
|
||||
let url = "https://wn01.link/"
|
||||
let title = ""
|
||||
let message = ""
|
||||
let domains = []
|
||||
|
||||
try {
|
||||
let res = await fetch(url)
|
||||
if (res.status == 200) {
|
||||
let html = await res.text()
|
||||
let document = new HtmlDocument(html)
|
||||
// 提取所有链接
|
||||
let links = document.querySelectorAll("a[href]")
|
||||
let seenDomains = new Set()
|
||||
|
||||
for (let link of links) {
|
||||
let href = link.attributes["href"]
|
||||
if (!href) continue
|
||||
|
||||
// 提取域名(支持 http:// 和 https://)
|
||||
let match = href.match(/^https?:\/\/([^\/]+)/)
|
||||
if (match) {
|
||||
let domain = match[1]
|
||||
// 只提取有效的域名,排除 wn01.link 自身和其他无关链接
|
||||
if (domain &&
|
||||
domain.includes(".") &&
|
||||
!domain.includes("wn01.link") &&
|
||||
!domain.includes("google.cn") &&
|
||||
!domain.includes("cdn-cgi") &&
|
||||
!seenDomains.has(domain)) {
|
||||
domains.push(domain)
|
||||
seenDomains.add(domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
document.dispose()
|
||||
|
||||
if (domains.length > 0) {
|
||||
title = "Update Success"
|
||||
message = "New domains:\n\n"
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 获取失败,使用内置域名
|
||||
}
|
||||
|
||||
if (domains.length == 0) {
|
||||
title = "Update Failed"
|
||||
message = `Using built-in domains:\n\n`
|
||||
domains = Wnacg.domains
|
||||
}
|
||||
|
||||
for (let i = 0; i < domains.length; i++) {
|
||||
message = message + `Fetched Domain ${i + 1}: ${domains[i]}\n`
|
||||
}
|
||||
message = message + `\nTotal: ${domains.length} domain(s)`
|
||||
|
||||
if (showConfirmDialog) {
|
||||
UI.showDialog(
|
||||
title,
|
||||
message,
|
||||
[
|
||||
{
|
||||
text: "Cancel",
|
||||
callback: () => { }
|
||||
},
|
||||
{
|
||||
text: "Apply",
|
||||
callback: () => this.overwriteDomains(domains)
|
||||
}
|
||||
]
|
||||
)
|
||||
} else {
|
||||
this.overwriteDomains(domains)
|
||||
}
|
||||
}
|
||||
|
||||
parseComic(c) {
|
||||
let link = c.querySelector("div.pic_box > a").attributes["href"];
|
||||
let id = RegExp("(?<=-aid-)[0-9]+").exec(link)[0];
|
||||
@@ -93,7 +202,7 @@ class Wnacg extends ComicSource {
|
||||
*/
|
||||
load: async (page) => {
|
||||
let res = await Network.get(this.baseUrl, {})
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid Status Code ${res.status}`
|
||||
}
|
||||
let document = new HtmlDocument(res.body)
|
||||
@@ -273,7 +382,7 @@ class Wnacg extends ComicSource {
|
||||
},
|
||||
],
|
||||
// enable ranking page
|
||||
enableRankingPage: false,
|
||||
enableRankingPage: true,
|
||||
}
|
||||
|
||||
/// category comic loading related
|
||||
@@ -288,7 +397,7 @@ class Wnacg extends ComicSource {
|
||||
*/
|
||||
load: async (category, param, options, page) => {
|
||||
let url = this.baseUrl + param
|
||||
if(page !== 0) {
|
||||
if (page !== 0) {
|
||||
if (!url.includes("-")) {
|
||||
url = url.replaceAll(".html", "-.html");
|
||||
}
|
||||
@@ -299,7 +408,7 @@ class Wnacg extends ComicSource {
|
||||
}
|
||||
|
||||
let res = await Network.get(url, {})
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid Status Code ${res.status}`
|
||||
}
|
||||
let document = new HtmlDocument(res.body)
|
||||
@@ -309,13 +418,50 @@ class Wnacg extends ComicSource {
|
||||
comics.push(this.parseComic(comicElement))
|
||||
}
|
||||
let pagesLink = document.querySelectorAll("div.f_left.paginator > a");
|
||||
let pages = Number(pagesLink[pagesLink.length-1].text)
|
||||
let pages = Number(pagesLink[pagesLink.length - 1].text)
|
||||
document.dispose()
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: pages,
|
||||
}
|
||||
},
|
||||
ranking: {
|
||||
options: [
|
||||
"day-Day",
|
||||
"week-Week",
|
||||
"month-Month",
|
||||
],
|
||||
load: async (option, page) => {
|
||||
let url = `${this.baseUrl}/albums-favorite_ranking-type-${option}.html`
|
||||
if (page !== 0) {
|
||||
url = `${this.baseUrl}/albums-favorite_ranking-page-${page}-type-${option}.html`
|
||||
}
|
||||
|
||||
let res = await Network.get(url, {})
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid Status Code ${res.status}`
|
||||
}
|
||||
|
||||
let document = new HtmlDocument(res.body)
|
||||
let comicElements = document.querySelectorAll("div.grid div.gallary_wrap > ul.cc > li")
|
||||
let comics = []
|
||||
for (let comicElement of comicElements) {
|
||||
comics.push(this.parseComic(comicElement))
|
||||
}
|
||||
|
||||
let pagesLink = document.querySelectorAll("div.f_left.paginator > a")
|
||||
let pages = 1
|
||||
if (pagesLink.length > 0) {
|
||||
pages = Number(pagesLink[pagesLink.length - 1].text)
|
||||
}
|
||||
|
||||
document.dispose()
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: pages,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// search related
|
||||
@@ -329,11 +475,11 @@ class Wnacg extends ComicSource {
|
||||
*/
|
||||
load: async (keyword, options, page) => {
|
||||
let url = `${this.baseUrl}/search/?q=${encodeURIComponent(keyword)}&f=_all&s=create_time_DESC&syn=yes`
|
||||
if(page !== 0) {
|
||||
if (page !== 0) {
|
||||
url += `&p=${page}`
|
||||
}
|
||||
let res = await Network.get(url, {})
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid Status Code ${res.status}`
|
||||
}
|
||||
let document = new HtmlDocument(res.body)
|
||||
@@ -368,16 +514,16 @@ class Wnacg extends ComicSource {
|
||||
* @returns {Promise<any>} - return any value to indicate success
|
||||
*/
|
||||
addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => {
|
||||
if(!isAdding) {
|
||||
if (!isAdding) {
|
||||
let res = await Network.get(`${this.baseUrl}/users-fav_del-id-${favoriteId}.html?ajax=true&_t=${randomDouble(0, 1)}`, {})
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw 'Delete failed'
|
||||
}
|
||||
} else {
|
||||
let res = await Network.post(`${this.baseUrl}/users-save_fav-id-${comicId}.html`, {
|
||||
'content-type': 'application/x-www-form-urlencoded'
|
||||
}, `favc_id=${folderId}`)
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw 'Delete failed'
|
||||
}
|
||||
}
|
||||
@@ -392,13 +538,13 @@ class Wnacg extends ComicSource {
|
||||
*/
|
||||
loadFolders: async (comicId) => {
|
||||
let res = await Network.get(`${this.baseUrl}/users-addfav-id-210814.html`, {})
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw 'Load failed'
|
||||
}
|
||||
let document = new HtmlDocument(res.body)
|
||||
let data = {}
|
||||
document.querySelectorAll("option").forEach((option => {
|
||||
if (option.attributes["value"] === "") return
|
||||
if (option.attributes["value"] === "") return
|
||||
data[option.attributes["value"]] = option.text
|
||||
}))
|
||||
return {
|
||||
@@ -415,7 +561,7 @@ class Wnacg extends ComicSource {
|
||||
let res = await Network.post(`${this.baseUrl}/users-favc_save-id.html`, {
|
||||
'content-type': 'application/x-www-form-urlencoded'
|
||||
}, `favc_name=${encodeURIComponent(name)}`)
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw 'Add failed'
|
||||
}
|
||||
return 'ok'
|
||||
@@ -427,7 +573,7 @@ class Wnacg extends ComicSource {
|
||||
*/
|
||||
deleteFolder: async (folderId) => {
|
||||
let res = await Network.get(`${this.baseUrl}/users-favclass_del-id-${folderId}.html?ajax=true&_t=${randomDouble()}`, {})
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw 'Delete failed'
|
||||
}
|
||||
return 'ok'
|
||||
@@ -442,7 +588,7 @@ class Wnacg extends ComicSource {
|
||||
loadComics: async (page, folder) => {
|
||||
let url = `${this.baseUrl}/users-users_fav-page-${page}-c-${folder}.html.html`
|
||||
let res = await Network.get(url, {})
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid Status Code ${res.status}`
|
||||
}
|
||||
let document = new HtmlDocument(res.body)
|
||||
@@ -469,8 +615,8 @@ class Wnacg extends ComicSource {
|
||||
})
|
||||
let pages = 1
|
||||
let pagesLink = document.querySelectorAll("div.f_left.paginator > a")
|
||||
if(pagesLink.length > 0) {
|
||||
pages = Number(pagesLink[pagesLink.length-1].text)
|
||||
if (pagesLink.length > 0) {
|
||||
pages = Number(pagesLink[pagesLink.length - 1].text)
|
||||
}
|
||||
document.dispose()
|
||||
return {
|
||||
@@ -489,7 +635,7 @@ class Wnacg extends ComicSource {
|
||||
*/
|
||||
loadInfo: async (id) => {
|
||||
let res = await Network.get(`${this.baseUrl}/photos-index-page-1-aid-${id}.html`, {})
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid Status Code ${res.status}`
|
||||
}
|
||||
let document = new HtmlDocument(res.body)
|
||||
@@ -504,7 +650,7 @@ class Wnacg extends ComicSource {
|
||||
let tags = new Map()
|
||||
tags.set("頁數", [pages])
|
||||
tags.set("分類", [category])
|
||||
if(tagsDom.length > 0) {
|
||||
if (tagsDom.length > 0) {
|
||||
tags.set("標籤", tagsDom.map((e) => e.text))
|
||||
}
|
||||
let description = document.querySelector("div.asTBcell.uwconn > p").text;
|
||||
@@ -529,16 +675,16 @@ class Wnacg extends ComicSource {
|
||||
loadThumbnails: async (id, next) => {
|
||||
next = next || '1'
|
||||
let res = await Network.get(`${this.baseUrl}/photos-index-page-${next}-aid-${id}.html`, {});
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid Status Code ${res.status}`
|
||||
}
|
||||
let document = new HtmlDocument(res.body)
|
||||
let thumbnails = document.querySelectorAll("div.pic_box.tb > a > img").map((e) => {
|
||||
return 'https:' + e.attributes["src"]
|
||||
})
|
||||
next = (Number(next)+1).toString()
|
||||
next = (Number(next) + 1).toString()
|
||||
let pagesLink = document.querySelector("div.f_left.paginator").children
|
||||
if(pagesLink[pagesLink.length-1].classNames.includes("thispage")) {
|
||||
if (pagesLink[pagesLink.length - 1].classNames.includes("thispage")) {
|
||||
next = null
|
||||
}
|
||||
return {
|
||||
@@ -554,13 +700,13 @@ class Wnacg extends ComicSource {
|
||||
*/
|
||||
loadEp: async (comicId, epId) => {
|
||||
let res = await Network.get(`${this.baseUrl}/photos-gallery-aid-${comicId}.html`, {})
|
||||
if(res.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid Status Code ${res.status}`
|
||||
}
|
||||
const regex = RegExp(String.raw`//[^"]+/[^"]+\.[^"]+`, 'g');
|
||||
const matches = Array.from(res.body.matchAll(regex));
|
||||
return {
|
||||
images: matches.map((e) => 'https:' + e[0].substring(0, e[0].length-1))
|
||||
images: matches.map((e) => 'https:' + e[0].substring(0, e[0].length - 1))
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -578,11 +724,60 @@ class Wnacg extends ComicSource {
|
||||
}
|
||||
|
||||
settings = {
|
||||
domain: {
|
||||
title: "Domain",
|
||||
refreshDomains: {
|
||||
title: "Refresh Domain List",
|
||||
type: "callback",
|
||||
buttonText: "Refresh",
|
||||
callback: () => this.refreshDomains(true)
|
||||
},
|
||||
refreshDomainsOnStart: {
|
||||
title: "Refresh Domain List on Startup",
|
||||
type: "switch",
|
||||
default: true,
|
||||
},
|
||||
domainSelection: {
|
||||
title: "Domain Selection",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: '0', text: 'Custom Domain' },
|
||||
{ value: '1', text: 'Domain 1' },
|
||||
{ value: '2', text: 'Domain 2' },
|
||||
{ value: '3', text: 'Domain 3' }
|
||||
],
|
||||
default: "0",
|
||||
},
|
||||
domain0: {
|
||||
title: "Custom Domain",
|
||||
type: "input",
|
||||
validator: '^(?!:\\/\\/)(?=.{1,253})([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$',
|
||||
default: 'www.wnacg.com',
|
||||
validator: String.raw`^(?!:\/\/)(?=.{1,253})([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`,
|
||||
default: 'wnacg.com',
|
||||
},
|
||||
}
|
||||
|
||||
translation = {
|
||||
'zh_CN': {
|
||||
'Refresh Domain List': '刷新域名列表',
|
||||
'Refresh': '刷新',
|
||||
'Refresh Domain List on Startup': '启动时刷新域名列表',
|
||||
'Domain Selection': '域名选择',
|
||||
'Custom Domain': '自定义域名',
|
||||
'Custom domain is not set': '未设置自定义域名',
|
||||
'Selected domain is unavailable': '所选域名不可用,请先刷新域名列表',
|
||||
'Day': '日',
|
||||
'Week': '周',
|
||||
'Month': '月',
|
||||
},
|
||||
'zh_TW': {
|
||||
'Refresh Domain List': '刷新域名列表',
|
||||
'Refresh': '刷新',
|
||||
'Refresh Domain List on Startup': '啟動時刷新域名列表',
|
||||
'Domain Selection': '域名選擇',
|
||||
'Custom Domain': '自定義域名',
|
||||
'Custom domain is not set': '未設置自定義域名',
|
||||
'Selected domain is unavailable': '所選域名不可用,請先刷新域名列表',
|
||||
'Day': '日',
|
||||
'Week': '周',
|
||||
'Month': '月',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user