DMZJ: tweak page list parsing and remove special lists (#16427)

* remove special lists

* use kotlin class for tags

* adjust page list

* bump version

* tweak preference summary
This commit is contained in:
stevenyomi 2023-05-14 10:39:03 +08:00 committed by GitHub
parent ce08808666
commit 24147b6556
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 40 additions and 135 deletions

View File

@ -116,7 +116,9 @@ class ChapterImagesDto(
@ProtoNumber(3) val name: String,
@ProtoNumber(4) val order: Int,
@ProtoNumber(5) val direction: Int,
// initial letter is sometimes different from that in original URLs, see manga ID 56649
@ProtoNumber(6) val lowResImages: List<String>,
// page count of low-res images
@ProtoNumber(7) val pageCount: Int?,
@ProtoNumber(8) val images: List<String>,
@ProtoNumber(9) val commentCount: Int,

View File

@ -6,7 +6,7 @@ ext {
extName = 'DMZJ'
pkgNameSuffix = 'zh.dmzj'
extClass = '.Dmzj'
extVersionCode = 38
extVersionCode = 39
}
apply from: "$rootDir/common.gradle"

View File

@ -97,11 +97,9 @@ object ApiV3 {
@Serializable
class ChapterImagesDto(
private val id: Int,
private val comic_id: Int,
private val page_url: List<String>,
) {
fun toPageList() = parsePageList(comic_id, id, page_url, emptyList())
fun toPageList() = parsePageList(page_url)
}
@Serializable

View File

@ -20,20 +20,17 @@ object ApiV4 {
fun mangaInfoUrl(id: String) = "$v4apiUrl/comic/detail/$id?uid=2665531"
fun parseMangaInfo(response: Response): ParseResult {
fun parseMangaInfo(response: Response): MangaDto? {
val result: ResponseDto<MangaDto> = response.decrypt()
return when (val manga = result.data) {
null -> ParseResult.Error(result.message)
else -> ParseResult.Ok(manga)
}
return result.data
}
// path = "mangaId/chapterId"
fun chapterImagesUrl(path: String) = "$v4apiUrl/comic/chapter/$path"
fun parseChapterImages(response: Response): ArrayList<Page> {
fun parseChapterImages(response: Response, isLowRes: Boolean): ArrayList<Page> {
val result: ResponseDto<ChapterImagesDto> = response.decrypt()
return result.data!!.toPageList()
return result.data!!.toPageList(isLowRes)
}
fun rankingUrl(page: Int, filters: RankingGroup) =
@ -128,12 +125,18 @@ object ApiV4 {
@Serializable
class ChapterImagesDto(
@ProtoNumber(1) private val id: Int,
@ProtoNumber(2) private val mangaId: Int,
@ProtoNumber(6) private val lowResImages: List<String>,
@ProtoNumber(8) private val images: List<String>,
) {
fun toPageList() = parsePageList(mangaId, id, images, lowResImages)
fun toPageList(isLowRes: Boolean) =
// page count can be messy, see manga ID 55847 chapters 107-109
if (images.size == lowResImages.size) {
parsePageList(images, lowResImages)
} else if (isLowRes) {
parsePageList(lowResImages, lowResImages)
} else {
parsePageList(images)
}
}
// same as ApiV3.MangaDto
@ -167,10 +170,5 @@ object ApiV4 {
@ProtoNumber(3) val data: T?,
)
sealed interface ParseResult {
class Ok(val manga: MangaDto) : ParseResult
class Error(val message: String?) : ParseResult
}
private val cipher by lazy { RSA.getPrivateKey("MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAK8nNR1lTnIfIes6oRWJNj3mB6OssDGx0uGMpgpbVCpf6+VwnuI2stmhZNoQcM417Iz7WqlPzbUmu9R4dEKmLGEEqOhOdVaeh9Xk2IPPjqIu5TbkLZRxkY3dJM1htbz57d/roesJLkZXqssfG5EJauNc+RcABTfLb4IiFjSMlTsnAgMBAAECgYEAiz/pi2hKOJKlvcTL4jpHJGjn8+lL3wZX+LeAHkXDoTjHa47g0knYYQteCbv+YwMeAGupBWiLy5RyyhXFoGNKbbnvftMYK56hH+iqxjtDLnjSDKWnhcB7089sNKaEM9Ilil6uxWMrMMBH9v2PLdYsqMBHqPutKu/SigeGPeiB7VECQQDizVlNv67go99QAIv2n/ga4e0wLizVuaNBXE88AdOnaZ0LOTeniVEqvPtgUk63zbjl0P/pzQzyjitwe6HoCAIpAkEAxbOtnCm1uKEp5HsNaXEJTwE7WQf7PrLD4+BpGtNKkgja6f6F4ld4QZ2TQ6qvsCizSGJrjOpNdjVGJ7bgYMcczwJBALvJWPLmDi7ToFfGTB0EsNHZVKE66kZ/8Stx+ezueke4S556XplqOflQBjbnj2PigwBN/0afT+QZUOBOjWzoDJkCQClzo+oDQMvGVs9GEajS/32mJ3hiWQZrWvEzgzYRqSf3XVcEe7PaXSd8z3y3lACeeACsShqQoc8wGlaHXIJOHTcCQQCZw5127ZGs8ZDTSrogrH73Kw/HvX55wGAeirKYcv28eauveCG7iyFR0PFB/P/EDZnyb+ifvyEFlucPUI0+Y87F") }
}

View File

@ -24,7 +24,7 @@ object CommentsInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (request.tag(Tag::class.java) == null) return response
if (request.tag(Tag::class) == null) return response
val comments = ApiV3.parseChapterComments(response)
.take(MAX_HEIGHT / (UNIT * 2))

View File

@ -4,11 +4,9 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.net.URLDecoder
import kotlin.math.max
const val PREFIX_ID_SEARCH = "id:"
@ -42,34 +40,18 @@ fun String.formatChapterName(): String {
return "$number$type"
}
private const val imageSmallUrl = "https://imgsmall.idmzj.com"
fun parsePageList(
mangaId: Int,
chapterId: Int,
images: List<String>,
lowResImages: List<String>,
lowResImages: List<String> = List(images.size) { "" },
): ArrayList<Page> {
// page count can be messy, see manga ID 55847 chapters 107-109
val pageCount = max(images.size, lowResImages.size)
val pageCount = images.size
val list = ArrayList<Page>(pageCount + 1) // for comments page
for (i in 0 until pageCount) {
val imageUrl = images.getOrNull(i)?.fixFilename()?.toHttps()
val lowResUrl = lowResImages.getOrElse(i) {
// this is sometimes different in low-res URLs and might fail, see manga ID 56649
val initial = imageUrl!!.decodePath().toHttpUrl().pathSegments[0]
"$imageSmallUrl/$initial/$mangaId/$chapterId/$i.jpg"
}.toHttps()
list.add(Page(i, url = lowResUrl, imageUrl = imageUrl ?: lowResUrl))
list.add(Page(i, lowResImages[i], images[i]))
}
return list
}
fun String.toHttps() = "https:" + substringAfter(':')
// see https://github.com/tachiyomiorg/tachiyomi-extensions/issues/3457
fun String.fixFilename() = if (endsWith(".jp")) this + 'g' else this
fun String.decodePath(): String = URLDecoder.decode(this, "UTF-8")
const val COMMENTS_FLAG = "COMMENTS"

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.extension.zh.dmzj
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
@ -32,7 +31,6 @@ class Dmzj : ConfigurableSource, HttpSource() {
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.migrate()
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(ImageUrlInterceptor)
@ -57,18 +55,7 @@ class Dmzj : ConfigurableSource, HttpSource() {
private fun fetchMangaInfoV4(id: String): ApiV4.MangaDto? {
val response = retryClient.newCall(GET(ApiV4.mangaInfoUrl(id), headers)).execute()
return when (val result = ApiV4.parseMangaInfo(response)) {
is ApiV4.ParseResult.Ok -> {
val manga = result.manga
if (manga.isLicensed) preferences.addLicensed(id)
manga
}
is ApiV4.ParseResult.Error -> {
Log.e("DMZJ", "no data for manga $id: ${result.message}")
preferences.addHidden(id)
null
}
}
return ApiV4.parseMangaInfo(response)
}
override fun popularMangaRequest(page: Int) = GET(ApiV3.popularMangaUrl(page), headers)
@ -139,9 +126,7 @@ class Dmzj : ConfigurableSource, HttpSource() {
}
private fun fetchMangaDetails(id: String): SManga {
if (id !in preferences.hiddenList) {
fetchMangaInfoV4(id)?.run { return toSManga() }
}
fetchMangaInfoV4(id)?.run { return toSManga() }
val response = client.newCall(GET(ApiV3.mangaInfoUrlV1(id), headers)).execute()
return ApiV3.parseMangaDetailsV1(response)
}
@ -159,16 +144,14 @@ class Dmzj : ConfigurableSource, HttpSource() {
throw UnsupportedOperationException()
}
override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException("Not used.")
override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.fromCallable {
val id = manga.url.extractMangaId()
if (id !in preferences.licensedList && id !in preferences.hiddenList) {
val result = fetchMangaInfoV4(id)
if (result != null && !result.isLicensed) {
return@fromCallable result.parseChapterList()
}
val result = fetchMangaInfoV4(id)
if (result != null && !result.isLicensed) {
return@fromCallable result.parseChapterList()
}
val response = client.newCall(GET(ApiV3.mangaInfoUrlV1(id), headers)).execute()
ApiV3.parseChapterListV1(response)
@ -186,7 +169,7 @@ class Dmzj : ConfigurableSource, HttpSource() {
return Observable.fromCallable {
val response = retryClient.newCall(GET(ApiV4.chapterImagesUrl(path), headers)).execute()
val result = try {
ApiV4.parseChapterImages(response)
ApiV4.parseChapterImages(response, preferences.imageQuality == LOW_RES)
} catch (_: Throwable) {
client.newCall(GET(ApiV3.chapterImagesUrlV1(path), headers)).execute()
.let(ApiV3::parseChapterImagesV1)
@ -204,31 +187,31 @@ class Dmzj : ConfigurableSource, HttpSource() {
// see https://github.com/tachiyomiorg/tachiyomi-extensions/issues/10475
override fun imageRequest(page: Page): Request {
val url = page.url
val url = page.url.takeIf { it.isNotEmpty() }
val imageUrl = page.imageUrl!!
if (url == COMMENTS_FLAG) {
return GET(imageUrl, headers).newBuilder()
.tag(CommentsInterceptor.Tag::class.java, CommentsInterceptor.Tag())
.tag(CommentsInterceptor.Tag::class, CommentsInterceptor.Tag())
.build()
}
val fallbackUrl = when (preferences.imageQuality) {
AUTO_RES -> url
ORIGINAL_RES -> null
LOW_RES -> return GET(url, headers)
LOW_RES -> if (url == null) null else return GET(url, headers)
else -> url
}
return GET(imageUrl, headers).newBuilder()
.tag(ImageUrlInterceptor.Tag::class.java, ImageUrlInterceptor.Tag(fallbackUrl))
.tag(ImageUrlInterceptor.Tag::class, ImageUrlInterceptor.Tag(fallbackUrl))
.build()
}
// Unused, we can get image urls directly from the chapter page
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("This method should not be called!")
throw UnsupportedOperationException()
override fun getFilterList() = getFilterListInternal(preferences.isMultiGenreFilter)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
getPreferencesInternal(screen.context, preferences).forEach(screen::addPreference)
getPreferencesInternal(screen.context).forEach(screen::addPreference)
}
}

View File

@ -11,7 +11,7 @@ object ImageUrlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val tag = request.tag(Tag::class.java) ?: return chain.proceed(request)
val tag = request.tag(Tag::class) ?: return chain.proceed(request)
try {
val response = chain.proceed(request)

View File

@ -3,20 +3,21 @@ package eu.kanade.tachiyomi.extension.zh.dmzj
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.SwitchPreferenceCompat
// Legacy preferences:
// "apiRatelimitPreference" -> 1..10 default "5"
// "imgCDNRatelimitPreference" -> 1..10 default "5"
// "licensedList" -> StringSet of manga ID
// "hiddenList" -> StringSet of manga ID
fun getPreferencesInternal(context: Context, preferences: SharedPreferences) = arrayOf(
fun getPreferencesInternal(context: Context) = arrayOf(
ListPreference(context).apply {
key = IMAGE_QUALITY_PREF
title = "图片质量"
summary = "%s\n如果选择“只用原图”可能会有部分图片无法加载"
entries = arrayOf("优先原图", "只用原图", "只用低清")
summary = "%s\n修改后,已加载的章节需要清除章节缓存才能生效"
entries = arrayOf("优先原图", "只用原图 (加载出错概率更高)", "优先低清")
entryValues = arrayOf(AUTO_RES, ORIGINAL_RES, LOW_RES)
setDefaultValue(AUTO_RES)
},
@ -34,18 +35,6 @@ fun getPreferencesInternal(context: Context, preferences: SharedPreferences) = a
summary = "可以更精细地筛选出同时符合多个题材的作品。"
setDefaultValue(false)
},
MultiSelectListPreference(context).setupIdList(
LICENSED_LIST_PREF,
"特殊漫画 ID 列表 (1)",
preferences.licensedList.toTypedArray(),
),
MultiSelectListPreference(context).setupIdList(
HIDDEN_LIST_PREF,
"特殊漫画 ID 列表 (2)",
preferences.hiddenList.toTypedArray(),
),
)
val SharedPreferences.imageQuality get() = getString(IMAGE_QUALITY_PREF, AUTO_RES)!!
@ -54,50 +43,6 @@ val SharedPreferences.showChapterComments get() = getBoolean(CHAPTER_COMMENTS_PR
val SharedPreferences.isMultiGenreFilter get() = getBoolean(MULTI_GENRE_FILTER_PREF, false)
val SharedPreferences.licensedList: Set<String> get() = getStringSet(LICENSED_LIST_PREF, emptySet())!!
val SharedPreferences.hiddenList: Set<String> get() = getStringSet(HIDDEN_LIST_PREF, emptySet())!!
fun SharedPreferences.addLicensed(id: String) = addToSet(LICENSED_LIST_PREF, id, licensedList)
fun SharedPreferences.addHidden(id: String) = addToSet(HIDDEN_LIST_PREF, id, hiddenList)
private fun MultiSelectListPreference.setupIdList(
key: String,
title: String,
values: Array<String>,
): MultiSelectListPreference {
this.key = key
this.title = title
summary = "如果漫画网页版可以正常访问,但是应用内章节目录加载异常,可以点开列表删除记录。" +
"删除方法是【取消勾选】要删除的 ID 再点击确定,勾选的项目会保留。" +
"如果点开为空,就表示没有记录。刷新漫画页并展开简介即可查看 ID。"
entries = values
entryValues = values
setDefaultValue(emptySet<Nothing>())
return this
}
@Synchronized
private fun SharedPreferences.addToSet(key: String, id: String, oldSet: Set<String>) {
if (id in oldSet) return
val newSet = HashSet<String>((oldSet.size + 1) * 2)
newSet.addAll(oldSet)
newSet.add(id)
edit().putStringSet(key, newSet).apply()
}
fun SharedPreferences.migrate(): SharedPreferences {
val currentVersion = 1
val versionPref = "version"
val oldVersion = getInt(versionPref, 0)
if (oldVersion >= currentVersion) return this
val editor = edit()
if (oldVersion < 1) {
editor.remove(LICENSED_LIST_PREF).remove(HIDDEN_LIST_PREF)
}
editor.putInt(versionPref, currentVersion).apply()
return this
}
private const val IMAGE_QUALITY_PREF = "imageSourcePreference"
const val AUTO_RES = "PREFER_ORIG_RES"
const val ORIGINAL_RES = "ORIG_RES_ONLY"
@ -105,6 +50,3 @@ const val LOW_RES = "LOW_RES_ONLY"
private const val CHAPTER_COMMENTS_PREF = "chapterComments"
private const val MULTI_GENRE_FILTER_PREF = "multiGenreFilter"
private const val LICENSED_LIST_PREF = "licensedList"
private const val HIDDEN_LIST_PREF = "hiddenList"