Baozi Manhua: fix page list parsing and cleanup (#12560)

* Baozi Manhua: fix page list parsing and cleanup

* extract banner lib

* update changelog

* add rate limit
This commit is contained in:
stevenyomi 2022-07-14 20:40:42 +08:00 committed by GitHub
parent 53cd29f87e
commit 2e8e4b1916
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 176 additions and 254 deletions

View File

@ -3,7 +3,7 @@
package="eu.kanade.tachiyomi.extension"> package="eu.kanade.tachiyomi.extension">
<application> <application>
<activity <activity
android:name=".zh.baozimanhua.BaozimanhuaUrlActivity" android:name=".zh.baozimanhua.BaoziUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">

View File

@ -1,3 +1,10 @@
## 1.3.9 (2022-07-12)
- 修复章节图片解析
- 整理代码
- 优化移除横幅功能,添加设置项
- 增加限速
## 1.3.8 (2022-06-07) ## 1.3.8 (2022-06-07)
- 更好地处理章节上传日期(需要 app 版本 > 0.13.4 或预览版 ≥ r4442 - 更好地处理章节上传日期(需要 app 版本 > 0.13.4 或预览版 ≥ r4442

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@ -2,10 +2,14 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
ext { ext {
extName = 'Baozimanhua' extName = 'Baozi Manhua'
pkgNameSuffix = 'zh.baozimanhua' pkgNameSuffix = 'zh.baozimanhua'
extClass = '.Baozimanhua' extClass = '.Baozi'
extVersionCode = 8 extVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
dependencies {
implementation 'com.github.stevenyomi:baozibanner:9ac9b08e1d' // 1.0
}

View File

@ -1,21 +0,0 @@
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'''\
"
const val BANNER_WIDTH = 800
const val BANNER_HEIGHT = 282
'''
with open('src/eu/kanade/tachiyomi/extension/zh/baozimanhua/BannerData.kt', 'wb') as f:
f.write(head)
f.write(b64encode(data))
f.write(tail)

File diff suppressed because one or more lines are too long

View File

@ -1,80 +0,0 @@
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
object BannerInterceptor : Interceptor {
private const val w = BANNER_WIDTH
private const val h = BANNER_HEIGHT
private const val size = w * h
private const val threshold = w * h * 3 // 1 per pixel per channel
private val bannerBuffer by lazy {
val buffer = Base64.decode(BANNER_BASE64, Base64.DEFAULT)
val banner = BitmapFactory.decodeByteArray(buffer, 0, buffer.size)
val pixels = IntArray(size)
banner.getPixels(pixels, 0, w, 0, 0, w, h)
pixels
}
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 positions = checkBanner(bitmap)
return if (positions == 0) {
response.newBuilder().body(content.toResponseBody(contentType)).build()
} else {
val result = Bitmap.createBitmap(
bitmap, 0, if (positions and TOP == TOP) h else 0,
bitmap.width, bitmap.height - if (positions == BOTH) h * 2 else 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): Int {
if (image.width < w || image.height < h) return 0
if ((image.width - w) % 2 != 0) return 0
val pad = (image.width - w) / 2
val buf = IntArray(size)
var result = 0
image.getPixels(buf, 0, w, pad, 0, w, h) // top
if (isIdentical(bannerBuffer, buf)) result = result or TOP
image.getPixels(buf, 0, w, pad, image.height - h, w, h) // bottom
if (isIdentical(bannerBuffer, buf)) result = result or BOTTOM
return result
}
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 const val TOP = 0b01
private const val BOTTOM = 0b10
private const val BOTH = 0b11
const val COMIC_IMAGE_SUFFIX = "#baozi"
}

View File

@ -2,12 +2,14 @@ package eu.kanade.tachiyomi.extension.zh.baozimanhua
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import com.github.stevenyomi.baozibanner.BaoziBanner
import eu.kanade.tachiyomi.AppInfo import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -15,7 +17,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -26,7 +27,7 @@ import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class Baozimanhua : ParsedHttpSource(), ConfigurableSource { class Baozi : ParsedHttpSource(), ConfigurableSource {
override val id = 5724751873601868259 override val id = 5724751873601868259
@ -41,8 +42,14 @@ class Baozimanhua : ParsedHttpSource(), ConfigurableSource {
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder() private val bannerInterceptor = BaoziBanner(
.addInterceptor(BannerInterceptor).build() level = preferences.getString(BaoziBanner.PREF, DEFAULT_LEVEL)!!.toInt()
)
override val client = network.client.newBuilder()
.rateLimit(2)
.addInterceptor(bannerInterceptor)
.build()
override fun chapterListSelector() = throw UnsupportedOperationException("Not used.") override fun chapterListSelector() = throw UnsupportedOperationException("Not used.")
@ -116,8 +123,8 @@ class Baozimanhua : ParsedHttpSource(), ConfigurableSource {
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select(".comic-contain > amp-img").mapIndexed { index, element -> return document.select(".comic-contain amp-img").mapIndexed { index, element ->
Page(index, imageUrl = element.attr("src").trim() + BannerInterceptor.COMIC_IMAGE_SUFFIX) Page(index, imageUrl = element.attr("src"))
} }
} }
@ -175,142 +182,30 @@ class Baozimanhua : ParsedHttpSource(), ConfigurableSource {
} }
} }
override fun getFilterList() = FilterList( override fun getFilterList() = getFilters()
Filter.Header("注意:不影響按標題搜索"),
TagFilter(),
RegionFilter(),
StatusFilter(),
StartFilter()
)
private open class UriPartFilter(name: String, val query: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = "$query=${vals[state].second}"
}
private class TagFilter : UriPartFilter(
"标签",
"type",
arrayOf(
Pair("全部", "all"),
Pair("都市", "dushi"),
Pair("冒险", "mouxian"),
Pair("热血", "rexie"),
Pair("恋爱", "lianai"),
Pair("耽美", "danmei"),
Pair("武侠", "wuxia"),
Pair("格斗", "gedou"),
Pair("科幻", "kehuan"),
Pair("魔幻", "mohuan"),
Pair("推理", "tuili"),
Pair("玄幻", "xuanhuan"),
Pair("日常", "richang"),
Pair("生活", "shenghuo"),
Pair("搞笑", "gaoxiao"),
Pair("校园", "xiaoyuan"),
Pair("奇幻", "qihuan"),
Pair("萌系", "mengxi"),
Pair("穿越", "chuanyue"),
Pair("后宫", "hougong"),
Pair("战争", "zhanzheng"),
Pair("历史", "lishi"),
Pair("剧情", "juqing"),
Pair("同人", "tongren"),
Pair("竞技", "jingji"),
Pair("励志", "lizhi"),
Pair("治愈", "zhiyu"),
Pair("机甲", "jijia"),
Pair("纯爱", "chunai"),
Pair("美食", "meishi"),
Pair("恶搞", "egao"),
Pair("虐心", "nuexin"),
Pair("动作", "dongzuo"),
Pair("惊险", "liangxian"),
Pair("唯美", "weimei"),
Pair("复仇", "fuchou"),
Pair("脑洞", "naodong"),
Pair("宫斗", "gongdou"),
Pair("运动", "yundong"),
Pair("灵异", "lingyi"),
Pair("古风", "gufeng"),
Pair("权谋", "quanmou"),
Pair("节操", "jiecao"),
Pair("明星", "mingxing"),
Pair("暗黑", "anhei"),
Pair("社会", "shehui"),
Pair("音乐舞蹈", "yinlewudao"),
Pair("东方", "dongfang"),
Pair("AA", "aa"),
Pair("悬疑", "xuanyi"),
Pair("轻小说", "qingxiaoshuo"),
Pair("霸总", "bazong"),
Pair("萝莉", "luoli"),
Pair("战斗", "zhandou"),
Pair("惊悚", "liangsong"),
Pair("百合", "yuri"),
Pair("大女主", "danuzhu"),
Pair("幻想", "huanxiang"),
Pair("少女", "shaonu"),
Pair("少年", "shaonian"),
Pair("性转", "xingzhuanhuan"),
Pair("重生", "zhongsheng"),
Pair("韩漫", "hanman"),
Pair("其它", "qita")
)
)
private class RegionFilter : UriPartFilter(
"地区",
"region",
arrayOf(
Pair("全部", "all"),
Pair("国漫", "cn"),
Pair("日本", "jp"),
Pair("韩国", "kr"),
Pair("欧美", "en")
)
)
private class StatusFilter : UriPartFilter(
"进度",
"state",
arrayOf(
Pair("全部", "all"),
Pair("连载中", "serial"),
Pair("已完结", "pub")
)
)
private class StartFilter : UriPartFilter(
"标题开头",
"filter",
arrayOf(
Pair("全部", "*"),
Pair("ABCD", "ABCD"),
Pair("EFGH", "EFGH"),
Pair("IJKL", "IJKL"),
Pair("MNOP", "MNOP"),
Pair("QRST", "QRST"),
Pair("UVW", "UVW"),
Pair("XYZ", "XYZ"),
Pair("0-9", "0-9")
)
)
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val mirrorPref = androidx.preference.ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = MIRROR_PREF key = MIRROR_PREF
title = MIRROR_PREF_TITLE title = MIRROR_PREF_TITLE
entries = MIRRORS entries = MIRRORS
entryValues = MIRRORS entryValues = MIRRORS
summary = MIRROR_PREF_SUMMARY summary = MIRROR_PREF_SUMMARY
setDefaultValue(MIRRORS[0]) setDefaultValue(MIRRORS[0])
}.let { screen.addPreference(it) }
ListPreference(screen.context).apply {
key = BaoziBanner.PREF
title = BaoziBanner.PREF_TITLE
summary = BaoziBanner.PREF_SUMMARY
entries = BaoziBanner.PREF_ENTRIES
entryValues = BaoziBanner.PREF_VALUES
setDefaultValue(DEFAULT_LEVEL)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putString(MIRROR_PREF, newValue as String).commit() bannerInterceptor.level = (newValue as String).toInt()
true
} }
} }.let { screen.addPreference(it) }
screen.addPreference(mirrorPref)
} }
companion object { companion object {
@ -318,16 +213,12 @@ class Baozimanhua : ParsedHttpSource(), ConfigurableSource {
private const val MIRROR_PREF = "MIRROR" private const val MIRROR_PREF = "MIRROR"
private const val MIRROR_PREF_TITLE = "使用镜像网址" private const val MIRROR_PREF_TITLE = "使用镜像网址"
private const val MIRROR_PREF_SUMMARY = "使用镜像网址。重启软件生效。" private const val MIRROR_PREF_SUMMARY = "重启生效,已选择:%s"
private val MIRRORS = arrayOf("cn.baozimh.com", "cn.webmota.com") private val MIRRORS = arrayOf("cn.baozimh.com", "cn.webmota.com")
private val DATE_FORMAT = SimpleDateFormat("yyyy年MM月dd日", Locale.ENGLISH) private const val DEFAULT_LEVEL = BaoziBanner.NORMAL.toString()
private val isNewDateLogic = run {
val commitCount = AppInfo.getVersionName().substringAfter('-', "") private val DATE_FORMAT by lazy { SimpleDateFormat("yyyy年MM月dd日", Locale.ENGLISH) }
if (commitCount.isNotEmpty()) // Preview private val isNewDateLogic = AppInfo.getVersionCode() >= 81
commitCount.toInt() >= 4442
else // Stable
AppInfo.getVersionCode() >= 81
}
} }
} }

