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:
parent
a711214620
commit
70395f46c1
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue