Webnovel updates (#15871)

* Webnovel updates

* Update src/en/webnovel/src/eu/kanade/tachiyomi/extension/en/webnovel/Webnovel.kt

---------

Co-authored-by: arkon <arkon@users.noreply.github.com>
This commit is contained in:
AntsyLich 2023-03-28 19:35:55 +06:00 committed by GitHub
parent a711214620
commit 70395f46c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 50 additions and 54 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Webnovel.com' extName = 'Webnovel.com'
pkgNameSuffix = 'en.webnovel' pkgNameSuffix = 'en.webnovel'
extClass = '.Webnovel' extClass = '.Webnovel'
extVersionCode = 6 extVersionCode = 7
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl import okhttp3.HttpUrl
@ -18,7 +17,6 @@ import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
@ -111,20 +109,17 @@ class Webnovel : HttpSource() {
} }
// Manga details // Manga details
// TODO: Cleanup this block when ext-lib 1.4 is released override fun getMangaUrl(manga: SManga): String = "$baseUrl/comic/${manga.getId}"
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl/comic/${manga.getId}", headers)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(internalMangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
mangaDetailsParse(response) mangaDetailsParse(response)
} }
} }
private fun internalMangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseApiUrl/comic/getComicDetailPage?comicId=${manga.getId}", headers) return GET("$baseApiUrl/comic/getComicDetailPage?comicId=${manga.getId}", headers)
} }
@ -158,8 +153,8 @@ class Webnovel : HttpSource() {
val updateTimes = chapters.map { it.publishTime.toDate() } val updateTimes = chapters.map { it.publishTime.toDate() }
val filteredChapters = chapters val filteredChapters = chapters
// 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 has same tier unlocked as chapter's. // We check if user's tier same or more than chapter's.
.filter { it.userLevel == it.chapterLevel } .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)
@ -185,44 +180,40 @@ class Webnovel : HttpSource() {
if (contains("now", ignoreCase = true)) return Date().time if (contains("now", ignoreCase = true)) return Date().time
val number = DIGIT_REGEX.find(this)?.value?.toIntOrNull() ?: return 0 val number = DIGIT_REGEX.find(this)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val field = when {
contains("yr") -> Calendar.YEAR
return when { contains("mth") -> Calendar.MONTH
contains("yr") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis contains("d") -> Calendar.DAY_OF_MONTH
contains("mth") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis contains("h") -> Calendar.HOUR
contains("d") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis contains("min") -> Calendar.MINUTE
contains("h") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis else -> return 0
contains("min") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
else -> 0
} }
return Calendar.getInstance().apply { add(field, -number) }.timeInMillis
} }
// Pages // Pages
// TODO: Cleanup this block when ext-lib 1.4 is released override fun getChapterUrl(chapter: SChapter): String {
override fun pageListRequest(chapter: SChapter): Request {
val (comicId, chapterId) = chapter.getMangaAndChapterId val (comicId, chapterId) = chapter.getMangaAndChapterId
return GET("$baseUrl/comic/$comicId/$chapterId", headers) return "$baseUrl/comic/$comicId/$chapterId"
} }
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(internalPageListRequest(chapter)) return client.newCall(pageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
pageListParse(response) pageListParse(response)
} }
} }
private fun internalPageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val (comicId, chapterId) = chapter.getMangaAndChapterId val (comicId, chapterId) = chapter.getMangaAndChapterId
return pageListRequest(comicId, chapterId) return pageListRequest(comicId, chapterId)
} }
private val pageRequestHeaders by lazy {
headers.newBuilder().set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0").build()
}
private fun pageListRequest(comicId: String, chapterId: String): Request { private fun pageListRequest(comicId: String, chapterId: String): Request {
return GET("$baseUrl/comic/$comicId/$chapterId", pageRequestHeaders) // 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}")
} }
data class ChapterPage( data class ChapterPage(
@ -239,20 +230,10 @@ class Webnovel : HttpSource() {
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup() val chapterContent = response.checkAndParseAs<ChapterContentResponseDto>().chapterContent
val chapterId = response.request.url.pathSegments[2] return chapterContent.pages.map { ChapterPage(it.id, it.url) }
return document.parseToChapterPage(chapterId).mapIndexed { i, chapterPage -> .also { chapterPageCache[chapterContent.id.toString()] = it }
Page(i, imageUrl = chapterPage.url) .mapIndexed { i, chapterPage -> Page(i, imageUrl = chapterPage.url) }
}
}
private fun Document.parseToChapterPage(chapterId: String): List<ChapterPage> {
return select("#comicPageContainer img").map {
ChapterPage(
id = it.attr("data-page"),
url = it.attr("data-original"),
)
}.also { chapterPageCache[chapterId] = it }
} }
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not Used") override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not Used")
@ -267,7 +248,7 @@ class Webnovel : HttpSource() {
private val SManga.getId: String private val SManga.getId: String
get() { get() {
if (url.toLongOrNull() == null) throw Exception(MIGRATE_MESSAGE) url.toLongOrNull() ?: throw Exception(MIGRATE_MESSAGE)
return url return url
} }
@ -291,7 +272,7 @@ class Webnovel : HttpSource() {
?.takeIf { csrfTokenName in it } ?.takeIf { csrfTokenName in it }
?.substringAfter("$csrfTokenName=") ?.substringAfter("$csrfTokenName=")
?.substringBefore(";") ?.substringBefore(";")
?: throw IOException("'$csrfTokenName' cookie not found.\nOpen in webview to set it.") ?: throw IOException("Open in WebView to set necessary cookies.")
val newUrl = originalRequestUrl.newBuilder() val newUrl = originalRequestUrl.newBuilder()
.addQueryParameter(csrfTokenName, csrfToken) .addQueryParameter(csrfTokenName, csrfToken)
@ -318,11 +299,9 @@ class Webnovel : HttpSource() {
if (cachedPageUrl != null && isPageUrlStillValid(cachedPageUrl.toHttpUrl())) return chain.proceed(originalRequest) if (cachedPageUrl != null && isPageUrlStillValid(cachedPageUrl.toHttpUrl())) return chain.proceed(originalRequest)
// Time to get it from site // Time to get it from site
val pageListResponse = chain.proceed(pageListRequest(comicId, chapterId)) chain.proceed(pageListRequest(comicId, chapterId)).use { pageListParse(it) }
val chapterPages = pageListResponse.asJsoup().parseToChapterPage(chapterId)
pageListResponse.close()
val newPageUrl = chapterPages.firstOrNull { it.id == pageId }?.url?.toHttpUrl() val newPageUrl = chapterPageCache[chapterId]?.firstOrNull { it.id == pageId }?.url?.toHttpUrl()
?: throw IOException("Couldn't regenerate expired image url") ?: throw IOException("Couldn't regenerate expired image url")
val newRequest = originalRequest.newBuilder().url(newPageUrl).build() val newRequest = originalRequest.newBuilder().url(newPageUrl).build()
@ -330,11 +309,11 @@ class Webnovel : HttpSource() {
} }
private fun isPageUrlStillValid(imageUrl: HttpUrl): Boolean { private fun isPageUrlStillValid(imageUrl: HttpUrl): Boolean {
val urlGenerationTime = imageUrl.queryParameter("t")?.toLongOrNull() val urlGenerationTime = imageUrl.queryParameter("t")?.toLongOrNull()?.times(1000)
?: throw IOException("Parameter 't' missing from page url or isn't a long") ?: throw IOException("Couldn't get image generation time from page url")
// Urls are valid for 10 minutes after generation. We check for 9min and 30s just to be safe // Urls are valid for 10 minutes after generation. We check for 9min and 30s just to be safe
return (Date().time / 1000) - urlGenerationTime <= 570 return Date().time - urlGenerationTime <= 570000
} }
private inline fun <reified T> Response.checkAndParseAs(): T = use { private inline fun <reified T> Response.checkAndParseAs(): T = use {
@ -351,7 +330,7 @@ class Webnovel : HttpSource() {
private const val QUERY_SEARCH_PATH = "/search/result" private const val QUERY_SEARCH_PATH = "/search/result"
private const val FILTER_SEARCH_PATH = "/category/categoryAjax" private const val FILTER_SEARCH_PATH = "/category/categoryAjax"
private const val MIGRATE_MESSAGE = "Migrate this entry from \"Webnovel.com\" to \"Webnovel.com\" to update url" private const val MIGRATE_MESSAGE = "Migrate this entry from \"Webnovel.com\" to \"Webnovel.com\" to update its URL"
private val DIGIT_REGEX = "(\\d+)".toRegex() private val DIGIT_REGEX = "(\\d+)".toRegex()

View File

@ -60,3 +60,20 @@ data class ComicChapterDto(
val chapterLevel: Int, val chapterLevel: Int,
val userLevel: Int, val userLevel: Int,
) )
@Serializable
data class ChapterContentResponseDto(
@SerialName("chapterInfo") val chapterContent: ChapterContentDto
)
@Serializable
data class ChapterContentDto(
@SerialName("chapterId") val id: Long,
@SerialName("chapterPage") val pages: List<ChapterPageDto>
)
@Serializable
data class ChapterPageDto(
@SerialName("pageId") val id: String,
val url: String
)