mirror of
https://github.com/venera-app/venera-configs.git
synced 2025-09-27 08:27:24 +00:00
Compare commits
23 Commits
6e52854782
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
603fefe9be | ||
![]() |
cd941b92ef | ||
62fbe9294b | |||
91823846a0 | |||
ef87d90e89 | |||
![]() |
a991dac6d6 | ||
![]() |
c9fdc8367a | ||
![]() |
aafc7078ba | ||
![]() |
edebc0c430 | ||
![]() |
b6448c2055 | ||
![]() |
ca2f626483 | ||
![]() |
8a26cff469 | ||
![]() |
c281495cee | ||
![]() |
170eb738b9 | ||
![]() |
0d2ec4a85a | ||
![]() |
714353cf64 | ||
![]() |
65bb0d244d | ||
![]() |
2b8c532817 | ||
ee0a98ec33 | |||
ccc157b4f2 | |||
f2aacf2baa | |||
![]() |
f4bc304d1b | ||
![]() |
08756ee659 |
@@ -1,4 +1,19 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
|
||||
/**
|
||||
* @typedef {Object} PageJumpTarget
|
||||
* @Property {string} page - The page name (search, category)
|
||||
* @Property {Object} attributes - The attributes of the page
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* page: "search",
|
||||
* attributes: {
|
||||
* keyword: "example",
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
|
||||
class NewComicSource extends ComicSource {
|
||||
// Note: The fields which are marked as [Optional] should be removed if not used
|
||||
|
||||
@@ -256,20 +271,42 @@ class NewComicSource extends ComicSource {
|
||||
```
|
||||
*/
|
||||
},
|
||||
// provide options for category comic loading
|
||||
// [Optional] provide options for category comic loading
|
||||
optionList: [
|
||||
{
|
||||
// [Optional] The label will not be displayed if it is empty.
|
||||
label: "",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"newToOld-New to Old",
|
||||
"oldToNew-Old to New"
|
||||
],
|
||||
// [Optional] {string[]} - show this option only when the value not in the list
|
||||
// [Optional] {string[]} - show this option only when the category not in the list
|
||||
notShowWhen: null,
|
||||
// [Optional] {string[]} - show this option only when the value in the list
|
||||
// [Optional] {string[]} - show this option only when the category in the list
|
||||
showWhen: null
|
||||
}
|
||||
],
|
||||
/**
|
||||
* [Optional] load options dynamically. If `optionList` is provided, this will be ignored.
|
||||
* @since 1.5.0
|
||||
* @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
|
||||
*/
|
||||
optionLoader: async (category, param) => {
|
||||
return [
|
||||
{
|
||||
// [Optional] The label will not be displayed if it is empty.
|
||||
label: "",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"newToOld-New to Old",
|
||||
"oldToNew-Old to New"
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
ranking: {
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
|
71
_venera_.js
71
_venera_.js
@@ -4,6 +4,18 @@ Venera JavaScript Library
|
||||
This library provides a set of APIs for interacting with the Venera app.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @function sendMessage
|
||||
* @global
|
||||
* @param {Object} message
|
||||
* @returns {any}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set a timeout to execute a callback function after a specified delay.
|
||||
* @param callback {Function}
|
||||
* @param delay {number} - delay in milliseconds
|
||||
*/
|
||||
function setTimeout(callback, delay) {
|
||||
sendMessage({
|
||||
method: 'delay',
|
||||
@@ -42,8 +54,6 @@ let Convert = {
|
||||
/**
|
||||
* @param str {string}
|
||||
* @returns {ArrayBuffer}
|
||||
*
|
||||
* @since 1.4.3
|
||||
*/
|
||||
encodeGbk: (str) => {
|
||||
return sendMessage({
|
||||
@@ -57,8 +67,6 @@ let Convert = {
|
||||
/**
|
||||
* @param value {ArrayBuffer}
|
||||
* @returns {string}
|
||||
*
|
||||
* @since 1.4.3
|
||||
*/
|
||||
decodeGbk: (value) => {
|
||||
return sendMessage({
|
||||
@@ -1042,20 +1050,6 @@ function ImageLoadingConfig({url, method, data, headers, onResponse, modifyImage
|
||||
this.onLoadFailed = onLoadFailed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PageJumpTarget
|
||||
* @Property {string} page - The page name (search, category)
|
||||
* @Property {Object} attributes - The attributes of the page
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* page: "search",
|
||||
* attributes: {
|
||||
* keyword: "example",
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
|
||||
class ComicSource {
|
||||
name = ""
|
||||
|
||||
@@ -1404,3 +1398,44 @@ let APP = {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set clipboard text
|
||||
* @param text {string}
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* @since 1.3.4
|
||||
*/
|
||||
function setClipboard(text) {
|
||||
return sendMessage({
|
||||
method: 'setClipboard',
|
||||
text: text
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clipboard text
|
||||
* @returns {Promise<string>}
|
||||
*
|
||||
* @since 1.3.4
|
||||
*/
|
||||
function getClipboard() {
|
||||
return sendMessage({
|
||||
method: 'getClipboard'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a function with arguments. The function will be executed in the engine pool which is not in the main thread.
|
||||
* @param func {string} - A js code string which can be evaluated to a function. The function will receive the args as its only argument.
|
||||
* @param args {any[]} - The arguments to pass to the function.
|
||||
* @returns {Promise<any>} - The result of the function.
|
||||
* @since 1.5.0
|
||||
*/
|
||||
function compute(func, ...args) {
|
||||
return sendMessage({
|
||||
method: 'compute',
|
||||
function: func,
|
||||
args: args
|
||||
})
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ class Baihehui extends ComicSource {
|
||||
minAppVersion = "1.4.0"
|
||||
|
||||
// update url
|
||||
url = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/baihehui.js"
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/baihehui.js"
|
||||
|
||||
settings = {
|
||||
domains: {
|
||||
|
@@ -4,7 +4,7 @@ class Comick extends ComicSource {
|
||||
version = "1.1.1"
|
||||
minAppVersion = "1.4.0"
|
||||
// update url
|
||||
url = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/comick.js"
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/comick.js"
|
||||
|
||||
settings = {
|
||||
domains: {
|
||||
|
@@ -4,7 +4,7 @@ class CopyManga extends ComicSource {
|
||||
|
||||
key = "copy_manga"
|
||||
|
||||
version = "1.3.6"
|
||||
version = "1.3.8"
|
||||
|
||||
minAppVersion = "1.2.1"
|
||||
|
||||
@@ -12,30 +12,42 @@ class CopyManga extends ComicSource {
|
||||
|
||||
get headers() {
|
||||
let token = this.loadData("token");
|
||||
let secret = "M2FmMDg1OTAzMTEwMzJlZmUwNjYwNTUwYTA1NjNhNTM="
|
||||
|
||||
let now = new Date(Date.now());
|
||||
let year = now.getFullYear();
|
||||
let month = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
let day = now.getDate().toString().padStart(2, '0');
|
||||
let ts = Math.floor(now.getTime() / 1000).toString()
|
||||
|
||||
if (!token) {
|
||||
token = "";
|
||||
} else {
|
||||
token = " " + token;
|
||||
}
|
||||
let now = new Date(Date.now());
|
||||
let year = now.getFullYear();
|
||||
let month = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
let day = now.getDate().toString().padStart(2, '0');
|
||||
|
||||
let sig = Convert.hmacString(
|
||||
Convert.decodeBase64(secret),
|
||||
Convert.encodeUtf8(ts),
|
||||
"sha256"
|
||||
)
|
||||
|
||||
return {
|
||||
"User-Agent": "COPY/2.3.2",
|
||||
"User-Agent": "COPY/3.0.0",
|
||||
"source": "copyApp",
|
||||
"deviceinfo": this.deviceinfo,
|
||||
"dt": `${year}.${month}.${day}`,
|
||||
"platform": "3",
|
||||
"referer": `com.copymanga.app-2.3.2`,
|
||||
"version": "2.3.2",
|
||||
"referer": `com.copymanga.app-3.0.0`,
|
||||
"version": "3.0.0",
|
||||
"device": this.device,
|
||||
"pseudoid": this.pseudoid,
|
||||
"Accept": "application/json",
|
||||
"region": this.copyRegion,
|
||||
"authorization": `Token${token}`,
|
||||
"umstring": "b4c89ca4104ea9a97750314d791520ac",
|
||||
"x-auth-timestamp": ts,
|
||||
"x-auth-signature": sig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +608,7 @@ class CopyManga extends ComicSource {
|
||||
let getChapters = async (id, groups) => {
|
||||
let fetchSingle = async (id, path) => {
|
||||
let res = await Network.get(
|
||||
`${this.apiUrl}/api/v3/comic/${id}/group/${path}/chapters?limit=500&offset=0&in_mainland=true&request_id=`,
|
||||
`${this.apiUrl}/api/v3/comic/${id}/group/${path}/chapters?limit=100&offset=0&in_mainland=true&request_id=`,
|
||||
this.headers
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
@@ -610,11 +622,11 @@ class CopyManga extends ComicSource {
|
||||
eps.set(id, title);
|
||||
});
|
||||
let maxChapter = data.results.total;
|
||||
if (maxChapter > 500) {
|
||||
let offset = 500;
|
||||
if (maxChapter > 100) {
|
||||
let offset = 100;
|
||||
while (offset < maxChapter) {
|
||||
res = await Network.get(
|
||||
`${this.apiUrl}/api/v3/comic/${id}/group/${path}/chapters?limit=500&offset=${offset}`,
|
||||
`${this.apiUrl}/api/v3/comic/${id}/group/${path}/chapters?limit=100&offset=${offset}`,
|
||||
this.headers
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
@@ -626,7 +638,7 @@ class CopyManga extends ComicSource {
|
||||
let id = e.uuid;
|
||||
eps.set(id, title)
|
||||
});
|
||||
offset += 500;
|
||||
offset += 100;
|
||||
}
|
||||
}
|
||||
return eps;
|
||||
|
@@ -7,7 +7,7 @@ class Ehentai extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "ehentai"
|
||||
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
|
||||
minAppVersion = "1.0.0"
|
||||
|
||||
@@ -1182,7 +1182,7 @@ class Ehentai extends ComicSource {
|
||||
if(url.includes('?')) {
|
||||
url = url.split('?')[0]
|
||||
}
|
||||
let reg = RegExp("https?://(e-|ex)hentai.org/g/(\\d+)/(\\w+)/")
|
||||
let reg = RegExp("https?://(e-|ex)hentai.org/g/(\\d+)/(\\w+)/?$")
|
||||
let match = reg.exec(url)
|
||||
if(match) {
|
||||
return `${this.baseUrl}/g/${match[2]}/${match[3]}/`
|
||||
|
130
hitomi.js
130
hitomi.js
@@ -995,7 +995,7 @@ class Hitomi extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "hitomi";
|
||||
|
||||
version = "1.1.0";
|
||||
version = "1.1.2";
|
||||
|
||||
minAppVersion = "1.4.6";
|
||||
|
||||
@@ -1004,7 +1004,7 @@ class Hitomi extends ComicSource {
|
||||
|
||||
galleryCache = [];
|
||||
categoryResultCache = undefined;
|
||||
searchResultCache = undefined;
|
||||
searchResultCaches = new Map();
|
||||
|
||||
_mapGalleryBlockInfoToComic(n) {
|
||||
return new Comic({
|
||||
@@ -1088,95 +1088,24 @@ class Hitomi extends ComicSource {
|
||||
title: "hitomi.la",
|
||||
parts: [
|
||||
{
|
||||
name: "Language",
|
||||
name: "语言",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
{
|
||||
label: "Chinese",
|
||||
target: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: "language",
|
||||
param: "chinese",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "English",
|
||||
target: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: "language",
|
||||
param: "english",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
categories: ["汉语", "英语"],
|
||||
itemType: "category",
|
||||
categoryParams: ["language:chinese", "language:english"],
|
||||
},
|
||||
{
|
||||
name: "类别",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
{
|
||||
label: "doujinshi",
|
||||
target: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: "type",
|
||||
param: "doujinshi",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "manga",
|
||||
target: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: "type",
|
||||
param: "manga",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "artistcg",
|
||||
target: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: "type",
|
||||
param: "artistcg",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "gamecg",
|
||||
target: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: "type",
|
||||
param: "gamecg",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "imageset",
|
||||
target: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: "type",
|
||||
param: "imageset",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "anime",
|
||||
target: {
|
||||
page: "category",
|
||||
attributes: {
|
||||
category: "type",
|
||||
param: "anime",
|
||||
},
|
||||
},
|
||||
},
|
||||
categories: ["同人志", "漫画", "画师CG", "游戏CG", "图集", "动画"],
|
||||
itemType: "category",
|
||||
categoryParams: [
|
||||
"type:doujinshi",
|
||||
"type:manga",
|
||||
"type:artistcg",
|
||||
"type:gamecg",
|
||||
"type:imageset",
|
||||
"type:anime",
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -1195,9 +1124,11 @@ class Hitomi extends ComicSource {
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (category, param, options, page) => {
|
||||
const term = param;
|
||||
if (!term.includes(":"))
|
||||
throw new Error("不合法的标签,请使用namespace:tag的格式");
|
||||
if (page === 1) {
|
||||
const option = parseInt(options[0]);
|
||||
const term = category + ":" + param;
|
||||
const searchOptions = {
|
||||
term,
|
||||
orderby: "date",
|
||||
@@ -1351,6 +1282,7 @@ class Hitomi extends ComicSource {
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (keyword, options, page) => {
|
||||
const cacheKey = (keyword || "") + "|" + options.join(",");
|
||||
if (page === 1) {
|
||||
const option = parseInt(options[0]);
|
||||
const term = keyword;
|
||||
@@ -1392,11 +1324,11 @@ class Hitomi extends ComicSource {
|
||||
const comics = (await get_galleryblocks(result.gids)).map((n) =>
|
||||
this._mapGalleryBlockInfoToComic(n)
|
||||
);
|
||||
this.searchResultCache = {
|
||||
this.searchResultCaches.set(cacheKey, {
|
||||
type: "single",
|
||||
state: result.state,
|
||||
count: result.count,
|
||||
};
|
||||
});
|
||||
return {
|
||||
comics,
|
||||
maxPage: Math.ceil(result.count / 25),
|
||||
@@ -1407,20 +1339,21 @@ class Hitomi extends ComicSource {
|
||||
result.gids.slice(25 * page - 25, 25 * page)
|
||||
)
|
||||
).map((n) => this._mapGalleryBlockInfoToComic(n));
|
||||
this.searchResultCache = {
|
||||
this.searchResultCaches.set(cacheKey, {
|
||||
type: "all",
|
||||
gids: result.gids,
|
||||
count: result.count,
|
||||
};
|
||||
});
|
||||
return {
|
||||
comics,
|
||||
maxPage: Math.ceil(result.count / 25),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (this.searchResultCache.type === "single") {
|
||||
const searchResultCache = this.searchResultCaches.get(cacheKey);
|
||||
if (searchResultCache.type === "single") {
|
||||
const result = await getSingleTagSearchPage({
|
||||
state: this.searchResultCache.state,
|
||||
state: searchResultCache.state,
|
||||
page: page - 1,
|
||||
});
|
||||
const comics = (await get_galleryblocks(result.galleryids)).map((n) =>
|
||||
@@ -1428,17 +1361,17 @@ class Hitomi extends ComicSource {
|
||||
);
|
||||
return {
|
||||
comics,
|
||||
maxPage: Math.ceil(this.searchResultCache.count / 25),
|
||||
maxPage: Math.ceil(searchResultCache.count / 25),
|
||||
};
|
||||
} else {
|
||||
const comics = (
|
||||
await get_galleryblocks(
|
||||
this.searchResultCache.gids.slice(25 * page - 25, 25 * page)
|
||||
searchResultCache.gids.slice(25 * page - 25, 25 * page)
|
||||
)
|
||||
).map((n) => this._mapGalleryBlockInfoToComic(n));
|
||||
return {
|
||||
comics,
|
||||
maxPage: Math.ceil(this.searchResultCache.count / 25),
|
||||
maxPage: Math.ceil(searchResultCache.count / 25),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1523,10 +1456,11 @@ class Hitomi extends ComicSource {
|
||||
const data = await get_gallery_detail(id);
|
||||
|
||||
const tags = new Map();
|
||||
if ("type" in data) tags.set("type", [data.type]);
|
||||
if ("type" in data && data.type) tags.set("type", [data.type]);
|
||||
if (data.groups.length) tags.set("groups", data.groups);
|
||||
if (data.artists.length) tags.set("artists", data.artists);
|
||||
if ("language" in data) tags.set("language", [data.language]);
|
||||
if ("language" in data && data.language)
|
||||
tags.set("language", [data.language]);
|
||||
if (data.series.length) tags.set("series", data.series);
|
||||
if (data.characters.length) tags.set("characters", data.characters);
|
||||
if (data.females.length) tags.set("females", data.females);
|
||||
|
218
ikmmh.js
218
ikmmh.js
@@ -1,22 +1,56 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
|
||||
function getValidatorCookie(htmlString) {
|
||||
// 正则表达式匹配 document.cookie 设置语句
|
||||
const cookieRegex = /document\.cookie\s*=\s*"([^"]+)"/;
|
||||
const match = htmlString.match(cookieRegex);
|
||||
|
||||
if (!match) {
|
||||
return null; // 没有找到 cookie 设置语句
|
||||
}
|
||||
|
||||
const cookieSetting = match[1];
|
||||
const cookies = cookieSetting.split(';');
|
||||
if (cookies.length === 0) {
|
||||
return null
|
||||
}
|
||||
const nameValuePart = cookies[0].trim();
|
||||
const equalsIndex = nameValuePart.indexOf('=');
|
||||
|
||||
const name = nameValuePart.substring(0, equalsIndex);
|
||||
const value = nameValuePart.substring(equalsIndex + 1);
|
||||
|
||||
return new Cookie({ name, value, domain: "www.ikmmh.com" })
|
||||
}
|
||||
|
||||
function needPassValidator(htmlString) {
|
||||
var cookie = getValidatorCookie(htmlString)
|
||||
if (cookie != null) {
|
||||
Network.setCookies(Ikm.baseUrl, [cookie])
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
class Ikm extends ComicSource {
|
||||
// 基础配置
|
||||
name = "爱看漫";
|
||||
key = "ikmmh";
|
||||
version = "1.0.3";
|
||||
version = "1.0.5";
|
||||
minAppVersion = "1.0.0";
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/ikmmh.js";
|
||||
// 常量定义
|
||||
static baseUrl = "https://ymcdnyfqdapp.ikmmh.com";
|
||||
static Mobile_UA = "Mozilla/5.0 (Linux; Android) Mobile";
|
||||
static baseUrl = "https://www.ikmmh.com";
|
||||
static Mobile_UA = "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 Edg/140.0.0.0";
|
||||
static webHeaders = {
|
||||
"User-Agent": Ikm.Mobile_UA,
|
||||
Accept:
|
||||
"Accept":
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
};
|
||||
static jsonHead = {
|
||||
"User-Agent": Ikm.Mobile_UA,
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
Accept: "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Encoding": "gzip",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
};
|
||||
@@ -24,7 +58,7 @@ class Ikm extends ComicSource {
|
||||
static thumbConfig = (url) => ({
|
||||
headers: {
|
||||
...Ikm.webHeaders,
|
||||
Referer: Ikm.baseUrl,
|
||||
"referer": Ikm.baseUrl,
|
||||
},
|
||||
});
|
||||
// 账号系统
|
||||
@@ -38,14 +72,26 @@ class Ikm extends ComicSource {
|
||||
);
|
||||
if (res.status !== 200)
|
||||
throw new Error(`登录失败,状态码:${res.status}`);
|
||||
|
||||
if (needPassValidator(res.body)) {
|
||||
// rePost
|
||||
res = await Network.post(
|
||||
`${Ikm.baseUrl}/api/user/userarr/login`,
|
||||
Ikm.jsonHead,
|
||||
`user=${account}&pass=${pwd}`
|
||||
);
|
||||
}
|
||||
|
||||
let data = JSON.parse(res.body);
|
||||
if (data.code !== 0) throw new Error(data.msg || "登录异常");
|
||||
if (data.code !== 0)
|
||||
throw new Error(data.msg || "登录异常");
|
||||
|
||||
return "ok";
|
||||
} catch (err) {
|
||||
throw new Error(`登录失败:${err.message}`);
|
||||
}
|
||||
},
|
||||
logout: () => Network.deleteCookies("ymcdnyfqdapp.ikmmh.com"),
|
||||
logout: () => Network.deleteCookies("www.ikmmh.com"),
|
||||
registerWebsite: `${Ikm.baseUrl}/user/register/`,
|
||||
};
|
||||
// 探索页面
|
||||
@@ -58,6 +104,12 @@ class Ikm extends ComicSource {
|
||||
let res = await Network.get(`${Ikm.baseUrl}/`, Ikm.webHeaders);
|
||||
if (res.status !== 200)
|
||||
throw new Error(`加载探索页面失败,状态码:${res.status}`);
|
||||
|
||||
if (needPassValidator(res.body)) {
|
||||
// rePost
|
||||
res = await Network.get(`${Ikm.baseUrl}/`, Ikm.webHeaders);
|
||||
}
|
||||
|
||||
let document = new HtmlDocument(res.body);
|
||||
let parseComic = (e) => {
|
||||
let title = e.querySelector("div.title").text.split("~")[0];
|
||||
@@ -72,10 +124,10 @@ class Ikm extends ComicSource {
|
||||
};
|
||||
};
|
||||
return {
|
||||
本周推荐: document
|
||||
"本周推荐": document
|
||||
.querySelectorAll("div.module-good-fir > div.item")
|
||||
.map(parseComic),
|
||||
今日更新: document
|
||||
"今日更新": document
|
||||
.querySelectorAll("div.module-day-fir > div.item")
|
||||
.map(parseComic),
|
||||
};
|
||||
@@ -90,6 +142,21 @@ class Ikm extends ComicSource {
|
||||
category = {
|
||||
title: "爱看漫",
|
||||
parts: [
|
||||
{
|
||||
name: "更新",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"星期一",
|
||||
"星期二",
|
||||
"星期三",
|
||||
"星期四",
|
||||
"星期五",
|
||||
"星期六",
|
||||
"星期日",
|
||||
],
|
||||
itemType: "category",
|
||||
categoryParams: ["1", "2", "3", "4", "5", "6", "7"],
|
||||
},
|
||||
{
|
||||
name: "分类",
|
||||
// fixed 或者 random
|
||||
@@ -139,38 +206,13 @@ class Ikm extends ComicSource {
|
||||
"历史",
|
||||
"战争",
|
||||
"恐怖",
|
||||
"霸总",
|
||||
"全部",
|
||||
"连载中",
|
||||
"已完结",
|
||||
"全部",
|
||||
"日漫",
|
||||
"港台",
|
||||
"美漫",
|
||||
"国漫",
|
||||
"韩漫",
|
||||
"未分类",
|
||||
"霸总"
|
||||
],
|
||||
// category或者search
|
||||
// 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画
|
||||
// 如果为search, 将进入搜索页面
|
||||
itemType: "category",
|
||||
},
|
||||
{
|
||||
name: "更新",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"星期一",
|
||||
"星期二",
|
||||
"星期三",
|
||||
"星期四",
|
||||
"星期五",
|
||||
"星期六",
|
||||
"星期日",
|
||||
],
|
||||
itemType: "category",
|
||||
categoryParams: ["1", "2", "3", "4", "5", "6", "7"],
|
||||
},
|
||||
}
|
||||
],
|
||||
enableRankingPage: false,
|
||||
};
|
||||
@@ -186,6 +228,15 @@ class Ikm extends ComicSource {
|
||||
);
|
||||
if (res.status !== 200)
|
||||
throw new Error(`分类请求失败,状态码:${res.status}`);
|
||||
|
||||
if (needPassValidator(res.body)) {
|
||||
// rePost
|
||||
res = await Network.get(
|
||||
`${Ikm.baseUrl}/update/${param}.html`,
|
||||
Ikm.webHeaders
|
||||
);
|
||||
}
|
||||
|
||||
let document = new HtmlDocument(res.body);
|
||||
let comics = document.querySelectorAll("li.comic-item").map((e) => ({
|
||||
title: e.querySelector("p.title").text.split("~")[0],
|
||||
@@ -195,7 +246,7 @@ class Ikm extends ComicSource {
|
||||
}));
|
||||
return {
|
||||
comics,
|
||||
maxPage: 1,
|
||||
maxPage: 1
|
||||
};
|
||||
} else {
|
||||
res = await Network.post(
|
||||
@@ -205,6 +256,17 @@ class Ikm extends ComicSource {
|
||||
options[0]
|
||||
}&page=${page}`
|
||||
);
|
||||
|
||||
if (needPassValidator(res.body)) {
|
||||
// rePost
|
||||
res = await Network.post(
|
||||
`${Ikm.baseUrl}/api/comic/index/lists`,
|
||||
Ikm.jsonHead,
|
||||
`area=${options[1]}&tags=${encodeURIComponent(category)}&full=${options[0]
|
||||
}&page=${page}`
|
||||
);
|
||||
}
|
||||
|
||||
let resData = JSON.parse(res.body);
|
||||
return {
|
||||
comics: resData.data.map((e) => ({
|
||||
@@ -270,6 +332,15 @@ class Ikm extends ComicSource {
|
||||
`${Ikm.baseUrl}/search?searchkey=${encodeURIComponent(keyword)}`,
|
||||
Ikm.webHeaders
|
||||
);
|
||||
|
||||
if (needPassValidator(res.body)) {
|
||||
// rePost
|
||||
res = await Network.get(
|
||||
`${Ikm.baseUrl}/search?searchkey=${encodeURIComponent(keyword)}`,
|
||||
Ikm.webHeaders
|
||||
);
|
||||
}
|
||||
|
||||
let document = new HtmlDocument(res.body);
|
||||
return {
|
||||
comics: document.querySelectorAll("li.comic-item").map((e) => ({
|
||||
@@ -296,6 +367,12 @@ class Ikm extends ComicSource {
|
||||
if (isAdding) {
|
||||
// 获取漫画信息
|
||||
let infoRes = await Network.get(comicId, Ikm.webHeaders);
|
||||
|
||||
if (needPassValidator(infoRes.body)) {
|
||||
// rePost
|
||||
infoRes = await Network.get(comicId, Ikm.webHeaders);
|
||||
}
|
||||
|
||||
let name = new HtmlDocument(infoRes.body).querySelector(
|
||||
"meta[property='og:title']"
|
||||
).attributes["content"];
|
||||
@@ -315,6 +392,16 @@ class Ikm extends ComicSource {
|
||||
Ikm.jsonHead,
|
||||
`articleid=${id}`
|
||||
);
|
||||
|
||||
if (needPassValidator(res.body)) {
|
||||
// rePost
|
||||
res = await Network.post(
|
||||
`${Ikm.baseUrl}/api/user/bookcase/del`,
|
||||
Ikm.jsonHead,
|
||||
`articleid=${id}`
|
||||
);
|
||||
}
|
||||
|
||||
let data = JSON.parse(res.body);
|
||||
if (data.code !== "0") throw new Error(data.msg || "取消收藏失败");
|
||||
return "ok";
|
||||
@@ -332,6 +419,15 @@ class Ikm extends ComicSource {
|
||||
if (res.status !== 200) {
|
||||
throw "加载收藏失败:" + res.status;
|
||||
}
|
||||
|
||||
if (needPassValidator(res.body)) {
|
||||
// rePost
|
||||
res = await Network.get(
|
||||
`${Ikm.baseUrl}/user/bookcase`,
|
||||
Ikm.webHeaders
|
||||
);
|
||||
}
|
||||
|
||||
let document = new HtmlDocument(res.body);
|
||||
return {
|
||||
comics: document.querySelectorAll("div.bookrack-item").map((e) => ({
|
||||
@@ -348,7 +444,21 @@ class Ikm extends ComicSource {
|
||||
// 漫画详情
|
||||
comic = {
|
||||
loadInfo: async (id) => {
|
||||
// 加载收藏页并判断是否收藏
|
||||
let isFavorite = false;
|
||||
try {
|
||||
let favorites = await this.favorites.loadComics(1, null);
|
||||
isFavorite = favorites.comics.some((comic) => comic.id === id);
|
||||
} catch (error) {
|
||||
console.error("加载收藏页失败:", error);
|
||||
}
|
||||
let res = await Network.get(id, Ikm.webHeaders);
|
||||
|
||||
if (needPassValidator(res.body)) {
|
||||
// rePost
|
||||
res = await Network.get(id, Ikm.webHeaders);
|
||||
}
|
||||
|
||||
let document = new HtmlDocument(res.body);
|
||||
let comicId = id.match(/\d+/)[0];
|
||||
// 获取章节数据
|
||||
@@ -356,7 +466,7 @@ class Ikm extends ComicSource {
|
||||
`${Ikm.baseUrl}/api/comic/zyz/chapterlink?id=${comicId}`,
|
||||
{
|
||||
...Ikm.jsonHead,
|
||||
Referer: id,
|
||||
"referer": id,
|
||||
}
|
||||
);
|
||||
let epData = JSON.parse(epRes.body);
|
||||
@@ -388,29 +498,18 @@ class Ikm extends ComicSource {
|
||||
);
|
||||
let intro = desc?.[1]?.trim().replace(/\s+/g, " ") || "";
|
||||
|
||||
// 获取更新日期
|
||||
let fullDateStr = document
|
||||
.querySelector('meta[property="og:cartoon:update_time"]')
|
||||
.attributes["content"]; // "2025-07-18 08:37:02"
|
||||
let date = new Date(fullDateStr);
|
||||
let year = date.getFullYear();
|
||||
let month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始,要加1
|
||||
let day = String(date.getDate()).padStart(2, "0");
|
||||
let updateTime = `${year}-${month}-${day}`;
|
||||
|
||||
return new ComicDetails({
|
||||
return {
|
||||
title: title.split("~")[0],
|
||||
cover: thumb,
|
||||
description: intro,
|
||||
updateTime: updateTime,
|
||||
tags: {
|
||||
作者: [
|
||||
"作者": [
|
||||
document
|
||||
.querySelector("div.book-container__author")
|
||||
.text.split("作者:")[1],
|
||||
],
|
||||
最新章节: [document.querySelector("div.update > a > em").text],
|
||||
标签: document
|
||||
"更新": [document.querySelector("div.update > a > em").text],
|
||||
"标签": document
|
||||
.querySelectorAll("div.book-hero__detail > div.tags > a")
|
||||
.map((e) => e.text.trim())
|
||||
.filter((text) => text),
|
||||
@@ -423,12 +522,19 @@ class Ikm extends ComicSource {
|
||||
cover: e.querySelector("div.thumb_img").attributes["data-src"],
|
||||
id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`,
|
||||
})),
|
||||
});
|
||||
isFavorite: isFavorite,
|
||||
};
|
||||
},
|
||||
onThumbnailLoad: Ikm.thumbConfig,
|
||||
loadEp: async (comicId, epId) => {
|
||||
try {
|
||||
let res = await Network.get(epId, Ikm.webHeaders);
|
||||
|
||||
if (needPassValidator(res.body)) {
|
||||
// rePost
|
||||
res = await Network.get(epId, Ikm.webHeaders);
|
||||
}
|
||||
|
||||
let document = new HtmlDocument(res.body);
|
||||
return {
|
||||
images: document
|
||||
@@ -444,7 +550,7 @@ class Ikm extends ComicSource {
|
||||
url,
|
||||
headers: {
|
||||
...Ikm.webHeaders,
|
||||
Referer: epId,
|
||||
"referer": epId,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
31
index.json
31
index.json
@@ -3,7 +3,7 @@
|
||||
"name": "拷贝漫画",
|
||||
"fileName": "copy_manga.js",
|
||||
"key": "copy_manga",
|
||||
"version": "1.3.6"
|
||||
"version": "1.3.8"
|
||||
},
|
||||
{
|
||||
"name": "Komiic",
|
||||
@@ -21,13 +21,13 @@
|
||||
"name": "Picacg",
|
||||
"fileName": "picacg.js",
|
||||
"key": "picacg",
|
||||
"version": "1.0.3"
|
||||
"version": "1.0.5"
|
||||
},
|
||||
{
|
||||
"name": "nhentai",
|
||||
"fileName": "nhentai.js",
|
||||
"key": "nhentai",
|
||||
"version": "1.0.4"
|
||||
"version": "1.0.6"
|
||||
},
|
||||
{
|
||||
"name": "紳士漫畫",
|
||||
@@ -40,13 +40,13 @@
|
||||
"name": "ehentai",
|
||||
"fileName": "ehentai.js",
|
||||
"key": "ehentai",
|
||||
"version": "1.1.3"
|
||||
"version": "1.1.4"
|
||||
},
|
||||
{
|
||||
"name": "禁漫天堂",
|
||||
"fileName": "jm.js",
|
||||
"key": "jm",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"description": "禁漫天堂漫畫源, 不能使用時請嘗試切換分流"
|
||||
},
|
||||
{
|
||||
@@ -60,19 +60,19 @@
|
||||
"name": "爱看漫",
|
||||
"fileName": "ikmmh.js",
|
||||
"key": "ikmmh",
|
||||
"version": "1.0.3"
|
||||
"version": "1.0.5"
|
||||
},
|
||||
{
|
||||
"name": "少年ジャンプ+",
|
||||
"fileName": "shonen_jump_plus.js",
|
||||
"key": "shonen_jump_plus",
|
||||
"version": "1.0.2"
|
||||
"version": "1.1.0"
|
||||
},
|
||||
{
|
||||
"name": "hitomi.la",
|
||||
"fileName": "hitomi.js",
|
||||
"key": "hitomi",
|
||||
"version": "1.1.0"
|
||||
"version": "1.1.2"
|
||||
},
|
||||
{
|
||||
"name": "comick",
|
||||
@@ -90,23 +90,30 @@
|
||||
"name": "再漫画",
|
||||
"fileName": "zaimanhua.js",
|
||||
"key": "zaimanhua",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.1"
|
||||
},
|
||||
{
|
||||
"name": "漫画柜",
|
||||
"fileName": "manhuagui.js",
|
||||
"key": "manhuagui",
|
||||
"version": "1.0.0"
|
||||
"key": "ManHuaGui",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
{
|
||||
"name": "优酷漫画",
|
||||
"fileName": "ykmh.js",
|
||||
"key": "ykmh",
|
||||
"version": "1.0.0"
|
||||
},{
|
||||
},
|
||||
{
|
||||
"name": "漫蛙吧",
|
||||
"fileName": "manwaba.js",
|
||||
"key": "manwaba",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Lanraragi",
|
||||
"fileName": "lanraragi.js",
|
||||
"key": "lanraragi",
|
||||
"version": "1.1.0"
|
||||
}
|
||||
]
|
||||
|
132
jm.js
132
jm.js
@@ -7,22 +7,22 @@ class JM extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "jm"
|
||||
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
|
||||
minAppVersion = "1.2.5"
|
||||
minAppVersion = "1.5.0"
|
||||
|
||||
static jmVersion = "2.0.1"
|
||||
static jmVersion = "2.0.6"
|
||||
|
||||
static jmPkgName = "com.example.app"
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/jm.js"
|
||||
|
||||
static apiDomains = [
|
||||
"www.cdnaspa.vip",
|
||||
"www.cdnaspa.club",
|
||||
"www.cdnplaystation6.vip",
|
||||
"www.cdnplaystation6.cc"
|
||||
static fallbackServers = [
|
||||
"www.cdntwice.org",
|
||||
"www.cdnsha.org",
|
||||
"www.cdnaspa.cc",
|
||||
"www.cdnntr.cc",
|
||||
];
|
||||
|
||||
static imageUrl = "https://cdn-msp.jmapinodeudzn.net"
|
||||
@@ -121,11 +121,11 @@ class JM extends ComicSource {
|
||||
* @param showConfirmDialog {boolean}
|
||||
*/
|
||||
async refreshApiDomains(showConfirmDialog) {
|
||||
let url = "https://jmapp03-1308024008.cos.ap-jakarta.myqcloud.com/server-2024.txt"
|
||||
let url = "https://rup4a04-c02.tos-cn-hongkong.bytepluses.com/newsvr-2025.txt"
|
||||
let domainSecret = "diosfjckwpqpdfjkvnqQjsik"
|
||||
let title = ""
|
||||
let message = ""
|
||||
let jm3_Server = []
|
||||
let servers = []
|
||||
let domains = []
|
||||
let res = await fetch(
|
||||
url,
|
||||
@@ -134,20 +134,20 @@ class JM extends ComicSource {
|
||||
if (res.status === 200) {
|
||||
let data = this.convertData(await res.text(), domainSecret)
|
||||
let json = JSON.parse(data)
|
||||
if (json["jm3_Server"]) {
|
||||
if (json["Server"]) {
|
||||
title = "Update Success"
|
||||
message = "\n"
|
||||
jm3_Server = json["jm3_Server"]
|
||||
servers = json["Server"].slice(0, 4)
|
||||
}
|
||||
}
|
||||
if (jm3_Server.length === 0) {
|
||||
if (servers.length === 0) {
|
||||
title = "Update Failed"
|
||||
message = `Using built-in domains:\n\n`
|
||||
domains = JM.apiDomains
|
||||
servers = JM.fallbackServers
|
||||
}
|
||||
for (let [domain, index] of jm3_Server) {
|
||||
message = message + `${index}: ${domain}\n`
|
||||
domains.push(domain)
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
message = message + `線路${i + 1}: ${servers[i]}\n\n`
|
||||
domains.push(servers[i])
|
||||
}
|
||||
if (showConfirmDialog) {
|
||||
UI.showDialog(
|
||||
@@ -370,6 +370,12 @@ class JM extends ComicSource {
|
||||
/// title of the category page, used to identify the page, it should be unique
|
||||
title: "禁漫天堂",
|
||||
parts: [
|
||||
{
|
||||
name: "每週必看",
|
||||
type: "fixed",
|
||||
categories: ["每週必看"],
|
||||
itemType: "category",
|
||||
},
|
||||
{
|
||||
name: "成人A漫",
|
||||
type: "fixed",
|
||||
@@ -480,33 +486,74 @@ class JM extends ComicSource {
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (category, param, options, page) => {
|
||||
param ??= category
|
||||
param = encodeURIComponent(param)
|
||||
let res = await this.get(`${this.baseUrl}/categories/filter?o=${options[0]}&c=${param}&page=${page}`)
|
||||
let data = JSON.parse(res)
|
||||
let total = data.total
|
||||
let maxPage = Math.ceil(total / 80)
|
||||
let comics = data.content.map((e) => this.parseComic(e))
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: maxPage
|
||||
if (category !== "每週必看") {
|
||||
param ??= category
|
||||
param = encodeURIComponent(param)
|
||||
let res = await this.get(`${this.baseUrl}/categories/filter?o=${options[0]}&c=${param}&page=${page}`)
|
||||
let data = JSON.parse(res)
|
||||
let total = data.total
|
||||
let maxPage = Math.ceil(total / 80)
|
||||
let comics = data.content.map((e) => this.parseComic(e))
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: maxPage
|
||||
}
|
||||
} else {
|
||||
let res = await this.get(`${this.baseUrl}/week/filter?id=${options[0]}&page=1&type=${options[1]}&page=0`)
|
||||
let data = JSON.parse(res)
|
||||
let comics = data.list.map((e) => this.parseComic(e))
|
||||
return {
|
||||
comics: comics,
|
||||
maxPage: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
// provide options for category comic loading
|
||||
optionList: [
|
||||
{
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"mr-最新",
|
||||
"mv-總排行",
|
||||
"mv_m-月排行",
|
||||
"mv_w-周排行",
|
||||
"mv_t-日排行",
|
||||
"mp-最多圖片",
|
||||
"tf-最多喜歡",
|
||||
],
|
||||
/**
|
||||
* [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
|
||||
*/
|
||||
optionLoader: async (category, param) => {
|
||||
if (category !== "每週必看") {
|
||||
return [
|
||||
{
|
||||
label: "排序",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
"mr-最新",
|
||||
"mv-總排行",
|
||||
"mv_m-月排行",
|
||||
"mv_w-周排行",
|
||||
"mv_t-日排行",
|
||||
"mp-最多圖片",
|
||||
"tf-最多喜歡",
|
||||
],
|
||||
}
|
||||
]
|
||||
} else {
|
||||
let res = await this.get(`${this.baseUrl}/week`)
|
||||
let data = JSON.parse(res)
|
||||
let options = []
|
||||
for (let e of data["categories"]) {
|
||||
options.push(`${e["id"]}-${e["time"]}`)
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: "時間",
|
||||
options: options,
|
||||
},
|
||||
{
|
||||
label: "類型",
|
||||
options: [
|
||||
"manga-日漫",
|
||||
"hanman-韓漫",
|
||||
"another-其他",
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
ranking: {
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: [
|
||||
@@ -767,7 +814,8 @@ class JM extends ComicSource {
|
||||
}
|
||||
return {
|
||||
headers: this.getImgHeaders(),
|
||||
modifyImage: `
|
||||
// gif 图片不需要修改
|
||||
modifyImage: url.endsWith(".gif") ? null : `
|
||||
let modifyImage = (image) => {
|
||||
const num = ${num}
|
||||
let blockSize = Math.floor(image.height / num)
|
||||
|
294
lanraragi.js
Normal file
294
lanraragi.js
Normal file
@@ -0,0 +1,294 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
class Lanraragi extends ComicSource {
|
||||
name = "Lanraragi"
|
||||
key = "lanraragi"
|
||||
version = "1.1.0"
|
||||
minAppVersion = "1.4.0"
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/lanraragi.js"
|
||||
|
||||
settings = {
|
||||
api: { title: "API", type: "input", default: "http://lrr.tvc-16.science" },
|
||||
apiKey: { title: "APIKEY", type: "input", default: "" }
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
const api = this.loadSetting('api') || this.settings.api.default
|
||||
|
||||
return api.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
get headers() {
|
||||
let apiKey = this.loadSetting('apiKey')
|
||||
if (apiKey) apiKey = "Bearer " + Convert.encodeBase64(Convert.encodeUtf8(apiKey))
|
||||
|
||||
return {
|
||||
"Authorization": `${apiKey}`,
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
const url = `${this.baseUrl}/api/categories`
|
||||
const res = await Network.get(url, this.headers)
|
||||
if (res.status !== 200) { this.saveData('categories', []); return }
|
||||
let data = []
|
||||
try { data = JSON.parse(res.body) } catch (_) { data = [] }
|
||||
if (!Array.isArray(data)) data = []
|
||||
this.saveData('categories', data)
|
||||
this.saveData('categories_ts', Date.now())
|
||||
} catch (_) { this.saveData('categories', []) }
|
||||
}
|
||||
|
||||
// account = {
|
||||
// login: async (account, pwd) => {},
|
||||
// loginWithWebview: { url: "", checkStatus: (url, title) => false, onLoginSuccess: () => {} },
|
||||
// loginWithCookies: { fields: ["ipb_member_id","ipb_pass_hash","igneous","star"], validate: async (values) => false },
|
||||
// logout: () => {},
|
||||
// registerWebsite: null,
|
||||
// }
|
||||
|
||||
explore = [
|
||||
{ title: "Lanraragi", type: "multiPageComicList", load: async (page = 1) => {
|
||||
const url = `${this.baseUrl}/api/archives`
|
||||
const res = await Network.get(url, this.headers)
|
||||
if (res.status !== 200) throw `Invalid status code: ${res.status}`
|
||||
const data = JSON.parse(res.body)
|
||||
const list = data.slice((page-1)*50, page*50)
|
||||
const parseComic = (item) => {
|
||||
let base = this.baseUrl.replace(/\/$/, '')
|
||||
if (!/^https?:\/\//.test(base)) base = 'http://' + base
|
||||
const cover = `${base}/api/archives/${item.arcid}/thumbnail`
|
||||
return new Comic({ id: item.arcid, title: item.title, subTitle: '', cover, tags: item.tags ? item.tags.split(',').map(t=>t.trim()).filter(Boolean) : [], description: `页数: ${item.pagecount} | 新: ${item.isnew} | 扩展: ${item.extension}` })
|
||||
}
|
||||
return { comics: list.map(parseComic), maxPage: Math.ceil(data.length/50) }
|
||||
}}
|
||||
]
|
||||
|
||||
category = {
|
||||
title: "Lanraragi",
|
||||
parts: [ { name: "ALL", type: "dynamic", loader: () => {
|
||||
const data = this.loadData('categories')
|
||||
if (!Array.isArray(data) || data.length === 0) throw 'Please check your API settings or categories.'
|
||||
const items = []
|
||||
for (const cat of data) {
|
||||
if (!cat) continue
|
||||
const id = cat.id ?? cat._id ?? cat.name
|
||||
const label = cat.name ?? String(id)
|
||||
try { items.push({ label, target: new PageJumpTarget({ page: 'category', attributes: { category: id, param: null } }) }) }
|
||||
catch (_) { items.push({ label, target: { page: 'category', attributes: { category: id, param: null } } }) }
|
||||
}
|
||||
return items
|
||||
} } ],
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
categoryComics = {
|
||||
load: async (category, param, options, page) => {
|
||||
// Use /search endpoint filtered by category tag value
|
||||
const base = (this.baseUrl || '').replace(/\/$/, '')
|
||||
const pageSize = 100
|
||||
const start = Math.max(0, (page - 1) * pageSize)
|
||||
|
||||
const qp = []
|
||||
const add = (k, v) => qp.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
||||
add('draw', String(Date.now() % 1000))
|
||||
add('columns[0][data]', '')
|
||||
add('columns[0][name]', 'title')
|
||||
add('columns[0][searchable]', 'true')
|
||||
add('columns[0][orderable]', 'true')
|
||||
add('columns[0][search][value]', '')
|
||||
add('columns[0][search][regex]', 'false')
|
||||
add('columns[1][data]', 'tags')
|
||||
add('columns[1][name]', 'artist')
|
||||
add('columns[1][searchable]', 'true')
|
||||
add('columns[1][orderable]', 'true')
|
||||
add('columns[1][search][value]', '')
|
||||
add('columns[1][search][regex]', 'false')
|
||||
add('columns[2][data]', 'tags')
|
||||
add('columns[2][name]', 'series')
|
||||
add('columns[2][searchable]', 'true')
|
||||
add('columns[2][orderable]', 'true')
|
||||
add('columns[2][search][value]', '')
|
||||
add('columns[2][search][regex]', 'false')
|
||||
add('columns[3][data]', 'tags')
|
||||
add('columns[3][name]', 'tags')
|
||||
add('columns[3][searchable]', 'true')
|
||||
add('columns[3][orderable]', 'false')
|
||||
// Filter by category identifier in tags column
|
||||
add('columns[3][search][value]', category || '')
|
||||
add('columns[3][search][regex]', 'false')
|
||||
add('order[0][column]', '0')
|
||||
add('order[0][dir]', 'asc')
|
||||
add('start', String(start))
|
||||
add('length', String(pageSize))
|
||||
add('search[value]', '')
|
||||
add('search[regex]', 'false')
|
||||
|
||||
const url = `${base}/search?${qp.join('&')}`
|
||||
const res = await Network.get(url, this.headers)
|
||||
if (res.status !== 200) throw `Invalid status code: ${res.status}`
|
||||
const data = JSON.parse(res.body)
|
||||
const list = Array.isArray(data.data) ? data.data : []
|
||||
const comics = list.map(item => {
|
||||
const cover = `${base}/api/archives/${item.arcid}/thumbnail`
|
||||
const tags = item.tags ? item.tags.split(',').map(t => t.trim()).filter(Boolean) : []
|
||||
return new Comic({
|
||||
id: item.arcid,
|
||||
title: item.title || item.filename || item.arcid,
|
||||
subTitle: '',
|
||||
cover,
|
||||
tags,
|
||||
description: `页数: ${item.pagecount} | 新: ${item.isnew} | 扩展: ${item.extension}`
|
||||
})
|
||||
})
|
||||
|
||||
const total = typeof data.recordsFiltered === 'number' && data.recordsFiltered >= 0
|
||||
? data.recordsFiltered
|
||||
: (list.length < pageSize ? start + list.length : start + pageSize)
|
||||
const maxPage = Math.max(1, Math.ceil(total / pageSize))
|
||||
return { comics, maxPage }
|
||||
}
|
||||
}
|
||||
|
||||
search = {
|
||||
load: async (keyword, options, page = 1) => {
|
||||
const base = (this.baseUrl || '').replace(/\/$/, '')
|
||||
|
||||
// Fetch all results once (start=-1), then page locally for consistent UX across servers
|
||||
const qp = []
|
||||
const add = (k, v) => qp.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
||||
const pick = (key, def) => {
|
||||
let v = options && (options[key])
|
||||
if (typeof v === 'string') {
|
||||
const idx = v.indexOf('-');
|
||||
if (idx > 0) v = v.slice(0, idx)
|
||||
}
|
||||
return (v === undefined || v === null || v === '') ? def : v
|
||||
}
|
||||
const sortby = pick(0, 'title')
|
||||
const order = pick(1, 'asc')
|
||||
const newonly = String(pick(2, 'false'))
|
||||
const untaggedonly = String(pick(3, 'false'))
|
||||
const groupby = String(pick(4, 'true'))
|
||||
|
||||
add('filter', (keyword || '').trim())
|
||||
add('start', '-1')
|
||||
add('sortby', sortby)
|
||||
add('order', order)
|
||||
add('newonly', newonly)
|
||||
add('untaggedonly', untaggedonly)
|
||||
add('groupby_tanks', groupby)
|
||||
|
||||
const url = `${base}/api/search?${qp.join('&')}`
|
||||
const res = await Network.get(url, this.headers)
|
||||
if (res.status !== 200) throw `Invalid status code: ${res.status}`
|
||||
const data = JSON.parse(res.body)
|
||||
const all = Array.isArray(data.data) ? data.data : []
|
||||
|
||||
const pageSize = 100
|
||||
const start = Math.max(0, (page - 1) * pageSize)
|
||||
const slice = all.slice(start, start + pageSize)
|
||||
|
||||
const comics = slice.map(item => {
|
||||
const cover = `${base}/api/archives/${item.arcid}/thumbnail`
|
||||
const tags = item.tags ? item.tags.split(',').map(t => t.trim()).filter(Boolean) : []
|
||||
return new Comic({
|
||||
id: item.arcid,
|
||||
title: item.title || item.filename || item.arcid,
|
||||
subTitle: '',
|
||||
cover,
|
||||
tags,
|
||||
description: `页数: ${item.pagecount ?? ''} | 新: ${item.isnew ?? ''} | 扩展: ${item.extension ?? ''}`
|
||||
})
|
||||
})
|
||||
|
||||
const total = (typeof data.recordsFiltered === 'number' && data.recordsFiltered >= 0)
|
||||
? data.recordsFiltered
|
||||
: all.length
|
||||
const maxPage = Math.max(1, Math.ceil(total / pageSize))
|
||||
return { comics, maxPage }
|
||||
},
|
||||
loadNext: async (keyword, options, next) => {
|
||||
const page = (typeof next === 'number' && next > 0) ? next : 1
|
||||
return await this.search.load(keyword, options, page)
|
||||
},
|
||||
optionList: [
|
||||
{ type: "select", options: ["title-按标题","lastread-最近阅读"], label: "sortby", default: "title" },
|
||||
{ type: "select", options: ["asc-升序","desc-降序"], label: "order", default: "asc" },
|
||||
{ type: "select", options: ["false-全部","true-仅新"], label: "newonly", default: "false" },
|
||||
{ type: "select", options: ["false-全部","true-仅未打标签"], label: "untaggedonly", default: "false" },
|
||||
{ type: "select", options: ["true-启用","false-禁用"], label: "groupby_tanks", default: "true" }
|
||||
],
|
||||
enableTagsSuggestions: false,
|
||||
}
|
||||
|
||||
// favorites = {
|
||||
// multiFolder: false,
|
||||
// addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => {},
|
||||
// loadFolders: async (comicId) => {},
|
||||
// addFolder: async (name) => {},
|
||||
// deleteFolder: async (folderId) => {},
|
||||
// loadComics: async (page, folder) => {},
|
||||
// loadNext: async (next, folder) => {},
|
||||
// singleFolderForSingleComic: false,
|
||||
// }
|
||||
|
||||
comic = {
|
||||
loadInfo: async (id) => {
|
||||
const url = `${this.baseUrl}/api/archives/${id}/metadata`
|
||||
const res = await Network.get(url, this.headers)
|
||||
if (res.status !== 200) throw `Invalid status code: ${res.status}`
|
||||
const data = JSON.parse(res.body)
|
||||
const cover = `${this.baseUrl}/api/archives/${id}/thumbnail`
|
||||
let tags = data.tags ? data.tags.split(',').map(t=>t.trim()).filter(Boolean) : []
|
||||
const rating = tags.find(t=>t.startsWith('rating:'))
|
||||
if (rating) tags = tags.filter(t=>!t.startsWith('rating:'))
|
||||
const chapters = new Map(); chapters.set(id, data.title || 'Local manga')
|
||||
return { title: data.title || data.filename || id, cover, description: data.summary || '', tags: { "Tags": tags, "Extension": [data.extension], "Rating": rating ? [rating.replace('rating:', '')] : [], "Page": [String(data.pagecount)] }, chapters }
|
||||
},
|
||||
loadThumbnails: async (id, next) => {
|
||||
const metaUrl = `${this.baseUrl}/api/archives/${id}/metadata`
|
||||
const res = await Network.get(metaUrl, this.headers)
|
||||
if (res.status !== 200) throw `Invalid status code: ${res.status}`
|
||||
const data = JSON.parse(res.body)
|
||||
const pagecount = data.pagecount || 1
|
||||
const thumbnails = []
|
||||
for (let i = 1; i <= pagecount; i++) thumbnails.push(`${this.baseUrl}/api/archives/${id}/thumbnail?page=${i}`)
|
||||
return { thumbnails, next: null }
|
||||
},
|
||||
starRating: async (id, rating) => {},
|
||||
loadEp: async (comicId, epId) => {
|
||||
const base = (this.baseUrl || '').replace(/\/$/, '')
|
||||
const url = `${base}/api/archives/${comicId}/files?force=false`
|
||||
const res = await Network.get(url, this.headers)
|
||||
if (res.status !== 200) throw `Invalid status code: ${res.status}`
|
||||
const data = JSON.parse(res.body)
|
||||
const images = (data.pages || []).map(p => {
|
||||
if (!p) return null
|
||||
const s = String(p)
|
||||
if (/^https?:\/\//i.test(s)) return s
|
||||
return `${base}${s.startsWith('/') ? s : '/' + s}`
|
||||
}).filter(Boolean)
|
||||
return { images }
|
||||
},
|
||||
onImageLoad: (url, comicId, epId) => {
|
||||
return {
|
||||
headers: this.headers
|
||||
}
|
||||
},
|
||||
onThumbnailLoad: (url) => {
|
||||
return {
|
||||
headers: this.headers
|
||||
}
|
||||
},
|
||||
// likeComic: async (id, isLike) => {},
|
||||
// loadComments: async (comicId, subId, page, replyTo) => {},
|
||||
// sendComment: async (comicId, subId, content, replyTo) => {},
|
||||
// likeComment: async (comicId, subId, commentId, isLike) => {},
|
||||
// voteComment: async (id, subId, commentId, isUp, isCancel) => {},
|
||||
// idMatch: null,
|
||||
// onClickTag: (namespace, tag) => {},
|
||||
// link: { domains: ['example.com'], linkToId: (url) => null },
|
||||
enableTagsTranslate: false,
|
||||
}
|
||||
}
|
665
manhuagui.js
665
manhuagui.js
@@ -1,26 +1,86 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
class ManHuaGui extends ComicSource {
|
||||
// Note: The fields which are marked as [Optional] should be removed if not used
|
||||
|
||||
// name of the source
|
||||
name = "漫画柜";
|
||||
|
||||
// unique id of the source
|
||||
key = "ManHuaGui";
|
||||
|
||||
version = "1.0.0";
|
||||
version = "1.1.0";
|
||||
|
||||
minAppVersion = "1.4.0";
|
||||
|
||||
// update url
|
||||
url =
|
||||
"https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/manhuagui.js";
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/manhuagui.js";
|
||||
|
||||
baseUrl = "https://www.manhuagui.com";
|
||||
|
||||
account = {
|
||||
login: async (username, password) => {
|
||||
let headers = {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
'accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'cache-control': 'no-cache',
|
||||
'pragma': 'no-cache',
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
'origin': this.baseUrl,
|
||||
'referer': `${this.baseUrl}/`,
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
|
||||
};
|
||||
let body = `txtUserName=${encodeURIComponent(username)}&txtPassword=${encodeURIComponent(password)}`;
|
||||
let res = await Network.post(`${this.baseUrl}/tools/submit_ajax.ashx?action=user_login`, headers, body);
|
||||
if (res.status !== 200) {
|
||||
throw "Invalid status code: " + res.status;
|
||||
}
|
||||
|
||||
let setCookieHeader = res.headers['set-cookie'];
|
||||
if (!setCookieHeader) {
|
||||
throw "Set-Cookie header not found";
|
||||
}
|
||||
|
||||
let cookies = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
||||
let myCookie = null;
|
||||
|
||||
for (let cookie of cookies) {
|
||||
let match = cookie.match(/my=([^;]+)/);
|
||||
if (match) {
|
||||
myCookie = match[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!myCookie) {
|
||||
throw "my cookie not found in Set-Cookie header";
|
||||
}
|
||||
|
||||
this.saveData('mhg_cookie', "my="+myCookie);
|
||||
return "ok";
|
||||
},
|
||||
|
||||
logout: function() {
|
||||
this.deleteData('mhg_cookie');
|
||||
},
|
||||
|
||||
registerWebsite: "https://www.manhuagui.com/user/register"
|
||||
|
||||
};
|
||||
|
||||
isAppVersionAfter(target) {
|
||||
if (!APP || !APP.version) return false;
|
||||
let current = APP.version;
|
||||
let targetArr = target.split('.');
|
||||
let currentArr = current.split('.');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (parseInt(currentArr[i]) < parseInt(targetArr[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async getHtml(url) {
|
||||
let mhg_cookie = this.loadData("mhg_cookie");
|
||||
let headers = {
|
||||
accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||
"cache-control": "no-cache",
|
||||
pragma: "no-cache",
|
||||
@@ -34,9 +94,9 @@ class ManHuaGui extends ComicSource {
|
||||
"sec-fetch-site": "same-origin",
|
||||
"sec-fetch-user": "?1",
|
||||
"upgrade-insecure-requests": "1",
|
||||
cookie: "country=US",
|
||||
Referer: "https://www.manhuagui.com/",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
cookie: mhg_cookie
|
||||
};
|
||||
let res = await Network.get(url, headers);
|
||||
if (res.status !== 200) {
|
||||
@@ -46,15 +106,30 @@ class ManHuaGui extends ComicSource {
|
||||
return document;
|
||||
}
|
||||
parseSimpleComic(e) {
|
||||
let url = e.querySelector(".ell > a").attributes["href"];
|
||||
let urlElement = e.querySelector(".ell > a");
|
||||
if (!urlElement) {
|
||||
console.warn("parseSimpleComic: Missing .ell > a element");
|
||||
return null;
|
||||
}
|
||||
let url = urlElement.attributes["href"];
|
||||
let id = url.split("/")[2];
|
||||
let title = e.querySelector(".ell > a").text.trim();
|
||||
let cover = e.querySelector("img").attributes["src"];
|
||||
let title = urlElement.text.trim();
|
||||
|
||||
let imgElement = e.querySelector("img");
|
||||
if (!imgElement) {
|
||||
console.warn("parseSimpleComic: Missing img element");
|
||||
return null;
|
||||
}
|
||||
let cover = imgElement.attributes["src"] || imgElement.attributes["data-src"];
|
||||
if (!cover) {
|
||||
cover = e.querySelector("img").attributes["data-src"];
|
||||
console.warn("parseSimpleComic: Missing cover attribute");
|
||||
return null;
|
||||
}
|
||||
cover = `https:${cover}`;
|
||||
let description = e.querySelector(".tt").text.trim();
|
||||
|
||||
let descriptionElement = e.querySelector(".tt");
|
||||
let description = descriptionElement ? descriptionElement.text.trim() : "";
|
||||
|
||||
return new Comic({
|
||||
id,
|
||||
title,
|
||||
@@ -67,7 +142,6 @@ class ManHuaGui extends ComicSource {
|
||||
let simple = this.parseSimpleComic(e);
|
||||
let sl = e.querySelector(".sl");
|
||||
let status = sl ? "连载" : "完结";
|
||||
// 如果能够找到 <span class="updateon">更新于:2020-03-31<em>3.9</em></span> 解析 更新和评分
|
||||
let tmp = e.querySelector(".updateon").childNodes;
|
||||
let update = tmp[0].replace("更新于:", "").trim();
|
||||
let tags = [status, update];
|
||||
@@ -384,47 +458,42 @@ class ManHuaGui extends ComicSource {
|
||||
// explore page list
|
||||
explore = [
|
||||
{
|
||||
// title of the page.
|
||||
// title is used to identify the page, it should be unique
|
||||
title: "漫画柜",
|
||||
|
||||
/// multiPartPage or multiPageComicList or mixed
|
||||
type: "singlePageWithMultiPart",
|
||||
|
||||
type: "multiPartPage",
|
||||
/**
|
||||
* load function
|
||||
* @param page {number | null} - page number, null for `singlePageWithMultiPart` type
|
||||
* @returns {{}}
|
||||
* - for `multiPartPage` type, return [{title: string, comics: Comic[], viewMore: PageJumpTarget}]
|
||||
* - for `multiPageComicList` type, for each page(1-based), return {comics: Comic[], maxPage: number}
|
||||
* - for `mixed` type, use param `page` as index. for each index(0-based), return {data: [], maxPage: number?}, data is an array contains Comic[] or {title: string, comics: Comic[], viewMore: string?}
|
||||
* 参考 manhuagui_explore.html,抓取“热门漫画最新更新”与 tab 板块
|
||||
*/
|
||||
load: async (page) => {
|
||||
let document = await this.getHtml(this.baseUrl);
|
||||
// log("info", this.name, `获取主页成功`);
|
||||
let tabs = document.querySelectorAll("#cmt-tab li");
|
||||
// log("info", this.name, tabs);
|
||||
let parts = document.querySelectorAll("#cmt-cont ul");
|
||||
// log("info", this.name, parts);
|
||||
let result = {};
|
||||
// tabs len = parts len
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
let title = tabs[i].text.trim();
|
||||
let comics = parts[i]
|
||||
.querySelectorAll("li")
|
||||
.map((e) => this.parseSimpleComic(e));
|
||||
result[title] = comics;
|
||||
}
|
||||
// log("info", this.name, result);
|
||||
return result;
|
||||
},
|
||||
let parts = [];
|
||||
|
||||
/**
|
||||
* Only use for `multiPageComicList` type.
|
||||
* `loadNext` would be ignored if `load` function is implemented.
|
||||
* @param next {string | null} - next page token, null if first page
|
||||
* @returns {Promise<{comics: Comic[], next: string?}>} - next is null if no next page.
|
||||
*/
|
||||
// 1. 热门漫画最新更新
|
||||
let updateSection = document.querySelector(".update-cont");
|
||||
if (updateSection) {
|
||||
let updateComics = [];
|
||||
let uls = updateSection.querySelectorAll("ul");
|
||||
for (let ul of uls) {
|
||||
let comics = ul.querySelectorAll("li").map(e => this.parseSimpleComic(e)).filter(c => c);
|
||||
updateComics.push(...comics);
|
||||
}
|
||||
if (updateComics.length > 0) {
|
||||
parts.push({ title: "热门漫画最新更新", comics: updateComics });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. tab 板块(热门连载漫画、经典完结漫画、最新上架漫画、2020新番漫画)
|
||||
let tabTitles = document.querySelectorAll("#cmt-tab li");
|
||||
let tabParts = document.querySelectorAll("#cmt-cont ul.cover-list");
|
||||
for (let i = 0; i < tabTitles.length; i++) {
|
||||
let title = tabTitles[i].text.trim();
|
||||
let comics = tabParts[i].querySelectorAll("li").map(e => this.parseSimpleComic(e)).filter(c => c);
|
||||
if (comics.length > 0) {
|
||||
parts.push({ title, comics });
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
},
|
||||
loadNext(next) {},
|
||||
},
|
||||
];
|
||||
@@ -541,6 +610,7 @@ class ManHuaGui extends ComicSource {
|
||||
let genre = param;
|
||||
let age = options[1];
|
||||
let status = options[2];
|
||||
let sort = options[3] || "index";
|
||||
// log(
|
||||
// "info",
|
||||
// this.name,
|
||||
@@ -549,7 +619,7 @@ class ManHuaGui extends ComicSource {
|
||||
// 字符串之间用“_”连接,空字符串除外
|
||||
let params = [area, genre, age, status].filter((e) => e != "").join("_");
|
||||
|
||||
let url = `${this.baseUrl}/list/${params}/index_p${page}.html`;
|
||||
let url = `${this.baseUrl}/list/${params}/${sort}_p${page}.html`;
|
||||
|
||||
let document = await this.getHtml(url);
|
||||
let maxPage = document
|
||||
@@ -558,7 +628,8 @@ class ManHuaGui extends ComicSource {
|
||||
maxPage = parseInt(maxPage);
|
||||
let comics = document
|
||||
.querySelectorAll("#contList > li")
|
||||
.map((e) => this.parseSimpleComic(e));
|
||||
.map((e) => this.parseSimpleComic(e))
|
||||
.filter((comic) => comic !== null); // 过滤掉 null 值
|
||||
return {
|
||||
comics,
|
||||
maxPage,
|
||||
@@ -590,6 +661,9 @@ class ManHuaGui extends ComicSource {
|
||||
{
|
||||
options: ["-全部", "lianzai-连载", "wanjie-完结"],
|
||||
},
|
||||
{
|
||||
options: ["update-最新更新", "index-最新发布", "view-人气最旺", "rate-评分最高"],
|
||||
},
|
||||
],
|
||||
ranking: {
|
||||
// 对于单个选项,使用“-”分隔值和文本,左侧为值,右侧为文本
|
||||
@@ -624,6 +698,76 @@ class ManHuaGui extends ComicSource {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 专门解析搜索结果页面中的漫画信息
|
||||
* @param {HTMLElement} item - 搜索结果中的单个漫画项
|
||||
* @returns {Comic} - 解析后的漫画对象
|
||||
*/
|
||||
parseSearchComic(item) {
|
||||
try {
|
||||
// 获取漫画链接和ID
|
||||
let linkElement = item.querySelector(".book-detail dl dt a");
|
||||
if (!linkElement) return null;
|
||||
|
||||
let url = linkElement.attributes["href"];
|
||||
let id = url.split("/")[2];
|
||||
let title = linkElement.text.trim();
|
||||
|
||||
// 获取封面图片
|
||||
let coverElement = item.querySelector(".book-cover .bcover img");
|
||||
let cover = coverElement ? coverElement.attributes["src"] : null;
|
||||
if (cover) {
|
||||
cover = cover.startsWith("//") ? `https:${cover}` : cover;
|
||||
}
|
||||
|
||||
// 获取更新状态和描述
|
||||
let statusElement = item.querySelector(".tags.status span .red");
|
||||
let status = statusElement ? statusElement.text.trim() : "";
|
||||
|
||||
let updateElement = item.querySelector(".tags.status span .red:nth-child(2)");
|
||||
let updateTime = updateElement ? updateElement.text.trim() : "";
|
||||
|
||||
// 获取评分信息
|
||||
let scoreElement = item.querySelector(".book-score .score-avg strong");
|
||||
let score = scoreElement ? scoreElement.text.trim() : "";
|
||||
|
||||
// 获取作者信息
|
||||
let authorElements = item.querySelectorAll(".tags a[href*='/author/']");
|
||||
let author = authorElements.length > 0
|
||||
? authorElements.map(a => a.text.trim()).join(", ")
|
||||
: "";
|
||||
|
||||
// 获取类型信息
|
||||
let typeElements = item.querySelectorAll(".tags a[href*='/list/']");
|
||||
let types = typeElements.length > 0
|
||||
? typeElements.map(a => a.text.trim())
|
||||
: [];
|
||||
|
||||
// 获取简介
|
||||
let introElement = item.querySelector(".intro span");
|
||||
let description = introElement ? introElement.text.replace("简介:", "").trim() : "";
|
||||
|
||||
// 如果简介为空,使用更新状态作为描述
|
||||
if (!description && status) {
|
||||
description = `状态: ${status}`;
|
||||
if (updateTime) description += `, 更新: ${updateTime}`;
|
||||
}
|
||||
|
||||
return new Comic({
|
||||
id,
|
||||
title,
|
||||
cover,
|
||||
description,
|
||||
tags: [...types, status],
|
||||
author,
|
||||
score
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("解析搜索结果项时出错:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// search related
|
||||
search = {
|
||||
/**
|
||||
@@ -634,44 +778,63 @@ class ManHuaGui extends ComicSource {
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (keyword, options, page) => {
|
||||
let url = `${this.baseUrl}/s/${keyword}_p${page}.html`;
|
||||
let url = ""
|
||||
if (options[0]) {
|
||||
let type = options[0].split("-")[0];
|
||||
if (type == '0') {
|
||||
url = `${this.baseUrl}/s/${keyword}_p${page}.html`;
|
||||
} else{
|
||||
url = `${this.baseUrl}/s/${keyword}_o${type}_p${page}.html`;
|
||||
}
|
||||
}else{
|
||||
url = `${this.baseUrl}/s/${keyword}_p${page}.html`;
|
||||
}
|
||||
let document = await this.getHtml(url);
|
||||
let comicNum = document
|
||||
.querySelector(".result-count")
|
||||
.querySelectorAll("strong")[1].text;
|
||||
|
||||
// 检查是否有结果计数元素
|
||||
let resultCount = document.querySelector(".result-count");
|
||||
if (!resultCount) {
|
||||
// 没有搜索结果或页面结构不同
|
||||
return {
|
||||
comics: [],
|
||||
maxPage: 1
|
||||
};
|
||||
}
|
||||
|
||||
let comicNum = resultCount.querySelectorAll("strong")[1].text;
|
||||
comicNum = parseInt(comicNum);
|
||||
// 每页10个
|
||||
let maxPage = Math.ceil(comicNum / 10);
|
||||
|
||||
let bookshelf = document
|
||||
.querySelector("#contList")
|
||||
.querySelectorAll("li");
|
||||
let comics = bookshelf.map((e) => this.parseComic(e));
|
||||
// 在搜索结果页面中,漫画列表位于 .book-result ul 下
|
||||
let comicList = document.querySelector(".book-result ul");
|
||||
if (!comicList) {
|
||||
return {
|
||||
comics: [],
|
||||
maxPage: maxPage || 1
|
||||
};
|
||||
}
|
||||
|
||||
// 使用专门的搜索解析函数解析每个漫画项
|
||||
let comics = comicList.querySelectorAll("li.cf")
|
||||
.map(item => this.parseSearchComic(item))
|
||||
.filter(comic => comic !== null); // 过滤掉解析失败的项
|
||||
|
||||
return {
|
||||
comics,
|
||||
maxPage,
|
||||
};
|
||||
},
|
||||
|
||||
// provide options for search
|
||||
optionList: [
|
||||
{
|
||||
// [Optional] default is `select`
|
||||
// type: select, multi-select, dropdown
|
||||
// For select, there is only one selected value
|
||||
// For multi-select, there are multiple selected values or none. The `load` function will receive a json string which is an array of selected values
|
||||
// For dropdown, there is one selected value at most. If no selected value, the `load` function will receive a null
|
||||
type: "select",
|
||||
// For a single option, use `-` to separate the value and text, left for value, right for text
|
||||
options: ["0-time", "1-popular"],
|
||||
// option label
|
||||
options: ["0-最新更新", "1-最近最热","2-最新上架", "3-评分最高"],
|
||||
label: "sort",
|
||||
// default selected options. If not set, use the first option as default
|
||||
default: null,
|
||||
},
|
||||
],
|
||||
|
||||
// enable tags suggestions
|
||||
enableTagsSuggestions: false,
|
||||
};
|
||||
|
||||
@@ -734,22 +897,46 @@ class ManHuaGui extends ComicSource {
|
||||
let updateTime = detail_list[8].text.trim();
|
||||
|
||||
// ANCHOR 章节信息
|
||||
let chapters = new Map();
|
||||
let chapter_list = document.querySelector("#chapter-list-1");
|
||||
if (!chapter_list) {
|
||||
chapter_list = document.querySelector("#chapter-list-0");
|
||||
}
|
||||
let lis = chapter_list.querySelectorAll("li");
|
||||
for (let li of lis) {
|
||||
let a = li.querySelector("a");
|
||||
let i = a.attributes["href"].split("/").pop().replace(".html", "");
|
||||
let title = a.querySelector("span").text.trim();
|
||||
chapters.set(i, title);
|
||||
}
|
||||
// chapters 升序
|
||||
chapters = new Map([...chapters].sort((a, b) => a[0] - b[0]));
|
||||
// 支持多分组
|
||||
let chaptersMap = new Map();
|
||||
|
||||
// 查找所有章节分组标题
|
||||
let chapterGroups = document.querySelectorAll(".chapter h4 span");
|
||||
|
||||
// 处理每个分组
|
||||
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);
|
||||
}
|
||||
|
||||
groupChapters = new Map([...groupChapters].sort((a, b) => a[0] - b[0]));
|
||||
|
||||
chaptersMap.set(groupName, groupChapters);
|
||||
}
|
||||
}
|
||||
|
||||
let chapters;
|
||||
if (this.isAppVersionAfter && this.isAppVersionAfter("1.3.0")) {
|
||||
chapters = chaptersMap;
|
||||
} else {
|
||||
chapters = new Map();
|
||||
for (let [_, groupChapters] of chaptersMap) {
|
||||
for (let [id, title] of groupChapters) {
|
||||
chapters.set(id, title);
|
||||
}
|
||||
}
|
||||
chapters = new Map([...chapters].sort((a, b) => a[0] - b[0]));
|
||||
}
|
||||
|
||||
//ANCHOR - 推荐
|
||||
let recommend = [];
|
||||
let similar = document.querySelector(".similar-list");
|
||||
if (similar) {
|
||||
@@ -784,7 +971,6 @@ class ManHuaGui extends ComicSource {
|
||||
let script = document.querySelectorAll("script")[4].innerHTML;
|
||||
let infos = this.getImgInfos(script);
|
||||
|
||||
// https://us.hamreus.com/ps3/y/yiquanchaoren/第190话重制版/003.jpg.webp?e=1754143606&m=DPpelwkhr-pS3OXJpS6VkQ
|
||||
let imgDomain = `https://us.hamreus.com`;
|
||||
let images = [];
|
||||
for (let f of infos.files) {
|
||||
@@ -792,7 +978,6 @@ class ManHuaGui extends ComicSource {
|
||||
imgDomain + infos.path + f + `?e=${infos.sl.e}&m=${infos.sl.m}`;
|
||||
images.push(imgUrl);
|
||||
}
|
||||
// log("warning", this.name, images);
|
||||
return {
|
||||
images,
|
||||
};
|
||||
@@ -858,5 +1043,307 @@ class ManHuaGui extends ComicSource {
|
||||
headers,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* [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(replyTo){
|
||||
page = replyTo.split('//')[1];
|
||||
replyTo = replyTo.split('//')[0];
|
||||
}
|
||||
|
||||
let url = `${this.baseUrl}/tools/submit_ajax.ashx?action=comment_list&book_id=${comicId}&page_index=${page}`;
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
let res = await Network.get(url, headers);
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw `获取评论失败,状态码: ${res.status}`;
|
||||
}
|
||||
|
||||
let data = JSON.parse(res.body);
|
||||
|
||||
const replyChains = new Map();
|
||||
const isSubReply = new Set();
|
||||
const replyToMap = new Map();
|
||||
|
||||
if (data.commentIds && data.commentIds.length > 0) {
|
||||
for (let commentIdString of data.commentIds) {
|
||||
const commentIds = commentIdString.split(',');
|
||||
if (commentIds.length > 1) {
|
||||
const mainCommentId = commentIds[commentIds.length - 1];
|
||||
|
||||
if (!replyChains.has(mainCommentId)) {
|
||||
replyChains.set(mainCommentId, []);
|
||||
}
|
||||
|
||||
for (let i = 0; i < commentIds.length - 1; i++) {
|
||||
const replyId = commentIds[i];
|
||||
isSubReply.add(replyId); // 标记为子回复
|
||||
|
||||
if (!replyChains.get(mainCommentId).includes(replyId)) {
|
||||
replyChains.get(mainCommentId).push(replyId);
|
||||
}
|
||||
|
||||
const targetId = (i === 0) ? mainCommentId : commentIds[i + 1];
|
||||
replyToMap.set(replyId, targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const commentList = [];
|
||||
|
||||
if (data.comments) {
|
||||
if (replyTo) {
|
||||
const replies = [...(replyChains.get(replyTo) || [])].reverse();
|
||||
|
||||
for (let replyId of replies) {
|
||||
const comment = data.comments[replyId];
|
||||
if (comment) {
|
||||
const directReplyToId = replyToMap.get(replyId);
|
||||
let replyUserName = "";
|
||||
if (directReplyToId && directReplyToId !== replyTo && data.comments[directReplyToId]) {
|
||||
replyUserName = data.comments[directReplyToId].user_name || "匿名用户";
|
||||
}
|
||||
|
||||
commentList.push(new Comment({
|
||||
id: `${comment.id}//${page}`,
|
||||
userName: replyUserName ?
|
||||
`${comment.user_name || "匿名用户"} ☞ ${replyUserName}` :
|
||||
comment.user_name || "匿名用户",
|
||||
avatar: comment.avatar ? `https:${comment.avatar}` : "https://cf.mhgui.com/images/default.png",
|
||||
content: comment.content ? comment.content : "已隐藏评论",
|
||||
time: comment.add_time,
|
||||
replyCount: 0, // 回复的回复暂不支持
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const mainComments = [];
|
||||
for (const [id, comment] of Object.entries(data.comments)) {
|
||||
if (!isSubReply.has(id)) {
|
||||
const replyCount = replyChains.has(id) ? replyChains.get(id).length : (comment.reply_count || 0);
|
||||
mainComments.push(new Comment({
|
||||
id: `${comment.id}//${page}`,
|
||||
userName: comment.user_name || "匿名用户",
|
||||
avatar: comment.avatar ? `https:${comment.avatar}` : "https://cf.mhgui.com/images/default.png",
|
||||
content: comment.content ? comment.content : "已隐藏评论",
|
||||
time: comment.add_time,
|
||||
replyCount: replyCount,
|
||||
}));
|
||||
}
|
||||
}
|
||||
commentList.push(...mainComments.reverse());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
comments: commentList,
|
||||
maxPage: replyTo ? 1 : (Math.ceil(data.total / 10) || 1)
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理标签点击事件
|
||||
* @param namespace {string} 标签命名空间
|
||||
* @param tag {string} 标签名称
|
||||
* @returns {Object} 跳转操作
|
||||
*/
|
||||
onClickTag: (namespace, tag) => {
|
||||
// 点击类型标签时,跳转到对应的分类页面
|
||||
if (namespace === "类型") {
|
||||
// 根据标签查找对应的参数值
|
||||
const categoryPart = this.category.parts.find(part => part.name === "类型");
|
||||
if (categoryPart) {
|
||||
const index = categoryPart.categories.findIndex(cat => cat === tag);
|
||||
if (index !== -1) {
|
||||
const param = categoryPart.categoryParams[index];
|
||||
return {
|
||||
action: 'category',
|
||||
keyword: tag,
|
||||
param: param
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (namespace === "作者") {
|
||||
return {
|
||||
action: 'search',
|
||||
keyword: tag,
|
||||
param: tag
|
||||
};
|
||||
}
|
||||
|
||||
// 默认返回null,表示不处理此类标签点击
|
||||
return null;
|
||||
},
|
||||
};
|
||||
/// favorites related
|
||||
favorites = {
|
||||
multiFolder: false,
|
||||
/**
|
||||
* load comics of the favorites
|
||||
* @param page {number} - page number
|
||||
* @param folder {string?} - folder name, unused for now
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
loadComics: async (page, folder) => {
|
||||
let mhg_cookie = this.loadData("mhg_cookie");
|
||||
if (!mhg_cookie) {
|
||||
throw "请先登录漫画柜账号";
|
||||
}
|
||||
let url = `${this.baseUrl}/user/book/shelf/${page}`;
|
||||
let document = await this.getHtml(url);
|
||||
let comicElements = document.querySelectorAll('.dy_content_li');
|
||||
let comics = [];
|
||||
for (let el of comicElements) {
|
||||
let a = el.querySelector('.dy_img a');
|
||||
if (!a) continue;
|
||||
let href = a.attributes['href'];
|
||||
let id = href.split('/')[2];
|
||||
let img = a.querySelector('img');
|
||||
let cover = img ? (img.attributes['src'] || img.attributes['data-src']) : '';
|
||||
if (cover && !cover.startsWith('http')) cover = 'https:' + cover;
|
||||
// dy_r 解析详细信息
|
||||
let dy_r = el.querySelector('.dy_r');
|
||||
let title = '';
|
||||
let updateTitle = '';
|
||||
let updateChapter = '';
|
||||
let updateDate = '';
|
||||
let lastReadChapter = '';
|
||||
let lastReadDate = '';
|
||||
if (dy_r) {
|
||||
// 标题
|
||||
let h3 = dy_r.querySelector('h3');
|
||||
if (h3) {
|
||||
let h3a = h3.querySelector('a');
|
||||
if (h3a) title = h3a.text.trim();
|
||||
}
|
||||
// 更新内容
|
||||
let pList = dy_r.querySelectorAll('p');
|
||||
if (pList.length > 0) {
|
||||
let updateP = pList[0];
|
||||
let updateEm = updateP.querySelectorAll('em');
|
||||
if (updateEm.length > 0) {
|
||||
let chapterA = updateEm[0].querySelector('a');
|
||||
if (chapterA) updateChapter = chapterA.text.trim();
|
||||
updateDate = updateEm.length > 1 ? updateEm[1].text.trim() : '';
|
||||
}
|
||||
updateTitle = updateP.text.replace(/更新内容:/, '').trim();
|
||||
}
|
||||
// 最近阅读
|
||||
if (pList.length > 1) {
|
||||
let readP = pList[1];
|
||||
let readEm = readP.querySelectorAll('em');
|
||||
if (readEm.length > 0) {
|
||||
let lastA = readEm[0].querySelector('a');
|
||||
if (lastA) lastReadChapter = lastA.text.trim();
|
||||
lastReadDate = readEm.length > 1 ? readEm[1].text.trim() : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
// 兼容无dy_r时的title
|
||||
if (!title) {
|
||||
if (a.attributes['title']) {
|
||||
title = a.attributes['title'];
|
||||
} else {
|
||||
title = a.text.trim();
|
||||
}
|
||||
}
|
||||
// tags 信息
|
||||
let tags = [];
|
||||
if (updateChapter) tags.push(`更新:${updateChapter}`);
|
||||
if (updateDate) tags.push(`更新日期:${updateDate}`);
|
||||
if (lastReadChapter) tags.push(`最近阅读:${lastReadChapter}`);
|
||||
if (lastReadDate) tags.push(`最近阅读时间:${lastReadDate}`);
|
||||
comics.push(new Comic({
|
||||
id,
|
||||
title,
|
||||
subTitle: updateChapter || updateTitle || '',
|
||||
cover,
|
||||
description: '',
|
||||
tags,
|
||||
}));
|
||||
}
|
||||
// 页码信息
|
||||
let maxPage = 1;
|
||||
// 优先用“共N记录”计算
|
||||
let recordInfo = document.querySelector('.flickr.right span');
|
||||
if (recordInfo) {
|
||||
let match = recordInfo.text.match(/共(\d+)记录/);
|
||||
if (match) {
|
||||
let total = parseInt(match[1], 10);
|
||||
maxPage = Math.ceil(total / 20);
|
||||
}
|
||||
} else {
|
||||
// 兼容旧逻辑
|
||||
let pageBtns = document.querySelectorAll('.page-btns a');
|
||||
for (let btn of pageBtns) {
|
||||
let num = parseInt(btn.text.trim(), 10);
|
||||
if (!isNaN(num) && num > maxPage) maxPage = num;
|
||||
}
|
||||
}
|
||||
return {
|
||||
comics,
|
||||
maxPage
|
||||
};
|
||||
},
|
||||
addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => {
|
||||
if (!isAdding) {
|
||||
throw '暂不支持取消收藏';
|
||||
}
|
||||
let mhg_cookie = this.loadData("mhg_cookie");
|
||||
if (!mhg_cookie) {
|
||||
throw "请先登录漫画柜账号";
|
||||
}
|
||||
let url = `${this.baseUrl}/tools/submit_ajax.ashx?action=user_book_shelf_add`;
|
||||
let headers = {
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
'referer': `${this.baseUrl}/comic/${comicId}/`,
|
||||
cookie: mhg_cookie,
|
||||
};
|
||||
let body = `book_id=${encodeURIComponent(comicId)}`;
|
||||
let res = await Network.post(url, headers, body);
|
||||
if (res.status !== 200) {
|
||||
throw `添加收藏失败,状态码: ${res.status}`;
|
||||
}
|
||||
let data = {};
|
||||
try {
|
||||
data = JSON.parse(res.body);
|
||||
} catch (e) {}
|
||||
if (data.state !== true && data.state !== 1) {
|
||||
throw data.msg || '添加收藏失败';
|
||||
}
|
||||
return 'ok';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ class ManWaBa extends ComicSource {
|
||||
minAppVersion = "1.4.0";
|
||||
|
||||
// update url
|
||||
url = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/manwaba.js";
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/manwaba.js";
|
||||
|
||||
api = "https://www.manwaba.com/api/v1";
|
||||
|
||||
|
23
nhentai.js
23
nhentai.js
@@ -7,7 +7,7 @@ class Nhentai extends ComicSource {
|
||||
// unique id of the source
|
||||
key = "nhentai"
|
||||
|
||||
version = "1.0.4"
|
||||
version = "1.0.6"
|
||||
|
||||
minAppVersion = "1.0.0"
|
||||
|
||||
@@ -328,6 +328,25 @@ class Nhentai extends ComicSource {
|
||||
|
||||
/// single comic related
|
||||
comic = {
|
||||
/**
|
||||
* [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) => {
|
||||
if(url.startsWith("//")) {
|
||||
url = "https:" + url
|
||||
} else if(!url.startsWith("http")) {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
return {
|
||||
url: url,
|
||||
}
|
||||
},
|
||||
/**
|
||||
* load comic info
|
||||
* @param id {string}
|
||||
@@ -504,7 +523,7 @@ class Nhentai extends ComicSource {
|
||||
'nhentai.net',
|
||||
],
|
||||
linkToId: (url) => {
|
||||
let regex = /\/g\/(\d+)\//g
|
||||
let regex = /\/g\/(\d+)\/?$/g
|
||||
let match = regex.exec(url)
|
||||
if(match) {
|
||||
return match[1]
|
||||
|
101
picacg.js
101
picacg.js
@@ -3,7 +3,7 @@ class Picacg extends ComicSource {
|
||||
|
||||
key = "picacg"
|
||||
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
|
||||
minAppVersion = "1.0.0"
|
||||
|
||||
@@ -164,6 +164,99 @@ class Picacg extends ComicSource {
|
||||
comics: comics
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Picacg H24",
|
||||
type: "multiPageComicList",
|
||||
load: async (page) => {
|
||||
if (!this.isLogged) {
|
||||
throw 'Not logged in'
|
||||
}
|
||||
let res = await Network.get(
|
||||
`${this.loadSetting('base_url')}/comics/leaderboard?tt=H24&ct=VC`,
|
||||
this.buildHeaders('GET', 'comics/leaderboard?tt=H24&ct=VC', this.loadData('token'))
|
||||
)
|
||||
if (res.status === 401) {
|
||||
await this.account.reLogin()
|
||||
res = await Network.get(
|
||||
`${this.loadSetting('base_url')}/comics/leaderboard?tt=H24&ct=VC`,
|
||||
this.buildHeaders('GET', 'comics/leaderboard?tt=H24&ct=VC', this.loadData('token'))
|
||||
)
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
throw 'Invalid status code: ' + res.status
|
||||
}
|
||||
let data = JSON.parse(res.body)
|
||||
let comics = []
|
||||
data.data.comics.forEach(c => {
|
||||
comics.push(this.parseComic(c))
|
||||
})
|
||||
return {
|
||||
comics: comics
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Picacg D7",
|
||||
type: "multiPageComicList",
|
||||
load: async (page) => {
|
||||
if (!this.isLogged) {
|
||||
throw 'Not logged in'
|
||||
}
|
||||
let res = await Network.get(
|
||||
`${this.loadSetting('base_url')}/comics/leaderboard?tt=D7&ct=VC`,
|
||||
this.buildHeaders('GET', 'comics/leaderboard?tt=D7&ct=VC', this.loadData('token'))
|
||||
)
|
||||
if (res.status === 401) {
|
||||
await this.account.reLogin()
|
||||
res = await Network.get(
|
||||
`${this.loadSetting('base_url')}/comics/leaderboard?tt=D7&ct=VC`,
|
||||
this.buildHeaders('GET', 'comics/leaderboard?tt=D7&ct=VC', this.loadData('token'))
|
||||
)
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
throw 'Invalid status code: ' + res.status
|
||||
}
|
||||
let data = JSON.parse(res.body)
|
||||
let comics = []
|
||||
data.data.comics.forEach(c => {
|
||||
comics.push(this.parseComic(c))
|
||||
})
|
||||
return {
|
||||
comics: comics
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Picacg D30",
|
||||
type: "multiPageComicList",
|
||||
load: async (page) => {
|
||||
if (!this.isLogged) {
|
||||
throw 'Not logged in'
|
||||
}
|
||||
let res = await Network.get(
|
||||
`${this.loadSetting('base_url')}/comics/leaderboard?tt=D30&ct=VC`,
|
||||
this.buildHeaders('GET', 'comics/leaderboard?tt=D30&ct=VC', this.loadData('token'))
|
||||
)
|
||||
if (res.status === 401) {
|
||||
await this.account.reLogin()
|
||||
res = await Network.get(
|
||||
`${this.loadSetting('base_url')}/comics/leaderboard?tt=D30&ct=VC`,
|
||||
this.buildHeaders('GET', 'comics/leaderboard?tt=D30&ct=VC', this.loadData('token'))
|
||||
)
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
throw 'Invalid status code: ' + res.status
|
||||
}
|
||||
let data = JSON.parse(res.body)
|
||||
let comics = []
|
||||
data.data.comics.forEach(c => {
|
||||
comics.push(this.parseComic(c))
|
||||
})
|
||||
return {
|
||||
comics: comics
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -691,6 +784,9 @@ class Picacg extends ComicSource {
|
||||
'zh_CN': {
|
||||
'Picacg Random': "哔咔随机",
|
||||
'Picacg Latest': "哔咔最新",
|
||||
'Picacg H24': "哔咔日榜",
|
||||
'Picacg D7': "哔咔周榜",
|
||||
'Picacg D30': "哔咔月榜",
|
||||
'New to old': "新到旧",
|
||||
'Old to new': "旧到新",
|
||||
'Most likes': "最多喜欢",
|
||||
@@ -710,6 +806,9 @@ class Picacg extends ComicSource {
|
||||
'zh_TW': {
|
||||
'Picacg Random': "哔咔隨機",
|
||||
'Picacg Latest': "哔咔最新",
|
||||
'Picacg H24': "哔咔日榜",
|
||||
'Picacg D7': "哔咔周榜",
|
||||
'Picacg D30': "哔咔月榜",
|
||||
'New to old': "新到舊",
|
||||
'Old to new': "舊到新",
|
||||
'Most likes': "最多喜歡",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
class ShonenJumpPlus extends ComicSource {
|
||||
name = "少年ジャンプ+";
|
||||
key = "shonen_jump_plus";
|
||||
version = "1.0.2";
|
||||
version = "1.1.0";
|
||||
minAppVersion = "1.2.1";
|
||||
url =
|
||||
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/shonen_jump_plus.js";
|
||||
@@ -10,13 +10,14 @@ class ShonenJumpPlus extends ComicSource {
|
||||
bearerToken = null;
|
||||
userAccountId = null;
|
||||
tokenExpiry = 0;
|
||||
latestVersion = "4.0.21";
|
||||
|
||||
get headers() {
|
||||
return {
|
||||
"Origin": "https://shonenjumpplus.com",
|
||||
"Referer": "https://shonenjumpplus.com/",
|
||||
"X-Giga-Device-Id": this.deviceId,
|
||||
"User-Agent": "ShonenJumpPlus-Android/4.0.21",
|
||||
"User-Agent": `ShonenJumpPlus-Android/${this.latestVersion}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +31,14 @@ class ShonenJumpPlus extends ComicSource {
|
||||
return result;
|
||||
}
|
||||
|
||||
init() { }
|
||||
async init() {
|
||||
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) {
|
||||
this.latestVersion = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
explore = [
|
||||
{
|
||||
@@ -85,8 +93,9 @@ class ShonenJumpPlus extends ComicSource {
|
||||
? cover.replace("{height}", "500").replace("{width}", "500")
|
||||
: "",
|
||||
tags: [],
|
||||
description: `Ranking: ${item.rank} · Views: ${item.viewCount || "Unknown"
|
||||
}`,
|
||||
description: `Ranking: ${item.rank} · Views: ${
|
||||
item.viewCount || "Unknown"
|
||||
}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,6 +125,9 @@ class ShonenJumpPlus extends ComicSource {
|
||||
const pageInfo = response?.data?.search?.pageInfo || {};
|
||||
|
||||
const comics = edges.map(({ node }) => {
|
||||
const authors = (node.author?.name || "").split(/\s*\/\s*/).filter(
|
||||
Boolean,
|
||||
);
|
||||
const cover = node.latestIssue?.thumbnailUriTemplate ||
|
||||
node.thumbnailUriTemplate;
|
||||
if (node.__typename === "Series") {
|
||||
@@ -123,9 +135,8 @@ class ShonenJumpPlus extends ComicSource {
|
||||
id: node.databaseId,
|
||||
title: node.title || "",
|
||||
cover: this.replaceCoverUrl(cover),
|
||||
extra: {
|
||||
author: node.author?.name || "",
|
||||
},
|
||||
description: node.description || "",
|
||||
tags: authors,
|
||||
});
|
||||
}
|
||||
if (node.__typename === "MagazineLabel") {
|
||||
@@ -150,16 +161,40 @@ class ShonenJumpPlus extends ComicSource {
|
||||
loadInfo: async (id) => {
|
||||
await this.ensureAuth();
|
||||
const seriesData = await this.fetchSeriesDetail(id);
|
||||
const chapters = await this.fetchEpisodes(id);
|
||||
const episodes = await this.fetchEpisodes(id);
|
||||
|
||||
const { chapters, latestPublishAt } = episodes.reduce(
|
||||
(acc, ep) => ({
|
||||
chapters: {
|
||||
...acc.chapters,
|
||||
[ep.databaseId]: ep.title || "",
|
||||
},
|
||||
latestPublishAt:
|
||||
ep.publishedAt && ep.publishedAt > acc.latestPublishAt
|
||||
? ep.publishedAt
|
||||
: acc.latestPublishAt,
|
||||
}),
|
||||
{ chapters: {}, latestPublishAt: "" },
|
||||
);
|
||||
|
||||
const maxDate = latestPublishAt > seriesData.openAt
|
||||
? latestPublishAt
|
||||
: seriesData.openAt;
|
||||
const updateDate = new Date(new Date(maxDate) - 60 * 60 * 1000);
|
||||
const authors = (seriesData.author?.name || "").split(/\s*\/\s*/).filter(
|
||||
Boolean,
|
||||
);
|
||||
|
||||
return new ComicDetails({
|
||||
title: seriesData.title || "",
|
||||
subtitle: seriesData.author?.name || "",
|
||||
subtitle: authors.join(" / "),
|
||||
cover: this.replaceCoverUrl(seriesData.thumbnailUriTemplate),
|
||||
description: seriesData.descriptionBanner?.text || "",
|
||||
description: seriesData.description || "",
|
||||
tags: {
|
||||
"Author": [seriesData.author?.name || ""],
|
||||
"Author": authors,
|
||||
"Update": [updateDate.toISOString().slice(0, 10)],
|
||||
},
|
||||
url: `https://shonenjumpplus.com/app/episode/${seriesData.publisherId}`,
|
||||
chapters,
|
||||
});
|
||||
},
|
||||
@@ -264,11 +299,10 @@ class ShonenJumpPlus extends ComicSource {
|
||||
"SeriesDetailEpisodeList",
|
||||
{ id, episodeOffset: 0, episodeFirst: 1500, episodeSort: "NUMBER_ASC" },
|
||||
);
|
||||
const episodes = response?.data?.series?.episodes?.edges || [];
|
||||
return episodes.reduce((chapters, { node }) => ({
|
||||
...chapters,
|
||||
[node.databaseId]: node.title || "",
|
||||
}), {});
|
||||
const episodes = (response?.data?.series?.episodes?.edges || []).map(
|
||||
(edge) => edge.node
|
||||
);
|
||||
return episodes;
|
||||
}
|
||||
|
||||
async fetchEpisodePages(episodeId) {
|
||||
@@ -352,7 +386,7 @@ const GraphQLQueries = {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
... on Series { id databaseId title thumbnailUriTemplate author { name } }
|
||||
... on Series { id databaseId title thumbnailUriTemplate author { name } description }
|
||||
... on MagazineLabel { id databaseId title thumbnailUriTemplate latestIssue { thumbnailUriTemplate } }
|
||||
}
|
||||
}
|
||||
@@ -361,15 +395,18 @@ const GraphQLQueries = {
|
||||
"SeriesDetail": `query SeriesDetail($id: String!) {
|
||||
series(databaseId: $id) {
|
||||
id databaseId title thumbnailUriTemplate
|
||||
author { name } descriptionBanner { text }
|
||||
author { name }
|
||||
description
|
||||
hashtags serialUpdateScheduleLabel
|
||||
openAt
|
||||
publisherId
|
||||
}
|
||||
}`,
|
||||
"SeriesDetailEpisodeList":
|
||||
`query SeriesDetailEpisodeList($id: String!, $episodeOffset: Int, $episodeFirst: Int, $episodeSort: ReadableProductSorting) {
|
||||
series(databaseId: $id) {
|
||||
episodes: readableProducts(types: [EPISODE], first: $episodeFirst, offset: $episodeOffset, sort: $episodeSort) {
|
||||
edges { node { databaseId title } }
|
||||
edges { node { databaseId title publishedAt } }
|
||||
}
|
||||
}
|
||||
}`,
|
||||
|
2
ykmh.js
2
ykmh.js
@@ -4,7 +4,7 @@ class YKMHSource extends ComicSource {
|
||||
key = "ykmh"
|
||||
version = "1.0.0"
|
||||
minAppVersion = "1.4.0"
|
||||
url = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/ykmh.js"
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/ykmh.js"
|
||||
|
||||
get baseUrl() {
|
||||
return "https://www.ykmh.net";
|
||||
|
798
zaimanhua.js
798
zaimanhua.js
@@ -1,418 +1,490 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
class ZaiManHua extends ComicSource {
|
||||
// Note: The fields which are marked as [Optional] should be removed if not used
|
||||
|
||||
// name of the source
|
||||
class Zaimanhua extends ComicSource {
|
||||
// 基础信息
|
||||
name = "再漫画";
|
||||
|
||||
// unique id of the source
|
||||
key = "zaimanhua";
|
||||
version = "1.0.1";
|
||||
minAppVersion = "1.0.0";
|
||||
url =
|
||||
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/zaimanhua.js";
|
||||
|
||||
version = "1.0.0";
|
||||
|
||||
minAppVersion = "1.4.0";
|
||||
|
||||
// update url
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/zaimanhua.js";
|
||||
|
||||
/**
|
||||
* fetch html content
|
||||
* @param url {string}
|
||||
* @param headers {object?}
|
||||
* @returns {Promise<{document:HtmlDocument}>}
|
||||
*/
|
||||
async fetchHtml(url, headers = {}) {
|
||||
let res = await Network.get(url, headers);
|
||||
if (res.status !== 200) {
|
||||
throw "Invalid status code: " + res.status;
|
||||
}
|
||||
let document = new HtmlDocument(res.body);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch json content
|
||||
* @param url {string}
|
||||
* @param headers {object?}
|
||||
* @returns {Promise<{data:object}>}
|
||||
*/
|
||||
async fetchJson(url, headers = {}) {
|
||||
let res = await Network.get(url, headers);
|
||||
return JSON.parse(res.body).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse json content
|
||||
* @param e object
|
||||
* @returns {Comic}
|
||||
*/
|
||||
parseJsonComic(e) {
|
||||
let id = e.comic_py;
|
||||
if (!id) {
|
||||
id = id.comicPy;
|
||||
}
|
||||
let title = e?.name;
|
||||
if (!title) {
|
||||
title = e?.title;
|
||||
}
|
||||
return new Comic({
|
||||
id: id.toString(),
|
||||
title: title.toString(),
|
||||
subtitle: e?.authors,
|
||||
tags: e?.types?.split("/"),
|
||||
cover: e?.cover,
|
||||
description: e?.last_update_chapter_name.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* [Optional] init function
|
||||
*/
|
||||
// 初始化请求头
|
||||
init() {
|
||||
this.domain = "https://www.zaimanhua.com";
|
||||
this.imgBase = "https://images.zaimanhua.com";
|
||||
this.baseUrl = "https://manhua.zaimanhua.com";
|
||||
this.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android) Mobile",
|
||||
"authorization": `Bearer ${this.loadData("token") || ""}`,
|
||||
};
|
||||
}
|
||||
// 构建 URL
|
||||
buildUrl(path) {
|
||||
return `https://v4api.zaimanhua.com/app/v1/${path}`;
|
||||
}
|
||||
|
||||
// explore page list
|
||||
//账户管理
|
||||
account = {
|
||||
login: async (username, password) => {
|
||||
try {
|
||||
const encryptedPwd = Convert.hexEncode(
|
||||
Convert.md5(Convert.encodeUtf8(password))
|
||||
);
|
||||
const res = await Network.post(
|
||||
"https://account-api.zaimanhua.com/v1/login/passwd",
|
||||
{ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" },
|
||||
`username=${username}&passwd=${encryptedPwd}`
|
||||
);
|
||||
|
||||
const data = JSON.parse(res.body);
|
||||
if (data.errno !== 0) throw new Error(data.errmsg);
|
||||
|
||||
this.saveData("token", data.data.user.token);
|
||||
this.headers.authorization = `Bearer ${data.data.user.token}`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
UI.showMessage(`登录失败: ${e.message}`);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
logout: () => {
|
||||
this.deleteData("token");
|
||||
},
|
||||
};
|
||||
|
||||
// 状态检查
|
||||
checkResponseStatus(res) {
|
||||
if (res.status === 401) {
|
||||
throw new Error("登录失效");
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`请求失败: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 漫画解析
|
||||
parseComic(comic) {
|
||||
// const safeString = (value) => (value || "").toString().trim();
|
||||
const safeString = (value) => (value != null ? value.toString() : "");
|
||||
const resolveId = () =>
|
||||
[comic.comic_id, comic.id].find((id) => id && id !== "0") || "";
|
||||
const resolveTags = () =>
|
||||
[comic.status, ...safeString(comic.types).split("/")].filter(Boolean);
|
||||
const resolveDescription = () => {
|
||||
const candidates = [
|
||||
comic.description,
|
||||
comic.last_update_chapter_name,
|
||||
comic.last_name,
|
||||
];
|
||||
return candidates.find((text) => text) || "";
|
||||
};
|
||||
|
||||
return {
|
||||
id: safeString(resolveId()),
|
||||
title: comic.title || comic.name,
|
||||
subTitle: comic.authors,
|
||||
cover: comic.cover,
|
||||
tags: resolveTags(),
|
||||
description: resolveDescription(),
|
||||
};
|
||||
}
|
||||
|
||||
//探索页面
|
||||
explore = [
|
||||
{
|
||||
// title of the page.
|
||||
// title is used to identify the page, it should be unique
|
||||
title: this.name,
|
||||
|
||||
/// TODO multiPartPage
|
||||
type: "singlePageWithMultiPart",
|
||||
|
||||
/**
|
||||
* load function
|
||||
* @param page {number | null} - page number, null for `singlePageWithMultiPart` type
|
||||
* @returns {{}}
|
||||
* - for `multiPartPage` type, return [{title: string, comics: Comic[], viewMore: PageJumpTarget}]
|
||||
* - for `multiPageComicList` type, for each page(1-based), return {comics: Comic[], maxPage: number}
|
||||
* - for `mixed` type, use param `page` as index. for each index(0-based), return {data: [], maxPage: number?}, data is an array contains Comic[] or {title: string, comics: Comic[], viewMore: string?}
|
||||
*/
|
||||
title: "再漫画 更新",
|
||||
type: "multiPageComicList",
|
||||
load: async (page) => {
|
||||
let result = {};
|
||||
// https://manhua.zaimanhua.com/api/v1/comic1/recommend/list?
|
||||
// channel=pc&app_name=zmh&version=1.0.0×tamp=1753547675981&uid=0
|
||||
let api = `${this.baseUrl}/api/v1/comic1/recommend/list`;
|
||||
let params = {
|
||||
channel: "pc",
|
||||
app_name: "zmh",
|
||||
version: "1.0.0",
|
||||
timestamp: Date.now(),
|
||||
uid: 0,
|
||||
const res = await Network.get(
|
||||
this.buildUrl(`comic/update/list/0/${page}`),
|
||||
this.headers
|
||||
);
|
||||
const data = JSON.parse(res.body).data;
|
||||
return {
|
||||
comics: data.map((item) => this.parseComic(item)),
|
||||
};
|
||||
let params_str = Object.keys(params)
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join("&");
|
||||
let url = `${api}?${params_str}`;
|
||||
const json = await this.fetchJson(url);
|
||||
let data = json.list;
|
||||
data.shift(); // 去掉第一个
|
||||
data.pop(); // 去掉最后一个
|
||||
data.map((arr) => {
|
||||
let title = arr.name;
|
||||
let comic_list = arr.list.map((item) => this.parseJsonComic(item));
|
||||
result[title] = comic_list;
|
||||
});
|
||||
|
||||
log("error", "再看漫画", result);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// categories
|
||||
// categories
|
||||
static categoryParamMap = {
|
||||
"全部": "0",
|
||||
"冒险": "4",
|
||||
"欢乐向": "5",
|
||||
"格斗": "6",
|
||||
"科幻": "7",
|
||||
"爱情": "8",
|
||||
"侦探": "9",
|
||||
"竞技": "10",
|
||||
"魔法": "11",
|
||||
"神鬼": "12",
|
||||
"校园": "13",
|
||||
"惊悚": "14",
|
||||
"其他": "16",
|
||||
"四格": "17",
|
||||
"亲情": "3242",
|
||||
"百合": "3243",
|
||||
"秀吉": "3244",
|
||||
"悬疑": "3245",
|
||||
"纯爱": "3246",
|
||||
"热血": "3248",
|
||||
"泛爱": "3249",
|
||||
"历史": "3250",
|
||||
"战争": "3251",
|
||||
"萌系": "3252",
|
||||
"宅系": "3253",
|
||||
"治愈": "3254",
|
||||
"励志": "3255",
|
||||
"武侠": "3324",
|
||||
"机战": "3325",
|
||||
"音乐舞蹈": "3326",
|
||||
"美食": "3327",
|
||||
"职场": "3328",
|
||||
"西方魔幻": "3365",
|
||||
"高清单行": "4459",
|
||||
"TS": "4518",
|
||||
"东方": "5077",
|
||||
"魔幻": "5806",
|
||||
"奇幻": "5848",
|
||||
"节操": "6219",
|
||||
"轻小说": "6316",
|
||||
"颜艺": "6437",
|
||||
"搞笑": "7568",
|
||||
"仙侠": "23388",
|
||||
"舰娘": "7900",
|
||||
"动画": "13627",
|
||||
"AA": "17192",
|
||||
"福瑞": "18522",
|
||||
"生存": "23323",
|
||||
"日常": "23388",
|
||||
"画集": "30788",
|
||||
"C100": "31137",
|
||||
};
|
||||
|
||||
//分类页面
|
||||
category = {
|
||||
/// title of the category page, used to identify the page, it should be unique
|
||||
title: this.name,
|
||||
title: "再漫画",
|
||||
parts: [
|
||||
{
|
||||
name: "类型",
|
||||
name: "排行榜",
|
||||
type: "fixed",
|
||||
categories: [
|
||||
"全部",
|
||||
"冒险",
|
||||
"搞笑",
|
||||
"格斗",
|
||||
"科幻",
|
||||
"爱情",
|
||||
"侦探",
|
||||
"竞技",
|
||||
"魔法",
|
||||
"校园",
|
||||
"百合",
|
||||
"耽美",
|
||||
"历史",
|
||||
"战争",
|
||||
"宅系",
|
||||
"治愈",
|
||||
"仙侠",
|
||||
"武侠",
|
||||
"职场",
|
||||
"神鬼",
|
||||
"奇幻",
|
||||
"生活",
|
||||
"其他",
|
||||
],
|
||||
categories: ["日排行", "周排行", "月排行", "总排行"],
|
||||
itemType: "category",
|
||||
categoryParams: ["0", "1", "2", "3"],
|
||||
},
|
||||
{
|
||||
name: "分类",
|
||||
type: "fixed",
|
||||
categories: Object.keys(Zaimanhua.categoryParamMap),
|
||||
categoryParams: Object.values(Zaimanhua.categoryParamMap),
|
||||
itemType: "category",
|
||||
categoryParams: [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"11",
|
||||
"13",
|
||||
"14",
|
||||
"15",
|
||||
"16",
|
||||
"17",
|
||||
"18",
|
||||
"19",
|
||||
"20",
|
||||
"21",
|
||||
"22",
|
||||
"23",
|
||||
"24",
|
||||
],
|
||||
},
|
||||
],
|
||||
// enable ranking page
|
||||
enableRankingPage: false,
|
||||
};
|
||||
|
||||
/// category comic loading related
|
||||
//分类漫画加载
|
||||
categoryComics = {
|
||||
/**
|
||||
* load comics of a category
|
||||
* @param category {string} - category name
|
||||
* @param param {string?} - category param
|
||||
* @param options {string[]} - options from optionList
|
||||
* @param page {number} - page number
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (category, param, options, page) => {
|
||||
let fil = `${this.baseUrl}/api/v1/comic1/filter`;
|
||||
let params = {
|
||||
timestamp: Date.now(),
|
||||
sortType: 0,
|
||||
page: page,
|
||||
size: 20,
|
||||
status: options[1],
|
||||
audience: options[0],
|
||||
theme: param,
|
||||
cate: options[2],
|
||||
};
|
||||
// 拼接url
|
||||
let params_str = Object.keys(params)
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join("&");
|
||||
// log("error", "再漫画", params_str);
|
||||
let url = `${fil}?${params_str}&firstLetter`;
|
||||
// log("error", "再漫画", url);
|
||||
|
||||
const json = await this.fetchJson(url);
|
||||
let comics = json.comicList.map((e) => this.parseJsonComic(e));
|
||||
let maxPage = Math.ceil(json.totalNum / params.size);
|
||||
// log("error", "再漫画", comics);
|
||||
return {
|
||||
comics,
|
||||
maxPage,
|
||||
};
|
||||
if (category.includes("排行")) {
|
||||
let res = await Network.get(
|
||||
this.buildUrl(
|
||||
`comic/rank/list?page=${page}&rank_type=${options}&by_time=${param}`
|
||||
),
|
||||
this.headers
|
||||
);
|
||||
return {
|
||||
comics: JSON.parse(res.body).data.map((item) =>
|
||||
this.parseComic(item)
|
||||
),
|
||||
maxPage: 10,
|
||||
};
|
||||
} else {
|
||||
param = Zaimanhua.categoryParamMap[category] || "0";
|
||||
let res = await Network.get(
|
||||
this.buildUrl(
|
||||
`comic/filter/list?status=${options[2]}&theme=${param}&zone=${options[3]}&cate=${options[1]}&sortType=${options[0]}&page=${page}&size=20`
|
||||
),
|
||||
this.headers
|
||||
);
|
||||
const data = JSON.parse(res.body).data;
|
||||
return {
|
||||
comics: data.comicList.map((item) => this.parseComic(item)),
|
||||
maxPage: Math.ceil(data.totalNum / 20),
|
||||
};
|
||||
}
|
||||
},
|
||||
// provide options for category comic loading
|
||||
optionList: [
|
||||
{
|
||||
options: ["0-全部", "3262-少年", "3263-少女", "3264-青年"],
|
||||
options: ["1-更新", "2-人气"],
|
||||
notShowWhen: null,
|
||||
showWhen: Object.keys(Zaimanhua.categoryParamMap),
|
||||
},
|
||||
{
|
||||
options: ["0-全部", "1-故事漫画", "2-四格多格"],
|
||||
options: [
|
||||
"0-全部",
|
||||
"3262-少年漫画",
|
||||
"3263-少女漫画",
|
||||
"3264-青年漫画",
|
||||
"13626-女青漫画",
|
||||
],
|
||||
notShowWhen: null,
|
||||
showWhen: Object.keys(Zaimanhua.categoryParamMap),
|
||||
},
|
||||
{
|
||||
options: ["0-全部", "1-连载", "2-完结"],
|
||||
options: ["0-全部", "2309-连载中", "2310-已完结", "29205-短篇"],
|
||||
notShowWhen: null,
|
||||
showWhen: Object.keys(Zaimanhua.categoryParamMap),
|
||||
},
|
||||
{
|
||||
options: [
|
||||
"0-全部",
|
||||
"2304-日本",
|
||||
"2305-韩国",
|
||||
"2306-欧美",
|
||||
"2307-港台",
|
||||
"2308-内地",
|
||||
"8435-其他",
|
||||
],
|
||||
notShowWhen: null,
|
||||
showWhen: Object.keys(Zaimanhua.categoryParamMap),
|
||||
},
|
||||
{
|
||||
options: ["0-人气", "1-吐槽", "2-订阅"],
|
||||
notshowWhen: null,
|
||||
showWhen: ["日排行", "周排行", "月排行", "总排行"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// search related
|
||||
//搜索
|
||||
search = {
|
||||
/**
|
||||
* load search result
|
||||
* @param keyword {string}
|
||||
* @param options {string[]} - options from optionList
|
||||
* @param page {number}
|
||||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||
*/
|
||||
load: async (keyword, options, page) => {
|
||||
let url = `${this.baseUrl}/app/v1/search/index?keyword=${keyword}&source=0&page=${page}&size=20`;
|
||||
const json = await this.fetchJson(url);
|
||||
let comics = json.comicList.map((e) => this.parseJsonComic(e));
|
||||
let maxPage = Math.ceil(json.totalNum / params.size);
|
||||
// log("error", "再漫画", comics);
|
||||
const res = await Network.get(
|
||||
this.buildUrl(
|
||||
`search/index?keyword=${encodeURIComponent(
|
||||
keyword
|
||||
)}&page=${page}&sort=0&size=20`
|
||||
),
|
||||
this.headers
|
||||
);
|
||||
const data = JSON.parse(res.body).data.list;
|
||||
return {
|
||||
comics,
|
||||
maxPage,
|
||||
comics: data.map((item) => this.parseComic(item)),
|
||||
};
|
||||
},
|
||||
|
||||
// provide options for search
|
||||
optionList: [],
|
||||
};
|
||||
|
||||
/// single comic related
|
||||
//收藏
|
||||
favorites = {
|
||||
multiFolder: false,
|
||||
addOrDelFavorite: async (comicId, folderId, isAdding) => {
|
||||
const path = isAdding ? "add" : "del";
|
||||
const res = await Network.get(
|
||||
this.buildUrl(`comic/sub/${path}?comic_id=${comicId}`),
|
||||
this.headers
|
||||
);
|
||||
const data = JSON.parse(res.body);
|
||||
if (data.errno !== 0) {
|
||||
throw new Error(data.errmsg || "操作失败");
|
||||
}
|
||||
return "ok";
|
||||
},
|
||||
loadComics: async (page) => {
|
||||
try {
|
||||
const res = await Network.get(
|
||||
this.buildUrl(`comic/sub/list?status=0&page=${page}&size=20`),
|
||||
this.headers
|
||||
);
|
||||
const data = JSON.parse(res.body).data;
|
||||
return {
|
||||
comics: data.subList.map((item) => this.parseComic(item)) ?? [],
|
||||
maxPage: Math.ceil(data.total / 20),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("加载收藏失败:", e);
|
||||
return { comics: [], maxPage: null };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 时间戳转换
|
||||
formatTimestamp(ts) {
|
||||
const date = new Date(ts * 1000);
|
||||
return date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
//漫画详情
|
||||
comic = {
|
||||
/**
|
||||
* load comic info
|
||||
* @param id {string}
|
||||
* @returns {Promise<ComicDetails>}
|
||||
*/
|
||||
loadInfo: async (id) => {
|
||||
const api = `${this.domain}/api/v1/comic1/comic/detail`;
|
||||
let params = {
|
||||
channel: "pc",
|
||||
app_name: "zmh",
|
||||
version: "1.0.0",
|
||||
timestamp: Date.now(),
|
||||
uid: 0,
|
||||
comic_py: id,
|
||||
const getFavoriteStatus = async (id) => {
|
||||
let res = await Network.get(
|
||||
this.buildUrl(`comic/sub/checkIsSub?objId=${id}&source=1`),
|
||||
this.headers
|
||||
);
|
||||
this.checkResponseStatus(res);
|
||||
return JSON.parse(res.body).data.isSub;
|
||||
};
|
||||
let params_str = Object.keys(params)
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join("&");
|
||||
let url = `${api}?${params_str}`;
|
||||
const json = await this.fetchJson(url);
|
||||
const info = json.comicInfo;
|
||||
const comic_id = info.id;
|
||||
let title = info.title;
|
||||
let author = info.authorInfo.authorName;
|
||||
let results = await Promise.all([
|
||||
Network.get(
|
||||
this.buildUrl(`comic/detail/${id}?channel=android`),
|
||||
this.headers
|
||||
),
|
||||
getFavoriteStatus.bind(this)(id),
|
||||
]);
|
||||
const response = JSON.parse(results[0].body);
|
||||
if (response.errno !== 0) throw new Error(response.errmsg || "加载失败");
|
||||
const data = response.data.data;
|
||||
|
||||
// 修复时间戳转换问题
|
||||
let lastUpdateTime = new Date(info.lastUpdateTime * 1000);
|
||||
let updateTime = `${lastUpdateTime.getFullYear()}-${
|
||||
lastUpdateTime.getMonth() + 1
|
||||
}-${lastUpdateTime.getDate()}`;
|
||||
|
||||
let description = info.description;
|
||||
let cover = info.cover;
|
||||
|
||||
let chapters = new Map();
|
||||
info.chapterList[0].data.forEach((e) => {
|
||||
chapters.set(e.chapter_id.toString(), e.chapter_title);
|
||||
});
|
||||
// chapters 按照key排序
|
||||
let chaptersSorted = new Map([...chapters].sort((a, b) => a[0] - b[0]));
|
||||
|
||||
// 获取推荐漫画
|
||||
const api2 = `${this.baseUrl}/api/v1/comic1/comic/same_list`;
|
||||
let params2 = {
|
||||
channel: "pc",
|
||||
app_name: "zmh",
|
||||
version: "1.0.0",
|
||||
timestamp: Date.now(),
|
||||
uid: 0,
|
||||
comic_id: comic_id,
|
||||
};
|
||||
let params2_str = Object.keys(params2)
|
||||
.map((key) => `${key}=${params2[key]}`)
|
||||
.join("&");
|
||||
let url2 = `${api2}?${params2_str}`;
|
||||
const json2 = await this.fetchJson(url2);
|
||||
let recommend = json2.data.comicList.map((e) => this.parseJsonComic(e));
|
||||
let tags = {
|
||||
状态: [info.status],
|
||||
类型: [info.readerGroup, ...info.types.split("/")],
|
||||
点击: [info.hitNumStr.toString()],
|
||||
订阅: [info.subNumStr],
|
||||
};
|
||||
|
||||
return new ComicDetails({
|
||||
title,
|
||||
subtitle: author,
|
||||
cover,
|
||||
description,
|
||||
tags,
|
||||
chapters: chaptersSorted,
|
||||
recommend,
|
||||
updateTime,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* load images of a chapter
|
||||
* @param comicId {string}
|
||||
* @param epId {string?}
|
||||
* @returns {Promise<{images: string[]}>}
|
||||
*/
|
||||
loadEp: async (comicId, epId) => {
|
||||
const api_ = `${this.domain}/api/v1/comic1/comic/detail`;
|
||||
// log("error", "再漫画", id);
|
||||
let params_ = {
|
||||
channel: "pc",
|
||||
app_name: "zmh",
|
||||
version: "1.0.0",
|
||||
timestamp: Date.now(),
|
||||
uid: 0,
|
||||
comic_py: comicId,
|
||||
};
|
||||
let params_str_ = Object.keys(params_)
|
||||
.map((key) => `${key}=${params_[key]}`)
|
||||
.join("&");
|
||||
let url_ = `${api_}?${params_str_}`;
|
||||
const json_ = await this.fetchJson(url_);
|
||||
const info_ = json_.comicInfo;
|
||||
const comic_id = info_.id;
|
||||
|
||||
const api = `${this.baseUrl}/api/v1/comic1/chapter/detail`;
|
||||
// comic_id=18114&chapter_id=36227
|
||||
let params = {
|
||||
channel: "pc",
|
||||
app_name: "zmh",
|
||||
version: "1.0.0",
|
||||
timestamp: Date.now(),
|
||||
uid: 0,
|
||||
comic_id: comic_id,
|
||||
chapter_id: epId,
|
||||
};
|
||||
let params_str = Object.keys(params)
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join("&");
|
||||
let url = `${api}?${params_str}`;
|
||||
const json = await this.fetchJson(url);
|
||||
const info = json.chapterInfo;
|
||||
function processChapters(groups) {
|
||||
return (groups || []).reduce((result, group) => {
|
||||
const groupTitle = group.title || "默认";
|
||||
const chapters = (group.data || [])
|
||||
.reverse()
|
||||
.map((ch) => [
|
||||
String(ch.chapter_id),
|
||||
`${ch.chapter_title.replace(
|
||||
/^(?:连载版?)?(\d+\.?\d*)([话卷])?$/,
|
||||
(_, n, t) => `第${n}${t || "话"}`
|
||||
)}`,
|
||||
]);
|
||||
result.set(groupTitle, new Map(chapters));
|
||||
return result;
|
||||
}, new Map());
|
||||
}
|
||||
// 分类标签
|
||||
const { authors, status, types } = data;
|
||||
const tagMapper = (arr) => arr.map((t) => t.tag_name);
|
||||
return {
|
||||
images: info.page_url,
|
||||
title: data.title,
|
||||
cover: data.cover,
|
||||
description: data.description,
|
||||
tags: {
|
||||
"作者": tagMapper(authors),
|
||||
"状态": [...tagMapper(status), data.last_update_chapter_name],
|
||||
"标签": tagMapper(types),
|
||||
},
|
||||
updateTime: this.formatTimestamp(data.last_updatetime),
|
||||
chapters: processChapters(data.chapters),
|
||||
isFavorite: results[1],
|
||||
subId: id,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* [Optional] provide configs for an image loading
|
||||
* @param url
|
||||
* @param comicId
|
||||
* @param epId
|
||||
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>}
|
||||
*/
|
||||
onImageLoad: (url, comicId, epId) => {
|
||||
return {};
|
||||
loadEp: async (comicId, epId) => {
|
||||
const res = await Network.get(
|
||||
this.buildUrl(`comic/chapter/${comicId}/${epId}`)
|
||||
);
|
||||
const data = JSON.parse(res.body).data.data;
|
||||
return { images: data.page_url_hd || data.page_url };
|
||||
},
|
||||
/**
|
||||
* [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 {};
|
||||
|
||||
loadComments: async (comicId, subId, page, replyTo) => {
|
||||
try {
|
||||
// 构建请求URL
|
||||
const url = this.buildUrl(
|
||||
`comment/list?page=${page}&size=30&type=4&objId=${
|
||||
subId || comicId
|
||||
}&sortBy=1`
|
||||
);
|
||||
const res = await Network.get(url, this.headers);
|
||||
this.checkResponseStatus(res);
|
||||
|
||||
const response = JSON.parse(res.body);
|
||||
const data = response.data;
|
||||
|
||||
/* 空数据检查 */
|
||||
if (!data || !data.commentIdList || !data.commentList) {
|
||||
UI.showMessage("暂时没有评论,快来发表第一条吧~");
|
||||
return { comments: [], maxPage: 0 };
|
||||
}
|
||||
|
||||
/* 处理评论ID列表 */
|
||||
// 标准化ID数组:处理null/字符串/数组等多种情况
|
||||
const rawIds = Array.isArray(data.commentIdList)
|
||||
? data.commentIdList
|
||||
: [];
|
||||
|
||||
// 展开所有ID并过滤无效值
|
||||
const allCommentIds = rawIds
|
||||
.map((idStr) => `${idStr || ""}`.split(",")) // 转换为字符串再分割
|
||||
.flat()
|
||||
.filter((id) => id.trim() !== "");
|
||||
|
||||
// 最终ID处理流程
|
||||
const processComments = () => {
|
||||
// 去重并验证ID有效性
|
||||
const validIds = [...new Set(allCommentIds)].filter((id) =>
|
||||
data.commentList.hasOwnProperty(id)
|
||||
);
|
||||
|
||||
// 过滤回复评论
|
||||
const filteredIds = replyTo
|
||||
? validIds.filter(
|
||||
(id) => data.commentList[id]?.to_comment_id == replyTo
|
||||
)
|
||||
: validIds;
|
||||
|
||||
// 转换为评论对象
|
||||
return filteredIds.map((id) => {
|
||||
const comment = data.commentList[id];
|
||||
return new Comment({
|
||||
userName: comment.nickname || "匿名用户",
|
||||
avatar: comment.photo || "",
|
||||
content: comment.content || "[内容已删除]",
|
||||
time: this.formatTimestamp(comment.create_time),
|
||||
replyCount: comment.reply_amount || 0,
|
||||
score: comment.like_amount || 0,
|
||||
id: String(id),
|
||||
parentId: comment.to_comment_id || null,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 当没有有效评论时显示提示
|
||||
const comments = processComments();
|
||||
if (comments.length === 0) {
|
||||
UI.showMessage(replyTo ? "该评论暂无回复" : "这里还没有评论哦~");
|
||||
}
|
||||
|
||||
return {
|
||||
comments: comments,
|
||||
maxPage: Math.ceil((data.total || 0) / 30),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("评论加载失败:", e);
|
||||
UI.showMessage(`加载评论失败: ${e.message}`);
|
||||
return { comments: [], maxPage: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
// 发送评论, 返回任意值表示成功.
|
||||
sendComment: async (comicId, subId, content, replyTo) => {
|
||||
if (!replyTo) {
|
||||
replyTo = 0;
|
||||
}
|
||||
let res = await Network.post(
|
||||
this.buildUrl(`comment/add`),
|
||||
{
|
||||
...this.headers,
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
},
|
||||
`obj_id=${subId}&content=${encodeURIComponent(
|
||||
content
|
||||
)}&to_comment_id=${replyTo}&type=4`
|
||||
);
|
||||
this.checkResponseStatus(res);
|
||||
let response = JSON.parse(res.body);
|
||||
if (response.errno !== 0) throw new Error(response.errmsg || "加载失败");
|
||||
return "ok";
|
||||
},
|
||||
// 点赞
|
||||
likeComment: async (comicId, subId, commentId, isLike) => {
|
||||
let res = await Network.post(
|
||||
this.buildUrl(`comment/addLike`),
|
||||
{
|
||||
...this.headers,
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
},
|
||||
`commentId=${commentId}&type=4`
|
||||
);
|
||||
this.checkResponseStatus(res);
|
||||
return "ok";
|
||||
},
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user