From 2edb3b61646953ece37876b5ea9b1b642f6d1612 Mon Sep 17 00:00:00 2001 From: Senxian Z <9044823+flashsphere@users.noreply.github.com> Date: Fri, 25 Apr 2025 01:10:48 +0800 Subject: [PATCH] Update 6Manhua parser (#8539) * Update 6Manhua parser * Code review changes * Code review changes * Code review changes --- src/zh/sixmh/build.gradle | 4 +- .../tachiyomi/extension/zh/sixmh/Crypto.kt | 30 +++--- .../extension/zh/sixmh/{Image.kt => Data.kt} | 4 +- .../zh/sixmh/SimpleParsedHttpSource.kt | 27 ++++++ .../tachiyomi/extension/zh/sixmh/SixMH.kt | 96 +++++++++++++------ 5 files changed, 118 insertions(+), 43 deletions(-) rename src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/{Image.kt => Data.kt} (67%) create mode 100644 src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SimpleParsedHttpSource.kt diff --git a/src/zh/sixmh/build.gradle b/src/zh/sixmh/build.gradle index e4b4bc861..5c517bb89 100644 --- a/src/zh/sixmh/build.gradle +++ b/src/zh/sixmh/build.gradle @@ -1,9 +1,7 @@ ext { extName = '6Manhua' extClass = '.SixMH' - themePkg = 'mccms' - baseUrl = 'https://www.liumanhua.com' - overrideVersionCode = 5 + extVersionCode = 13 isNsfw = true } diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Crypto.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Crypto.kt index 9f9a1092a..3187c7af0 100644 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Crypto.kt +++ b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Crypto.kt @@ -1,18 +1,24 @@ package eu.kanade.tachiyomi.extension.zh.sixmh import android.util.Base64 -import kotlin.experimental.xor +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec -private val keys = arrayOf("Ni1iWGQ5aU4=", "Ni1SWHlqcnk=", "Ni1vWXZ3Vnk=", "Ni00Wlk1N1U=", "Ni1tYkpwVTc=", "Ni02TU0yRWk=", "Ni01NFRpUXI=", "Ni1QaDV4eDk=", "Ni1iWWdlUFI=", "Ni1aOUEzYlc=") +internal fun decodeData(encodedData: String): String { + // Key derived from https://www.liumanhua.com/template/pc/liumanhua/js/index-v2.js line 571 + // excerpt: + // var _0x493e85 = CryptoJS[_0x5b6369(0x15c,'a#uL')]['Utf8'][_0x5b6369(0x147,'H0qZ')](_0x5b6369(0x152,')X69')); + // _0x5b6369(0x152,')X69') provides the key + val aesKey = "9S8\$vJnU2ANeSRoF" + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + val secretKeySpec = SecretKeySpec(aesKey.toByteArray(), "AES") -internal fun decodeData(encodedData: String, cid: Int): String { - val key = Base64.decode(keys[cid % keys.size], Base64.DEFAULT) - val keyLength = key.size - val decodedData = Base64.decode(encodedData, Base64.DEFAULT) - val decryptedData = StringBuilder() - for (i in decodedData.indices) { - val decryptedCharCode = decodedData[i] xor key[i % keyLength] - decryptedData.appendCodePoint(decryptedCharCode.toInt()) - } - return Base64.decode(decryptedData.toString(), Base64.DEFAULT).decodeToString() + val decodedBase64 = Base64.decode(encodedData, Base64.DEFAULT) + val iv = IvParameterSpec(decodedBase64.sliceArray(0 until 16)) + val cipherText = decodedBase64.sliceArray(16 until decodedBase64.size) + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv) + val decryptedText = cipher.doFinal(cipherText) + + return decryptedText.toString(Charsets.UTF_8) } diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Image.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Data.kt similarity index 67% rename from src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Image.kt rename to src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Data.kt index 967d8ebe7..60091e4ae 100644 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Image.kt +++ b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Data.kt @@ -3,4 +3,6 @@ package eu.kanade.tachiyomi.extension.zh.sixmh import kotlinx.serialization.Serializable @Serializable -data class Image(val id: String, val url: String) +data class Data( + val images: List, +) diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SimpleParsedHttpSource.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SimpleParsedHttpSource.kt new file mode 100644 index 000000000..2e93443ab --- /dev/null +++ b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SimpleParsedHttpSource.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.extension.zh.sixmh + +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +abstract class SimpleParsedHttpSource : ParsedHttpSource() { + + abstract fun simpleMangaSelector(): String + abstract fun simpleMangaFromElement(element: Element): SManga + abstract fun simpleNextPageSelector(): String? + + override fun popularMangaSelector() = simpleMangaSelector() + override fun popularMangaFromElement(element: Element) = simpleMangaFromElement(element) + override fun popularMangaNextPageSelector() = simpleNextPageSelector() + + override fun latestUpdatesSelector() = simpleMangaSelector() + override fun latestUpdatesFromElement(element: Element) = simpleMangaFromElement(element) + override fun latestUpdatesNextPageSelector() = simpleNextPageSelector() + + override fun searchMangaSelector() = simpleMangaSelector() + override fun searchMangaFromElement(element: Element) = simpleMangaFromElement(element) + override fun searchMangaNextPageSelector() = simpleNextPageSelector() + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() +} diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMH.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMH.kt index 70b669ae5..f3d524fcc 100644 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMH.kt +++ b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMH.kt @@ -1,38 +1,80 @@ package eu.kanade.tachiyomi.extension.zh.sixmh -import android.app.Application -import android.os.Build -import eu.kanade.tachiyomi.multisrc.mccms.MCCMS +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json +import keiyoushi.utils.parseAs +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request import okhttp3.Response -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element -class SixMH : MCCMS("六漫画", "https://www.liumanhua.com") { - private val dataRegex = Regex("var DATA = '([A-Za-z0-9+/=]+)'") - private val json by injectLazy() - override val versionId get() = 2 +class SixMH : SimpleParsedHttpSource() { + private val paramsRegex = Regex("params = '([A-Za-z0-9+/=]+)'") + override val versionId get() = 3 + override val name: String = "六漫画" + override val lang: String = "zh" + override val supportsLatest: Boolean = true + override val baseUrl: String = "https://www.liumanhua.com" - init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - // Delete old preferences for "6漫画/zh/1" - Injekt.get().deleteSharedPreferences("source_7259486566651312186") + override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/order/hits/page/$page", headers) + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/category/order/addtime/page/$page", headers) + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/index.php/search".toHttpUrl().newBuilder() + .addQueryParameter("key", query) + .build() + return GET(url, headers) + } + + override fun simpleNextPageSelector(): String? = null + override fun simpleMangaSelector(): String = "div.cy_list_mh ul" + override fun simpleMangaFromElement(element: Element): SManga = SManga.create().apply { + title = element.selectFirst("li.title > a")!!.text() + setUrlWithoutDomain(element.selectFirst("li.title > a")!!.absUrl("href")) + thumbnail_url = element.selectFirst("img")?.absUrl("src") + } + + // Details + override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + val element = document.selectFirst("div.cy_info")!! + title = element.selectFirst("div.cy_title")!!.text() + thumbnail_url = element.selectFirst("div.cy_info_cover > a > img.pic")?.absUrl("src") + description = element.selectFirst("div.cy_desc #comic-description")?.text() + + val infoElements = element.select("div.cy_xinxi") + author = infoElements[0].selectFirst("span:first-child > a")?.text() + status = parseStatus(infoElements[0].selectFirst("span:nth-child(2)")?.text()) + genre = infoElements[1].selectFirst("span:first-child > a")?.text() + } + + // Chapters + override fun chapterListSelector(): String = "ul#mh-chapter-list-ol-0 li.chapter__item" + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) + name = element.selectFirst("a > p")!!.text() + } + + // Pages + override fun pageListParse(response: Response): List { + val body = response.body.string() + val encodedData = paramsRegex.find(body)?.groupValues?.get(1) ?: "" + val decodedData = decodeData(encodedData) + + val images = decodedData.parseAs().images + return images.mapIndexed { index, url -> Page(index, imageUrl = url) } + } + + override fun pageListParse(document: Document): List = throw UnsupportedOperationException() + + private fun parseStatus(status: String?): Int { + return when { + status == null -> SManga.UNKNOWN + status.contains("连载") -> SManga.ONGOING + status.contains("完结") -> SManga.COMPLETED + else -> SManga.UNKNOWN } } - - override fun getMangaUrl(manga: SManga) = "https://m.liumanhua.com" + manga.url - override fun getChapterUrl(chapter: SChapter) = "https://m.liumanhua.com" + chapter.url - - override fun pageListParse(response: Response): List { - val encodedData = dataRegex.find(response.body.string())?.groupValues?.get(1) ?: "" - val cid = response.request.url.pathSegments.last().removeSuffix(".html").toIntOrNull() ?: 0 - val decodedData = decodeData(encodedData, cid) - val images = json.decodeFromString>(decodedData) - return images.mapIndexed { index, image -> Page(index, imageUrl = image.url) } - } }