mirror of
https://github.com/venera-app/venera-configs.git
synced 2025-12-16 17:31:16 +00:00
Compare commits
40 Commits
603fefe9be
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d469bce4d | ||
|
|
0e36a2a97c | ||
|
|
5a323c416b | ||
|
|
c8de505945 | ||
|
|
6a0e667775 | ||
|
|
a184ea975d | ||
|
|
504766de0e | ||
|
|
89d9573f65 | ||
|
|
a14e7552a5 | ||
|
|
99f36ab6f2 | ||
|
|
732111da2b | ||
|
|
ad8f7a40e0 | ||
|
|
933884e6ca | ||
|
|
d12969d4d7 | ||
|
|
0d3be98981 | ||
|
|
c193ea652f | ||
|
|
4864f0bf3b | ||
|
|
e18068fd8a | ||
|
|
848809a645 | ||
|
|
c9d921489a | ||
|
|
efb62f5fd6 | ||
|
|
21251d7a77 | ||
|
|
40b77839a7 | ||
|
|
e811e11ac6 | ||
|
|
91d1386f91 | ||
|
|
1b62163cd5 | ||
|
|
3ea825dea6 | ||
|
|
96433371e0 | ||
|
|
054be414b4 | ||
|
|
23e7866b5e | ||
|
|
75d5171b6f | ||
|
|
b2af0a518a | ||
|
|
a7b3e3b17d | ||
|
|
da9f8ab83b | ||
|
|
36996d8b21 | ||
|
|
e45ca0b996 | ||
|
|
0fe6ba47eb | ||
|
|
5f617b94da | ||
|
|
f9e998bd28 | ||
|
|
ff521df20d |
@@ -25,7 +25,7 @@ class NewComicSource extends ComicSource {
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
minAppVersion = "1.4.0"
|
||||
minAppVersion = "1.6.0"
|
||||
|
||||
// update url
|
||||
url = ""
|
||||
@@ -73,7 +73,8 @@ class NewComicSource extends ComicSource {
|
||||
loginWithWebview: {
|
||||
url: "",
|
||||
/**
|
||||
* check login status
|
||||
* check login status.
|
||||
* After successful login, the cookie will be automatically saved, and the localstorage can be retrieved using this.loadData("_localStorage").
|
||||
* @param url {string} - current url
|
||||
* @param title {string} - current title
|
||||
* @returns {boolean} - return true if login success
|
||||
@@ -819,4 +820,4 @@ class NewComicSource extends ComicSource {
|
||||
'zh_TW': {},
|
||||
'en': {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,7 +1334,7 @@ let UI = {
|
||||
* Show an input dialog
|
||||
* @param title {string}
|
||||
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
|
||||
* @param image {string?} - Available since 1.4.6. An optional image to show in the dialog. You can use this to show a captcha.
|
||||
* @param image {string | ArrayBuffer | null | undefined} - Since 1.4.6, you can pass an image url to show an image in the dialog. Since 1.5.3, you can also pass an ArrayBuffer to show a custom image.
|
||||
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
|
||||
*/
|
||||
showInputDialog: (title, validator, image) => {
|
||||
@@ -1403,7 +1403,7 @@ let APP = {
|
||||
* Set clipboard text
|
||||
* @param text {string}
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
*
|
||||
* @since 1.3.4
|
||||
*/
|
||||
function setClipboard(text) {
|
||||
@@ -1416,7 +1416,7 @@ function setClipboard(text) {
|
||||
/**
|
||||
* Get clipboard text
|
||||
* @returns {Promise<string>}
|
||||
*
|
||||
*
|
||||
* @since 1.3.4
|
||||
*/
|
||||
function getClipboard() {
|
||||
|
||||
10
baozi.js
10
baozi.js
@@ -5,7 +5,7 @@ class Baozi extends ComicSource {
|
||||
// 唯一标识符
|
||||
key = "baozi";
|
||||
|
||||
version = "1.1.0";
|
||||
version = "1.1.1";
|
||||
|
||||
minAppVersion = "1.0.0";
|
||||
|
||||
@@ -484,9 +484,13 @@ class Baozi extends ComicSource {
|
||||
}
|
||||
// 代理后图片水印更少
|
||||
let mobileImages = images.map((e) => {
|
||||
const regex = /scomic\/.*/;
|
||||
const regex = /\/[a-z]comic\/.*/;
|
||||
const match = e.match(regex);
|
||||
return `https://as-rsa1-usla.baozicdn.com/w640/${match[0]}`;
|
||||
if (match) {
|
||||
return `https://as-rsa1-usla.baozicdn.com/w640${match[0]}`;
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
});
|
||||
return { images: mobileImages };
|
||||
},
|
||||
|
||||
768
ccc.js
Normal file
768
ccc.js
Normal file
@@ -0,0 +1,768 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
class CCC extends ComicSource {
|
||||
// Note: The fields which are marked as [Optional] should be removed if not used
|
||||
|
||||
// name of the source
|
||||
name = "CCC追漫台"
|
||||
|
||||
// unique id of the source
|
||||
key = "ccc"
|
||||
|
||||
version = "1.0.1"
|
||||
|
||||
minAppVersion = "1.6.0"
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/ccc.js"
|
||||
|
||||
apiUrl = "https://api.creative-comic.tw"
|
||||
|
||||
processToken(body) {
|
||||
const result = JSON.parse(body);
|
||||
if (result.code != 0) {
|
||||
throw "登錄失敗";
|
||||
}
|
||||
this.saveData("expireTime", Math.floor(Date.now() / 1000) + result.expires_in);
|
||||
this.saveData("refreshToken", result.refresh_token);
|
||||
this.saveData("token", result.access_token);
|
||||
}
|
||||
|
||||
async getApiHeaders(login = false) {
|
||||
let token = this.loadData("token");
|
||||
if (!login && token) {
|
||||
if (Math.floor(Date.now() / 1000) > this.loadData("expireTime")) {
|
||||
const res = await Network.post(`${this.apiUrl}/token`, {
|
||||
device: "web_desktop",
|
||||
uuid: "null"
|
||||
}, {
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": "2",
|
||||
"client_secret": "9eAhsCX3VWtyqTmkUo5EEaoH4MNPxrn6ZRwse7tE",
|
||||
"refresh_token": this.loadData("refreshToken")
|
||||
});
|
||||
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 {
|
||||
device: "web_desktop",
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
device: "web_desktop",
|
||||
uuid: "null"
|
||||
}
|
||||
}
|
||||
|
||||
base64ToArrayBuffer(base64) {
|
||||
const base64Data = base64.split(',')[1] || base64;
|
||||
return Convert.decodeBase64(base64Data);
|
||||
}
|
||||
|
||||
async parseComics(url) {
|
||||
const res = await Network.get(url, await this.getApiHeaders());
|
||||
const result = [];
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
for (let c of jsonData["data"]) {
|
||||
const tags = [];
|
||||
for (let a of c["author"]) {
|
||||
tags.push(a["name"]);
|
||||
}
|
||||
if (typeof (c["type"]) == "object") {
|
||||
tags.push(c["type"]["name"]);
|
||||
}
|
||||
result.push({
|
||||
id: (("book_id" in c) ? c["book_id"] : c["id"]).toString(),
|
||||
title: c["name"],
|
||||
subtitle: c["brief"],
|
||||
description: c["description"],
|
||||
cover: c["image1"]??c["image2"]??c["image3"],
|
||||
tags: tags
|
||||
});
|
||||
}
|
||||
return { comics: result, maxPage: Math.ceil(jsonData["total"] / 20) };
|
||||
}
|
||||
|
||||
// [Optional] account related
|
||||
account = {
|
||||
/**
|
||||
* [Optional] login with account and password, return any value to indicate success
|
||||
* @param account {string}
|
||||
* @param pwd {string}
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
login: async (account, pwd) => {
|
||||
let res = await Network.get(`${this.apiUrl}/recaptcha#${randomInt(0, 999)}`, await this.getApiHeaders(true)); //使用隨機fragment來強制url重新加載
|
||||
const captcha = JSON.parse(res.body);
|
||||
if (captcha.message != "ok") {
|
||||
throw "登錄失敗";
|
||||
}
|
||||
const captcha_code = await UI.showInputDialog("驗證碼", null, this.base64ToArrayBuffer(captcha.result.img));
|
||||
res = await Network.post(`${this.apiUrl}/token`, await this.getApiHeaders(true), {
|
||||
"grant_type": "password",
|
||||
"client_id": "2",
|
||||
"client_secret": "9eAhsCX3VWtyqTmkUo5EEaoH4MNPxrn6ZRwse7tE",
|
||||
"username": account,
|
||||
"password": pwd,
|
||||
"key": captcha.result.key,
|
||||
"captcha": captcha_code
|
||||
})
|
||||
this.processToken(res.body);
|
||||
return "ok";
|
||||
},
|
||||
|
||||
/**
|
||||
* [Optional] login with webview
|
||||
*/
|
||||
loginWithWebview: {
|
||||
url: "https://www.creative-comic.tw/zh/login",
|
||||
/**
|
||||
* check login status.
|
||||
* After successful login, the cookie will be automatically saved, and the localstorage can be retrieved using this.loadData("_localStorage").
|
||||
* @param url {string} - current url
|
||||
* @param title {string} - current title
|
||||
* @returns {boolean} - return true if login success
|
||||
*/
|
||||
checkStatus: (url, title) => {
|
||||
return (title == "CCC追漫台");
|
||||
},
|
||||
/**
|
||||
* [Optional] Callback when login success
|
||||
*/
|
||||
onLoginSuccess: () => {
|
||||
const localStorage = this.loadData("_localStorage");
|
||||
if (localStorage) {
|
||||
const token = localStorage["accessToken"];
|
||||
let base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
base64 = base64 + '='.repeat((4 - base64.length % 4) % 4);
|
||||
const jsonPayload = decodeURIComponent(
|
||||
Convert.decodeUtf8(Convert.decodeBase64(base64))
|
||||
.split('')
|
||||
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
);
|
||||
this.saveData("expireTime", JSON.parse(jsonPayload)["exp"]);
|
||||
this.saveData("refreshToken", localStorage["refreshToken"]);
|
||||
this.saveData("token", token);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* logout function, clear account related data
|
||||
*/
|
||||
logout: () => {
|
||||
this.deleteData("expireTime");
|
||||
this.deleteData("refreshToken");
|
||||
this.deleteData("token");
|
||||
},
|
||||
|
||||
// {string?} - register url
|
||||
registerWebsite: "https://www.creative-comic.tw/zh/signup"
|
||||
}
|
||||
|
||||
// explore page list
|
||||
explore = [
|
||||
{
|
||||
// title of the page.
|
||||
// title is used to identify the page, it should be unique
|
||||
title: "CCC追漫台",
|
||||
|
||||
/// multiPartPage or multiPageComicList or mixed
|
||||
type: "singlePageWithMultiPart",
|
||||
|
||||
/**
|
||||
* load function
|
||||
* @param page {number | null} - page number, null for `singlePageWithMultiPart` type
|
||||
* @returns {{}}
|
||||
* - for `multiPartPage` type, return [{title: string, comics: Comic[], viewMore: string?}]
|
||||
* - for `multiPageComicList` type, for each page(1-based), return {comics: Comic[], maxPage: number}
|
||||
* - for `mixed` type, use param `page` as index. for each index(0-based), return {data: [], maxPage: number?}, data is an array contains Comic[] or {title: string, comics: Comic[], viewMore: string?}
|
||||
*/
|
||||
load: async () => {
|
||||
const res = await Network.get(`${this.apiUrl}/public/home_v2`, await this.getApiHeaders());
|
||||
const result = {};
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
let curTitle = null;
|
||||
for (let data of jsonData["templates"]) {
|
||||
if ([4, 5].indexOf(data["type"]) != -1) {
|
||||
continue;
|
||||
}
|
||||
const comics = [];
|
||||
for (let c of data["list"]) {
|
||||
comics.push({
|
||||
id: c["value"],
|
||||
title: c["name"],
|
||||
cover: c["image1"]??c["image2"]??c["image3"],
|
||||
tags: [c["book_type"]["name"]],
|
||||
subtitle: c["brief"]
|
||||
});
|
||||
}
|
||||
if (data["title"]) {
|
||||
curTitle = data["title"];
|
||||
result[curTitle] = comics;
|
||||
} else {
|
||||
result[curTitle] = result[curTitle].concat(comics);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// categories
|
||||
category = {
|
||||
/// title of the category page, used to identify the page, it should be unique
|
||||
title: "CCC追漫台",
|
||||
parts: [
|
||||
{
|
||||
name: "CCC追漫台",
|
||||
type: "fixed",
|
||||
categories: ["排行榜"],
|
||||
itemType: "category",
|
||||
categoryParams: ["top"]
|
||||
}
|
||||
],
|
||||
// enable ranking page
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
/// category comic loading related
|
||||
categoryComics = {
|
||||
/**
|
||||
* load comics of a category
|
||||
* @param category {string} - category name
|
||||
* @param param {string?} - category param
|
||||
* @param options {string[]} - options from optionList
|
||||
* @param page {number} - page number
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (category, param, options, page) => {
|
||||
if (options == null) {
|
||||
options = ["", "read"];
|
||||
}
|
||||
const type = options[0] ? `&type=${options[0]}` : "";
|
||||
const url = `${this.apiUrl}/rank?page=${page}&rows_per_page=20&rank=${options[1]}&class=2${type}`;
|
||||
return await this.parseComics(url);
|
||||
},
|
||||
/**
|
||||
* [Optional] load options dynamically. If `optionList` is provided, this will be ignored.
|
||||
* @param category {string}
|
||||
* @param param {string?}
|
||||
* @return {Promise<{options: string[], label?: string}[]>} - return a list of option group, each group contains a list of options
|
||||
*/
|
||||
optionList: [
|
||||
{
|
||||
label: "分類",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"2-劇情",
|
||||
"6-愛情",
|
||||
"5-青春成長",
|
||||
"3-幽默搞笑",
|
||||
"10-歷史古裝",
|
||||
"7-奇幻架空",
|
||||
"4-溫馨療癒",
|
||||
"9-冒險動作",
|
||||
"8-恐怖驚悚",
|
||||
"12-新感覺推薦",
|
||||
"11-推理懸疑",
|
||||
"13-活動"
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "排行榜",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"read-人氣榜",
|
||||
"buy-銷售榜",
|
||||
"donate-斗内榜",
|
||||
"collect-收藏榜"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
/**
|
||||
* load search result
|
||||
* @param keyword {string}
|
||||
* @param options {(string | null)[]} - options from optionList
|
||||
* @param page {number}
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (keyword, options, page) => {
|
||||
options[0] = "&sort_by=" + options[0];
|
||||
if (options[1]) {
|
||||
options[1] = "&type=" + options[1];
|
||||
}
|
||||
if (options[2]) {
|
||||
options[2] = "&serial=" + options[2];
|
||||
}
|
||||
if (options[3]) {
|
||||
options[3] = "&updated_at=" + options[3];
|
||||
}
|
||||
if (options[4]) {
|
||||
options[4] = "&literature_form=" + options[4];
|
||||
}
|
||||
if (options[5]) {
|
||||
options[5] = "&comic_type=" + options[5];
|
||||
}
|
||||
if (options[6]) {
|
||||
options[6] = "&publisher=" + options[6];
|
||||
}
|
||||
const url = `https://api.creative-comic.tw/book?page=${page}&rows_per_page=20&keyword=${keyword}&class=2${options.join("")}`;
|
||||
return await this.parseComics(url);
|
||||
},
|
||||
|
||||
// provide options for search
|
||||
optionList: [
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"updated_at-最新",
|
||||
"read_count-閲覽",
|
||||
"like_count-推薦",
|
||||
"collect_count-收藏"
|
||||
],
|
||||
// option label
|
||||
label: "排序"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"2-劇情",
|
||||
"6-愛情",
|
||||
"5-青春成長",
|
||||
"3-幽默搞笑",
|
||||
"10-歷史古裝",
|
||||
"7-奇幻架空",
|
||||
"4-溫馨療癒",
|
||||
"9-冒險動作",
|
||||
"8-恐怖驚悚",
|
||||
"12-新感覺推薦",
|
||||
"11-推理懸疑",
|
||||
"13-活動"
|
||||
],
|
||||
// option label
|
||||
label: "分類"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"2-已完結",
|
||||
"0-連載中"
|
||||
],
|
||||
// option label
|
||||
label: "連載狀態"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"month-本月",
|
||||
"week-本周"
|
||||
],
|
||||
// option label
|
||||
label: "更新日期"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"1-短篇",
|
||||
"2-中篇",
|
||||
"3-長篇"
|
||||
],
|
||||
// option label
|
||||
label: "作品篇幅"
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"-全部",
|
||||
"3-條漫",
|
||||
"2-格漫",
|
||||
],
|
||||
// option label
|
||||
label: "作品形式"
|
||||
},
|
||||
{
|
||||
type: "dropdown",
|
||||
options: [
|
||||
"-全部",
|
||||
"44-MOJOIN",
|
||||
"37-目宿媒體股份有限公司",
|
||||
"4-大辣出版",
|
||||
"18-MarsCat火星貓科技",
|
||||
"2-CCC創作集",
|
||||
"23-海穹文化",
|
||||
"11-國立歷史博物館",
|
||||
"6-未來數位",
|
||||
"34-虎尾建國眷村再造協會",
|
||||
"24-鏡文學股份有限公司",
|
||||
"43-Taiwan Comic City",
|
||||
"42-聯經出版事業股份有限公司",
|
||||
"48-東立出版社有限公司",
|
||||
"9-留守番工作室",
|
||||
"16-獨步文化",
|
||||
"21-尖端媒體集團",
|
||||
"29-相之丘tōkhiu books",
|
||||
"7-威向文化",
|
||||
"54-白範出版工作室",
|
||||
"22-時報文化出版企業股份有限公司",
|
||||
"20-國立臺灣工藝研究發展中心",
|
||||
"17-獨立出版",
|
||||
"51-大寬文化工作室",
|
||||
"32-金繪國際有限公司",
|
||||
"47-前衛出版社",
|
||||
"36-奇異果文創",
|
||||
"14-綺影映畫",
|
||||
"53-彰化縣政府",
|
||||
"31-艾德萊娛樂",
|
||||
"8-特有生物研究保育中心",
|
||||
"39-聚場文化",
|
||||
"38-XPG",
|
||||
"52-陌上商行有限公司",
|
||||
"49-國際合製|臺漫新視界",
|
||||
"40-KADOKAWA",
|
||||
"10-國立臺灣美術館",
|
||||
"26-金漫獎",
|
||||
"5-台灣東販",
|
||||
"45-國立國父紀念館",
|
||||
"35-國立臺灣歷史博物館",
|
||||
"15-蓋亞文化",
|
||||
"1-長鴻出版社",
|
||||
"19-柒拾陸號原子",
|
||||
"33-台灣角川",
|
||||
"28-一顆星工作室",
|
||||
"46-好人出版",
|
||||
"27-澄波藝術文化股份有限公司",
|
||||
"12-黑白文化",
|
||||
"13-慢工文化 Slowork Publishing",
|
||||
"30-經濟部智慧財產局",
|
||||
"50-Contents Lab. Blue TOKYO",
|
||||
"3-大塊文化",
|
||||
"25-目色出版",
|
||||
"41-文化內容策進院"
|
||||
],
|
||||
label: "出版社"
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
// favorite related
|
||||
favorites = {
|
||||
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) => {
|
||||
if (!this.isLogged) {
|
||||
throw "請先登錄";
|
||||
}
|
||||
const res = await Network.put(`${this.apiUrl}/book/${comicId}/collect`, await this.getApiHeaders(), { "is_collected": isAdding });
|
||||
if (JSON.parse(res.body)["message"] != "ok") {
|
||||
throw `${isAdding ? "添加" : "移除"}收藏失敗`;
|
||||
}
|
||||
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) => {
|
||||
return this.parseComics(`${this.apiUrl}/bookcase/collections?page=${page}&rows_per_page=20&sort_by=updated_at&class=2`);
|
||||
},
|
||||
singleFolderForSingleComic: true,
|
||||
}
|
||||
|
||||
/// single comic related
|
||||
comic = {
|
||||
freeRead: (data) => {
|
||||
let free_read = true;
|
||||
if (!data["is_free"]) {
|
||||
if (data["sales_plan"] != 0) {
|
||||
if ((data["is_coin_buy"] || data["is_point_buy"]) && !data["is_buy"]) {
|
||||
if ((data["is_coin_rent"] || data["is_point_rent"]) && !data["is_rent"]) {
|
||||
free_read = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return free_read;
|
||||
},
|
||||
/**
|
||||
* load comic info
|
||||
* @param id {string}
|
||||
* @returns {Promise<ComicDetails>}
|
||||
*/
|
||||
loadInfo: async (id) => {
|
||||
const res = await Network.get(`${this.apiUrl}/book/${id}/info`, await this.getApiHeaders());
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
const authors = [];
|
||||
for (let a of jsonData["author"]) {
|
||||
authors.push(a["name"]);
|
||||
}
|
||||
const tags = [];
|
||||
for (let t of jsonData["tags"]) {
|
||||
tags.push(t["name"]);
|
||||
}
|
||||
const chapter_res = await Network.get(`${this.apiUrl}/book/${id}/chapter`, await this.getApiHeaders());
|
||||
const chapterData = JSON.parse(chapter_res.body)["data"];
|
||||
const chapters = {};
|
||||
for (let c of chapterData["chapters"]) {
|
||||
chapters[c["id"].toString()] = `${!this.comic.freeRead(c) ? "[付費]" : ""}${c["vol_name"]}-${c["name"]}`;
|
||||
}
|
||||
const recommend_res = await Network.get(`${this.apiUrl}/book/${id}/recommend`, await this.getApiHeaders());
|
||||
const recommendData = JSON.parse(recommend_res.body)["data"];
|
||||
const recommends = [];
|
||||
for (let r of recommendData["hot"]) {
|
||||
recommends.push({
|
||||
title: r["name"],
|
||||
cover: r["image1"]??r["image2"]??r["image3"],
|
||||
id: r["id"].toString(),
|
||||
subtitle: r["brief"]
|
||||
});
|
||||
}
|
||||
for (let r of recommendData["history"]) {
|
||||
recommends.push({
|
||||
title: r["name"],
|
||||
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"]??r["image2"]??r["image3"],
|
||||
id: r["id"].toString()
|
||||
});
|
||||
}
|
||||
return new ComicDetails({
|
||||
title: jsonData["name"],
|
||||
subtitle: jsonData["brief"],
|
||||
cover: jsonData["image1"]??jsonData["image2"]??jsonData["image3"],
|
||||
description: jsonData["description"],
|
||||
likesCount: jsonData["like_count_only_uuid"],
|
||||
chapters: chapters,
|
||||
tags: {
|
||||
"作者": authors,
|
||||
"分類": [jsonData["type"]["name"]],
|
||||
"標籤": tags,
|
||||
},
|
||||
isFavorite: (jsonData["is_collected"] == 1),
|
||||
updateTime: jsonData["updated_at"],
|
||||
recommend: recommends
|
||||
})
|
||||
},
|
||||
/**
|
||||
* load images of a chapter
|
||||
* @param comicId {string}
|
||||
* @param epId {string?}
|
||||
* @returns {Promise<{images: string[]}>}
|
||||
*/
|
||||
loadEp: async (comicId, epId) => {
|
||||
const res = await Network.get(`${this.apiUrl}/book/chapter/${epId}`, await this.getApiHeaders());
|
||||
if (res.status == 403) {
|
||||
UI.showDialog("提示", "該章節需付費后閲讀", [
|
||||
{
|
||||
text: "取消",
|
||||
callback: () => { }
|
||||
},
|
||||
{
|
||||
text: "去購買",
|
||||
callback: () => {
|
||||
UI.launchUrl(`https://www.creative-comic.tw/zh/book/${comicId}/content`);
|
||||
}
|
||||
}
|
||||
]);
|
||||
return { images: [] };
|
||||
}
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
const images = [];
|
||||
for (let img of jsonData["chapter"]["proportion"]) {
|
||||
images.push(img["id"].toString());
|
||||
}
|
||||
return {
|
||||
images: images
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [Optional] provide configs for an image loading
|
||||
* @param url
|
||||
* @param comicId
|
||||
* @param epId
|
||||
* @returns {{} | Promise<{}>}
|
||||
*/
|
||||
onImageLoad: async (url, comicId, epId) => {
|
||||
const res = await Network.get(`${this.apiUrl}/book/chapter/image/${url}`, await this.getApiHeaders());
|
||||
const encryptedKey = Convert.decodeBase64(JSON.parse(res.body)["data"]["key"]);
|
||||
let token = this.loadData("token");
|
||||
if (token == null) {
|
||||
token = "freeforccc2020reading";
|
||||
}
|
||||
const hashArray = Convert.sha512(Convert.encodeUtf8(token));
|
||||
const pageKey = hashArray.slice(0, 32);
|
||||
const pageIv = hashArray.slice(15, 31);
|
||||
const decryptedKey = new Uint8Array(Convert.decryptAesCbc(encryptedKey, pageKey, pageIv));
|
||||
const padLen = decryptedKey[decryptedKey.length - 1];
|
||||
const [key, iv] = Convert.decodeUtf8(decryptedKey.slice(0, decryptedKey.length - padLen).buffer).split(":");
|
||||
return {
|
||||
url: `https://storage.googleapis.com/ccc-www/fs/chapter_content/encrypt/${url}/2`,
|
||||
onResponse: function (buffer) {
|
||||
function hexToBytes(hex) {
|
||||
if (hex.length % 2 !== 0) {
|
||||
throw new Error("Invalid hex string");
|
||||
}
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
const decrypted = new Uint8Array(Convert.decryptAesCbc(buffer, hexToBytes(key), hexToBytes(iv)));
|
||||
const padLen_ = decrypted[decrypted.length - 1];
|
||||
const base64 = Convert.decodeUtf8(decrypted.slice(0, decrypted.length - padLen_).buffer);
|
||||
const base64Data = base64.split(',')[1] || base64;
|
||||
return Convert.decodeBase64(base64Data);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [Optional] load comments
|
||||
* @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) => {
|
||||
const res = await Network.get(`${this.apiUrl}/book/${comicId}/reply?page=${page}&rows_per_page=20&sort_by=created_at&descending=true#${randomInt(0, 999)}`,
|
||||
//使用隨機fragment來强制url重新加載
|
||||
await this.getApiHeaders());
|
||||
const jsonData = JSON.parse(res.body)["data"];
|
||||
let maxPage = 0;
|
||||
const comments = [];
|
||||
if (replyTo) {
|
||||
for (let c of jsonData["data"]) {
|
||||
if (c["id"].toString() == replyTo) {
|
||||
for (let c_ of c["replies"]) {
|
||||
comments.push({
|
||||
userName: c_["member"]["name"] ? c_["member"]["name"] : c_["member"]["nickname"],
|
||||
avatar: c_["member"]["avatar"],
|
||||
content: c_["content"],
|
||||
time: c_["created_at"],
|
||||
id: c_["id"].toString(),
|
||||
isLiked: (c_["is_like"] == 1),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let c of jsonData["data"]) {
|
||||
comments.push({
|
||||
userName: c["member"]["name"] ? c["member"]["name"] : c["member"]["nickname"],
|
||||
avatar: c["member"]["avatar"],
|
||||
content: c["content"],
|
||||
time: c["created_at"],
|
||||
replyCount: c["reply_count"],
|
||||
id: c["id"].toString(),
|
||||
isLiked: (c["is_like"] == 1),
|
||||
score: c["like_count"]
|
||||
});
|
||||
}
|
||||
maxPage = Math.ceil(jsonData["total"] / 20);
|
||||
}
|
||||
return {
|
||||
comments: comments,
|
||||
maxPage: maxPage
|
||||
};
|
||||
},
|
||||
/**
|
||||
* [Optional] send a comment, return any value to indicate success
|
||||
* @param comicId {string}
|
||||
* @param subId {string?} - ComicDetails.subId
|
||||
* @param content {string}
|
||||
* @param replyTo {string?} - commentId to reply, not null when reply to a comment
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
sendComment: async (comicId, subId, content, replyTo) => {
|
||||
if (!this.isLogged) {
|
||||
throw "請先登錄";
|
||||
}
|
||||
let url = null;
|
||||
if (replyTo) {
|
||||
url = `${this.apiUrl}/book/reply/${replyTo}/reply`;
|
||||
} else {
|
||||
url = `${this.apiUrl}/book/${comicId}/reply`;
|
||||
}
|
||||
const boundary = "----geckoformboundary" + Math.random().toString(16).replace(".", "a") + Math.random().toString(16).replace(".", "a");
|
||||
const body = `--${boundary}\r\n` +
|
||||
`Content-Disposition: form-data; name="content"\r\n\r\n${content}\r\n` +
|
||||
`--${boundary}\r\n` +
|
||||
`Content-Disposition: form-data; name="is_spoiled"\r\n\r\n0\r\n` +
|
||||
`--${boundary}--\r\n`;
|
||||
const headers = await this.getApiHeaders();
|
||||
headers["Content-Type"] = `multipart/form-data; boundary=${boundary}`;
|
||||
const res = await Network.post(url, headers, body);
|
||||
if (JSON.parse(res.body)["message"] != "ok") {
|
||||
throw "評論失敗";
|
||||
}
|
||||
return "ok";
|
||||
},
|
||||
likeComment: async (comicId, subId, commentId, isLike) => {
|
||||
if (commentId.endsWith("@")) {
|
||||
throw "不支持點贊";
|
||||
}
|
||||
const res = await Network.put(`${this.apiUrl}/book/reply/${commentId.split("@")[0]}/like`,
|
||||
await this.getApiHeaders(), { "is_like": isLike ? 1 : 0 });
|
||||
if (JSON.parse(res.body)["message"] != "ok") {
|
||||
throw "點贊失敗";
|
||||
}
|
||||
return "ok";
|
||||
},
|
||||
/**
|
||||
* [Optional] Handle tag click event
|
||||
* @param namespace {string}
|
||||
* @param tag {string}
|
||||
* @returns {{action: string, keyword: string, param: string?}}
|
||||
*/
|
||||
onClickTag: (namespace, tag) => {
|
||||
return {
|
||||
action: 'search',
|
||||
keyword: tag
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
365
comic_walker.js
Normal file
365
comic_walker.js
Normal file
@@ -0,0 +1,365 @@
|
||||
class ComicWalker extends ComicSource {
|
||||
name = "カドコミ";
|
||||
key = "comic_walker";
|
||||
version = "1.0.0";
|
||||
minAppVersion = "1.6.0";
|
||||
url =
|
||||
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/comic_walker.js";
|
||||
|
||||
api_key = "ytBrdQ2ZYdRQguqEusVLxQVUgakNnVht";
|
||||
|
||||
latestVersion = "1.4.13";
|
||||
|
||||
api_base = "https://mobileapp.comic-walker.com";
|
||||
|
||||
get headers() {
|
||||
const headers = {
|
||||
"X-API-Environment-Key": this.api_key,
|
||||
"User-Agent": `BookWalkerApp/${this.latestVersion} (Android 13)`,
|
||||
"Host": "mobileapp.comic-walker.com",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
const token = this.loadData("token");
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async refreshToken() {
|
||||
const res = await this.request(
|
||||
`${this.api_base}/v1/users`,
|
||||
this.headers,
|
||||
"POST",
|
||||
);
|
||||
|
||||
this.saveData("token", res.resources.access_token);
|
||||
return res.resources.access_token;
|
||||
}
|
||||
|
||||
async request(url, headers, method = "GET", data) {
|
||||
let response;
|
||||
if (method === "GET") {
|
||||
response = await Network.get(url, headers);
|
||||
} else if (method === "POST") {
|
||||
response = await Network.post(url, headers, data);
|
||||
} else {
|
||||
throw new Error(`Unsupported method: ${method}`);
|
||||
}
|
||||
if (
|
||||
response.status === 204
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
response = JSON.parse(response.body);
|
||||
if (
|
||||
response.code === "invalid_request_parameter" ||
|
||||
response.code === "free_daily_reward_quota_exceeded" ||
|
||||
response.code === "unauthorized"
|
||||
) {
|
||||
await this.refreshToken();
|
||||
if (method === "GET") {
|
||||
response = await Network.get(url, this.headers);
|
||||
} else if (method === "POST") {
|
||||
response = await Network.post(url, this.headers, data);
|
||||
} else {
|
||||
throw new Error(`Unsupported method: ${method}`);
|
||||
}
|
||||
if (
|
||||
response.status === 204
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
response = JSON.parse(response.body);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const itunes_api = "https://itunes.apple.com/lookup?bundleId=jp.co.bookwalker.cwapp.ios&country=jp";
|
||||
|
||||
const resp = await Network.get(itunes_api);
|
||||
|
||||
if (resp.status == 200) {
|
||||
response = JSON.parse(resp.body);
|
||||
this.latestVersion = response.version;
|
||||
}
|
||||
|
||||
await this.refreshToken();
|
||||
}
|
||||
|
||||
explore = [
|
||||
{
|
||||
title: "カドコミ",
|
||||
type: "singlePageWithMultiPart",
|
||||
load: async () => {
|
||||
const res = await this.request(
|
||||
`${this.api_base}/v2/screens/home`,
|
||||
this.headers,
|
||||
);
|
||||
|
||||
const result = {};
|
||||
|
||||
const newArrivals = res.resources.new_arrival_comics.map((item) =>
|
||||
new Comic({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
cover: item.thumbnail_1x1 || "",
|
||||
tags: item.comic_labels?.map((l) => l.name) || [],
|
||||
}),
|
||||
);
|
||||
result["今日の更新"] = newArrivals;
|
||||
|
||||
const attention = res.resources.attention_comics.map((item) =>
|
||||
new Comic({
|
||||
id: item.comic_id,
|
||||
title: item.title,
|
||||
cover: item.image_url || "",
|
||||
tags: item.comic_labels?.map((l) => l.name) || [],
|
||||
}),
|
||||
);
|
||||
result["注目作品"] = attention;
|
||||
|
||||
for (const pickup of res.resources.pickup_comics) {
|
||||
const comics = pickup.comics.map((item) =>
|
||||
new Comic({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
cover: item.thumbnail_1x1 || "",
|
||||
tags: item.comic_labels?.map((l) => l.name) || [],
|
||||
}),
|
||||
);
|
||||
result[pickup.name] = comics;
|
||||
}
|
||||
|
||||
const newSerialization = res.resources.new_serialization_comics.map((item) =>
|
||||
new Comic({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
cover: item.thumbnail_1x1 || "",
|
||||
tags: item.comic_labels?.map((l) => l.name) || [],
|
||||
}),
|
||||
);
|
||||
result["新連載"] = newSerialization;
|
||||
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
search = {
|
||||
load: async (keyword, _, page) => {
|
||||
const res = await this.request(
|
||||
`${this.api_base}/v1/search/comics?keyword=${keyword}&limit=20&offset=${
|
||||
(page - 1) * 20
|
||||
}`,
|
||||
this.headers,
|
||||
);
|
||||
|
||||
const comics = res.resources.map((item) =>
|
||||
new Comic({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
cover: item.thumbnail_1x1 || "",
|
||||
tags: [
|
||||
...(item.authors?.map((a) => a.name) || []),
|
||||
...(item.comic_labels?.map((l) => l.name) || []),
|
||||
],
|
||||
})
|
||||
);
|
||||
const pageInfo = {
|
||||
hasNextPage: res.resources.length === 20,
|
||||
endCursor: null,
|
||||
};
|
||||
|
||||
return {
|
||||
comics,
|
||||
maxPage: pageInfo.hasNextPage ? (page || 1) + 1 : (page || 1),
|
||||
endCursor: pageInfo.endCursor,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
comic = {
|
||||
loadInfo: async (id) => {
|
||||
const res = await this.request(
|
||||
`${this.api_base}/v2/screens/comics/${id}`,
|
||||
this.headers,
|
||||
);
|
||||
const detail = res.resources.detail;
|
||||
|
||||
const totalCount = res.resources.episode_total_count || 0;
|
||||
let episodes = { resources: [] };
|
||||
for (let offset = 0; offset < totalCount; offset += 100) {
|
||||
const chunk = await this.request(
|
||||
`${this.api_base}/v1/comics/${id}/episodes?offset=${offset}&limit=100&sort=asc`,
|
||||
this.headers,
|
||||
);
|
||||
episodes.resources.push(...(chunk.resources || []));
|
||||
}
|
||||
|
||||
const tags = new Map();
|
||||
|
||||
if (detail.authors) {
|
||||
detail.authors.forEach((a) => {
|
||||
if (!tags.has(a.role)) tags.set(a.role, []);
|
||||
tags.get(a.role).push(a.name);
|
||||
});
|
||||
}
|
||||
|
||||
if (detail.comic_labels) {
|
||||
detail.comic_labels.forEach((l) => {
|
||||
if (!tags.has("Labels")) tags.set("Labels", []);
|
||||
tags.get("Labels").push(l.name);
|
||||
});
|
||||
}
|
||||
|
||||
if (detail.tags) {
|
||||
detail.tags.forEach((t) => {
|
||||
if (!tags.has(t.type)) tags.set(t.type, []);
|
||||
tags.get(t.type).push(t.name);
|
||||
});
|
||||
}
|
||||
|
||||
const chapters = new Map();
|
||||
for (const ep of episodes.resources) {
|
||||
let canRent = false;
|
||||
const plans = (ep.plans || []).filter((plan) =>
|
||||
plan.type !== "paid"
|
||||
);
|
||||
if (Array.isArray(plans) && plans.length > 0) {
|
||||
canRent = true;
|
||||
}
|
||||
const title = canRent ? ep.title : `❌ ${ep.title}`;
|
||||
chapters.set(ep.id, title);
|
||||
}
|
||||
|
||||
return new ComicDetails({
|
||||
title: detail.title,
|
||||
subtitle: detail.authors?.map((a) => a.name).join("・") || "",
|
||||
cover: detail.thumbnail_1x1 || "",
|
||||
description: detail.story?.replace(/<br\s*\/?>/gi, "\n") || "",
|
||||
tags,
|
||||
chapters,
|
||||
updateTime: detail.next_update_at,
|
||||
url: detail.share_url,
|
||||
maxPage: totalCount,
|
||||
});
|
||||
},
|
||||
|
||||
loadEp: async (comicId, epId) => {
|
||||
let detail = await this.request(
|
||||
`${this.api_base}/v1/episodes/${epId}`,
|
||||
this.headers,
|
||||
);
|
||||
const plans = (detail.plans || []).filter((plan) =>
|
||||
// plan.type !== "daily_video_free" &&
|
||||
plan.type !== "paid"
|
||||
);
|
||||
if (
|
||||
!Array.isArray(plans) ||
|
||||
plans.length === 0
|
||||
) {
|
||||
throw new Error("No available rental plans after filtering");
|
||||
}
|
||||
console.log(plans);
|
||||
const freePlan = plans.find((plan) => plan.type === "free");
|
||||
if (!freePlan) {
|
||||
const plan = plans[randomInt(0, plans.length - 1)];
|
||||
await this.request(
|
||||
`${this.api_base}/v1/users/me/rental_episodes`,
|
||||
this.headers,
|
||||
"POST",
|
||||
{ episode_id: epId, reading_method: plan.type },
|
||||
);
|
||||
}
|
||||
let res = await this.request(
|
||||
`${this.api_base}/v1/screens/comics/${comicId}/episodes/${epId}/viewer`,
|
||||
this.headers,
|
||||
);
|
||||
const manuscripts = res.resources.manuscripts || [];
|
||||
return {
|
||||
images: manuscripts.map((m) =>
|
||||
`${m.drm_image_url}&drm_hash=${m.drm_hash}`
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
onImageLoad: (url) => {
|
||||
let drm_hash = null;
|
||||
let cleanUrl = url;
|
||||
const drmHashMatch = url.match(/[?&]drm_hash=([^&]+)/);
|
||||
if (drmHashMatch) {
|
||||
drm_hash = decodeURIComponent(drmHashMatch[1]);
|
||||
cleanUrl = url.replace(/([?&])drm_hash=[^&]+(&)?/, (match, p1, p2) => {
|
||||
if (p2) return p1;
|
||||
return "";
|
||||
}).replace(/[?&]$/, "");
|
||||
}
|
||||
cleanUrl = cleanUrl.replace(/([?&])weight=[^&]+(&)?/, (match, p1, p2) => {
|
||||
if (p2) return p1;
|
||||
return "";
|
||||
}).replace(/[?&]$/, "");
|
||||
|
||||
cleanUrl = cleanUrl.replace(/([?&])height=[^&]+(&)?/, (match, p1, p2) => {
|
||||
if (p2) return p1;
|
||||
return "";
|
||||
}).replace(/[?&]$/, "");
|
||||
|
||||
if (drm_hash.length < 2) {
|
||||
throw new Error(
|
||||
"drm_hash must be at least 2 characters long",
|
||||
);
|
||||
}
|
||||
var version = drm_hash.slice(0, 2);
|
||||
if (version !== "01") {
|
||||
throw new Error("Unsupported version: " + version);
|
||||
}
|
||||
var key_part = drm_hash.slice(2);
|
||||
if (key_part.length < 16) {
|
||||
throw new Error(
|
||||
"Key part must be 16 characters long (8 hex numbers)",
|
||||
);
|
||||
}
|
||||
var key = [];
|
||||
for (var i = 0; i < 8; i++) {
|
||||
key.push(parseInt(key_part.slice(i * 2, i * 2 + 2), 16));
|
||||
}
|
||||
|
||||
const keyArray = key;
|
||||
const onResponseScript = `
|
||||
function onResponse(buffer) {
|
||||
var key = [${keyArray.join(',')}];
|
||||
var view = new Uint8Array(buffer);
|
||||
for (var i = 0; i < view.length; i++) {
|
||||
view[i] ^= key[i % key.length];
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
onResponse;
|
||||
`;
|
||||
return {
|
||||
url: cleanUrl,
|
||||
headers: this.headers,
|
||||
onResponse: async (buffer) => {
|
||||
return await compute(onResponseScript, buffer);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
onClickTag: (namespace, tag) => {
|
||||
if (
|
||||
namespace === "漫画" || namespace === "原作" ||
|
||||
namespace === "キャラクター原案" || namespace === "著者"
|
||||
) {
|
||||
return {
|
||||
action: "search",
|
||||
keyword: tag,
|
||||
param: null,
|
||||
};
|
||||
}
|
||||
throw "未支持此类Tag检索";
|
||||
},
|
||||
};
|
||||
}
|
||||
440
comick.js
440
comick.js
@@ -1,7 +1,7 @@
|
||||
class Comick extends ComicSource {
|
||||
name = "comick"
|
||||
key = "comick"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
minAppVersion = "1.4.0"
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/comick.js"
|
||||
@@ -11,32 +11,15 @@ class Comick extends ComicSource {
|
||||
title: "主页源",
|
||||
type: "select",
|
||||
options: [
|
||||
{value: "comick.io"},
|
||||
{value: "preview.comick.io"}
|
||||
{value: "comick.art"},
|
||||
],
|
||||
default: "preview.comick.io"
|
||||
},
|
||||
lang_len: {
|
||||
title: "最大语言数量(不建议大于5)",
|
||||
type: "select",
|
||||
options: [
|
||||
{value: "1"},
|
||||
{value: "2"},
|
||||
{value: "3"},
|
||||
{value: "4"},
|
||||
{value: "5"},
|
||||
{value: "8"},
|
||||
{value: "10"},
|
||||
{value: "15"},
|
||||
{value: "20"},
|
||||
],
|
||||
default: "3"
|
||||
default: "comick.art"
|
||||
},
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
let domain = this.loadSetting('domains') || this.settings.domains.default;
|
||||
return `https://${domain}`;
|
||||
// let domain = this.loadSetting('domains') || this.settings.domains.default;
|
||||
return `https://comick.art`;
|
||||
}
|
||||
|
||||
static comic_status = {
|
||||
@@ -341,7 +324,7 @@ class Comick extends ComicSource {
|
||||
}
|
||||
|
||||
static getRandomHeaders() {
|
||||
const userAgents = [
|
||||
let userAgents = [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
||||
@@ -353,7 +336,8 @@ class Comick extends ComicSource {
|
||||
"User-Agent": userAgents[Math.floor(Math.random() * userAgents.length)],
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Connection": "keep-alive"
|
||||
"Connection": "keep-alive",
|
||||
'referer': 'https://comick.art/'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -362,18 +346,17 @@ class Comick extends ComicSource {
|
||||
id: `${book.relates?.slug || 'unknown'}//${book.relates?.title || '未知标题'}`,
|
||||
title: book.relates?.title || '未知标题',
|
||||
cover: book.relates?.md_covers?.[0]?.b2key
|
||||
? `https://meo.comick.pictures/${book.relates.md_covers[0].b2key}`
|
||||
? `https://cdn1.comicknew.pictures/${book.relates.slug}/covers/${book.relates.md_covers[0].b2key}`
|
||||
: 'w7xqzd.jpg',
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
transformBookList(bookList, descriptionPrefix = "更新至:") {
|
||||
return bookList.map(book => ({
|
||||
id: `${book.slug || 'unknown'}//${book.title || '未知标题'}`,
|
||||
title: book.title || '未知标题',
|
||||
cover: book.md_covers?.[0]?.b2key
|
||||
? `https://meo.comick.pictures/${book.md_covers[0].b2key}`
|
||||
: 'w7xqzd.jpg',
|
||||
cover: book.default_thumbnail ? book.default_thumbnail : book.full_image_path ? book.full_image_path : 'https://comick.art/images/default-thumbnail.webp',
|
||||
tags: [],
|
||||
description: `${descriptionPrefix}${book.last_chapter || "未知"}`
|
||||
}));
|
||||
@@ -383,13 +366,11 @@ class Comick extends ComicSource {
|
||||
return {
|
||||
id: `${manga.slug || 'unknown'}//${manga.title || '未知标题'}`,
|
||||
title: manga.title || "无标题",
|
||||
cover: manga.md_covers?.[0]?.b2key
|
||||
? `https://meo.comick.pictures/${manga.md_covers[0].b2key}`
|
||||
: 'w7xqzd.jpg',
|
||||
cover: manga.default_thumbnail ? manga.default_thumbnail : manga.full_image_path ? manga.full_image_path : 'https://comick.art/images/default-thumbnail.webp',
|
||||
tags: [
|
||||
`更新时间: ${manga.uploaded_at ? new Date(manga.uploaded_at).toISOString().split('T')[0] : ''}`
|
||||
`更新时间: ${manga.uploaded_at ? new Date(manga.uploaded_at).toISOString().split('T')[0] : new Date(manga.created_at).toISOString().split('T')[0]}`
|
||||
],
|
||||
description: manga.desc || "暂无描述"
|
||||
description: manga.description || "暂无描述"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -397,24 +378,25 @@ class Comick extends ComicSource {
|
||||
title: "comick",
|
||||
type: "singlePageWithMultiPart",
|
||||
load: async () => {
|
||||
let url = this.baseUrl === "https://comick.io"
|
||||
? "https://comick.io/home2"
|
||||
: this.baseUrl;
|
||||
// let url = this.baseUrl === "https://comick.art"
|
||||
// ? "https://comick.art/home2"
|
||||
// : this.baseUrl;
|
||||
let url = 'https://comick.art/home'
|
||||
|
||||
let res = await Network.get(url);
|
||||
if (res.status !== 200) throw "Request Error: " + res.status;
|
||||
|
||||
let document = new HtmlDocument(res.body);
|
||||
let jsonData = JSON.parse(document.getElementById('__NEXT_DATA__').text);
|
||||
let mangaData = jsonData.props.pageProps.data;
|
||||
let jsonData = JSON.parse(document.getElementById('sv-data').text);
|
||||
let mangaData = jsonData.data;
|
||||
|
||||
// 使用统一函数转换数据
|
||||
const result = {
|
||||
"最近更新": this.transformBookList(mangaData.extendedNews),
|
||||
"最近上传": this.transformBookList(mangaData.news),
|
||||
"最近热门": this.transformBookList(mangaData.recentRank),
|
||||
"总热门": this.transformBookList(mangaData.rank),
|
||||
"完结": this.transformBookList(mangaData.completions),
|
||||
let result = {
|
||||
"最近更新": this.transformBookList(mangaData.most_follow_new['7']),
|
||||
"最近上传": this.transformBookList(mangaData.recent_add),
|
||||
// "最近热门": this.transformBookList(mangaData.follows['7']),
|
||||
"最近热门": this.transformBookList(mangaData.popular_ongoing),
|
||||
"完结": this.transformBookList(mangaData.completed)
|
||||
};
|
||||
|
||||
return result;
|
||||
@@ -437,18 +419,18 @@ class Comick extends ComicSource {
|
||||
categoryComics = {
|
||||
load: async (category, param, options, page) => {
|
||||
// 基础URL
|
||||
let url = "https://api.comick.io/v1.0/search?";
|
||||
let url = "https://comick.art/api/search?";
|
||||
let params = [
|
||||
`genres=${encodeURIComponent(param)}`,
|
||||
`genres[]=${encodeURIComponent(param)}`,
|
||||
`page=${encodeURIComponent(page)}`
|
||||
];
|
||||
|
||||
if (options[0]) {
|
||||
params.push(`sort=${encodeURIComponent(options[0].split("-")[0])}`);
|
||||
params.push(`order_by=${encodeURIComponent(options[0].split("-")[0])}`);
|
||||
}
|
||||
|
||||
if (options[1] && options[1] !== "-全部") {
|
||||
params.push(`country=${encodeURIComponent(options[1].split("-")[0])}`);
|
||||
params.push(`country[]=${encodeURIComponent(options[1].split("-")[0])}`);
|
||||
}
|
||||
|
||||
if (options[2]) {
|
||||
@@ -456,38 +438,42 @@ class Comick extends ComicSource {
|
||||
}
|
||||
|
||||
url += params.join('&');
|
||||
|
||||
let res = await Network.get(url);
|
||||
let headers = Comick.getRandomHeaders();
|
||||
let res = await Network.get(url=url, headers=headers);
|
||||
if (res.status !== 200) throw "Request Error: " + res.status;
|
||||
|
||||
let mangaList = JSON.parse(res.body);
|
||||
let mangaList = JSON.parse(res.body).data;
|
||||
if (!Array.isArray(mangaList)) throw "Invalid data format";
|
||||
|
||||
let maxpage = mangaList.total/mangaList.per_page
|
||||
return {
|
||||
comics: mangaList.map(this.getFormattedManga),
|
||||
maxPage: 50
|
||||
maxPage: maxpage
|
||||
};
|
||||
},
|
||||
optionList: [
|
||||
{options: ["uploaded-更新排序","user_follow_count-关注排序", "rating-评分排序", "created_at-创建排序"]},
|
||||
{options: ["created_at-更新排序","user_follow_count-关注排序", "rating-评分排序", "uploaded-创建排序"]},
|
||||
{options: ["-全部", "cn-国漫", "jp-日本", "kr-韩国", "others-欧美"]},
|
||||
{options: ["1-连载", "2-完结", "3-休刊", "4-暂停更新"]}
|
||||
{options: ["1-连载", "2-完结", "3-休刊", "4-暂停更新"]},
|
||||
]
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
load: async (keyword, options, page) => {
|
||||
let url = `https://api.comick.io/v1.0/search?q=${keyword}&limit=49&page=${page}`;
|
||||
let res = await Network.get(url);
|
||||
if (res.status !== 200) throw "Request Error: " + res.status;
|
||||
|
||||
let mangaList = JSON.parse(res.body);
|
||||
let headers = Comick.getRandomHeaders();
|
||||
let url = `https://comick.art/search?q=${keyword}&page=${page}`;
|
||||
let res = await Network.get(url=url, headers=headers);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
let document = new HtmlDocument(res.body)
|
||||
let jsonData = JSON.parse(document.getElementById('sv-data').text);
|
||||
let mangaList = jsonData.data;
|
||||
if (!Array.isArray(mangaList)) throw "Invalid data format";
|
||||
|
||||
let maxpage = mangaList.total/mangaList.per_page
|
||||
return {
|
||||
comics: mangaList.map(this.getFormattedManga),
|
||||
maxPage: 1
|
||||
maxPage: Math.ceil(maxpage)
|
||||
};
|
||||
},
|
||||
optionList: []
|
||||
@@ -497,138 +483,139 @@ class Comick extends ComicSource {
|
||||
comic = {
|
||||
id: null,
|
||||
buildId: null,
|
||||
|
||||
|
||||
loadInfo: async (id) => {
|
||||
let headers = Comick.getRandomHeaders();
|
||||
|
||||
|
||||
const [cId, cTitle] = id.split("//");
|
||||
let [cId, cTitle] = id.split("//");
|
||||
if (!cId) {
|
||||
throw "ID error: ";
|
||||
}
|
||||
|
||||
let res = await Network.get(`${this.baseUrl}/comic/${cId}`, { headers });
|
||||
let res = await Network.get(
|
||||
`https://comick.art/comic/${cId}`,
|
||||
headers
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
throw "Invalid status code: " + res.status
|
||||
}
|
||||
|
||||
let load_chapter = async (slug, comicData) => {
|
||||
let langBuckets = new Map();
|
||||
let latestTimestamp = null;
|
||||
let page = 1;
|
||||
let lastPage = 1;
|
||||
|
||||
// 加载漫画信息
|
||||
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 headers = Comick.getRandomHeaders();
|
||||
|
||||
let first = langMap[lang];
|
||||
if (first.vol == null && first.chap == null) {
|
||||
const chapters = new Map();
|
||||
chapters.set(`${first.hid || 'unknown'}//no//-1//${first.lang || 'unknown'}`, '无标卷');
|
||||
result[Comick.language_dict[lang] || lang] = chapters;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 构造章节请求 URL
|
||||
const url =
|
||||
`${this.baseUrl}/_next/data/${buildId}/comic/${id}/${first.hid || 'unknown'}` +
|
||||
(first.chap != null
|
||||
? `-chapter-${first.chap}`
|
||||
: `-volume-${first.vol}`) +
|
||||
`-${lang}.json?slug=${id}&` +
|
||||
(first.chap != null
|
||||
? `chapter=${first.hid || 'unknown'}`
|
||||
: `volume=${first.hid || 'unknown'}`)
|
||||
+
|
||||
(first.chap != null
|
||||
? `-chapter-${first.chap}`
|
||||
: `-volume-${first.vol}`) + `-${lang}`
|
||||
;
|
||||
|
||||
const res = await Network.get(url, { headers });
|
||||
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 || 'unknown'}//no//-1//${first.lang || 'unknown'}`;
|
||||
label = '无标卷';
|
||||
} else if (ch.chap != null) {
|
||||
key = `${ch.hid || 'unknown'}//chapter//${ch.chap}//${first.lang || 'unknown'}`;
|
||||
label = `第${ch.chap}话`;
|
||||
} else {
|
||||
key = `${ch.hid || 'unknown'}//volume//${ch.vol}//${first.lang || 'unknown'}`;
|
||||
label = `第${ch.vol}卷`;
|
||||
let collectChapters = (items) => {
|
||||
items.forEach(item => {
|
||||
let langCode = item?.lang || 'unknown';
|
||||
if (!langBuckets.has(langCode)) {
|
||||
langBuckets.set(langCode, []);
|
||||
}
|
||||
chapters.set(key, label);
|
||||
langBuckets.get(langCode).push(item);
|
||||
});
|
||||
};
|
||||
|
||||
console.log(`开始加载章节列表,漫画slug: ${slug}`);
|
||||
while (page <= lastPage) {
|
||||
let url = `https://comick.art/api/comics/${slug}/chapter-list?page=${page}`;
|
||||
let resCh = await Network.get(url=url, headers=Comick.getRandomHeaders());
|
||||
console.log(`请求章节列表页面 ${page},URL: ${resCh}`);
|
||||
if (resCh.status !== 200) {
|
||||
throw `Invalid status code: ${resCh.status}`;
|
||||
}
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(resCh.body);
|
||||
} catch (err) {
|
||||
throw "Invalid chapter list response";
|
||||
}
|
||||
|
||||
let data = Array.isArray(payload?.data) ? payload.data : [];
|
||||
if (page === 1 && data.length > 0) {
|
||||
latestTimestamp = data[0].updated_at || data[0].publish_at || data[0].created_at || null;
|
||||
}
|
||||
collectChapters(data);
|
||||
|
||||
let pagination = payload?.pagination;
|
||||
if (pagination && pagination.last_page != null) {
|
||||
let parsed = parseInt(pagination.last_page, 10);
|
||||
if (!Number.isNaN(parsed) && parsed > 0) {
|
||||
lastPage = parsed;
|
||||
}
|
||||
}
|
||||
page += 1;
|
||||
}
|
||||
|
||||
let result = new Map();
|
||||
|
||||
langBuckets.forEach((items, langCode) => {
|
||||
let chaptersMap = new Map();
|
||||
let orderedItems = items.slice().reverse(); // API 按最新在前,反转便于正序浏览
|
||||
|
||||
orderedItems.forEach(item => {
|
||||
let lang = item?.lang || 'unknown';
|
||||
let hid = item?.hid || 'unknown';
|
||||
let hasChap = item?.chap != null && item.chap !== "";
|
||||
let hasVol = item?.vol != null && item.vol !== "";
|
||||
let key;
|
||||
let label;
|
||||
|
||||
if (hasChap) {
|
||||
key = `${hid}//chapter//${item.chap}//${lang}`;
|
||||
label = `第${item.chap}话`;
|
||||
} else if (hasVol) {
|
||||
key = `${hid}//volume//${item.vol}//${lang}`;
|
||||
label = `第${item.vol}卷`;
|
||||
} else {
|
||||
key = `${hid}//no//-1//${lang}`;
|
||||
label = item?.title ? item.title : '无标卷';
|
||||
}
|
||||
|
||||
chaptersMap.set(key, label);
|
||||
});
|
||||
|
||||
result[Comick.language_dict[lang] || lang] = chapters;
|
||||
let displayLang = Comick.language_dict[langCode] || langCode || '未知语言';
|
||||
result.set(displayLang, chaptersMap);
|
||||
});
|
||||
|
||||
let updateTime = "暂无更新";
|
||||
if (latestTimestamp) {
|
||||
let date = new Date(latestTimestamp);
|
||||
if (!isNaN(date.getTime())) {
|
||||
updateTime = date.toISOString().split('T')[0];
|
||||
} else {
|
||||
updateTime = latestTimestamp;
|
||||
}
|
||||
// 5. 返回 Map<语言, Map<章节Key, 章节名称>>
|
||||
return [new Map(Object.entries(result)), updateTime];
|
||||
} else if (comicData?.last_chapter) {
|
||||
updateTime = `第${comicData.last_chapter}话`;
|
||||
}
|
||||
return [result, updateTime];
|
||||
};
|
||||
|
||||
//填充文章id:
|
||||
this.comic.id = id;
|
||||
let document = new HtmlDocument(res.body)
|
||||
let jsonData = JSON.parse(document.getElementById('__NEXT_DATA__').text);
|
||||
let comicData = jsonData.props?.pageProps?.comic;
|
||||
let authorData = jsonData.props?.pageProps?.authors || [];
|
||||
let jsonData = JSON.parse(document.getElementById('comic-data').text);
|
||||
let comicData = jsonData;
|
||||
let authorData = comicData.authors || [];
|
||||
let title = cTitle || comicData?.title || "未知标题";
|
||||
let status = comicData?.status || "1"; // 默认连载
|
||||
let cover = comicData?.md_covers?.[0]?.b2key ? `https://meo.comick.pictures/${comicData.md_covers[0].b2key}` : 'w7xqzd.jpg';
|
||||
let cover = comicData.default_thumbnail ? comicData.default_thumbnail : comicData.full_image_path ? comicData.full_image_path : 'https://comick.art/images/default-thumbnail.webp';
|
||||
let author = authorData[0]?.name || "未知作者";
|
||||
|
||||
// 提取标签的slug数组的代码
|
||||
let extractSlugs = (comicData) => {
|
||||
try {
|
||||
// 获取md_comic_md_genres数组
|
||||
const genres = comicData?.md_comic_md_genres;
|
||||
let genres = comicData?.md_comic_md_genres;
|
||||
if (!genres || !Array.isArray(genres)) {
|
||||
return [];
|
||||
}
|
||||
// 使用map提取每个md_genres中的slug
|
||||
const slugs = genres.map(genre => genre?.md_genres?.slug).filter(slug => slug != null);
|
||||
let slugs = genres.map(genre => genre?.md_genres?.slug).filter(slug => slug != null);
|
||||
return slugs;
|
||||
} catch (error) {
|
||||
return []; // 返回空数组作为容错处理
|
||||
@@ -637,93 +624,45 @@ class Comick extends ComicSource {
|
||||
|
||||
let tags = extractSlugs(comicData);
|
||||
// 转换 tags 数组,如果找不到对应值则保留原值
|
||||
const translatedTags = tags.map(tag => {
|
||||
let translatedTags = tags.map(tag => {
|
||||
return Comick.category_param_dict[tag] || tag; // 如果字典里没有,就返回原值
|
||||
});
|
||||
let description = comicData?.desc || "暂无描述";
|
||||
|
||||
//处理推荐列表
|
||||
let recommends = this.transReformBookList(comicData?.recommendations || []);
|
||||
//只要recommends数组前面十个,不够十个则就是recommends的长度
|
||||
recommends = recommends.slice(0, Math.min(recommends.length, 10));
|
||||
// //处理推荐列表
|
||||
// let recommends = this.transReformBookList(comicData?.relate_from || []);
|
||||
// //只要recommends数组前面十个,不够十个则就是recommends的长度
|
||||
// recommends = recommends.slice(0, Math.min(recommends.length, 10));
|
||||
|
||||
//处理空漫画
|
||||
let firstChapters = jsonData.props?.pageProps?.firstChapters || [];
|
||||
let fallbackUpdate = comicData?.last_chapter ? `第${comicData.last_chapter}话` : "暂无更新";
|
||||
let chapters = new Map();
|
||||
let updateTime = fallbackUpdate;
|
||||
|
||||
if((comicData?.chapter_count == 0 || !comicData?.chapter_count) && firstChapters.length == 0){
|
||||
let chapters = new Map()
|
||||
try {
|
||||
let temp = await load_chapter(cId, comicData);
|
||||
if (Array.isArray(temp)) {
|
||||
chapters = temp[0] instanceof Map ? temp[0] : chapters;
|
||||
updateTime = typeof temp[1] === 'string' && temp[1].length > 0 ? temp[1] : updateTime;
|
||||
}
|
||||
} catch (error) {
|
||||
chapters = new Map();
|
||||
}
|
||||
|
||||
if (chapters.size === 0) {
|
||||
return {
|
||||
title: title,
|
||||
cover: cover,
|
||||
description: description,
|
||||
tags: {
|
||||
"语言": [],
|
||||
"作者": [author],
|
||||
"更新": ["暂无更新"],
|
||||
"标签": translatedTags,
|
||||
"状态": [Comick.comic_status[status]]
|
||||
},
|
||||
chapters: chapters,
|
||||
}
|
||||
}
|
||||
|
||||
// let updateTime = comicData.last_chapter ? "第" + comicData.last_chapter + "话" : " "; //这里目前还无法实现更新时间
|
||||
let buildId = jsonData?.buildId;
|
||||
let slug = jsonData?.query?.slug;
|
||||
let firstChapter = firstChapters.length > 0 ? firstChapters[0] : null;
|
||||
|
||||
// 处理无章节的情况
|
||||
if (!firstChapter) {
|
||||
let chapters = new Map();
|
||||
let updateTime = comicData?.last_chapter ? "第" + comicData.last_chapter + "话" : "暂无更新";
|
||||
return {
|
||||
title: title,
|
||||
cover: cover,
|
||||
description: description,
|
||||
tags: {
|
||||
"作者": [author],
|
||||
"更新": [updateTime],
|
||||
"标签": translatedTags,
|
||||
"状态": [Comick.comic_status[status]]
|
||||
},
|
||||
chapters: chapters,
|
||||
recommend: recommends || []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 处理无标卷和无标话的情况
|
||||
if(firstChapter.vol == null && firstChapter.chap == null){
|
||||
for(let i = 0; i < firstChapters.length; i++) {
|
||||
if(firstChapters[i].vol != null || firstChapters[i].chap != null){
|
||||
firstChapter = firstChapters[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果处理完成之后依然章节没有卷和话信息,直接返回无标卷
|
||||
if(firstChapter.vol == null && firstChapter.chap == null){
|
||||
let chapters = new Map()
|
||||
let updateTime = comicData?.last_chapter ? "第" + comicData.last_chapter + "话" : "暂无更新";
|
||||
chapters.set((firstChapter.hid || 'unknown') + "//no//-1//" + (firstChapter.lang || 'unknown'), "无标卷")
|
||||
return {
|
||||
title: title,
|
||||
cover: cover,
|
||||
description: description,
|
||||
tags: {
|
||||
"作者": [author],
|
||||
"更新": [updateTime],
|
||||
"标签": translatedTags,
|
||||
"状态": [Comick.comic_status[status]]
|
||||
},
|
||||
chapters: chapters,
|
||||
recommend: recommends || []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//获取章节
|
||||
let temp = await load_chapter(firstChapters, comicData, buildId, cId);
|
||||
let chapters = temp[0];
|
||||
let updateTime = temp[1];
|
||||
|
||||
return {
|
||||
title: title,
|
||||
@@ -736,17 +675,17 @@ class Comick extends ComicSource {
|
||||
"状态": [Comick.comic_status[status]],
|
||||
},
|
||||
chapters: chapters,
|
||||
recommend: recommends || []
|
||||
//recommend: recommends || []
|
||||
}
|
||||
},
|
||||
loadEp: async (comicId, epId) => {
|
||||
const [cId, cTitle] = comicId.split("//");
|
||||
let [cId, cTitle] = comicId.split("//");
|
||||
if (!cId) {
|
||||
throw "ID error: ";
|
||||
}
|
||||
|
||||
const images = [];
|
||||
const [hid, type, chapter, lang] = epId.split("//");
|
||||
let images = [];
|
||||
let [hid, type, chapter, lang] = epId.split("//");
|
||||
|
||||
// 检查分割结果是否有效
|
||||
if (!hid || !type || !chapter || !lang) {
|
||||
@@ -757,21 +696,21 @@ class Comick extends ComicSource {
|
||||
let url = " ";
|
||||
if(type=="no"){
|
||||
// 如果是无标卷, 只看第一个
|
||||
url = `${this.baseUrl}/comic/${cId}/${hid}`;
|
||||
url = `https://comick.art/comic/${cId}/${hid}`;
|
||||
}else{
|
||||
url = `${this.baseUrl}/comic/${cId}/${hid}-${type}-${chapter}-${lang}`;
|
||||
url = `https://comick.art/comic/${cId}/${hid}-${type}-${chapter}-${lang}`;
|
||||
}
|
||||
|
||||
let maxAttempts = 100;
|
||||
|
||||
while (maxAttempts > 0) {
|
||||
const res = await Network.get(url);
|
||||
let res = await Network.get(url);
|
||||
if (res.status !== 200) break;
|
||||
|
||||
let document = new HtmlDocument(res.body)
|
||||
|
||||
let jsonData = JSON.parse(document.getElementById('__NEXT_DATA__').text); //json解析方式
|
||||
let imagesData = jsonData.props?.pageProps?.chapter?.md_images;
|
||||
let jsonData = JSON.parse(document.getElementById('sv-data').text); //json解析方式
|
||||
let imagesData = jsonData.chapter?.images;
|
||||
|
||||
// 检查图片数据是否存在
|
||||
if (!imagesData || !Array.isArray(imagesData)) {
|
||||
@@ -780,17 +719,15 @@ class Comick extends ComicSource {
|
||||
|
||||
// 解析当前页图片
|
||||
imagesData.forEach(image => {
|
||||
if (image?.b2key) {
|
||||
// 处理图片链接
|
||||
let imageUrl = `https://meo.comick.pictures/${image.b2key}`;
|
||||
let imageUrl = `${image.url}`;
|
||||
images.push(imageUrl);
|
||||
}
|
||||
});
|
||||
|
||||
// 查找下一页链接
|
||||
const nextLink = document.querySelector("a#next-chapter");
|
||||
let nextLink = document.querySelector("a#next-chapter");
|
||||
if (nextLink?.text?.match(/下一页|下一頁/)) {
|
||||
const nextUrl = nextLink.attributes?.['href'];
|
||||
let nextUrl = nextLink.attributes?.['href'];
|
||||
if (nextUrl) {
|
||||
url = nextUrl;
|
||||
} else {
|
||||
@@ -803,6 +740,27 @@ class Comick extends ComicSource {
|
||||
}
|
||||
return {images};
|
||||
},
|
||||
|
||||
onImageLoad: (url, comicId, epId) => {
|
||||
let headers = Comick.getRandomHeaders();
|
||||
return {
|
||||
url,
|
||||
method: "GET",
|
||||
headers,
|
||||
onLoadFailed: () => ({ url })
|
||||
}
|
||||
},
|
||||
|
||||
onThumbnailLoad: (url) => {
|
||||
let headers = Comick.getRandomHeaders();
|
||||
return {
|
||||
url : url,
|
||||
method: "GET",
|
||||
headers : headers,
|
||||
onLoadFailed: () => ({ url })
|
||||
}
|
||||
},
|
||||
|
||||
onClickTag: (namespace, tag) => {
|
||||
if (namespace === "标签") {
|
||||
let r_tag = Comick.reversed_category_param_dict[tag] || tag;
|
||||
|
||||
@@ -4,9 +4,9 @@ class CopyManga extends ComicSource {
|
||||
|
||||
key = "copy_manga"
|
||||
|
||||
version = "1.3.8"
|
||||
version = "1.4.0"
|
||||
|
||||
minAppVersion = "1.2.1"
|
||||
minAppVersion = "1.6.0"
|
||||
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/copy_manga.js"
|
||||
|
||||
@@ -864,6 +864,62 @@ class CopyManga extends ComicSource {
|
||||
return "ok"
|
||||
}
|
||||
},
|
||||
loadChapterComments: async (comicId, epId, page, replyTo) => {
|
||||
let url = `${this.apiUrl}/api/v3/roasts?chapter_id=${epId}&limit=20&offset=${(page - 1) * 20}`;
|
||||
let res = await Network.get(
|
||||
url,
|
||||
this.headers,
|
||||
);
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
|
||||
let data = JSON.parse(res.body);
|
||||
|
||||
let total = data.results.total;
|
||||
|
||||
return {
|
||||
comments: data.results.list.map(e => {
|
||||
return {
|
||||
userName: e.user_name,
|
||||
avatar: e.user_avatar,
|
||||
content: e.comment,
|
||||
time: e.create_at,
|
||||
replyCount: null,
|
||||
id: null,
|
||||
}
|
||||
}),
|
||||
maxPage: (total - (total % 20)) / 20 + 1,
|
||||
}
|
||||
},
|
||||
sendChapterComment: async (comicId, epId, content, replyTo) => {
|
||||
let token = this.loadData("token");
|
||||
if (!token) {
|
||||
throw "未登录"
|
||||
}
|
||||
let res = await Network.post(
|
||||
`${this.apiUrl}/api/v3/member/roast`,
|
||||
{
|
||||
...this.headers,
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
},
|
||||
`chapter_id=${epId}&roast=${encodeURIComponent(content)}`,
|
||||
);
|
||||
|
||||
if (res.status === 401) {
|
||||
throw `Login expired`;
|
||||
}
|
||||
|
||||
if (res.status !== 200) {
|
||||
if(res.status === 210) {
|
||||
throw `210:评论过于频繁或评论内容过短过长`;
|
||||
}
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
} else {
|
||||
return "ok"
|
||||
}
|
||||
},
|
||||
onClickTag: (namespace, tag) => {
|
||||
if (namespace === "标签") {
|
||||
return {
|
||||
|
||||
314
ehentai.js
314
ehentai.js
@@ -7,9 +7,9 @@ class Ehentai extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "ehentai"
|
||||
|
||||
version = "1.1.4"
|
||||
version = "1.1.8"
|
||||
|
||||
minAppVersion = "1.0.0"
|
||||
minAppVersion = "1.5.3"
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/ehentai.js"
|
||||
@@ -40,6 +40,35 @@ class Ehentai extends ComicSource {
|
||||
}
|
||||
}
|
||||
|
||||
async checkEHEvent() {
|
||||
if (!this.isLogged) {
|
||||
return;
|
||||
}
|
||||
if (!this.loadSetting("ehevent")) {
|
||||
return;
|
||||
}
|
||||
const lastEvent = this.loadData("lastEventTime");
|
||||
const newTime = new Date().toISOString().split("T")[0];
|
||||
if (lastEvent == newTime) {
|
||||
return;
|
||||
}
|
||||
const res = await Network.get("https://e-hentai.org/news.php", {});
|
||||
if (res.status !== 200) {
|
||||
return;
|
||||
}
|
||||
this.saveData("lastEventTime", newTime);
|
||||
const document = new HtmlDocument(res.body);
|
||||
const eventPane = document.getElementById("eventpane");
|
||||
if (eventPane == null) {
|
||||
return;
|
||||
}
|
||||
const dawnInfo = eventPane.querySelector("div > p:nth-child(2)");
|
||||
if (dawnInfo == null) {
|
||||
return;
|
||||
}
|
||||
UI.showMessage(dawnInfo.text);
|
||||
}
|
||||
|
||||
// [Optional] account related
|
||||
account = {
|
||||
|
||||
@@ -193,6 +222,9 @@ class Ehentai extends ComicSource {
|
||||
* @returns {Promise<{comics: Comic[], next: string?}>}
|
||||
*/
|
||||
async getGalleries(url, isLeaderBoard) {
|
||||
try {
|
||||
this.checkEHEvent();
|
||||
} catch (_) {}
|
||||
let t = isLeaderBoard ? 1 : 0;
|
||||
let res
|
||||
try {
|
||||
@@ -438,6 +470,9 @@ class Ehentai extends ComicSource {
|
||||
let stars = options[1];
|
||||
let language = options[2];
|
||||
let fcats = 1023
|
||||
if (!Array.isArray(category)) {
|
||||
category = [category];
|
||||
}
|
||||
for(let c of category) {
|
||||
fcats -= 1 << Number(c)
|
||||
}
|
||||
@@ -519,6 +554,7 @@ class Ehentai extends ComicSource {
|
||||
favorites = {
|
||||
// whether support multi folders
|
||||
multiFolder: true,
|
||||
singleFolderForSingleComic: true,
|
||||
/**
|
||||
* add or delete favorite.
|
||||
* throw `Login expired` to indicate login expired, App will automatically re-login and re-add/delete favorite
|
||||
@@ -566,6 +602,9 @@ class Ehentai extends ComicSource {
|
||||
* @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) => {
|
||||
try {
|
||||
this.checkEHEvent();
|
||||
} catch (_) {}
|
||||
let res = await Network.get(`${this.baseUrl}/favorites.php`, {});
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`
|
||||
@@ -615,6 +654,9 @@ class Ehentai extends ComicSource {
|
||||
* @returns {Promise<ComicDetails>}
|
||||
*/
|
||||
loadInfo: async (id) => {
|
||||
try {
|
||||
this.checkEHEvent();
|
||||
} catch (_) {}
|
||||
let res = await Network.get(id, {
|
||||
'cookie': 'nw=1'
|
||||
});
|
||||
@@ -626,6 +668,31 @@ class Ehentai extends ComicSource {
|
||||
}
|
||||
let document = new HtmlDocument(res.body);
|
||||
|
||||
if (this.isLogged && this.loadSetting("hvevent")) {
|
||||
const eventPane = document.getElementById("eventpane");
|
||||
if (eventPane != null) {
|
||||
const hvUrl = eventPane.querySelector('div > a')?.attributes['href'];
|
||||
if (hvUrl != null) {
|
||||
UI.showDialog(
|
||||
"HentaiVerse",
|
||||
this.translate("hentaiverse"),
|
||||
[
|
||||
{
|
||||
text: this.translate("cancel"),
|
||||
callback: () => {}
|
||||
},
|
||||
{
|
||||
text: this.translate("fight"),
|
||||
callback: () => {
|
||||
UI.launchUrl(hvUrl);
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tags = new Map();
|
||||
for(let tr of document.querySelectorAll("div#taglist > table > tbody > tr")) {
|
||||
tags.set(
|
||||
@@ -671,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"));
|
||||
@@ -696,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,
|
||||
@@ -865,7 +936,6 @@ class Ehentai extends ComicSource {
|
||||
*/
|
||||
onImageLoad: async (image, comicId, epId, nl) => {
|
||||
let first = await this.comic.loadThumbnails(comicId)
|
||||
console.log(first)
|
||||
let key = await this.comic.getKey(first.urls[0])
|
||||
let page = Number(image)
|
||||
|
||||
@@ -1084,32 +1154,155 @@ class Ehentai extends ComicSource {
|
||||
let document = new HtmlDocument(res.body)
|
||||
let body = document.querySelector("div#db")
|
||||
let index = this.baseUrl.includes("exhentai") ? 1 : 3
|
||||
let origin = body.children[index].children[0];
|
||||
let originCost = origin.querySelector("div > strong").text;
|
||||
let originSize = origin.querySelector("p > strong").text;
|
||||
let resample = body.children[index].children[1];
|
||||
let resampleCost = resample.querySelector("div > strong").text;
|
||||
let resampleSize = resample.querySelector("p > strong").text;
|
||||
return [
|
||||
{
|
||||
|
||||
let archives = []
|
||||
|
||||
// Parse H@H Download options from the table
|
||||
let hathTable = document.querySelector("table");
|
||||
if (hathTable) {
|
||||
let hathCells = hathTable.querySelectorAll("td");
|
||||
for (let cell of hathCells) {
|
||||
let link = cell.querySelector("a");
|
||||
if (link) {
|
||||
// Extract resolution from onclick attribute
|
||||
let onclick = link.attributes["onclick"];
|
||||
let resolutionMatch = onclick.match(/do_hathdl\('([^']+)'\)/);
|
||||
if (resolutionMatch) {
|
||||
let resolution = resolutionMatch[1];
|
||||
let linkText = link.text;
|
||||
let paragraphs = cell.querySelectorAll("p");
|
||||
let size = paragraphs.length > 1 ? paragraphs[1].text : "Unknown";
|
||||
let cost = paragraphs.length > 2 ? paragraphs[2].text : "Unknown";
|
||||
|
||||
archives.push({
|
||||
id: `h@h_${resolution}`,
|
||||
title: `H@H ${linkText}`,
|
||||
description: `Size: ${size}, Cost: ${cost}`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Skip disabled options (N/A) - don't add them to the list
|
||||
// This prevents users from accidentally selecting unavailable options
|
||||
let paragraphs = cell.querySelectorAll("p");
|
||||
if (paragraphs.length > 0) {
|
||||
let size = paragraphs.length > 1 ? paragraphs[1].text : "N/A";
|
||||
let cost = paragraphs.length > 2 ? paragraphs[2].text : "N/A";
|
||||
|
||||
// Only add if both size and cost are available (not "N/A")
|
||||
if (size !== "N/A" && cost !== "N/A") {
|
||||
let resolutionText = paragraphs[0].text;
|
||||
archives.push({
|
||||
id: `h@h_${resolutionText.toLowerCase().replace('x', '')}`,
|
||||
title: `H@H ${resolutionText}`,
|
||||
description: `Cost: ${cost}, Size: ${size}`,
|
||||
});
|
||||
}
|
||||
// If size or cost is "N/A", we simply skip this option
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Original Download
|
||||
let origin = body.children[index]?.children[0];
|
||||
if (origin) {
|
||||
let originCost = origin.querySelector("div > strong")?.text || "Unknown";
|
||||
let originSize = origin.querySelector("p > strong")?.text || "Unknown";
|
||||
archives.push({
|
||||
id: '0',
|
||||
title: 'Original',
|
||||
description: `Cost: ${originCost}, Size: ${originSize}`,
|
||||
},
|
||||
{
|
||||
});
|
||||
}
|
||||
|
||||
// Resample Download
|
||||
let resample = body.children[index]?.children[1];
|
||||
if (resample) {
|
||||
let resampleCost = resample.querySelector("div > strong")?.text || "Unknown";
|
||||
let resampleSize = resample.querySelector("p > strong")?.text || "Unknown";
|
||||
archives.push({
|
||||
id: '1',
|
||||
title: 'Resample',
|
||||
description: `Cost: ${resampleCost}, Size: ${resampleSize}`,
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
document.dispose()
|
||||
return archives
|
||||
},
|
||||
getDownloadUrl: async (cid, aid) => {
|
||||
let data = aid === '0'
|
||||
? "dltype=org&dlcheck=Download+Original+Archive"
|
||||
: "dltype=res&dlcheck=Download+Resample+Archive"
|
||||
let urlParseResult = this.parseUrl(cid)
|
||||
let gid = urlParseResult.id
|
||||
let token = urlParseResult.token
|
||||
|
||||
// Handle H@H Download options
|
||||
if (aid.startsWith('h@h_')) {
|
||||
let resolution = aid.substring(4); // Remove 'h@h_' prefix
|
||||
|
||||
// For H@H downloads, send the command directly to archiver.php
|
||||
let hathRes = await Network.post(`${this.baseUrl}/archiver.php?gid=${gid}&token=${token}`, {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}, `hathdl_xres=${resolution}`)
|
||||
|
||||
if (hathRes.status !== 200) {
|
||||
throw `Failed to send H@H download command: ${hathRes.status}`
|
||||
}
|
||||
|
||||
// Parse response for any error messages
|
||||
let hathDocument = new HtmlDocument(hathRes.body)
|
||||
let errorElement = hathDocument.querySelector("p.br")
|
||||
|
||||
if (errorElement) {
|
||||
let errorMessage = errorElement.text
|
||||
hathDocument.dispose()
|
||||
|
||||
if (errorMessage.includes("H@H client")) {
|
||||
throw "You need an H@H client associated with your account to use this feature"
|
||||
} else if (errorMessage.includes("offline")) {
|
||||
throw "Your H@H client appears to be offline. Please start it and try again"
|
||||
} else if (errorMessage.includes("resolution")) {
|
||||
throw "This gallery cannot be downloaded at the selected resolution"
|
||||
} else {
|
||||
throw errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
// Check for success message or assume success if no error
|
||||
let successMessage = hathDocument.querySelector("p")?.text
|
||||
hathDocument.dispose()
|
||||
|
||||
let resolutionText = resolution === 'org' ? 'Original' :
|
||||
resolution === '800' ? '800x' :
|
||||
resolution === '1280' ? '1280x' :
|
||||
resolution === '1920' ? '1920x' :
|
||||
resolution === '2560' ? '2560x' : resolution;
|
||||
|
||||
// For H@H downloads, return a special value to indicate remote download
|
||||
// This should close the window without creating a local download task
|
||||
// let message = successMessage && successMessage.includes("successfully")
|
||||
// ? `H@H download command sent successfully (${resolutionText}). Check your H@H client.`
|
||||
// : `H@H download command sent (${resolutionText}). Check your H@H client.`;
|
||||
|
||||
// // Show success message to user
|
||||
// UI.showMessage(message);
|
||||
|
||||
// Return empty string to avoid type error and prevent download task creation
|
||||
return "";
|
||||
}
|
||||
|
||||
// Handle regular downloads (Original and Resample)
|
||||
let data;
|
||||
switch(aid) {
|
||||
case '0':
|
||||
data = "dltype=org&dlcheck=Download+Original+Archive";
|
||||
break;
|
||||
case '1':
|
||||
data = "dltype=res&dlcheck=Download+Resample+Archive";
|
||||
break;
|
||||
default:
|
||||
throw "Invalid archive type";
|
||||
}
|
||||
|
||||
let res = await Network.post(`${this.baseUrl}/archiver.php?gid=${gid}&token=${token}`, {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}, data)
|
||||
@@ -1151,6 +1344,16 @@ class Ehentai extends ComicSource {
|
||||
* @returns {{action: string, keyword: string, param: string?}}
|
||||
*/
|
||||
onClickTag: (namespace, tag) => {
|
||||
if (namespace == "Category") {
|
||||
const categories = ["misc", "doujinshi", "manga", "artist cg", "game cg", "image set", "cosplay", "asian porn", "non-h", "western"];
|
||||
return {
|
||||
page: "search",
|
||||
attributes: {
|
||||
'keyword': "",
|
||||
'options': [categories.indexOf(tag.toLowerCase()).toString(), "", ""]
|
||||
}
|
||||
};
|
||||
}
|
||||
if(tag.includes(' ')) {
|
||||
tag = `"${tag}"`
|
||||
}
|
||||
@@ -1219,12 +1422,27 @@ class Ehentai extends ComicSource {
|
||||
],
|
||||
default: 'e-hentai.org',
|
||||
},
|
||||
ehevent: {
|
||||
title: "ehevent",
|
||||
type: "switch",
|
||||
default: false
|
||||
},
|
||||
hvevent: {
|
||||
title: "hvevent",
|
||||
type: "switch",
|
||||
default: false
|
||||
},
|
||||
}
|
||||
|
||||
// [Optional] translations for the strings in this config
|
||||
translation = {
|
||||
'zh_CN': {
|
||||
"domain": "域名",
|
||||
"ehevent": "触发黎明事件",
|
||||
"hvevent": "提示HV遭遇战",
|
||||
"hentaiverse": "你遇到了怪物!",
|
||||
"fight":"战斗",
|
||||
"cancel":"取消",
|
||||
"language": "语言",
|
||||
"artist": "画师",
|
||||
"male": "男性",
|
||||
@@ -1236,6 +1454,7 @@ class Ehentai extends ComicSource {
|
||||
"group": "团队",
|
||||
"cosplayer": "Coser",
|
||||
"reclass": "重新分类",
|
||||
"uploader": "上传者",
|
||||
"Languages": "语言",
|
||||
"Artists": "画师",
|
||||
"Characters": "角色",
|
||||
@@ -1246,9 +1465,21 @@ class Ehentai extends ComicSource {
|
||||
"Category": "分类",
|
||||
"Min Stars": "最少星星",
|
||||
"Language": "语言",
|
||||
"H@H Original": "H@H 原版",
|
||||
"H@H 800x": "H@H 800x",
|
||||
"H@H 1280x": "H@H 1280x",
|
||||
"H@H 1920x": "H@H 1920x",
|
||||
"H@H 2560x": "H@H 2560x",
|
||||
"Original": "原版",
|
||||
"Resample": "重采样",
|
||||
},
|
||||
'zh_TW': {
|
||||
'domain': '域名',
|
||||
"ehevent": "觸發黎明事件",
|
||||
"hvevent": "提示HV遭遇戰",
|
||||
"hentaiverse": "你遇到了怪物!",
|
||||
"fight":"戰鬥",
|
||||
"cancel":"取消",
|
||||
"language": "語言",
|
||||
"artist": "畫師",
|
||||
"male": "男性",
|
||||
@@ -1260,6 +1491,7 @@ class Ehentai extends ComicSource {
|
||||
"group": "團隊",
|
||||
"cosplayer": "Coser",
|
||||
"reclass": "重新分類",
|
||||
"uploader": "上傳者",
|
||||
"Languages": "語言",
|
||||
"Artists": "畫師",
|
||||
"Characters": "角色",
|
||||
@@ -1270,6 +1502,50 @@ class Ehentai extends ComicSource {
|
||||
"Category": "分類",
|
||||
"Min Stars": "最少星星",
|
||||
"Language": "語言",
|
||||
"H@H Original": "H@H 原版",
|
||||
"H@H 800x": "H@H 800x",
|
||||
"H@H 1280x": "H@H 1280x",
|
||||
"H@H 1920x": "H@H 1920x",
|
||||
"H@H 2560x": "H@H 2560x",
|
||||
"Original": "原版",
|
||||
"Resample": "重採樣",
|
||||
},
|
||||
'en_US': {
|
||||
"domain": "Domain",
|
||||
"ehevent": "Trigger Dawn Event",
|
||||
"hvevent": "HV Encounter Alert",
|
||||
"hentaiverse": "You have encountered a monster!",
|
||||
"fight": "Fight",
|
||||
"cancel": "Cancel",
|
||||
"language": "Language",
|
||||
"artist": "Artist",
|
||||
"male": "Male",
|
||||
"female": "Female",
|
||||
"mixed": "Mixed",
|
||||
"other": "Other",
|
||||
"parody": "Parody",
|
||||
"character": "Character",
|
||||
"group": "Group",
|
||||
"cosplayer": "Cosplayer",
|
||||
"reclass": "Reclass",
|
||||
"uploader": "Uploader",
|
||||
"Languages": "Languages",
|
||||
"Artists": "Artists",
|
||||
"Characters": "Characters",
|
||||
"Groups": "Groups",
|
||||
"Tags": "Tags",
|
||||
"Parodies": "Parodies",
|
||||
"Categories": "Categories",
|
||||
"Category": "Category",
|
||||
"Min Stars": "Min Stars",
|
||||
"Language": "Language",
|
||||
"H@H Original": "H@H Original",
|
||||
"H@H 800x": "H@H 800x",
|
||||
"H@H 1280x": "H@H 1280x",
|
||||
"H@H 1920x": "H@H 1920x",
|
||||
"H@H 2560x": "H@H 2560x",
|
||||
"Original": "Original",
|
||||
"Resample": "Resample"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
332
goda.js
Normal file
332
goda.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
class Goda extends ComicSource {
|
||||
// Note: The fields which are marked as [Optional] should be removed if not used
|
||||
|
||||
// name of the source
|
||||
name = "GoDa漫画"
|
||||
|
||||
// unique id of the source
|
||||
key = "goda"
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
minAppVersion = "1.4.0"
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/goda.js"
|
||||
|
||||
settings = {
|
||||
domains: {
|
||||
title: "域名",
|
||||
type: "input",
|
||||
default: "godamh.com"
|
||||
},
|
||||
api: {
|
||||
title: "API域名",
|
||||
type: "input",
|
||||
default: "api-get-v3.mgsearcher.com"
|
||||
},
|
||||
image: {
|
||||
title: "图片域名",
|
||||
type: "input",
|
||||
default: "t40-1-4.g-mh.online"
|
||||
}
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return `https://${this.loadSetting("domains")}`;
|
||||
}
|
||||
|
||||
get apiUrl() {
|
||||
return `https://${this.loadSetting("api")}/api`;
|
||||
}
|
||||
|
||||
get imageUrl() {
|
||||
return `https://${this.loadSetting("image")}`;
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0",
|
||||
"Referer": this.baseUrl
|
||||
};
|
||||
}
|
||||
|
||||
parseComics(doc) {
|
||||
console.warn(doc)
|
||||
const result = [];
|
||||
for (let item of doc.querySelectorAll(".pb-2")) {
|
||||
result.push(new Comic({
|
||||
id: item.querySelector("a").attributes["href"],
|
||||
title: item.querySelector("h3").text,
|
||||
cover: item.querySelector("img").attributes["src"]
|
||||
}))
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// explore page list
|
||||
explore = [
|
||||
{
|
||||
// title of the page.
|
||||
// title is used to identify the page, it should be unique
|
||||
title: this.name,
|
||||
|
||||
/// multiPartPage or multiPageComicList or mixed
|
||||
type: "multiPartPage",
|
||||
|
||||
load: async () => {
|
||||
const res = await Network.get(this.baseUrl, this.headers);
|
||||
const document = new HtmlDocument(res.body);
|
||||
const result = [{ title: "近期更新", comics: [], viewMore: null }];
|
||||
for (let item of document.querySelector(".pb-unit-md").querySelectorAll(".slicarda")) {
|
||||
result[0].comics.push(new Comic({
|
||||
id: item.attributes["href"],
|
||||
title: item.querySelector("h3").text,
|
||||
cover: item.querySelector("img").attributes["src"]
|
||||
}))
|
||||
}
|
||||
const cardlists = document.querySelectorAll(".cardlist");
|
||||
const hometitles = document.querySelectorAll(".hometitle");
|
||||
for (let i = 0; i < hometitles.length; i++) {
|
||||
result.push({
|
||||
title: hometitles[i].querySelector("h2").text,
|
||||
comics: this.parseComics(cardlists[i]),
|
||||
viewMore: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: hometitles[i].querySelector("h2").text,
|
||||
param: hometitles[i].attributes["href"]
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// categories
|
||||
category = {
|
||||
/// title of the category page, used to identify the page, it should be unique
|
||||
title: this.name,
|
||||
parts: [
|
||||
{
|
||||
name: "类型",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"全部",
|
||||
"韩漫",
|
||||
"热门漫画",
|
||||
"国漫",
|
||||
"其他",
|
||||
"日漫",
|
||||
"欧美"
|
||||
],
|
||||
itemType: "category",
|
||||
categoryParams: [
|
||||
"/manga",
|
||||
"/manga-genre/kr",
|
||||
"/manga-genre/hots",
|
||||
"/manga-genre/cn",
|
||||
"/manga-genre/qita",
|
||||
"/manga-genre/jp",
|
||||
"/manga-genre/ou-mei"
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "标签",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"复仇",
|
||||
"古风",
|
||||
"奇幻",
|
||||
"逆袭",
|
||||
"异能",
|
||||
"宅向",
|
||||
"穿越",
|
||||
"热血",
|
||||
"纯爱",
|
||||
"系统",
|
||||
"重生",
|
||||
"冒险",
|
||||
"灵异",
|
||||
"大女主",
|
||||
"剧情",
|
||||
"恋爱",
|
||||
"玄幻",
|
||||
"女神",
|
||||
"科幻",
|
||||
"魔幻",
|
||||
"推理",
|
||||
"猎奇",
|
||||
"治愈",
|
||||
"都市",
|
||||
"异形",
|
||||
"青春",
|
||||
"末日",
|
||||
"悬疑",
|
||||
"修仙",
|
||||
"战斗"
|
||||
],
|
||||
itemType: "category",
|
||||
categoryParams: [
|
||||
"/manga-tag/fuchou",
|
||||
"/manga-tag/gufeng",
|
||||
"/manga-tag/qihuan",
|
||||
"/manga-tag/nixi",
|
||||
"/manga-tag/yineng",
|
||||
"/manga-tag/zhaixiang",
|
||||
"/manga-tag/chuanyue",
|
||||
"/manga-tag/rexue",
|
||||
"/manga-tag/chunai",
|
||||
"/manga-tag/xitong",
|
||||
"/manga-tag/zhongsheng",
|
||||
"/manga-tag/maoxian",
|
||||
"/manga-tag/lingyi",
|
||||
"/manga-tag/danvzhu",
|
||||
"/manga-tag/juqing",
|
||||
"/manga-tag/lianai",
|
||||
"/manga-tag/xuanhuan",
|
||||
"/manga-tag/nvshen",
|
||||
"/manga-tag/kehuan",
|
||||
"/manga-tag/mohuan",
|
||||
"/manga-tag/tuili",
|
||||
"/manga-tag/lieqi",
|
||||
"/manga-tag/zhiyu",
|
||||
"/manga-tag/doushi",
|
||||
"/manga-tag/yixing",
|
||||
"/manga-tag/qingchun",
|
||||
"/manga-tag/mori",
|
||||
"/manga-tag/xuanyi",
|
||||
"/manga-tag/xiuxian",
|
||||
"/manga-tag/zhandou"
|
||||
],
|
||||
}
|
||||
],
|
||||
// enable ranking page
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
/// category comic loading related
|
||||
categoryComics = {
|
||||
load: async (category, params, options, page) => {
|
||||
const res = await Network.get(`${this.baseUrl}${params}/page/${page}`, this.headers);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const document = new HtmlDocument(res.body);
|
||||
let maxPage = null;
|
||||
try {
|
||||
maxPage = parseInt(document.querySelectorAll("button.text-small").pop().text.replaceAll("\n", "").replaceAll(" ", ""));
|
||||
} catch(_) {
|
||||
maxPage = 1;
|
||||
}
|
||||
return {
|
||||
comics: this.parseComics(document),
|
||||
maxPage: maxPage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
load: async (keyword, options, page) => {
|
||||
const res = await Network.get(`${this.baseUrl}/s/${keyword}?page=${page}`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const document = new HtmlDocument(res.body);
|
||||
let maxPage = null;
|
||||
try {
|
||||
maxPage = parseInt(document.querySelectorAll("button.text-small").pop().text.replaceAll("\n", "").replaceAll(" ", ""));
|
||||
} catch(_) {
|
||||
maxPage = 1;
|
||||
}
|
||||
return {
|
||||
comics: this.parseComics(document),
|
||||
maxPage: maxPage
|
||||
};
|
||||
},
|
||||
// enable tags suggestions
|
||||
enableTagsSuggestions: false,
|
||||
}
|
||||
|
||||
/// single comic related
|
||||
comic = {
|
||||
onThumbnailLoad: (url) => {
|
||||
return {
|
||||
headers: this.headers
|
||||
}
|
||||
},
|
||||
loadInfo: async (id) => {
|
||||
const res = await Network.get(this.baseUrl + id);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const document = new HtmlDocument(res.body);
|
||||
const title = document.querySelector(".text-xl").text.trim().split(" ")[0]
|
||||
const cover = document.querySelector(".object-cover").attributes["src"];
|
||||
const description = document.querySelector("p.text-medium").text;
|
||||
const infos = document.querySelectorAll("div.py-1");
|
||||
const tags = { "作者": [], "类型": [], "标签": [] };
|
||||
for (let author of infos[0].querySelectorAll("a > span")) {
|
||||
let author_name = author.text.trim();
|
||||
if (author_name.endsWith(",")) {
|
||||
author_name = author_name.slice(0, -1).trim();
|
||||
}
|
||||
tags["作者"].push(author_name);
|
||||
}
|
||||
for (let category of infos[1].querySelectorAll("a > span")) {
|
||||
let category_name = category.text.trim();
|
||||
if (category_name.endsWith(",")) {
|
||||
category_name = category_name.slice(0, -1).trim();
|
||||
}
|
||||
tags["类型"].push(category_name);
|
||||
}
|
||||
for (let tag of infos[2].querySelectorAll("a")) {
|
||||
tags["标签"].push(tag.text.replace("\n", "").replaceAll(" ", "").replace("#", ""));
|
||||
}
|
||||
const mangaId = document.querySelector("#mangachapters").attributes["data-mid"];
|
||||
const jsonRes = await Network.get(`${this.apiUrl}/manga/get?mid=${mangaId}&mode=all&t=${Date.now()}`, this.headers);
|
||||
const jsonData = JSON.parse(jsonRes.body);
|
||||
const chapters = {};
|
||||
for (let ch of jsonData["data"]["chapters"]) {
|
||||
chapters[`${mangaId}@${ch["id"]}`] = ch["attributes"]["title"];
|
||||
}
|
||||
const recommend = [];
|
||||
for (let item of document.querySelectorAll("div.cardlist > div.pb-2")) {
|
||||
recommend.push(new Comic({
|
||||
id: item.querySelector("a").attributes["href"],
|
||||
title: item.querySelector("h3").text,
|
||||
cover: item.querySelector("img").attributes["src"]
|
||||
}));
|
||||
}
|
||||
return new ComicDetails({
|
||||
title: title,
|
||||
cover: cover,
|
||||
description: description,
|
||||
tags: tags,
|
||||
chapters: chapters,
|
||||
recommend: recommend,
|
||||
});
|
||||
},
|
||||
|
||||
loadEp: async (comicId, epId) => {
|
||||
const ids = epId.split("@");
|
||||
const res = await Network.get(`${this.apiUrl}/chapter/getinfo?m=${ids[0]}&c=${ids[1]}`, this.headers);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const jsonData = JSON.parse(res.body);
|
||||
const images = [];
|
||||
for (let i of jsonData["data"]["info"]["images"]["images"]) {
|
||||
images.push(this.imageUrl + i["url"]);
|
||||
}
|
||||
return { images };
|
||||
},
|
||||
|
||||
// enable tags translate
|
||||
enableTagsTranslate: false,
|
||||
}
|
||||
}
|
||||
84
index.json
84
index.json
@@ -3,19 +3,19 @@
|
||||
"name": "拷贝漫画",
|
||||
"fileName": "copy_manga.js",
|
||||
"key": "copy_manga",
|
||||
"version": "1.3.8"
|
||||
"version": "1.4.0"
|
||||
},
|
||||
{
|
||||
"name": "Komiic",
|
||||
"fileName": "komiic.js",
|
||||
"key": "Komiic",
|
||||
"version": "1.0.2"
|
||||
"version": "1.0.3"
|
||||
},
|
||||
{
|
||||
"name": "包子漫画",
|
||||
"fileName": "baozi.js",
|
||||
"key": "baozi",
|
||||
"version": "1.1.0"
|
||||
"version": "1.1.1"
|
||||
},
|
||||
{
|
||||
"name": "Picacg",
|
||||
@@ -33,27 +33,27 @@
|
||||
"name": "紳士漫畫",
|
||||
"fileName": "wnacg.js",
|
||||
"key": "wnacg",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"description": "紳士漫畫漫畫源, 不能使用時請嘗試更換URL"
|
||||
},
|
||||
{
|
||||
"name": "ehentai",
|
||||
"fileName": "ehentai.js",
|
||||
"key": "ehentai",
|
||||
"version": "1.1.4"
|
||||
"version": "1.1.8"
|
||||
},
|
||||
{
|
||||
"name": "禁漫天堂",
|
||||
"fileName": "jm.js",
|
||||
"key": "jm",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"description": "禁漫天堂漫畫源, 不能使用時請嘗試切換分流"
|
||||
},
|
||||
{
|
||||
"name": "MangaDex",
|
||||
"fileName": "manga_dex.js",
|
||||
"key": "manga_dex",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Account feature is not supported yet."
|
||||
},
|
||||
{
|
||||
@@ -66,7 +66,7 @@
|
||||
"name": "少年ジャンプ+",
|
||||
"fileName": "shonen_jump_plus.js",
|
||||
"key": "shonen_jump_plus",
|
||||
"version": "1.1.0"
|
||||
"version": "1.1.1"
|
||||
},
|
||||
{
|
||||
"name": "hitomi.la",
|
||||
@@ -78,7 +78,7 @@
|
||||
"name": "comick",
|
||||
"fileName": "comick.js",
|
||||
"key": "comick",
|
||||
"version": "1.1.1"
|
||||
"version": "1.2.0"
|
||||
},
|
||||
{
|
||||
"name": "优酷漫画",
|
||||
@@ -90,30 +90,72 @@
|
||||
"name": "再漫画",
|
||||
"fileName": "zaimanhua.js",
|
||||
"key": "zaimanhua",
|
||||
"version": "1.0.1"
|
||||
"version": "1.0.2"
|
||||
},
|
||||
{
|
||||
"name": "漫画柜",
|
||||
"fileName": "manhuagui.js",
|
||||
"key": "ManHuaGui",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
{
|
||||
"name": "优酷漫画",
|
||||
"fileName": "ykmh.js",
|
||||
"key": "ykmh",
|
||||
"version": "1.0.0"
|
||||
"name": "漫画柜",
|
||||
"fileName": "manhuagui.js",
|
||||
"key": "ManHuaGui",
|
||||
"version": "1.2.1"
|
||||
},
|
||||
{
|
||||
"name": "漫蛙吧",
|
||||
"fileName": "manwaba.js",
|
||||
"key": "manwaba",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.2"
|
||||
},
|
||||
{
|
||||
"name": "Lanraragi",
|
||||
"fileName": "lanraragi.js",
|
||||
"key": "lanraragi",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
{
|
||||
"name": "Komga",
|
||||
"fileName": "komga.js",
|
||||
"key": "komga",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "カドコミ",
|
||||
"fileName": "comic_walker.js",
|
||||
"key": "comic_walker",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "漫画1234",
|
||||
"fileName": "mh1234.js",
|
||||
"key": "mh1234",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "CCC追漫台",
|
||||
"fileName": "ccc.js",
|
||||
"key": "ccc",
|
||||
"version": "1.0.1"
|
||||
},
|
||||
{
|
||||
"name": "GoDa漫画",
|
||||
"fileName": "goda.js",
|
||||
"key": "goda",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "18漫画",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
||||
16
jm.js
16
jm.js
@@ -7,11 +7,11 @@ class JM extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "jm"
|
||||
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
|
||||
minAppVersion = "1.5.0"
|
||||
|
||||
static jmVersion = "2.0.6"
|
||||
static jmVersion = "2.0.11"
|
||||
|
||||
static jmPkgName = "com.example.app"
|
||||
|
||||
@@ -127,11 +127,13 @@ class JM extends ComicSource {
|
||||
let message = ""
|
||||
let servers = []
|
||||
let domains = []
|
||||
let res = await fetch(
|
||||
url,
|
||||
{headers: this.baseHeaders}
|
||||
)
|
||||
if (res.status === 200) {
|
||||
let res = null;
|
||||
try {
|
||||
res = await fetch(url, { headers: this.baseHeaders });
|
||||
} catch (error) {
|
||||
res = null;
|
||||
}
|
||||
if (res && res.status === 200) {
|
||||
let data = this.convertData(await res.text(), domainSecret)
|
||||
let json = JSON.parse(data)
|
||||
if (json["Server"]) {
|
||||
|
||||
752
komga.js
Normal file
752
komga.js
Normal file
@@ -0,0 +1,752 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
class Komga extends ComicSource {
|
||||
name = "Komga"
|
||||
|
||||
key = "komga"
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
minAppVersion = "1.4.0"
|
||||
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/komga.js"
|
||||
|
||||
settings = {
|
||||
base_url: {
|
||||
title: "服务器地址",
|
||||
type: "input",
|
||||
default: "https://demo.komga.org",
|
||||
validator: "^(https?:\\/\\/).+$"
|
||||
},
|
||||
// default_username: {
|
||||
// title: "默认账号",
|
||||
// type: "input",
|
||||
// default: "demo@komga.org"
|
||||
// },
|
||||
// default_password: {
|
||||
// title: "默认密码",
|
||||
// type: "input",
|
||||
// default: "komga-demo"
|
||||
// }
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
let raw = this.loadSetting('base_url')
|
||||
if (typeof raw !== 'string' || !raw.trim()) {
|
||||
raw = this.settings.base_url.default
|
||||
}
|
||||
let value = raw.trim()
|
||||
if (!/^https?:\/\//i.test(value)) {
|
||||
value = `https://${value}`
|
||||
}
|
||||
return value.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
get authToken() {
|
||||
const stored = this.loadData('komga_auth')
|
||||
if (stored) {
|
||||
return stored
|
||||
}
|
||||
const username = this.loadSetting('default_username')
|
||||
const password = this.loadSetting('default_password')
|
||||
if (!username || !password) {
|
||||
return null
|
||||
}
|
||||
const encoded = Convert.encodeBase64(Convert.encodeUtf8(`${username}:${password}`))
|
||||
return typeof encoded === 'string' ? encoded : Convert.decodeUtf8(encoded)
|
||||
}
|
||||
|
||||
get headers() {
|
||||
const headers = { "Accept": "application/json" }
|
||||
const token = this.authToken
|
||||
if (token) headers["Authorization"] = `Basic ${token}`
|
||||
return headers
|
||||
}
|
||||
|
||||
get imageHeaders() {
|
||||
const token = this.authToken
|
||||
return token ? { "Authorization": `Basic ${token}` } : {}
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.refreshReferenceData(false)
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
|
||||
account = {
|
||||
login: async (account, pwd) => {
|
||||
if (!account || !pwd) {
|
||||
throw '账号或密码不能为空'
|
||||
}
|
||||
const basic = Convert.encodeBase64(Convert.encodeUtf8(`${account}:${pwd}`))
|
||||
const token = typeof basic === 'string' ? basic : Convert.decodeUtf8(basic)
|
||||
const res = await Network.get(
|
||||
this.buildUrl('/api/v2/users/me'),
|
||||
{
|
||||
"Accept": "application/json",
|
||||
"Authorization": `Basic ${token}`
|
||||
}
|
||||
)
|
||||
if (res.status === 401) {
|
||||
throw '账号或密码错误'
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
throw `登录失败: ${res.status}`
|
||||
}
|
||||
this.saveData('komga_auth', token)
|
||||
this.saveData('komga_account_email', account)
|
||||
await this.refreshReferenceData(true)
|
||||
return account
|
||||
},
|
||||
logout: () => {
|
||||
this.deleteData('komga_auth')
|
||||
this.deleteData('komga_account_email')
|
||||
this.deleteData('komga_libraries')
|
||||
this.deleteData('komga_tags')
|
||||
this.deleteData('komga_genres')
|
||||
this.deleteData('komga_languages')
|
||||
this.deleteData('komga_collections')
|
||||
this.deleteData('komga_meta_ts')
|
||||
},
|
||||
registerWebsite: null
|
||||
}
|
||||
|
||||
explore = [
|
||||
{
|
||||
title: "Komga",
|
||||
type: "singlePageWithMultiPart",
|
||||
load: async () => {
|
||||
await this.refreshReferenceData(false)
|
||||
const feeds = {}
|
||||
const latest = await this.fetchSeriesList('/api/v1/series/latest', { size: 12, page: 0 })
|
||||
if (latest.comics.length) feeds["最新上架"] = latest.comics
|
||||
const updated = await this.fetchSeriesList('/api/v1/series/updated', { size: 12, page: 0 })
|
||||
if (updated.comics.length) feeds["最近更新"] = updated.comics
|
||||
const libraries = this.loadData('komga_libraries')
|
||||
if (Array.isArray(libraries)) {
|
||||
for (const library of libraries.slice(0, 4)) {
|
||||
const list = await this.fetchSeriesList('/api/v1/series', {
|
||||
page: 0,
|
||||
size: 12,
|
||||
sort: ['metadata.lastModified,desc'],
|
||||
library_id: [library.id]
|
||||
})
|
||||
if (list.comics.length) feeds[`书库 ${library.name}`] = list.comics
|
||||
}
|
||||
}
|
||||
if (!Object.keys(feeds).length) {
|
||||
throw '未找到可展示的数据,请确认已登录且服务器可用'
|
||||
}
|
||||
return feeds
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
category = {
|
||||
title: "Komga",
|
||||
parts: [
|
||||
{
|
||||
name: "常用",
|
||||
type: "dynamic",
|
||||
loader: () => (
|
||||
[
|
||||
{
|
||||
label: "all",
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'all',
|
||||
param: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "书库",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const libraries = this.loadData('komga_libraries')
|
||||
if (!Array.isArray(libraries) || !libraries.length) {
|
||||
return []
|
||||
}
|
||||
return libraries.map((library) => ({
|
||||
label: library.name,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'library',
|
||||
param: library.id,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "合集",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const collections = this.loadData('komga_collections')
|
||||
if (!Array.isArray(collections) || !collections.length) {
|
||||
return []
|
||||
}
|
||||
return collections.map((collection) => ({
|
||||
label: collection.name,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'collection',
|
||||
param: collection.id,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "标签",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const tags = this.loadData('komga_tags')
|
||||
if (!Array.isArray(tags) || !tags.length) {
|
||||
return []
|
||||
}
|
||||
return tags.map((tag) => ({
|
||||
label: tag,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'tag',
|
||||
param: tag,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "语言",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const languages = this.loadData('komga_languages')
|
||||
if (!Array.isArray(languages) || !languages.length) {
|
||||
return []
|
||||
}
|
||||
return languages.map((lang) => ({
|
||||
label: lang,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'language',
|
||||
param: lang,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "题材",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const genres = this.loadData('komga_genres')
|
||||
if (!Array.isArray(genres) || !genres.length) {
|
||||
return []
|
||||
}
|
||||
return genres.map((genre) => ({
|
||||
label: genre,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'genre',
|
||||
param: genre,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
],
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
categoryComics = {
|
||||
load: async (category, param, options, page) => {
|
||||
await this.refreshReferenceData(false)
|
||||
const pageIndex = Math.max(0, (page || 1) - 1)
|
||||
const defaultSort = category === 'all' ? 'created,desc' : 'metadata.lastModified,desc'
|
||||
const sortValue = this.extractOption(options, 0, defaultSort)
|
||||
const query = {
|
||||
page: pageIndex,
|
||||
size: 30,
|
||||
sort: [sortValue]
|
||||
}
|
||||
if (category === 'all') {
|
||||
// const list = await this.fetchBookList('/api/v1/books', query)
|
||||
// return {
|
||||
// comics: list.comics,
|
||||
// maxPage: Math.max(1, list.totalPages)
|
||||
// }
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
if (category === 'library' && param) {
|
||||
query.library_id = [param]
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
if (category === 'collection' && param) {
|
||||
const list = await this.fetchSeriesList(`/api/v1/collections/${param}/series`, query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (category === 'tag' && param) {
|
||||
query.tag = [param]
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (category === 'language' && param){
|
||||
query.language = [param]
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
|
||||
// if (category === 'genre' && param) query.genre = [param]
|
||||
query.genre = [param]
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
},
|
||||
optionList: [
|
||||
{
|
||||
options: [
|
||||
'*created,desc-添加时间(新→旧)',
|
||||
'created,asc-添加时间(旧→新)',
|
||||
'metadata.lastModified,desc-更新时间(新→旧)',
|
||||
'metadata.lastModified,asc-更新时间(旧→新)',
|
||||
'metadata.titleSort,asc-标题(A-Z)',
|
||||
'metadata.titleSort,desc-标题(Z-A)'
|
||||
],
|
||||
notShowWhen: null,
|
||||
showWhen: null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
search = {
|
||||
load: async (keyword, options, page) => {
|
||||
const pageIndex = Math.max(0, (page || 1) - 1)
|
||||
const sortValue = this.extractOption(options, 0, 'metadata.lastModified,desc')
|
||||
const query = {
|
||||
page: pageIndex,
|
||||
size: 30,
|
||||
sort: [sortValue]
|
||||
}
|
||||
let term = (keyword || '').trim()
|
||||
const colonIdx = term.indexOf(':')
|
||||
if (colonIdx > 0) {
|
||||
const prefix = term.slice(0, colonIdx).toLowerCase()
|
||||
const value = term.slice(colonIdx + 1).trim()
|
||||
if (value) {
|
||||
if (prefix === 'tag') query.tag = [value]
|
||||
else if (prefix === 'author') query.author = [`${value},`]
|
||||
else if (prefix === 'language') query.language = [value]
|
||||
else if (prefix === 'genre') query.genre = [value]
|
||||
else if (prefix === 'publisher') query.publisher = [value]
|
||||
else query.search = value
|
||||
}
|
||||
term = ''
|
||||
}
|
||||
if (term) query.search = term
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
},
|
||||
optionList: [
|
||||
{
|
||||
type: 'select',
|
||||
options: [
|
||||
'*metadata.lastModified,desc-更新时间(新→旧)',
|
||||
'metadata.lastModified,asc-更新时间(旧→新)',
|
||||
'metadata.titleSort,asc-标题(A-Z)',
|
||||
'metadata.titleSort,desc-标题(Z-A)'
|
||||
],
|
||||
label: '排序',
|
||||
default: null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
comic = {
|
||||
loadInfo: async (id) => {
|
||||
const bookId = this.extractBookId(id)
|
||||
if (bookId) {
|
||||
return await this.loadBookDetails(bookId)
|
||||
}
|
||||
const [series, booksPage] = await Promise.all([
|
||||
this.getJson(`/api/v1/series/${id}`),
|
||||
this.getJson(`/api/v1/series/${id}/books`, {
|
||||
unpaged: true,
|
||||
sort: ['metadata.numberSort,asc']
|
||||
})
|
||||
])
|
||||
const books = Array.isArray(booksPage?.content) ? booksPage.content : []
|
||||
const readable = books.filter((book) => this.isSupportedBook(book))
|
||||
readable.sort((a, b) => this.compareBooks(a, b))
|
||||
const chapters = new Map()
|
||||
readable.forEach((book, index) => {
|
||||
chapters.set(book.id, this.formatBookTitle(book, index))
|
||||
})
|
||||
const metadata = series?.metadata || {}
|
||||
const summary = series?.booksMetadata?.summary || metadata.summary || ''
|
||||
const authors = this.collectAuthors(series?.booksMetadata?.authors)
|
||||
const genres = Array.isArray(metadata.genres) ? metadata.genres : []
|
||||
const tags = Array.isArray(series?.booksMetadata?.tags) ? series.booksMetadata.tags : []
|
||||
const description = summary || '暂无简介'
|
||||
const tagSections = {}
|
||||
if (authors.length) tagSections['作者'] = authors
|
||||
if (genres.length) tagSections['类型'] = this.uniqueArray(genres)
|
||||
if (tags.length) tagSections['标签'] = this.uniqueArray(tags)
|
||||
if (!readable.length && books.length) {
|
||||
tagSections['提示'] = ['该系列包含的项目暂不支持阅读']
|
||||
}
|
||||
const info = new ComicDetails({
|
||||
title: metadata.title || series?.name || id,
|
||||
subTitle: authors.slice(0, 3).join(', '),
|
||||
cover: this.buildUrl(`/api/v1/series/${id}/thumbnail`),
|
||||
description,
|
||||
tags: tagSections,
|
||||
chapters,
|
||||
updateTime: this.formatDate(series?.lastModified),
|
||||
uploadTime: this.formatDate(series?.created),
|
||||
url: series?.url || this.buildUrl(`/series/${id}`)
|
||||
})
|
||||
return info
|
||||
},
|
||||
loadEp: async (comicId, epId) => {
|
||||
let bookId = epId || comicId
|
||||
if (typeof bookId === 'string' && bookId.startsWith('book:')) {
|
||||
bookId = bookId.slice(5)
|
||||
}
|
||||
if (typeof comicId === 'string' && comicId.startsWith('book:') && !epId) {
|
||||
bookId = comicId.slice(5)
|
||||
}
|
||||
const pages = await this.getJson(`/api/v1/books/${bookId}/pages`)
|
||||
const list = Array.isArray(pages) ? pages : []
|
||||
list.sort((a, b) => (a?.number ?? 0) - (b?.number ?? 0))
|
||||
const zeroBased = list.some((page) => (page?.number ?? 1) === 0)
|
||||
const images = list
|
||||
.filter((page) => this.isPageRenderable(page))
|
||||
.map((page) => {
|
||||
const number = page?.number ?? 0
|
||||
return this.buildUrl(`/api/v1/books/${bookId}/pages/${number}`, zeroBased ? { zero_based: true } : null)
|
||||
})
|
||||
return { images }
|
||||
},
|
||||
onImageLoad: (url) => {
|
||||
return {
|
||||
headers: this.imageHeaders
|
||||
}
|
||||
},
|
||||
onThumbnailLoad: () => {
|
||||
return {
|
||||
headers: this.imageHeaders
|
||||
}
|
||||
},
|
||||
onClickTag: (namespace, tag) => {
|
||||
if (!tag) throw '无效的标签'
|
||||
const ns = (namespace || '').toLowerCase()
|
||||
if (ns === '作者') {
|
||||
return {
|
||||
action: 'search',
|
||||
keyword: `author:${tag}`,
|
||||
param: null,
|
||||
}
|
||||
}
|
||||
if (ns === '类型' || ns === '标签') {
|
||||
return {
|
||||
action: 'category',
|
||||
keyword: `genre:${tag}`,
|
||||
param: `${tag}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
action: 'search',
|
||||
keyword: tag,
|
||||
param: null,
|
||||
}
|
||||
},
|
||||
enableTagsTranslate: false,
|
||||
}
|
||||
|
||||
async refreshReferenceData(force) {
|
||||
const token = this.authToken
|
||||
if (!token) {
|
||||
this.saveData('komga_libraries', [])
|
||||
this.saveData('komga_tags', [])
|
||||
this.saveData('komga_genres', [])
|
||||
this.saveData('komga_languages', [])
|
||||
this.saveData('komga_collections', [])
|
||||
return
|
||||
}
|
||||
const now = Date.now()
|
||||
const last = this.loadData('komga_meta_ts')
|
||||
if (!force && last && now - last < 5 * 60 * 1000) return
|
||||
try {
|
||||
const [libraries, tags, languages, collections, genres] = await Promise.all([
|
||||
this.getJson('/api/v1/libraries'),
|
||||
this.getJson('/api/v1/tags/series'),
|
||||
this.getJson('/api/v1/languages'),
|
||||
this.getJson('/api/v1/collections', { unpaged: true, sort: ['name,asc'] }),
|
||||
this.getJson('/api/v1/genres')
|
||||
])
|
||||
const libraryList = Array.isArray(libraries) ? libraries.filter((library) => library && library.id) : []
|
||||
const collectionPage = collections && typeof collections === 'object' ? collections : null
|
||||
const collectionList = Array.isArray(collectionPage?.content) ? collectionPage.content : Array.isArray(collections) ? collections : []
|
||||
this.saveData('komga_libraries', libraryList)
|
||||
this.saveData('komga_tags', Array.isArray(tags) ? tags : [])
|
||||
this.saveData('komga_genres', Array.isArray(genres) ? genres : [])
|
||||
this.saveData('komga_languages', Array.isArray(languages) ? languages : [])
|
||||
this.saveData('komga_collections', collectionList)
|
||||
this.saveData('komga_meta_ts', now)
|
||||
} catch (error) {
|
||||
this.saveData('komga_libraries', [])
|
||||
this.saveData('komga_tags', [])
|
||||
this.saveData('komga_genres', [])
|
||||
this.saveData('komga_languages', [])
|
||||
this.saveData('komga_collections', [])
|
||||
if (String(error) === 'Login expired') throw error
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSeriesList(path, query) {
|
||||
const data = await this.getJson(path, query)
|
||||
const content = Array.isArray(data?.content) ? data.content : []
|
||||
const comics = content.map((item) => this.parseSeries(item)).filter(Boolean)
|
||||
return {
|
||||
comics,
|
||||
totalPages: data?.totalPages ?? 1
|
||||
}
|
||||
}
|
||||
|
||||
async fetchBookList(path, query) {
|
||||
const data = await this.getJson(path, query)
|
||||
const content = Array.isArray(data?.content) ? data.content : []
|
||||
const comics = content.map((item) => this.parseBook(item)).filter(Boolean)
|
||||
return {
|
||||
comics,
|
||||
totalPages: data?.totalPages ?? 1
|
||||
}
|
||||
}
|
||||
|
||||
parseBook(book) {
|
||||
if (!book || !this.isSupportedBook(book)) return null
|
||||
const metadata = book.metadata || {}
|
||||
const title = metadata.title || book.name || book.id
|
||||
const authors = this.collectAuthors(metadata.authors)
|
||||
const tags = Array.isArray(metadata.tags) ? metadata.tags : []
|
||||
const description = metadata.summary || ''
|
||||
const subtitleParts = []
|
||||
if (book.seriesTitle) subtitleParts.push(book.seriesTitle)
|
||||
if (authors.length) subtitleParts.push(authors[0])
|
||||
return new Comic({
|
||||
id: `book:${book.id}`,
|
||||
title,
|
||||
subTitle: subtitleParts.join(' · '),
|
||||
cover: this.buildUrl(`/api/v1/books/${book.id}/thumbnail`),
|
||||
tags: this.uniqueArray(tags).slice(0, 12),
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
extractBookId(id) {
|
||||
if (typeof id !== 'string') return null
|
||||
return id.startsWith('book:') ? id.slice(5) : null
|
||||
}
|
||||
|
||||
async loadBookDetails(bookId) {
|
||||
const book = await this.getJson(`/api/v1/books/${bookId}`)
|
||||
if (!book) throw '未找到该图书'
|
||||
const metadata = book.metadata || {}
|
||||
const authors = this.collectAuthors(metadata.authors)
|
||||
const tags = this.uniqueArray(Array.isArray(metadata.tags) ? metadata.tags : [])
|
||||
const description = metadata.summary || '暂无简介'
|
||||
const tagSections = {}
|
||||
if (authors.length) tagSections['作者'] = authors
|
||||
if (tags.length) tagSections['标签'] = tags
|
||||
if (book.seriesTitle) tagSections['系列'] = [book.seriesTitle]
|
||||
if (!this.isSupportedBook(book)) tagSections['提示'] = ['该图书暂不支持阅读']
|
||||
const chapters = new Map()
|
||||
const chapterTitle = metadata.title || book.name || '立即阅读'
|
||||
chapters.set(book.id, chapterTitle)
|
||||
return new ComicDetails({
|
||||
title: metadata.title || book.name || bookId,
|
||||
subTitle: book.seriesTitle || authors.slice(0, 3).join(', '),
|
||||
cover: this.buildUrl(`/api/v1/books/${bookId}/thumbnail`),
|
||||
description,
|
||||
tags: tagSections,
|
||||
chapters,
|
||||
updateTime: this.formatDate(book.lastModified),
|
||||
uploadTime: this.formatDate(book.created),
|
||||
url: book.url || this.buildUrl(`/books/${bookId}`)
|
||||
})
|
||||
}
|
||||
|
||||
parseSeries(series) {
|
||||
if (!series) return null
|
||||
const metadata = series.metadata || {}
|
||||
const title = metadata.title || series.name || series.id
|
||||
const authors = this.collectAuthors(series?.booksMetadata?.authors)
|
||||
const tags = []
|
||||
if (Array.isArray(metadata.genres)) tags.push(...metadata.genres)
|
||||
if (Array.isArray(series?.booksMetadata?.tags)) tags.push(...series.booksMetadata.tags)
|
||||
const description = series?.booksMetadata?.summary || metadata.summary || ''
|
||||
return new Comic({
|
||||
id: series.id,
|
||||
title,
|
||||
subTitle: authors.slice(0, 2).join(', '),
|
||||
cover: this.buildUrl(`/api/v1/series/${series.id}/thumbnail`),
|
||||
tags: this.uniqueArray(tags).slice(0, 12),
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
collectAuthors(authors) {
|
||||
if (!Array.isArray(authors)) return []
|
||||
return this.uniqueArray(authors.map((author) => author?.name).filter(Boolean))
|
||||
}
|
||||
|
||||
uniqueArray(list) {
|
||||
if (!Array.isArray(list)) return []
|
||||
const set = new Set()
|
||||
const result = []
|
||||
for (const item of list) {
|
||||
const value = typeof item === 'string' ? item.trim() : ''
|
||||
if (!value) continue
|
||||
const key = value.toLowerCase()
|
||||
if (set.has(key)) continue
|
||||
set.add(key)
|
||||
result.push(value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
isSupportedBook(book) {
|
||||
if (!book || !book.media) return false
|
||||
const status = String(book.media.status || '').toUpperCase()
|
||||
if (status && status !== 'READY') return false
|
||||
const mediaType = String(book.media.mediaType || '').toLowerCase()
|
||||
if (!mediaType) return false
|
||||
if (mediaType.includes('epub') || mediaType.includes('pdf') || mediaType.includes('mobi')) return false
|
||||
if ((book.media.pagesCount || 0) <= 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
isPageRenderable(page) {
|
||||
if (!page) return false
|
||||
const mediaType = String(page.mediaType || '').toLowerCase()
|
||||
if (!mediaType) return true
|
||||
return mediaType.startsWith('image/') || mediaType.includes('jpeg') || mediaType.includes('png') || mediaType.includes('webp')
|
||||
}
|
||||
|
||||
compareBooks(a, b) {
|
||||
const aSort = typeof a?.metadata?.numberSort === 'number' ? a.metadata.numberSort : NaN
|
||||
const bSort = typeof b?.metadata?.numberSort === 'number' ? b.metadata.numberSort : NaN
|
||||
if (!Number.isNaN(aSort) && !Number.isNaN(bSort)) return aSort - bSort
|
||||
const aNumber = parseFloat(a?.metadata?.number)
|
||||
const bNumber = parseFloat(b?.metadata?.number)
|
||||
if (!Number.isNaN(aNumber) && !Number.isNaN(bNumber)) return aNumber - bNumber
|
||||
return (a?.metadata?.title || a?.name || '').localeCompare(b?.metadata?.title || b?.name || '')
|
||||
}
|
||||
|
||||
formatBookTitle(book, index) {
|
||||
const metadata = book?.metadata || {}
|
||||
if (metadata.title) return metadata.title
|
||||
if (metadata.number) return `第${metadata.number}卷`
|
||||
if (book?.number != null) return `第${book.number}卷`
|
||||
return `章节 ${index + 1}`
|
||||
}
|
||||
|
||||
extractOption(options, index, fallback) {
|
||||
if (!Array.isArray(options) || options.length <= index) return fallback
|
||||
let value = options[index]
|
||||
if (typeof value !== 'string') return fallback
|
||||
if (value.startsWith('*')) value = value.slice(1)
|
||||
const idx = value.indexOf('-')
|
||||
return idx > -1 ? value.slice(0, idx) : value
|
||||
}
|
||||
|
||||
async getJson(path, query) {
|
||||
const res = await Network.get(this.buildUrl(path, query), this.headers)
|
||||
this.ensureOk(res)
|
||||
const text = res.body
|
||||
if (!text) return null
|
||||
return JSON.parse(text)
|
||||
}
|
||||
|
||||
ensureOk(res) {
|
||||
if (!res) throw '请求失败'
|
||||
if (res.status === 401 || res.status === 403) throw 'Login expired'
|
||||
if (res.status < 200 || res.status >= 300) throw `请求失败: ${res.status}`
|
||||
}
|
||||
|
||||
buildUrl(path, query) {
|
||||
let url = path
|
||||
if (!/^https?:\/\//i.test(path)) {
|
||||
url = `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
}
|
||||
const qs = this.buildQuery(query)
|
||||
return qs ? `${url}?${qs}` : url
|
||||
}
|
||||
|
||||
buildQuery(query) {
|
||||
if (!query) return ''
|
||||
const parts = []
|
||||
for (const key of Object.keys(query)) {
|
||||
const value = query[key]
|
||||
if (value === undefined || value === null) continue
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (item === undefined || item === null) continue
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`)
|
||||
}
|
||||
} else {
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
}
|
||||
}
|
||||
return parts.join('&')
|
||||
}
|
||||
|
||||
formatDate(value) {
|
||||
if (!value) return null
|
||||
try {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return null
|
||||
return date.toISOString().split('T')[0]
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
118
manga_dex.js
118
manga_dex.js
@@ -8,7 +8,7 @@ class MangaDex extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "manga_dex"
|
||||
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
|
||||
minAppVersion = "1.4.0"
|
||||
|
||||
@@ -239,16 +239,15 @@ class MangaDex extends ComicSource {
|
||||
// randomNumber: 5,
|
||||
|
||||
// load function for dynamic type
|
||||
loader: () => {
|
||||
loader: () => {
|
||||
let categories = []
|
||||
for (let tag of Object.keys(this.tags)) {
|
||||
categories.push({
|
||||
label: tag,
|
||||
target: {
|
||||
page: "search",
|
||||
attributes: {
|
||||
keyword: `tag:${tag}`,
|
||||
},
|
||||
action: "category",
|
||||
keyword: tag,
|
||||
param: this.tags[tag],
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -260,6 +259,113 @@ class MangaDex extends ComicSource {
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
categoryComics = {
|
||||
load: async (category, param, options = [], page = 1) => {
|
||||
if (!param) {
|
||||
throw new Error("No tag id provided for category comics")
|
||||
}
|
||||
|
||||
const parseOption = (option, fallback) => {
|
||||
if (option === undefined || option === null || option === "") {
|
||||
return fallback
|
||||
}
|
||||
let value = option.split("-")[0]
|
||||
return value || fallback
|
||||
}
|
||||
|
||||
const sortOption = parseOption(options[0], "popular")
|
||||
const ratingOption = parseOption(options[1], "any")
|
||||
const statusOption = parseOption(options[2], "any")
|
||||
|
||||
let params = [
|
||||
"includes[]=cover_art",
|
||||
"includes[]=artist",
|
||||
"includes[]=author",
|
||||
"hasAvailableChapters=true",
|
||||
`limit=${this.comicsPerPage}`,
|
||||
`includedTags[]=${encodeURIComponent(param)}`
|
||||
]
|
||||
|
||||
if (page && page > 1) {
|
||||
params.push(`offset=${(page - 1) * this.comicsPerPage}`)
|
||||
}
|
||||
|
||||
if (sortOption !== "any") {
|
||||
const orderMap = {
|
||||
popular: "followedCount",
|
||||
follows: "followedCount",
|
||||
recent: "createdAt",
|
||||
updated: "latestUploadedChapter",
|
||||
rating: "rating"
|
||||
}
|
||||
const orderKey = orderMap[sortOption]
|
||||
if (orderKey) {
|
||||
params.push(`order[${orderKey}]=desc`)
|
||||
}
|
||||
}
|
||||
|
||||
let ratingList
|
||||
if (ratingOption === "any") {
|
||||
ratingList = ["safe", "suggestive", "erotica"]
|
||||
} else {
|
||||
ratingList = [ratingOption]
|
||||
}
|
||||
for (let rating of ratingList) {
|
||||
params.push(`contentRating[]=${encodeURIComponent(rating)}`)
|
||||
}
|
||||
|
||||
if (statusOption !== "any") {
|
||||
params.push(`status[]=${encodeURIComponent(statusOption)}`)
|
||||
}
|
||||
|
||||
let url = `https://api.mangadex.org/manga?${params.join("&")}`
|
||||
let res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok")
|
||||
}
|
||||
let data = await res.json()
|
||||
let total = data['total'] || 0
|
||||
let comics = []
|
||||
for (let comic of data['data'] || []) {
|
||||
comics.push(this.api.parseComic(comic))
|
||||
}
|
||||
let maxPage = total ? Math.ceil(total / this.comicsPerPage) : (comics.length < this.comicsPerPage ? page : page + 1)
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: maxPage
|
||||
}
|
||||
},
|
||||
optionList: [
|
||||
{
|
||||
options: [
|
||||
"any-Any",
|
||||
"popular-Popular",
|
||||
"recent-Recent",
|
||||
"updated-Updated",
|
||||
"rating-Rating",
|
||||
"follows-Follows"
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"any-Any",
|
||||
"safe-Safe",
|
||||
"suggestive-Suggestive",
|
||||
"erotica-Erotica"
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"any-Any",
|
||||
"ongoing-Ongoing",
|
||||
"completed-Completed",
|
||||
"hiatus-Hiatus",
|
||||
"cancelled-Cancelled"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
/**
|
||||
|
||||
158
manhuagui.js
158
manhuagui.js
@@ -4,7 +4,7 @@ class ManHuaGui extends ComicSource {
|
||||
|
||||
key = "ManHuaGui";
|
||||
|
||||
version = "1.1.0";
|
||||
version = "1.2.1";
|
||||
|
||||
minAppVersion = "1.4.0";
|
||||
|
||||
@@ -453,6 +453,14 @@ class ManHuaGui extends ComicSource {
|
||||
let imgInfos = extractFields(imgData);
|
||||
return imgInfos;
|
||||
};
|
||||
|
||||
this.decodeViewState = function (viewState) {
|
||||
if (!viewState) {
|
||||
return null;
|
||||
}
|
||||
let decoded = LZString.decompressFromBase64(viewState);
|
||||
return decoded;
|
||||
};
|
||||
}
|
||||
|
||||
// explore page list
|
||||
@@ -848,6 +856,7 @@ class ManHuaGui extends ComicSource {
|
||||
loadInfo: async (id) => {
|
||||
let url = `${this.baseUrl}/comic/${id}/`;
|
||||
let document = await this.getHtml(url);
|
||||
|
||||
// ANCHOR 基本信息
|
||||
let book = document.querySelector(".book-cont");
|
||||
let title = book
|
||||
@@ -896,30 +905,88 @@ class ManHuaGui extends ComicSource {
|
||||
};
|
||||
let updateTime = detail_list[8].text.trim();
|
||||
|
||||
// ANCHOR 章节信息
|
||||
let chapterDocument = document;
|
||||
let isAdultWarning = document.querySelector("#checkAdult");
|
||||
let viewStateElement = document.querySelector("#__VIEWSTATE");
|
||||
if (isAdultWarning && viewStateElement) {
|
||||
let viewStateValue = viewStateElement.attributes["value"];
|
||||
if (viewStateValue) {
|
||||
let decodedViewState = this.decodeViewState(viewStateValue);
|
||||
if (decodedViewState) {
|
||||
let sanitized = decodedViewState.trim();
|
||||
sanitized = sanitized.replace(/^\/\/+/, "").trim();
|
||||
if (!/class=['"]chapter['"]/.test(sanitized)) {
|
||||
sanitized = `<div class="chapter">${sanitized}</div>`;
|
||||
}
|
||||
try {
|
||||
chapterDocument = new HtmlDocument(sanitized);
|
||||
} catch (error) {
|
||||
console.error("解析成人章节列表失败:", error);
|
||||
chapterDocument = document;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 支持多分组
|
||||
let chaptersMap = new Map();
|
||||
|
||||
// 查找所有章节分组标题
|
||||
let chapterGroups = document.querySelectorAll(".chapter h4 span");
|
||||
let chapterGroups = chapterDocument.querySelectorAll(".chapter h4 span");
|
||||
if (chapterGroups.length === 0) {
|
||||
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 = document.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);
|
||||
}
|
||||
}
|
||||
@@ -1170,6 +1237,63 @@ class ManHuaGui extends ComicSource {
|
||||
maxPage: replyTo ? 1 : (Math.ceil(data.total / 10) || 1)
|
||||
};
|
||||
},
|
||||
|
||||
sendComment: async (comicId, subId, content, replyTo) => {
|
||||
let mhg_cookie = this.loadData("mhg_cookie");
|
||||
if (!mhg_cookie) {
|
||||
throw "请先登录漫画柜账号";
|
||||
}
|
||||
let url = `${this.baseUrl}/tools/submit_ajax.ashx?action=comment_add`;
|
||||
|
||||
let headers = {
|
||||
accept: "application/json, text/javascript, */*; q=0.01",
|
||||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||
"cache-control": "no-cache",
|
||||
pragma: "no-cache",
|
||||
"sec-ch-ua": '"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
Referer: `${this.baseUrl}/comic/${comicId}/`,
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
cookie: mhg_cookie,
|
||||
dnt:1,
|
||||
origin: 'https://www.manhuagui.com',
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
};
|
||||
|
||||
let bodyParams = ''
|
||||
bodyParams += `book_id=${comicId}&`;
|
||||
// double-encode to match site submission behaviour
|
||||
bodyParams += `txtContent=${encodeURIComponent(encodeURIComponent(content))}&`;
|
||||
if (replyTo) {
|
||||
bodyParams += `to_comment_id=${replyTo.split('//')[0]}`;
|
||||
}else{
|
||||
bodyParams += `to_comment_id=0`;
|
||||
}
|
||||
|
||||
let res = await Network.post(url, headers, bodyParams);
|
||||
|
||||
if (res.status === 401) {
|
||||
error(`Login expired`);
|
||||
return;
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
throw `发送评论失败,状态码: ${res.status}`;
|
||||
}
|
||||
|
||||
|
||||
// 获取post请求的响应的json
|
||||
let data = JSON.parse(res.body);
|
||||
if (data.status !== 1) {
|
||||
throw `发送评论失败: ${data.msg}`;
|
||||
}
|
||||
|
||||
return 'ok';
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理标签点击事件
|
||||
|
||||
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
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
49
manwaba.js
49
manwaba.js
@@ -8,14 +8,15 @@ class ManWaBa extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "manwaba";
|
||||
|
||||
version = "1.0.0";
|
||||
version = "1.0.2";
|
||||
|
||||
minAppVersion = "1.4.0";
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/manwaba.js";
|
||||
|
||||
api = "https://www.manwaba.com/api/v1";
|
||||
//api = "https://www.manwaba.com/api"; //重定向之前的地址无法使用分类
|
||||
api = "https://www.mhtmh.org/api";
|
||||
|
||||
init() {
|
||||
/**
|
||||
@@ -82,15 +83,15 @@ class ManWaBa extends ComicSource {
|
||||
type: "",
|
||||
flag: false,
|
||||
};
|
||||
const url = `${this.api}/json/home`;
|
||||
const url = `${this.api}/home`;
|
||||
const data = await this.fetchJson(url, { params }).then(
|
||||
(res) => res.data
|
||||
);
|
||||
let magnaList = {
|
||||
热门: data.comicList,
|
||||
古风: data.gufengList,
|
||||
玄幻: data.xuanhuanList,
|
||||
校园: data.xiaoyuanList,
|
||||
最新完整版: data.gufengList,
|
||||
最新更新: data.xuanhuanList,
|
||||
热门收藏: data.xiaoyuanList,
|
||||
};
|
||||
function parseComic(comic) {
|
||||
return new Comic({
|
||||
@@ -196,7 +197,31 @@ class ManWaBa extends ComicSource {
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (category, param, options, page) => {
|
||||
let url = `${this.api}/json/cate`;
|
||||
let pathMap = {
|
||||
"": "/cate",
|
||||
"热血": "/cate/hotblooded",
|
||||
"玄幻": "/cate/xuanhuan",
|
||||
"恋爱": "/cate/romance",
|
||||
"冒险": "/cate/adventure",
|
||||
"古风": "/cate/historical",
|
||||
"都市": "/cate/urban",
|
||||
"穿越": "/cate/transmigration",
|
||||
"奇幻": "/cate/fantasy",
|
||||
"搞笑": "/cate/comedy",
|
||||
"少男": "/cate/shounen",
|
||||
"战斗": "/cate/action",
|
||||
"重生": "/cate/rebirth",
|
||||
"逆袭": "/cate/counterattack",
|
||||
"爆笑": "/cate/hilarious",
|
||||
"少年": "/cate/youth",
|
||||
"系统": "/cate/system",
|
||||
"BL": "/cate/bl",
|
||||
"韩漫": "/cate/manhwa",
|
||||
"完整版": "/cate/fullversion",
|
||||
"19r": "/cate/19plus",
|
||||
"台版": "/cate/taiwanver",
|
||||
};
|
||||
let url = this.api + pathMap[param] || "/cate";
|
||||
let payload = JSON.stringify({
|
||||
page: {
|
||||
page: page,
|
||||
@@ -228,7 +253,7 @@ class ManWaBa extends ComicSource {
|
||||
let data = await this.fetchJson(url, {
|
||||
method: "POST",
|
||||
payload,
|
||||
}).then((res) => res.data);
|
||||
}).then((res) => res.data.list);
|
||||
|
||||
function parseComic(comic) {
|
||||
return new Comic({
|
||||
@@ -280,7 +305,7 @@ class ManWaBa extends ComicSource {
|
||||
*/
|
||||
load: async (keyword, options, page) => {
|
||||
const pageSize = 20;
|
||||
let url = `${this.api}/json/search`;
|
||||
let url = `${this.api}/search`;
|
||||
let params = {
|
||||
keyword,
|
||||
type: "mh",
|
||||
@@ -316,13 +341,13 @@ class ManWaBa extends ComicSource {
|
||||
* @returns {Promise<ComicDetails>}s
|
||||
*/
|
||||
loadInfo: async (id) => {
|
||||
let url = `${this.api}/json/comic/${id}`;
|
||||
let url = `${this.api}/comic/${id}`;
|
||||
let data = await this.fetchJson(url, { payload: undefined }).then(
|
||||
(res) => res.data
|
||||
);
|
||||
this.logger.warn(`loadInfo: ${data}`);
|
||||
let chapterId = data.id;
|
||||
let chapterApi = `${this.api}/json/comic/chapter`;
|
||||
let chapterApi = `${this.api}/comic/chapter`;
|
||||
let params = {
|
||||
comicId: chapterId,
|
||||
page: 1,
|
||||
@@ -375,7 +400,7 @@ class ManWaBa extends ComicSource {
|
||||
let imageRes = await this.fetchJson(imgApi, {
|
||||
params: {
|
||||
...params,
|
||||
pageSize: pageNum,
|
||||
page_size: pageNum,
|
||||
},
|
||||
}).then((res) => res.data.images);
|
||||
let images = imageRes.map((item) => item.url);
|
||||
|
||||
321
mh1234.js
Normal file
321
mh1234.js
Normal file
@@ -0,0 +1,321 @@
|
||||
class MH1234 extends ComicSource {
|
||||
// name of the source
|
||||
name = "漫画1234"
|
||||
|
||||
// unique id of the source
|
||||
key = "mh1234"
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
minAppVersion = "1.4.0"
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/mh1234.js"
|
||||
|
||||
settings = {
|
||||
domains: {
|
||||
title: "域名",
|
||||
type: "input",
|
||||
default: "amh1234.com"
|
||||
}
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return `https://b.${this.loadSetting('domains')}`;
|
||||
}
|
||||
|
||||
// explore page list
|
||||
explore = [{
|
||||
title: "漫画1234",
|
||||
type: "singlePageWithMultiPart",
|
||||
load: async () => {
|
||||
const result = {};
|
||||
const res = await Network.get(this.baseUrl);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const doc = new HtmlDocument(res.body);
|
||||
const mangaLists = doc.querySelectorAll("div.imgBox");
|
||||
for (let list of mangaLists) {
|
||||
const tabTitle = list.querySelector(".Title").text;
|
||||
const items = [];
|
||||
for (let item of list.querySelectorAll("li.list-comic")) {
|
||||
const info = item.querySelectorAll("a")[1];
|
||||
items.push(new Comic({
|
||||
id: item.attributes["data-key"],
|
||||
title: item.querySelector("a.txtA").text,
|
||||
cover: item.querySelector("img").attributes["src"]
|
||||
}));
|
||||
}
|
||||
result[tabTitle] = items;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}];
|
||||
|
||||
// categories
|
||||
category = {
|
||||
/// title of the category page, used to identify the page, it should be unique
|
||||
title: "漫画1234",
|
||||
parts: [
|
||||
{
|
||||
name: "题材",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"全部", "少年热血", "武侠格斗", "科幻魔幻", "竞技体育", "爆笑喜剧", "侦探推理", "恐怖灵异", "耽美人生",
|
||||
"少女爱情", "恋爱生活", "生活漫画", "战争漫画", "故事漫画", "其他漫画", "爱情", "唯美", "武侠", "玄幻",
|
||||
"后宫", "治愈", "励志", "古风", "校园", "虐心", "魔幻", "冒险", "欢乐向", "节操", "悬疑", "历史", "职场",
|
||||
"神鬼", "明星", "穿越", "百合", "西方魔幻", "纯爱", "音乐舞蹈", "轻小说", "侦探", "伪娘", "仙侠", "四格",
|
||||
"剧情", "萌系", "东方", "性转换", "宅系", "美食", "脑洞", "惊险", "爆笑", "都市", "蔷薇", "恋爱", "格斗",
|
||||
"科幻", "魔法", "奇幻", "热血", "其他", "搞笑", "生活", "恐怖", "架空", "竞技", "战争", "搞笑喜剧", "青春",
|
||||
"浪漫", "爽流", "神话", "轻松", "日常", "家庭", "婚姻", "动作", "战斗", "异能", "内涵", "同人", "惊奇",
|
||||
"正剧", "推理", "宠物", "温馨", "异世界", "颜艺", "惊悚", "舰娘","机战", "彩虹", "耽美", "轻松搞笑",
|
||||
"修真恋爱架空", "复仇", "霸总", "段子", "逆袭", "烧脑", "娱乐圈", "纠结", "感动", "豪门", "体育", "机甲",
|
||||
"末世", "灵异", "僵尸", "宫廷", "权谋", "未来", "科技", "商战", "乡村", "震撼", "游戏", "重口味", "血腥",
|
||||
"逗比", "丧尸", "神魔", "修真", "社会", "召唤兽", "装逼", "新作", "漫改", "真人", "运动", "高智商", "悬疑推理",
|
||||
"机智", "史诗", "萝莉", "宫斗", "御姐", "恶搞", "精品", "日更", "小说改编", "防疫", "吸血", "暗黑", "总裁",
|
||||
"重生", "大女主", "系统", "神仙", "末日", "怪物", "妖怪", "修仙", "宅斗", "神豪", "高甜", "电竞", "豪快",
|
||||
"猎奇", "多世界", "性转", "少女", "改编", "女生", "乙女", "男生", "兄弟情", "智斗", "少男", "连载", "奇幻冒险",
|
||||
"古风穿越", "浪漫爱情", "古装", "幽默搞笑", "偶像", "小僵尸", "BL", "少年", "橘味", "情感", "经典",
|
||||
"腹黑", "都市大女主", "致郁", "美少女", "少儿", "暖萌", "长条", "限制级", "知音漫客", "氪金", "独家",
|
||||
"亲情", "现代", "武侠仙侠", "西幻", "超级英雄", "女神", "幻想", "欧风", "养成", "动作冒险", "GL", "橘调",
|
||||
"悬疑灵异", "古代宫廷", "欧式宫廷", "游戏竞技", "橘系", "奇幻爱情", "架空世界", "ゆり", "福瑞", "秀吉", "现代言情",
|
||||
"古代言情", "豪门总裁", "现言萌宝", "玄幻言情", "虐渣", "团宠", "古言萌宝", "现言甜宠", "古言脑洞", "AA", "金手指",
|
||||
"玄幻脑洞", "都市脑洞", "甜宠", "伦理", "生存", "TL", "悬疑脑洞", "黑暗", "独特", "成长", "幻想言情", "直播",
|
||||
"游戏体育", "现言脑洞", "音乐", "双男主", "迪化", "LGBTQ+", "正能量", "军事", "ABO", "悬疑恐怖",
|
||||
"玄幻科幻", "投稿", "种田", "经营", "反套路", "无节操", "强强", "克苏鲁", "无敌流", "冒险热血", "畅销",
|
||||
"大人系", "宅向", "萌娃", "宠兽", "异形", "撒糖", "诡异", "言情", "西方", "滑稽搞笑", "同居", "人外",
|
||||
"白切黑", "并肩作战", "救赎", "戏精", "美强惨", "非人类", "原创", "黑白漫", "无限流",
|
||||
"升级", "爽", "轻橘", "女帝", "偏执", "自由", "星际", "可盐可甜", "反差萌", "聪颖", "智商在线",
|
||||
"倔强", "狼人", "欢喜冤家", "吸血鬼", "萌宠", "学校", "台湾作品", "彩色", "武术", "短篇", "契约", "魔王",
|
||||
"无敌", "美女", "暧昧", "网游", "宅男", "追逐梦想", "冒险奇幻", "疯批", "中二", "召唤", "法宝", "钓系", "鬼怪",
|
||||
"占有欲", "阳光", "元气", "强制爱", "黑道", "马甲", "阴郁", "忧郁", "哲理", "病娇", "喜剧", "江湖恩怨",
|
||||
"相爱相杀", "萌", "SM", "精选", "生子", "年下", "18+限制", "日久生情", "梦想", "多攻", "竹马", "骨科", "gnbq"
|
||||
],
|
||||
itemType: "category",
|
||||
categoryParams: [
|
||||
"", "shaonianrexue", "wuxiagedou", "kehuanmohuan", "jingjitiyu", "baoxiaoxiju", "zhentantuili", "kongbulingyi",
|
||||
"danmeirensheng", "shaonvaiqing", "lianaishenghuo", "shenghuomanhua", "zhanzhengmanhua", "gushimanhua",
|
||||
"qitamanhua", "aiqing", "weimei", "wuxia", "xuanhuan", "hougong", "zhiyu", "lizhi", "gufeng", "xiaoyuan", "nuexin",
|
||||
"mohuan", "maoxian", "huanlexiang", "jiecao", "xuanyi", "lishi", "zhichang", "shengui", "mingxing", "chuanyue",
|
||||
"baihe", "xifangmohuan", "chunai", "yinyuewudao", "qingxiaoshuo", "zhentan", "weiniang", "xianxia", "sige", "juqing",
|
||||
"mengxi", "dongfang", "xingzhuanhuan", "zhaixi", "meishi", "naodong", "jingxian", "baoxiao", "dushi", "qiangwei",
|
||||
"lianai", "gedou", "kehuan", "mofa", "qihuan", "rexue", "qita", "gaoxiao", "shenghuo", "kongbu", "jiakong", "jingji",
|
||||
"zhanzheng", "gaoxiaoxiju", "qingchun", "langman", "shuangliu", "shenhua", "qingsong", "richang", "jiating", "hunyin",
|
||||
"dongzuo", "zhandou", "yineng", "neihan", "tongren", "jingqi", "zhengju", "tuili", "chongwu", "wenxin", "yishijie",
|
||||
"yanyi", "jingsong", "jianniang", "jizhan", "caihong", "danmei", "qingsonggaoxiao", "xiuzhenlianaijiakong", "fuchou",
|
||||
"bazong", "duanzi", "nixi", "shaonao", "yulequan", "jiujie", "gandong", "haomen", "tiyu", "jijia", "moshi", "lingyi",
|
||||
"jiangshi", "gongting", "quanmou", "weilai", "keji", "shangzhan", "xiangcun", "zhenhan", "youxi",
|
||||
"zhongkouwei", "xuexing", "doubi", "sangshi", "shenmo", "xiuzhen", "shehui", "zhaohuanshou", "zhuangbi",
|
||||
"xinzuo", "mangai", "zhenren", "yundong", "gaozhishang", "xuanyituili", "jizhi", "shishi", "luoli","gongdou",
|
||||
"yujie", "egao", "jingpin", "rigeng", "xiaoshuogaibian", "fangyi", "xixie", "anhei", "zongcai", "zhongsheng",
|
||||
"danvzhu", "xitong", "shenxian", "mori", "guaiwu", "yaoguai", "xiuxian", "zhaidou", "shenhao", "gaotian",
|
||||
"dianjing", "haokuai", "lieqi", "duoshijie", "xingzhuan", "shaonv", "gaibian", "nvsheng", "yinv", "nansheng",
|
||||
"xiongdiqing", "zhidou", "shaonan", "lianzai", "qihuanmaoxian", "gufengchuanyue", "langmanaiqing", "guzhuang",
|
||||
"youmogaoxiao", "ouxiang", "xiaojiangshi", "BL", "shaonian", "juwei", "qinggan", "jingdian",
|
||||
"fuhei", "dushidanvzhu", "zhiyu2", "meishaonv", "shaoer", "nuanmeng", "changtiao", "xianzhiji", "zhiyinmanke",
|
||||
"kejin", "dujia", "qinqing", "xiandai", "wuxiaxianxia", "xihuan", "chaojiyingxiong", "nvshen", "huanxiang",
|
||||
"oufeng", "yangcheng", "dongzuomaoxian", "GL", "judiao", "xuanyilingyi", "gudaigongting", "oushigongting",
|
||||
"youxijingji", "juxi", "qihuanaiqing", "jiakongshijie", "unknown", "furui", "xiuji", "xiandaiyanqing", "gudaiyanqing",
|
||||
"haomenzongcai", "xianyanmengbao", "xuanhuanyanqing", "nuezha", "tuanchong", "guyanmengbao", "xianyantianchong",
|
||||
"guyannaodong", "AA", "jinshouzhi", "xuanhuannaodong", "dushinaodong", "tianchong", "lunli", "shengcun", "TL",
|
||||
"xuanyinaodong", "heian", "dute", "chengzhang", "huanxiangyanqing", "zhibo", "youxitiyu", "xianyannaodong",
|
||||
"yinyue", "shuangnanzhu", "dihua", "LGBTQ", "zhengnengliang", "junshi", "ABO", "xuanyikongbu", "xuanhuankehuan", "tougao",
|
||||
"zhongtian", "jingying", "fantaolu", "wujiecao", "qiangqiang", "kesulu", "wudiliu", "maoxianrexue", "changxiao",
|
||||
"darenxi", "zhaixiang", "mengwa", "chongshou", "yixing", "satang", "guiyi", "yanqing", "xifang", "huajigaoxiao", "tongju",
|
||||
"renwai", "baiqiehei", "bingjianzuozhan", "jiushu", "xijing", "meiqiangcan", "feirenlei", "yuanchuang", "heibaiman",
|
||||
"wuxianliu", "shengji", "shuang", "qingju", "nvdi", "pianzhi", "ziyou", "xingji", "keyanketian", "fanchameng", "congying",
|
||||
"zhishangzaixian", "juejiang", "langren", "huanxiyuanjia", "xixiegui", "mengchong", "xuexiao", "taiwanzuopin", "caise",
|
||||
"wushu", "duanpian", "qiyue", "mowang", "wudi", "meinv", "aimei", "wangyou", "zhainan", "zhuizhumengxiang", "maoxianqihuan",
|
||||
"fengpi", "zhonger", "zhaohuan", "fabao", "diaoxi", "guiguai", "zhanyouyu", "yangguang", "yuanqi", "qiangzhiai", "heidao",
|
||||
"majia", "yinyu", "youyu", "zheli", "bingjiao", "xiju", "jianghuenyuan", "xiangaixiangsha", "meng", "SM", "jingxuan", "shengzi",
|
||||
"nianxia", "18xianzhi", "rijiushengqing", "mengxiang", "duogong", "zhuma", "guke", "gnbq"
|
||||
],
|
||||
}
|
||||
],
|
||||
// enable ranking page
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
parseComics(html, onePage = false) {
|
||||
const doc = new HtmlDocument(html);
|
||||
const comics = [];
|
||||
for (let comic of doc.querySelectorAll(".itemBox")) {
|
||||
comics.push(new Comic({
|
||||
id: comic.attributes["data-key"],
|
||||
title: comic.querySelector(".title").text,
|
||||
cover: comic.querySelector("img").attributes["src"]
|
||||
}));
|
||||
}
|
||||
return {comics: comics, maxPage: onePage ? 1 : parseInt(doc.querySelector("#total-page").attributes["value"])};
|
||||
}
|
||||
|
||||
parseList(doc) {
|
||||
const comics = [];
|
||||
for (let comic of doc.querySelectorAll(".list-comic")) {
|
||||
comics.push(new Comic({
|
||||
id: comic.attributes["data-key"],
|
||||
title: comic.querySelector(".txtA").text,
|
||||
cover: comic.querySelector("img").attributes["src"]
|
||||
}));
|
||||
}
|
||||
return comics;
|
||||
}
|
||||
|
||||
/// category comic loading related
|
||||
categoryComics = {
|
||||
load: async (category, params, options, page) => {
|
||||
if (params.endsWith(".html")) {
|
||||
const res = await Network.get(`${this.baseUrl}${params}`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
return this.parseComics(res.body, true);
|
||||
} else {
|
||||
const res = await Network.get(`${this.baseUrl}/list/?filter=${params}-${options[0]}-${options[1]}-${options[2]}&sort=${options[3]}&page=${page}`);
|
||||
console.warn(`${this.baseUrl}/list/?filter=${params}-${options[0]}-${options[1]}-${options[2]}&sort=${options[3]}&page=${page}`)
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const doc = new HtmlDocument(res.body);
|
||||
return {comics: this.parseList(doc),
|
||||
maxPage: parseInt(doc.querySelector("#total-page").attributes["value"])};
|
||||
}
|
||||
},
|
||||
optionLoader: async (category, params) => {
|
||||
if (!params.endsWith(".html")) {
|
||||
return [
|
||||
{
|
||||
options: [
|
||||
"-全部",
|
||||
"ertong-儿童漫画",
|
||||
"shaonian-少年漫画",
|
||||
"shaonv-少女漫画",
|
||||
"qingnian-青年漫画",
|
||||
"bailingmanhua-白领漫画",
|
||||
"tongrenmanhua-同人漫画"
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"-全部",
|
||||
"wanjie-已完结",
|
||||
"lianzai-连载中",
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"-全部",
|
||||
"rhmh-日韩",
|
||||
"dlmh-大陆",
|
||||
"gtmh-港台",
|
||||
"taiwan-台湾",
|
||||
"ommh-欧美",
|
||||
"hanguo-韩国",
|
||||
"qtmg-其他",
|
||||
]
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"update-更新",
|
||||
"post-发布",
|
||||
"click-点击",
|
||||
]
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
load: async (keyword, options, page) => {
|
||||
const res = await Network.get(`${this.baseUrl}/search/?keywords=${keyword}&sort=${options[0]}&page=${page}`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
return this.parseComics(res.body);
|
||||
},
|
||||
|
||||
// provide options for search
|
||||
optionList: [
|
||||
{
|
||||
options: [
|
||||
"update-更新",
|
||||
"post-发布",
|
||||
"click-点击",
|
||||
],
|
||||
label: "排序"
|
||||
}
|
||||
],
|
||||
|
||||
// enable tags suggestions
|
||||
enableTagsSuggestions: false,
|
||||
}
|
||||
|
||||
/// single comic related
|
||||
comic = {
|
||||
loadInfo: async (id) => {
|
||||
const res = await Network.get(`${this.baseUrl}/comic/${id}.html`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const doc = new HtmlDocument(res.body);
|
||||
const title = doc.querySelector(".BarTit").text;
|
||||
const cover = doc.querySelector(".pic").querySelector("img").attributes["src"];
|
||||
const description = doc.querySelector("#full-des")?.text;
|
||||
const infos = doc.querySelectorAll(".txtItme");
|
||||
const tags = [];
|
||||
for (let tag of doc.querySelector(".sub_r").querySelectorAll("a")) {
|
||||
const tag_name = tag.text;
|
||||
if (tag_name.length > 0) {
|
||||
tags.push(tag_name);
|
||||
}
|
||||
}
|
||||
const chapters = {};
|
||||
const chapterElements = doc.querySelector(".chapter-warp")?.querySelectorAll("li");
|
||||
if (chapterElements) {
|
||||
for (let ch of chapterElements) {
|
||||
const id = ch.querySelector("a").attributes["href"].replace("/comic/", "").replace(".html", "").split("/").join("_");
|
||||
chapters[id] = ch.querySelector("span").text;
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: title,
|
||||
cover: cover,
|
||||
description: description,
|
||||
tags: {
|
||||
"作者": [infos[0].text.replaceAll("\n", "").replaceAll("\r", "").trim()],
|
||||
"更新": [infos[3].querySelector(".date").text],
|
||||
"标签": tags.slice(0,-1)
|
||||
},
|
||||
chapters: chapters,
|
||||
recommend: this.parseList(doc)
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
loadEp: async (comicId, epId) => {
|
||||
const ids = epId.split("_");
|
||||
const res = await Network.get(`${this.baseUrl}/comic/${ids[0]}/${ids[1]}.html`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const html = res.body;
|
||||
const start = html.search(`var chapterImages = `) + 22;
|
||||
const end = html.search(`;var chapterPath = `) - 2;
|
||||
const end2 = html.search(`;var chapterPrice`) - 1;
|
||||
const images = html.substring(start, end).split(`","`);
|
||||
const cpath = html.substring(end + 22, end2);
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
images[i] = "https://gmh1234.wszwhg.net/" + cpath + images[i].replaceAll("\\", "");
|
||||
images[i] = images[i].replaceAll("//", "/");
|
||||
}
|
||||
return { images };
|
||||
},
|
||||
|
||||
// enable tags translate
|
||||
enableTagsTranslate: false,
|
||||
}
|
||||
}
|
||||
316
mh18.js
Normal file
316
mh18.js
Normal file
@@ -0,0 +1,316 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
class MH18 extends ComicSource {
|
||||
// Note: The fields which are marked as [Optional] should be removed if not used
|
||||
|
||||
// name of the source
|
||||
name = "18漫画"
|
||||
|
||||
// unique id of the source
|
||||
key = "mh18"
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
minAppVersion = "1.4.0"
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/mh18.js"
|
||||
|
||||
settings = {
|
||||
domains: {
|
||||
title: "域名",
|
||||
type: "input",
|
||||
default: "18mh.org"
|
||||
}
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return `https://${this.loadSetting("domains")}`;
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0",
|
||||
"Referer": this.baseUrl
|
||||
};
|
||||
}
|
||||
|
||||
parseComics(doc) {
|
||||
console.warn(doc)
|
||||
const result = [];
|
||||
for (let item of doc.querySelectorAll(".pb-2")) {
|
||||
result.push(new Comic({
|
||||
id: item.querySelector("a").attributes["href"],
|
||||
title: item.querySelector("h3").text,
|
||||
cover: item.querySelector("img").attributes["src"]
|
||||
}))
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// explore page list
|
||||
explore = [
|
||||
{
|
||||
// title of the page.
|
||||
// title is used to identify the page, it should be unique
|
||||
title: this.name,
|
||||
|
||||
/// multiPartPage or multiPageComicList or mixed
|
||||
type: "multiPartPage",
|
||||
|
||||
load: async () => {
|
||||
const res = await Network.get(this.baseUrl, this.headers);
|
||||
const document = new HtmlDocument(res.body);
|
||||
const result = [{ title: "近期更新", comics: [], viewMore: null }];
|
||||
for (let item of document.querySelector(".pb-unit-md").querySelectorAll(".slicarda")) {
|
||||
result[0].comics.push(new Comic({
|
||||
id: item.attributes["href"],
|
||||
title: item.querySelector("h3").text,
|
||||
cover: item.querySelector("img").attributes["src"]
|
||||
}))
|
||||
}
|
||||
const cardlists = document.querySelectorAll(".cardlist");
|
||||
const hometitles = document.querySelectorAll(".hometitle");
|
||||
for (let i = 0; i < hometitles.length; i++) {
|
||||
result.push({
|
||||
title: hometitles[i].querySelector("h2").text,
|
||||
comics: this.parseComics(cardlists[i]),
|
||||
viewMore: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: hometitles[i].querySelector("h2").text,
|
||||
param: hometitles[i].attributes["href"]
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// categories
|
||||
category = {
|
||||
/// title of the category page, used to identify the page, it should be unique
|
||||
title: this.name,
|
||||
parts: [
|
||||
{
|
||||
name: "类型",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"全部",
|
||||
"韓漫",
|
||||
"真人寫真",
|
||||
"日漫",
|
||||
"AI寫真",
|
||||
"熱門漫畫"
|
||||
],
|
||||
itemType: "category",
|
||||
categoryParams: [
|
||||
"/manga",
|
||||
"/manga-genre/hanman",
|
||||
"/manga-genre/zhenrenxiezhen",
|
||||
"/manga-genre/riman",
|
||||
"/manga-genre/aixiezhen",
|
||||
"/manga-genre/hots"
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "标签",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"多人",
|
||||
"慾望",
|
||||
"正妹",
|
||||
"同居",
|
||||
"女學生",
|
||||
"劇情",
|
||||
"偷情",
|
||||
"校园",
|
||||
"逆襲",
|
||||
"办公室",
|
||||
"誘惑",
|
||||
"反转",
|
||||
"熟女",
|
||||
"人妻",
|
||||
"初戀",
|
||||
"少妇",
|
||||
"刺激",
|
||||
"女大学生",
|
||||
"治疗",
|
||||
"超能力",
|
||||
"浪漫校园",
|
||||
"戏剧",
|
||||
"学姐",
|
||||
"大学生",
|
||||
"泳衣",
|
||||
"暧昧",
|
||||
"写真",
|
||||
"女神",
|
||||
"大尺度",
|
||||
"纯情警察"
|
||||
],
|
||||
itemType: "category",
|
||||
categoryParams: [
|
||||
"/manga-tag/duoren",
|
||||
"/manga-tag/yuwang",
|
||||
"/manga-tag/zhengmei",
|
||||
"/manga-tag/tongju",
|
||||
"/manga-tag/nxuesheng",
|
||||
"/manga-tag/juqing",
|
||||
"/manga-tag/touqing",
|
||||
"/manga-tag/xiaoyuan",
|
||||
"/manga-tag/nixi",
|
||||
"/manga-tag/bangongshi",
|
||||
"/manga-tag/youhuo",
|
||||
"/manga-tag/fanzhuan",
|
||||
"/manga-tag/shun",
|
||||
"/manga-tag/renqi",
|
||||
"/manga-tag/chulian",
|
||||
"/manga-tag/shaofu",
|
||||
"/manga-tag/ciji",
|
||||
"/manga-tag/ndaxuesheng",
|
||||
"/manga-tag/zhiliao",
|
||||
"/manga-tag/chaonengli",
|
||||
"/manga-tag/langmanxiaoyuan",
|
||||
"/manga-tag/xiju",
|
||||
"/manga-tag/xuejie",
|
||||
"/manga-tag/daxuesheng",
|
||||
"/manga-tag/yongyi",
|
||||
"/manga-tag/aimei",
|
||||
"/manga-tag/xiezhen",
|
||||
"/manga-tag/nshen",
|
||||
"/manga-tag/dachidu",
|
||||
"/manga-tag/chunqingjingcha"
|
||||
],
|
||||
}
|
||||
],
|
||||
// enable ranking page
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
/// category comic loading related
|
||||
categoryComics = {
|
||||
load: async (category, params, options, page) => {
|
||||
const res = await Network.get(`${this.baseUrl}${params}/page/${page}`, this.headers);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const document = new HtmlDocument(res.body);
|
||||
let maxPage = null;
|
||||
try {
|
||||
maxPage = parseInt(document.querySelectorAll("button.text-small").pop().text.replaceAll("\n", "").replaceAll(" ", ""));
|
||||
} catch (_) {
|
||||
maxPage = 1;
|
||||
}
|
||||
return {
|
||||
comics: this.parseComics(document),
|
||||
maxPage: maxPage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
load: async (keyword, options, page) => {
|
||||
const res = await Network.get(`${this.baseUrl}/s/${keyword}?page=${page}`);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const document = new HtmlDocument(res.body);
|
||||
let maxPage = null;
|
||||
try {
|
||||
maxPage = parseInt(document.querySelectorAll("button.text-small").pop().text.replaceAll("\n", "").replaceAll(" ", ""));
|
||||
} catch (_) {
|
||||
maxPage = 1;
|
||||
}
|
||||
return {
|
||||
comics: this.parseComics(document),
|
||||
maxPage: maxPage
|
||||
};
|
||||
},
|
||||
// enable tags suggestions
|
||||
enableTagsSuggestions: false,
|
||||
}
|
||||
|
||||
/// single comic related
|
||||
comic = {
|
||||
onThumbnailLoad: (url) => {
|
||||
return {
|
||||
headers: this.headers
|
||||
}
|
||||
},
|
||||
loadInfo: async (id) => {
|
||||
if (!id.startsWith("http")) {
|
||||
id = this.baseUrl + id;
|
||||
}
|
||||
const res = await Network.get(id);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const document = new HtmlDocument(res.body);
|
||||
const title = document.querySelector(".text-xl").text.trim().split(" ")[0]
|
||||
const cover = document.querySelector(".object-cover").attributes["src"];
|
||||
const description = document.querySelector("p.text-medium").text;
|
||||
const infos = document.querySelectorAll("div.py-1");
|
||||
const tags = { "作者": [], "类型": [], "标签": [] };
|
||||
for (let author of infos[0].querySelectorAll("a > span")) {
|
||||
let author_name = author.text.trim();
|
||||
if (author_name.endsWith(",")) {
|
||||
author_name = author_name.slice(0, -1).trim();
|
||||
}
|
||||
tags["作者"].push(author_name);
|
||||
}
|
||||
for (let category of infos[1].querySelectorAll("a > span")) {
|
||||
let category_name = category.text.trim();
|
||||
if (category_name.endsWith(",")) {
|
||||
category_name = category_name.slice(0, -1).trim();
|
||||
}
|
||||
tags["类型"].push(category_name);
|
||||
}
|
||||
for (let tag of infos[2].querySelectorAll("a")) {
|
||||
tags["标签"].push(tag.text.replace("\n", "").replaceAll(" ", "").replace("#", ""));
|
||||
}
|
||||
const mangaId = document.querySelector("#mangachapters").attributes["data-mid"];
|
||||
const chapterRes = await Network.get(`${this.baseUrl}/manga/get?mid=${mangaId}&mode=all&t=${Date.now()}`, this.headers);
|
||||
const chapterDoc = new HtmlDocument(chapterRes.body);
|
||||
const chapters = {};
|
||||
for (let ch of chapterDoc.querySelectorAll(".chapteritem")) {
|
||||
const info = ch.querySelector("a");
|
||||
chapters[`${info.attributes["data-ms"]}@${info.attributes["data-cs"]}`] = ch.querySelector(".chaptertitle").text;
|
||||
}
|
||||
const recommend = [];
|
||||
for (let item of document.querySelectorAll("div.cardlist > div.pb-2")) {
|
||||
recommend.push(new Comic({
|
||||
id: item.querySelector("a").attributes["href"],
|
||||
title: item.querySelector("h3").text,
|
||||
cover: item.querySelector("img").attributes["src"]
|
||||
}));
|
||||
}
|
||||
return new ComicDetails({
|
||||
title: title,
|
||||
cover: cover,
|
||||
description: description,
|
||||
tags: tags,
|
||||
chapters: chapters,
|
||||
recommend: recommend,
|
||||
});
|
||||
},
|
||||
|
||||
loadEp: async (comicId, epId) => {
|
||||
const ids = epId.split("@");
|
||||
const res = await Network.get(`${this.baseUrl}/chapter/getcontent?m=${ids[0]}&c=${ids[1]}`, this.headers);
|
||||
if (res.status !== 200) {
|
||||
throw `Invalid status code: ${res.status}`;
|
||||
}
|
||||
const document = new HtmlDocument(res.body);
|
||||
const images = [];
|
||||
for (let i of document.querySelector("#chapcontent").querySelectorAll("img")) {
|
||||
images.push(i.attributes["data-src"] ? i.attributes["data-src"] : i.attributes["src"]);
|
||||
}
|
||||
return { images };
|
||||
},
|
||||
|
||||
// enable tags translate
|
||||
enableTagsTranslate: false,
|
||||
}
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
class ShonenJumpPlus extends ComicSource {
|
||||
name = "少年ジャンプ+";
|
||||
key = "shonen_jump_plus";
|
||||
version = "1.1.0";
|
||||
version = "1.1.1";
|
||||
minAppVersion = "1.2.1";
|
||||
url =
|
||||
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/shonen_jump_plus.js";
|
||||
@@ -10,7 +10,7 @@ class ShonenJumpPlus extends ComicSource {
|
||||
bearerToken = null;
|
||||
userAccountId = null;
|
||||
tokenExpiry = 0;
|
||||
latestVersion = "4.0.21";
|
||||
latestVersion = "4.0.24";
|
||||
|
||||
get headers() {
|
||||
return {
|
||||
@@ -32,10 +32,13 @@ class ShonenJumpPlus extends ComicSource {
|
||||
}
|
||||
|
||||
async init() {
|
||||
const url = "https://apps.apple.com/jp/app/少年ジャンプ-人気漫画が読める雑誌アプリ/id875750302";
|
||||
const url = "https://apps.apple.com/jp/app/id875750302";
|
||||
|
||||
const resp = await Network.get(url);
|
||||
const match = resp.body.match(/":\[\{\\"versionDisplay\\":\\"([\d.]+)\\",\\"rele/);
|
||||
if (match) {
|
||||
|
||||
const match = resp.body.match(/whats-new__latest__version">[^<]*?([\d.]+)</);
|
||||
|
||||
if (match && match[1]) {
|
||||
this.latestVersion = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
260
wnacg.js
260
wnacg.js
@@ -7,15 +7,39 @@ class Wnacg extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "wnacg"
|
||||
|
||||
version = "1.0.2"
|
||||
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)
|
||||
@@ -357,6 +503,7 @@ class Wnacg extends ComicSource {
|
||||
favorites = {
|
||||
// whether support multi folders
|
||||
multiFolder: true,
|
||||
isOldToNewSort: true,
|
||||
/**
|
||||
* add or delete favorite.
|
||||
* throw `Login expired` to indicate login expired, App will automatically re-login and re-add/delete favorite
|
||||
@@ -367,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'
|
||||
}
|
||||
}
|
||||
@@ -391,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 {
|
||||
@@ -414,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'
|
||||
@@ -426,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'
|
||||
@@ -441,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)
|
||||
@@ -468,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 {
|
||||
@@ -488,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)
|
||||
@@ -503,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;
|
||||
@@ -528,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 {
|
||||
@@ -553,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))
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -577,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': '月',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
36
zaimanhua.js
36
zaimanhua.js
@@ -2,7 +2,7 @@ class Zaimanhua extends ComicSource {
|
||||
// 基础信息
|
||||
name = "再漫画";
|
||||
key = "zaimanhua";
|
||||
version = "1.0.1";
|
||||
version = "1.0.2";
|
||||
minAppVersion = "1.0.0";
|
||||
url =
|
||||
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/zaimanhua.js";
|
||||
@@ -16,8 +16,31 @@ class Zaimanhua extends ComicSource {
|
||||
}
|
||||
// 构建 URL
|
||||
buildUrl(path) {
|
||||
this.signTask();
|
||||
return `https://v4api.zaimanhua.com/app/v1/${path}`;
|
||||
}
|
||||
// 每日签到
|
||||
async signTask() {
|
||||
if (!this.isLogged) {
|
||||
return;
|
||||
}
|
||||
if (!this.loadSetting("signTask")) {
|
||||
return;
|
||||
}
|
||||
const lastSign = this.loadData("lastSign");
|
||||
const newTime = new Date().toISOString().split("T")[0];
|
||||
if (lastSign == newTime) {
|
||||
return;
|
||||
}
|
||||
const res = await Network.post("https://i.zaimanhua.com/lpi/v1/task/sign_in", this.headers);
|
||||
if (res.status !== 200) {
|
||||
return;
|
||||
}
|
||||
this.saveData("lastSign", newTime);
|
||||
if (JSON.parse(res.body)["errno"] == 0) {
|
||||
UI.showMessage("签到成功");
|
||||
}
|
||||
}
|
||||
|
||||
//账户管理
|
||||
account = {
|
||||
@@ -368,7 +391,8 @@ class Zaimanhua extends ComicSource {
|
||||
},
|
||||
loadEp: async (comicId, epId) => {
|
||||
const res = await Network.get(
|
||||
this.buildUrl(`comic/chapter/${comicId}/${epId}`)
|
||||
this.buildUrl(`comic/chapter/${comicId}/${epId}`),
|
||||
this.headers
|
||||
);
|
||||
const data = JSON.parse(res.body).data.data;
|
||||
return { images: data.page_url_hd || data.page_url };
|
||||
@@ -487,4 +511,12 @@ class Zaimanhua extends ComicSource {
|
||||
return "ok";
|
||||
},
|
||||
};
|
||||
|
||||
settings = {
|
||||
signTask: {
|
||||
title: "每日签到",
|
||||
type: "switch",
|
||||
default: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user