Compare commits

..

4 Commits

Author SHA1 Message Date
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
6 changed files with 1264 additions and 824 deletions

View File

@@ -995,7 +995,7 @@ class Hitomi extends ComicSource {
// unique id of the source // unique id of the source
key = "hitomi"; key = "hitomi";
version = "1.1.0"; version = "1.1.1";
minAppVersion = "1.4.6"; minAppVersion = "1.4.6";
@@ -1523,10 +1523,10 @@ class Hitomi extends ComicSource {
const data = await get_gallery_detail(id); const data = await get_gallery_detail(id);
const tags = new Map(); 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.groups.length) tags.set("groups", data.groups);
if (data.artists.length) tags.set("artists", data.artists); 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.series.length) tags.set("series", data.series);
if (data.characters.length) tags.set("characters", data.characters); if (data.characters.length) tags.set("characters", data.characters);
if (data.females.length) tags.set("females", data.females); if (data.females.length) tags.set("females", data.females);

892
ikmmh.js
View File

@@ -1,452 +1,440 @@
class Ikm extends ComicSource { class Ikm extends ComicSource {
// 基础配置 // 基础配置
name = "爱看漫"; name = "爱看漫";
key = "ikmmh"; key = "ikmmh";
version = "1.0.3"; version = "1.0.4";
minAppVersion = "1.0.0"; minAppVersion = "1.0.0";
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/ikmmh.js"; url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/ikmmh.js";
// 常量定义 // 常量定义
static baseUrl = "https://ymcdnyfqdapp.ikmmh.com"; static baseUrl = "https://ymcdnyfqdapp.ikmmh.com";
static Mobile_UA = "Mozilla/5.0 (Linux; Android) Mobile"; static Mobile_UA = "Mozilla/5.0 (Linux; Android) Mobile";
static webHeaders = { static webHeaders = {
"User-Agent": Ikm.Mobile_UA, "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", "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 = { static jsonHead = {
"User-Agent": Ikm.Mobile_UA, "User-Agent": Ikm.Mobile_UA,
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "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", "Accept-Encoding": "gzip",
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
}; };
// 统一缩略图加载配置 // 统一缩略图加载配置
static thumbConfig = (url) => ({ static thumbConfig = (url) => ({
headers: { headers: {
...Ikm.webHeaders, ...Ikm.webHeaders,
Referer: Ikm.baseUrl, "referer": Ikm.baseUrl,
}, },
}); });
// 账号系统 // 账号系统
account = { account = {
login: async (account, pwd) => { login: async (account, pwd) => {
try { try {
let res = await Network.post( let res = await Network.post(
`${Ikm.baseUrl}/api/user/userarr/login`, `${Ikm.baseUrl}/api/user/userarr/login`,
Ikm.jsonHead, Ikm.jsonHead,
`user=${account}&pass=${pwd}` `user=${account}&pass=${pwd}`
); );
if (res.status !== 200) if (res.status !== 200)
throw new Error(`登录失败,状态码:${res.status}`); throw new Error(`登录失败,状态码:${res.status}`);
let data = JSON.parse(res.body); 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"; return "ok";
} catch (err) { } catch (err) {
throw new Error(`登录失败:${err.message}`); throw new Error(`登录失败:${err.message}`);
} }
}, },
logout: () => Network.deleteCookies("ymcdnyfqdapp.ikmmh.com"), logout: () => Network.deleteCookies("ymcdnyfqdapp.ikmmh.com"),
registerWebsite: `${Ikm.baseUrl}/user/register/`, registerWebsite: `${Ikm.baseUrl}/user/register/`,
}; };
// 探索页面 // 探索页面
explore = [ explore = [
{ {
title: this.name, title: this.name,
type: "singlePageWithMultiPart", type: "singlePageWithMultiPart",
load: async () => { load: async () => {
try { try {
let res = await Network.get(`${Ikm.baseUrl}/`, Ikm.webHeaders); let res = await Network.get(`${Ikm.baseUrl}/`, Ikm.webHeaders);
if (res.status !== 200) if (res.status !== 200)
throw new Error(`加载探索页面失败,状态码:${res.status}`); throw new Error(`加载探索页面失败,状态码:${res.status}`);
let document = new HtmlDocument(res.body); let document = new HtmlDocument(res.body);
let parseComic = (e) => { let parseComic = (e) => {
let title = e.querySelector("div.title").text.split("~")[0]; let title = e.querySelector("div.title").text.split("~")[0];
let cover = e.querySelector("div.thumb_img").attributes["data-src"]; let cover = e.querySelector("div.thumb_img").attributes["data-src"];
let link = `${Ikm.baseUrl}${ let link = `${Ikm.baseUrl}${
e.querySelector("a").attributes["href"] e.querySelector("a").attributes["href"]
}`; }`;
return { return {
title, title,
cover, cover,
id: link, id: link,
}; };
}; };
return { return {
本周推荐: document "本周推荐": document
.querySelectorAll("div.module-good-fir > div.item") .querySelectorAll("div.module-good-fir > div.item")
.map(parseComic), .map(parseComic),
今日更新: document "今日更新": document
.querySelectorAll("div.module-day-fir > div.item") .querySelectorAll("div.module-day-fir > div.item")
.map(parseComic), .map(parseComic),
}; };
} catch (err) { } catch (err) {
throw new Error(`探索页面加载失败:${err.message}`); throw new Error(`探索页面加载失败:${err.message}`);
} }
}, },
onThumbnailLoad: Ikm.thumbConfig, onThumbnailLoad: Ikm.thumbConfig,
}, },
]; ];
// 分类页面 // 分类页面
category = { category = {
title: "爱看漫", title: "爱看漫",
parts: [ parts: [
{ {
name: "分类", name: "更新",
// fixed 或者 random type: "fixed",
// random用于分类数量相当多时, 随机显示其中一部分 categories: [
type: "fixed", "星期一",
// 如果类型为random, 需要提供此字段, 表示同时显示的数量 "星期二",
// randomNumber: 5, "星期三",
categories: [ "星期四",
"全部", "星期五",
"长条", "星期六",
"大女主", "星期日",
"百合", ],
"耽美", itemType: "category",
"纯爱", categoryParams: ["1", "2", "3", "4", "5", "6", "7"],
"後宫", },
"韩漫", {
"奇幻", name: "分类",
"轻小说", // fixed 或者 random
"生活", // random用于分类数量相当多时, 随机显示其中一部分
"悬疑", type: "fixed",
"格斗", // 如果类型为random, 需要提供此字段, 表示同时显示的数量
"搞笑", // randomNumber: 5,
"伪娘", categories: [
"竞技", "全部",
"职场", "长条",
"萌系", "大女主",
"冒险", "百合",
"治愈", "耽美",
"都市", "纯爱",
"霸总", "後宫",
"神鬼", "韩漫",
"侦探", "奇幻",
"爱情", "轻小说",
"古风", "生活",
"欢乐向", "悬疑",
"科幻", "格斗",
"穿越", "搞笑",
"性转换", "伪娘",
"校园", "竞技",
"美食", "职场",
"悬疑", "萌系",
"剧情", "冒险",
"热血", "治愈",
"节操", "都市",
"励志", "霸总",
"异世界", "神鬼",
"历史", "侦探",
"战争", "爱情",
"恐怖", "古风",
"霸总", "欢乐向",
"全部", "科幻",
"连载中", "穿越",
"已完结", "性转换",
"全部", "校园",
"日漫", "美食",
"港台", "悬疑",
"美漫", "剧情",
"国漫", "热血",
"韩漫", "节操",
"未分类", "励志",
], "异世界",
// category或者search "历史",
// 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画 "战争",
// 如果为search, 将进入搜索页面 "恐怖",
itemType: "category", "霸总"
}, ],
{ // category或者search
name: "更新", // 如果为category, 点击后将进入分类漫画页面, 使用下方的`categoryComics`加载漫画
type: "fixed", // 如果为search, 将进入搜索页面
categories: [ itemType: "category",
"星期一", }
"星期二", ],
"星期三", enableRankingPage: false,
"星期四", };
"星期五", // 分类漫画加载
"星期六", categoryComics = {
"星期日", load: async (category, param, options, page) => {
], try {
itemType: "category", let res;
categoryParams: ["1", "2", "3", "4", "5", "6", "7"], if (param) {
}, res = await Network.get(
], `${Ikm.baseUrl}/update/${param}.html`,
enableRankingPage: false, Ikm.webHeaders
}; );
// 分类漫画加载 if (res.status !== 200)
categoryComics = { throw new Error(`分类请求失败,状态码:${res.status}`);
load: async (category, param, options, page) => { let document = new HtmlDocument(res.body);
try { let comics = document.querySelectorAll("li.comic-item").map((e) => ({
let res; title: e.querySelector("p.title").text.split("~")[0],
if (param) { cover: e.querySelector("img").attributes["src"],
res = await Network.get( id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`,
`${Ikm.baseUrl}/update/${param}.html`, subTitle: e.querySelector("span.chapter").text,
Ikm.webHeaders }));
); return {
if (res.status !== 200) comics,
throw new Error(`分类请求失败,状态码:${res.status}`); maxPage: 1
let document = new HtmlDocument(res.body); };
let comics = document.querySelectorAll("li.comic-item").map((e) => ({ } else {
title: e.querySelector("p.title").text.split("~")[0], res = await Network.post(
cover: e.querySelector("img").attributes["src"], `${Ikm.baseUrl}/api/comic/index/lists`,
id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`, Ikm.jsonHead,
subTitle: e.querySelector("span.chapter").text, `area=${options[1]}&tags=${encodeURIComponent(category)}&full=${
})); options[0]
return { }&page=${page}`
comics, );
maxPage: 1, let resData = JSON.parse(res.body);
}; return {
} else { comics: resData.data.map((e) => ({
res = await Network.post( id: `${Ikm.baseUrl}${e.info_url}`,
`${Ikm.baseUrl}/api/comic/index/lists`, title: e.name.split("~")[0],
Ikm.jsonHead, subTitle: e.author,
`area=${options[1]}&tags=${encodeURIComponent(category)}&full=${ cover: e.cover,
options[0] tags: e.tags,
}&page=${page}` description: e.lastchapter,
); })),
let resData = JSON.parse(res.body); maxPage: resData.end || 1,
return { };
comics: resData.data.map((e) => ({ }
id: `${Ikm.baseUrl}${e.info_url}`, } catch (err) {
title: e.name.split("~")[0], throw new Error(`分类加载失败:${err.message}`);
subTitle: e.author, }
cover: e.cover, },
tags: e.tags, onThumbnailLoad: Ikm.thumbConfig,
description: e.lastchapter, optionList: [
})), {
maxPage: resData.end || 1, // 对于单个选项, 使用-分割, 左侧为用于数据加载的值, 即传给load函数的options参数; 右侧为显示给用户的文本
};
} options: ["3-全部", "4-连载中", "1-已完结"],
} catch (err) { notShowWhen: [
throw new Error(`分类加载失败:${err.message}`); "星期一",
} "星期二",
}, "星期三",
onThumbnailLoad: Ikm.thumbConfig, "星期四",
optionList: [ "星期五",
{ "星期六",
// 对于单个选项, 使用-分割, 左侧为用于数据加载的值, 即传给load函数的options参数; 右侧为显示给用户的文本 "星期日",
],
options: ["3-全部", "4-连载中", "1-已完结"], showWhen: null,
notShowWhen: [ },
"星期一", {
"星期二", options: [
"星期三", "9-全部",
"星期四", "1-日漫",
"星期五", "2-港台",
"星期六", "3-美漫",
"星期日", "4-国漫",
], "5-韩漫",
showWhen: null, "6-未分类",
}, ],
{ notShowWhen: [
options: [ "星期一",
"9-全部", "星期二",
"1-日漫", "星期三",
"2-港台", "星期四",
"3-美漫", "星期五",
"4-国漫", "星期六",
"5-韩漫", "星期日",
"6-未分类", ],
], showWhen: null,
notShowWhen: [ },
"星期一", ],
"星期二", };
"星期三", // 搜索功能
"星期四", search = {
"星期五", load: async (keyword, options, page) => {
"星期六", try {
"星期日", let res = await Network.get(
], `${Ikm.baseUrl}/search?searchkey=${encodeURIComponent(keyword)}`,
showWhen: null, Ikm.webHeaders
}, );
], let document = new HtmlDocument(res.body);
}; return {
// 搜索功能 comics: document.querySelectorAll("li.comic-item").map((e) => ({
search = { title: e.querySelector("p.title").text.split("~")[0],
load: async (keyword, options, page) => { cover: e.querySelector("img").attributes["src"],
try { id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`,
let res = await Network.get( subTitle: e.querySelector("span.chapter").text,
`${Ikm.baseUrl}/search?searchkey=${encodeURIComponent(keyword)}`, })),
Ikm.webHeaders maxPage: 1,
); };
let document = new HtmlDocument(res.body); } catch (err) {
return { throw new Error(`搜索失败:${err.message}`);
comics: document.querySelectorAll("li.comic-item").map((e) => ({ }
title: e.querySelector("p.title").text.split("~")[0], },
cover: e.querySelector("img").attributes["src"], onThumbnailLoad: Ikm.thumbConfig,
id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`, optionList: [],
subTitle: e.querySelector("span.chapter").text, };
})), // 收藏功能
maxPage: 1, favorites = {
}; multiFolder: false,
} catch (err) { addOrDelFavorite: async (comicId, folderId, isAdding) => {
throw new Error(`搜索失败:${err.message}`); try {
} let id = comicId.match(/\d+/)[0];
}, if (isAdding) {
onThumbnailLoad: Ikm.thumbConfig, // 获取漫画信息
optionList: [], let infoRes = await Network.get(comicId, Ikm.webHeaders);
}; let name = new HtmlDocument(infoRes.body).querySelector(
// 收藏功能 "meta[property='og:title']"
favorites = { ).attributes["content"];
multiFolder: false, // 添加收藏
addOrDelFavorite: async (comicId, folderId, isAdding) => { let res = await Network.post(
try { `${Ikm.baseUrl}/api/user/bookcase/add`,
let id = comicId.match(/\d+/)[0]; Ikm.jsonHead,
if (isAdding) { `articleid=${id}&articlename=${encodeURIComponent(name)}`
// 获取漫画信息 );
let infoRes = await Network.get(comicId, Ikm.webHeaders); let data = JSON.parse(res.body);
let name = new HtmlDocument(infoRes.body).querySelector( if (data.code !== "0") throw new Error(data.msg || "收藏失败");
"meta[property='og:title']" return "ok";
).attributes["content"]; } else {
// 添加收藏 // 删除收藏
let res = await Network.post( let res = await Network.post(
`${Ikm.baseUrl}/api/user/bookcase/add`, `${Ikm.baseUrl}/api/user/bookcase/del`,
Ikm.jsonHead, Ikm.jsonHead,
`articleid=${id}&articlename=${encodeURIComponent(name)}` `articleid=${id}`
); );
let data = JSON.parse(res.body); 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"; return "ok";
} else { }
// 删除收藏 } catch (err) {
let res = await Network.post( throw new Error(`收藏操作失败:${err.message}`);
`${Ikm.baseUrl}/api/user/bookcase/del`, }
Ikm.jsonHead, },
`articleid=${id}` //加载收藏
); loadComics: async (page, folder) => {
let data = JSON.parse(res.body); let res = await Network.get(
if (data.code !== "0") throw new Error(data.msg || "取消收藏失败"); `${Ikm.baseUrl}/user/bookcase`,
return "ok"; Ikm.webHeaders
} );
} catch (err) { if (res.status !== 200) {
throw new Error(`收藏操作失败:${err.message}`); throw "加载收藏失败:" + res.status;
} }
}, let document = new HtmlDocument(res.body);
//加载收藏 return {
loadComics: async (page, folder) => { comics: document.querySelectorAll("div.bookrack-item").map((e) => ({
let res = await Network.get( title: e.querySelector("h3").text.split("~")[0],
`${Ikm.baseUrl}/user/bookcase`, subTitle: e.querySelector("p.desc").text,
Ikm.webHeaders cover: e.querySelector("img").attributes["src"],
); id: `${Ikm.baseUrl}/book/${e.attributes["data-id"]}/`,
if (res.status !== 200) { })),
throw "加载收藏失败:" + res.status; maxPage: 1,
} };
let document = new HtmlDocument(res.body); },
return { onThumbnailLoad: Ikm.thumbConfig,
comics: document.querySelectorAll("div.bookrack-item").map((e) => ({ };
title: e.querySelector("h3").text.split("~")[0], // 漫画详情
subTitle: e.querySelector("p.desc").text, comic = {
cover: e.querySelector("img").attributes["src"], loadInfo: async (id) => {
id: `${Ikm.baseUrl}/book/${e.attributes["data-id"]}/`, // 加载收藏页并判断是否收藏
})), let isFavorite = false;
maxPage: 1, try {
}; let favorites = await this.favorites.loadComics(1, null);
}, isFavorite = favorites.comics.some((comic) => comic.id === id);
onThumbnailLoad: Ikm.thumbConfig, } catch (error) {
}; console.error("加载收藏页失败:", error);
// 漫画详情 }
comic = { let res = await Network.get(id, Ikm.webHeaders);
loadInfo: async (id) => { let document = new HtmlDocument(res.body);
let res = await Network.get(id, Ikm.webHeaders); let comicId = id.match(/\d+/)[0];
let document = new HtmlDocument(res.body); // 获取章节数据
let comicId = id.match(/\d+/)[0]; let epRes = await Network.get(
// 获取章节数据 `${Ikm.baseUrl}/api/comic/zyz/chapterlink?id=${comicId}`,
let epRes = await Network.get( {
`${Ikm.baseUrl}/api/comic/zyz/chapterlink?id=${comicId}`, ...Ikm.jsonHead,
{ "referer": id,
...Ikm.jsonHead, }
Referer: id, );
} let epData = JSON.parse(epRes.body);
); let eps = new Map();
let epData = JSON.parse(epRes.body); if (epData.data && epData.data.length > 0 && epData.data[0].list) {
let eps = new Map(); epData.data[0].list.forEach((e) => {
if (epData.data && epData.data.length > 0 && epData.data[0].list) { let title = e.name;
epData.data[0].list.forEach((e) => { let id = `${Ikm.baseUrl}${e.url}`;
let title = e.name; eps.set(id, title);
let id = `${Ikm.baseUrl}${e.url}`; });
eps.set(id, title); } else {
}); throw new Error(`章节数据格式异常`);
} else { }
throw new Error(`章节数据格式异常`);
} let title = document.querySelector(
"div.book-hero__detail > div.title"
let title = document.querySelector( ).text;
"div.book-hero__detail > div.title" let escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
).text; let thumb =
let escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); document
let thumb = .querySelector("div.coverimg")
document .attributes["style"].match(/\((.*?)\)/)?.[1] || "";
.querySelector("div.coverimg") let desc = document
.attributes["style"].match(/\((.*?)\)/)?.[1] || ""; .querySelector("article.book-container__detail")
let desc = document .text.match(
.querySelector("article.book-container__detail") new RegExp(
.text.match( `漫画名:${escapedTitle}(?:(?:[^。]*?(?:简介|漫画简介)\\s*[:]?\\s*)|(?:[^。]*?))([\\s\\S]+?)\\.\\.\\.。`
new RegExp( )
`漫画名:${escapedTitle}(?:(?:[^。]*?(?:简介|漫画简介)\\s*[:]?\\s*)|(?:[^。]*?))([\\s\\S]+?)\\.\\.\\.。` );
) let intro = desc?.[1]?.trim().replace(/\s+/g, " ") || "";
);
let intro = desc?.[1]?.trim().replace(/\s+/g, " ") || ""; return {
title: title.split("~")[0],
// 获取更新日期 cover: thumb,
let fullDateStr = document description: intro,
.querySelector('meta[property="og:cartoon:update_time"]') tags: {
.attributes["content"]; // "2025-07-18 08:37:02" "作者": [
let date = new Date(fullDateStr); document
let year = date.getFullYear(); .querySelector("div.book-container__author")
let month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始要加1 .text.split("作者:")[1],
let day = String(date.getDate()).padStart(2, "0"); ],
let updateTime = `${year}-${month}-${day}`; "更新": [document.querySelector("div.update > a > em").text],
"标签": document
return new ComicDetails({ .querySelectorAll("div.book-hero__detail > div.tags > a")
title: title.split("~")[0], .map((e) => e.text.trim())
cover: thumb, .filter((text) => text),
description: intro, },
updateTime: updateTime, chapters: eps,
tags: { recommend: document
作者: [ .querySelectorAll("div.module-guessu > div.item")
document .map((e) => ({
.querySelector("div.book-container__author") title: e.querySelector("div.title").text.split("~")[0],
.text.split("作者:")[1], cover: e.querySelector("div.thumb_img").attributes["data-src"],
], id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`,
最新章节: [document.querySelector("div.update > a > em").text], })),
标签: document isFavorite: isFavorite,
.querySelectorAll("div.book-hero__detail > div.tags > a") };
.map((e) => e.text.trim()) },
.filter((text) => text), onThumbnailLoad: Ikm.thumbConfig,
}, loadEp: async (comicId, epId) => {
chapters: eps, try {
recommend: document let res = await Network.get(epId, Ikm.webHeaders);
.querySelectorAll("div.module-guessu > div.item") let document = new HtmlDocument(res.body);
.map((e) => ({ return {
title: e.querySelector("div.title").text.split("~")[0], images: document
cover: e.querySelector("div.thumb_img").attributes["data-src"], .querySelectorAll("img.lazy")
id: `${Ikm.baseUrl}${e.querySelector("a").attributes["href"]}`, .map((e) => e.attributes["data-src"]),
})), };
}); } catch (err) {
}, throw new Error(`加载章节失败:${err.message}`);
onThumbnailLoad: Ikm.thumbConfig, }
loadEp: async (comicId, epId) => { },
try { onImageLoad: (url, comicId, epId) => {
let res = await Network.get(epId, Ikm.webHeaders); return {
let document = new HtmlDocument(res.body); url,
return { headers: {
images: document ...Ikm.webHeaders,
.querySelectorAll("img.lazy") "referer": epId,
.map((e) => e.attributes["data-src"]), },
}; };
} catch (err) { },
throw new Error(`加载章节失败:${err.message}`); };
} }
},
onImageLoad: (url, comicId, epId) => {
return {
url,
headers: {
...Ikm.webHeaders,
Referer: epId,
},
};
},
};
}

View File

@@ -21,7 +21,7 @@
"name": "Picacg", "name": "Picacg",
"fileName": "picacg.js", "fileName": "picacg.js",
"key": "picacg", "key": "picacg",
"version": "1.0.3" "version": "1.0.5"
}, },
{ {
"name": "nhentai", "name": "nhentai",
@@ -60,7 +60,7 @@
"name": "爱看漫", "name": "爱看漫",
"fileName": "ikmmh.js", "fileName": "ikmmh.js",
"key": "ikmmh", "key": "ikmmh",
"version": "1.0.3" "version": "1.0.4"
}, },
{ {
"name": "少年ジャンプ+", "name": "少年ジャンプ+",
@@ -72,7 +72,7 @@
"name": "hitomi.la", "name": "hitomi.la",
"fileName": "hitomi.js", "fileName": "hitomi.js",
"key": "hitomi", "key": "hitomi",
"version": "1.1.0" "version": "1.1.1"
}, },
{ {
"name": "comick", "name": "comick",
@@ -90,7 +90,7 @@
"name": "再漫画", "name": "再漫画",
"fileName": "zaimanhua.js", "fileName": "zaimanhua.js",
"key": "zaimanhua", "key": "zaimanhua",
"version": "1.0.0" "version": "1.0.1"
}, },
{ {
"name": "漫画柜", "name": "漫画柜",
@@ -109,5 +109,11 @@
"fileName": "manwaba.js", "fileName": "manwaba.js",
"key": "manwaba", "key": "manwaba",
"version": "1.0.0" "version": "1.0.0"
},
{
"name": "Lanraragi",
"fileName": "lanraragi.js",
"key": "lanraragi",
"version": "1.0.0"
} }
] ]

275
lanraragi.js Normal file
View File

@@ -0,0 +1,275 @@
/** @type {import('./_venera_.js')} */
class Lanraragi extends ComicSource {
name = "Lanraragi"
key = "lanraragi"
version = "1.0.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" }
}
get baseUrl() {
const api = this.loadSetting('api') || this.settings.api.default
return api.replace(/\/$/, '')
}
async init() {
try {
const url = `${this.baseUrl}/api/categories`
const res = await Network.get(url)
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)
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)
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)
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)
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)
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)
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) => ({}),
// onThumbnailLoad: (url) => ({}),
// 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,
}
}

103
picacg.js
View File

@@ -3,7 +3,7 @@ class Picacg extends ComicSource {
key = "picacg" key = "picacg"
version = "1.0.4" version = "1.0.5"
minAppVersion = "1.0.0" minAppVersion = "1.0.0"
@@ -164,6 +164,99 @@ class Picacg extends ComicSource {
comics: comics 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': { 'zh_CN': {
'Picacg Random': "哔咔随机", 'Picacg Random': "哔咔随机",
'Picacg Latest': "哔咔最新", 'Picacg Latest': "哔咔最新",
'Picacg H24': "哔咔日榜",
'Picacg D7': "哔咔周榜",
'Picacg D30': "哔咔月榜",
'New to old': "新到旧", 'New to old': "新到旧",
'Old to new': "旧到新", 'Old to new': "旧到新",
'Most likes': "最多喜欢", 'Most likes': "最多喜欢",
@@ -710,6 +806,9 @@ class Picacg extends ComicSource {
'zh_TW': { 'zh_TW': {
'Picacg Random': "哔咔隨機", 'Picacg Random': "哔咔隨機",
'Picacg Latest': "哔咔最新", 'Picacg Latest': "哔咔最新",
'Picacg H24': "哔咔日榜",
'Picacg D7': "哔咔周榜",
'Picacg D30': "哔咔月榜",
'New to old': "新到舊", 'New to old': "新到舊",
'Old to new': "舊到新", 'Old to new': "舊到新",
'Most likes': "最多喜歡", 'Most likes': "最多喜歡",
@@ -727,4 +826,4 @@ class Picacg extends ComicSource {
'Sort': "排序", 'Sort': "排序",
}, },
} }
} }

View File

@@ -1,418 +1,490 @@
/** @type {import('./_venera_.js')} */ class Zaimanhua extends ComicSource {
class ZaiManHua extends ComicSource { // 基础信息
// Note: The fields which are marked as [Optional] should be removed if not used
// name of the source
name = "再漫画"; name = "再漫画";
// unique id of the source
key = "zaimanhua"; key = "zaimanhua";
version = "1.0.1";
minAppVersion = "1.0.0";
url =
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/zaimanhua.js";
version = "1.0.0"; // 初始化请求头
minAppVersion = "1.4.0";
// update url
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/zaimanhua.js";
/**
* fetch html content
* @param url {string}
* @param headers {object?}
* @returns {Promise<{document:HtmlDocument}>}
*/
async fetchHtml(url, headers = {}) {
let res = await Network.get(url, headers);
if (res.status !== 200) {
throw "Invalid status code: " + res.status;
}
let document = new HtmlDocument(res.body);
return document;
}
/**
* fetch json content
* @param url {string}
* @param headers {object?}
* @returns {Promise<{data:object}>}
*/
async fetchJson(url, headers = {}) {
let res = await Network.get(url, headers);
return JSON.parse(res.body).data;
}
/**
* parse json content
* @param e object
* @returns {Comic}
*/
parseJsonComic(e) {
let id = e.comic_py;
if (!id) {
id = id.comicPy;
}
let title = e?.name;
if (!title) {
title = e?.title;
}
return new Comic({
id: id.toString(),
title: title.toString(),
subtitle: e?.authors,
tags: e?.types?.split("/"),
cover: e?.cover,
description: e?.last_update_chapter_name.toString(),
});
}
/**
* [Optional] init function
*/
init() { init() {
this.domain = "https://www.zaimanhua.com"; this.headers = {
this.imgBase = "https://images.zaimanhua.com"; "User-Agent": "Mozilla/5.0 (Linux; Android) Mobile",
this.baseUrl = "https://manhua.zaimanhua.com"; "authorization": `Bearer ${this.loadData("token") || ""}`,
};
}
// 构建 URL
buildUrl(path) {
return `https://v4api.zaimanhua.com/app/v1/${path}`;
} }
// explore page list //账户管理
account = {
login: async (username, password) => {
try {
const encryptedPwd = Convert.hexEncode(
Convert.md5(Convert.encodeUtf8(password))
);
const res = await Network.post(
"https://account-api.zaimanhua.com/v1/login/passwd",
{ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" },
`username=${username}&passwd=${encryptedPwd}`
);
const data = JSON.parse(res.body);
if (data.errno !== 0) throw new Error(data.errmsg);
this.saveData("token", data.data.user.token);
this.headers.authorization = `Bearer ${data.data.user.token}`;
return true;
} catch (e) {
UI.showMessage(`登录失败: ${e.message}`);
throw e;
}
},
logout: () => {
this.deleteData("token");
},
};
// 状态检查
checkResponseStatus(res) {
if (res.status === 401) {
throw new Error("登录失效");
}
if (res.status !== 200) {
throw new Error(`请求失败: ${res.status}`);
}
}
// 漫画解析
parseComic(comic) {
// const safeString = (value) => (value || "").toString().trim();
const safeString = (value) => (value != null ? value.toString() : "");
const resolveId = () =>
[comic.comic_id, comic.id].find((id) => id && id !== "0") || "";
const resolveTags = () =>
[comic.status, ...safeString(comic.types).split("/")].filter(Boolean);
const resolveDescription = () => {
const candidates = [
comic.description,
comic.last_update_chapter_name,
comic.last_name,
];
return candidates.find((text) => text) || "";
};
return {
id: safeString(resolveId()),
title: comic.title || comic.name,
subTitle: comic.authors,
cover: comic.cover,
tags: resolveTags(),
description: resolveDescription(),
};
}
//探索页面
explore = [ explore = [
{ {
// title of the page. title: "再漫画 更新",
// title is used to identify the page, it should be unique type: "multiPageComicList",
title: this.name,
/// TODO multiPartPage
type: "singlePageWithMultiPart",
/**
* load function
* @param page {number | null} - page number, null for `singlePageWithMultiPart` type
* @returns {{}}
* - for `multiPartPage` type, return [{title: string, comics: Comic[], viewMore: PageJumpTarget}]
* - for `multiPageComicList` type, for each page(1-based), return {comics: Comic[], maxPage: number}
* - for `mixed` type, use param `page` as index. for each index(0-based), return {data: [], maxPage: number?}, data is an array contains Comic[] or {title: string, comics: Comic[], viewMore: string?}
*/
load: async (page) => { load: async (page) => {
let result = {}; const res = await Network.get(
// https://manhua.zaimanhua.com/api/v1/comic1/recommend/list? this.buildUrl(`comic/update/list/0/${page}`),
// channel=pc&app_name=zmh&version=1.0.0&timestamp=1753547675981&uid=0 this.headers
let api = `${this.baseUrl}/api/v1/comic1/recommend/list`; );
let params = { const data = JSON.parse(res.body).data;
channel: "pc", return {
app_name: "zmh", comics: data.map((item) => this.parseComic(item)),
version: "1.0.0",
timestamp: Date.now(),
uid: 0,
}; };
let params_str = Object.keys(params)
.map((key) => `${key}=${params[key]}`)
.join("&");
let url = `${api}?${params_str}`;
const json = await this.fetchJson(url);
let data = json.list;
data.shift(); // 去掉第一个
data.pop(); // 去掉最后一个
data.map((arr) => {
let title = arr.name;
let comic_list = arr.list.map((item) => this.parseJsonComic(item));
result[title] = comic_list;
});
log("error", "再看漫画", result);
return result;
}, },
}, },
]; ];
// categories static categoryParamMap = {
// categories "全部": "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 = { category = {
/// title of the category page, used to identify the page, it should be unique title: "再漫画",
title: this.name,
parts: [ parts: [
{ {
name: "类型", name: "排行榜",
type: "fixed", type: "fixed",
categories: [ categories: ["日排行", "周排行", "月排行", "总排行"],
"全部", itemType: "category",
"冒险", categoryParams: ["0", "1", "2", "3"],
"搞笑", },
"格斗", {
"科幻", name: "分类",
"爱情", type: "fixed",
"侦探", categories: Object.keys(Zaimanhua.categoryParamMap),
"竞技", categoryParams: Object.values(Zaimanhua.categoryParamMap),
"魔法",
"校园",
"百合",
"耽美",
"历史",
"战争",
"宅系",
"治愈",
"仙侠",
"武侠",
"职场",
"神鬼",
"奇幻",
"生活",
"其他",
],
itemType: "category", itemType: "category",
categoryParams: [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"11",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20",
"21",
"22",
"23",
"24",
],
}, },
], ],
// enable ranking page
enableRankingPage: false,
}; };
/// category comic loading related //分类漫画加载
categoryComics = { 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) => { load: async (category, param, options, page) => {
let fil = `${this.baseUrl}/api/v1/comic1/filter`; if (category.includes("排行")) {
let params = { let res = await Network.get(
timestamp: Date.now(), this.buildUrl(
sortType: 0, `comic/rank/list?page=${page}&rank_type=${options}&by_time=${param}`
page: page, ),
size: 20, this.headers
status: options[1], );
audience: options[0], return {
theme: param, comics: JSON.parse(res.body).data.map((item) =>
cate: options[2], this.parseComic(item)
}; ),
// 拼接url maxPage: 10,
let params_str = Object.keys(params) };
.map((key) => `${key}=${params[key]}`) } else {
.join("&"); param = Zaimanhua.categoryParamMap[category] || "0";
// log("error", "再漫画", params_str); let res = await Network.get(
let url = `${fil}?${params_str}&firstLetter`; this.buildUrl(
// log("error", "再漫画", url); `comic/filter/list?status=${options[2]}&theme=${param}&zone=${options[3]}&cate=${options[1]}&sortType=${options[0]}&page=${page}&size=20`
),
const json = await this.fetchJson(url); this.headers
let comics = json.comicList.map((e) => this.parseJsonComic(e)); );
let maxPage = Math.ceil(json.totalNum / params.size); const data = JSON.parse(res.body).data;
// log("error", "再漫画", comics); return {
return { comics: data.comicList.map((item) => this.parseComic(item)),
comics, maxPage: Math.ceil(data.totalNum / 20),
maxPage, };
}; }
}, },
// provide options for category comic loading
optionList: [ optionList: [
{ {
options: ["0-全部", "3262-少年", "3263-少女", "3264-青年"], options: ["1-更新", "2-人气"],
notShowWhen: null,
showWhen: Object.keys(Zaimanhua.categoryParamMap),
}, },
{ {
options: ["0-全部", "1-故事漫画", "2-四格多格"], options: [
"0-全部",
"3262-少年漫画",
"3263-少女漫画",
"3264-青年漫画",
"13626-女青漫画",
],
notShowWhen: null,
showWhen: Object.keys(Zaimanhua.categoryParamMap),
}, },
{ {
options: ["0-全部", "1-连载", "2-完结"], options: ["0-全部", "2309-连载", "2310-已完结", "29205-短篇"],
notShowWhen: null,
showWhen: Object.keys(Zaimanhua.categoryParamMap),
},
{
options: [
"0-全部",
"2304-日本",
"2305-韩国",
"2306-欧美",
"2307-港台",
"2308-内地",
"8435-其他",
],
notShowWhen: null,
showWhen: Object.keys(Zaimanhua.categoryParamMap),
},
{
options: ["0-人气", "1-吐槽", "2-订阅"],
notshowWhen: null,
showWhen: ["日排行", "周排行", "月排行", "总排行"],
}, },
], ],
}; };
/// search related //搜索
search = { 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) => { load: async (keyword, options, page) => {
let url = `${this.baseUrl}/app/v1/search/index?keyword=${keyword}&source=0&page=${page}&size=20`; const res = await Network.get(
const json = await this.fetchJson(url); this.buildUrl(
let comics = json.comicList.map((e) => this.parseJsonComic(e)); `search/index?keyword=${encodeURIComponent(
let maxPage = Math.ceil(json.totalNum / params.size); keyword
// log("error", "再漫画", comics); )}&page=${page}&sort=0&size=20`
),
this.headers
);
const data = JSON.parse(res.body).data.list;
return { return {
comics, comics: data.map((item) => this.parseComic(item)),
maxPage,
}; };
}, },
// provide options for search
optionList: [], optionList: [],
}; };
/// single comic related //收藏
favorites = {
multiFolder: false,
addOrDelFavorite: async (comicId, folderId, isAdding) => {
const path = isAdding ? "add" : "del";
const res = await Network.get(
this.buildUrl(`comic/sub/${path}?comic_id=${comicId}`),
this.headers
);
const data = JSON.parse(res.body);
if (data.errno !== 0) {
throw new Error(data.errmsg || "操作失败");
}
return "ok";
},
loadComics: async (page) => {
try {
const res = await Network.get(
this.buildUrl(`comic/sub/list?status=0&page=${page}&size=20`),
this.headers
);
const data = JSON.parse(res.body).data;
return {
comics: data.subList.map((item) => this.parseComic(item)) ?? [],
maxPage: Math.ceil(data.total / 20),
};
} catch (e) {
console.error("加载收藏失败:", e);
return { comics: [], maxPage: null };
}
},
};
// 时间戳转换
formatTimestamp(ts) {
const date = new Date(ts * 1000);
return date.toISOString().split("T")[0];
}
//漫画详情
comic = { comic = {
/**
* load comic info
* @param id {string}
* @returns {Promise<ComicDetails>}
*/
loadInfo: async (id) => { loadInfo: async (id) => {
const api = `${this.domain}/api/v1/comic1/comic/detail`; const getFavoriteStatus = async (id) => {
let params = { let res = await Network.get(
channel: "pc", this.buildUrl(`comic/sub/checkIsSub?objId=${id}&source=1`),
app_name: "zmh", this.headers
version: "1.0.0", );
timestamp: Date.now(), this.checkResponseStatus(res);
uid: 0, return JSON.parse(res.body).data.isSub;
comic_py: id,
}; };
let params_str = Object.keys(params) let results = await Promise.all([
.map((key) => `${key}=${params[key]}`) Network.get(
.join("&"); this.buildUrl(`comic/detail/${id}?channel=android`),
let url = `${api}?${params_str}`; this.headers
const json = await this.fetchJson(url); ),
const info = json.comicInfo; getFavoriteStatus.bind(this)(id),
const comic_id = info.id; ]);
let title = info.title; const response = JSON.parse(results[0].body);
let author = info.authorInfo.authorName; if (response.errno !== 0) throw new Error(response.errmsg || "加载失败");
const data = response.data.data;
// 修复时间戳转换问题 function processChapters(groups) {
let lastUpdateTime = new Date(info.lastUpdateTime * 1000); return (groups || []).reduce((result, group) => {
let updateTime = `${lastUpdateTime.getFullYear()}-${ const groupTitle = group.title || "默认";
lastUpdateTime.getMonth() + 1 const chapters = (group.data || [])
}-${lastUpdateTime.getDate()}`; .reverse()
.map((ch) => [
let description = info.description; String(ch.chapter_id),
let cover = info.cover; `${ch.chapter_title.replace(
/^(?:连载版?)?(\d+\.?\d*)([话卷])?$/,
let chapters = new Map(); (_, n, t) => `${n}${t || "话"}`
info.chapterList[0].data.forEach((e) => { )}`,
chapters.set(e.chapter_id.toString(), e.chapter_title); ]);
}); result.set(groupTitle, new Map(chapters));
// chapters 按照key排序 return result;
let chaptersSorted = new Map([...chapters].sort((a, b) => a[0] - b[0])); }, new Map());
}
// 获取推荐漫画 // 分类标签
const api2 = `${this.baseUrl}/api/v1/comic1/comic/same_list`; const { authors, status, types } = data;
let params2 = { const tagMapper = (arr) => arr.map((t) => t.tag_name);
channel: "pc",
app_name: "zmh",
version: "1.0.0",
timestamp: Date.now(),
uid: 0,
comic_id: comic_id,
};
let params2_str = Object.keys(params2)
.map((key) => `${key}=${params2[key]}`)
.join("&");
let url2 = `${api2}?${params2_str}`;
const json2 = await this.fetchJson(url2);
let recommend = json2.data.comicList.map((e) => this.parseJsonComic(e));
let tags = {
状态: [info.status],
类型: [info.readerGroup, ...info.types.split("/")],
点击: [info.hitNumStr.toString()],
订阅: [info.subNumStr],
};
return new ComicDetails({
title,
subtitle: author,
cover,
description,
tags,
chapters: chaptersSorted,
recommend,
updateTime,
});
},
/**
* load images of a chapter
* @param comicId {string}
* @param epId {string?}
* @returns {Promise<{images: string[]}>}
*/
loadEp: async (comicId, epId) => {
const api_ = `${this.domain}/api/v1/comic1/comic/detail`;
// log("error", "再漫画", id);
let params_ = {
channel: "pc",
app_name: "zmh",
version: "1.0.0",
timestamp: Date.now(),
uid: 0,
comic_py: comicId,
};
let params_str_ = Object.keys(params_)
.map((key) => `${key}=${params_[key]}`)
.join("&");
let url_ = `${api_}?${params_str_}`;
const json_ = await this.fetchJson(url_);
const info_ = json_.comicInfo;
const comic_id = info_.id;
const api = `${this.baseUrl}/api/v1/comic1/chapter/detail`;
// comic_id=18114&chapter_id=36227
let params = {
channel: "pc",
app_name: "zmh",
version: "1.0.0",
timestamp: Date.now(),
uid: 0,
comic_id: comic_id,
chapter_id: epId,
};
let params_str = Object.keys(params)
.map((key) => `${key}=${params[key]}`)
.join("&");
let url = `${api}?${params_str}`;
const json = await this.fetchJson(url);
const info = json.chapterInfo;
return { return {
images: info.page_url, title: data.title,
cover: data.cover,
description: data.description,
tags: {
"作者": tagMapper(authors),
"状态": [...tagMapper(status), data.last_update_chapter_name],
"标签": tagMapper(types),
},
updateTime: this.formatTimestamp(data.last_updatetime),
chapters: processChapters(data.chapters),
isFavorite: results[1],
subId: id,
}; };
}, },
/** loadEp: async (comicId, epId) => {
* [Optional] provide configs for an image loading const res = await Network.get(
* @param url this.buildUrl(`comic/chapter/${comicId}/${epId}`)
* @param comicId );
* @param epId const data = JSON.parse(res.body).data.data;
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>} return { images: data.page_url_hd || data.page_url };
*/
onImageLoad: (url, comicId, epId) => {
return {};
}, },
/**
* [Optional] provide configs for a thumbnail loading loadComments: async (comicId, subId, page, replyTo) => {
* @param url {string} try {
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>} // 构建请求URL
* const url = this.buildUrl(
* `ImageLoadingConfig.modifyImage` and `ImageLoadingConfig.onLoadFailed` will be ignored. `comment/list?page=${page}&size=30&type=4&objId=${
* They are not supported for thumbnails. subId || comicId
*/ }&sortBy=1`
onThumbnailLoad: (url) => { );
return {}; 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";
}, },
}; };
} }