Update WebNovel (#19549)

- Address icon and name changes (was "Webnovel" before)
- Make upload dates more accurate
This commit is contained in:
AntsyLich 2024-01-05 23:27:56 +06:00 committed by GitHub
parent fd0a1f0328
commit 4af9f73ec7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 39 additions and 29 deletions

View File

@ -3,10 +3,10 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Webnovel.com' extName = 'WebNovel'
pkgNameSuffix = 'en.webnovel' pkgNameSuffix = 'en.webnovel'
extClass = '.Webnovel' extClass = '.WebNovel'
extVersionCode = 8 extVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 176 KiB

View File

@ -24,12 +24,14 @@ import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
class Webnovel : HttpSource() { class WebNovel : HttpSource() {
override val name = "Webnovel.com" override val name = "WebNovel"
override val baseUrl = "https://www.webnovel.com" override val baseUrl = "https://www.webnovel.com"
override val id = 4081135203808920563
private val baseApiUrl = "$baseUrl$BASE_API_ENDPOINT" private val baseApiUrl = "$baseUrl$BASE_API_ENDPOINT"
private val baseCoverURl = baseUrl.replace("www", "img") private val baseCoverURl = baseUrl.replace("www", "img")
@ -52,9 +54,7 @@ class Webnovel : HttpSource() {
override fun popularMangaRequest(page: Int): Request = searchMangaRequest( override fun popularMangaRequest(page: Int): Request = searchMangaRequest(
page = page, page = page,
query = "", query = "",
filters = FilterList( filters = FilterList(SortByFilter(default = 1)),
SortByFilter(default = 1),
),
) )
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response) override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
@ -63,9 +63,7 @@ class Webnovel : HttpSource() {
override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest( override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(
page = page, page = page,
query = "", query = "",
filters = FilterList( filters = FilterList(SortByFilter(default = 5)),
SortByFilter(default = 5),
),
) )
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response) override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
@ -92,10 +90,9 @@ class Webnovel : HttpSource() {
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val browseResponseDto = if (response.request.url.toString().contains(QUERY_SEARCH_PATH)) { val browseResponseDto = if (response.request.url.toString().contains(QUERY_SEARCH_PATH)) {
response.checkAndParseAs<QuerySearchResponseDto>().browseResponse response.parseAsForWebNovel<QuerySearchResponseDto>().browseResponse
} else { } else {
// Due to the previous line this automatically parses as "BrowseResponseDto" response.parseAsForWebNovel()
response.checkAndParseAs()
} }
val manga = browseResponseDto.items.map { val manga = browseResponseDto.items.map {
@ -125,7 +122,7 @@ class Webnovel : HttpSource() {
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val comic = response.checkAndParseAs<ComicDetailInfoResponseDto>().info val comic = response.parseAsForWebNovel<ComicDetailInfoResponseDto>().info
return SManga.create().apply { return SManga.create().apply {
title = comic.name title = comic.name
url = comic.id url = comic.id
@ -154,15 +151,22 @@ class Webnovel : HttpSource() {
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val chapterList = response.checkAndParseAs<ComicChapterListDto>() val chapterList = response.parseAsForWebNovel<ComicChapterListDto>()
val comic = chapterList.comicInfo val comic = chapterList.comicInfo
val chapters = chapterList.comicChapters.reversed().asSequence() val chapters = chapterList.comicChapters.reversed().asSequence()
val updateTimes = chapters.map { it.publishTime.toDate() } val accurateUpdateTimes = runCatching {
val filteredChapters = chapters client.newCall(GET("$WEBNOVEL_UPLOAD_TIME/${comic.id}.json"))
.execute()
.parseAs<Map<String, Long>>()
}
.getOrDefault(emptyMap())
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 // 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. // We check if user's tier same or more than chapter's.
.filter { it.userLevel >= it.chapterLevel } val filteredChapters = chapters.filter { it.userLevel >= it.chapterLevel }
// When new privileged chapter is released oldest privileged chapter becomes normal one (in most cases) // 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) // but since those normal chapter retain the original upload time we improvise. (This isn't optimal but meh)
@ -221,7 +225,7 @@ class Webnovel : HttpSource() {
private fun pageListRequest(comicId: String, chapterId: String): Request { private fun pageListRequest(comicId: String, chapterId: String): Request {
// Given a high [width] parameter it gives the highest resolution image available // Given a high [width] parameter it gives the highest resolution image available
return GET("$baseApiUrl/comic/getContent?comicId=$comicId&chapterId=$chapterId&width=${Short.MAX_VALUE}") return GET("$baseApiUrl/comic/getContent?comicId=$comicId&chapterId=$chapterId&width=9999")
} }
data class ChapterPage( data class ChapterPage(
@ -238,7 +242,7 @@ class Webnovel : HttpSource() {
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val chapterContent = response.checkAndParseAs<ChapterContentResponseDto>().chapterContent val chapterContent = response.parseAsForWebNovel<ChapterContentResponseDto>().chapterContent
return chapterContent.pages.map { ChapterPage(it.id, it.url) } return chapterContent.pages.map { ChapterPage(it.id, it.url) }
.also { chapterPageCache[chapterContent.id.toString()] = it } .also { chapterPageCache[chapterContent.id.toString()] = it }
.mapIndexed { i, chapterPage -> Page(i, imageUrl = chapterPage.url) } .mapIndexed { i, chapterPage -> Page(i, imageUrl = chapterPage.url) }
@ -277,13 +281,13 @@ class Webnovel : HttpSource() {
if (!originalRequestUrl.toString().contains(BASE_API_ENDPOINT)) return chain.proceed(originalRequest) if (!originalRequestUrl.toString().contains(BASE_API_ENDPOINT)) return chain.proceed(originalRequest)
val csrfToken = originalRequest.header("cookie") val csrfToken = originalRequest.header("cookie")
?.takeIf { csrfTokenName in it } ?.takeIf { CSRF_TOKEN_NAME in it }
?.substringAfter("$csrfTokenName=") ?.substringAfter("$CSRF_TOKEN_NAME=")
?.substringBefore(";") ?.substringBefore(";")
?: throw IOException("Open in WebView to set necessary cookies.") ?: throw IOException("Open in WebView to set necessary cookies.")
val newUrl = originalRequestUrl.newBuilder() val newUrl = originalRequestUrl.newBuilder()
.addQueryParameter(csrfTokenName, csrfToken) .addQueryParameter(CSRF_TOKEN_NAME, csrfToken)
.build() .build()
val newRequest = originalRequest.newBuilder().url(newUrl).build() val newRequest = originalRequest.newBuilder().url(newUrl).build()
@ -324,8 +328,12 @@ class Webnovel : HttpSource() {
return Date().time - urlGenerationTime <= 570000 return Date().time - urlGenerationTime <= 570000
} }
private inline fun <reified T> Response.checkAndParseAs(): T = use { private inline fun <reified T> Response.parseAs(): T = use {
val parsed = json.decodeFromString<ResponseDto<T>>(it.body.string()) json.decodeFromString<T>(it.body.string())
}
private inline fun <reified T> Response.parseAsForWebNovel(): T = use {
val parsed = parseAs<ResponseDto<T>>()
if (parsed.code != 0) error("Error ${parsed.code}: ${parsed.msg}") if (parsed.code != 0) error("Error ${parsed.code}: ${parsed.msg}")
requireNotNull(parsed.data) { "Response data is null" } requireNotNull(parsed.data) { "Response data is null" }
} }
@ -342,6 +350,8 @@ class Webnovel : HttpSource() {
private val DIGIT_REGEX = "(\\d+)".toRegex() private val DIGIT_REGEX = "(\\d+)".toRegex()
private const val csrfTokenName = "_csrfToken" private const val CSRF_TOKEN_NAME = "_csrfToken"
private const val WEBNOVEL_UPLOAD_TIME = "https://antsylich.github.io/webnovel-upload-time"
} }
} }