View File

@ -0,0 +1,126 @@
package eu.kanade.tachiyomi.extension.zh.baozimanhua
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters() = FilterList(
Filter.Header("注意:不影響按標題搜索"),
TagFilter(),
RegionFilter(),
StatusFilter(),
StartFilter()
)
open class UriPartFilter(name: String, private val query: String, private val vals: Array<Pair<String, String>>) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = "$query=${vals[state].second}"
}
private class TagFilter : UriPartFilter(
"标签",
"type",
arrayOf(
Pair("全部", "all"),
Pair("都市", "dushi"),
Pair("冒险", "mouxian"),
Pair("热血", "rexie"),
Pair("恋爱", "lianai"),
Pair("耽美", "danmei"),
Pair("武侠", "wuxia"),
Pair("格斗", "gedou"),
Pair("科幻", "kehuan"),
Pair("魔幻", "mohuan"),
Pair("推理", "tuili"),
Pair("玄幻", "xuanhuan"),
Pair("日常", "richang"),
Pair("生活", "shenghuo"),
Pair("搞笑", "gaoxiao"),
Pair("校园", "xiaoyuan"),
Pair("奇幻", "qihuan"),
Pair("萌系", "mengxi"),
Pair("穿越", "chuanyue"),
Pair("后宫", "hougong"),
Pair("战争", "zhanzheng"),
Pair("历史", "lishi"),
Pair("剧情", "juqing"),
Pair("同人", "tongren"),
Pair("竞技", "jingji"),
Pair("励志", "lizhi"),
Pair("治愈", "zhiyu"),
Pair("机甲", "jijia"),
Pair("纯爱", "chunai"),
Pair("美食", "meishi"),
Pair("恶搞", "egao"),
Pair("虐心", "nuexin"),
Pair("动作", "dongzuo"),
Pair("惊险", "liangxian"),
Pair("唯美", "weimei"),
Pair("复仇", "fuchou"),
Pair("脑洞", "naodong"),
Pair("宫斗", "gongdou"),
Pair("运动", "yundong"),
Pair("灵异", "lingyi"),
Pair("古风", "gufeng"),
Pair("权谋", "quanmou"),
Pair("节操", "jiecao"),
Pair("明星", "mingxing"),
Pair("暗黑", "anhei"),
Pair("社会", "shehui"),
Pair("音乐舞蹈", "yinlewudao"),
Pair("东方", "dongfang"),
Pair("AA", "aa"),
Pair("悬疑", "xuanyi"),
Pair("轻小说", "qingxiaoshuo"),
Pair("霸总", "bazong"),
Pair("萝莉", "luoli"),
Pair("战斗", "zhandou"),
Pair("惊悚", "liangsong"),
Pair("百合", "yuri"),
Pair("大女主", "danuzhu"),
Pair("幻想", "huanxiang"),
Pair("少女", "shaonu"),
Pair("少年", "shaonian"),
Pair("性转", "xingzhuanhuan"),
Pair("重生", "zhongsheng"),
Pair("韩漫", "hanman"),
Pair("其它", "qita")
)
)
private class RegionFilter : UriPartFilter(
"地区",
"region",
arrayOf(
Pair("全部", "all"),
Pair("国漫", "cn"),
Pair("日本", "jp"),
Pair("韩国", "kr"),
Pair("欧美", "en")
)
)
private class StatusFilter : UriPartFilter(
"进度",
"state",
arrayOf(
Pair("全部", "all"),
Pair("连载中", "serial"),
Pair("已完结", "pub")
)
)
private class StartFilter : UriPartFilter(
"标题开头",
"filter",
arrayOf(
Pair("全部", "*"),
Pair("ABCD", "ABCD"),
Pair("EFGH", "EFGH"),
Pair("IJKL", "IJKL"),
Pair("MNOP", "MNOP"),
Pair("QRST", "QRST"),
Pair("UVW", "UVW"),
Pair("XYZ", "XYZ"),
Pair("0-9", "0-9")
)
)

View File

@ -7,7 +7,7 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import kotlin.system.exitProcess import kotlin.system.exitProcess
class BaozimanhuaUrlActivity : Activity() { class BaoziUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
@ -19,7 +19,7 @@ class BaozimanhuaUrlActivity : Activity() {
} }
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH" action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Baozimanhua.ID_SEARCH_PREFIX}$id") putExtra("query", "${Baozi.ID_SEARCH_PREFIX}$id")
putExtra("filter", packageName) putExtra("filter", packageName)
} }