Compare commits

..

2 Commits

Author SHA1 Message Date
Brooklyn Bartly
6e52854782 Manwaba (#118)
* feat: 添加漫蛙漫画源实现基础功能

* refactor(api): 重构网络请求和数据处理逻辑

- 将 `fetchJson` 拆分为 `getJson` 和 `postJson` 方法,增强类型安全
- 更新基础 URL 移除尾部斜杠
- 实现分类、搜索和详情页的实际 API 调用
- 完善漫画状态显示和分类选项
- 移除冗余的 `initFunc` 方法

* refactor: 移除未实现的收藏相关功能代码

清理未实现的收藏功能相关代码,包括添加/删除收藏、加载收藏夹等功能,以保持代码库整洁

* refactor(manwaba): 重构API响应处理逻辑并实现loadInfo方法

- 修改getJson方法直接返回完整JSON响应,不再处理特定code和data字段
- 重构分类列表和搜索结果的data字段处理逻辑
- 实现loadInfo方法获取漫画详情信息

* refactor(api): 重构API请求方法并更新基础URL

- 将多个独立的请求方法合并为统一的fetchJson方法
- 更新基础URL为新的API端点
- 简化参数处理和请求逻辑
- 移除不再使用的工具方法

* fix: 将漫画ID转换为字符串类型以避免潜在的类型错误

* refactor: 将baseUrl重命名为api以提升代码可读性

统一将baseUrl变量名改为api,使其更符合实际用途,提高代码可读性和一致性

* refactor(ManWaBa): 优化fetchJson默认参数并添加日志功能

- 为fetchJson方法的payload参数添加默认值undefined
- 新增logger对象提供error/info/warn日志方法
- 在loadInfo方法中添加日志记录
- 移除未使用的可选方法以简化代码结构

* fix: 修复fetchJson调用时payload参数未定义的问题

确保在调用fetchJson时明确传递payload为undefined,避免潜在的类型错误

* refactor(ManWaBa): 优化漫画信息加载和章节图片获取逻辑

重构漫画信息加载和章节图片获取的代码,提取重复参数为变量,简化请求逻辑
移除未使用的onImageLoad和onThumbnailLoad方法,集中处理图片获取功能

* feat: 添加漫蛙吧源到index.json
2025-07-28 17:54:39 +08:00
Pacalini
b5ba37794a jm: update jm3 api (#117) 2025-07-28 17:54:13 +08:00
3 changed files with 464 additions and 47 deletions

View File

@@ -46,7 +46,7 @@
"name": "禁漫天堂", "name": "禁漫天堂",
"fileName": "jm.js", "fileName": "jm.js",
"key": "jm", "key": "jm",
"version": "1.1.4", "version": "1.2.0",
"description": "禁漫天堂漫畫源, 不能使用時請嘗試切換分流" "description": "禁漫天堂漫畫源, 不能使用時請嘗試切換分流"
}, },
{ {
@@ -103,5 +103,10 @@
"fileName": "ykmh.js", "fileName": "ykmh.js",
"key": "ykmh", "key": "ykmh",
"version": "1.0.0" "version": "1.0.0"
},{
"name": "漫蛙吧",
"fileName": "manwaba.js",
"key": "manwaba",
"version": "1.0.0"
} }
] ]

117
jm.js
View File

