Baozimanhua: fix metadata, remove banner and add mirror preference (#11864)
* Baozimanhua: fix metadata and remove banner * Baozimanhua: add preference to set mirror URL * Baozimanhua: add changelog * Baozimanhua: recycle banner bitmap * Baozimanhua: update selector and URL
							
								
								
									
										18
									
								
								src/zh/baozimanhua/CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,18 @@
 | 
			
		||||
## 1.2.4 (2022-05-14)
 | 
			
		||||
 | 
			
		||||
- 图源名改为中文
 | 
			
		||||
- 自动切除图片中的 banner
 | 
			
		||||
- 在设置中可以选择镜像网址
 | 
			
		||||
- 更新解析选择器和地址
 | 
			
		||||
 | 
			
		||||
## 1.2.3 (2022-01-27)
 | 
			
		||||
 | 
			
		||||
- 修复图片列表解析
 | 
			
		||||
 | 
			
		||||
## 1.2.2 (2021-12-06)
 | 
			
		||||
 | 
			
		||||
- (批量更改) 修复安卓 12 URL 跳转
 | 
			
		||||
 | 
			
		||||
## 1.2.1 (2021-08-31)
 | 
			
		||||
 | 
			
		||||
- 第一版
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/zh/baozimanhua/banner.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 32 KiB  | 
@ -1,12 +1,11 @@
 | 
			
		||||
apply plugin: 'com.android.application'
 | 
			
		||||
apply plugin: 'kotlin-android'
 | 
			
		||||
apply plugin: 'kotlinx-serialization'
 | 
			
		||||
 | 
			
		||||
ext {
 | 
			
		||||
    extName = 'Baozimanhua'
 | 
			
		||||
    pkgNameSuffix = 'zh.baozimanhua'
 | 
			
		||||
    extClass = '.Baozimanhua'
 | 
			
		||||
    extVersionCode = 3
 | 
			
		||||
    extVersionCode = 4
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$rootDir/common.gradle"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								src/zh/baozimanhua/convert_banner.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,17 @@
 | 
			
		||||
from base64 import b64encode
 | 
			
		||||
 | 
			
		||||
with open('banner.jpg', 'rb') as f:
 | 
			
		||||
    data = f.read()
 | 
			
		||||
 | 
			
		||||
head = b'''\
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.baozimanhua
 | 
			
		||||
 | 
			
		||||
const val BANNER_BASE64 = "\
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
tail = b'"\n'
 | 
			
		||||
 | 
			
		||||
with open('src/eu/kanade/tachiyomi/extension/zh/baozimanhua/BannerData.kt', 'wb') as f:
 | 
			
		||||
    f.write(head)
 | 
			
		||||
    f.write(b64encode(data))
 | 
			
		||||
    f.write(tail)
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.7 KiB  | 
| 
		 Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB  | 
| 
		 Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.1 KiB  | 
| 
		 Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.3 KiB  | 
| 
		 Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB  | 
| 
		 Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 64 KiB  | 
@ -0,0 +1,84 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.baozimanhua
 | 
			
		||||
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import android.graphics.BitmapFactory
 | 
			
		||||
import android.util.Base64
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaType
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import okhttp3.ResponseBody.Companion.toResponseBody
 | 
			
		||||
import java.io.ByteArrayOutputStream
 | 
			
		||||
import kotlin.math.abs
 | 
			
		||||
 | 
			
		||||
class BannerInterceptor : Interceptor {
 | 
			
		||||
    private val banner: Bitmap by lazy {
 | 
			
		||||
        val buffer = Base64.decode(BANNER_BASE64, Base64.DEFAULT)
 | 
			
		||||
        BitmapFactory.decodeByteArray(buffer, 0, buffer.size)
 | 
			
		||||
    }
 | 
			
		||||
    private val w by lazy { banner.width }
 | 
			
		||||
    private val h by lazy { banner.height }
 | 
			
		||||
    private val size by lazy { w * h }
 | 
			
		||||
    private val bannerBuffer by lazy {
 | 
			
		||||
        val buffer = IntArray(size)
 | 
			
		||||
        banner.getPixels(buffer, 0, w, 0, 0, w, h)
 | 
			
		||||
        banner.recycle()
 | 
			
		||||
        buffer
 | 
			
		||||
    }
 | 
			
		||||
    private val threshold by lazy { w * h * 3 / 2 } // 0.5 per pixel per channel
 | 
			
		||||
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
        val url = chain.request().url.toString()
 | 
			
		||||
        val response = chain.proceed(chain.request())
 | 
			
		||||
        if (!url.endsWith(COMIC_IMAGE_SUFFIX)) return response
 | 
			
		||||
        val body = response.body!!
 | 
			
		||||
        val contentType = body.contentType()
 | 
			
		||||
        val content = body.bytes()
 | 
			
		||||
        val bitmap = BitmapFactory.decodeByteArray(content, 0, content.size)
 | 
			
		||||
        val position = checkBanner(bitmap)
 | 
			
		||||
        return if (position == null) {
 | 
			
		||||
            response.newBuilder().body(content.toResponseBody(contentType)).build()
 | 
			
		||||
        } else {
 | 
			
		||||
            val result = Bitmap.createBitmap(
 | 
			
		||||
                bitmap, 0,
 | 
			
		||||
                when (position) {
 | 
			
		||||
                    BannerPosition.TOP -> h
 | 
			
		||||
                    BannerPosition.BOTTOM -> 0
 | 
			
		||||
                },
 | 
			
		||||
                bitmap.width, bitmap.height - h
 | 
			
		||||
            )
 | 
			
		||||
            val output = ByteArrayOutputStream()
 | 
			
		||||
            result.compress(Bitmap.CompressFormat.JPEG, 90, output)
 | 
			
		||||
            val responseBody = output.toByteArray().toResponseBody("image/jpeg".toMediaType())
 | 
			
		||||
            response.newBuilder().body(responseBody).build()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun checkBanner(image: Bitmap): BannerPosition? {
 | 
			
		||||
        if (image.width < w || image.height < h) return null
 | 
			
		||||
        if ((image.width - w) % 2 != 0) return null
 | 
			
		||||
        val pad = (image.width - w) / 2
 | 
			
		||||
        val buf = IntArray(size)
 | 
			
		||||
        image.getPixels(buf, 0, w, pad, 0, w, h) // top
 | 
			
		||||
        if (isIdentical(bannerBuffer, buf)) return BannerPosition.TOP
 | 
			
		||||
        image.getPixels(buf, 0, w, pad, image.height - h, w, h) // bottom
 | 
			
		||||
        if (isIdentical(bannerBuffer, buf)) return BannerPosition.BOTTOM
 | 
			
		||||
        return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun isIdentical(a: IntArray, b: IntArray): Boolean {
 | 
			
		||||
        var diff = 0
 | 
			
		||||
        for (i in 0 until size) {
 | 
			
		||||
            val pixel0 = a[i]
 | 
			
		||||
            val pixel1 = b[i]
 | 
			
		||||
            diff += abs((pixel0 and 0xFF) - (pixel1 and 0xFF))
 | 
			
		||||
            diff += abs((pixel0 shr 8 and 0xFF) - (pixel1 shr 8 and 0xFF))
 | 
			
		||||
            diff += abs((pixel0 shr 16 and 0xFF) - (pixel1 shr 16 and 0xFF))
 | 
			
		||||
            if (diff > threshold) return false
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private enum class BannerPosition { TOP, BOTTOM }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const val COMIC_IMAGE_SUFFIX = "#baozi"
 | 
			
		||||
@ -1,7 +1,11 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.baozimanhua
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import androidx.preference.PreferenceScreen
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import eu.kanade.tachiyomi.source.ConfigurableSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
@ -16,18 +20,27 @@ import okhttp3.Response
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
class Baozimanhua : ParsedHttpSource() {
 | 
			
		||||
class Baozimanhua : ParsedHttpSource(), ConfigurableSource {
 | 
			
		||||
 | 
			
		||||
    override val name = "Baozimanhua"
 | 
			
		||||
    override val id = 5724751873601868259
 | 
			
		||||
 | 
			
		||||
    override val baseUrl = "https://cn.baozimh.com"
 | 
			
		||||
    override val name = "包子漫画"
 | 
			
		||||
 | 
			
		||||
    private val preferences: SharedPreferences by lazy {
 | 
			
		||||
        Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val baseUrl = "https://${preferences.getString(MIRROR_PREF, MIRRORS[0])}"
 | 
			
		||||
 | 
			
		||||
    override val lang = "zh"
 | 
			
		||||
 | 
			
		||||
    override val supportsLatest = true
 | 
			
		||||
 | 
			
		||||
    override val client: OkHttpClient = network.cloudflareClient
 | 
			
		||||
    override val client: OkHttpClient = network.cloudflareClient.newBuilder()
 | 
			
		||||
        .addNetworkInterceptor(BannerInterceptor()).build()
 | 
			
		||||
 | 
			
		||||
    override fun chapterListSelector(): String = "div.pure-g[id^=chapter] > div"
 | 
			
		||||
 | 
			
		||||
@ -42,7 +55,7 @@ class Baozimanhua : ParsedHttpSource() {
 | 
			
		||||
 | 
			
		||||
    override fun chapterFromElement(element: Element): SChapter {
 | 
			
		||||
        return SChapter.create().apply {
 | 
			
		||||
            url = element.select("a").attr("href").trim()
 | 
			
		||||
            setUrlWithoutDomain(element.select("a").attr("href").trim())
 | 
			
		||||
            name = element.text()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -51,7 +64,7 @@ class Baozimanhua : ParsedHttpSource() {
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaFromElement(element: Element): SManga {
 | 
			
		||||
        return SManga.create().apply {
 | 
			
		||||
            url = element.attr("href")!!.trim()
 | 
			
		||||
            setUrlWithoutDomain(element.attr("href")!!.trim())
 | 
			
		||||
            title = element.attr("title")!!.trim()
 | 
			
		||||
            thumbnail_url = element.select("> amp-img").attr("src")!!.trim()
 | 
			
		||||
        }
 | 
			
		||||
@ -59,7 +72,7 @@ class Baozimanhua : ParsedHttpSource() {
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaNextPageSelector() = throw java.lang.UnsupportedOperationException("Not used.")
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaRequest(page: Int): Request = GET("https://www.baozimh.com/classify", headers)
 | 
			
		||||
    override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/classify", headers)
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaParse(response: Response): MangasPage {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
@ -104,8 +117,8 @@ class Baozimanhua : ParsedHttpSource() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun pageListParse(document: Document): List<Page> {
 | 
			
		||||
        val pages = document.select("section.comic-contain > amp-img").mapIndexed() { index, element ->
 | 
			
		||||
            Page(index, imageUrl = element.attr("src").trim())
 | 
			
		||||
        val pages = document.select(".comic-contain > .chapter-img > img").mapIndexed { index, element ->
 | 
			
		||||
            Page(index, imageUrl = element.attr("data-src").trim() + COMIC_IMAGE_SUFFIX)
 | 
			
		||||
        }
 | 
			
		||||
        return pages
 | 
			
		||||
    }
 | 
			
		||||
@ -301,7 +314,28 @@ class Baozimanhua : ParsedHttpSource() {
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun setupPreferenceScreen(screen: PreferenceScreen) {
 | 
			
		||||
        val mirrorPref = androidx.preference.ListPreference(screen.context).apply {
 | 
			
		||||
            key = MIRROR_PREF
 | 
			
		||||
            title = MIRROR_PREF_TITLE
 | 
			
		||||
            entries = MIRRORS
 | 
			
		||||
            entryValues = MIRRORS
 | 
			
		||||
            summary = MIRROR_PREF_SUMMARY
 | 
			
		||||
 | 
			
		||||
            setDefaultValue(MIRRORS[0])
 | 
			
		||||
            setOnPreferenceChangeListener { _, newValue ->
 | 
			
		||||
                preferences.edit().putString(MIRROR_PREF, newValue as String).commit()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        screen.addPreference(mirrorPref)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val ID_SEARCH_PREFIX = "id:"
 | 
			
		||||
 | 
			
		||||
        private const val MIRROR_PREF = "MIRROR"
 | 
			
		||||
        private const val MIRROR_PREF_TITLE = "使用镜像网址"
 | 
			
		||||
        private const val MIRROR_PREF_SUMMARY = "使用镜像网址。重启软件生效。"
 | 
			
		||||
        private val MIRRORS = arrayOf("cn.baozimh.com", "cn.webmota.com")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||