From 8acd1707ae4a955bd7377a6d61ce6f7421342d2e Mon Sep 17 00:00:00 2001 From: stevenyomi <95685115+stevenyomi@users.noreply.github.com> Date: Sun, 24 Aug 2025 09:52:01 +0000 Subject: [PATCH] Remove DMZJ (#10248) --- src/zh/dmzj/API.md | 140 ----------- src/zh/dmzj/AndroidManifest.xml | 43 ---- src/zh/dmzj/build.gradle | 7 - src/zh/dmzj/res/mipmap-hdpi/ic_launcher.png | Bin 3756 -> 0 bytes src/zh/dmzj/res/mipmap-mdpi/ic_launcher.png | Bin 2047 -> 0 bytes src/zh/dmzj/res/mipmap-xhdpi/ic_launcher.png | Bin 5379 -> 0 bytes src/zh/dmzj/res/mipmap-xxhdpi/ic_launcher.png | Bin 9872 -> 0 bytes .../dmzj/res/mipmap-xxxhdpi/ic_launcher.png | Bin 14023 -> 0 bytes .../tachiyomi/extension/zh/dmzj/ApiSearch.kt | 64 ----- .../tachiyomi/extension/zh/dmzj/ApiV3.kt | 135 ----------- .../tachiyomi/extension/zh/dmzj/ApiV4.kt | 174 -------------- .../extension/zh/dmzj/CommentsInterceptor.kt | 69 ------ .../tachiyomi/extension/zh/dmzj/Common.kt | 57 ----- .../tachiyomi/extension/zh/dmzj/Dmzj.kt | 224 ------------------ .../extension/zh/dmzj/DmzjUrlActivity.kt | 44 ---- .../tachiyomi/extension/zh/dmzj/Filters.kt | 201 ---------------- .../extension/zh/dmzj/ImageUrlInterceptor.kt | 41 ---- .../extension/zh/dmzj/Preferences.kt | 63 ----- .../extension/zh/dmzj/RetryInterceptor.kt | 18 -- .../tachiyomi/extension/zh/dmzj/utils/RSA.kt | 42 ---- 20 files changed, 1322 deletions(-) delete mode 100644 src/zh/dmzj/API.md delete mode 100644 src/zh/dmzj/AndroidManifest.xml delete mode 100644 src/zh/dmzj/build.gradle delete mode 100644 src/zh/dmzj/res/mipmap-hdpi/ic_launcher.png delete mode 100644 src/zh/dmzj/res/mipmap-mdpi/ic_launcher.png delete mode 100644 src/zh/dmzj/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 src/zh/dmzj/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 src/zh/dmzj/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ApiSearch.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ApiV3.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ApiV4.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/CommentsInterceptor.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Common.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Dmzj.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/DmzjUrlActivity.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Filters.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ImageUrlInterceptor.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Preferences.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/RetryInterceptor.kt delete mode 100644 src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/utils/RSA.kt diff --git a/src/zh/dmzj/API.md b/src/zh/dmzj/API.md deleted file mode 100644 index 9e844a338..000000000 --- a/src/zh/dmzj/API.md +++ /dev/null @@ -1,140 +0,0 @@ -# API v4 - -## Manga details - -Mostly taken from [here](https://github.com/xiaoyaocz/dmzj_flutter/blob/aac6ba3/lib/protobuf/comic/detail_response.proto). - -```protobuf -syntax = "proto3"; - -package dmzj.comic; - - -message ComicDetailResponse { - int32 Errno = 1; - string Errmsg = 2; - ComicDetailInfoResponse Data= 3; -} - -message ComicDetailInfoResponse { - int32 Id = 1; - string Title = 2; - int32 Direction=3; - int32 Islong=4; - int32 IsDmzj=5; - string Cover=6; - string Description=7; - int64 LastUpdatetime=8; - string LastUpdateChapterName=9; - int32 Copyright=10; - string FirstLetter=11; - string ComicPy=12; - int32 Hidden=13; - int32 HotNum=14; - int32 HitNum=15; - int32 Uid=16; - int32 IsLock=17; - int32 LastUpdateChapterId=18; - repeated ComicDetailTypeItemResponse Types=19; - repeated ComicDetailTypeItemResponse Status=20; - repeated ComicDetailTypeItemResponse Authors=21; - int32 SubscribeNum=22; - repeated ComicDetailChapterResponse Chapters=23; - int32 IsNeedLogin=24; - //object UrlLinks=25; { string name = 1; repeated object links = 2; } - // link { int32 = 1; string name = 2; string uriOrApk = 3; string icon = 4; string packageName = 5; string apk = 6; int32 = 7; } - int32 IsHideChapter=26; - //repeated object DhUrlLinks=27; { string name = 1; } - -} - -message ComicDetailTypeItemResponse { - int32 TagId = 1; - string TagName = 2; -} - -message ComicDetailChapterResponse { - string Title = 1; - repeated ComicDetailChapterInfoResponse Data=2; -} -message ComicDetailChapterInfoResponse { - int32 ChapterId = 1; - string ChapterTitle = 2; - int64 Updatetime=3; - int32 Filesize=4; - int32 ChapterOrder=5; -} -``` - -## Ranking - -Taken from [here](https://github.com/xiaoyaocz/dmzj_flutter/blob/e7f1b1e/lib/protobuf/comic/rank_list_response.proto). - -```protobuf -syntax = "proto3"; - -package dmzj.comic; - - -message ComicRankListResponse { - int32 Errno = 1; - string Errmsg = 2; - repeated ComicRankListItemResponse Data= 3; -} - -message ComicRankListItemResponse { - int32 ComicId = 1; - string Title = 2; - string Authors=3; - string Status=4; - string Cover=5; - string Types=6; - int64 LastUpdatetime=7; - string LastUpdateChapterName=8; - string ComicPy=9; - int32 Num=10; - int32 TagId=11; - string ChapterName=12; - int32 ChapterId=13; -} -``` - -## Chapter images - -```kotlin -@Serializable -class ResponseDto( - @ProtoNumber(1) val code: Int?, - @ProtoNumber(2) val message: String?, - @ProtoNumber(3) val data: T?, -) - -@Serializable -class ChapterImagesDto( - @ProtoNumber(1) val id: Int, - @ProtoNumber(2) val mangaId: Int, - @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, - // page count of low-res images - @ProtoNumber(7) val pageCount: Int?, - @ProtoNumber(8) val images: List, - @ProtoNumber(9) val commentCount: Int, -) -``` - -# Unused legacy API - -## Chapter images - -```kotlin -val webviewPageListApiUrl = "https://m.dmzj.com/chapinfo" -GET("$webviewPageListApiUrl/${chapter.url}.html") -``` - -```kotlin -val oldPageListApiUrl = "http://api.m.dmzj.com" // this domain has an expired certificate -GET("$oldPageListApiUrl/comic/chapter/${chapter.url}.html") -``` diff --git a/src/zh/dmzj/AndroidManifest.xml b/src/zh/dmzj/AndroidManifest.xml deleted file mode 100644 index dfea2cf99..000000000 --- a/src/zh/dmzj/AndroidManifest.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/src/zh/dmzj/build.gradle b/src/zh/dmzj/build.gradle deleted file mode 100644 index a10b02998..000000000 --- a/src/zh/dmzj/build.gradle +++ /dev/null @@ -1,7 +0,0 @@ -ext { - extName = 'DMZJ' - extClass = '.Dmzj' - extVersionCode = 46 -} - -apply from: "$rootDir/common.gradle" diff --git a/src/zh/dmzj/res/mipmap-hdpi/ic_launcher.png b/src/zh/dmzj/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 48efce5172e9ab13eb107c07056c22ee3ae334d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3756 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 diff --git a/src/zh/dmzj/res/mipmap-xhdpi/ic_launcher.png b/src/zh/dmzj/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 49b01579343536f3b9ab99a5898bc9317f85236e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5379 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 g9vzii`mCy|hMj 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 - if (result.errmsg.isNotBlank()) { - throw Exception(result.errmsg) - } else { - val url = response.request.url - val page = url.queryParameter("page")?.toInt() - val size = url.queryParameter("size")?.toInt() - return if (result.data != null) { - MangasPage(result.data.comicList.map { it.toSManga() }, page!! * size!! < result.data.totalNum) - } else { - MangasPage(emptyList(), false) - } - } - } - - @Serializable - class MangaDtoV1( - private val id: Int, - private val name: String, - private val authors: String, - private val cover: String, - ) { - fun toSManga() = SManga.create().apply { - url = getMangaUrl(id.toString()) - title = name - author = authors - thumbnail_url = cover - } - } - - @Serializable - class SearchResultDto( - val comicList: List, - val totalNum: Int, - ) - - @Serializable - class ResponseDto( - val errmsg: String = "", - val data: T, - ) -} diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ApiV3.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ApiV3.kt deleted file mode 100644 index d94240071..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ApiV3.kt +++ /dev/null @@ -1,135 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -import android.content.SharedPreferences -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonPrimitive -import okhttp3.Response -import org.jsoup.parser.Parser - -object ApiV3 { - lateinit var preferences: SharedPreferences - private val v3apiUrl: String - get() = if (preferences.isOlderV3API == true) { - "https://v3api.idmzj.com" - } else { - "https://nnv3api.dmzj.com" - } - - private const val apiUrl = "https://api.dmzj.com" - - fun popularMangaUrl(page: Int) = "$v3apiUrl/classify/0/0/${page - 1}.json" - - fun latestUpdatesUrl(page: Int) = "$v3apiUrl/classify/0/1/${page - 1}.json" - - fun pageUrl(page: Int, filters: FilterList) = "$v3apiUrl/classify/${parseFilters(filters)}/${page - 1}.json" - - fun parsePage(response: Response): MangasPage { - val data: List = response.parseAs() - return MangasPage(data.map { it.toSManga() }, data.isNotEmpty()) - } - - fun mangaInfoUrlV1(id: String) = "$apiUrl/dynamic/comicinfo/$id.json" - - private fun parseMangaInfoV1(response: Response): ResponseDto = try { - response.parseAs() - } catch (_: Throwable) { - throw Exception("获取漫画信息失败") - } - - fun parseMangaDetailsV1(response: Response): SManga { - return parseMangaInfoV1(response).data.info.toSManga() - } - - fun parseChapterListV1(response: Response): List { - val data = parseMangaInfoV1(response).data - return buildList(data.list.size + data.alone.size) { - data.list.mapTo(this) { - it.toSChapter() - } - data.alone.mapTo(this) { - it.toSChapter().apply { name = "单行本: $name" } - } - } - } - - fun chapterImagesUrlV1(path: String) = "https://m.idmzj.com/chapinfo/$path.html" - - fun parseChapterImagesV1(response: Response) = - response.parseAs().toPageList() - - fun chapterCommentsUrl(path: String) = "$v3apiUrl/viewPoint/0/$path.json" - - fun parseChapterComments(response: Response, count: Int): List { - val result: List = response.parseAs() - if (result.isEmpty()) return listOf("没有吐槽") - val aggregated = result.groupBy({ it.content }, { it.num }).map { (content, likes) -> - ChapterCommentDto(Parser.unescapeEntities(content, false), likes.sum()) - } as ArrayList - aggregated.sort() - return aggregated.take(count).map { it.toString() } - } - - @Serializable - class MangaDto( - private val id: JsonPrimitive, // can be int or string - private val title: String, - private val authors: String?, - private val status: String, - private val cover: String, - private val types: String, - private val description: String? = null, - ) { - fun toSManga() = SManga.create().apply { - url = getMangaUrl(id.content) - title = this@MangaDto.title - author = authors?.formatList() - genre = types.formatList() - status = parseStatus(this@MangaDto.status) - thumbnail_url = cover - - val desc = this@MangaDto.description ?: return@apply - description = "$desc\n\n漫画 ID (2): ${id.content}" // hidden - initialized = true - } - } - - @Serializable - class ChapterDto( - private val id: String, - private val comic_id: String, - private val chapter_name: String, - private val updatetime: String, - ) { - fun toSChapter() = SChapter.create().apply { - url = "$comic_id/$id" - name = chapter_name.formatChapterName() - date_upload = updatetime.toLong() * 1000 - } - } - - @Serializable - class ChapterImagesDto( - private val page_url: List, - ) { - fun toPageList() = parsePageList(page_url) - } - - @Serializable - class ChapterCommentDto( - val content: String, - val num: Int, - ) : Comparable { - override fun toString() = if (num > 0) "$content [+$num]" else content - override fun compareTo(other: ChapterCommentDto) = other.num.compareTo(num) // descending - } - - @Serializable - class DataDto(val info: MangaDto, val list: List, val alone: List) - - @Serializable - class ResponseDto(val data: DataDto) -} diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ApiV4.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ApiV4.kt deleted file mode 100644 index 15eb39165..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ApiV4.kt +++ /dev/null @@ -1,174 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -import eu.kanade.tachiyomi.extension.zh.dmzj.utils.RSA -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 kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.protobuf.ProtoBuf -import kotlinx.serialization.protobuf.ProtoNumber -import kotlinx.serialization.serializer -import okhttp3.Response -import kotlin.reflect.KType -import kotlin.reflect.typeOf - -object ApiV4 { - - private const val v4apiUrl = "https://nnv4api.dmzj.com" - - fun mangaInfoUrl(id: String) = "$v4apiUrl/comic/detail/$id?uid=2665531" - - fun parseMangaInfo(response: Response): MangaDto? { - val result: ResponseDto = response.decrypt() - return result.data - } - - // path = "mangaId/chapterId" - fun chapterImagesUrl(path: String) = "$v4apiUrl/comic/chapter/$path" - - fun parseChapterImages(response: Response, isLowRes: Boolean): ArrayList { - val result: ResponseDto = response.decrypt() - return result.data!!.toPageList(isLowRes) - } - - fun rankingUrl(page: Int, filters: RankingGroup) = - "$v4apiUrl/comic/rank/list?${filters.parse()}&uid=2665531&page=$page" - - fun parseRanking(response: Response): MangasPage { - val result: ResponseDto> = response.decrypt() - val data = result.data ?: return MangasPage(emptyList(), false) - return MangasPage(data.map { it.toSManga() }, data.isNotEmpty()) - } - - private inline fun Response.decrypt(): T = decrypt(typeOf()) - - @Suppress("UNCHECKED_CAST") - private fun Response.decrypt(type: KType): T { - val bytes = RSA.decrypt(body.string(), cipher) - val deserializer = serializer(type) as KSerializer - return ProtoBuf.decodeFromByteArray(deserializer, bytes) - } - - @Serializable - class MangaDto( - @ProtoNumber(1) private val id: Int, - @ProtoNumber(2) private val title: String, - @ProtoNumber(6) private val cover: String, - @ProtoNumber(7) private val description: String, - @ProtoNumber(19) private val genres: List, - @ProtoNumber(20) private val status: List, - @ProtoNumber(21) private val authors: List, - @ProtoNumber(23) private val chapterGroups: List, - ) { - val isLicensed get() = chapterGroups.isEmpty() - - fun toSManga() = SManga.create().apply { - url = getMangaUrl(id.toString()) - title = this@MangaDto.title - author = authors.joinToString { it.name } - description = if (isLicensed) { - "${this@MangaDto.description}\n\n漫画 ID (1): $id" - } else { - this@MangaDto.description - } - genre = genres.joinToString { it.name } - status = parseStatus(this@MangaDto.status[0].name) - thumbnail_url = cover - initialized = true - } - - fun parseChapterList(): List { - val mangaId = id.toString() - val size = chapterGroups.sumOf { it.size } - return chapterGroups.flatMapTo(ArrayList(size)) { - it.toSChapterList(mangaId) - } - } - } - - @Serializable - class TagDto(@ProtoNumber(2) val name: String) - - @Serializable - class ChapterGroupDto( - @ProtoNumber(1) private val name: String, - @ProtoNumber(2) private val chapters: List, - ) { - fun toSChapterList(mangaId: String): List { - val groupName = name - val isDefaultGroup = groupName == "连载" - return chapters.map { - it.toSChapterInternal().apply { - url = "$mangaId/$url" - if (!isDefaultGroup) name = "$groupName: $name" - } - } - } - - val size get() = chapters.size - } - - @Serializable - class ChapterDto( - @ProtoNumber(1) private val id: Int, - @ProtoNumber(2) private val name: String, - @ProtoNumber(3) private val updateTime: Long, - ) { - fun toSChapterInternal() = SChapter.create().apply { - url = id.toString() - name = this@ChapterDto.name.formatChapterName() - date_upload = updateTime * 1000 - } - } - - @Serializable - class ChapterImagesDto( - @ProtoNumber(6) private val lowResImages: List, - @ProtoNumber(8) private val images: List, - ) { - 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 - @Serializable - class RankingItemDto( - @ProtoNumber(1) private val id: Int?, - @ProtoNumber(2) private val title: String, - @ProtoNumber(3) private val authors: String, - @ProtoNumber(4) private val status: String, - @ProtoNumber(5) private val cover: String, - @ProtoNumber(6) private val genres: String, - @ProtoNumber(9) private val slug: String?, - ) { - fun toSManga() = SManga.create().apply { - url = when { - id != null -> getMangaUrl(id.toString()) - slug != null -> PREFIX_ID_SEARCH + slug - else -> throw Exception("无法解析") - } - title = this@RankingItemDto.title - author = authors.formatList() - genre = genres.formatList() - status = parseStatus(this@RankingItemDto.status) - thumbnail_url = cover - } - } - - @Serializable - class ResponseDto( - @ProtoNumber(2) val message: String?, - @ProtoNumber(3) val data: T?, - ) - - 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") } -} diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/CommentsInterceptor.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/CommentsInterceptor.kt deleted file mode 100644 index 4508db3fa..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/CommentsInterceptor.kt +++ /dev/null @@ -1,69 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -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 okhttp3.Interceptor -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody -import java.io.ByteArrayOutputStream - -object CommentsInterceptor : Interceptor { - - class Tag - - 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(Tag::class) == null) return response - - val comments = ApiV3.parseChapterComments(response, MAX_HEIGHT / (UNIT * 2)) - - 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 output = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.PNG, 0, output) - val body = output.toByteArray().toResponseBody("image/png".toMediaType()) - return response.newBuilder().body(body).build() - } -} diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Common.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Common.kt deleted file mode 100644 index e067604ad..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Common.kt +++ /dev/null @@ -1,57 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -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.Response -import uy.kohesive.injekt.injectLazy -import java.net.URLDecoder - -const val PREFIX_ID_SEARCH = "id:" - -val json: Json by injectLazy() - -inline fun Response.parseAs(): T { - return json.decodeFromString(body.string()) -} - -fun getMangaUrl(id: String) = "/comic/comic_$id.json?version=2.7.019" - -fun String.extractMangaId(): String { - val start = 13 // length of "/comic/comic_" - return substring(start, indexOf('.', start)) -} - -fun String.formatList() = replace("/", ", ") - -fun parseStatus(status: String): Int = when (status) { - "连载中" -> SManga.ONGOING - "已完结" -> SManga.COMPLETED - else -> SManga.UNKNOWN -} - -private val chapterNameRegex = Regex("""(?:连载版?)?(\d[.\d]*)([话卷])?""") - -fun String.formatChapterName(): String { - val match = chapterNameRegex.matchEntire(this) ?: return this - val (number, optionalType) = match.destructured - val type = optionalType.ifEmpty { "话" } - return "第$number$type" -} - -fun parsePageList( - images: List, - lowResImages: List = List(images.size) { "" }, -): ArrayList { - val pageCount = images.size - val list = ArrayList(pageCount + 1) // for comments page - for (i in 0 until pageCount) { - list.add(Page(i, lowResImages[i], images[i])) - } - return list -} - -fun String.decodePath(): String = URLDecoder.decode(this, "UTF-8") - -const val COMMENTS_FLAG = "COMMENTS" diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Dmzj.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Dmzj.kt deleted file mode 100644 index 16b89e9dc..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Dmzj.kt +++ /dev/null @@ -1,224 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -import android.content.SharedPreferences -import androidx.preference.PreferenceScreen -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.interceptor.rateLimit -import eu.kanade.tachiyomi.source.ConfigurableSource -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.getPreferences -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import rx.Observable - -/** - * Dmzj source - */ - -class Dmzj : ConfigurableSource, HttpSource() { - override val lang = "zh" - override val supportsLatest = true - override val name = "动漫之家" - override val baseUrl = "https://m.idmzj.com" - - private val preferences: SharedPreferences = getPreferences() - init { - ApiV3.preferences = preferences - } - - override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .addInterceptor(ImageUrlInterceptor) - .addInterceptor(CommentsInterceptor) - .rateLimit(4) - .apply { - val interceptors = interceptors() - val index = interceptors.indexOfFirst { "Brotli" in it.javaClass.simpleName } - if (index >= 0) { - interceptors.add(interceptors.removeAt(index)) - } - } - .build() - - // API v4 randomly fails - private val retryClient = network.cloudflareClient.newBuilder() - .addInterceptor(RetryInterceptor) - .rateLimit(2) - .build() - - private fun fetchIdBySlug(slug: String): String { - val request = GET("https://manhua.dmzj.com/$slug/", headers) - val html = client.newCall(request).execute().body.string() - val start = "g_comic_id = \"" - val startIndex = html.indexOf(start) + start.length - val endIndex = html.indexOf('"', startIndex) - return html.substring(startIndex, endIndex) - } - - private fun fetchMangaInfoV4(id: String): ApiV4.MangaDto? { - val response = retryClient.newCall(GET(ApiV4.mangaInfoUrl(id), headers)).execute() - return ApiV4.parseMangaInfo(response) - } - - override fun popularMangaRequest(page: Int) = GET(ApiV3.popularMangaUrl(page), headers) - - override fun popularMangaParse(response: Response) = ApiV3.parsePage(response) - - override fun latestUpdatesRequest(page: Int) = GET(ApiV3.latestUpdatesUrl(page), headers) - - override fun latestUpdatesParse(response: Response) = ApiV3.parsePage(response) - - private fun searchMangaById(id: String): MangasPage { - val idNumber = if (id.all { it.isDigit() }) { - id - } else { - // Chinese Pinyin ID - fetchIdBySlug(id) - } - - val sManga = fetchMangaDetails(idNumber) - - return MangasPage(listOf(sManga), false) - } - - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return if (query.isEmpty()) { - val ranking = filters.filterIsInstance().firstOrNull() - if (ranking != null && ranking.isEnabled) { - val call = retryClient.newCall(GET(ApiV4.rankingUrl(page, ranking), headers)) - return Observable.fromCallable { - val result = ApiV4.parseRanking(call.execute()) - // result has no manga ID if filtered by certain genres; this can be slow - for (manga in result.mangas) if (manga.url.startsWith(PREFIX_ID_SEARCH)) { - manga.url = getMangaUrl(fetchIdBySlug(manga.url.removePrefix(PREFIX_ID_SEARCH))) - } - result - } - } - val call = client.newCall(GET(ApiV3.pageUrl(page, filters), headers)) - Observable.fromCallable { ApiV3.parsePage(call.execute()) } - } else if (query.startsWith(PREFIX_ID_SEARCH)) { - // ID may be numbers or Chinese pinyin - val id = query.removePrefix(PREFIX_ID_SEARCH).removeSuffix(".html") - Observable.fromCallable { searchMangaById(id) } - } else { - val request = GET(ApiSearch.searchUrlV1(page, query), headers) - Observable.fromCallable { - // this API fails randomly, and might return empty list - repeat(5) { - val result = ApiSearch.parsePageV1(client.newCall(request).execute()) - if (result.mangas.isNotEmpty()) return@fromCallable result - } - throw Exception("搜索出错或无结果") - } - } - } - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - throw UnsupportedOperationException() - } - - override fun searchMangaParse(response: Response): MangasPage { - throw UnsupportedOperationException() - } - - override fun fetchMangaDetails(manga: SManga): Observable { - val id = manga.url.extractMangaId() - return Observable.fromCallable { fetchMangaDetails(id) } - } - - private fun fetchMangaDetails(id: String): SManga { - fetchMangaInfoV4(id)?.run { return toSManga() } - val response = client.newCall(GET(ApiV3.mangaInfoUrlV1(id), headers)).execute() - return ApiV3.parseMangaDetailsV1(response) - } - - override fun mangaDetailsRequest(manga: SManga): Request { - throw UnsupportedOperationException() - } - - override fun getMangaUrl(manga: SManga): String { - val cid = manga.url.extractMangaId() - return "$baseUrl/info/$cid.html" - } - - override fun mangaDetailsParse(response: Response) = SManga.create().apply { - throw UnsupportedOperationException() - } - - override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException() - - override fun fetchChapterList(manga: SManga): Observable> { - return Observable.fromCallable { - val id = manga.url.extractMangaId() - 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) - } - } - - override fun chapterListParse(response: Response): List { - throw UnsupportedOperationException() - } - - override fun getChapterUrl(chapter: SChapter) = "$baseUrl/view/${chapter.url}.html" - - override fun fetchPageList(chapter: SChapter): Observable> { - val path = chapter.url - return Observable.fromCallable { - val response = retryClient.newCall(GET(ApiV4.chapterImagesUrl(path), headers)).execute() - val result = try { - ApiV4.parseChapterImages(response, preferences.imageQuality == LOW_RES) - } catch (_: Throwable) { - client.newCall(GET(ApiV3.chapterImagesUrlV1(path), headers)).execute() - .let(ApiV3::parseChapterImagesV1) - } - if (preferences.showChapterComments) { - result.add(Page(result.size, COMMENTS_FLAG, ApiV3.chapterCommentsUrl(path))) - } - result - } - } - - override fun pageListParse(response: Response): List { - throw UnsupportedOperationException() - } - - // see https://github.com/tachiyomiorg/tachiyomi-extensions/issues/10475 - override fun imageRequest(page: Page): Request { - val url = page.url.takeIf { it.isNotEmpty() } - val imageUrl = page.imageUrl!! - if (url == COMMENTS_FLAG) { - return GET(imageUrl, headers).newBuilder() - .tag(CommentsInterceptor.Tag::class, CommentsInterceptor.Tag()) - .build() - } - val fallbackUrl = when (preferences.imageQuality) { - AUTO_RES -> url - ORIGINAL_RES -> null - LOW_RES -> if (url == null) null else return GET(url, headers) - else -> url - } - return GET(imageUrl, headers).newBuilder() - .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() - - override fun getFilterList() = getFilterListInternal(preferences.isMultiGenreFilter) - - override fun setupPreferenceScreen(screen: PreferenceScreen) { - getPreferencesInternal(screen.context).forEach(screen::addPreference) - } -} diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/DmzjUrlActivity.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/DmzjUrlActivity.kt deleted file mode 100644 index d0fed1ccb..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/DmzjUrlActivity.kt +++ /dev/null @@ -1,44 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.util.Log -import kotlin.system.exitProcess - -/** - * Springboard that accepts https://www.dmzj.com/info/xxx intents and redirects them to - * the main tachiyomi process. The idea is to not install the intent filter unless - * you have this extension installed, but still let the main tachiyomi app control - * things. - */ -class DmzjUrlActivity : Activity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val pathSegments = intent?.data?.pathSegments - if (pathSegments != null && pathSegments.size > 0) { - val titleId = if (pathSegments.size > 1) { - pathSegments[1] // [m,www].dmzj.com/info/{titleId} - } else { - pathSegments[0] // manhua.dmzj.com/{titleId} - } - val mainIntent = Intent().apply { - action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "$PREFIX_ID_SEARCH$titleId") - putExtra("filter", packageName) - } - - try { - startActivity(mainIntent) - } catch (e: ActivityNotFoundException) { - Log.e("DmzjUrlActivity", e.toString()) - } - } else { - Log.e("DmzjUrlActivity", "Could not parse uri from intent $intent") - } - - finish() - exitProcess(0) - } -} diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Filters.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Filters.kt deleted file mode 100644 index f437390d8..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Filters.kt +++ /dev/null @@ -1,201 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.source.model.FilterList - -fun getFilterListInternal(isMultiGenre: Boolean) = FilterList( - RankingGroup(), - Filter.Separator(), - Filter.Header("分类筛选(查看排行榜、搜索文本时无效)"), - if (isMultiGenre) GenreGroup() else GenreSelectFilter(), - StatusFilter(), - ReaderFilter(), - RegionFilter(), - SortFilter(), -) - -// region Ranking filters - -class RankingGroup : Filter.Group>( - "排行榜(搜索文本时无效)", - listOf>( - EnabledFilter(), - TimeFilter(), - SortFilter(), - GenreFilter(), - ), -) { - val isEnabled get() = (state[0] as EnabledFilter).state - - fun parse() = state.filterIsInstance().joinToString("&") { it.uriPart } - - private class EnabledFilter : CheckBox("查看排行榜") - - private class TimeFilter : QueryFilter( - "榜单", - "by_time", - arrayOf( - Pair("日排行", "0"), - Pair("周排行", "1"), - Pair("月排行", "2"), - Pair("总排行", "3"), - ), - ) - - private class SortFilter : QueryFilter( - "排序", - "rank_type", - arrayOf( - Pair("人气", "0"), - Pair("吐槽", "1"), - Pair("订阅", "2"), - ), - ) - - private class GenreFilter : QueryFilter("题材(慎用/易出错)", "tag_id", genres) - - private open class QueryFilter( - name: String, - private val query: String, - values: Array>, - ) : SelectFilter(name, values) { - override val uriPart get() = query + '=' + super.uriPart - } -} - -// endregion - -// region Normal filters - -fun parseFilters(filters: FilterList): String { - val tags = filters.filterIsInstance().mapNotNull { - it.uriPart.takeUnless(String::isEmpty) - }.joinToString("-").ifEmpty { "0" } - val sort = filters.filterIsInstance().firstOrNull()?.uriPart ?: "0" - return "$tags/$sort" -} - -private interface TagFilter : UriPartFilter - -private class GenreSelectFilter : TagFilter, SelectFilter("题材", genres) - -private class GenreGroup : TagFilter, Filter.Group( - "题材(作品需包含勾选的所有项目)", - genres.drop(1).map { GenreFilter(it.first, it.second) }, -) { - override val uriPart get() = state.filter { it.state }.joinToString("-") { it.value } -} - -private class GenreFilter(name: String, val value: String) : Filter.CheckBox(name) - -private class StatusFilter : TagFilter, SelectFilter( - "状态", - arrayOf( - Pair("全部", ""), - Pair("连载中", "2309"), - Pair("已完结", "2310"), - ), -) - -private class ReaderFilter : TagFilter, SelectFilter( - "受众", - arrayOf( - Pair("全部", ""), - Pair("少年漫画", "3262"), - Pair("少女漫画", "3263"), - Pair("青年漫画", "3264"), - Pair("女青漫画", "13626"), - ), -) - -private class RegionFilter : TagFilter, SelectFilter( - "地域", - arrayOf( - Pair("全部", ""), - Pair("日本", "2304"), - Pair("韩国", "2305"), - Pair("欧美", "2306"), - Pair("港台", "2307"), - Pair("内地", "2308"), - Pair("其他", "8453"), - ), -) - -private class SortFilter : SelectFilter( - "排序", - arrayOf( - Pair("人气", "0"), - Pair("更新", "1"), - ), -) - -// endregion - -private val genres - get() = arrayOf( - Pair("全部", ""), - 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("2021大赛", "23399"), - Pair("未来漫画家", "25011"), - ) - -interface UriPartFilter { - val uriPart: String -} - -private open class SelectFilter( - name: String, - values: Array>, -) : UriPartFilter, Filter.Select( - name = name, - values = Array(values.size) { values[it].first }, -) { - private val uriParts = Array(values.size) { values[it].second } - override val uriPart get() = uriParts[state] -} diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ImageUrlInterceptor.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ImageUrlInterceptor.kt deleted file mode 100644 index caaf0035b..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/ImageUrlInterceptor.kt +++ /dev/null @@ -1,41 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -import android.util.Log -import okhttp3.Interceptor -import okhttp3.Response -import java.io.IOException - -object ImageUrlInterceptor : Interceptor { - - class Tag(val url: String?) - - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val tag = request.tag(Tag::class) ?: return chain.proceed(request) - - try { - val response = chain.proceed(request) - if (response.isSuccessful) return response - response.close() - Log.e("DMZJ", "failed to fetch '${request.url}': HTTP ${response.code}") - } catch (e: IOException) { - Log.e("DMZJ", "failed to fetch '${request.url}'", e) - } - - // this can sometimes bypass encoding issues by decoding '+' to ' ' - val decodedUrl = request.url.toString().decodePath() - val newRequest = request.newBuilder().url(decodedUrl).build() - try { - val response = chain.proceed(newRequest) - if (response.isSuccessful) return response - response.close() - Log.e("DMZJ", "failed to fetch '$decodedUrl': HTTP ${response.code}") - } catch (e: IOException) { - Log.e("DMZJ", "failed to fetch '$decodedUrl'", e) - } - - val url = tag.url ?: throw IOException() - val fallbackRequest = request.newBuilder().url(url).build() - return chain.proceed(fallbackRequest) - } -} diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Preferences.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Preferences.kt deleted file mode 100644 index b68c2a105..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/Preferences.kt +++ /dev/null @@ -1,63 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -import android.content.Context -import android.content.SharedPreferences -import androidx.preference.ListPreference -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) = arrayOf( - - ListPreference(context).apply { - key = IMAGE_QUALITY_PREF - title = "图片质量" - summary = "%s\n修改后,已加载的章节需要清除章节缓存才能生效。" - entries = arrayOf("优先原图", "只用原图 (加载出错概率更高)", "优先低清") - entryValues = arrayOf(AUTO_RES, ORIGINAL_RES, LOW_RES) - setDefaultValue(AUTO_RES) - }, - - SwitchPreferenceCompat(context).apply { - key = CHAPTER_COMMENTS_PREF - title = "章末吐槽页" - summary = "修改后,已加载的章节需要清除章节缓存才能生效。" - setDefaultValue(false) - }, - - SwitchPreferenceCompat(context).apply { - key = MULTI_GENRE_FILTER_PREF - title = "分类筛选时允许勾选多个题材" - summary = "可以更精细地筛选出同时符合多个题材的作品。" - setDefaultValue(false) - }, - - SwitchPreferenceCompat(context).apply { - key = DMZJ_V3API_PREF - title = "V3API选择" - summary = "是否使用旧版v3API(默认nnv3api)" - setDefaultValue(false) - }, -) - -val SharedPreferences.imageQuality get() = getString(IMAGE_QUALITY_PREF, AUTO_RES)!! - -val SharedPreferences.showChapterComments get() = getBoolean(CHAPTER_COMMENTS_PREF, false) - -val SharedPreferences.isMultiGenreFilter get() = getBoolean(MULTI_GENRE_FILTER_PREF, false) - -val SharedPreferences.isOlderV3API get() = getBoolean(DMZJ_V3API_PREF, false) - -private const val IMAGE_QUALITY_PREF = "imageSourcePreference" -const val AUTO_RES = "PREFER_ORIG_RES" -const val ORIGINAL_RES = "ORIG_RES_ONLY" -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 DMZJ_V3API_PREF = "v3APIVersion" diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/RetryInterceptor.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/RetryInterceptor.kt deleted file mode 100644 index f6e45754a..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/RetryInterceptor.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj - -import android.util.Log -import okhttp3.Interceptor -import okhttp3.Response - -object RetryInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - repeat(2) { - val response = chain.proceed(request) - if (response.isSuccessful) return response - response.close() - Log.e("DMZJ", "failed to fetch '${request.url}': HTTP ${response.code}") - } - return chain.proceed(request) - } -} diff --git a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/utils/RSA.kt b/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/utils/RSA.kt deleted file mode 100644 index 4ec3595f7..000000000 --- a/src/zh/dmzj/src/eu/kanade/tachiyomi/extension/zh/dmzj/utils/RSA.kt +++ /dev/null @@ -1,42 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.dmzj.utils - -import android.util.Base64 -import java.security.KeyFactory -import java.security.PrivateKey -import java.security.spec.PKCS8EncodedKeySpec -import javax.crypto.Cipher -import kotlin.math.min - -object RSA { - private val cipher by lazy(LazyThreadSafetyMode.NONE) { - Cipher.getInstance("RSA/ECB/PKCS1Padding") - } - - private const val MAX_DECRYPT_BLOCK = 128 - - fun getPrivateKey(privateKey: String): PrivateKey { - val keyBytes = Base64.decode(privateKey, Base64.DEFAULT) - val pkcs8KeySpec = PKCS8EncodedKeySpec(keyBytes) - val keyFactory = KeyFactory.getInstance("RSA") - val privateK = keyFactory.generatePrivate(pkcs8KeySpec) - return privateK - } - - @Synchronized // because Cipher is not thread-safe - fun decrypt(encrypted: String, key: PrivateKey): ByteArray { - val cipher = this.cipher - cipher.init(Cipher.DECRYPT_MODE, key) // always reset in case of illegal state - val encryptedData = Base64.decode(encrypted, Base64.DEFAULT) - val inputLen = encryptedData.size - - val result = ByteArray(inputLen) - var resultSize = 0 - - for (offset in 0 until inputLen step MAX_DECRYPT_BLOCK) { - val blockLen = min(MAX_DECRYPT_BLOCK, inputLen - offset) - resultSize += cipher.doFinal(encryptedData, offset, blockLen, result, resultSize) - } - - return result.copyOf(resultSize) - } -}