Update WebNovel (#19549)
- Address icon and name changes (was "Webnovel" before) - Make upload dates more accurate
|
@ -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"
|
||||||
|
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 176 KiB |
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|