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