@@ -7,25 +7,31 @@ class JM extends ComicSource {
// unique id of the source // unique id of the source
key = "jm" key = "jm"
version = "1.1.4" version = "1.2.0"
minAppVersion = "1.2.5" minAppVersion = "1.2.5"
static jmVersion = "2.0.1"
static jmPkgName = "com.example.app"
// update url // update url
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/jm.js" url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/jm.js"
static apiDomains = [ static apiDomains = [
"www.jmapiproxyxxx.vip", "www.cdnaspa.vip",
"www.cdnblackmyth.club", "www.cdnaspa.club",
"www.cdnmhws.cc", "www.cdnplaystation6.vip",
"www.cdnmhwscc.org" "www.cdnplaystation6.cc"
]; ];
static imageUrl = "https://cdn-msp.jmapinodeudzn.net" static imageUrl = "https://cdn-msp.jmapinodeudzn.net"
static apiUa = "Mozilla/5.0 (Linux; Android 10; K; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.0.0 Mobile Safari/537.36" static ua = "Mozilla/5.0 (Linux; Android 10; K; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.0.0 Mobile Safari/537.36"
static imgUa = "okhttp/3.12.1" get ua() {
return JM.ua;
}
get baseUrl() { get baseUrl() {
let index = parseInt(this.loadSetting('apiDomain')) - 1 let index = parseInt(this.loadSetting('apiDomain')) - 1
@@ -48,12 +54,49 @@ class JM extends ComicSource {
return /^\d+$/.test(str) return /^\d+$/.test(str)
} }
get apiUa() { get baseHeaders() {
return JM.apiUa; return {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"Connection": "keep-alive",
"Origin": "https://localhost",
"Referer": "https://localhost/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"X-Requested-With": JM.jmPkgName,
}
} }
get imgUa() { getApiHeaders(time) {
return JM.imgUa; const jmAuthKey = "18comicAPPContent"
let token = Convert.md5(Convert.encodeUtf8(`${time}${jmAuthKey}`))
return {
...this.baseHeaders,
"Authorization": "Bearer",
"Sec-Fetch-Storage-Access": "active",
"token": Convert.hexEncode(token),
"tokenparam": `${time},${JM.jmVersion}`,
"User-Agent": this.ua,
}
}
getImgHeaders() {
return {
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"Connection": "keep-alive",
"Referer": "https://localhost/",
"Sec-Fetch-Dest": "image",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Storage-Access": "active",
"User-Agent": this.ua,
"X-Requested-With": JM.jmPkgName,
}
} }
getCoverUrl(id) { getCoverUrl(id) {
@@ -78,32 +121,33 @@ class JM extends ComicSource {
* @param showConfirmDialog {boolean} * @param showConfirmDialog {boolean}
*/ */
async refreshApiDomains(showConfirmDialog) { async refreshApiDomains(showConfirmDialog) {
let today = new Date(); let url = "https://jmapp03-1308024008.cos.ap-jakarta.myqcloud.com/server-2024.txt"
let url = "https://jmappc01-1308024008.cos.ap-guangzhou.myqcloud.com/server-2024.txt"
let domainSecret = "diosfjckwpqpdfjkvnqQjsik" let domainSecret = "diosfjckwpqpdfjkvnqQjsik"
let title = "" let title = ""
let message = "" let message = ""
let jm3_Server = []
let domains = [] let domains = []
let res = await fetch( let res = await fetch(
`${url}?time=${today.getFullYear()}${today.getMonth() + 1}${today.getDate()}`, url,
{headers: {"User-Agent": this.imgUa}} {headers: this.baseHeaders}
) )
if (res.status === 200) { if (res.status === 200) {
let data = this.convertData(await res.text(), domainSecret) let data = this.convertData(await res.text(), domainSecret)
let json = JSON.parse(data) let json = JSON.parse(data)
if (json["Server"]) { if (json["jm3_Server"]) {
title = "Update Success" title = "Update Success"
message = "New domains:\n\n" message = "\n"
domains = json["Server"] jm3_Server = json["jm3_Server"]
} }
} }
if (domains.length === 0) { if (jm3_Server.length === 0) {
title = "Update Failed" title = "Update Failed"
message = `Using built-in domains:\n\n` message = `Using built-in domains:\n\n`
domains = JM.apiDomains domains = JM.apiDomains
} }
for (let i = 0; i < domains.length; i++) { for (let [domain, index] of jm3_Server) {
message = message + `Stream ${i + 1}: ${domains[i]}\n` message = message + `${index}: ${domain}\n`
domains.push(domain)
} }
if (showConfirmDialog) { if (showConfirmDialog) {
UI.showDialog( UI.showDialog(
@@ -135,7 +179,7 @@ class JM extends ComicSource {
async refreshImgUrl(showMessage) { async refreshImgUrl(showMessage) {
let index = this.loadSetting('imageStream') let index = this.loadSetting('imageStream')
let res = await this.get( let res = await this.get(
`${this.baseUrl}/setting?app_img_shunt=${index}` `${this.baseUrl}/setting?app_img_shunt=${index}?express=`
) )
let setting = JSON.parse(res) let setting = JSON.parse(res)
if (setting["img_host"]) { if (setting["img_host"]) {
@@ -174,19 +218,6 @@ class JM extends ComicSource {
}) })
} }
getHeaders(time) {
const jmVersion = "1.7.6"
const jmAuthKey = "18comicAPPContent"
let token = Convert.md5(Convert.encodeUtf8(`${time}${jmAuthKey}`))
return {
"token": Convert.hexEncode(token),
"tokenparam": `${time},${jmVersion}`,
"Accept-Encoding": "gzip",
"User-Agent": this.apiUa,
}
}
/** /**
* *
* @param input {string} * @param input {string}
@@ -213,7 +244,7 @@ class JM extends ComicSource {
async get(url) { async get(url) {
let time = Math.floor(Date.now() / 1000) let time = Math.floor(Date.now() / 1000)
let kJmSecret = "185Hcomic3PAPP7R" let kJmSecret = "185Hcomic3PAPP7R"
let res = await Network.get(url, this.getHeaders(time)) let res = await Network.get(url, this.getApiHeaders(time))
if(res.status !== 200) { if(res.status !== 200) {
if(res.status === 401) { if(res.status === 401) {
let json = JSON.parse(res.body) let json = JSON.parse(res.body)
@@ -237,7 +268,7 @@ class JM extends ComicSource {
let time = Math.floor(Date.now() / 1000) let time = Math.floor(Date.now() / 1000)
let kJmSecret = "185Hcomic3PAPP7R" let kJmSecret = "185Hcomic3PAPP7R"
let res = await Network.post(url, { let res = await Network.post(url, {
...this.getHeaders(time), ...this.getApiHeaders(time),
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
}, body) }, body)
if(res.status !== 200) { if(res.status !== 200) {
@@ -634,7 +665,7 @@ class JM extends ComicSource {
if (id.startsWith('jm')) { if (id.startsWith('jm')) {
id = id.substring(2) id = id.substring(2)
} }
let res = await this.get(`${this.baseUrl}/album?comicName=&id=${id}`); let res = await this.get(`${this.baseUrl}/album?id=${id}`);
let data = JSON.parse(res) let data = JSON.parse(res)
let author = data.author ?? [] let author = data.author ?? []
let chapters = new Map() let chapters = new Map()
@@ -735,10 +766,7 @@ class JM extends ComicSource {
return {} return {}
} }
return { return {
headers: { headers: this.getImgHeaders(),
"Accept-Encoding": "gzip",
"User-Agent": this.imgUa,
},
modifyImage: ` modifyImage: `
let modifyImage = (image) => { let modifyImage = (image) => {
const num = ${num} const num = ${num}
@@ -773,10 +801,7 @@ class JM extends ComicSource {
*/ */
onThumbnailLoad: (url) => { onThumbnailLoad: (url) => {
return { return {
headers: { headers: this.getImgHeaders()
"Accept-Encoding": "gzip",
"User-Agent": this.imgUa,
}
} }
}, },
/** /**

387
manwaba.js Normal file
View File

@@ -0,0 +1,387 @@
/** @type {import('./_venera_.js')} */
class ManWaBa 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 = "manwaba";
version = "1.0.0";
minAppVersion = "1.4.0";
// update url
url = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/manwaba.js";
api = "https://www.manwaba.com/api/v1";
init() {
/**
* Sends an HTTP request.
* @param {string} url - The URL to send the request to.
* @param {string} method - The HTTP method (e.g., GET, POST, PUT, PATCH, DELETE).
* @param {Object} params - The query parameters to include in the request.
* @param {Object} headers - The headers to include in the request.
* @param {string} payload - The payload to include in the request.
* @returns {Promise<Object>} The response from the request.
*/
this.fetchJson = async (
url,
{ method = "GET", params, headers, payload }
) => {
if (params) {
let params_str = Object.keys(params)
.map((key) => `${key}=${params[key]}`)
.join("&");
url += `?${params_str}`;
}
let res = await Network.sendRequest(method, url, headers, payload);
if (res.status !== 200) {
throw `Invalid status code: ${res.status}, body: ${res.body}`;
}
let json = JSON.parse(res.body);
return json;
};
this.logger = {
error: (msg) => {
log("error", this.name, msg);
},
info: (msg) => {
log("info", this.name, msg);
},
warn: (msg) => {
log("warning", this.name, msg);
},
};
}
// explore page list
explore = [
{
// title of the page.
// title is used to identify the page, it should be unique
title: this.name,
/// multiPartPage or multiPageComicList or mixed
type: "singlePageWithMultiPart",
/**
* load function
* @param page {number | null} - page number, null for `singlePageWithMultiPart` type
* @returns {{}}
* - for `multiPartPage` type, return [{title: string, comics: Comic[], viewMore: PageJumpTarget}]
* - for `multiPageComicList` type, for each page(1-based), return {comics: Comic[], maxPage: number}
* - for `mixed` type, use param `page` as index. for each index(0-based), return {data: [], maxPage: number?}, data is an array contains Comic[] or {title: string, comics: Comic[], viewMore: string?}
*/
load: async (page) => {
let params = {
page: 1,
pageSize: 6,
type: "",
flag: false,
};
const url = `${this.api}/json/home`;
const data = await this.fetchJson(url, { params }).then(
(res) => res.data
);
let magnaList = {
热门: data.comicList,
古风: data.gufengList,
玄幻: data.xuanhuanList,
校园: data.xiaoyuanList,
};
function parseComic(comic) {
return new Comic({
id: comic.id.toString(),
title: comic.title,
subTitle: comic.author,
cover: comic.pic,
tags: comic.tags.split(","),
});
}
let result = {};
for (let key in magnaList) {
result[key] = magnaList[key].map(parseComic);
}
return result;
},
},
];
// categories
category = {
/// title of the category page, used to identify the page, it should be unique
title: this.name,
parts: [
{
// title of the part
name: "类型",
// fixed or random or dynamic
// if random, need to provide `randomNumber` field, which indicates the number of comics to display at the same time
// if dynamic, need to provide `loader` field, which indicates the function to load comics
type: "fixed",
// Remove this if type is dynamic
categories: [
"全部",
"热血",
"玄幻",
"恋爱",
"冒险",
"古风",
"都市",
"穿越",
"奇幻",
"其他",
"搞笑",
"少男",
"战斗",
"重生",
"逆袭",
"爆笑",
"少年",
"后宫",
"系统",
"BL",
"韩漫",
"完整版",
"19r",
"台版",
],
itemType: "category",
categoryParams: [
"",
"热血",
"玄幻",
"恋爱",
"冒险",
"古风",
"都市",
"穿越",
"奇幻",
"其他",
"搞笑",
"少男",
"战斗",
"重生",
"逆袭",
"爆笑",
"少年",
"后宫",
"系统",
"BL",
"韩漫",
"完整版",
"19r",
"台版",
],
},
],
// 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 url = `${this.api}/json/cate`;
let payload = JSON.stringify({
page: {
page: page,
pageSize: 10,
},
category: "comic",
sort: parseInt(options[2]),
comic: {
status: parseInt(options[0] == "2" ? -1 : options[0]),
day: parseInt(options[1]),
tag: param,
},
video: {
year: 0,
typeId: 0,
typeId1: 0,
area: "",
lang: "",
status: -1,
day: 0,
},
novel: {
status: -1,
day: 0,
sortId: 0,
},
});
let data = await this.fetchJson(url, {
method: "POST",
payload,
}).then((res) => res.data);
function parseComic(comic) {
return new Comic({
id: comic.url.split("/").pop(),
title: comic.title,
subTitle: comic.author,
cover: comic.pic,
tags: comic.tags.split(","),
description: comic.intro,
status: comic.status == 0 ? "连载中" : "已完结",
});
}
return {
comics: data.map(parseComic),
maxPage: 100,
};
},
// provide options for category comic loading
optionList: [
{
options: ["2-全部", "0-连载中", "1-已完结"],
},
{
options: [
"0-全部",
"1-周一",
"2-周二",
"3-周三",
"4-周四",
"5-周五",
"6-周六",
"7-周日",
],
},
{
options: ["0-更新", "1-新作", "2-畅销", "3-热门", "4-收藏"],
},
],
};
/// 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) => {
const pageSize = 20;
let url = `${this.api}/json/search`;
let params = {
keyword,
type: "mh",
page,
pageSize,
};
let data = await this.fetchJson(url, { params }).then((res) => res.data);
let total = data.total;
let comics = data.list.map((item) => {
return new Comic({
id: item.id.toString(),
title: item.title,
subTitle: item.author,
cover: item.cover,
tags: item.tags.split(","),
description: item.description,
status: item.status == 0 ? "连载中" : "已完结",
});
});
let maxPage = Math.ceil(total / pageSize);
return {
comics,
maxPage,
};
},
};
/// single comic related
comic = {
/**
* load comic info
* @param id {string}
* @returns {Promise<ComicDetails>}s
*/
loadInfo: async (id) => {
let url = `${this.api}/json/comic/${id}`;
let data = await this.fetchJson(url, { payload: undefined }).then(
(res) => res.data
);
this.logger.warn(`loadInfo: ${data}`);
let chapterId = data.id;
let chapterApi = `${this.api}/json/comic/chapter`;
let params = {
comicId: chapterId,
page: 1,
pageSize: 1,
};
let pageRes = await this.fetchJson(chapterApi, { params });
let total = pageRes.pagination.total;
let chapterRes = await this.fetchJson(chapterApi, {
params: {
...params,
pageSize: total,
},
});
let chapterList = chapterRes.data;
let chapters = new Map();
chapterList.forEach((item) => {
chapters.set(item.id.toString(), item.title.toString());
});
return new ComicDetails({
title: data.title.toString(),
subTitle: data.author.toString(),
cover: data.cover,
tags: {
类型: data.tags.split(","),
状态: data.status == 0 ? "连载中" : "已完结",
},
chapters,
description: data.intro,
updateTime: new Date(data.editTime * 1000).toLocaleDateString(),
});
},
/**
* load images of a chapter
* @param comicId {string}
* @param epId {string?}
* @returns {Promise<{images: string[]}>}
*/
loadEp: async (comicId, epId) => {
let imgApi = `${this.api}/comic/image/${epId}`;
let params = {
page: 1,
pageSize: 1,
imageSource: "https://tu.mhttu.cc",
};
let pageNum = await this.fetchJson(imgApi, {
params,
}).then((res) => res.data.pagination.total);
let imageRes = await this.fetchJson(imgApi, {
params: {
...params,
pageSize: pageNum,
},
}).then((res) => res.data.images);
let images = imageRes.map((item) => item.url);
return {
images,
};
},
};
}