Compare commits

...

38 Commits

Author SHA1 Message Date
Naomi
603fefe9be [ikmmh] Add pass validator (#154) 2025-09-14 16:36:53 +08:00
Gandum2077
cd941b92ef [hitomi.la] bugfix (#152)
* [hitomi.la]Fix issue that galleries without language tag cannot be loaded

* [hitomi.la] fix title error on loading by category

* [hitomi.la] Fixed a bug where results could conflict when multiple searches occur simultaneously.

* [hitomi.la] Update to version 1.1.2
2025-09-05 17:41:42 +08:00
62fbe9294b [jm] 添加每周必看 2025-09-03 23:05:31 +08:00
91823846a0 Update template 2025-09-03 22:02:39 +08:00
ef87d90e89 Update venera api. 2025-09-03 20:31:57 +08:00
nyne
a991dac6d6 Update Venera API 2025-09-02 22:17:32 +08:00
Cusox.
c9fdc8367a fix: update index.json (#151) 2025-09-02 22:16:34 +08:00
Cusox.
aafc7078ba Lanraragi ApiKey 鉴权支持 (#148)
* feat: auth with `apiKey`

* chore: revise version
2025-09-01 20:43:17 +08:00
Pacalini
edebc0c430 jm: fix domain api (#147) 2025-09-01 20:43:04 +08:00
Pacalini
b6448c2055 copy: update headers & chapter limit (#140)
* copy: update headers & chapter limit

* copy: bump version
2025-08-24 18:41:20 +08:00
Pacalini
ca2f626483 eh&nh: fix url open (#142) 2025-08-24 18:09:05 +08:00
Zion
8a26cff469 Update manhuagui (#136)
* add new source from comick

* fix some code

* fix gif load and comic list info(none-type/chapter/volume)

* add some comick hidden tags

* revise coding error in file

* info updata time

* fix no-EN error

* add new function

- Multi-language comic selection support
- Added comic recommendations
- Fixed empty chapter return bug
- Resolved tag click issues
- Optimized data processing

* Optimize network request

Remove redundant requests and prevent async deadlocks

* Update comick.js

* new small comic source from baihehui

* Fixed some bugs and added some sorting methods

* Fixed some bugs and added some sorting methods

* Add a new resource from ykmh

* Remove invalid request

* fixed chapter api

* Update index.json

* Update index.json

* Update comick.js

* fix search bug from manhuagui

Fix ”querySelectorAll“ bug in search page.
Add multi-group of chapters in info page.

* add lanraragi

* Update manhuagui.js

login, comment, favorites

* Update index.json

* Update index.json
2025-08-21 16:49:27 +08:00
Pacalini
c281495cee copy: update headers (#137) 2025-08-18 21:12:51 +08:00
Gandum2077
170eb738b9 [hitomi.la]Fix issue that galleries without language tag cannot be loaded (#132) 2025-08-16 18:52:12 +08:00
Zion
0d2ec4a85a Add LANraragi config (#130)
* add new source from comick

* fix some code

* fix gif load and comic list info(none-type/chapter/volume)

* add some comick hidden tags

* revise coding error in file

* info updata time

* fix no-EN error

* add new function

- Multi-language comic selection support
- Added comic recommendations
- Fixed empty chapter return bug
- Resolved tag click issues
- Optimized data processing

* Optimize network request

Remove redundant requests and prevent async deadlocks

* Update comick.js

* new small comic source from baihehui

* Fixed some bugs and added some sorting methods

* Fixed some bugs and added some sorting methods

* Add a new resource from ykmh

* Remove invalid request

* fixed chapter api

* Update index.json

* Update index.json

* Update comick.js

* fix search bug from manhuagui

Fix ”querySelectorAll“ bug in search page.
Add multi-group of chapters in info page.

* add lanraragi
2025-08-16 18:51:56 +08:00
lost one
714353cf64 update zaimanhua and ikmmh (#127)
* 显示收藏状态

* 新增 再漫画

#48

* 更新 ikmmh.js

* 更新 zaimanhua.js

* Update index.json
2025-08-16 18:51:42 +08:00
UCPr
65bb0d244d feat: 为picacg探索页面添加日榜、周榜、月榜 (#128)
* feat: 为picacg探索页面添加日榜、周榜、月榜

* Update index.json
2025-08-16 18:49:53 +08:00
LiuliFox
2b8c532817 [shonen_jump_plus] Optimize comic details parsing and use AppStore to get latest app version (#122) 2025-08-08 14:33:53 +08:00
ee0a98ec33 [nhentai] fix cover 2025-08-03 17:26:29 +08:00
ccc157b4f2 Update urls. 2025-08-03 16:58:09 +08:00
f2aacf2baa [jm] Fix gif. 2025-08-03 16:53:12 +08:00
nyne
f4bc304d1b Update index.json 2025-08-01 09:18:41 +08:00
Zion
08756ee659 fix search bug from manhuagui (#119)
* add new source from comick

* fix some code

* fix gif load and comic list info(none-type/chapter/volume)

* add some comick hidden tags

* revise coding error in file

* info updata time

* fix no-EN error

* add new function

- Multi-language comic selection support
- Added comic recommendations
- Fixed empty chapter return bug
- Resolved tag click issues
- Optimized data processing

* Optimize network request

Remove redundant requests and prevent async deadlocks

* Update comick.js

* new small comic source from baihehui

* Fixed some bugs and added some sorting methods

* Fixed some bugs and added some sorting methods

* Add a new resource from ykmh

* Remove invalid request

* fixed chapter api

* Update index.json

* Update index.json

* Update comick.js

* fix search bug from manhuagui

Fix ”querySelectorAll“ bug in search page.
Add multi-group of chapters in info page.
2025-08-01 08:52:44 +08:00
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
Brooklyn Bartly
7dce35fd5a 包子漫画水印去除 (#114)
* refactor(baozi): 格式化代码并优化漫画章节解析逻辑

* feat: 更新包子漫画图片代理地址以优化水印问题

将图片地址从s1.baozicdn.com替换为as-rsa1-usla.baozicdn.com/w640,减少代理后的图片水印

* chore: 更新包子漫画插件版本至1.1.0

* feat(图片加载): 添加图片加载时的自定义请求头

为漫画图片加载添加自定义请求头,包括User-Agent等信息,以适配服务器要求

* refactor(图片代理): 优化移动端图片代理逻辑并移除无用代码

- 使用正则匹配简化图片URL替换逻辑
- 移除不再使用的onImageLoad方法
2025-07-27 15:55:55 +08:00
nyne
ad91da8e0f Merge pull request #115 from morning-start/zaimanhua
Zaimanhua
2025-07-27 15:55:20 +08:00
nyne
4a18a7de3a Merge branch 'main' into zaimanhua 2025-07-27 15:54:47 +08:00
Brooklyn Bartly
5ff8254dd5 Manhuagui (#102)
* feat: 添加漫画柜漫画源实现

实现漫画柜(ManHuaGui)漫画源的完整功能,包括:
- 首页探索页面的多分区加载
- 分类浏览功能支持多种筛选条件
- 搜索功能支持按时间和人气排序
- 漫画详情页面的完整信息展示
- 章节图片的加载功能

* feat: 添加漫画柜扩展支持

* chore: 在.gitignore中添加test目录

避免将测试生成的临时文件提交到版本控制

* feat(漫画源): 实现图片信息提取逻辑并优化图片URL生成

* fix(manhuagui): 修复封面图片加载和章节列表获取问题

- 删除别名信息,可能为空
- 当封面图片src属性不存在时,尝试使用data-src属性
- 处理章节列表可能存在于不同DOM节点的情况
- 移除部分调试日志输出
- 为缩略图请求添加必要的headers

* refactor: 将类名从NewComicSource重命名为ManHuaGui
2025-07-27 15:52:55 +08:00
morning-start
fb20c68024 feat: 添加再漫画源配置文件 2025-07-27 01:21:53 +08:00
morning-start
631298ce1b refactor(zaimanhua): 使用API接口替代HTML解析获取漫画数据
移除parseCoverComic方法,改为通过API接口获取漫画数据并重构parseJsonComic方法处理返回的JSON数据。同时修改首页加载逻辑,直接调用API接口获取推荐漫画列表,提高数据获取的稳定性和效率。
2025-07-27 01:20:28 +08:00
morning-start
f812964e55 fix: 修复章节ID和点击数转换为字符串的问题
确保章节ID和点击数字段始终作为字符串处理,避免潜在的类型错误。同时修正URL参数拼接中的变量名错误。
2025-07-27 00:23:29 +08:00
morning-start
2e13f5fce9 fix(漫画源): 修复时间戳转换和章节排序问题,实现章节图片加载
- 修复时间戳需要乘以1000的问题
- 对章节按照ID进行排序
- 实现章节图片加载功能
- 完善漫画详情页的标签信息
2025-07-27 00:05:57 +08:00
morning-start
0976105138 refactor(zaimanhua): 简化 parseJsonComic 方法中的对象创建逻辑
直接使用对象属性初始化 Comic 对象,避免不必要的中间变量
2025-07-26 23:00:52 +08:00
morning-start
b1b8b8cab9 feat(漫画详情): 实现漫画详情页的加载功能
添加从API获取漫画详细信息的实现,包括标题、作者、封面、描述、章节列表和推荐漫画
使用baseUrl代替硬编码的域名,提高代码可维护性
移除未使用的parseListComic方法
2025-07-26 22:58:19 +08:00
morning-start
fd59c132a2 fix: 修复再漫画搜索功能返回结果问题
搜索功能未正确返回漫画列表和最大页数,添加缺失的返回数据逻辑
2025-07-26 22:14:43 +08:00
morning-start
a5b1fd6ca2 refactor(zaimanhua): 重构漫画源接口实现和数据结构
- 修改fetchHtml和fetchJson返回类型,增加错误处理
- 简化漫画信息解析逻辑,移除冗余字段
- 重构分类页面实现,使用固定分类选项
- 实现分类漫画加载接口,支持分页和筛选
2025-07-26 22:14:14 +08:00
morning-start
2174c13e16 feat: 添加再漫画源配置文件并更新.gitignore
添加zaimanhua.js作为新的漫画源配置文件,包含完整的漫画源实现
在.gitignore中新增test/目录忽略规则
2025-07-26 20:27:08 +08:00
20 changed files with 4072 additions and 1061 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea
.vscode
test/

View File

@@ -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: [

View File

@@ -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
})
}

View File

@@ -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: {

836
baozi.js
View File

@@ -1,386 +1,494 @@
class Baozi extends ComicSource {
// 此漫画源的名称
name = "包子漫画"
// 此漫画源的名称
name = "包子漫画";
// 唯一标识符
key = "baozi"
// 唯一标识符
key = "baozi";
version = "1.0.5"
version = "1.1.0";
minAppVersion = "1.0.0"
minAppVersion = "1.0.0";
// 更新链接
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/baozi.js"
// 更新链接
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/baozi.js";
settings = {
language: {
title: "简繁切换",
type: "select",
options: [
{ value: "cn", text: "简体" },
{ value: "tw", text: "繁體" }
],
default: "cn"
settings = {
language: {
title: "简繁切换",
type: "select",
options: [
{ value: "cn", text: "简体" },
{ value: "tw", text: "繁體" },
],
default: "cn",
},
domains: {
title: "主域名",
type: "select",
options: [
{ value: "baozimhcn.com" },
{ value: "webmota.com" },
{ value: "kukuc.co" },
{ value: "twmanga.com" },
{ value: "dinnerku.com" },
],
default: "baozimhcn.com",
},
};
// 动态生成完整域名
get lang() {
return this.loadSetting("language") || this.settings.language.default;
}
get baseUrl() {
let domain = this.loadSetting("domains") || this.settings.domains.default;
return `https://${this.lang}.${domain}`;
}
/// 账号
/// 设置为null禁用账号功能
account = {
/// 登录
/// 返回任意值表示登录成功
login: async (account, pwd) => {
let res = await Network.post(
`${this.baseUrl}/api/bui/signin`,
{
"content-type":
"multipart/form-data; boundary=----WebKitFormBoundaryFUNUxpOwyUaDop8s",
},
domains: {
title: "主域名",
type: "select",
options: [
{ value: "baozimhcn.com" },
{ value: "webmota.com" },
{ value: "kukuc.co" },
{ value: "twmanga.com" },
{ value: "dinnerku.com" }
],
default: "baozimhcn.com"
'------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name="username"\r\n\r\n' +
account +
'\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name="password"\r\n\r\n' +
pwd +
"\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s--\r\n"
);
if (res.status !== 200) {
throw "Invalid status code: " + res.status;
}
let json = JSON.parse(res.body);
let token = json.data;
Network.setCookies(this.baseUrl, [
new Cookie({
name: "TSID",
value: token,
domain: this.loadSetting("domains") || this.settings.domains.default,
}),
]);
return "ok";
},
// 退出登录时将会调用此函数
logout: function () {
Network.deleteCookies(
this.loadSetting("domains") || this.settings.domains.default
);
},
get registerWebsite() {
return `${this.baseUrl}/user/signup`;
},
};
/// 解析漫画列表
parseComic(e) {
let url = e.querySelector("a").attributes["href"];
let id = url.split("/").pop();
let title = e.querySelector("h3").text.trim();
let cover = e.querySelector("a > amp-img").attributes["src"];
let tags = e.querySelectorAll("div.tabs > span").map((e) => e.text.trim());
let description = e.querySelector("small").text.trim();
return {
id: id,
title: title,
cover: cover,
tags: tags,
description: description,
};
}
parseJsonComic(e) {
return {
id: e.comic_id,
title: e.name,
subTitle: e.author,
cover: `https://static-tw.baozimh.com/cover/${e.topic_img}?w=285&h=375&q=100`,
tags: e.type_names,
};
}
/// 探索页面
/// 一个漫画源可以有多个探索页面
explore = [
{
/// 标题
/// 标题同时用作标识符, 不能重复
title: "包子漫画",
/// singlePageWithMultiPart 或者 multiPageComicList
type: "singlePageWithMultiPart",
load: async () => {
var res = await Network.get(this.baseUrl);
if (res.status !== 200) {
throw "Invalid status code: " + res.status;
}
}
// 动态生成完整域名
get lang() {
return this.loadSetting('language') || this.settings.language.default;
}
get baseUrl() {
let domain = this.loadSetting('domains') || this.settings.domains.default;
return `https://${this.lang}.${domain}`;
}
/// 账号
/// 设置为null禁用账号功能
account = {
/// 登录
/// 返回任意值表示登录成功
login: async (account, pwd) => {
let res = await Network.post(`${this.baseUrl}/api/bui/signin`, {
'content-type': 'multipart/form-data; boundary=----WebKitFormBoundaryFUNUxpOwyUaDop8s'
}, "------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\n" + account + "\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s\r\nContent-Disposition: form-data; name=\"password\"\r\n\r\n" + pwd + "\r\n------WebKitFormBoundaryFUNUxpOwyUaDop8s--\r\n")
if (res.status !== 200) {
throw "Invalid status code: " + res.status
}
let json = JSON.parse(res.body)
let token = json.data
Network.setCookies(this.baseUrl, [
new Cookie({
name: 'TSID',
value: token,
domain: this.loadSetting('domains') || this.settings.domains.default
}),
])
return 'ok'
},
// 退出登录时将会调用此函数
logout: function () {
Network.deleteCookies(this.loadSetting('domains') || this.settings.domains.default)
},
get registerWebsite() {
return `${this.baseUrl}/user/signup`
let document = new HtmlDocument(res.body);
let parts = document.querySelectorAll("div.index-recommend-items");
let result = {};
for (let part of parts) {
let title = part.querySelector("div.catalog-title").text.trim();
let comics = part
.querySelectorAll("div.comics-card")
.map((e) => this.parseComic(e));
if (comics.length > 0) {
result[title] = comics;
}
}
}
return result;
},
},
];
/// 分类页面
/// 一个漫画源只能有一个分类页面, 也可以没有, 设置为null禁用分类页面
category = {
/// 标题, 同时为标识符, 不能与其他漫画源的分类页面重复
title: "包子漫画",
parts: [
{
name: "类型",
// fixed 或者 random
// random用于分类数量相当多时, 随机显示其中一部分
type: "fixed",
// 如果类型为random, 需要提供此字段, 表示同时显示的数量
// randomNumber: 5,
categories: [
"全部",
"恋爱",
"纯爱",
"古风",
"异能",
"悬疑",
"剧情",
"科幻",
"奇幻",
"玄幻",
"穿越",
"冒险",
"推理",
"武侠",
"格斗",
"战争",
"热血",
"搞笑",
"大女主",
"都市",
"总裁",
"后宫",
"日常",
"韩漫",
"少年",
"其它",
],
// category或者search
// 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画
// 如果为search, 将进入搜索页面
itemType: "category",
// 若提供, 数量需要和`categories`一致, `categoryComics.load`方法将会收到此参数
categoryParams: [
"all",
"lianai",
"chunai",
"gufeng",
"yineng",
"xuanyi",
"juqing",
"kehuan",
"qihuan",
"xuanhuan",
"chuanyue",
"mouxian",
"tuili",
"wuxia",
"gedou",
"zhanzheng",
"rexie",
"gaoxiao",
"danuzhu",
"dushi",
"zongcai",
"hougong",
"richang",
"hanman",
"shaonian",
"qita",
],
},
],
enableRankingPage: false,
};
/// 分类漫画页面, 即点击分类标签后进入的页面
categoryComics = {
load: async (category, param, options, page) => {
let res = await Network.get(
`${this.baseUrl}/api/bzmhq/amp_comic_list?type=${param}&region=${options[0]}&state=${options[1]}&filter=%2a&page=${page}&limit=36&language=${this.lang}&__amp_source_origin=${this.baseUrl}`
);
if (res.status !== 200) {
throw "Invalid status code: " + res.status;
}
let maxPage = null;
let json = JSON.parse(res.body);
if (!json.next) {
maxPage = page;
}
return {
comics: json.items.map((e) => this.parseJsonComic(e)),
maxPage: maxPage,
};
},
// 提供选项
optionList: [
{
options: ["all-全部", "cn-国漫", "jp-日本", "kr-韩国", "en-欧美"],
},
{
options: ["all-全部", "serial-连载中", "pub-已完结"],
},
],
};
/// 搜索
search = {
load: async (keyword, options, page) => {
let res = await Network.get(`${this.baseUrl}/search?q=${keyword}`);
if (res.status !== 200) {
throw "Invalid status code: " + res.status;
}
let document = new HtmlDocument(res.body);
let comics = document
.querySelectorAll("div.comics-card")
.map((e) => this.parseComic(e));
return {
comics: comics,
maxPage: 1,
};
},
// 提供选项
optionList: [],
};
/// 收藏
favorites = {
/// 是否为多收藏夹
multiFolder: false,
/// 添加或者删除收藏
addOrDelFavorite: async (comicId, folderId, isAdding) => {
if (!isAdding) {
let res = await Network.post(
`${this.baseUrl}/user/operation_v2?op=del_bookmark&comic_id=${comicId}`
);
if (!res.status || res.status >= 400) {
throw "Invalid status code: " + res.status;
}
return "ok";
} else {
let res = await Network.post(
`${this.baseUrl}/user/operation_v2?op=set_bookmark&comic_id=${comicId}&chapter_slot=0`
);
if (!res.status || res.status >= 400) {
throw "Invalid status code: " + res.status;
}
return "ok";
}
},
// 加载收藏夹, 仅当multiFolder为true时有效
// 当comicId不为null时, 需要同时返回包含该漫画的收藏夹
loadFolders: null,
/// 加载漫画
loadComics: async (page, folder) => {
let res = await Network.get(`${this.baseUrl}/user/my_bookshelf`);
if (res.status !== 200) {
throw "Invalid status code: " + res.status;
}
let document = new HtmlDocument(res.body);
function parseComic(e) {
let title = e.querySelector("h4 > a").text.trim();
let url = e.querySelector("h4 > a").attributes["href"];
let id = url.split("/").pop();
let author = e
.querySelector("div.info > ul")
.children[1].text.split("")[1]
.trim();
let description = e
.querySelector("div.info > ul")
.children[4].children[0].text.trim();
/// 解析漫画列表
parseComic(e) {
let url = e.querySelector("a").attributes['href']
let id = url.split("/").pop()
let title = e.querySelector("h3").text.trim()
let cover = e.querySelector("a > amp-img").attributes["src"]
let tags = e.querySelectorAll("div.tabs > span").map(e => e.text.trim())
let description = e.querySelector("small").text.trim()
return {
id: id,
title: title,
subTitle: author,
description: description,
cover: e.querySelector("amp-img").attributes["src"],
};
}
let comics = document
.querySelectorAll("div.bookshelf-items")
.map((e) => parseComic(e));
return {
comics: comics,
maxPage: 1,
};
},
};
/// 单个漫画相关
comic = {
// 加载漫画信息
loadInfo: async (id) => {
let res = await Network.get(`${this.baseUrl}/comic/${id}`);
if (res.status !== 200) {
throw "Invalid status code: " + res.status;
}
let document = new HtmlDocument(res.body);
let title = document.querySelector("h1.comics-detail__title").text.trim();
let cover = document.querySelector("div.l-content > div > div > amp-img")
.attributes["src"];
let author = document
.querySelector("h2.comics-detail__author")
.text.trim();
let tags = document
.querySelectorAll("div.tag-list > span")
.map((e) => e.text.trim());
tags = [...tags.filter((e) => e !== "")];
let updateTime = document
.querySelector("div.supporting-text > div > span > em")
?.text.trim()
.replace("(", "")
.replace(")", "");
if (!updateTime) {
const getLastChapterText = () => {
// 合并所有章节容器(处理可能存在多个列表的情况)
const containers = [
...document.querySelectorAll(
"#chapter-items, #chapters_other_list"
),
];
let allChapters = [];
containers.forEach((container) => {
const chapters = container.querySelectorAll(".comics-chapters > a");
allChapters.push(...Array.from(chapters));
});
const lastChapter = allChapters[allChapters.length - 1];
return (
lastChapter?.querySelector("div > span")?.text.trim() ||
"暂无更新信息"
);
};
updateTime = getLastChapterText();
}
let description = document
.querySelector("p.comics-detail__desc")
.text.trim();
let chapters = new Map();
let i = 0;
for (let c of document.querySelectorAll(
"div#chapter-items > div.comics-chapters > a > div > span"
)) {
chapters.set(i.toString(), c.text.trim());
i++;
}
for (let c of document.querySelectorAll(
"div#chapters_other_list > div.comics-chapters > a > div > span"
)) {
chapters.set(i.toString(), c.text.trim());
i++;
}
if (i === 0) {
// 将倒序的最新章节反转
const spans = Array.from(
document.querySelectorAll("div.comics-chapters > a > div > span")
).reverse();
for (let c of spans) {
chapters.set(i.toString(), c.text.trim());
i++;
}
}
let recommend = [];
for (let c of document.querySelectorAll("div.recommend--item")) {
if (c.querySelectorAll("div.tag-comic").length > 0) {
let title = c.querySelector("span").text.trim();
let cover = c.querySelector("amp-img").attributes["src"];
let url = c.querySelector("a").attributes["href"];
let id = url.split("/").pop();
recommend.push({
id: id,
title: title,
cover: cover,
tags: tags,
description: description
});
}
}
}
// updateTime 将 Y年 M月 D日 转化为 Y-M-D
let updateDate = updateTime
.replace(/年/g, "-")
.replace(/月/g, "-")
.replace(/日/g, "");
parseJsonComic(e) {
return {
id: e.comic_id,
title: e.name,
subTitle: e.author,
cover: `https://static-tw.baozimh.com/cover/${e.topic_img}?w=285&h=375&q=100`,
tags: e.type_names,
}
}
/// 探索页面
/// 一个漫画源可以有多个探索页面
explore = [{
/// 标题
/// 标题同时用作标识符, 不能重复
title: "包子漫画",
/// singlePageWithMultiPart 或者 multiPageComicList
type: "singlePageWithMultiPart",
load: async () => {
var res = await Network.get(this.baseUrl)
if (res.status !== 200) {
throw "Invalid status code: " + res.status
}
let document = new HtmlDocument(res.body)
let parts = document.querySelectorAll("div.index-recommend-items")
let result = {}
for (let part of parts) {
let title = part.querySelector("div.catalog-title").text.trim()
let comics = part.querySelectorAll("div.comics-card").map(e => this.parseComic(e))
if (comics.length > 0) {
result[title] = comics
}
}
return result
}
}
]
/// 分类页面
/// 一个漫画源只能有一个分类页面, 也可以没有, 设置为null禁用分类页面
category = {
/// 标题, 同时为标识符, 不能与其他漫画源的分类页面重复
title: "包子漫画",
parts: [{
name: "类型",
// fixed 或者 random
// random用于分类数量相当多时, 随机显示其中一部分
type: "fixed",
// 如果类型为random, 需要提供此字段, 表示同时显示的数量
// randomNumber: 5,
categories: ['全部', '恋爱', '纯爱', '古风', '异能', '悬疑', '剧情', '科幻', '奇幻', '玄幻', '穿越', '冒险', '推理', '武侠', '格斗', '战争', '热血', '搞笑', '大女主', '都市', '总裁', '后宫', '日常', '韩漫', '少年', '其它'],
// category或者search
// 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画
// 如果为search, 将进入搜索页面
itemType: "category",
// 若提供, 数量需要和`categories`一致, `categoryComics.load`方法将会收到此参数
categoryParams: ['all', 'lianai', 'chunai', 'gufeng', 'yineng', 'xuanyi', 'juqing', 'kehuan', 'qihuan', 'xuanhuan', 'chuanyue', 'mouxian', 'tuili', 'wuxia', 'gedou', 'zhanzheng', 'rexie', 'gaoxiao', 'danuzhu', 'dushi', 'zongcai', 'hougong', 'richang', 'hanman', 'shaonian', 'qita']
}
],
enableRankingPage: false,
}
/// 分类漫画页面, 即点击分类标签后进入的页面
categoryComics = {
load: async (category, param, options, page) => {
let res = await Network.get(`${this.baseUrl}/api/bzmhq/amp_comic_list?type=${param}&region=${options[0]}&state=${options[1]}&filter=%2a&page=${page}&limit=36&language=${this.lang}&__amp_source_origin=${this.baseUrl}`)
if (res.status !== 200) {
throw "Invalid status code: " + res.status
}
let maxPage = null
let json = JSON.parse(res.body)
if (!json.next) {
maxPage = page
}
return {
comics: json.items.map(e => this.parseJsonComic(e)),
maxPage: maxPage
}
return new ComicDetails({
title: title,
cover: cover,
description: description,
tags: {
作者: [author],
标签: tags,
},
// 提供选项
optionList: [{
options: [
"all-全部",
"cn-国漫",
"jp-日本",
"kr-韩国",
"en-欧美",
],
}, {
options: [
"all-全部",
"serial-连载中",
"pub-已完结",
],
},
],
}
chapters: chapters,
recommend: recommend,
updateTime: updateDate,
});
},
loadEp: async (comicId, epId) => {
const images = [];
let currentPageUrl = `${this.baseUrl}/comic/chapter/${comicId}/0_${epId}.html`;
let maxAttempts = 100;
/// 搜索
search = {
load: async (keyword, options, page) => {
let res = await Network.get(`${this.baseUrl}/search?q=${keyword}`)
if (res.status !== 200) {
throw "Invalid status code: " + res.status
}
let document = new HtmlDocument(res.body)
let comics = document.querySelectorAll("div.comics-card").map(e => this.parseComic(e))
return {
comics: comics,
maxPage: 1
}
},
while (maxAttempts > 0) {
const res = await Network.get(currentPageUrl);
if (res.status !== 200) break;
// 提供选项
optionList: []
}
// 解析当前页图片
const doc = new HtmlDocument(res.body);
doc
.querySelectorAll("ul.comic-contain > div > amp-img")
.forEach((img) => {
const src = img?.attributes?.["src"];
if (typeof src === "string") images.push(src);
});
/// 收藏
favorites = {
/// 是否为多收藏夹
multiFolder: false,
/// 添加或者删除收藏
addOrDelFavorite: async (comicId, folderId, isAdding) => {
if (!isAdding) {
let res = await Network.post(`${this.baseUrl}/user/operation_v2?op=del_bookmark&comic_id=${comicId}`)
if (!res.status || res.status >= 400) {
throw "Invalid status code: " + res.status
}
return 'ok'
} else {
let res = await Network.post(`${this.baseUrl}/user/operation_v2?op=set_bookmark&comic_id=${comicId}&chapter_slot=0`)
if (!res.status || res.status >= 400) {
throw "Invalid status code: " + res.status
}
return 'ok'
}
},
// 加载收藏夹, 仅当multiFolder为true时有效
// 当comicId不为null时, 需要同时返回包含该漫画的收藏夹
loadFolders: null,
/// 加载漫画
loadComics: async (page, folder) => {
let res = await Network.get(`${this.baseUrl}/user/my_bookshelf`)
if (res.status !== 200) {
throw "Invalid status code: " + res.status
}
let document = new HtmlDocument(res.body)
function parseComic(e) {
let title = e.querySelector("h4 > a").text.trim()
let url = e.querySelector("h4 > a").attributes['href']
let id = url.split("/").pop()
let author = e.querySelector("div.info > ul").children[1].text.split("")[1].trim()
let description = e.querySelector("div.info > ul").children[4].children[0].text.trim()
return {
id: id,
title: title,
subTitle: author,
description: description,
cover: e.querySelector("amp-img").attributes['src']
}
}
let comics = document.querySelectorAll("div.bookshelf-items").map(e => parseComic(e))
return {
comics: comics,
maxPage: 1
}
// 查找下一页链接
const nextLink = doc.querySelector("a#next-chapter");
if (nextLink?.text?.match(/下一页|下一頁/)) {
currentPageUrl = nextLink.attributes["href"];
} else {
break;
}
}
/// 单个漫画相关
comic = {
// 加载漫画信息
loadInfo: async (id) => {
let res = await Network.get(`${this.baseUrl}/comic/${id}`)
if (res.status !== 200) {
throw "Invalid status code: " + res.status
}
let document = new HtmlDocument(res.body)
let title = document.querySelector("h1.comics-detail__title").text.trim()
let cover = document.querySelector("div.l-content > div > div > amp-img").attributes['src']
let author = document.querySelector("h2.comics-detail__author").text.trim()
let tags = document.querySelectorAll("div.tag-list > span").map(e => e.text.trim())
tags = [...tags.filter(e => e !== "")]
let updateTime = document.querySelector("div.supporting-text > div > span > em")?.text.trim().replace('(', '').replace(')', '')
if (!updateTime) {
const getLastChapterText = () => {
// 合并所有章节容器(处理可能存在多个列表的情况)
const containers = [
...document.querySelectorAll("#chapter-items, #chapters_other_list")
];
let allChapters = [];
containers.forEach(container => {
const chapters = container.querySelectorAll(".comics-chapters > a");
allChapters.push(...Array.from(chapters));
});
const lastChapter = allChapters[allChapters.length - 1];
return lastChapter?.querySelector("div > span")?.text.trim() || "暂无更新信息";
};
updateTime = getLastChapterText();
}
let description = document.querySelector("p.comics-detail__desc").text.trim()
let chapters = new Map()
let i = 0
for (let c of document.querySelectorAll("div#chapter-items > div.comics-chapters > a > div > span")) {
chapters.set(i.toString(), c.text.trim())
i++
}
for (let c of document.querySelectorAll("div#chapters_other_list > div.comics-chapters > a > div > span")) {
chapters.set(i.toString(), c.text.trim())
i++
}
if (i === 0) {
// 将倒序的最新章节反转
const spans = Array.from(document.querySelectorAll("div.comics-chapters > a > div > span")).reverse();
for (let c of spans) {
chapters.set(i.toString(), c.text.trim());
i++;
}
}
let recommend = []
for (let c of document.querySelectorAll("div.recommend--item")) {
if (c.querySelectorAll("div.tag-comic").length > 0) {
let title = c.querySelector("span").text.trim()
let cover = c.querySelector("amp-img").attributes['src']
let url = c.querySelector("a").attributes['href']
let id = url.split("/").pop()
recommend.push({
id: id,
title: title,
cover: cover
})
}
}
// updateTime 将 Y年 M月 D日 转化为 Y-M-D
let updateDate = updateTime.replace(/年/g, '-').replace(/月/g, '-').replace(/日/g, '');
return new ComicDetails({
title: title,
cover: cover,
description: description,
tags: {
"作者": [author],
"标签": tags
},
chapters: chapters,
recommend: recommend,
updateTime: updateDate,
})
},
loadEp: async (comicId, epId) => {
const images = [];
let currentPageUrl = `${this.baseUrl}/comic/chapter/${comicId}/0_${epId}.html`;
let maxAttempts = 100;
while (maxAttempts > 0) {
const res = await Network.get(currentPageUrl);
if (res.status !== 200) break;
// 解析当前页图片
const doc = new HtmlDocument(res.body);
doc.querySelectorAll("ul.comic-contain > div > amp-img").forEach(img => {
const src = img?.attributes?.['src'];
if (typeof src === 'string') images.push(src);
});
// 查找下一页链接
const nextLink = doc.querySelector("a#next-chapter");
if (nextLink?.text?.match(/下一页|下一頁/)) {
currentPageUrl = nextLink.attributes['href'];
} else {
break;
}
maxAttempts--;
}
// 代理后图片水印更少
return { images };
}
}
maxAttempts--;
}
// 代理后图片水印更少
let mobileImages = images.map((e) => {
const regex = /scomic\/.*/;
const match = e.match(regex);
return `https://as-rsa1-usla.baozicdn.com/w640/${match[0]}`;
});
return { images: mobileImages };
},
};
}

View File

@@ -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: {

View File

@@ -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;

View File

@@ -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
View File

@@ -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
View File

@@ -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,
},
};
},

View File

@@ -3,7 +3,7 @@
"name": "拷贝漫画",
"fileName": "copy_manga.js",
"key": "copy_manga",
"version": "1.3.6"
"version": "1.3.8"
},
{
"name": "Komiic",
@@ -15,19 +15,19 @@
"name": "包子漫画",
"fileName": "baozi.js",
"key": "baozi",
"version": "1.0.5"
"version": "1.1.0"
},
{
"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.1.4",
"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",
@@ -85,5 +85,35 @@
"fileName": "ykmh.js",
"key": "ykmh",
"version": "1.0.0"
},
{
"name": "再漫画",
"fileName": "zaimanhua.js",
"key": "zaimanhua",
"version": "1.0.1"
},
{
"name": "漫画柜",
"fileName": "manhuagui.js",
"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"
}
]

219
jm.js
View File

@@ -7,25 +7,31 @@ class JM extends ComicSource {
// unique id of the source
key = "jm"
version = "1.1.4"
version = "1.3.0"
minAppVersion = "1.2.5"
minAppVersion = "1.5.0"
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.jmapiproxyxxx.vip",
"www.cdnblackmyth.club",
"www.cdnmhws.cc",
"www.cdnmhwscc.org"
static fallbackServers = [
"www.cdntwice.org",
"www.cdnsha.org",
"www.cdnaspa.cc",
"www.cdnntr.cc",
];
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() {
let index = parseInt(this.loadSetting('apiDomain')) - 1
@@ -48,12 +54,49 @@ class JM extends ComicSource {
return /^\d+$/.test(str)
}
get apiUa() {
return JM.apiUa;
get baseHeaders() {
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() {
return JM.imgUa;
getApiHeaders(time) {
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) {
@@ -78,32 +121,33 @@ class JM extends ComicSource {
* @param showConfirmDialog {boolean}
*/
async refreshApiDomains(showConfirmDialog) {
let today = new Date();
let url = "https://jmappc01-1308024008.cos.ap-guangzhou.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 servers = []
let domains = []
let res = await fetch(
`${url}?time=${today.getFullYear()}${today.getMonth() + 1}${today.getDate()}`,
{headers: {"User-Agent": this.imgUa}}
url,
{headers: this.baseHeaders}
)
if (res.status === 200) {
let data = this.convertData(await res.text(), domainSecret)
let json = JSON.parse(data)
if (json["Server"]) {
title = "Update Success"
message = "New domains:\n\n"
domains = json["Server"]
message = "\n"
servers = json["Server"].slice(0, 4)
}
}
if (domains.length === 0) {
if (servers.length === 0) {
title = "Update Failed"
message = `Using built-in domains:\n\n`
domains = JM.apiDomains
servers = JM.fallbackServers
}
for (let i = 0; i < domains.length; i++) {
message = message + `Stream ${i + 1}: ${domains[i]}\n`
for (let i = 0; i < servers.length; i++) {
message = message + `線路${i + 1}: ${servers[i]}\n\n`
domains.push(servers[i])
}
if (showConfirmDialog) {
UI.showDialog(
@@ -135,7 +179,7 @@ class JM extends ComicSource {
async refreshImgUrl(showMessage) {
let index = this.loadSetting('imageStream')
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)
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}
@@ -213,7 +244,7 @@ class JM extends ComicSource {
async get(url) {
let time = Math.floor(Date.now() / 1000)
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 === 401) {
let json = JSON.parse(res.body)
@@ -237,7 +268,7 @@ class JM extends ComicSource {
let time = Math.floor(Date.now() / 1000)
let kJmSecret = "185Hcomic3PAPP7R"
let res = await Network.post(url, {
...this.getHeaders(time),
...this.getApiHeaders(time),
"Content-Type": "application/x-www-form-urlencoded"
}, body)
if(res.status !== 200) {
@@ -339,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",
@@ -449,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: [
@@ -634,7 +712,7 @@ class JM extends ComicSource {
if (id.startsWith('jm')) {
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 author = data.author ?? []
let chapters = new Map()
@@ -735,11 +813,9 @@ class JM extends ComicSource {
return {}
}
return {
headers: {
"Accept-Encoding": "gzip",
"User-Agent": this.imgUa,
},
modifyImage: `
headers: this.getImgHeaders(),
// gif 图片不需要修改
modifyImage: url.endsWith(".gif") ? null : `
let modifyImage = (image) => {
const num = ${num}
let blockSize = Math.floor(image.height / num)
@@ -773,10 +849,7 @@ class JM extends ComicSource {
*/
onThumbnailLoad: (url) => {
return {
headers: {
"Accept-Encoding": "gzip",
"User-Agent": this.imgUa,
}
headers: this.getImgHeaders()
}
},
/**

294
lanraragi.js Normal file
View 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,
}
}

1349
manhuagui.js Normal file

File diff suppressed because it is too large Load Diff

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://git.nyne.dev/nyne/venera-configs/raw/branch/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,
};
},
};
}

View File

@@ -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
View File

@@ -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': "最多喜歡",

View File

@@ -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 } }
}
}
}`,

View File

@@ -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";

490
zaimanhua.js Normal file
View File

@@ -0,0 +1,490 @@
class Zaimanhua extends ComicSource {
// 基础信息
name = "再漫画";
key = "zaimanhua";
version = "1.0.1";
minAppVersion = "1.0.0";
url =
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/zaimanhua.js";
// 初始化请求头
init() {
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}`;
}
//账户管理
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: "再漫画 更新",
type: "multiPageComicList",
load: async (page) => {
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)),
};
},
},
];
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: "再漫画",
parts: [
{
name: "排行榜",
type: "fixed",
categories: ["日排行", "周排行", "月排行", "总排行"],
itemType: "category",
categoryParams: ["0", "1", "2", "3"],
},
{
name: "分类",
type: "fixed",
categories: Object.keys(Zaimanhua.categoryParamMap),
categoryParams: Object.values(Zaimanhua.categoryParamMap),
itemType: "category",
},
],
};
//分类漫画加载
categoryComics = {
load: async (category, param, options, page) => {
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),
};
}
},
optionList: [
{
options: ["1-更新", "2-人气"],
notShowWhen: null,
showWhen: Object.keys(Zaimanhua.categoryParamMap),
},
{
options: [
"0-全部",
"3262-少年漫画",
"3263-少女漫画",
"3264-青年漫画",
"13626-女青漫画",
],
notShowWhen: null,
showWhen: Object.keys(Zaimanhua.categoryParamMap),
},
{
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 = {
load: async (keyword, options, page) => {
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: data.map((item) => this.parseComic(item)),
};
},
optionList: [],
};
//收藏
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 = {
loadInfo: async (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 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;
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 {
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,
};
},
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 };
},
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";
},
};
}