Compare commits

...

11 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
10 changed files with 392 additions and 196 deletions

View File

@@ -1,4 +1,19 @@
/** @type {import('./_venera_.js')} */ /** @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 { class NewComicSource extends ComicSource {
// Note: The fields which are marked as [Optional] should be removed if not used // 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: [ 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 // For a single option, use `-` to separate the value and text, left for value, right for text
options: [ options: [
"newToOld-New to Old", "newToOld-New to Old",
"oldToNew-Old to New" "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, 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 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: { ranking: {
// For a single option, use `-` to separate the value and text, left for value, right for text // For a single option, use `-` to separate the value and text, left for value, right for text
options: [ options: [

View File

@@ -4,6 +4,18 @@ Venera JavaScript Library
This library provides a set of APIs for interacting with the Venera app. 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) { function setTimeout(callback, delay) {
sendMessage({ sendMessage({
method: 'delay', method: 'delay',
@@ -42,8 +54,6 @@ let Convert = {
/** /**
* @param str {string} * @param str {string}
* @returns {ArrayBuffer} * @returns {ArrayBuffer}
*
* @since 1.4.3
*/ */
encodeGbk: (str) => { encodeGbk: (str) => {
return sendMessage({ return sendMessage({
@@ -57,8 +67,6 @@ let Convert = {
/** /**
* @param value {ArrayBuffer} * @param value {ArrayBuffer}
* @returns {string} * @returns {string}
*
* @since 1.4.3
*/ */
decodeGbk: (value) => { decodeGbk: (value) => {
return sendMessage({ return sendMessage({
@@ -1042,20 +1050,6 @@ function ImageLoadingConfig({url, method, data, headers, onResponse, modifyImage
this.onLoadFailed = onLoadFailed; 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 { class ComicSource {
name = "" 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

@@ -4,7 +4,7 @@ class CopyManga extends ComicSource {
key = "copy_manga" key = "copy_manga"
version = "1.3.7" version = "1.3.8"
minAppVersion = "1.2.1" minAppVersion = "1.2.1"
@@ -14,13 +14,18 @@ class CopyManga extends ComicSource {
let token = this.loadData("token"); let token = this.loadData("token");
let secret = "M2FmMDg1OTAzMTEwMzJlZmUwNjYwNTUwYTA1NjNhNTM=" 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) { if (!token) {
token = ""; token = "";
} else { } else {
token = " " + token; token = " " + token;
} }
let ts = Math.floor(Date.now() / 1000).toString()
let sig = Convert.hmacString( let sig = Convert.hmacString(
Convert.decodeBase64(secret), Convert.decodeBase64(secret),
Convert.encodeUtf8(ts), Convert.encodeUtf8(ts),
@@ -31,6 +36,7 @@ class CopyManga extends ComicSource {
"User-Agent": "COPY/3.0.0", "User-Agent": "COPY/3.0.0",
"source": "copyApp", "source": "copyApp",
"deviceinfo": this.deviceinfo, "deviceinfo": this.deviceinfo,
"dt": `${year}.${month}.${day}`,
"platform": "3", "platform": "3",
"referer": `com.copymanga.app-3.0.0`, "referer": `com.copymanga.app-3.0.0`,
"version": "3.0.0", "version": "3.0.0",
@@ -602,7 +608,7 @@ class CopyManga extends ComicSource {
let getChapters = async (id, groups) => { let getChapters = async (id, groups) => {
let fetchSingle = async (id, path) => { let fetchSingle = async (id, path) => {
let res = await Network.get( 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 this.headers
); );
if (res.status !== 200) { if (res.status !== 200) {
@@ -616,11 +622,11 @@ class CopyManga extends ComicSource {
eps.set(id, title); eps.set(id, title);
}); });
let maxChapter = data.results.total; let maxChapter = data.results.total;
if (maxChapter > 500) { if (maxChapter > 100) {
let offset = 500; let offset = 100;
while (offset < maxChapter) { while (offset < maxChapter) {
res = await Network.get( 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 this.headers
); );
if (res.status !== 200) { if (res.status !== 200) {
@@ -632,7 +638,7 @@ class CopyManga extends ComicSource {
let id = e.uuid; let id = e.uuid;
eps.set(id, title) eps.set(id, title)
}); });
offset += 500; offset += 100;
} }
} }
return eps; return eps;

View File

@@ -7,7 +7,7 @@ class Ehentai extends ComicSource {
// unique id of the source // unique id of the source
key = "ehentai" key = "ehentai"
version = "1.1.3" version = "1.1.4"
minAppVersion = "1.0.0" minAppVersion = "1.0.0"
@@ -1182,7 +1182,7 @@ class Ehentai extends ComicSource {
if(url.includes('?')) { if(url.includes('?')) {
url = url.split('?')[0] 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) let match = reg.exec(url)
if(match) { if(match) {
return `${this.baseUrl}/g/${match[2]}/${match[3]}/` return `${this.baseUrl}/g/${match[2]}/${match[3]}/`

128
hitomi.js
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.1"; version = "1.1.2";
minAppVersion = "1.4.6"; minAppVersion = "1.4.6";
@@ -1004,7 +1004,7 @@ class Hitomi extends ComicSource {
galleryCache = []; galleryCache = [];
categoryResultCache = undefined; categoryResultCache = undefined;
searchResultCache = undefined; searchResultCaches = new Map();
_mapGalleryBlockInfoToComic(n) { _mapGalleryBlockInfoToComic(n) {
return new Comic({ return new Comic({
@@ -1088,95 +1088,24 @@ class Hitomi extends ComicSource {
title: "hitomi.la", title: "hitomi.la",
parts: [ parts: [
{ {
name: "Language", name: "语言",
type: "fixed", type: "fixed",
categories: [ categories: ["汉语", "英语"],
{ itemType: "category",
label: "Chinese", categoryParams: ["language:chinese", "language:english"],
target: {
page: "category",
attributes: {
category: "language",
param: "chinese",
},
},
},
{
label: "English",
target: {
page: "category",
attributes: {
category: "language",
param: "english",
},
},
},
],
}, },
{ {
name: "类别", name: "类别",
type: "fixed", type: "fixed",
categories: [ categories: ["同人志", "漫画", "画师CG", "游戏CG", "图集", "动画"],
{ itemType: "category",
label: "doujinshi", categoryParams: [
target: { "type:doujinshi",
page: "category", "type:manga",
attributes: { "type:artistcg",
category: "type", "type:gamecg",
param: "doujinshi", "type:imageset",
}, "type:anime",
},
},
{
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",
},
},
},
], ],
}, },
], ],
@@ -1195,9 +1124,11 @@ class Hitomi extends ComicSource {
* @returns {Promise<{comics: Comic[], maxPage: number}>} * @returns {Promise<{comics: Comic[], maxPage: number}>}
*/ */
load: async (category, param, options, page) => { load: async (category, param, options, page) => {
const term = param;
if (!term.includes(":"))
throw new Error("不合法的标签请使用namespace:tag的格式");
if (page === 1) { if (page === 1) {
const option = parseInt(options[0]); const option = parseInt(options[0]);
const term = category + ":" + param;
const searchOptions = { const searchOptions = {
term, term,
orderby: "date", orderby: "date",
@@ -1351,6 +1282,7 @@ class Hitomi extends ComicSource {
* @returns {Promise<{comics: Comic[], maxPage: number}>} * @returns {Promise<{comics: Comic[], maxPage: number}>}
*/ */
load: async (keyword, options, page) => { load: async (keyword, options, page) => {
const cacheKey = (keyword || "") + "|" + options.join(",");
if (page === 1) { if (page === 1) {
const option = parseInt(options[0]); const option = parseInt(options[0]);
const term = keyword; const term = keyword;
@@ -1392,11 +1324,11 @@ class Hitomi extends ComicSource {
const comics = (await get_galleryblocks(result.gids)).map((n) => const comics = (await get_galleryblocks(result.gids)).map((n) =>
this._mapGalleryBlockInfoToComic(n) this._mapGalleryBlockInfoToComic(n)
); );
this.searchResultCache = { this.searchResultCaches.set(cacheKey, {
type: "single", type: "single",
state: result.state, state: result.state,
count: result.count, count: result.count,
}; });
return { return {
comics, comics,
maxPage: Math.ceil(result.count / 25), maxPage: Math.ceil(result.count / 25),
@@ -1407,20 +1339,21 @@ class Hitomi extends ComicSource {
result.gids.slice(25 * page - 25, 25 * page) result.gids.slice(25 * page - 25, 25 * page)
) )
).map((n) => this._mapGalleryBlockInfoToComic(n)); ).map((n) => this._mapGalleryBlockInfoToComic(n));
this.searchResultCache = { this.searchResultCaches.set(cacheKey, {
type: "all", type: "all",
gids: result.gids, gids: result.gids,
count: result.count, count: result.count,
}; });
return { return {
comics, comics,
maxPage: Math.ceil(result.count / 25), maxPage: Math.ceil(result.count / 25),
}; };
} }
} else { } else {
if (this.searchResultCache.type === "single") { const searchResultCache = this.searchResultCaches.get(cacheKey);
if (searchResultCache.type === "single") {
const result = await getSingleTagSearchPage({ const result = await getSingleTagSearchPage({
state: this.searchResultCache.state, state: searchResultCache.state,
page: page - 1, page: page - 1,
}); });
const comics = (await get_galleryblocks(result.galleryids)).map((n) => const comics = (await get_galleryblocks(result.galleryids)).map((n) =>
@@ -1428,17 +1361,17 @@ class Hitomi extends ComicSource {
); );
return { return {
comics, comics,
maxPage: Math.ceil(this.searchResultCache.count / 25), maxPage: Math.ceil(searchResultCache.count / 25),
}; };
} else { } else {
const comics = ( const comics = (
await get_galleryblocks( 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)); ).map((n) => this._mapGalleryBlockInfoToComic(n));
return { return {
comics, comics,
maxPage: Math.ceil(this.searchResultCache.count / 25), maxPage: Math.ceil(searchResultCache.count / 25),
}; };
} }
} }
@@ -1526,7 +1459,8 @@ class Hitomi extends ComicSource {
if ("type" in data && data.type) 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 && data.language) 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);

128
ikmmh.js
View File

@@ -1,13 +1,47 @@
/** @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 { class Ikm extends ComicSource {
// 基础配置 // 基础配置
name = "爱看漫"; name = "爱看漫";
key = "ikmmh"; key = "ikmmh";
version = "1.0.4"; version = "1.0.5";
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://www.ikmmh.com";
static Mobile_UA = "Mozilla/5.0 (Linux; Android) Mobile"; 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 = { static webHeaders = {
"User-Agent": Ikm.Mobile_UA, "User-Agent": Ikm.Mobile_UA,
"Accept": "Accept":
@@ -38,14 +72,26 @@ class Ikm extends ComicSource {
); );
if (res.status !== 200) if (res.status !== 200)
throw new Error(`登录失败,状态码:${res.status}`); 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); 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("www.ikmmh.com"),
registerWebsite: `${Ikm.baseUrl}/user/register/`, registerWebsite: `${Ikm.baseUrl}/user/register/`,
}; };
// 探索页面 // 探索页面
@@ -58,6 +104,12 @@ class Ikm extends ComicSource {
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}`);
if (needPassValidator(res.body)) {
// rePost
res = await Network.get(`${Ikm.baseUrl}/`, Ikm.webHeaders);
}
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];
@@ -176,6 +228,15 @@ class Ikm extends ComicSource {
); );
if (res.status !== 200) if (res.status !== 200)
throw new Error(`分类请求失败,状态码:${res.status}`); 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 document = new HtmlDocument(res.body);
let comics = document.querySelectorAll("li.comic-item").map((e) => ({ let comics = document.querySelectorAll("li.comic-item").map((e) => ({
title: e.querySelector("p.title").text.split("~")[0], title: e.querySelector("p.title").text.split("~")[0],
@@ -195,6 +256,17 @@ class Ikm extends ComicSource {
options[0] options[0]
}&page=${page}` }&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); let resData = JSON.parse(res.body);
return { return {
comics: resData.data.map((e) => ({ comics: resData.data.map((e) => ({
@@ -260,6 +332,15 @@ class Ikm extends ComicSource {
`${Ikm.baseUrl}/search?searchkey=${encodeURIComponent(keyword)}`, `${Ikm.baseUrl}/search?searchkey=${encodeURIComponent(keyword)}`,
Ikm.webHeaders 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); let document = new HtmlDocument(res.body);
return { return {
comics: document.querySelectorAll("li.comic-item").map((e) => ({ comics: document.querySelectorAll("li.comic-item").map((e) => ({
@@ -286,6 +367,12 @@ class Ikm extends ComicSource {
if (isAdding) { if (isAdding) {
// 获取漫画信息 // 获取漫画信息
let infoRes = await Network.get(comicId, Ikm.webHeaders); 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( let name = new HtmlDocument(infoRes.body).querySelector(
"meta[property='og:title']" "meta[property='og:title']"
).attributes["content"]; ).attributes["content"];
@@ -305,6 +392,16 @@ class Ikm extends ComicSource {
Ikm.jsonHead, Ikm.jsonHead,
`articleid=${id}` `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); 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";
@@ -322,6 +419,15 @@ class Ikm extends ComicSource {
if (res.status !== 200) { if (res.status !== 200) {
throw "加载收藏失败:" + res.status; throw "加载收藏失败:" + res.status;
} }
if (needPassValidator(res.body)) {
// rePost
res = await Network.get(
`${Ikm.baseUrl}/user/bookcase`,
Ikm.webHeaders
);
}
let document = new HtmlDocument(res.body); let document = new HtmlDocument(res.body);
return { return {
comics: document.querySelectorAll("div.bookrack-item").map((e) => ({ comics: document.querySelectorAll("div.bookrack-item").map((e) => ({
@@ -347,6 +453,12 @@ class Ikm extends ComicSource {
console.error("加载收藏页失败:", error); console.error("加载收藏页失败:", error);
} }
let res = await Network.get(id, Ikm.webHeaders); 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 document = new HtmlDocument(res.body);
let comicId = id.match(/\d+/)[0]; let comicId = id.match(/\d+/)[0];
// 获取章节数据 // 获取章节数据
@@ -417,6 +529,12 @@ class Ikm extends ComicSource {
loadEp: async (comicId, epId) => { loadEp: async (comicId, epId) => {
try { try {
let res = await Network.get(epId, Ikm.webHeaders); 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); let document = new HtmlDocument(res.body);
return { return {
images: document images: document

View File

@@ -3,7 +3,7 @@
"name": "拷贝漫画", "name": "拷贝漫画",
"fileName": "copy_manga.js", "fileName": "copy_manga.js",
"key": "copy_manga", "key": "copy_manga",
"version": "1.3.7" "version": "1.3.8"
}, },
{ {
"name": "Komiic", "name": "Komiic",
@@ -27,7 +27,7 @@
"name": "nhentai", "name": "nhentai",
"fileName": "nhentai.js", "fileName": "nhentai.js",
"key": "nhentai", "key": "nhentai",
"version": "1.0.5" "version": "1.0.6"
}, },
{ {
"name": "紳士漫畫", "name": "紳士漫畫",
@@ -40,13 +40,13 @@
"name": "ehentai", "name": "ehentai",
"fileName": "ehentai.js", "fileName": "ehentai.js",
"key": "ehentai", "key": "ehentai",
"version": "1.1.3" "version": "1.1.4"
}, },
{ {
"name": "禁漫天堂", "name": "禁漫天堂",
"fileName": "jm.js", "fileName": "jm.js",
"key": "jm", "key": "jm",
"version": "1.2.1", "version": "1.3.0",
"description": "禁漫天堂漫畫源, 不能使用時請嘗試切換分流" "description": "禁漫天堂漫畫源, 不能使用時請嘗試切換分流"
}, },
{ {
@@ -60,7 +60,7 @@
"name": "爱看漫", "name": "爱看漫",
"fileName": "ikmmh.js", "fileName": "ikmmh.js",
"key": "ikmmh", "key": "ikmmh",
"version": "1.0.4" "version": "1.0.5"
}, },
{ {
"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.1" "version": "1.1.2"
}, },
{ {
"name": "comick", "name": "comick",
@@ -114,6 +114,6 @@
"name": "Lanraragi", "name": "Lanraragi",
"fileName": "lanraragi.js", "fileName": "lanraragi.js",
"key": "lanraragi", "key": "lanraragi",
"version": "1.0.0" "version": "1.1.0"
} }
] ]

129
jm.js
View File

@@ -7,22 +7,22 @@ class JM extends ComicSource {
// unique id of the source // unique id of the source
key = "jm" key = "jm"
version = "1.2.1" version = "1.3.0"
minAppVersion = "1.2.5" minAppVersion = "1.5.0"
static jmVersion = "2.0.1" static jmVersion = "2.0.6"
static jmPkgName = "com.example.app" static jmPkgName = "com.example.app"
// update url // update url
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/jm.js" url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/jm.js"
static apiDomains = [ static fallbackServers = [
"www.cdnaspa.vip", "www.cdntwice.org",
"www.cdnaspa.club", "www.cdnsha.org",
"www.cdnplaystation6.vip", "www.cdnaspa.cc",
"www.cdnplaystation6.cc" "www.cdnntr.cc",
]; ];
static imageUrl = "https://cdn-msp.jmapinodeudzn.net" static imageUrl = "https://cdn-msp.jmapinodeudzn.net"
@@ -121,11 +121,11 @@ class JM extends ComicSource {
* @param showConfirmDialog {boolean} * @param showConfirmDialog {boolean}
*/ */
async refreshApiDomains(showConfirmDialog) { async refreshApiDomains(showConfirmDialog) {
let url = "https://jmapp03-1308024008.cos.ap-jakarta.myqcloud.com/server-2024.txt" let url = "https://rup4a04-c02.tos-cn-hongkong.bytepluses.com/newsvr-2025.txt"
let domainSecret = "diosfjckwpqpdfjkvnqQjsik" let domainSecret = "diosfjckwpqpdfjkvnqQjsik"
let title = "" let title = ""
let message = "" let message = ""
let jm3_Server = [] let servers = []
let domains = [] let domains = []
let res = await fetch( let res = await fetch(
url, url,
@@ -134,20 +134,20 @@ class JM extends ComicSource {
if (res.status === 200) { if (res.status === 200) {
let data = this.convertData(await res.text(), domainSecret) let data = this.convertData(await res.text(), domainSecret)
let json = JSON.parse(data) let json = JSON.parse(data)
if (json["jm3_Server"]) { if (json["Server"]) {
title = "Update Success" title = "Update Success"
message = "\n" message = "\n"
jm3_Server = json["jm3_Server"] servers = json["Server"].slice(0, 4)
} }
} }
if (jm3_Server.length === 0) { if (servers.length === 0) {
title = "Update Failed" title = "Update Failed"
message = `Using built-in domains:\n\n` message = `Using built-in domains:\n\n`
domains = JM.apiDomains servers = JM.fallbackServers
} }
for (let [domain, index] of jm3_Server) { for (let i = 0; i < servers.length; i++) {
message = message + `${index}: ${domain}\n` message = message + `線路${i + 1}: ${servers[i]}\n\n`
domains.push(domain) domains.push(servers[i])
} }
if (showConfirmDialog) { if (showConfirmDialog) {
UI.showDialog( UI.showDialog(
@@ -370,6 +370,12 @@ class JM extends ComicSource {
/// title of the category page, used to identify the page, it should be unique /// title of the category page, used to identify the page, it should be unique
title: "禁漫天堂", title: "禁漫天堂",
parts: [ parts: [
{
name: "每週必看",
type: "fixed",
categories: ["每週必看"],
itemType: "category",
},
{ {
name: "成人A漫", name: "成人A漫",
type: "fixed", type: "fixed",
@@ -480,33 +486,74 @@ class JM extends ComicSource {
* @returns {Promise<{comics: Comic[], maxPage: number}>} * @returns {Promise<{comics: Comic[], maxPage: number}>}
*/ */
load: async (category, param, options, page) => { load: async (category, param, options, page) => {
param ??= category if (category !== "每週必看") {
param = encodeURIComponent(param) param ??= category
let res = await this.get(`${this.baseUrl}/categories/filter?o=${options[0]}&c=${param}&page=${page}`) param = encodeURIComponent(param)
let data = JSON.parse(res) let res = await this.get(`${this.baseUrl}/categories/filter?o=${options[0]}&c=${param}&page=${page}`)
let total = data.total let data = JSON.parse(res)
let maxPage = Math.ceil(total / 80) let total = data.total
let comics = data.content.map((e) => this.parseComic(e)) let maxPage = Math.ceil(total / 80)
return { let comics = data.content.map((e) => this.parseComic(e))
comics: comics, return {
maxPage: maxPage 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: [ * [Optional] load options dynamically. If `optionList` is provided, this will be ignored.
{ * @param category {string}
// For a single option, use `-` to separate the value and text, left for value, right for text * @param param {string?}
options: [ * @return {Promise<{options: string[], label?: string}[]>} - return a list of option group, each group contains a list of options
"mr-最新", */
"mv-總排行", optionLoader: async (category, param) => {
"mv_m-月排行", if (category !== "每週必看") {
"mv_w-周排行", return [
"mv_t-日排行", {
"mp-最多圖片", label: "排序",
"tf-最多喜歡", // 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: { ranking: {
// For a single option, use `-` to separate the value and text, left for value, right for text // For a single option, use `-` to separate the value and text, left for value, right for text
options: [ options: [

View File

@@ -2,23 +2,34 @@
class Lanraragi extends ComicSource { class Lanraragi extends ComicSource {
name = "Lanraragi" name = "Lanraragi"
key = "lanraragi" key = "lanraragi"
version = "1.0.0" version = "1.1.0"
minAppVersion = "1.4.0" minAppVersion = "1.4.0"
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/lanraragi.js" url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/lanraragi.js"
settings = { settings = {
api: { title: "API", type: "input", default: "http://lrr.tvc-16.science" } api: { title: "API", type: "input", default: "http://lrr.tvc-16.science" },
apiKey: { title: "APIKEY", type: "input", default: "" }
} }
get baseUrl() {
get baseUrl() {
const api = this.loadSetting('api') || this.settings.api.default const api = this.loadSetting('api') || this.settings.api.default
return api.replace(/\/$/, '') return api.replace(/\/$/, '')
} }
get headers() {
let apiKey = this.loadSetting('apiKey')
if (apiKey) apiKey = "Bearer " + Convert.encodeBase64(Convert.encodeUtf8(apiKey))
return {
"Authorization": `${apiKey}`,
}
}
async init() { async init() {
try { try {
const url = `${this.baseUrl}/api/categories` const url = `${this.baseUrl}/api/categories`
const res = await Network.get(url) const res = await Network.get(url, this.headers)
if (res.status !== 200) { this.saveData('categories', []); return } if (res.status !== 200) { this.saveData('categories', []); return }
let data = [] let data = []
try { data = JSON.parse(res.body) } catch (_) { data = [] } try { data = JSON.parse(res.body) } catch (_) { data = [] }
@@ -39,7 +50,7 @@ class Lanraragi extends ComicSource {
explore = [ explore = [
{ title: "Lanraragi", type: "multiPageComicList", load: async (page = 1) => { { title: "Lanraragi", type: "multiPageComicList", load: async (page = 1) => {
const url = `${this.baseUrl}/api/archives` const url = `${this.baseUrl}/api/archives`
const res = await Network.get(url) const res = await Network.get(url, this.headers)
if (res.status !== 200) throw `Invalid status code: ${res.status}` if (res.status !== 200) throw `Invalid status code: ${res.status}`
const data = JSON.parse(res.body) const data = JSON.parse(res.body)
const list = data.slice((page-1)*50, page*50) const list = data.slice((page-1)*50, page*50)
@@ -114,7 +125,7 @@ class Lanraragi extends ComicSource {
add('search[regex]', 'false') add('search[regex]', 'false')
const url = `${base}/search?${qp.join('&')}` const url = `${base}/search?${qp.join('&')}`
const res = await Network.get(url) const res = await Network.get(url, this.headers)
if (res.status !== 200) throw `Invalid status code: ${res.status}` if (res.status !== 200) throw `Invalid status code: ${res.status}`
const data = JSON.parse(res.body) const data = JSON.parse(res.body)
const list = Array.isArray(data.data) ? data.data : [] const list = Array.isArray(data.data) ? data.data : []
@@ -169,7 +180,7 @@ class Lanraragi extends ComicSource {
add('groupby_tanks', groupby) add('groupby_tanks', groupby)
const url = `${base}/api/search?${qp.join('&')}` const url = `${base}/api/search?${qp.join('&')}`
const res = await Network.get(url) const res = await Network.get(url, this.headers)
if (res.status !== 200) throw `Invalid status code: ${res.status}` if (res.status !== 200) throw `Invalid status code: ${res.status}`
const data = JSON.parse(res.body) const data = JSON.parse(res.body)
const all = Array.isArray(data.data) ? data.data : [] const all = Array.isArray(data.data) ? data.data : []
@@ -225,7 +236,7 @@ class Lanraragi extends ComicSource {
comic = { comic = {
loadInfo: async (id) => { loadInfo: async (id) => {
const url = `${this.baseUrl}/api/archives/${id}/metadata` const url = `${this.baseUrl}/api/archives/${id}/metadata`
const res = await Network.get(url) const res = await Network.get(url, this.headers)
if (res.status !== 200) throw `Invalid status code: ${res.status}` if (res.status !== 200) throw `Invalid status code: ${res.status}`
const data = JSON.parse(res.body) const data = JSON.parse(res.body)
const cover = `${this.baseUrl}/api/archives/${id}/thumbnail` const cover = `${this.baseUrl}/api/archives/${id}/thumbnail`
@@ -237,7 +248,7 @@ class Lanraragi extends ComicSource {
}, },
loadThumbnails: async (id, next) => { loadThumbnails: async (id, next) => {
const metaUrl = `${this.baseUrl}/api/archives/${id}/metadata` const metaUrl = `${this.baseUrl}/api/archives/${id}/metadata`
const res = await Network.get(metaUrl) const res = await Network.get(metaUrl, this.headers)
if (res.status !== 200) throw `Invalid status code: ${res.status}` if (res.status !== 200) throw `Invalid status code: ${res.status}`
const data = JSON.parse(res.body) const data = JSON.parse(res.body)
const pagecount = data.pagecount || 1 const pagecount = data.pagecount || 1
@@ -249,7 +260,7 @@ class Lanraragi extends ComicSource {
loadEp: async (comicId, epId) => { loadEp: async (comicId, epId) => {
const base = (this.baseUrl || '').replace(/\/$/, '') const base = (this.baseUrl || '').replace(/\/$/, '')
const url = `${base}/api/archives/${comicId}/files?force=false` const url = `${base}/api/archives/${comicId}/files?force=false`
const res = await Network.get(url) const res = await Network.get(url, this.headers)
if (res.status !== 200) throw `Invalid status code: ${res.status}` if (res.status !== 200) throw `Invalid status code: ${res.status}`
const data = JSON.parse(res.body) const data = JSON.parse(res.body)
const images = (data.pages || []).map(p => { const images = (data.pages || []).map(p => {
@@ -260,8 +271,16 @@ class Lanraragi extends ComicSource {
}).filter(Boolean) }).filter(Boolean)
return { images } return { images }
}, },
// onImageLoad: (url, comicId, epId) => ({}), onImageLoad: (url, comicId, epId) => {
// onThumbnailLoad: (url) => ({}), return {
headers: this.headers
}
},
onThumbnailLoad: (url) => {
return {
headers: this.headers
}
},
// likeComic: async (id, isLike) => {}, // likeComic: async (id, isLike) => {},
// loadComments: async (comicId, subId, page, replyTo) => {}, // loadComments: async (comicId, subId, page, replyTo) => {},
// sendComment: async (comicId, subId, content, replyTo) => {}, // sendComment: async (comicId, subId, content, replyTo) => {},

View File

@@ -7,7 +7,7 @@ class Nhentai extends ComicSource {
// unique id of the source // unique id of the source
key = "nhentai" key = "nhentai"
version = "1.0.5" version = "1.0.6"
minAppVersion = "1.0.0" minAppVersion = "1.0.0"
@@ -523,7 +523,7 @@ class Nhentai extends ComicSource {
'nhentai.net', 'nhentai.net',
], ],
linkToId: (url) => { linkToId: (url) => {
let regex = /\/g\/(\d+)\//g let regex = /\/g\/(\d+)\/?$/g
let match = regex.exec(url) let match = regex.exec(url)
if(match) { if(match) {
return match[1] return match[1]