Zaimanhua: fix image url expire problem & incorrect chapter upload time (#5455)

* Zaimanhua: fix incorrect chapter upload time

For some chapters, api will always return current time as upload time.

* Zaimanhua: fix image url expire problem

Store params in the fragment of the imageUrl, re-fetch the image URL if loading fails
This commit is contained in:
zhongfly 2024-10-13 09:33:32 +08:00 committed by Draff
parent 86c7d2e64b
commit d1b75272d0
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 61 additions and 11 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Zaimanhua' extName = 'Zaimanhua'
extClass = '.Zaimanhua' extClass = '.Zaimanhua'
extVersionCode = 1 extVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -15,6 +15,10 @@ 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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl import okhttp3.HttpUrl
@ -24,9 +28,13 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.Response import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.security.MessageDigest import java.security.MessageDigest
import java.util.concurrent.TimeUnit
class Zaimanhua : HttpSource(), ConfigurableSource { class Zaimanhua : HttpSource(), ConfigurableSource {
override val lang = "zh" override val lang = "zh"
@ -37,11 +45,16 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
private val apiUrl = "https://v4api.zaimanhua.com/app/v1" private val apiUrl = "https://v4api.zaimanhua.com/app/v1"
private val accountApiUrl = "https://account-api.zaimanhua.com/v1" private val accountApiUrl = "https://account-api.zaimanhua.com/v1"
private val json by injectLazy<Json>()
private val preferences: SharedPreferences = private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
override val client: OkHttpClient = override val client: OkHttpClient = network.client.newBuilder()
network.client.newBuilder().rateLimit(5).addInterceptor(::authIntercept).build() .rateLimit(5)
.addInterceptor(::authIntercept)
.addInterceptor(::imageRetryInterceptor)
.build()
private fun authIntercept(chain: Interceptor.Chain): Response { private fun authIntercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
@ -129,24 +142,48 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
} }
} }
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
// PageList // PageList
// path: "/comic/chapter/mangaId/chapterId" // path: "/comic/chapter/mangaId/chapterId"
override fun pageListRequest(chapter: SChapter) = private fun pageListApiRequest(path: String): Request =
GET("$apiUrl/comic/chapter/${chapter.url}", apiHeaders) GET("$apiUrl/comic/chapter/$path", apiHeaders, USE_CACHE)
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
val response = client.newCall(pageListApiRequest(chapter.url)).execute()
val result = response.parseAs<ResponseDto<DataWrapperDto<ChapterImagesDto>>>() val result = response.parseAs<ResponseDto<DataWrapperDto<ChapterImagesDto>>>()
if (result.errmsg.isNotBlank()) { if (result.errmsg.isNotBlank()) {
throw Exception(result.errmsg) throw Exception(result.errmsg)
} else { } else {
return result.data.data!!.images.mapIndexed { index, it -> return Observable.just(
Page(index, imageUrl = it) result.data.data!!.images.mapIndexed { index, it ->
} val fragment = json.encodeToString(ImageRetryParamsDto(chapter.url, index))
Page(index, imageUrl = "$it#$fragment")
},
)
} }
} }
private fun imageRetryInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val fragment = request.url.fragment
if (response.isSuccessful || request.url.host != "images.zaimanhua.com" || fragment == null) return response
response.close()
val params = json.decodeFromString<ImageRetryParamsDto>(fragment)
val pageListResponse = client.newCall(pageListApiRequest(params.url)).execute()
val result = pageListResponse.parseAs<ResponseDto<DataWrapperDto<ChapterImagesDto>>>()
if (result.errmsg.isNotBlank()) {
throw IOException(result.errmsg)
} else {
val imageUrl = result.data.data!!.images[params.index]
return chain.proceed(GET(imageUrl, headers))
}
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
// Popular // Popular
private fun rankApiUrl(): HttpUrl.Builder = private fun rankApiUrl(): HttpUrl.Builder =
"$apiUrl/comic/rank/list".toHttpUrl().newBuilder().addQueryParameter("by_time", "3") "$apiUrl/comic/rank/list".toHttpUrl().newBuilder().addQueryParameter("by_time", "3")
@ -187,6 +224,9 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
return MangasPage(mangas.map { it.toSManga() }, true) return MangasPage(mangas.map { it.toSManga() }, true)
} }
companion object {
val USE_CACHE = CacheControl.Builder().maxStale(170, TimeUnit.SECONDS).build()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
EditTextPreference(screen.context).apply { EditTextPreference(screen.context).apply {

View File

@ -47,10 +47,14 @@ class ChapterGroupDto(
fun toSChapterList(mangaId: String): List<SChapter> { fun toSChapterList(mangaId: String): List<SChapter> {
val groupName = title val groupName = title
val isDefaultGroup = groupName == "连载" val isDefaultGroup = groupName == "连载"
val current = System.currentTimeMillis()
return data.map { return data.map {
it.toSChapterInternal().apply { it.toSChapterInternal().apply {
url = "$mangaId/$url" url = "$mangaId/$url"
if (!isDefaultGroup) scanlator = groupName if (!isDefaultGroup) scanlator = groupName
// For some chapters, api will always return current time as upload time
// Therefore upload times that differ too little from the current time will be ignored
if ((current - date_upload) < 10000) date_upload = 0
} }
} }
} }
@ -142,3 +146,9 @@ class ResponseDto<T>(
val errmsg: String = "", val errmsg: String = "",
val data: T, val data: T,
) )
@Serializable
data class ImageRetryParamsDto(
val url: String,
val index: Int,
)