add 紳士漫畫;

update template
This commit is contained in:
nyne
2024-10-20 11:12:53 +08:00
parent ccf3fb20af
commit 2d1c696ab3
4 changed files with 743 additions and 26 deletions

585
wnacg.js Normal file
View 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',
},
}
}