mirror of
https://github.com/venera-app/venera-configs.git
synced 2025-09-27 08:27:24 +00:00
add 紳士漫畫;
update template
This commit is contained in:
@@ -324,9 +324,10 @@ class NewComicSource extends ComicSource {
|
|||||||
* @param comicId {string}
|
* @param comicId {string}
|
||||||
* @param folderId {string}
|
* @param folderId {string}
|
||||||
* @param isAdding {boolean} - true for add, false for delete
|
* @param isAdding {boolean} - true for add, false for delete
|
||||||
|
* @param favoriteId {string?} - [Comic.favoriteId]
|
||||||
* @returns {Promise<any>} - return any value to indicate success
|
* @returns {Promise<any>} - return any value to indicate success
|
||||||
*/
|
*/
|
||||||
addOrDelFavorite: async (comicId, folderId, isAdding) => {
|
addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => {
|
||||||
/*
|
/*
|
||||||
```
|
```
|
||||||
let res = await Network.post('...')
|
let res = await Network.post('...')
|
||||||
@@ -362,6 +363,38 @@ class NewComicSource extends ComicSource {
|
|||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* add a folder
|
||||||
|
* @param name {string}
|
||||||
|
* @returns {Promise<any>} - return any value to indicate success
|
||||||
|
*/
|
||||||
|
addFolder: async (name) => {
|
||||||
|
/*
|
||||||
|
```
|
||||||
|
let res = await Network.post('...')
|
||||||
|
if (res.status === 401) {
|
||||||
|
throw `Login expired`;
|
||||||
|
}
|
||||||
|
return 'ok'
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* delete a folder
|
||||||
|
* @param folderId {string}
|
||||||
|
* @returns {Promise<void>} - return any value to indicate success
|
||||||
|
*/
|
||||||
|
deleteFolder: async (folderId) => {
|
||||||
|
/*
|
||||||
|
```
|
||||||
|
let res = await Network.delete('...')
|
||||||
|
if (res.status === 401) {
|
||||||
|
throw `Login expired`;
|
||||||
|
}
|
||||||
|
return 'ok'
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* load comics in a folder
|
* load comics in a folder
|
||||||
* throw `Login expired` to indicate login expired, App will automatically re-login retry.
|
* throw `Login expired` to indicate login expired, App will automatically re-login retry.
|
||||||
|
143
_venera_.js
143
_venera_.js
@@ -242,9 +242,31 @@ function createUuid() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random integer between min and max
|
||||||
|
* @param min {number}
|
||||||
|
* @param max {number}
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
function randomInt(min, max) {
|
function randomInt(min, max) {
|
||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: 'random',
|
method: 'random',
|
||||||
|
type: 'int',
|
||||||
|
min: min,
|
||||||
|
max: max
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random double between min and max
|
||||||
|
* @param min {number}
|
||||||
|
* @param max {number}
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function randomDouble(min, max) {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'random',
|
||||||
|
type: 'double',
|
||||||
min: min,
|
min: min,
|
||||||
max: max
|
max: max
|
||||||
});
|
});
|
||||||
@@ -478,8 +500,8 @@ class HtmlDocument {
|
|||||||
key: this.key,
|
key: this.key,
|
||||||
query: query
|
query: query
|
||||||
})
|
})
|
||||||
if(!k) return null;
|
if(k == null) return null;
|
||||||
return new HtmlElement(k);
|
return new HtmlElement(k, this.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -494,7 +516,19 @@ class HtmlDocument {
|
|||||||
key: this.key,
|
key: this.key,
|
||||||
query: query
|
query: query
|
||||||
})
|
})
|
||||||
return ks.map(k => new HtmlElement(k));
|
return ks.map(k => new HtmlElement(k, this.key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose the HTML document.
|
||||||
|
* This should be called when the document is no longer needed.
|
||||||
|
*/
|
||||||
|
dispose() {
|
||||||
|
sendMessage({
|
||||||
|
method: "html",
|
||||||
|
function: "dispose",
|
||||||
|
key: this.key
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,12 +538,16 @@ class HtmlDocument {
|
|||||||
class HtmlElement {
|
class HtmlElement {
|
||||||
key = 0;
|
key = 0;
|
||||||
|
|
||||||
|
doc = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for HtmlDom.
|
* Constructor for HtmlDom.
|
||||||
* @param {number} k - The key of the element.
|
* @param {number} k - The key of the element.
|
||||||
|
* @param {number} doc - The key of the document.
|
||||||
*/
|
*/
|
||||||
constructor(k) {
|
constructor(k, doc) {
|
||||||
this.key = k;
|
this.key = k;
|
||||||
|
this.doc = doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -520,7 +558,8 @@ class HtmlElement {
|
|||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: "html",
|
method: "html",
|
||||||
function: "getText",
|
function: "getText",
|
||||||
key: this.key
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +571,8 @@ class HtmlElement {
|
|||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: "html",
|
method: "html",
|
||||||
function: "getAttributes",
|
function: "getAttributes",
|
||||||
key: this.key
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,10 +586,11 @@ class HtmlElement {
|
|||||||
method: "html",
|
method: "html",
|
||||||
function: "dom_querySelector",
|
function: "dom_querySelector",
|
||||||
key: this.key,
|
key: this.key,
|
||||||
query: query
|
query: query,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
if(!k) return null;
|
if(k == null) return null;
|
||||||
return new HtmlElement(k);
|
return new HtmlElement(k, this.doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -562,9 +603,10 @@ class HtmlElement {
|
|||||||
method: "html",
|
method: "html",
|
||||||
function: "dom_querySelectorAll",
|
function: "dom_querySelectorAll",
|
||||||
key: this.key,
|
key: this.key,
|
||||||
query: query
|
query: query,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
return ks.map(k => new HtmlElement(k));
|
return ks.map(k => new HtmlElement(k, this.doc));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -575,9 +617,10 @@ class HtmlElement {
|
|||||||
let ks = sendMessage({
|
let ks = sendMessage({
|
||||||
method: "html",
|
method: "html",
|
||||||
function: "getChildren",
|
function: "getChildren",
|
||||||
key: this.key
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
return ks.map(k => new HtmlElement(k));
|
return ks.map(k => new HtmlElement(k, this.doc));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -588,9 +631,10 @@ class HtmlElement {
|
|||||||
let ks = sendMessage({
|
let ks = sendMessage({
|
||||||
method: "html",
|
method: "html",
|
||||||
function: "getNodes",
|
function: "getNodes",
|
||||||
key: this.key
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
return ks.map(k => new HtmlNode(k));
|
return ks.map(k => new HtmlNode(k, this.doc));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -601,7 +645,8 @@ class HtmlElement {
|
|||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: "html",
|
method: "html",
|
||||||
function: "getInnerHTML",
|
function: "getInnerHTML",
|
||||||
key: this.key
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,18 +658,61 @@ class HtmlElement {
|
|||||||
let k = sendMessage({
|
let k = sendMessage({
|
||||||
method: "html",
|
method: "html",
|
||||||
function: "getParent",
|
function: "getParent",
|
||||||
key: this.key
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
if(!k) return null;
|
if(k == null) return null;
|
||||||
return new HtmlElement(k);
|
return new HtmlElement(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get class names of the element.
|
||||||
|
* @returns {string[]} An array of class names.
|
||||||
|
*/
|
||||||
|
get classNames() {
|
||||||
|
return sendMessage({
|
||||||
|
method: "html",
|
||||||
|
function: "getClassNames",
|
||||||
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get id of the element.
|
||||||
|
* @returns {string | null} The id of the element.
|
||||||
|
*/
|
||||||
|
get id() {
|
||||||
|
return sendMessage({
|
||||||
|
method: "html",
|
||||||
|
function: "getId",
|
||||||
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get local name of the element.
|
||||||
|
* @returns {string} The tag name of the element.
|
||||||
|
*/
|
||||||
|
get localName() {
|
||||||
|
return sendMessage({
|
||||||
|
method: "html",
|
||||||
|
function: "getLocalName",
|
||||||
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HtmlNode {
|
class HtmlNode {
|
||||||
key = 0;
|
key = 0;
|
||||||
|
|
||||||
constructor(k) {
|
doc = 0;
|
||||||
|
|
||||||
|
constructor(k, doc) {
|
||||||
this.key = k;
|
this.key = k;
|
||||||
|
this.doc = doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -635,7 +723,8 @@ class HtmlNode {
|
|||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: "html",
|
method: "html",
|
||||||
function: "node_text",
|
function: "node_text",
|
||||||
key: this.key
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +736,8 @@ class HtmlNode {
|
|||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: "html",
|
method: "html",
|
||||||
function: "node_type",
|
function: "node_type",
|
||||||
key: this.key
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,10 +749,11 @@ class HtmlNode {
|
|||||||
let k = sendMessage({
|
let k = sendMessage({
|
||||||
method: "html",
|
method: "html",
|
||||||
function: "node_toElement",
|
function: "node_toElement",
|
||||||
key: this.key
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
if(!k) return null;
|
if(k == null) return null;
|
||||||
return new HtmlElement(k);
|
return new HtmlElement(k, this.doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,9 +788,10 @@ let console = {
|
|||||||
* @param description {string}
|
* @param description {string}
|
||||||
* @param maxPage {number?}
|
* @param maxPage {number?}
|
||||||
* @param language {string?}
|
* @param language {string?}
|
||||||
|
* @param favoriteId {string?} - Only set this field if the comic is from favorites page
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function Comic({id, title, subtitle, cover, tags, description, maxPage, language}) {
|
function Comic({id, title, subtitle, cover, tags, description, maxPage, language, favoriteId}) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.subtitle = subtitle;
|
this.subtitle = subtitle;
|
||||||
@@ -708,6 +800,7 @@ function Comic({id, title, subtitle, cover, tags, description, maxPage, language
|
|||||||
this.description = description;
|
this.description = description;
|
||||||
this.maxPage = maxPage;
|
this.maxPage = maxPage;
|
||||||
this.language = language;
|
this.language = language;
|
||||||
|
this.favoriteId = favoriteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -34,5 +34,11 @@
|
|||||||
"fileName": "nhentai.js",
|
"fileName": "nhentai.js",
|
||||||
"key": "nhentai",
|
"key": "nhentai",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "紳士漫畫",
|
||||||
|
"fileName": "wnacg.js",
|
||||||
|
"key": "wnacg",
|
||||||
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
585
wnacg.js
Normal file
585
wnacg.js
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
class Wnacg extends ComicSource {
|
||||||
|
// Note: The fields which are marked as [Optional] should be removed if not used
|
||||||
|
|
||||||
|
// name of the source
|
||||||
|
name = "紳士漫畫"
|
||||||
|
|
||||||
|
// unique id of the source
|
||||||
|
key = "wnacg"
|
||||||
|
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
minAppVersion = "1.0.0"
|
||||||
|
|
||||||
|
// update url
|
||||||
|
url = "https://raw.githubusercontent.com/venera-app/venera-configs/refs/heads/main/wnacg.js"
|
||||||
|
|
||||||
|
get baseUrl() {
|
||||||
|
return `https://${this.loadSetting('domain')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Optional] account related
|
||||||
|
account = {
|
||||||
|
/**
|
||||||
|
* login, return any value to indicate success
|
||||||
|
* @param account {string}
|
||||||
|
* @param pwd {string}
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
login: async (account, pwd) => {
|
||||||
|
let res = await Network.post(
|
||||||
|
`${this.baseUrl}/users-check_login.html`,
|
||||||
|
{
|
||||||
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
`login_name=${encodeURIComponent(account)}&login_pass=${encodeURIComponent(pwd)}`
|
||||||
|
)
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw 'Login failed'
|
||||||
|
}
|
||||||
|
let json = JSON.parse(res.body)
|
||||||
|
if(json['html'].includes('登錄成功')) {
|
||||||
|
return 'ok'
|
||||||
|
}
|
||||||
|
throw 'Login failed'
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* logout function, clear account related data
|
||||||
|
*/
|
||||||
|
logout: () => {
|
||||||
|
Network.deleteCookies(this.baseUrl)
|
||||||
|
},
|
||||||
|
|
||||||
|
// {string?} - register url
|
||||||
|
registerWebsite: null
|
||||||
|
}
|
||||||
|
|
||||||
|
parseComic(c) {
|
||||||
|
let link = c.querySelector("div.pic_box > a").attributes["href"];
|
||||||
|
let id = RegExp("(?<=-aid-)[0-9]+").exec(link)[0];
|
||||||
|
let image =
|
||||||
|
c.querySelector("div.pic_box > a > img").attributes["src"];
|
||||||
|
image = `https:${image}`;
|
||||||
|
let name = c.querySelector("div.info > div.title > a").text;
|
||||||
|
let info = c.querySelector("div.info > div.info_col").text.trim();
|
||||||
|
info = info.replaceAll('\n', '');
|
||||||
|
info = info.replaceAll('\t', '');
|
||||||
|
return new Comic({
|
||||||
|
id: id,
|
||||||
|
title: name,
|
||||||
|
cover: image,
|
||||||
|
description: info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// explore page list
|
||||||
|
explore = [
|
||||||
|
{
|
||||||
|
// title of the page.
|
||||||
|
// title is used to identify the page, it should be unique
|
||||||
|
title: "紳士漫畫",
|
||||||
|
|
||||||
|
/// multiPartPage or multiPageComicList or mixed
|
||||||
|
type: "multiPartPage",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load function
|
||||||
|
* @param page {number | null} - page number, null for `singlePageWithMultiPart` type
|
||||||
|
* @returns {{}}
|
||||||
|
* - for `multiPartPage` type, return [{title: string, comics: Comic[], viewMore: string?}]
|
||||||
|
* - for `multiPageComicList` type, for each page(1-based), return {comics: Comic[], maxPage: number}
|
||||||
|
* - for `mixed` type, use param `page` as index. for each index(0-based), return {data: [], maxPage: number?}, data is an array contains Comic[] or {title: string, comics: Comic[], viewMore: string?}
|
||||||
|
*/
|
||||||
|
load: async (page) => {
|
||||||
|
let res = await Network.get(this.baseUrl, {})
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw `Invalid Status Code ${res.status}`
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body)
|
||||||
|
let titleBlocks = document.querySelectorAll("div.title_sort");
|
||||||
|
let comicBlocks = document.querySelectorAll("div.bodywrap");
|
||||||
|
if (titleBlocks.length !== comicBlocks.length) {
|
||||||
|
throw "Invalid Page"
|
||||||
|
}
|
||||||
|
let result = []
|
||||||
|
for (let i = 0; i < titleBlocks.length; i++) {
|
||||||
|
let title = titleBlocks[i].querySelector("div.title_h2").text.replaceAll('\n', '').trim()
|
||||||
|
let link = titleBlocks[i].querySelector("div.r > a").attributes["href"]
|
||||||
|
let comics = []
|
||||||
|
let comicBlock = comicBlocks[i]
|
||||||
|
let comicElements = comicBlock.querySelectorAll("div.gallary_wrap > ul.cc > li")
|
||||||
|
for (let comicElement of comicElements) {
|
||||||
|
comics.push(this.parseComic(comicElement))
|
||||||
|
}
|
||||||
|
result.push({
|
||||||
|
title: title,
|
||||||
|
comics: comics,
|
||||||
|
viewMore: `category:${link}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
document.dispose()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// categories
|
||||||
|
category = {
|
||||||
|
/// title of the category page, used to identify the page, it should be unique
|
||||||
|
title: "紳士漫畫",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
// title of the part
|
||||||
|
name: "最新",
|
||||||
|
|
||||||
|
// fixed or random
|
||||||
|
// if random, need to provide `randomNumber` field, which indicates the number of comics to display at the same time
|
||||||
|
type: "fixed",
|
||||||
|
|
||||||
|
// number of comics to display at the same time
|
||||||
|
// randomNumber: 5,
|
||||||
|
|
||||||
|
categories: ["最新"],
|
||||||
|
|
||||||
|
// category or search
|
||||||
|
// if `category`, use categoryComics.load to load comics
|
||||||
|
// if `search`, use search.load to load comics
|
||||||
|
itemType: "category",
|
||||||
|
|
||||||
|
// [Optional] {string[]?} must have same length as categories, used to provide loading param for each category
|
||||||
|
categoryParams: ["/albums.html"],
|
||||||
|
|
||||||
|
// [Optional] {string} cannot be used with `categoryParams`, set all category params to this value
|
||||||
|
groupParam: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// title of the part
|
||||||
|
name: "同人誌",
|
||||||
|
|
||||||
|
// fixed or random
|
||||||
|
// if random, need to provide `randomNumber` field, which indicates the number of comics to display at the same time
|
||||||
|
type: "fixed",
|
||||||
|
|
||||||
|
// number of comics to display at the same time
|
||||||
|
// randomNumber: 5,
|
||||||
|
|
||||||
|
categories: ["同人誌", "漢化", "日語", "English", "CG畫集", "3D漫畫", "寫真Cosplay"],
|
||||||
|
|
||||||
|
// category or search
|
||||||
|
// if `category`, use categoryComics.load to load comics
|
||||||
|
// if `search`, use search.load to load comics
|
||||||
|
itemType: "category",
|
||||||
|
|
||||||
|
// [Optional] {string[]?} must have same length as categories, used to provide loading param for each category
|
||||||
|
categoryParams: [
|
||||||
|
"/albums-index-cate-5.html",
|
||||||
|
"/albums-index-cate-1.html",
|
||||||
|
"/albums-index-cate-12.html",
|
||||||
|
"/albums-index-cate-16.html",
|
||||||
|
"/albums-index-cate-2.html",
|
||||||
|
"/albums-index-cate-22.html",
|
||||||
|
"/albums-index-cate-3.html",
|
||||||
|
],
|
||||||
|
|
||||||
|
// [Optional] {string} cannot be used with `categoryParams`, set all category params to this value
|
||||||
|
groupParam: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// title of the part
|
||||||
|
name: "單行本",
|
||||||
|
|
||||||
|
// fixed or random
|
||||||
|
// if random, need to provide `randomNumber` field, which indicates the number of comics to display at the same time
|
||||||
|
type: "fixed",
|
||||||
|
|
||||||
|
// number of comics to display at the same time
|
||||||
|
// randomNumber: 5,
|
||||||
|
|
||||||
|
categories: ["單行本", "漢化", "日語", "English",],
|
||||||
|
|
||||||
|
// category or search
|
||||||
|
// if `category`, use categoryComics.load to load comics
|
||||||
|
// if `search`, use search.load to load comics
|
||||||
|
itemType: "category",
|
||||||
|
|
||||||
|
// [Optional] {string[]?} must have same length as categories, used to provide loading param for each category
|
||||||
|
categoryParams: [
|
||||||
|
"/albums-index-cate-6.html",
|
||||||
|
"/albums-index-cate-9.html",
|
||||||
|
"/albums-index-cate-13.html",
|
||||||
|
"/albums-index-cate-17.html",
|
||||||
|
],
|
||||||
|
|
||||||
|
// [Optional] {string} cannot be used with `categoryParams`, set all category params to this value
|
||||||
|
groupParam: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// title of the part
|
||||||
|
name: "雜誌短篇",
|
||||||
|
|
||||||
|
// fixed or random
|
||||||
|
// if random, need to provide `randomNumber` field, which indicates the number of comics to display at the same time
|
||||||
|
type: "fixed",
|
||||||
|
|
||||||
|
// number of comics to display at the same time
|
||||||
|
// randomNumber: 5,
|
||||||
|
|
||||||
|
categories: ["雜誌短篇", "漢化", "日語", "English",],
|
||||||
|
|
||||||
|
// category or search
|
||||||
|
// if `category`, use categoryComics.load to load comics
|
||||||
|
// if `search`, use search.load to load comics
|
||||||
|
itemType: "category",
|
||||||
|
|
||||||
|
// [Optional] {string[]?} must have same length as categories, used to provide loading param for each category
|
||||||
|
categoryParams: [
|
||||||
|
"/albums-index-cate-7.html",
|
||||||
|
"/albums-index-cate-10.html",
|
||||||
|
"/albums-index-cate-14.html",
|
||||||
|
"/albums-index-cate-18.html",
|
||||||
|
],
|
||||||
|
|
||||||
|
// [Optional] {string} cannot be used with `categoryParams`, set all category params to this value
|
||||||
|
groupParam: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// title of the part
|
||||||
|
name: "韓漫",
|
||||||
|
|
||||||
|
// fixed or random
|
||||||
|
// if random, need to provide `randomNumber` field, which indicates the number of comics to display at the same time
|
||||||
|
type: "fixed",
|
||||||
|
|
||||||
|
// number of comics to display at the same time
|
||||||
|
// randomNumber: 5,
|
||||||
|
|
||||||
|
categories: ["韓漫", "漢化", "生肉",],
|
||||||
|
|
||||||
|
// category or search
|
||||||
|
// if `category`, use categoryComics.load to load comics
|
||||||
|
// if `search`, use search.load to load comics
|
||||||
|
itemType: "category",
|
||||||
|
|
||||||
|
// [Optional] {string[]?} must have same length as categories, used to provide loading param for each category
|
||||||
|
categoryParams: [
|
||||||
|
"/albums-index-cate-19.html",
|
||||||
|
"/albums-index-cate-20.html",
|
||||||
|
"/albums-index-cate-21.html",
|
||||||
|
],
|
||||||
|
|
||||||
|
// [Optional] {string} cannot be used with `categoryParams`, set all category params to this value
|
||||||
|
groupParam: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// enable ranking page
|
||||||
|
enableRankingPage: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// category comic loading related
|
||||||
|
categoryComics = {
|
||||||
|
/**
|
||||||
|
* load comics of a category
|
||||||
|
* @param category {string} - category name
|
||||||
|
* @param param {string?} - category param
|
||||||
|
* @param options {string[]} - options from optionList
|
||||||
|
* @param page {number} - page number
|
||||||
|
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||||
|
*/
|
||||||
|
load: async (category, param, options, page) => {
|
||||||
|
let url = this.baseUrl + param
|
||||||
|
if(page !== 0) {
|
||||||
|
if (!url.includes("-")) {
|
||||||
|
url = url.replaceAll(".html", "-.html");
|
||||||
|
}
|
||||||
|
url = url.replaceAll("index", "");
|
||||||
|
let lr = url.split("albums-");
|
||||||
|
lr[1] = `index-page-${page}${lr[1]}`;
|
||||||
|
url = `${lr[0]}albums-${lr[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await Network.get(url, {})
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw `Invalid Status Code ${res.status}`
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body)
|
||||||
|
let comicElements = document.querySelectorAll("div.grid div.gallary_wrap > ul.cc > li")
|
||||||
|
let comics = []
|
||||||
|
for (let comicElement of comicElements) {
|
||||||
|
comics.push(this.parseComic(comicElement))
|
||||||
|
}
|
||||||
|
let pagesLink = document.querySelectorAll("div.f_left.paginator > a");
|
||||||
|
let pages = Number(pagesLink[pagesLink.length-1].text)
|
||||||
|
document.dispose()
|
||||||
|
return {
|
||||||
|
comics: comics,
|
||||||
|
maxPage: pages,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// search related
|
||||||
|
search = {
|
||||||
|
/**
|
||||||
|
* load search result
|
||||||
|
* @param keyword {string}
|
||||||
|
* @param options {string[]} - options from optionList
|
||||||
|
* @param page {number}
|
||||||
|
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||||
|
*/
|
||||||
|
load: async (keyword, options, page) => {
|
||||||
|
let url = `${this.baseUrl}/search/?q=${encodeURIComponent(keyword)}&f=_all&s=create_time_DESC&syn=yes`
|
||||||
|
if(page !== 0) {
|
||||||
|
url += `&p=${page}`
|
||||||
|
}
|
||||||
|
let res = await Network.get(url, {})
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw `Invalid Status Code ${res.status}`
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body)
|
||||||
|
let comicElements = document.querySelectorAll("div.grid div.gallary_wrap > ul.cc > li")
|
||||||
|
let comics = []
|
||||||
|
for (let comicElement of comicElements) {
|
||||||
|
comics.push(this.parseComic(comicElement))
|
||||||
|
}
|
||||||
|
let total = document.querySelectorAll("p.result > b")[0].text.match(/\d+/)
|
||||||
|
let pages = Math.ceil(Number(total) / 24)
|
||||||
|
document.dispose()
|
||||||
|
return {
|
||||||
|
comics: comics,
|
||||||
|
maxPage: pages,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// favorite related
|
||||||
|
favorites = {
|
||||||
|
// whether support multi folders
|
||||||
|
multiFolder: true,
|
||||||
|
/**
|
||||||
|
* add or delete favorite.
|
||||||
|
* throw `Login expired` to indicate login expired, App will automatically re-login and re-add/delete favorite
|
||||||
|
* @param comicId {string}
|
||||||
|
* @param folderId {string}
|
||||||
|
* @param isAdding {boolean} - true for add, false for delete
|
||||||
|
* @param favoriteId {string?} - [Comic.favoriteId]
|
||||||
|
* @returns {Promise<any>} - return any value to indicate success
|
||||||
|
*/
|
||||||
|
addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => {
|
||||||
|
if(!isAdding) {
|
||||||
|
let res = await Network.get(`${this.baseUrl}/users-fav_del-id-${favoriteId}.html?ajax=true&_t=${randomDouble(0, 1)}`, {})
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw 'Delete failed'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let res = await Network.post(`${this.baseUrl}/users-save_fav-id-${comicId}.html`, {
|
||||||
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
|
}, `favc_id=${folderId}`)
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw 'Delete failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'ok'
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* load favorite folders.
|
||||||
|
* throw `Login expired` to indicate login expired, App will automatically re-login retry.
|
||||||
|
* if comicId is not null, return favorite folders which contains the comic.
|
||||||
|
* @param comicId {string?}
|
||||||
|
* @returns {Promise<{folders: {[p: string]: string}, favorited: string[]}>} - `folders` is a map of folder id to folder name, `favorited` is a list of folder id which contains the comic
|
||||||
|
*/
|
||||||
|
loadFolders: async (comicId) => {
|
||||||
|
let res = await Network.get(`${this.baseUrl}/users-addfav-id-210814.html`, {})
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw 'Load failed'
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body)
|
||||||
|
let data = {}
|
||||||
|
document.querySelectorAll("option").forEach((option => {
|
||||||
|
if (option.attributes["value"] === "") return
|
||||||
|
data[option.attributes["value"]] = option.text
|
||||||
|
}))
|
||||||
|
return {
|
||||||
|
folders: data,
|
||||||
|
favorited: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* add a folder
|
||||||
|
* @param name {string}
|
||||||
|
* @returns {Promise<any>} - return any value to indicate success
|
||||||
|
*/
|
||||||
|
addFolder: async (name) => {
|
||||||
|
let res = await Network.post(`${this.baseUrl}/users-favc_save-id.html`, {
|
||||||
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
|
}, `favc_name=${encodeURIComponent(name)}`)
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw 'Add failed'
|
||||||
|
}
|
||||||
|
return 'ok'
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* delete a folder
|
||||||
|
* @param folderId {string}
|
||||||
|
* @returns {Promise<void>} - return any value to indicate success
|
||||||
|
*/
|
||||||
|
deleteFolder: async (folderId) => {
|
||||||
|
let res = await Network.get(`${this.baseUrl}/users-favclass_del-id-${folderId}.html?ajax=true&_t=${randomDouble()}`, {})
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw 'Delete failed'
|
||||||
|
}
|
||||||
|
return 'ok'
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* load comics in a folder
|
||||||
|
* throw `Login expired` to indicate login expired, App will automatically re-login retry.
|
||||||
|
* @param page {number}
|
||||||
|
* @param folder {string?} - folder id, null for non-multi-folder
|
||||||
|
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||||||
|
*/
|
||||||
|
loadComics: async (page, folder) => {
|
||||||
|
let url = `${this.baseUrl}/users-users_fav-page-${page}-c-${folder}.html.html`
|
||||||
|
let res = await Network.get(url, {})
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw `Invalid Status Code ${res.status}`
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body)
|
||||||
|
let comicBlocks = document.querySelectorAll("div.asTB")
|
||||||
|
let comics = comicBlocks.map((comic) => {
|
||||||
|
let cover = comic.querySelector("div.asTBcell.thumb > div > img").attributes["src"]
|
||||||
|
cover = 'https:' + cover
|
||||||
|
let time = comic.querySelector("div.box_cel.u_listcon > p.l_catg > span").text.replaceAll("創建時間:", "")
|
||||||
|
let name = comic.querySelector("div.box_cel.u_listcon > p.l_title > a").text;
|
||||||
|
let link = comic.querySelector("div.box_cel.u_listcon > p.l_title > a").attributes["href"];
|
||||||
|
let id = RegExp("(?<=-aid-)[0-9]+").exec(link)[0];
|
||||||
|
let info = comic.querySelector("div.box_cel.u_listcon > p.l_detla").text;
|
||||||
|
let pages = Number(RegExp("(?<=頁數:)[0-9]+").exec(info)[0])
|
||||||
|
let delUrl = comic.querySelector("div.box_cel.u_listcon > p.alopt > a").attributes["onclick"];
|
||||||
|
let favoriteId = RegExp("(?<=del-id-)[0-9]+").exec(delUrl)[0];
|
||||||
|
return new Comic({
|
||||||
|
id: id,
|
||||||
|
title: name,
|
||||||
|
subtitle: time,
|
||||||
|
cover: cover,
|
||||||
|
pages: pages,
|
||||||
|
favoriteId: favoriteId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
let pages = 1
|
||||||
|
let pagesLink = document.querySelectorAll("div.f_left.paginator > a")
|
||||||
|
if(pagesLink.length > 0) {
|
||||||
|
pages = Number(pagesLink[pagesLink.length-1].text)
|
||||||
|
}
|
||||||
|
document.dispose()
|
||||||
|
return {
|
||||||
|
comics: comics,
|
||||||
|
maxPage: pages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// single comic related
|
||||||
|
comic = {
|
||||||
|
/**
|
||||||
|
* load comic info
|
||||||
|
* @param id {string}
|
||||||
|
* @returns {Promise<ComicDetails>}
|
||||||
|
*/
|
||||||
|
loadInfo: async (id) => {
|
||||||
|
let res = await Network.get(`${this.baseUrl}/photos-index-page-1-aid-${id}.html`, {})
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw `Invalid Status Code ${res.status}`
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body)
|
||||||
|
let title = document.querySelector("div.userwrap > h2").text
|
||||||
|
let cover = document.querySelector("div.userwrap > div.asTB > div.asTBcell.uwthumb > img").attributes["src"]
|
||||||
|
cover = 'https:' + cover
|
||||||
|
cover = cover.substring(0, 6) + cover.substring(8)
|
||||||
|
let labels = document.querySelectorAll("div.asTBcell.uwconn > label")
|
||||||
|
let category = labels[0].text.split(":")[1]
|
||||||
|
let pages = Number(RegExp("\\d+").exec(labels[1].text.split(":")[1])[0]);
|
||||||
|
let tagsDom = document.querySelectorAll("a.tagshow");
|
||||||
|
let tags = new Map()
|
||||||
|
tags.set("分類", [category])
|
||||||
|
if(tagsDom.length > 0) {
|
||||||
|
tags.set("標籤", tagsDom.map((e) => e.text))
|
||||||
|
}
|
||||||
|
let description = document.querySelector("div.asTBcell.uwconn > p").text;
|
||||||
|
let uploader = document.querySelector("div.asTBcell.uwuinfo > a > p").text;
|
||||||
|
|
||||||
|
return new ComicDetails({
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
cover: cover,
|
||||||
|
pages: pages,
|
||||||
|
tags: tags,
|
||||||
|
description: description,
|
||||||
|
uploader: uploader,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* [Optional] load thumbnails of a comic
|
||||||
|
* @param id {string}
|
||||||
|
* @param next {string | null | undefined} - next page token, null for first page
|
||||||
|
* @returns {Promise<{thumbnails: string[], next: string?}>} - `next` is next page token, null for no more
|
||||||
|
*/
|
||||||
|
loadThumbnails: async (id, next) => {
|
||||||
|
next = next || '1'
|
||||||
|
let res = await Network.get(`${this.baseUrl}/photos-index-page-${next}-aid-${id}.html`, {});
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw `Invalid Status Code ${res.status}`
|
||||||
|
}
|
||||||
|
let document = new HtmlDocument(res.body)
|
||||||
|
let thumbnails = document.querySelectorAll("div.pic_box.tb > a > img").map((e) => {
|
||||||
|
return 'https:' + e.attributes["src"]
|
||||||
|
})
|
||||||
|
next = (Number(next)+1).toString()
|
||||||
|
let pagesLink = document.querySelector("div.f_left.paginator").children
|
||||||
|
if(pagesLink[pagesLink.length-1].classNames.includes("thispage")) {
|
||||||
|
next = null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
thumbnails: thumbnails,
|
||||||
|
next: next
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* load images of a chapter
|
||||||
|
* @param comicId {string}
|
||||||
|
* @param epId {string?}
|
||||||
|
* @returns {Promise<{images: string[]}>}
|
||||||
|
*/
|
||||||
|
loadEp: async (comicId, epId) => {
|
||||||
|
let res = await Network.get(`${this.baseUrl}/photos-gallery-aid-${comicId}.html`, {})
|
||||||
|
if(res.status !== 200) {
|
||||||
|
throw `Invalid Status Code ${res.status}`
|
||||||
|
}
|
||||||
|
const regex = RegExp(String.raw`//[^"]+/[^"]+\.[^"]+`, 'g');
|
||||||
|
const matches = Array.from(res.body.matchAll(regex));
|
||||||
|
return {
|
||||||
|
images: matches.map((e) => 'https:' + e[0].substring(0, e[0].length-1))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* [Optional] Handle tag click event
|
||||||
|
* @param namespace {string}
|
||||||
|
* @param tag {string}
|
||||||
|
* @returns {{action: string, keyword: string, param: string?}}
|
||||||
|
*/
|
||||||
|
onClickTag: (namespace, tag) => {
|
||||||
|
return {
|
||||||
|
action: 'search',
|
||||||
|
keyword: tag,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
domain: {
|
||||||
|
title: "Domain",
|
||||||
|
type: "input",
|
||||||
|
validator: '^(?!:\\/\\/)(?=.{1,253})([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$',
|
||||||
|
default: 'www.wnacg.com',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user