Add 6Manhua (#12413)
* Add 6Manhua * extract unpacker library * point library version to SHA
This commit is contained in:
parent
ca282cfe27
commit
4164917567
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -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 |
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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-",
|
||||||
|
)
|
Loading…
Reference in New Issue