Zaimanhua: add genre filter & check token expiration (#10357)
* Zaimanhua: make comments list immutable * Zaimanhua: add genre filter Also refactors the ranking filter to allow disabling it. * Zaimanhua: check JWT token expiration * Zaimanhua: use parseAs functions from utils * misc
This commit is contained in:
parent
0fae25ac43
commit
105e329c47
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Zaimanhua'
|
||||
extClass = '.Zaimanhua'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 13
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import eu.kanade.tachiyomi.extension.zh.zaimanhua.Zaimanhua.Companion.COMMENTS_FLAG
|
||||
import keiyoushi.utils.parseAs
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Response
|
||||
@ -60,7 +61,7 @@ object CommentsInterceptor : Interceptor {
|
||||
true,
|
||||
)
|
||||
|
||||
val comments = parseChapterComments(response).toMutableList()
|
||||
val comments = parseChapterComments(response)
|
||||
val paintBody = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = BODY_FONT_SIZE
|
||||
|
||||
@ -1,21 +1,6 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.zaimanhua
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
val json: Json by injectLazy()
|
||||
|
||||
inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
|
||||
inline fun <reified T> ResponseBody.parseAs(): T {
|
||||
return json.decodeFromString(this.string())
|
||||
}
|
||||
|
||||
fun parseStatus(status: String): Int = when (status) {
|
||||
"连载中" -> SManga.ONGOING
|
||||
|
||||
@ -19,17 +19,6 @@ class RankingGroup : Filter.Group<Filter<*>>(
|
||||
SortFilter(),
|
||||
),
|
||||
) {
|
||||
private class TimeFilter : QueryFilter(
|
||||
"榜单",
|
||||
"by_time",
|
||||
arrayOf(
|
||||
Pair("日排行", "0"),
|
||||
Pair("周排行", "1"),
|
||||
Pair("月排行", "2"),
|
||||
Pair("总排行", "3"),
|
||||
),
|
||||
)
|
||||
|
||||
private class SortFilter : QueryFilter(
|
||||
"排序",
|
||||
"rank_type",
|
||||
@ -40,3 +29,130 @@ class RankingGroup : Filter.Group<Filter<*>>(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
class TimeFilter : QueryFilter(
|
||||
"榜单",
|
||||
"by_time",
|
||||
arrayOf(
|
||||
Pair("不查看榜单", ""),
|
||||
Pair("日排行", "0"),
|
||||
Pair("周排行", "1"),
|
||||
Pair("月排行", "2"),
|
||||
Pair("总排行", "3"),
|
||||
),
|
||||
)
|
||||
|
||||
class GenreGroup : Filter.Group<Filter<*>>(
|
||||
"筛选出满足以下所有条件的漫画",
|
||||
listOf<Filter<*>>(
|
||||
SortTypeFilter(),
|
||||
StatusFilter(),
|
||||
CateFilter(),
|
||||
ZoneFilter(),
|
||||
ThemeFilter(),
|
||||
),
|
||||
) {
|
||||
|
||||
private class SortTypeFilter : QueryFilter(
|
||||
"排序",
|
||||
"sortType",
|
||||
arrayOf(
|
||||
Pair("更新排序", "1"),
|
||||
Pair("人气排序", "2"),
|
||||
),
|
||||
)
|
||||
|
||||
private class StatusFilter : QueryFilter(
|
||||
"进度",
|
||||
"status",
|
||||
arrayOf(
|
||||
Pair("全部", "0"),
|
||||
Pair("连载中", "2309"),
|
||||
Pair("已完结", "2310"),
|
||||
Pair("短篇", "29205"),
|
||||
),
|
||||
)
|
||||
|
||||
private class CateFilter : QueryFilter(
|
||||
"读者群",
|
||||
"cate",
|
||||
arrayOf(
|
||||
Pair("全部", "0"),
|
||||
Pair("少年漫画", "3262"),
|
||||
Pair("少女漫画", "3263"),
|
||||
Pair("青年漫画", "3264"),
|
||||
Pair("女青漫画", "13626"),
|
||||
),
|
||||
)
|
||||
|
||||
private class ZoneFilter : QueryFilter(
|
||||
"地区",
|
||||
"zone",
|
||||
arrayOf(
|
||||
Pair("全部", "0"),
|
||||
Pair("日本", "2304"),
|
||||
Pair("韩国", "2305"),
|
||||
Pair("欧美", "2306"),
|
||||
Pair("港台", "2307"),
|
||||
Pair("内地", "2308"),
|
||||
Pair("其他", "8435"),
|
||||
),
|
||||
)
|
||||
|
||||
private class ThemeFilter : QueryFilter(
|
||||
"题材",
|
||||
"theme",
|
||||
arrayOf(
|
||||
Pair("全部", "0"),
|
||||
Pair("冒险", "4"),
|
||||
Pair("欢乐向", "5"),
|
||||
Pair("格斗", "6"),
|
||||
Pair("科幻", "7"),
|
||||
Pair("爱情", "8"),
|
||||
Pair("侦探", "9"),
|
||||
Pair("竞技", "10"),
|
||||
Pair("魔法", "11"),
|
||||
Pair("神鬼", "12"),
|
||||
Pair("校园", "13"),
|
||||
Pair("惊悚", "14"),
|
||||
Pair("其他", "16"),
|
||||
Pair("四格", "17"),
|
||||
Pair("亲情", "3242"),
|
||||
Pair("ゆり", "3243"),
|
||||
Pair("秀吉", "3244"),
|
||||
Pair("悬疑", "3245"),
|
||||
Pair("纯爱", "3246"),
|
||||
Pair("热血", "3248"),
|
||||
Pair("泛爱", "3249"),
|
||||
Pair("历史", "3250"),
|
||||
Pair("战争", "3251"),
|
||||
Pair("萌系", "3252"),
|
||||
Pair("宅系", "3253"),
|
||||
Pair("治愈", "3254"),
|
||||
Pair("励志", "3255"),
|
||||
Pair("武侠", "3324"),
|
||||
Pair("机战", "3325"),
|
||||
Pair("音乐舞蹈", "3326"),
|
||||
Pair("美食", "3327"),
|
||||
Pair("职场", "3328"),
|
||||
Pair("西方魔幻", "3365"),
|
||||
Pair("高清单行", "4459"),
|
||||
Pair("TS", "4518"),
|
||||
Pair("东方", "5077"),
|
||||
Pair("魔幻", "5806"),
|
||||
Pair("奇幻", "5848"),
|
||||
Pair("节操", "6219"),
|
||||
Pair("轻小说", "6316"),
|
||||
Pair("颜艺", "6437"),
|
||||
Pair("搞笑", "7568"),
|
||||
Pair("仙侠", "7900"),
|
||||
Pair("舰娘", "13627"),
|
||||
Pair("动画", "17192"),
|
||||
Pair("AA", "18522"),
|
||||
Pair("福瑞", "23323"),
|
||||
Pair("生存", "23388"),
|
||||
Pair("日常", "30788"),
|
||||
Pair("画集", "31137"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.zaimanhua
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
@ -9,13 +10,16 @@ import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.firstInstanceOrNull
|
||||
import keiyoushi.utils.getPreferences
|
||||
import keiyoushi.utils.parseAs
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -66,7 +70,7 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
}
|
||||
|
||||
val response = chain.proceed(request)
|
||||
if (!request.headers["authorization"].isNullOrBlank() && response.peekBody(Long.MAX_VALUE).parseAs<ResponseDto<DataWrapperDto<CanReadDto>>>().data.data?.canRead != false) {
|
||||
if (!request.headers["authorization"].isNullOrBlank() && response.peekBody(Long.MAX_VALUE).string().parseAs<ResponseDto<DataWrapperDto<CanReadDto>>>().data.data?.canRead != false) {
|
||||
return response
|
||||
}
|
||||
var token: String = preferences.getString(TOKEN_PREF, "")!!
|
||||
@ -75,9 +79,9 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
val password = preferences.getString(PASSWORD_PREF, "")!!
|
||||
token = getToken(username, password)
|
||||
if (token.isBlank()) {
|
||||
preferences.edit().putString(TOKEN_PREF, "").apply()
|
||||
preferences.edit().putString(USERNAME_PREF, "").apply()
|
||||
preferences.edit().putString(PASSWORD_PREF, "").apply()
|
||||
preferences.edit().putString(TOKEN_PREF, "")
|
||||
.putString(USERNAME_PREF, "")
|
||||
.putString(PASSWORD_PREF, "").apply()
|
||||
return response
|
||||
} else {
|
||||
preferences.edit().putString(TOKEN_PREF, token).apply()
|
||||
@ -101,6 +105,11 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
|
||||
private fun isValid(token: String): Boolean {
|
||||
if (token.isBlank()) return false
|
||||
val parts = token.split(".")
|
||||
if (parts.size != 3) throw Exception("token格式错误,不符合JWT规范")
|
||||
val payload = Base64.decode(parts[1], Base64.DEFAULT).toString(Charsets.UTF_8).parseAs<JwtPayload>()
|
||||
if (payload.expirationTime * 1000 < System.currentTimeMillis()) return false
|
||||
|
||||
val response = client.newCall(
|
||||
GET(
|
||||
"$accountApiUrl/userInfo/get",
|
||||
@ -233,24 +242,32 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
apiHeaders,
|
||||
)
|
||||
|
||||
private fun genreApiUrl(): HttpUrl.Builder =
|
||||
"$apiUrl/comic/filter/list".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("size", DEFAULT_PAGE_SIZE.toString())
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response)
|
||||
|
||||
// Search
|
||||
private fun searchApiUrl(): HttpUrl.Builder =
|
||||
"$apiUrl/search/index".toHttpUrl().newBuilder().addQueryParameter("source", "0")
|
||||
.addQueryParameter("size", "20")
|
||||
.addQueryParameter("size", DEFAULT_PAGE_SIZE.toString())
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val ranking = filters.filterIsInstance<RankingGroup>().firstOrNull()
|
||||
val url = if (query.isEmpty() && ranking != null) {
|
||||
rankApiUrl().apply {
|
||||
ranking.state.filterIsInstance<QueryFilter>().forEach {
|
||||
it.addQuery(this)
|
||||
}
|
||||
val ranking = filters.firstInstanceOrNull<RankingGroup>()
|
||||
val genres = filters.firstInstanceOrNull<GenreGroup>()
|
||||
val url = when {
|
||||
query.isEmpty() && ranking != null && (ranking.state[0] as TimeFilter).state != 0 -> rankApiUrl().apply {
|
||||
ranking.state.filterIsInstance<QueryFilter>().forEach { it.addQuery(this) }
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
} else {
|
||||
searchApiUrl().apply {
|
||||
|
||||
query.isEmpty() && genres != null -> genreApiUrl().apply {
|
||||
genres.state.filterIsInstance<QueryFilter>().forEach { it.addQuery(this) }
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
|
||||
else -> searchApiUrl().apply {
|
||||
addQueryParameter("keyword", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
@ -258,12 +275,15 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage =
|
||||
if (response.request.url.toString().startsWith("$apiUrl/comic/rank/list")) {
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val url = response.request.url
|
||||
return if (url.toString().startsWith("$apiUrl/comic/rank/list")) {
|
||||
latestUpdatesParse(response)
|
||||
} else {
|
||||
response.parseAs<ResponseDto<PageDto>>().data.toMangasPage()
|
||||
// "$apiUrl/comic/filter/list" or "$apiUrl/search/index"
|
||||
response.parseAs<ResponseDto<PageDto>>().data.toMangasPage(url.queryParameter("page")!!.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
// Latest
|
||||
// "$apiUrl/comic/update/list/1/$page" is same content
|
||||
@ -280,6 +300,9 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
RankingGroup(),
|
||||
Filter.Separator(),
|
||||
Filter.Header("分类(搜索/查看排行榜时无效)"),
|
||||
GenreGroup(),
|
||||
)
|
||||
|
||||
private fun chapterCommentsUrl(comicId: String, chapterId: String) = "$apiUrl/viewpoint/list?comicId=$comicId&chapterId=$chapterId"
|
||||
@ -292,6 +315,7 @@ class Zaimanhua : HttpSource(), ConfigurableSource {
|
||||
const val COMMENTS_PREF = "COMMENTS"
|
||||
const val COMMENTS_FLAG = "COMMENTS"
|
||||
const val IMAGE_RETRY_FLAG = "IMAGE_RETRY"
|
||||
const val DEFAULT_PAGE_SIZE = 20
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.zaimanhua
|
||||
|
||||
import eu.kanade.tachiyomi.extension.zh.zaimanhua.Zaimanhua.Companion.DEFAULT_PAGE_SIZE
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@ -100,23 +101,37 @@ class ChapterImagesDto(
|
||||
|
||||
@Serializable
|
||||
class PageDto(
|
||||
// Only genre(/comic/filter/list) use `comicList`, others use `list`
|
||||
@JsonNames("comicList")
|
||||
private val list: List<PageItemDto>?,
|
||||
private val page: Int,
|
||||
private val size: Int,
|
||||
// Genre(/comic/filter/list) doesn't have `page` and `size`
|
||||
private val page: Int?,
|
||||
private val size: Int?,
|
||||
// Only genre(/comic/filter/list) use `totalNum`, others use `total`
|
||||
@JsonNames("totalNum")
|
||||
private val total: Int,
|
||||
) {
|
||||
fun toMangasPage(): MangasPage {
|
||||
fun toMangasPage(page: Int): MangasPage {
|
||||
val currentPage = this.page ?: page
|
||||
val pageSize = this.size ?: DEFAULT_PAGE_SIZE
|
||||
if (list.isNullOrEmpty()) throw Exception("漫画结果为空,请检查输入")
|
||||
val hasNextPage = page * size < total
|
||||
val hasNextPage = currentPage * pageSize < total
|
||||
return MangasPage(list.map { it.toSManga() }, hasNextPage)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class PageItemDto(
|
||||
// must have at least one of id and comicId
|
||||
// Genre(/comic/filter/list) only have `id`
|
||||
// Ranking(/comic/rank/list) only have `comic_id`
|
||||
// latest(/comic/update/list) have both `id` (always 0) and `comic_id`
|
||||
// Search(/search/index) have both `id` and `comic_id` (always 0)
|
||||
private val id: Int?,
|
||||
@SerialName("comic_id")
|
||||
private val comicId: Int,
|
||||
private val comicId: Int?,
|
||||
// Only genre(/comic/filter/list) use `name`, others use `title`
|
||||
@JsonNames("name")
|
||||
private val title: String,
|
||||
private val authors: String?,
|
||||
private val status: String?,
|
||||
@ -124,7 +139,7 @@ class PageItemDto(
|
||||
private val types: String?,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = (this@PageItemDto.id?.takeIf { it != 0 } ?: this@PageItemDto.comicId).toString()
|
||||
url = (this@PageItemDto.comicId?.takeIf { it != 0 } ?: this@PageItemDto.id)!!.toString()
|
||||
title = this@PageItemDto.title
|
||||
author = authors?.formatList()
|
||||
genre = types?.formatList()
|
||||
@ -192,3 +207,9 @@ class CommentDataDto(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class JwtPayload(
|
||||
@SerialName("exp")
|
||||
val expirationTime: Long,
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user