Add 6Manhua (#12413)

* Add 6Manhua

* extract unpacker library

* point library version to SHA
This commit is contained in:
stevenyomi 2022-07-03 11:04:46 +08:00 committed by GitHub
parent ca282cfe27
commit 4164917567
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 275 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

16
src/zh/sixmh/build.gradle Normal file
View File

@ -0,0 +1,16 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = '6Manhua'
pkgNameSuffix = 'zh.sixmh'
extClass = '.SixMH'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation 'com.github.stevenyomi:unpacker:919be5cb30' // SHA of 1.0 tag
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1,58 @@
package eu.kanade.tachiyomi.extension.zh.sixmh
import android.os.SystemClock
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import java.util.concurrent.TimeUnit
// See https://github.com/tachiyomiorg/tachiyomi/pull/7389
internal class NonblockingRateLimiter(
private val permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) : Interceptor {
private val requestQueue = ArrayList<Long>(permits)
private val rateLimitMillis = unit.toMillis(period)
override fun intercept(chain: Interceptor.Chain): Response {
// Ignore canceled calls, otherwise they would jam the queue
if (chain.call().isCanceled()) {
throw IOException()
}
synchronized(requestQueue) {
val now = SystemClock.elapsedRealtime()
val waitTime = if (requestQueue.size < permits) {
0
} else {
val oldestReq = requestQueue[0]
val newestReq = requestQueue[permits - 1]
if (newestReq - oldestReq > rateLimitMillis) {
0
} else {
oldestReq + rateLimitMillis - now // Remaining time
}
}
// Final check
if (chain.call().isCanceled()) {
throw IOException()
}
if (requestQueue.size == permits) {
requestQueue.removeAt(0)
}
if (waitTime > 0) {
requestQueue.add(now + waitTime)
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
} else {
requestQueue.add(now)
}
}
return chain.proceed(chain.request())
}
}

View File

@ -0,0 +1,170 @@
package eu.kanade.tachiyomi.extension.zh.sixmh
import com.github.stevenyomi.unpacker.Unpacker
import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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 eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable
import rx.Single
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class SixMH : ParsedHttpSource() {
override val name = "6漫画"
override val lang = "zh"
override val baseUrl = PC_URL
override val supportsLatest = true
private val json: Json by injectLazy()
override val client = network.client.newBuilder()
.addInterceptor(NonblockingRateLimiter(2))
.build()
override fun popularMangaRequest(page: Int) = GET("$PC_URL/rank/1-$page.html", headers)
override fun popularMangaNextPageSelector() = "li.thisclass:not(:last-of-type)"
override fun popularMangaSelector() = "div.cy_list_mh > ul"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
with(element.child(1).child(0)) {
url = attr("href")
title = ownText()
}
thumbnail_url = element.selectFirst(Evaluator.Tag("img")).attr("src")
}
override fun latestUpdatesRequest(page: Int) = GET("$PC_URL/rank/5-$page.html", headers)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotBlank()) {
return GET("$PC_URL/search.php?keyword=$query", headers)
} else {
filters.filterIsInstance<PageFilter>().firstOrNull()?.run {
return GET("$PC_URL$path$page.html", headers)
}
return popularMangaRequest(page)
}
}
private fun pcRequest(manga: SManga) = GET("$PC_URL${manga.url}", headers)
private fun mobileRequest(manga: SManga) = GET("$MOBILE_URL${manga.url}", headers)
// for WebView
override fun mangaDetailsRequest(manga: SManga) = mobileRequest(manga)
override fun mangaDetailsParse(document: Document) = throw UnsupportedOperationException("Not used.")
// fetchMangaDetails fetches and parses PC page first, then mobile page
// fetchChapterList does in the opposite order, to make use of transparent cache
// in this way, the latter requests will be responded with 304 Not Modified (in most cases)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Single.create<SManga> {
val document = client.newCall(pcRequest(manga)).execute().asJsoup()
val result = SManga.create().apply {
val box = document.selectFirst(Evaluator.Id("intro_l"))
val details = box.getElementsByTag("span")
author = details[0].text().removePrefix("作者:")
status = when (details[1].child(0).ownText()) {
"连载中" -> SManga.ONGOING
"已完结" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
genre = buildList {
add(details[2].ownText().removePrefix("类别:"))
details[3].ownText().removePrefix("标签:").split(Regex("[ -~]+"))
.filterTo(this) { it.isNotEmpty() }
}.joinToString()
description = box.selectFirst(Evaluator.Tag("p")).ownText()
thumbnail_url = box.selectFirst(Evaluator.Tag("img")).attr("src")
}
val mobileDocument = client.newCall(mobileRequest(manga)).execute().asJsoup()
val details = mobileDocument.selectFirst(Evaluator.Class("author"))
.ownText().trim().split(Regex(""" +"""))
if (details.size >= 3) {
result.description = details[2] + '\n' + result.description
}
it.onSuccess(result)
}.toObservable()
override fun chapterListSelector() = throw UnsupportedOperationException("Not used.")
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Not used.")
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Single.create<List<SChapter>> {
val document = client.newCall(mobileRequest(manga)).execute().asJsoup()
val list = mutableListOf<SChapter>()
document.select(".chapter-list > a, dd[class^=gengduo]").forEach { element ->
if (element.tagName() == "a") {
val chapter = SChapter.create().apply {
url = element.attr("href")
name = element.text()
}
list.add(chapter)
} else {
val path = manga.url
val body = FormBody.Builder().apply {
addEncoded("id", element.attr("data-id"))
addEncoded("id2", element.attr("data-vid"))
}.build()
client.newCall(POST("$MOBILE_URL/bookchapter/", headers, body)).execute()
.parseAs<List<ChapterDto>>().mapTo(list) { it.toSChapter(path) }
}
}
if (isNewDateLogic && list.isNotEmpty()) {
val pcDocument = client.newCall(pcRequest(manga)).execute().asJsoup()
pcDocument.selectFirst(".cy_zhangjie_top font")?.run {
list[0].date_upload = dateFormat.parse(ownText())?.time ?: 0
}
}
it.onSuccess(list)
}.toObservable()
override fun pageListRequest(chapter: SChapter) = GET("$MOBILE_URL${chapter.url}", headers)
override fun pageListParse(response: Response): List<Page> {
val result = Unpacker.unpack(response.body!!.string(), "[", "]")
.ifEmpty { return emptyList() }
.replace("\\", "")
.removeSurrounding("\"").split("\",\"")
return result.mapIndexed { i, url -> Page(i, imageUrl = url) }
}
override fun pageListParse(document: Document) = throw UnsupportedOperationException("Not used.")
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used.")
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(body!!.byteStream())
}
override fun getFilterList() = FilterList(listOf(PageFilter()))
companion object {
// redirect URL: http://www.6mh9.com/
private const val DOMAIN = "sixmh7.com"
private const val PC_URL = "http://www.$DOMAIN"
private const val MOBILE_URL = "http://m.$DOMAIN"
private val isNewDateLogic = AppInfo.getVersionCode() >= 81
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
}
}

View File

@ -0,0 +1,29 @@
package eu.kanade.tachiyomi.extension.zh.sixmh
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.SChapter
import kotlinx.serialization.Serializable
@Serializable
class ChapterDto(val chapterid: String, val chaptername: String) {
fun toSChapter(path: String) = SChapter.create().apply {
url = "$path$chapterid.html"
name = chaptername
}
}
internal class PageFilter : Filter.Select<String>("排行榜/分类", PAGE_NAMES) {
val path get() = PAGE_PATHS[state]
}
private val PAGE_NAMES = arrayOf(
"人气榜", "周读榜", "月读榜", "火爆榜", "更新榜", "新漫榜",
"冒险热血", "武侠格斗", "科幻魔幻", "侦探推理", "耽美爱情", "生活漫画",
"推荐漫画", "完结漫画", "连载漫画",
)
private val PAGE_PATHS = arrayOf(
"/rank/1-", "/rank/2-", "/rank/3-", "/rank/4-", "/rank/5-", "/rank/6-",
"/sort/1-", "/sort/2-", "/sort/3-", "/sort/4-", "/sort/5-", "/sort/6-",
"/sort/11-", "/sort/12-", "/sort/13-",
)