MCCMS: update sources (#15843)

* MCCMS: update sources

* Change cover placeholder

* Refactor
This commit is contained in:
stevenyomi 2023-03-29 06:13:08 +08:00 committed by GitHub
parent 84e849aaca
commit 7b8233ebe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 299 additions and 97 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.haoman6
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSWeb
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
class Haoman6 : MCCMSWeb("好漫6", "https://www.haoman6.com") {
override fun SManga.cleanup() = apply {
description = description?.substringBefore(title)
title = title.removeSuffix("(最新在线)").removeSuffix("-")
}
override fun pageListRequest(chapter: SChapter) =
GET(baseUrl + chapter.url, headers)
override val lazyLoadImageAttr = "mob-ec"
}

View File

@ -1,7 +1,3 @@
package eu.kanade.tachiyomi.extension.zh.haoman8
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
class Haoman8 : MCCMS("好漫8", "https://caiji.haoman8.com", hasCategoryPage = true) {
override val lazyLoadImageAttr = "data-original"
}
class Haoman8 : MCCMSAcgn("好漫8", "https://www.haoman8.com", hasCategoryPage = false)

View File

@ -0,0 +1,117 @@
package eu.kanade.tachiyomi.extension.zh.haoman8
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSWeb
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.select.Evaluator
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
// TODO: Category page
open class MCCMSAcgn(
name: String,
baseUrl: String,
lang: String = "zh",
hasCategoryPage: Boolean = true,
) : MCCMSWeb(name, baseUrl, lang, hasCategoryPage) {
override fun parseListing(document: Document): MangasPage {
if (document.location().contains("search")) {
return searchMangaParse(document)
}
val list = document.selectFirst(Evaluator.Class("acgn-comic-list"))
?: return MangasPage(emptyList(), false)
val mangas = list.children().map {
SManga.create().apply {
val titleElement = it.selectFirst(Evaluator.Tag("h3"))!!.child(0)
url = titleElement.attr("href")
title = titleElement.ownText()
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!
.attr("style").split("'")[1]
}.cleanup()
}
val hasNextPage = run { // default pagination
val pagination = document.selectFirst(Evaluator.Class("acgn-pages"))!!
pagination.children().last()!!.tagName() == "a"
}
return MangasPage(mangas, hasNextPage)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val request = super.searchMangaRequest(page, query, filters)
return if (query.isNotBlank()) {
// TODO: Fix Captcha
throw Exception("暂不支持搜索,请等待后续插件更新")
// request.newBuilder().headers(headers).build()
} else {
request
}
}
private fun searchMangaParse(document: Document): MangasPage {
val entries = document.select(Evaluator.Class("comic-list-item")).map {
SManga.create().apply {
val titleElement = it.selectFirst(Evaluator.Class("comic-name"))!!.child(0)
url = titleElement.attr("href")
title = titleElement.ownText()
author = it.selectFirst(Evaluator.Class("comic-author"))?.ownText()
genre = it.selectFirst(Evaluator.Class("comic-tags"))?.run {
children().joinToString { it.ownText() }
}
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("src")
}.cleanup()
}
return MangasPage(entries, false)
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup().selectFirst(Evaluator.Class("acgn-model-detail-frontcover"))!!
title = document.selectFirst(Evaluator.Tag("h1"))!!.ownText()
description = document.selectFirst(Evaluator.Class("desc-content"))?.ownText()
genre = document.select("ul.tags > a[href]").joinToString { it.ownText() }
thumbnail_url = document.selectFirst(Evaluator.Tag("img"))?.attr("src")
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val scripts = document.select(Evaluator.Tag("script"))
val js = scripts[scripts.size - 2].data()
val start = js.indexOf('[')
val end = js.lastIndexOf(']') + 1
val replaced = js.substring(start, end).replace('\'', '"')
val list: List<AcgnChapter> = json.decodeFromString(replaced)
val dateFormat = dateFormat
return list.asReversed().map { it.toSChapter(dateFormat) }
}
override val lazyLoadImageAttr get() = "data-echo"
@Serializable
class AcgnChapter(
private val name: String,
private val url: String,
private val time: String,
) {
fun toSChapter(dateFormat: SimpleDateFormat) = SChapter.create().apply {
url = this@AcgnChapter.url
name = this@AcgnChapter.name
date_upload = dateFormat.parse(time)?.time ?: 0
}
}
private val json: Json by injectLazy()
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
}

View File

@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.extension.zh.kuaikuai3
class Kuaikuai3 : MCCMSReduced("快快漫画3", "https://mobile3.manhuaorg.com") {
override fun headersBuilder() = super.headersBuilder()
.set("User-Agent", "okhttp/3.14.7")
}

View File

@ -0,0 +1,105 @@
package eu.kanade.tachiyomi.extension.zh.kuaikuai3
import eu.kanade.tachiyomi.multisrc.mccms.DecryptInterceptor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.select.Evaluator
open class MCCMSReduced(
override val name: String,
override val baseUrl: String,
) : HttpSource() {
override val lang = "zh"
override val supportsLatest get() = false
override val client by lazy {
network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.addInterceptor(DecryptInterceptor)
.build()
}
private fun searchOnly(): Nothing = throw Exception("此图源只支持搜索")
private val noWebView = "https://stevenyomi.github.io/echo#本图源不支持网页查看"
override fun popularMangaRequest(page: Int) = searchOnly()
override fun popularMangaParse(response: Response) = searchOnly()
override fun latestUpdatesRequest(page: Int) = searchOnly()
override fun latestUpdatesParse(response: Response) = searchOnly()
override fun getMangaUrl(manga: SManga) = noWebView
override fun getChapterUrl(chapter: SChapter) = noWebView
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 searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val placeholder = "$baseUrl/template/pc/default/images/bg_loadimg_3x4.png"
val entries = document.select(Evaluator.Tag("a")).map { link ->
SManga.create().apply {
url = link.attr("href")
title = link.ownText()
thumbnail_url = placeholder
}
}
return MangasPage(entries, false)
}
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
val metaProperties = HashMap<String, String>()
for (element in document.head().children()) {
if (element.tagName() == "meta" && element.hasAttr("property")) {
val key = element.attr("property").removePrefix("og:")
metaProperties[key] = element.attr("content")
}
}
return SManga.create().apply {
title = metaProperties["title"]!!
author = metaProperties["novel:author"]
description = metaProperties["description"]
val statusText = metaProperties["novel:status"]
status = when {
statusText == null -> SManga.UNKNOWN
'连' in statusText -> SManga.ONGOING
'完' in statusText -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
thumbnail_url = metaProperties["image"]
}
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select(Evaluator.Class("j-chapter-link")).asReversed().map { link ->
SChapter.create().apply {
url = link.attr("href")
name = link.ownText()
}
}
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select(Evaluator.Tag("img")).mapIndexed { index, img ->
Page(index, imageUrl = img.attr("data-original"))
}
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.extension.zh.manhuawu
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
import eu.kanade.tachiyomi.multisrc.mccms.MangaDto
class Manhuawu : MCCMS("漫画屋", "https://www.mhua5.com", hasCategoryPage = true) {
override fun MangaDto.prepare() = copy(url = "/comic-$id.html")
override fun getMangaId(url: String) = url.substringAfterLast('-').substringBeforeLast('.')
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.mccms
import android.util.Base64
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
@ -12,25 +13,38 @@ object DecryptInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val key = when (request.url.topPrivateDomain()) {
"bcebos.com" -> key1
null -> key2
val host = request.url.host
val type = when {
host.endsWith("bcebos.com") -> 1
host.endsWith("mhrsrc.com") -> 2
else -> return response
}
val data = decrypt(response.body.bytes(), key)
val data = decrypt(response.body.bytes(), type)
val body = data.toResponseBody("image/jpeg".toMediaType())
return response.newBuilder().body(body).build()
}
@Synchronized
private fun decrypt(input: ByteArray, key: SecretKeySpec): ByteArray {
private fun decrypt(input: ByteArray, type: Int): ByteArray {
val cipher = cipher
cipher.init(Cipher.DECRYPT_MODE, key, iv)
return cipher.doFinal(input)
val decodedInput: ByteArray
when (type) {
1 -> {
decodedInput = input
cipher.init(Cipher.DECRYPT_MODE, key1, iv)
}
2 -> {
decodedInput = Base64.decode(input, Base64.DEFAULT)
cipher.init(Cipher.DECRYPT_MODE, key2, iv2)
}
else -> return input
}
return cipher.doFinal(decodedInput)
}
private val cipher by lazy(LazyThreadSafetyMode.NONE) { Cipher.getInstance("DESede/CBC/PKCS5Padding") }
private val key1 by lazy(LazyThreadSafetyMode.NONE) { SecretKeySpec("OW84U8Eerdb99rtsTXWSILDO".toByteArray(), "DESede") }
private val key2 by lazy(LazyThreadSafetyMode.NONE) { SecretKeySpec("OW84U8Eerdb99rtsTXWSILEC".toByteArray(), "DESede") }
private val key2 by lazy(LazyThreadSafetyMode.NONE) { SecretKeySpec("ys6n2GvmgEyB3rELDX1gaTBf".toByteArray(), "DESede") }
private val iv by lazy(LazyThreadSafetyMode.NONE) { IvParameterSpec("SK8bncVu".toByteArray()) }
private val iv2 by lazy(LazyThreadSafetyMode.NONE) { IvParameterSpec("2pnB3NI2".toByteArray()) }
}

View File

@ -18,7 +18,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.Single
import uy.kohesive.injekt.injectLazy
import kotlin.concurrent.thread
@ -49,13 +48,14 @@ open class MCCMS(
.add("Referer", baseUrl)
protected open fun SManga.cleanup(): SManga = this
protected open fun MangaDto.prepare(): MangaDto = this
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/api/data/comic?page=$page&size=$PAGE_SIZE&order=hits", headers)
override fun popularMangaParse(response: Response): MangasPage {
val list: List<MangaDto> = response.parseAs()
return MangasPage(list.map { it.toSManga().cleanup() }, list.size >= PAGE_SIZE)
return MangasPage(list.map { it.prepare().toSManga().cleanup() }, list.size >= PAGE_SIZE)
}
override fun latestUpdatesRequest(page: Int): Request =
@ -91,14 +91,14 @@ open class MCCMS(
.toString()
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val list: List<MangaDto> = response.parseAs()
val list = response.parseAs<List<MangaDto>>().map { it.prepare() }
list.find { it.url == manga.url }!!.toSManga().cleanup()
}
}
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException("Not used.")
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException("Not used.")
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Single.create<List<SChapter>> { subscriber ->
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
val id = getMangaId(manga.url)
val dataResponse = client.newCall(GET("$baseUrl/api/data/chapter?mid=$id", headers)).execute()
val dataList: List<ChapterDataDto> = dataResponse.parseAs() // unordered
@ -107,12 +107,12 @@ open class MCCMS(
val response = client.newCall(GET("$baseUrl/api/comic/chapter?mid=$id", headers)).execute()
val list: List<ChapterDto> = response.parseAs()
val result = list.map { it.toSChapter(date = dateMap[it.id.toInt()] ?: 0) }.asReversed()
subscriber.onSuccess(result)
}.toObservable()
result
}
protected open fun getMangaId(url: String) = url.substringAfterLast('/')
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Not used.")
override fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException("Not used.")
override fun pageListRequest(chapter: SChapter): Request =
GET(baseUrl + chapter.url, pcHeaders)

View File

@ -9,7 +9,8 @@ import java.util.Locale
internal const val PAGE_SIZE = 30
@Serializable
class MangaDto(
data class MangaDto(
val id: String,
private val name: String,
private val pic: String,
private val serialize: String,
@ -28,8 +29,8 @@ class MangaDto(
val date = dateFormat.parse(addtime)?.time ?: 0
val isUpdating = System.currentTimeMillis() - date <= 30L * 24 * 3600 * 1000 // a month
status = when {
serialize.startsWith('连') || isUpdating -> SManga.ONGOING
serialize.startsWith('完') -> SManga.COMPLETED
'连' in serialize || isUpdating -> SManga.ONGOING
'完' in serialize -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
thumbnail_url = pic

View File

@ -6,24 +6,8 @@ import generator.ThemeSourceGenerator
class MCCMSGenerator : ThemeSourceGenerator {
override val themeClass = "MCCMS"
override val themePkg = "mccms"
override val baseVersionCode = 5
override val baseVersionCode = 6
override val sources = listOf(
SingleLang(
name = "Haoman6",
baseUrl = "https://www.haoman6.com",
lang = "zh",
className = "Haoman6",
sourceName = "好漫6",
overrideVersionCode = 3,
),
SingleLang( // previously: app2.haoman6.com, app2.haomanwu.com
name = "Haomanwu",
baseUrl = "https://move.bookcomic.org",
lang = "zh",
className = "Haomanwu",
sourceName = "好漫屋",
overrideVersionCode = 3,
),
SingleLang( // same as: www.haoman6.cc
name = "Haoman6 (g-lens)",
baseUrl = "https://www.g-lens.com",
@ -32,58 +16,31 @@ class MCCMSGenerator : ThemeSourceGenerator {
sourceName = "好漫6 (g-lens)",
overrideVersionCode = 0,
),
SingleLang( // same as: www.haoman8.com
SingleLang( // same as: caiji.haoman8.com
name = "Haoman8",
baseUrl = "https://caiji.haoman8.com",
baseUrl = "https://www.haoman8.com",
lang = "zh",
className = "Haoman8",
sourceName = "好漫8",
overrideVersionCode = 0,
),
SingleLang(
name = "Kuaikuai Manhua",
baseUrl = "https://mobile.manhuaorg.com",
name = "Kuaikuai Manhua 3",
baseUrl = "https://mobile3.manhuaorg.com",
lang = "zh",
className = "Kuaikuai",
sourceName = "快快漫画",
className = "Kuaikuai3",
sourceName = "快快漫画3",
overrideVersionCode = 0,
),
SingleLang(
name = "bz Manhua",
baseUrl = "https://www2.pupumanhua.com",
name = "Manhuawu",
baseUrl = "https://www.mhua5.com",
lang = "zh",
className = "bzManhua",
sourceName = "包子漫画搬运",
className = "Manhuawu",
sourceName = "漫画屋",
overrideVersionCode = 0,
),
// The following sources are from https://www.yy123.cyou/ and are configured to use MCCMSNsfw
SingleLang( // 103=寄宿日记, same as: www.hanman.top (different URL format)
name = "Damao Manhua",
baseUrl = "https://www.hanman.cyou",
lang = "zh",
isNsfw = true,
className = "DamaoManhua",
sourceName = "大猫漫画",
overrideVersionCode = 0,
),
SingleLang( // 103=诡秘的姐妹
name = "Heihei Manhua",
baseUrl = "https://www.hhmh.cyou",
lang = "zh",
isNsfw = true,
className = "HHMH",
sourceName = "嘿嘿漫画",
overrideVersionCode = 0,
),
SingleLang( // 103=望月仙女傳說, same as: www.hanman.men
name = "Tudou Manhua",
baseUrl = "https://www.ptcomic.com",
lang = "zh",
isNsfw = true,
className = "PtComic",
sourceName = "土豆漫画",
overrideVersionCode = 0,
),
SingleLang( // 103=校园梦精记, same as: www.hmanwang.com, www.quanman8.com, www.lmmh.cc, www.xinmanba.com
name = "Dida Manhua",
baseUrl = "https://www.didamanhua.com",

View File

@ -21,7 +21,7 @@ open class MCCMSWeb(
hasCategoryPage: Boolean = true,
) : MCCMS(name, baseUrl, lang, hasCategoryPage) {
fun parseListing(document: Document): MangasPage {
protected open fun parseListing(document: Document): MangasPage {
val mangas = document.select(Evaluator.Class("common-comic-item")).map {
SManga.create().apply {
val titleElement = it.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
@ -84,6 +84,12 @@ open class MCCMSWeb(
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
if (manga.url == "/index.php/search") return Observable.just(manga)
return client.newCall(GET(baseUrl + manga.url, pcHeaders)).asObservableSuccess().map { response ->
mangaDetailsParse(response)
}
}
override fun mangaDetailsParse(response: Response): SManga {
return run {
SManga.create().apply {
val document = response.asJsoup().selectFirst(Evaluator.Class("de-info__box"))!!
title = document.selectFirst(Evaluator.Class("comic-title"))!!.ownText()
@ -98,6 +104,12 @@ open class MCCMSWeb(
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
if (manga.url == "/index.php/search") return Observable.just(emptyList())
return client.newCall(GET(baseUrl + manga.url, pcHeaders)).asObservableSuccess().map { response ->
chapterListParse(response)
}
}
override fun chapterListParse(response: Response): List<SChapter> {
return run {
response.asJsoup().selectFirst(Evaluator.Class("chapter__list-box"))!!.children().map {
val link = it.child(0)
SChapter.create().apply {