Jinmantiantang: update mirror URLs and refactor (#12059)
* Jinmantiantang: update mirror URLs * Jinmantiantang: optimize assets * Jinmantiantang: extract interceptor and cleanup * Jinmantiantang: make interceptor singleton
| @ -23,15 +23,11 @@ | |||||||
|                     android:pathPattern="/album/..*" |                     android:pathPattern="/album/..*" | ||||||
|                     android:scheme="https" /> |                     android:scheme="https" /> | ||||||
|                 <data |                 <data | ||||||
|                     android:host="jmcomic9.cc" |                     android:host="jmcomic.asia" | ||||||
|                     android:pathPattern="/album/..*" |                     android:pathPattern="/album/..*" | ||||||
|                     android:scheme="https" /> |                     android:scheme="https" /> | ||||||
|                 <data |                 <data | ||||||
|                     android:host="jmcomic.mobi" |                     android:host="jmcomic1.asia" | ||||||
|                     android:pathPattern="/album/..*" |  | ||||||
|                     android:scheme="https" /> |  | ||||||
|                 <data |  | ||||||
|                     android:host="jmcomic1.mobi" |  | ||||||
|                     android:pathPattern="/album/..*" |                     android:pathPattern="/album/..*" | ||||||
|                     android:scheme="https" /> |                     android:scheme="https" /> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ ext { | |||||||
|     extName = 'Jinmantiantang' |     extName = 'Jinmantiantang' | ||||||
|     pkgNameSuffix = 'zh.jinmantiantang' |     pkgNameSuffix = 'zh.jinmantiantang' | ||||||
|     extClass = '.Jinmantiantang' |     extClass = '.Jinmantiantang' | ||||||
|     extVersionCode = 24 |     extVersionCode = 25 | ||||||
|     isNsfw = true |     isNsfw = true | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 8.4 KiB | 
| Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 93 KiB | 
| Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.1 KiB | 
| Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 26 KiB | 
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 452 KiB After Width: | Height: | Size: 259 KiB | 
| @ -2,10 +2,6 @@ package eu.kanade.tachiyomi.extension.zh.jinmantiantang | |||||||
| 
 | 
 | ||||||
| import android.app.Application | import android.app.Application | ||||||
| import android.content.SharedPreferences | import android.content.SharedPreferences | ||||||
| import android.graphics.Bitmap |  | ||||||
| import android.graphics.BitmapFactory |  | ||||||
| import android.graphics.Canvas |  | ||||||
| import android.graphics.Rect |  | ||||||
| import androidx.preference.EditTextPreference | import androidx.preference.EditTextPreference | ||||||
| import androidx.preference.PreferenceScreen | import androidx.preference.PreferenceScreen | ||||||
| import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor | import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor | ||||||
| @ -22,24 +18,17 @@ import eu.kanade.tachiyomi.source.online.ParsedHttpSource | |||||||
| import eu.kanade.tachiyomi.util.asJsoup | import eu.kanade.tachiyomi.util.asJsoup | ||||||
| import okhttp3.HttpUrl | import okhttp3.HttpUrl | ||||||
| import okhttp3.HttpUrl.Companion.toHttpUrlOrNull | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull | ||||||
| import okhttp3.MediaType.Companion.toMediaTypeOrNull |  | ||||||
| import okhttp3.OkHttpClient | import okhttp3.OkHttpClient | ||||||
| import okhttp3.Request | import okhttp3.Request | ||||||
| import okhttp3.Response | import okhttp3.Response | ||||||
| import okhttp3.ResponseBody.Companion.toResponseBody |  | ||||||
| import org.jsoup.nodes.Document | import org.jsoup.nodes.Document | ||||||
| import org.jsoup.nodes.Element | import org.jsoup.nodes.Element | ||||||
| import org.jsoup.select.Elements | import org.jsoup.select.Elements | ||||||
| import rx.Observable | import rx.Observable | ||||||
| import uy.kohesive.injekt.Injekt | import uy.kohesive.injekt.Injekt | ||||||
| import uy.kohesive.injekt.api.get | import uy.kohesive.injekt.api.get | ||||||
| import java.io.ByteArrayOutputStream |  | ||||||
| import java.io.InputStream |  | ||||||
| import java.math.BigInteger |  | ||||||
| import java.security.MessageDigest |  | ||||||
| import java.text.SimpleDateFormat | import java.text.SimpleDateFormat | ||||||
| import java.util.Locale | import java.util.Locale | ||||||
| import kotlin.math.floor |  | ||||||
| 
 | 
 | ||||||
| class Jinmantiantang : ConfigurableSource, ParsedHttpSource() { | class Jinmantiantang : ConfigurableSource, ParsedHttpSource() { | ||||||
| 
 | 
 | ||||||
| @ -58,82 +47,12 @@ class Jinmantiantang : ConfigurableSource, ParsedHttpSource() { | |||||||
|     // Add rate limit to fix manga thumbnail load failure |     // Add rate limit to fix manga thumbnail load failure | ||||||
|     private val mainSiteRateLimitInterceptor = SpecificHostRateLimitInterceptor(baseHttpUrl, preferences.getString(MAINSITE_RATELIMIT_PREF, "1")!!.toInt(), preferences.getString(MAINSITE_RATELIMIT_PERIOD, "3")!!.toLong()) |     private val mainSiteRateLimitInterceptor = SpecificHostRateLimitInterceptor(baseHttpUrl, preferences.getString(MAINSITE_RATELIMIT_PREF, "1")!!.toInt(), preferences.getString(MAINSITE_RATELIMIT_PERIOD, "3")!!.toLong()) | ||||||
| 
 | 
 | ||||||
|     // 220980 |  | ||||||
|     // 算法 html页面 1800 行左右 |  | ||||||
|     // 图片开始分割的ID编号 |  | ||||||
|     private val scrambleId = 220980 |  | ||||||
| 
 |  | ||||||
|     // 处理URL请求 |     // 处理URL请求 | ||||||
|     override val client: OkHttpClient = network.cloudflareClient |     override val client: OkHttpClient = network.cloudflareClient | ||||||
|         .newBuilder() |         .newBuilder() | ||||||
|         .addNetworkInterceptor(mainSiteRateLimitInterceptor) |         .addNetworkInterceptor(mainSiteRateLimitInterceptor) | ||||||
| //        .addNetworkInterceptor(RateLimitInterceptor(1, 3)) | //        .addNetworkInterceptor(RateLimitInterceptor(1, 3)) | ||||||
|         .addInterceptor( |         .addInterceptor(ScrambledImageInterceptor).build() | ||||||
|             fun(chain): Response { |  | ||||||
|                 val url = chain.request().url.toString() |  | ||||||
|                 val response = chain.proceed(chain.request()) |  | ||||||
|                 if (!url.contains("media/photos", ignoreCase = true)) return response // 对非漫画图片连接直接放行 |  | ||||||
|                 if (url.substring(url.indexOf("photos/") + 7, url.lastIndexOf("/")).toInt() < scrambleId) return response // 对在漫画章节ID为220980之前的图片未进行图片分割,直接放行 |  | ||||||
| // 章节ID:220980(包含)之后的漫画(2020.10.27之后)图片进行了分割getRows倒序处理 |  | ||||||
|                 val aid = url.substring(url.indexOf("photos/") + 7, url.lastIndexOf("/")).toInt() |  | ||||||
|                 val imgIndex: String = url.substringAfterLast("/").substringBefore(".") |  | ||||||
|                 val res = response.body!!.byteStream().use { |  | ||||||
|                     decodeImage(it, getRows(aid, imgIndex)) |  | ||||||
|                 } |  | ||||||
|                 val mediaType = "image/avif,image/webp,image/apng,image/*,*/*".toMediaTypeOrNull() |  | ||||||
|                 val outputBytes = res.toResponseBody(mediaType) |  | ||||||
|                 return response.newBuilder().body(outputBytes).build() |  | ||||||
|             } |  | ||||||
|         ).build() |  | ||||||
| 
 |  | ||||||
|     private fun getRows(aid: Int, imgIndex: String): Int { |  | ||||||
|         fun md5(input: String): String { |  | ||||||
|             val md = MessageDigest.getInstance("MD5") |  | ||||||
|             return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return if (aid >= 268850) { |  | ||||||
|             2 * (md5(aid.toString() + imgIndex).last().toInt() % 10) + 2 |  | ||||||
|         } else { |  | ||||||
|             10 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // 对被分割的图片进行分割,排序处理 |  | ||||||
|     private fun decodeImage(img: InputStream, rows: Int): ByteArray { |  | ||||||
|         // 使用bitmap进行图片处理 |  | ||||||
|         val input = BitmapFactory.decodeStream(img) |  | ||||||
|         // 漫画高度 and width |  | ||||||
|         val height = input.height |  | ||||||
|         val width = input.width |  | ||||||
|         // 未除尽像素 |  | ||||||
|         val remainder = (height % rows) |  | ||||||
|         // 创建新的图片对象 |  | ||||||
|         val resultBitmap = Bitmap.createBitmap(input.width, input.height, Bitmap.Config.ARGB_8888) |  | ||||||
|         val canvas = Canvas(resultBitmap) |  | ||||||
|         // 分割图片 |  | ||||||
|         for (x in 0 until rows) { |  | ||||||
|             // 分割算法(详情见html源码页的方法"function scramble_image(img)") |  | ||||||
|             var copyH = floor(height / rows.toDouble()).toInt() |  | ||||||
|             var py = copyH * (x) |  | ||||||
|             val y = height - (copyH * (x + 1)) - remainder |  | ||||||
|             if (x == 0) { |  | ||||||
|                 copyH += remainder |  | ||||||
|             } else { |  | ||||||
|                 py += remainder |  | ||||||
|             } |  | ||||||
|             // 要裁剪的区域 |  | ||||||
|             val crop = Rect(0, y, width, y + copyH) |  | ||||||
|             // 裁剪后应放置到新图片对象的区域 |  | ||||||
|             val splic = Rect(0, py, width, py + copyH) |  | ||||||
| 
 |  | ||||||
|             canvas.drawBitmap(input, crop, splic, null) |  | ||||||
|         } |  | ||||||
|         // 创建输出流 |  | ||||||
|         val output = ByteArrayOutputStream() |  | ||||||
|         resultBitmap.compress(Bitmap.CompressFormat.JPEG, 100, output) |  | ||||||
|         return output.toByteArray() |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     // 点击量排序(人气) |     // 点击量排序(人气) | ||||||
|     override fun popularMangaRequest(page: Int): Request { |     override fun popularMangaRequest(page: Int): Request { | ||||||
| @ -311,7 +230,7 @@ class Jinmantiantang : ConfigurableSource, ParsedHttpSource() { | |||||||
| 
 | 
 | ||||||
|     // 漫画图片信息 |     // 漫画图片信息 | ||||||
|     override fun pageListParse(document: Document): List<Page> { |     override fun pageListParse(document: Document): List<Page> { | ||||||
|         fun internalParse(document: Document, pages: MutableList<Page>): List<Page> { |         tailrec fun internalParse(document: Document, pages: MutableList<Page>): List<Page> { | ||||||
|             val elements = document.select("div[class=center scramble-page][id*=0]") |             val elements = document.select("div[class=center scramble-page][id*=0]") | ||||||
|             for (element in elements) { |             for (element in elements) { | ||||||
|                 pages.apply { |                 pages.apply { | ||||||
| @ -322,9 +241,10 @@ class Jinmantiantang : ConfigurableSource, ParsedHttpSource() { | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             return document.select("a.prevnext").firstOrNull() |             return when (val next = document.select("a.prevnext").firstOrNull()) { | ||||||
|                 ?.let { internalParse(client.newCall(GET(it.attr("abs:href"), headers)).execute().asJsoup(), pages) } |                 null -> pages | ||||||
|                 ?: pages |                 else -> internalParse(client.newCall(GET(next.attr("abs:href"), headers)).execute().asJsoup(), pages) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return internalParse(document, mutableListOf()) |         return internalParse(document, mutableListOf()) | ||||||
| @ -555,14 +475,15 @@ class Jinmantiantang : ConfigurableSource, ParsedHttpSource() { | |||||||
|         private val PERIOD_ENTRIES_ARRAY = (1..60).map { i -> i.toString() }.toTypedArray() |         private val PERIOD_ENTRIES_ARRAY = (1..60).map { i -> i.toString() }.toTypedArray() | ||||||
|         private val SITE_ENTRIES_ARRAY_DESCRIPTION = arrayOf( |         private val SITE_ENTRIES_ARRAY_DESCRIPTION = arrayOf( | ||||||
|             "主站", "海外分流", |             "主站", "海外分流", | ||||||
|             "中国大陆总站", "中国大陆分流1", "中国大陆分流2" |             "中国大陆总站", "中国大陆分流1", "中国大陆分流1" | ||||||
|         ) |         ) | ||||||
|         private val SITE_ENTRIES_ARRAY_VALUE = (0..4).map { i -> i.toString() }.toTypedArray() |         private val SITE_ENTRIES_ARRAY_VALUE = (0..4).map { i -> i.toString() }.toTypedArray() | ||||||
|  | 
 | ||||||
|         // List is based on https://jmcomic.bet/ |         // List is based on https://jmcomic.bet/ | ||||||
|         // Please also update AndroidManifest |         // Please also update AndroidManifest | ||||||
|         private val SITE_ENTRIES_ARRAY = arrayOf( |         private val SITE_ENTRIES_ARRAY = arrayOf( | ||||||
|             DEFAULT_SITE, "18comic.org", |             DEFAULT_SITE, "18comic.org", | ||||||
|             "jmcomic.mobi", "jmcomic1.mobi", "jmcomic1.mobi" |             "jmcomic.asia", "jmcomic1.asia", "jmcomic1.asia" | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,88 @@ | |||||||
|  | package eu.kanade.tachiyomi.extension.zh.jinmantiantang | ||||||
|  | 
 | ||||||
|  | import android.graphics.Bitmap | ||||||
|  | import android.graphics.BitmapFactory | ||||||
|  | import android.graphics.Canvas | ||||||
|  | import android.graphics.Rect | ||||||
|  | import okhttp3.Interceptor | ||||||
|  | import okhttp3.MediaType.Companion.toMediaType | ||||||
|  | import okhttp3.Response | ||||||
|  | import okhttp3.ResponseBody.Companion.toResponseBody | ||||||
|  | import java.io.ByteArrayOutputStream | ||||||
|  | import java.io.InputStream | ||||||
|  | import java.math.BigInteger | ||||||
|  | import java.security.MessageDigest | ||||||
|  | import kotlin.math.floor | ||||||
|  | 
 | ||||||
|  | object ScrambledImageInterceptor : Interceptor { | ||||||
|  |     override fun intercept(chain: Interceptor.Chain): Response { | ||||||
|  |         val url = chain.request().url.toString() | ||||||
|  |         val response = chain.proceed(chain.request()) | ||||||
|  |         if (!url.contains("media/photos", ignoreCase = true)) return response // 对非漫画图片连接直接放行 | ||||||
|  |         if (url.substring(url.indexOf("photos/") + 7, url.lastIndexOf("/")).toInt() < scrambleId) return response // 对在漫画章节ID为220980之前的图片未进行图片分割,直接放行 | ||||||
|  | // 章节ID:220980(包含)之后的漫画(2020.10.27之后)图片进行了分割getRows倒序处理 | ||||||
|  |         val aid = url.substring(url.indexOf("photos/") + 7, url.lastIndexOf("/")).toInt() | ||||||
|  |         val imgIndex: String = url.substringAfterLast("/").substringBefore(".") | ||||||
|  |         val res = response.body!!.byteStream().use { | ||||||
|  |             decodeImage(it, getRows(aid, imgIndex)) | ||||||
|  |         } | ||||||
|  |         val outputBytes = res.toResponseBody(jpegMediaType) | ||||||
|  |         return response.newBuilder().body(outputBytes).build() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 220980 | ||||||
|  |     // 算法 html页面 1800 行左右 | ||||||
|  |     // 图片开始分割的ID编号 | ||||||
|  |     private const val scrambleId = 220980 | ||||||
|  | 
 | ||||||
|  |     private fun getRows(aid: Int, imgIndex: String): Int { | ||||||
|  |         fun md5(input: String): String { | ||||||
|  |             val md = MessageDigest.getInstance("MD5") | ||||||
|  |             return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return if (aid >= 268850) { | ||||||
|  |             2 * (md5(aid.toString() + imgIndex).last().toInt() % 10) + 2 | ||||||
|  |         } else { | ||||||
|  |             10 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 对被分割的图片进行分割,排序处理 | ||||||
|  |     private fun decodeImage(img: InputStream, rows: Int): ByteArray { | ||||||
|  |         // 使用bitmap进行图片处理 | ||||||
|  |         val input = BitmapFactory.decodeStream(img) | ||||||
|  |         // 漫画高度 and width | ||||||
|  |         val height = input.height | ||||||
|  |         val width = input.width | ||||||
|  |         // 未除尽像素 | ||||||
|  |         val remainder = (height % rows) | ||||||
|  |         // 创建新的图片对象 | ||||||
|  |         val resultBitmap = Bitmap.createBitmap(input.width, input.height, Bitmap.Config.ARGB_8888) | ||||||
|  |         val canvas = Canvas(resultBitmap) | ||||||
|  |         // 分割图片 | ||||||
|  |         for (x in 0 until rows) { | ||||||
|  |             // 分割算法(详情见html源码页的方法"function scramble_image(img)") | ||||||
|  |             var copyH = floor(height / rows.toDouble()).toInt() | ||||||
|  |             var py = copyH * (x) | ||||||
|  |             val y = height - (copyH * (x + 1)) - remainder | ||||||
|  |             if (x == 0) { | ||||||
|  |                 copyH += remainder | ||||||
|  |             } else { | ||||||
|  |                 py += remainder | ||||||
|  |             } | ||||||
|  |             // 要裁剪的区域 | ||||||
|  |             val crop = Rect(0, y, width, y + copyH) | ||||||
|  |             // 裁剪后应放置到新图片对象的区域 | ||||||
|  |             val splic = Rect(0, py, width, py + copyH) | ||||||
|  | 
 | ||||||
|  |             canvas.drawBitmap(input, crop, splic, null) | ||||||
|  |         } | ||||||
|  |         // 创建输出流 | ||||||
|  |         val output = ByteArrayOutputStream() | ||||||
|  |         resultBitmap.compress(Bitmap.CompressFormat.JPEG, 90, output) | ||||||
|  |         return output.toByteArray() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private val jpegMediaType = "image/jpeg".toMediaType() | ||||||
|  | } | ||||||
 kasperskier
						kasperskier