mirror of
https://github.com/venera-app/venera-configs.git
synced 2025-12-16 17:31:16 +00:00
Compare commits
8 Commits
054be414b4
...
e811e11ac6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e811e11ac6 | ||
|
|
91d1386f91 | ||
|
|
1b62163cd5 | ||
|
|
3ea825dea6 | ||
|
|
96433371e0 | ||
|
|
23e7866b5e | ||
|
|
75d5171b6f | ||
|
|
b2af0a518a |
@@ -33,7 +33,7 @@
|
||||
"name": "紳士漫畫",
|
||||
"fileName": "wnacg.js",
|
||||
"key": "wnacg",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"description": "紳士漫畫漫畫源, 不能使用時請嘗試更換URL"
|
||||
},
|
||||
{
|
||||
@@ -109,5 +109,11 @@
|
||||
"fileName": "lanraragi.js",
|
||||
"key": "lanraragi",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
{
|
||||
"name": "Komga",
|
||||
"fileName": "komga.js",
|
||||
"key": "komga",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
|
||||
752
komga.js
Normal file
752
komga.js
Normal file
@@ -0,0 +1,752 @@
|
||||
/** @type {import('./_venera_.js')} */
|
||||
class Komga extends ComicSource {
|
||||
name = "Komga"
|
||||
|
||||
key = "komga"
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
minAppVersion = "1.4.0"
|
||||
|
||||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/komga.js"
|
||||
|
||||
settings = {
|
||||
base_url: {
|
||||
title: "服务器地址",
|
||||
type: "input",
|
||||
default: "https://demo.komga.org",
|
||||
validator: "^(https?:\\/\\/).+$"
|
||||
},
|
||||
// default_username: {
|
||||
// title: "默认账号",
|
||||
// type: "input",
|
||||
// default: "demo@komga.org"
|
||||
// },
|
||||
// default_password: {
|
||||
// title: "默认密码",
|
||||
// type: "input",
|
||||
// default: "komga-demo"
|
||||
// }
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
let raw = this.loadSetting('base_url')
|
||||
if (typeof raw !== 'string' || !raw.trim()) {
|
||||
raw = this.settings.base_url.default
|
||||
}
|
||||
let value = raw.trim()
|
||||
if (!/^https?:\/\//i.test(value)) {
|
||||
value = `https://${value}`
|
||||
}
|
||||
return value.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
get authToken() {
|
||||
const stored = this.loadData('komga_auth')
|
||||
if (stored) {
|
||||
return stored
|
||||
}
|
||||
const username = this.loadSetting('default_username')
|
||||
const password = this.loadSetting('default_password')
|
||||
if (!username || !password) {
|
||||
return null
|
||||
}
|
||||
const encoded = Convert.encodeBase64(Convert.encodeUtf8(`${username}:${password}`))
|
||||
return typeof encoded === 'string' ? encoded : Convert.decodeUtf8(encoded)
|
||||
}
|
||||
|
||||
get headers() {
|
||||
const headers = { "Accept": "application/json" }
|
||||
const token = this.authToken
|
||||
if (token) headers["Authorization"] = `Basic ${token}`
|
||||
return headers
|
||||
}
|
||||
|
||||
get imageHeaders() {
|
||||
const token = this.authToken
|
||||
return token ? { "Authorization": `Basic ${token}` } : {}
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.refreshReferenceData(false)
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
|
||||
account = {
|
||||
login: async (account, pwd) => {
|
||||
if (!account || !pwd) {
|
||||
throw '账号或密码不能为空'
|
||||
}
|
||||
const basic = Convert.encodeBase64(Convert.encodeUtf8(`${account}:${pwd}`))
|
||||
const token = typeof basic === 'string' ? basic : Convert.decodeUtf8(basic)
|
||||
const res = await Network.get(
|
||||
this.buildUrl('/api/v2/users/me'),
|
||||
{
|
||||
"Accept": "application/json",
|
||||
"Authorization": `Basic ${token}`
|
||||
}
|
||||
)
|
||||
if (res.status === 401) {
|
||||
throw '账号或密码错误'
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
throw `登录失败: ${res.status}`
|
||||
}
|
||||
this.saveData('komga_auth', token)
|
||||
this.saveData('komga_account_email', account)
|
||||
await this.refreshReferenceData(true)
|
||||
return account
|
||||
},
|
||||
logout: () => {
|
||||
this.deleteData('komga_auth')
|
||||
this.deleteData('komga_account_email')
|
||||
this.deleteData('komga_libraries')
|
||||
this.deleteData('komga_tags')
|
||||
this.deleteData('komga_genres')
|
||||
this.deleteData('komga_languages')
|
||||
this.deleteData('komga_collections')
|
||||
this.deleteData('komga_meta_ts')
|
||||
},
|
||||
registerWebsite: null
|
||||
}
|
||||
|
||||
explore = [
|
||||
{
|
||||
title: "Komga",
|
||||
type: "singlePageWithMultiPart",
|
||||
load: async () => {
|
||||
await this.refreshReferenceData(false)
|
||||
const feeds = {}
|
||||
const latest = await this.fetchSeriesList('/api/v1/series/latest', { size: 12, page: 0 })
|
||||
if (latest.comics.length) feeds["最新上架"] = latest.comics
|
||||
const updated = await this.fetchSeriesList('/api/v1/series/updated', { size: 12, page: 0 })
|
||||
if (updated.comics.length) feeds["最近更新"] = updated.comics
|
||||
const libraries = this.loadData('komga_libraries')
|
||||
if (Array.isArray(libraries)) {
|
||||
for (const library of libraries.slice(0, 4)) {
|
||||
const list = await this.fetchSeriesList('/api/v1/series', {
|
||||
page: 0,
|
||||
size: 12,
|
||||
sort: ['metadata.lastModified,desc'],
|
||||
library_id: [library.id]
|
||||
})
|
||||
if (list.comics.length) feeds[`书库 ${library.name}`] = list.comics
|
||||
}
|
||||
}
|
||||
if (!Object.keys(feeds).length) {
|
||||
throw '未找到可展示的数据,请确认已登录且服务器可用'
|
||||
}
|
||||
return feeds
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
category = {
|
||||
title: "Komga",
|
||||
parts: [
|
||||
{
|
||||
name: "常用",
|
||||
type: "dynamic",
|
||||
loader: () => (
|
||||
[
|
||||
{
|
||||
label: "all",
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'all',
|
||||
param: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "书库",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const libraries = this.loadData('komga_libraries')
|
||||
if (!Array.isArray(libraries) || !libraries.length) {
|
||||
return []
|
||||
}
|
||||
return libraries.map((library) => ({
|
||||
label: library.name,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'library',
|
||||
param: library.id,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "合集",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const collections = this.loadData('komga_collections')
|
||||
if (!Array.isArray(collections) || !collections.length) {
|
||||
return []
|
||||
}
|
||||
return collections.map((collection) => ({
|
||||
label: collection.name,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'collection',
|
||||
param: collection.id,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "标签",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const tags = this.loadData('komga_tags')
|
||||
if (!Array.isArray(tags) || !tags.length) {
|
||||
return []
|
||||
}
|
||||
return tags.map((tag) => ({
|
||||
label: tag,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'tag',
|
||||
param: tag,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "语言",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const languages = this.loadData('komga_languages')
|
||||
if (!Array.isArray(languages) || !languages.length) {
|
||||
return []
|
||||
}
|
||||
return languages.map((lang) => ({
|
||||
label: lang,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'language',
|
||||
param: lang,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "题材",
|
||||
type: "dynamic",
|
||||
loader: () => {
|
||||
const genres = this.loadData('komga_genres')
|
||||
if (!Array.isArray(genres) || !genres.length) {
|
||||
return []
|
||||
}
|
||||
return genres.map((genre) => ({
|
||||
label: genre,
|
||||
target: {
|
||||
page: 'category',
|
||||
attributes: {
|
||||
category: 'genre',
|
||||
param: genre,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
],
|
||||
enableRankingPage: false,
|
||||
}
|
||||
|
||||
categoryComics = {
|
||||
load: async (category, param, options, page) => {
|
||||
await this.refreshReferenceData(false)
|
||||
const pageIndex = Math.max(0, (page || 1) - 1)
|
||||
const defaultSort = category === 'all' ? 'created,desc' : 'metadata.lastModified,desc'
|
||||
const sortValue = this.extractOption(options, 0, defaultSort)
|
||||
const query = {
|
||||
page: pageIndex,
|
||||
size: 30,
|
||||
sort: [sortValue]
|
||||
}
|
||||
if (category === 'all') {
|
||||
// const list = await this.fetchBookList('/api/v1/books', query)
|
||||
// return {
|
||||
// comics: list.comics,
|
||||
// maxPage: Math.max(1, list.totalPages)
|
||||
// }
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
if (category === 'library' && param) {
|
||||
query.library_id = [param]
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
if (category === 'collection' && param) {
|
||||
const list = await this.fetchSeriesList(`/api/v1/collections/${param}/series`, query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (category === 'tag' && param) {
|
||||
query.tag = [param]
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (category === 'language' && param){
|
||||
query.language = [param]
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
}
|
||||
|
||||
// if (category === 'genre' && param) query.genre = [param]
|
||||
query.genre = [param]
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
},
|
||||
optionList: [
|
||||
{
|
||||
options: [
|
||||
'*created,desc-添加时间(新→旧)',
|
||||
'created,asc-添加时间(旧→新)',
|
||||
'metadata.lastModified,desc-更新时间(新→旧)',
|
||||
'metadata.lastModified,asc-更新时间(旧→新)',
|
||||
'metadata.titleSort,asc-标题(A-Z)',
|
||||
'metadata.titleSort,desc-标题(Z-A)'
|
||||
],
|
||||
notShowWhen: null,
|
||||
showWhen: null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
search = {
|
||||
load: async (keyword, options, page) => {
|
||||
const pageIndex = Math.max(0, (page || 1) - 1)
|
||||
const sortValue = this.extractOption(options, 0, 'metadata.lastModified,desc')
|
||||
const query = {
|
||||
page: pageIndex,
|
||||
size: 30,
|
||||
sort: [sortValue]
|
||||
}
|
||||
let term = (keyword || '').trim()
|
||||
const colonIdx = term.indexOf(':')
|
||||
if (colonIdx > 0) {
|
||||
const prefix = term.slice(0, colonIdx).toLowerCase()
|
||||
const value = term.slice(colonIdx + 1).trim()
|
||||
if (value) {
|
||||
if (prefix === 'tag') query.tag = [value]
|
||||
else if (prefix === 'author') query.author = [`${value},`]
|
||||
else if (prefix === 'language') query.language = [value]
|
||||
else if (prefix === 'genre') query.genre = [value]
|
||||
else if (prefix === 'publisher') query.publisher = [value]
|
||||
else query.search = value
|
||||
}
|
||||
term = ''
|
||||
}
|
||||
if (term) query.search = term
|
||||
const list = await this.fetchSeriesList('/api/v1/series', query)
|
||||
return {
|
||||
comics: list.comics,
|
||||
maxPage: Math.max(1, list.totalPages)
|
||||
}
|
||||
},
|
||||
optionList: [
|
||||
{
|
||||
type: 'select',
|
||||
options: [
|
||||
'*metadata.lastModified,desc-更新时间(新→旧)',
|
||||
'metadata.lastModified,asc-更新时间(旧→新)',
|
||||
'metadata.titleSort,asc-标题(A-Z)',
|
||||
'metadata.titleSort,desc-标题(Z-A)'
|
||||
],
|
||||
label: '排序',
|
||||
default: null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
comic = {
|
||||
loadInfo: async (id) => {
|
||||
const bookId = this.extractBookId(id)
|
||||
if (bookId) {
|
||||
return await this.loadBookDetails(bookId)
|
||||
}
|
||||
const [series, booksPage] = await Promise.all([
|
||||
this.getJson(`/api/v1/series/${id}`),
|
||||
this.getJson(`/api/v1/series/${id}/books`, {
|
||||
unpaged: true,
|
||||
sort: ['metadata.numberSort,asc']
|
||||
})
|
||||
])
|
||||
const books = Array.isArray(booksPage?.content) ? booksPage.content : []
|
||||
const readable = books.filter((book) => this.isSupportedBook(book))
|
||||
readable.sort((a, b) => this.compareBooks(a, b))
|
||||
const chapters = new Map()
|
||||
readable.forEach((book, index) => {
|
||||
chapters.set(book.id, this.formatBookTitle(book, index))
|
||||
})
|
||||
const metadata = series?.metadata || {}
|
||||
const summary = series?.booksMetadata?.summary || metadata.summary || ''
|
||||
const authors = this.collectAuthors(series?.booksMetadata?.authors)
|
||||
const genres = Array.isArray(metadata.genres) ? metadata.genres : []
|
||||
const tags = Array.isArray(series?.booksMetadata?.tags) ? series.booksMetadata.tags : []
|
||||
const description = summary || '暂无简介'
|
||||
const tagSections = {}
|
||||
if (authors.length) tagSections['作者'] = authors
|
||||
if (genres.length) tagSections['类型'] = this.uniqueArray(genres)
|
||||
if (tags.length) tagSections['标签'] = this.uniqueArray(tags)
|
||||
if (!readable.length && books.length) {
|
||||
tagSections['提示'] = ['该系列包含的项目暂不支持阅读']
|
||||
}
|
||||
const info = new ComicDetails({
|
||||
title: metadata.title || series?.name || id,
|
||||
subTitle: authors.slice(0, 3).join(', '),
|
||||
cover: this.buildUrl(`/api/v1/series/${id}/thumbnail`),
|
||||
description,
|
||||
tags: tagSections,
|
||||
chapters,
|
||||
updateTime: this.formatDate(series?.lastModified),
|
||||
uploadTime: this.formatDate(series?.created),
|
||||
url: series?.url || this.buildUrl(`/series/${id}`)
|
||||
})
|
||||
return info
|
||||
},
|
||||
loadEp: async (comicId, epId) => {
|
||||
let bookId = epId || comicId
|
||||
if (typeof bookId === 'string' && bookId.startsWith('book:')) {
|
||||
bookId = bookId.slice(5)
|
||||
}
|
||||
if (typeof comicId === 'string' && comicId.startsWith('book:') && !epId) {
|
||||
bookId = comicId.slice(5)
|
||||
}
|
||||
const pages = await this.getJson(`/api/v1/books/${bookId}/pages`)
|
||||
const list = Array.isArray(pages) ? pages : []
|
||||
list.sort((a, b) => (a?.number ?? 0) - (b?.number ?? 0))
|
||||
const zeroBased = list.some((page) => (page?.number ?? 1) === 0)
|
||||
const images = list
|
||||
.filter((page) => this.isPageRenderable(page))
|
||||
.map((page) => {
|
||||
const number = page?.number ?? 0
|
||||
return this.buildUrl(`/api/v1/books/${bookId}/pages/${number}`, zeroBased ? { zero_based: true } : null)
|
||||
})
|
||||
return { images }
|
||||
},
|
||||
onImageLoad: (url) => {
|
||||
return {
|
||||
headers: this.imageHeaders
|
||||
}
|
||||
},
|
||||
onThumbnailLoad: () => {
|
||||
return {
|
||||
headers: this.imageHeaders
|
||||
}
|
||||
},
|
||||
onClickTag: (namespace, tag) => {
|
||||
if (!tag) throw '无效的标签'
|
||||
const ns = (namespace || '').toLowerCase()
|
||||
if (ns === '作者') {
|
||||
return {
|
||||
action: 'search',
|
||||
keyword: `author:${tag}`,
|
||||
param: null,
|
||||
}
|
||||
}
|
||||
if (ns === '类型' || ns === '标签') {
|
||||
return {
|
||||
action: 'category',
|
||||
keyword: `genre:${tag}`,
|
||||
param: `${tag}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
action: 'search',
|
||||
keyword: tag,
|
||||
param: null,
|
||||
}
|
||||
},
|
||||
enableTagsTranslate: false,
|
||||
}
|
||||
|
||||
async refreshReferenceData(force) {
|
||||
const token = this.authToken
|
||||
if (!token) {
|
||||
this.saveData('komga_libraries', [])
|
||||
this.saveData('komga_tags', [])
|
||||
this.saveData('komga_genres', [])
|
||||
this.saveData('komga_languages', [])
|
||||
this.saveData('komga_collections', [])
|
||||
return
|
||||
}
|
||||
const now = Date.now()
|
||||
const last = this.loadData('komga_meta_ts')
|
||||
if (!force && last && now - last < 5 * 60 * 1000) return
|
||||
try {
|
||||
const [libraries, tags, languages, collections, genres] = await Promise.all([
|
||||
this.getJson('/api/v1/libraries'),
|
||||
this.getJson('/api/v1/tags/series'),
|
||||
this.getJson('/api/v1/languages'),
|
||||
this.getJson('/api/v1/collections', { unpaged: true, sort: ['name,asc'] }),
|
||||
this.getJson('/api/v1/genres')
|
||||
])
|
||||
const libraryList = Array.isArray(libraries) ? libraries.filter((library) => library && library.id) : []
|
||||
const collectionPage = collections && typeof collections === 'object' ? collections : null
|
||||
const collectionList = Array.isArray(collectionPage?.content) ? collectionPage.content : Array.isArray(collections) ? collections : []
|
||||
this.saveData('komga_libraries', libraryList)
|
||||
this.saveData('komga_tags', Array.isArray(tags) ? tags : [])
|
||||
this.saveData('komga_genres', Array.isArray(genres) ? genres : [])
|
||||
this.saveData('komga_languages', Array.isArray(languages) ? languages : [])
|
||||
this.saveData('komga_collections', collectionList)
|
||||
this.saveData('komga_meta_ts', now)
|
||||
} catch (error) {
|
||||
this.saveData('komga_libraries', [])
|
||||
this.saveData('komga_tags', [])
|
||||
this.saveData('komga_genres', [])
|
||||
this.saveData('komga_languages', [])
|
||||
this.saveData('komga_collections', [])
|
||||
if (String(error) === 'Login expired') throw error
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSeriesList(path, query) {
|
||||
const data = await this.getJson(path, query)
|
||||
const content = Array.isArray(data?.content) ? data.content : []
|
||||
const comics = content.map((item) => this.parseSeries(item)).filter(Boolean)
|
||||
return {
|
||||
comics,
|
||||
totalPages: data?.totalPages ?? 1
|
||||
}
|
||||
}
|
||||
|
||||
async fetchBookList(path, query) {
|
||||
const data = await this.getJson(path, query)
|
||||
const content = Array.isArray(data?.content) ? data.content : []
|
||||
const comics = content.map((item) => this.parseBook(item)).filter(Boolean)
|
||||
return {
|
||||
comics,
|
||||
totalPages: data?.totalPages ?? 1
|
||||
}
|
||||
}
|
||||
|
||||
parseBook(book) {
|
||||
if (!book || !this.isSupportedBook(book)) return null
|
||||
const metadata = book.metadata || {}
|
||||
const title = metadata.title || book.name || book.id
|
||||
const authors = this.collectAuthors(metadata.authors)
|
||||
const tags = Array.isArray(metadata.tags) ? metadata.tags : []
|
||||
const description = metadata.summary || ''
|
||||
const subtitleParts = []
|
||||
if (book.seriesTitle) subtitleParts.push(book.seriesTitle)
|
||||
if (authors.length) subtitleParts.push(authors[0])
|
||||
return new Comic({
|
||||
id: `book:${book.id}`,
|
||||
title,
|
||||
subTitle: subtitleParts.join(' · '),
|
||||
cover: this.buildUrl(`/api/v1/books/${book.id}/thumbnail`),
|
||||
tags: this.uniqueArray(tags).slice(0, 12),
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
extractBookId(id) {
|
||||
if (typeof id !== 'string') return null
|
||||
return id.startsWith('book:') ? id.slice(5) : null
|
||||
}
|
||||
|
||||
async loadBookDetails(bookId) {
|
||||
const book = await this.getJson(`/api/v1/books/${bookId}`)
|
||||
if (!book) throw '未找到该图书'
|
||||
const metadata = book.metadata || {}
|
||||
const authors = this.collectAuthors(metadata.authors)
|
||||
const tags = this.uniqueArray(Array.isArray(metadata.tags) ? metadata.tags : [])
|
||||
const description = metadata.summary || '暂无简介'
|
||||
const tagSections = {}
|
||||
if (authors.length) tagSections['作者'] = authors
|
||||
if (tags.length) tagSections['标签'] = tags
|
||||
if (book.seriesTitle) tagSections['系列'] = [book.seriesTitle]
|
||||
if (!this.isSupportedBook(book)) tagSections['提示'] = ['该图书暂不支持阅读']
|
||||
const chapters = new Map()
|
||||
const chapterTitle = metadata.title || book.name || '立即阅读'
|
||||
chapters.set(book.id, chapterTitle)
|
||||
return new ComicDetails({
|
||||
title: metadata.title || book.name || bookId,
|
||||
subTitle: book.seriesTitle || authors.slice(0, 3).join(', '),
|
||||
cover: this.buildUrl(`/api/v1/books/${bookId}/thumbnail`),
|
||||
description,
|
||||
tags: tagSections,
|
||||
chapters,
|
||||
updateTime: this.formatDate(book.lastModified),
|
||||
uploadTime: this.formatDate(book.created),
|
||||
url: book.url || this.buildUrl(`/books/${bookId}`)
|
||||
})
|
||||
}
|
||||
|
||||
parseSeries(series) {
|
||||
if (!series) return null
|
||||
const metadata = series.metadata || {}
|
||||
const title = metadata.title || series.name || series.id
|
||||
const authors = this.collectAuthors(series?.booksMetadata?.authors)
|
||||
const tags = []
|
||||
if (Array.isArray(metadata.genres)) tags.push(...metadata.genres)
|
||||
if (Array.isArray(series?.booksMetadata?.tags)) tags.push(...series.booksMetadata.tags)
|
||||
const description = series?.booksMetadata?.summary || metadata.summary || ''
|
||||
return new Comic({
|
||||
id: series.id,
|
||||
title,
|
||||
subTitle: authors.slice(0, 2).join(', '),
|
||||
cover: this.buildUrl(`/api/v1/series/${series.id}/thumbnail`),
|
||||
tags: this.uniqueArray(tags).slice(0, 12),
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
collectAuthors(authors) {
|
||||
if (!Array.isArray(authors)) return []
|
||||
return this.uniqueArray(authors.map((author) => author?.name).filter(Boolean))
|
||||
}
|
||||
|
||||
uniqueArray(list) {
|
||||
if (!Array.isArray(list)) return []
|
||||
const set = new Set()
|
||||
const result = []
|
||||
for (const item of list) {
|
||||
const value = typeof item === 'string' ? item.trim() : ''
|
||||
if (!value) continue
|
||||
const key = value.toLowerCase()
|
||||
if (set.has(key)) continue
|
||||
set.add(key)
|
||||
result.push(value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
isSupportedBook(book) {
|
||||
if (!book || !book.media) return false
|
||||
const status = String(book.media.status || '').toUpperCase()
|
||||
if (status && status !== 'READY') return false
|
||||
const mediaType = String(book.media.mediaType || '').toLowerCase()
|
||||
if (!mediaType) return false
|
||||
if (mediaType.includes('epub') || mediaType.includes('pdf') || mediaType.includes('mobi')) return false
|
||||
if ((book.media.pagesCount || 0) <= 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
isPageRenderable(page) {
|
||||
if (!page) return false
|
||||
const mediaType = String(page.mediaType || '').toLowerCase()
|
||||
if (!mediaType) return true
|
||||
return mediaType.startsWith('image/') || mediaType.includes('jpeg') || mediaType.includes('png') || mediaType.includes('webp')
|
||||
}
|
||||
|
||||
compareBooks(a, b) {
|
||||
const aSort = typeof a?.metadata?.numberSort === 'number' ? a.metadata.numberSort : NaN
|
||||
const bSort = typeof b?.metadata?.numberSort === 'number' ? b.metadata.numberSort : NaN
|
||||
if (!Number.isNaN(aSort) && !Number.isNaN(bSort)) return aSort - bSort
|
||||
const aNumber = parseFloat(a?.metadata?.number)
|
||||
const bNumber = parseFloat(b?.metadata?.number)
|
||||
if (!Number.isNaN(aNumber) && !Number.isNaN(bNumber)) return aNumber - bNumber
|
||||
return (a?.metadata?.title || a?.name || '').localeCompare(b?.metadata?.title || b?.name || '')
|
||||
}
|
||||
|
||||
formatBookTitle(book, index) {
|
||||
const metadata = book?.metadata || {}
|
||||
if (metadata.title) return metadata.title
|
||||
if (metadata.number) return `第${metadata.number}卷`
|
||||
if (book?.number != null) return `第${book.number}卷`
|
||||
return `章节 ${index + 1}`
|
||||
}
|
||||
|
||||
extractOption(options, index, fallback) {
|
||||
if (!Array.isArray(options) || options.length <= index) return fallback
|
||||
let value = options[index]
|
||||
if (typeof value !== 'string') return fallback
|
||||
if (value.startsWith('*')) value = value.slice(1)
|
||||
const idx = value.indexOf('-')
|
||||
return idx > -1 ? value.slice(0, idx) : value
|
||||
}
|
||||
|
||||
async getJson(path, query) {
|
||||
const res = await Network.get(this.buildUrl(path, query), this.headers)
|
||||
this.ensureOk(res)
|
||||
const text = res.body
|
||||
if (!text) return null
|
||||
return JSON.parse(text)
|
||||
}
|
||||
|
||||
ensureOk(res) {
|
||||
if (!res) throw '请求失败'
|
||||
if (res.status === 401 || res.status === 403) throw 'Login expired'
|
||||
if (res.status < 200 || res.status >= 300) throw `请求失败: ${res.status}`
|
||||
}
|
||||
|
||||
buildUrl(path, query) {
|
||||
let url = path
|
||||
if (!/^https?:\/\//i.test(path)) {
|
||||
url = `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
}
|
||||
const qs = this.buildQuery(query)
|
||||
return qs ? `${url}?${qs}` : url
|
||||
}
|
||||
|
||||
buildQuery(query) {
|
||||
if (!query) return ''
|
||||
const parts = []
|
||||
for (const key of Object.keys(query)) {
|
||||
const value = query[key]
|
||||
if (value === undefined || value === null) continue
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (item === undefined || item === null) continue
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`)
|
||||
}
|
||||
} else {
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
}
|
||||
}
|
||||
return parts.join('&')
|
||||
}
|
||||
|
||||
formatDate(value) {
|
||||
if (!value) return null
|
||||
try {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return null
|
||||
return date.toISOString().split('T')[0]
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
wnacg.js
1
wnacg.js
@@ -357,6 +357,7 @@ class Wnacg extends ComicSource {
|
||||
favorites = {
|
||||
// whether support multi folders
|
||||
multiFolder: true,
|
||||
isOldToNewSort: true,
|
||||
/**
|
||||
* add or delete favorite.
|
||||
* throw `Login expired` to indicate login expired, App will automatically re-login and re-add/delete favorite
|
||||
|
||||
Reference in New Issue
Block a user