Files
venera-configs/wnacg.js
lost one c75cbafb25 fix wnacg.js (#68)
修复页数显示
2025-05-08 22:57:21 +08:00

588 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.2"
minAppVersion = "1.0.0"
// update url
url = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@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(/\s+/g, '')
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:${title}@${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.replaceAll(',', '')
const comicsPerPage = 24
let pages = Math.ceil(Number(total) / comicsPerPage)
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 = labels[1].text.split("")[1];
let tagsDom = document.querySelectorAll("a.tagshow");
let tags = new Map()
tags.set("頁數", [pages])
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',
},
}
}