WebNovel: Fix cover url and refactor (#6672)
This commit is contained in:
parent
f03fd3c5f7
commit
be9c14bcae
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'WebNovel'
|
||||
extClass = '.WebNovel'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 13
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -22,7 +22,6 @@ import uy.kohesive.injekt.injectLazy
|
|||
import java.io.IOException
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class WebNovel : HttpSource() {
|
||||
|
||||
|
@ -34,7 +33,7 @@ class WebNovel : HttpSource() {
|
|||
|
||||
private val baseApiUrl = "$baseUrl$BASE_API_ENDPOINT"
|
||||
|
||||
private val baseCoverURl = baseUrl.replace("www", "img")
|
||||
private val baseCoverURl = baseUrl.replace("www", "book-pic")
|
||||
|
||||
private val baseCdnUrl = baseUrl.replace("www", "comic-image")
|
||||
|
||||
|
@ -89,60 +88,22 @@ class WebNovel : HttpSource() {
|
|||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val browseResponseDto = if (response.request.url.toString().contains(QUERY_SEARCH_PATH)) {
|
||||
response.parseAsForWebNovel<QuerySearchResponseDto>().browseResponse
|
||||
return if (response.request.url.toString().contains(QUERY_SEARCH_PATH)) {
|
||||
response.parseAsForWebNovel<QuerySearchResponse>().toMangasPage(::getCoverUrl)
|
||||
} else {
|
||||
response.parseAsForWebNovel()
|
||||
response.parseAsForWebNovel<FilterSearchResponse>().toMangasPage(::getCoverUrl)
|
||||
}
|
||||
|
||||
val manga = browseResponseDto.items.map {
|
||||
SManga.create().apply {
|
||||
title = it.name
|
||||
url = it.id
|
||||
thumbnail_url = getCoverUrl(it.id)
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(manga, browseResponseDto.isLast == 0)
|
||||
}
|
||||
|
||||
// Manga details
|
||||
override fun getMangaUrl(manga: SManga): String = "$baseUrl/comic/${manga.getId}"
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET("$baseApiUrl/comic/getComicDetailPage?comicId=${manga.getId}", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val comic = response.parseAsForWebNovel<ComicDetailInfoResponseDto>().info
|
||||
return SManga.create().apply {
|
||||
title = comic.name
|
||||
url = comic.id
|
||||
thumbnail_url = getCoverUrl(comic.id)
|
||||
author = comic.authorName
|
||||
description = buildString {
|
||||
append(comic.description)
|
||||
if (comic.actionStatus == ComicDetailInfoDto.ONGOING && comic.updateCycle.isNotBlank()) {
|
||||
append("\n\nInformation:")
|
||||
append("\n• ${comic.updateCycle.replaceFirstChar { it.uppercase(Locale.ENGLISH) }}")
|
||||
}
|
||||
}
|
||||
genre = comic.categoryName
|
||||
status = when (comic.actionStatus) {
|
||||
ComicDetailInfoDto.ONGOING -> SManga.ONGOING
|
||||
ComicDetailInfoDto.COMPLETED -> SManga.COMPLETED
|
||||
ComicDetailInfoDto.ON_HIATUS -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
return response.parseAsForWebNovel<ComicDetailInfoResponse>().toSManga(::getCoverUrl)
|
||||
}
|
||||
|
||||
// chapters
|
||||
|
@ -151,9 +112,9 @@ class WebNovel : HttpSource() {
|
|||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val chapterList = response.parseAsForWebNovel<ComicChapterListDto>()
|
||||
val comic = chapterList.comicInfo
|
||||
val chapters = chapterList.comicChapters.reversed().asSequence()
|
||||
val chapterList = response.parseAsForWebNovel<ComicChapterListResponse>()
|
||||
val comic = chapterList.comic
|
||||
val chapters = chapterList.chapters.reversed().asSequence()
|
||||
|
||||
val accurateUpdateTimes = runCatching {
|
||||
client.newCall(GET("$WEBNOVEL_UPLOAD_TIME/${comic.id}.json"))
|
||||
|
@ -164,30 +125,19 @@ class WebNovel : HttpSource() {
|
|||
|
||||
val updateTimes = chapters.map { accurateUpdateTimes[it.id] ?: it.publishTime.toDate() }
|
||||
|
||||
// You can pay to get some chapter earlier than others. This privilege is divided into some tiers
|
||||
// We check if user's tier same or more than chapter's.
|
||||
val filteredChapters = chapters.filter { it.userLevel >= it.chapterLevel }
|
||||
val filteredChapters = chapters.filter { it.isVisible }
|
||||
|
||||
// When new privileged chapter is released oldest privileged chapter becomes normal one (in most cases)
|
||||
// but since those normal chapter retain the original upload time we improvise. (This isn't optimal but meh)
|
||||
return filteredChapters.zip(updateTimes) { chapter, updateTime ->
|
||||
val namePrefix = when {
|
||||
chapter.isPremium && !chapter.isAccessibleByUser -> "\uD83D\uDD12 "
|
||||
else -> ""
|
||||
}
|
||||
SChapter.create().apply {
|
||||
name = namePrefix + chapter.name
|
||||
name = if (chapter.isLocked) "\uD83D\uDD12 ${chapter.name}" else chapter.name
|
||||
url = "${comic.id}:${chapter.id}"
|
||||
date_upload = updateTime
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
|
||||
private val ComicChapterDto.isPremium get() = isVip != 0 || price != 0
|
||||
|
||||
// This can mean the chapter is free or user has paid to unlock it (check with [isPremium] for this case)
|
||||
private val ComicChapterDto.isAccessibleByUser get() = isAuth == 1
|
||||
|
||||
private fun String.toDate(): Long {
|
||||
if (contains("now", ignoreCase = true)) return Date().time
|
||||
|
||||
|
@ -224,7 +174,7 @@ class WebNovel : HttpSource() {
|
|||
}
|
||||
|
||||
private fun pageListRequest(comicId: String, chapterId: String): Request {
|
||||
// Given a high [width] parameter it gives the highest resolution image available
|
||||
// Given a high [width] value WebNovel returns the highest resolution image publicly available
|
||||
return GET("$baseApiUrl/comic/getContent?comicId=$comicId&chapterId=$chapterId&width=9999")
|
||||
}
|
||||
|
||||
|
@ -235,14 +185,13 @@ class WebNovel : HttpSource() {
|
|||
|
||||
// LinkedHashMap with a capacity of 25. When exceeding the capacity the oldest entry is removed.
|
||||
private val chapterPageCache = object : LinkedHashMap<String, List<ChapterPage>>() {
|
||||
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, List<ChapterPage>>?): Boolean {
|
||||
return size > 25
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val chapterContent = response.parseAsForWebNovel<ChapterContentResponseDto>().chapterContent
|
||||
val chapterContent = response.parseAsForWebNovel<ChapterContentResponse>().data
|
||||
return chapterContent.pages.map { ChapterPage(it.id, it.url) }
|
||||
.also { chapterPageCache[chapterContent.id.toString()] = it }
|
||||
.mapIndexed { i, chapterPage -> Page(i, imageUrl = chapterPage.url) }
|
||||
|
@ -271,8 +220,8 @@ class WebNovel : HttpSource() {
|
|||
return comicId to chapterId
|
||||
}
|
||||
|
||||
private fun getCoverUrl(comicId: String): String {
|
||||
return "$baseCoverURl/bookcover/$comicId/0/600.jpg"
|
||||
private fun getCoverUrl(comicId: String, coverUpdatedAt: Long): String {
|
||||
return "$baseCoverURl/bookcover/$comicId?imageId=$coverUpdatedAt&imageMogr2/thumbnail/1024x"
|
||||
}
|
||||
|
||||
private fun csrfTokenInterceptor(chain: Interceptor.Chain): Response {
|
||||
|
@ -333,9 +282,9 @@ class WebNovel : HttpSource() {
|
|||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAsForWebNovel(): T = use {
|
||||
val parsed = parseAs<ResponseDto<T>>()
|
||||
val parsed = parseAs<ResponseWrapper<T>>()
|
||||
if (parsed.code != 0) error("Error ${parsed.code}: ${parsed.msg}")
|
||||
requireNotNull(parsed.data) { "Response data is null" }
|
||||
requireNotNull(parsed.data) { "Received response data was null" }
|
||||
}
|
||||
|
||||
private inline fun <reified T> List<*>.firstInstanceOrNull() = firstOrNull { it is T } as? T
|
||||
|
|
|
@ -1,86 +1,181 @@
|
|||
package eu.kanade.tachiyomi.extension.en.webnovel
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class ResponseDto<T>(
|
||||
class ResponseWrapper<T>(
|
||||
val code: Int,
|
||||
val data: T?,
|
||||
val msg: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class QuerySearchResponseDto(
|
||||
@SerialName("comicInfo") val browseResponse: BrowseResponseDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BrowseResponseDto(
|
||||
val isLast: Int,
|
||||
@JsonNames("comicItems") val items: List<ComicInfoDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ComicInfoDto(
|
||||
@JsonNames("bookId", "comicId") val id: String,
|
||||
@JsonNames("bookName", "comicName") val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ComicDetailInfoResponseDto(
|
||||
@SerialName("comicInfo") val info: ComicDetailInfoDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ComicDetailInfoDto(
|
||||
@SerialName("comicId") val id: String,
|
||||
@SerialName("comicName") val name: String,
|
||||
val actionStatus: Int,
|
||||
val authorName: String,
|
||||
val categoryName: String,
|
||||
val description: String,
|
||||
val updateCycle: String,
|
||||
class QuerySearchResponse(
|
||||
@SerialName("comicInfo") private val response: BrowseResponse<QuerySearchItem>,
|
||||
) {
|
||||
companion object {
|
||||
const val ONGOING = 1
|
||||
const val COMPLETED = 2
|
||||
const val ON_HIATUS = 3
|
||||
fun toMangasPage(coverUrl: (id: String, coverUpdatedAt: Long) -> String): MangasPage {
|
||||
return response.toMangasPage(coverUrl)
|
||||
}
|
||||
}
|
||||
|
||||
typealias FilterSearchResponse = BrowseResponse<FilterSearchItem>
|
||||
|
||||
@Serializable
|
||||
data class ComicChapterListDto(
|
||||
val comicInfo: ComicInfoDto,
|
||||
val comicChapters: List<ComicChapterDto>,
|
||||
class BrowseResponse<T : ComicItem>(
|
||||
private val isLast: Int,
|
||||
@JsonNames("comicItems") private val items: List<T>,
|
||||
) {
|
||||
fun toMangasPage(coverUrl: (id: String, coverUpdatedAt: Long) -> String): MangasPage {
|
||||
return MangasPage(
|
||||
mangas = items.map { it.toSManga(coverUrl) },
|
||||
hasNextPage = isLast == 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ComicChapterDto(
|
||||
class QuerySearchItem(
|
||||
@SerialName("comicId") private val id: String,
|
||||
@SerialName("bookName") private val title: String,
|
||||
@SerialName("categoryName") private val genre: String,
|
||||
@SerialName("CV") private val coverUpdatedAt: Long,
|
||||
) : ComicItem {
|
||||
override fun toSManga(coverUrl: (id: String, coverUpdatedAt: Long) -> String): SManga {
|
||||
return SManga.create().also {
|
||||
it.url = id
|
||||
it.title = title
|
||||
it.genre = genre
|
||||
it.thumbnail_url = coverUrl(id, coverUpdatedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class FilterSearchItem(
|
||||
@SerialName("bookId") private val id: String,
|
||||
@SerialName("bookName") private val title: String,
|
||||
@SerialName("authorName") private val author: String,
|
||||
private val description: String,
|
||||
@SerialName("categoryName") private val genre: String,
|
||||
@SerialName("coverUpdateTime") private val coverUpdatedAt: Long,
|
||||
) : ComicItem {
|
||||
override fun toSManga(coverUrl: (id: String, coverUpdatedAt: Long) -> String): SManga {
|
||||
return SManga.create().also {
|
||||
it.url = id
|
||||
it.title = title
|
||||
it.author = author
|
||||
it.description = description
|
||||
it.genre = genre
|
||||
it.thumbnail_url = coverUrl(id, coverUpdatedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ComicDetailInfoResponse(
|
||||
@SerialName("comicInfo") private val comic: ComicDetailInfo,
|
||||
) : ComicItem {
|
||||
override fun toSManga(coverUrl: (id: String, coverUpdatedAt: Long) -> String): SManga {
|
||||
return comic.toSManga(coverUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ComicDetailInfo(
|
||||
@SerialName("comicId") private val id: String,
|
||||
@SerialName("comicName") private val title: String,
|
||||
@SerialName("authorName") private val author: String,
|
||||
private val description: String,
|
||||
private val updateCycle: String,
|
||||
@SerialName("categoryName") private val genre: String,
|
||||
@SerialName("actionStatus") private val status: Int,
|
||||
@SerialName("CV") private val coverUpdatedAt: Long,
|
||||
) : ComicItem {
|
||||
override fun toSManga(coverUrl: (id: String, coverUpdatedAt: Long) -> String): SManga {
|
||||
return SManga.create().also {
|
||||
it.url = id
|
||||
it.title = title
|
||||
it.author = author
|
||||
it.description = buildString {
|
||||
append(description)
|
||||
if (status == ONGOING && updateCycle.isNotBlank()) {
|
||||
append("\n\nInformation:")
|
||||
append("\n• ${updateCycle.replaceFirstChar { c -> c.uppercase(Locale.ENGLISH) }}")
|
||||
}
|
||||
}
|
||||
it.genre = genre
|
||||
it.status = when (status) {
|
||||
ONGOING -> SManga.ONGOING
|
||||
COMPLETED -> SManga.COMPLETED
|
||||
ON_HIATUS -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
it.thumbnail_url = coverUrl(id, coverUpdatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ONGOING = 1
|
||||
private const val COMPLETED = 2
|
||||
private const val ON_HIATUS = 3
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ComicChapterListResponse(
|
||||
@SerialName("comicInfo") val comic: Comic,
|
||||
@SerialName("comicChapters") val chapters: List<ComicChapter>,
|
||||
) {
|
||||
@Serializable
|
||||
class Comic(@SerialName("comicId") val id: String)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ComicChapter(
|
||||
@SerialName("chapterId") val id: String,
|
||||
@SerialName("chapterName") val name: String,
|
||||
val publishTime: String,
|
||||
val price: Int,
|
||||
val isVip: Int,
|
||||
val isAuth: Int,
|
||||
val chapterLevel: Int,
|
||||
val userLevel: Int,
|
||||
|
||||
private val chapterLevel: Int,
|
||||
private val userLevel: Int,
|
||||
private val price: Int,
|
||||
private val isVip: Int,
|
||||
private val isAuth: Int,
|
||||
) {
|
||||
val isLocked = isPremium && !isAccessibleByUser
|
||||
|
||||
// You can pay to get some chapter earlier than others. This privilege is divided into some tiers
|
||||
// We check if user's tier same or more than chapter's.
|
||||
val isVisible = userLevel >= chapterLevel
|
||||
|
||||
private val isPremium: Boolean get() = isVip != 0 || price != 0
|
||||
|
||||
// This can mean the chapter is free or user has paid to unlock it (check with [isPremium] for this case)
|
||||
private val isAccessibleByUser: Boolean get() = isAuth == 1
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterContentResponse(
|
||||
@SerialName("chapterInfo") val data: ChapterContent,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterContentResponseDto(
|
||||
@SerialName("chapterInfo") val chapterContent: ChapterContentDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterContentDto(
|
||||
class ChapterContent(
|
||||
@SerialName("chapterId") val id: Long,
|
||||
@SerialName("chapterPage") val pages: List<ChapterPageDto>,
|
||||
@SerialName("chapterPage") val pages: List<ChapterPage>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterPageDto(
|
||||
class ChapterPage(
|
||||
@SerialName("pageId") val id: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
interface ComicItem {
|
||||
fun toSManga(coverUrl: (id: String, coverUpdatedAt: Long) -> String): SManga
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue