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:
		
							parent
							
								
									ce08808666
								
							
						
					
					
						commit
						24147b6556
					
				| @ -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, | ||||
|  | ||||
| @ -6,7 +6,7 @@ ext { | ||||
|     extName = 'DMZJ' | ||||
|     pkgNameSuffix = 'zh.dmzj' | ||||
|     extClass = '.Dmzj' | ||||
|     extVersionCode = 38 | ||||
|     extVersionCode = 39 | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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") } | ||||
| } | ||||
|  | ||||
| @ -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)) | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 stevenyomi
						stevenyomi