Zaimanhua: Fix login check & add chapter comments (#10328)
* Zaimanhua: fix login check When the token expires, accessing restricted chapters still returns errno=0. Therefore, the approach has been changed to check whether canRead is false. * Zaimanhua: Use constants for preference keys * Zaimanhua: Add chapter comments This commit adds a feature to display comments at the end of each chapter. - Added a new `CommentDataDto` and a custom `LastStringFromArrayListSerializer` to handle the comment data structure. - Implemented a `commentsInterceptor` to fetch and render comments as an image. - Added a preference option to enable/disable chapter comments. - Updated `fetchPageList` to include the comment page if the preference is enabled. - Modified `imageRequest` to handle comment page requests. - Added helper functions `chapterCommentsUrl` and `parseChapterComments`. * Zaimanhua: tag image requests
This commit is contained in:
parent
e49d76ff14
commit
6c255f4658
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Zaimanhua'
|
||||
extClass = '.Zaimanhua'
|
||||
extVersionCode = 10
|
||||
extVersionCode = 11
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,80 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.zaimanhua
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import eu.kanade.tachiyomi.extension.zh.zaimanhua.Zaimanhua.Companion.COMMENTS_FLAG
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||
import okio.Buffer
|
||||
|
||||
fun parseChapterComments(response: Response, count: Int): List<String> {
|
||||
val result = response.parseAs<ResponseDto<CommentDataDto>>()
|
||||
val comments = result.data.toCommentList()
|
||||
return if (result.errmsg.isNotBlank()) {
|
||||
throw Exception(result.errmsg)
|
||||
} else {
|
||||
comments.take(count)
|
||||
}
|
||||
}
|
||||
|
||||
object CommentsInterceptor : Interceptor {
|
||||
private const val MAX_HEIGHT = 1920
|
||||
private const val WIDTH = 1080
|
||||
private const val UNIT = 32
|
||||
private const val UNIT_F = UNIT.toFloat()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
if (request.tag(String::class) != COMMENTS_FLAG) {
|
||||
return response
|
||||
}
|
||||
val comments = parseChapterComments(response, MAX_HEIGHT / (UNIT * 2) - 1).toMutableList()
|
||||
comments.add(0, "章末吐槽:")
|
||||
|
||||
val paint = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = UNIT_F
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
var height = UNIT
|
||||
val layouts = comments.map {
|
||||
@Suppress("DEPRECATION")
|
||||
StaticLayout(it, paint, WIDTH - 2 * UNIT, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
|
||||
}.takeWhile {
|
||||
val lineHeight = it.height + UNIT
|
||||
if (height + lineHeight <= MAX_HEIGHT) {
|
||||
height += lineHeight
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val bitmap = Bitmap.createBitmap(WIDTH, height, Bitmap.Config.ARGB_8888)
|
||||
bitmap.eraseColor(Color.WHITE)
|
||||
val canvas = Canvas(bitmap)
|
||||
|
||||
var y = UNIT
|
||||
for (layout in layouts) {
|
||||
canvas.save()
|
||||
canvas.translate(UNIT_F, y.toFloat())
|
||||
layout.draw(canvas)
|
||||
canvas.restore()
|
||||
y += layout.height + UNIT
|
||||
}
|
||||
|
||||
val responseBody = Buffer().run {
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream())
|
||||
asResponseBody("image/jpeg".toMediaType())
|
||||
}
|
||||
return response.newBuilder().body(responseBody).build()
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
@ -43,7 +44,7 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
private val mobileBaseUrl = "https://m.zaimanhua.com"
|
||||
private val apiUrl = "https://v4api.zaimanhua.com/app/v1"
|
||||
private val accountApiUrl = "https://account-api.zaimanhua.com/v1"
|
||||
private val checkTokenRegex = Regex("""$apiUrl/comic/(detail|chapter)""")
|
||||
private val checkTokenRegex = Regex("""$apiUrl/comic/chapter""")
|
||||
|
||||
private val json by injectLazy<Json>()
|
||||
|
||||
@ -53,6 +54,7 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
.rateLimit(5)
|
||||
.addInterceptor(::authIntercept)
|
||||
.addInterceptor(::imageRetryInterceptor)
|
||||
.addInterceptor(CommentsInterceptor)
|
||||
.build()
|
||||
|
||||
private fun authIntercept(chain: Interceptor.Chain): Response {
|
||||
@ -64,21 +66,21 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
}
|
||||
|
||||
val response = chain.proceed(request)
|
||||
if (!request.headers["authorization"].isNullOrBlank() && response.peekBody(Long.MAX_VALUE).parseAs<SimpleResponseDto>().errno == 0) {
|
||||
if (!request.headers["authorization"].isNullOrBlank() && response.peekBody(Long.MAX_VALUE).parseAs<ResponseDto<DataWrapperDto<CanReadDto>>>().data.data?.canRead != false) {
|
||||
return response
|
||||
}
|
||||
var token: String = preferences.getString("TOKEN", "")!!
|
||||
var token: String = preferences.getString(TOKEN_PREF, "")!!
|
||||
if (!isValid(token)) {
|
||||
val username = preferences.getString("USERNAME", "")!!
|
||||
val password = preferences.getString("PASSWORD", "")!!
|
||||
val username = preferences.getString(USERNAME_PREF, "")!!
|
||||
val password = preferences.getString(PASSWORD_PREF, "")!!
|
||||
token = getToken(username, password)
|
||||
if (token.isBlank()) {
|
||||
preferences.edit().putString("TOKEN", "").apply()
|
||||
preferences.edit().putString("USERNAME", "").apply()
|
||||
preferences.edit().putString("PASSWORD", "").apply()
|
||||
preferences.edit().putString(TOKEN_PREF, "").apply()
|
||||
preferences.edit().putString(USERNAME_PREF, "").apply()
|
||||
preferences.edit().putString(PASSWORD_PREF, "").apply()
|
||||
return response
|
||||
} else {
|
||||
preferences.edit().putString("TOKEN", token).apply()
|
||||
preferences.edit().putString(TOKEN_PREF, token).apply()
|
||||
apiHeaders = apiHeaders.newBuilder().setToken(token).build()
|
||||
}
|
||||
} else if (!request.headers["authorization"].isNullOrBlank() && request.headers["authorization"] == "Bearer $token") {
|
||||
@ -95,7 +97,7 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
if (token.isNotBlank()) set("authorization", "Bearer $token")
|
||||
}
|
||||
|
||||
private var apiHeaders = headersBuilder().setToken(preferences.getString("TOKEN", "")!!).build()
|
||||
private var apiHeaders = headersBuilder().setToken(preferences.getString(TOKEN_PREF, "")!!).build()
|
||||
|
||||
private fun isValid(token: String): Boolean {
|
||||
if (token.isBlank()) return false
|
||||
@ -176,12 +178,18 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
if (!result.data.data!!.canRead) {
|
||||
throw Exception("用户权限不足,请提升用户等级")
|
||||
}
|
||||
return Observable.just(
|
||||
result.data.data.images.mapIndexed { index, it ->
|
||||
return Observable.fromCallable {
|
||||
val images = result.data.data.images
|
||||
val pageList = images.mapIndexedTo(ArrayList(images.size + 1)) { index, it ->
|
||||
val fragment = json.encodeToString(ImageRetryParamsDto(chapter.url, index))
|
||||
Page(index, imageUrl = "$it#$fragment")
|
||||
},
|
||||
)
|
||||
}
|
||||
if (preferences.getBoolean(COMMENTS_PREF, false)) {
|
||||
val (mangaId, chapterId) = chapter.url.split("/", limit = 2)
|
||||
pageList.add(Page(pageList.size, COMMENTS_FLAG, chapterCommentsUrl(mangaId, chapterId)))
|
||||
}
|
||||
pageList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +197,7 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
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
|
||||
if (response.isSuccessful || request.tag(String::class) != IMAGE_RETRY_FLAG || fragment == null) return response
|
||||
response.close()
|
||||
|
||||
val params = json.decodeFromString<ImageRetryParamsDto>(fragment)
|
||||
@ -203,6 +211,14 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val flag = if (page.url == COMMENTS_FLAG) COMMENTS_FLAG else IMAGE_RETRY_FLAG
|
||||
val reqHeaders = if (page.url == COMMENTS_FLAG) apiHeaders else headers
|
||||
return GET(page.imageUrl!!, reqHeaders).newBuilder()
|
||||
.tag(String::class, flag)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
// Popular
|
||||
@ -266,39 +282,54 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
RankingGroup(),
|
||||
)
|
||||
|
||||
private fun chapterCommentsUrl(comicId: String, chapterId: String) = "$apiUrl/viewpoint/list?comicId=$comicId&chapterId=$chapterId"
|
||||
|
||||
companion object {
|
||||
val USE_CACHE = CacheControl.Builder().maxStale(170, TimeUnit.SECONDS).build()
|
||||
const val USERNAME_PREF = "USERNAME"
|
||||
const val PASSWORD_PREF = "PASSWORD"
|
||||
const val TOKEN_PREF = "TOKEN"
|
||||
const val COMMENTS_PREF = "COMMENTS"
|
||||
const val COMMENTS_FLAG = "COMMENTS"
|
||||
const val IMAGE_RETRY_FLAG = "IMAGE_RETRY"
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = COMMENTS_PREF
|
||||
title = "章末吐槽页"
|
||||
summary = "修改后,已加载的章节需要清除章节缓存才能生效。"
|
||||
setDefaultValue(false)
|
||||
}.let(screen::addPreference)
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = "USERNAME"
|
||||
key = USERNAME_PREF
|
||||
title = "用户名"
|
||||
summary = "该配置被修改后,会清空令牌(Token)以便重新登录;如果登录失败,会清空该配置"
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
// clean token after username/password changed
|
||||
preferences.edit().putString("TOKEN", "").apply()
|
||||
preferences.edit().putString(TOKEN_PREF, "").apply()
|
||||
true
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = "PASSWORD"
|
||||
key = PASSWORD_PREF
|
||||
title = "密码"
|
||||
summary = "该配置被修改后,会清空令牌(Token)以便重新登录;如果登录失败,会清空该配置"
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
// clean token after username/password changed
|
||||
preferences.edit().putString("TOKEN", "").apply()
|
||||
preferences.edit().putString(TOKEN_PREF, "").apply()
|
||||
true
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = "TOKEN"
|
||||
key = TOKEN_PREF
|
||||
title = "令牌(Token)"
|
||||
summary = "当前登录状态:${
|
||||
if (preferences.getString("TOKEN", "").isNullOrEmpty()) "未登录" else "已登录"
|
||||
if (preferences.getString(TOKEN_PREF, "").isNullOrEmpty()) "未登录" else "已登录"
|
||||
}\n填写用户名和密码后,不会立刻尝试登录,会在下次请求时自动尝试"
|
||||
|
||||
setEnabled(false)
|
||||
|
@ -5,7 +5,9 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
@ -170,3 +172,23 @@ data class ImageRetryParamsDto(
|
||||
val url: String,
|
||||
val index: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class CanReadDto(
|
||||
val canRead: Boolean?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class CommentDataDto(
|
||||
val list: List<JsonArray>?,
|
||||
) {
|
||||
fun toCommentList(): List<String> {
|
||||
return if (list.isNullOrEmpty()) {
|
||||
listOf("没有吐槽")
|
||||
} else {
|
||||
list.map { item ->
|
||||
item.last().jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user