Update MCCMS sources (#9631)

* Add back Manhuawu, closes #1567

* Clean up 6Manhua

* Add Miaoqu Manhua, closes #4482

* Add 2 French sources
This commit is contained in:
stevenyomi 2025-07-14 08:47:55 +00:00 committed by Draff
parent 42d0c589d6
commit 87cd9dc9fb
Signed by: Draff
GPG Key ID: E8A89F3211677653
28 changed files with 371 additions and 118 deletions

View File

@ -0,0 +1,77 @@
package eu.kanade.tachiyomi.multisrc.mccms
object Intl {
var lang = "zh"
val sort
get() = when (lang) {
"zh" -> "排序"
else -> "Sort by"
}
val popular
get() = when (lang) {
"zh" -> "热门人气"
else -> "Popular"
}
val latest
get() = when (lang) {
"zh" -> "更新时间"
else -> "Latest"
}
val score
get() = when (lang) {
"zh" -> "评分"
else -> "Score"
}
val status
get() = when (lang) {
"zh" -> "进度"
else -> "Status"
}
val all
get() = when (lang) {
"zh" -> "全部"
else -> "All"
}
val ongoing
get() = when (lang) {
"zh" -> "连载"
else -> "Ongoing"
}
val completed
get() = when (lang) {
"zh" -> "完结"
else -> "Completed"
}
val genreWeb
get() = when (lang) {
"zh" -> "标签"
else -> "Genre"
}
val genreApi
get() = when (lang) {
"zh" -> "标签(搜索文本时无效)"
else -> "Genre (ignored for text search)"
}
val categoryWeb
get() = when (lang) {
"zh" -> "分类筛选(搜索时无效)"
else -> "Category filters (ignored for text search)"
}
val tapReset
get() = when (lang) {
"zh" -> "点击“重置”尝试刷新标签分类"
else -> "Tap 'Reset' to load genres"
}
}

View File

@ -9,15 +9,13 @@ 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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import keiyoushi.utils.parseAs as parseAsRaw
/**
* 漫城CMS http://mccms.cn/
@ -25,16 +23,26 @@ import java.net.URLEncoder
open class MCCMS(
override val name: String,
override val baseUrl: String,
override val lang: String = "zh",
final override val lang: String = "zh",
private val config: MCCMSConfig = MCCMSConfig(),
) : HttpSource() {
override val supportsLatest = true
override val supportsLatest get() = true
private val json: Json by injectLazy()
init {
Intl.lang = lang
}
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.addInterceptor { chain -> // for thumbnail requests
var request = chain.request()
val referer = request.header("Referer")
if (referer != null && !request.url.toString().startsWith(referer)) {
request = request.newBuilder().removeHeader("Referer").build()
}
chain.proceed(request)
}
.build()
}
@ -42,12 +50,14 @@ open class MCCMS(
.add("User-Agent", System.getProperty("http.agent")!!)
.add("Referer", baseUrl)
protected open fun SManga.cleanup(): SManga = 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() }, list.size >= PAGE_SIZE)
return MangasPage(list.map { it.toSManga().cleanup() }, list.size >= PAGE_SIZE)
}
override fun latestUpdatesRequest(page: Int): Request =
@ -86,7 +96,7 @@ open class MCCMS(
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val list = response.parseAs<List<MangaDto>>()
list.first { it.cleanUrl == mangaUrl }.toSManga()
list.first { it.cleanUrl == mangaUrl }.toSManga().cleanup()
}
}
@ -120,9 +130,7 @@ open class MCCMS(
// Don't send referer
override fun imageRequest(page: Page) = GET(page.imageUrl!!, pcHeaders)
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream<ResultDto<T>>(it.body.byteStream()).data
}
private inline fun <reified T> Response.parseAs(): T = parseAsRaw<ResultDto<T>>().data
override fun getFilterList(): FilterList {
val genreData = config.genreData.also { it.fetchGenres(this) }

View File

@ -12,6 +12,8 @@ val pcHeaders = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; W
fun String.removePathPrefix() = removePrefix("/index.php")
fun String.mobileUrl() = replace("//www.", "//m.")
open class MCCMSConfig(
hasCategoryPage: Boolean = true,
val textSearchOnlyPageOne: Boolean = false,

View File

@ -26,11 +26,11 @@ data class MangaDto(
title = Entities.unescape(name)
author = Entities.unescape(this@MangaDto.author)
description = Entities.unescape(content)
genre = tags.joinToString()
status = when {
'连' in serialize || isUpdating(addtime) -> SManga.ONGOING
'完' in serialize -> SManga.COMPLETED
else -> SManga.UNKNOWN
genre = Entities.unescape(tags.joinToString())
status = when (serialize) {
"连载", "連載中", "En cours", "OnGoing" -> SManga.ONGOING
"完结", "已完結", "Terminé", "Complete", "Complété" -> SManga.COMPLETED
else -> if (isUpdating(addtime)) SManga.ONGOING else SManga.UNKNOWN
}
thumbnail_url = "$pic#$id"
initialized = true

View File

@ -18,32 +18,31 @@ open class MCCMSFilter(
val query get() = queries[state]
}
class SortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES)
class WebSortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES_WEB)
class SortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES)
class WebSortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES_WEB)
private val SORT_NAMES = arrayOf("热门人气", "更新时间", "评分")
private val SORT_QUERIES = arrayOf("order=hits", "order=addtime", "order=score")
private val SORT_QUERIES_WEB = arrayOf("order/hits", "order/addtime", "order/score")
private val SORT_NAMES get() = arrayOf(Intl.popular, Intl.latest, Intl.score)
private val SORT_QUERIES get() = arrayOf("order=hits", "order=addtime", "order=score")
private val SORT_QUERIES_WEB get() = arrayOf("order/hits", "order/addtime", "order/score")
class StatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES)
class WebStatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES_WEB)
class StatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES)
class WebStatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES_WEB)
private val STATUS_NAMES = arrayOf("全部", "连载", "完结")
private val STATUS_QUERIES = arrayOf("", "serialize=连载", "serialize=完结")
private val STATUS_QUERIES_WEB = arrayOf("", "finish/1", "finish/2")
private val STATUS_NAMES get() = arrayOf(Intl.all, Intl.ongoing, Intl.completed)
private val STATUS_QUERIES get() = arrayOf("", "serialize=连载", "serialize=完结")
private val STATUS_QUERIES_WEB get() = arrayOf("", "finish/1", "finish/2")
class GenreFilter(private val values: Array<String>, private val queries: Array<String>) {
private val apiQueries get() = queries.run {
Array(size) { i -> "type[tags]=" + this[i] }
Array(size) { i -> "type[tags]=" + this[i] }.apply { this[0] = "" }
}
private val webQueries get() = queries.run {
Array(size) { i -> "tags/" + this[i] }
Array(size) { i -> "tags/" + this[i] }.apply { this[0] = "" }
}
val filter get() = MCCMSFilter("标签(搜索文本时无效)", values, apiQueries, isTypeQuery = true)
val webFilter get() = MCCMSFilter("标签", values, webQueries, isTypeQuery = true)
val filter get() = MCCMSFilter(Intl.genreApi, values, apiQueries, isTypeQuery = true)
val webFilter get() = MCCMSFilter(Intl.genreWeb, values, webQueries, isTypeQuery = true)
}
class GenreData(hasCategoryPage: Boolean) {
@ -55,7 +54,12 @@ class GenreData(hasCategoryPage: Boolean) {
status = FETCHING
thread {
try {
val response = source.client.newCall(GET("${source.baseUrl}/category/", pcHeaders)).execute()
val request = when (source) {
// Web sources parse listings whenever possible. They call this function for mobile pages.
is MCCMSWeb -> GET("${source.baseUrl.mobileUrl()}/category/", source.headers)
else -> GET("${source.baseUrl}/category/", pcHeaders)
}
val response = source.client.newCall(request).execute()
parseGenres(response.asJsoup(), this)
} catch (e: Exception) {
status = NOT_FETCHED
@ -74,7 +78,7 @@ class GenreData(hasCategoryPage: Boolean) {
internal fun parseGenres(document: Document, genreData: GenreData) {
if (genreData.status == GenreData.FETCHED || genreData.status == GenreData.NO_DATA) return
val box = document.selectFirst(".cate-selector, .cy_list_l")
val box = document.selectFirst(".cate-selector, .cy_list_l, .ticai, .stui-screen__list")
if (box == null || "/tags/" in document.location()) {
genreData.status = GenreData.NOT_FETCHED
return
@ -85,7 +89,7 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
return
}
val result = buildList(genres.size + 1) {
add(Pair("全部", ""))
add(Pair(Intl.all, ""))
genres.mapTo(this) {
val tagId = it.attr("href").substringAfterLast('/')
Pair(it.text(), tagId)
@ -100,14 +104,14 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
internal fun getFilters(genreData: GenreData): FilterList {
val list = buildList(4) {
add(StatusFilter())
if (Intl.lang == "zh") add(StatusFilter())
add(SortFilter())
if (genreData.status == GenreData.NO_DATA) return@buildList
add(Filter.Separator())
if (genreData.status == GenreData.FETCHED) {
add(genreData.genreFilter.filter)
} else {
add(Filter.Header("点击“重置”尝试刷新标签分类"))
add(Filter.Header(Intl.tapReset))
}
}
return FilterList(list)
@ -115,13 +119,13 @@ internal fun getFilters(genreData: GenreData): FilterList {
internal fun getWebFilters(genreData: GenreData): FilterList {
val list = buildList(4) {
add(Filter.Header("分类筛选(搜索时无效)"))
add(Filter.Header(Intl.categoryWeb))
add(WebStatusFilter())
add(WebSortFilter())
when (genreData.status) {
GenreData.NO_DATA -> return@buildList
GenreData.FETCHED -> add(genreData.genreFilter.webFilter)
else -> add(Filter.Header("点击“重置”尝试刷新标签分类"))
else -> add(Filter.Header(Intl.tapReset))
}
}
return FilterList(list)

View File

@ -13,39 +13,45 @@ import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable
open class MCCMSWeb(
override val name: String,
override val baseUrl: String,
override val lang: String = "zh",
private val config: MCCMSConfig = MCCMSConfig(),
final override val lang: String = "zh",
protected val config: MCCMSConfig = MCCMSConfig(),
) : HttpSource() {
override val supportsLatest get() = true
init {
Intl.lang = lang
}
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.addInterceptor { chain ->
val response = chain.proceed(chain.request())
if (response.request.url.encodedPath == "/err/comic") {
throw IOException(response.body.string().substringBefore('\n'))
}
response
}
.build()
}
override fun headersBuilder() = Headers.Builder()
.add("User-Agent", System.getProperty("http.agent")!!)
private fun parseListing(document: Document): MangasPage {
open fun parseListing(document: Document): MangasPage {
parseGenres(document, config.genreData)
val mangas = document.select(Evaluator.Class("common-comic-item")).map {
SManga.create().apply {
val titleElement = it.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
url = titleElement.attr("href").removePathPrefix()
title = titleElement.ownText()
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
}
}
val mangas = document.select(simpleMangaSelector()).map(::simpleMangaFromElement)
val hasNextPage = run { // default pagination
val buttons = document.selectFirst(Evaluator.Id("Pagination"))!!.select(Evaluator.Tag("a"))
val buttons = document.selectFirst("#Pagination, .NewPages")!!.select(Evaluator.Tag("a"))
val count = buttons.size
// Next page != Last page
buttons[count - 1].attr("href") != buttons[count - 2].attr("href")
@ -53,6 +59,15 @@ open class MCCMSWeb(
return MangasPage(mangas, hasNextPage)
}
open fun simpleMangaSelector() = ".common-comic-item"
open fun simpleMangaFromElement(element: Element) = SManga.create().apply {
val titleElement = element.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
url = titleElement.attr("href").removePathPrefix()
title = titleElement.ownText()
thumbnail_url = element.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/order/hits/page/$page", pcHeaders)
override fun popularMangaParse(response: Response) = parseListing(response.asJsoup())
@ -104,6 +119,8 @@ open class MCCMSWeb(
return super.fetchMangaDetails(manga)
}
override fun getMangaUrl(manga: SManga) = baseUrl.mobileUrl() + manga.url
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
override fun mangaDetailsParse(response: Response): SManga {
@ -127,17 +144,23 @@ open class MCCMSWeb(
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
override fun chapterListParse(response: Response): List<SChapter> {
return run {
response.asJsoup().selectFirst(Evaluator.Class("chapter__list-box"))!!.children().map {
return getDescendingChapters(
response.asJsoup().select(chapterListSelector()).map {
val link = it.child(0)
SChapter.create().apply {
url = link.attr("href").removePathPrefix()
name = link.ownText()
name = link.text()
}
}.asReversed()
}
},
)
}
open fun chapterListSelector() = ".chapter__list-box > li"
open fun getDescendingChapters(chapters: List<SChapter>) = chapters.asReversed()
override fun getChapterUrl(chapter: SChapter) = baseUrl.mobileUrl() + chapter.url
override fun pageListRequest(chapter: SChapter): Request =
GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders)

View File

@ -0,0 +1,9 @@
ext {
extName = 'En Ligne Manga'
extClass = '.EnLigneManga'
themePkg = 'mccms'
baseUrl = 'https://www.enlignemanga.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.fr.enlignemanga
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig
import eu.kanade.tachiyomi.source.model.SManga
class EnLigneManga : MCCMS(
"En Ligne Manga",
"https://www.enlignemanga.com",
"fr",
MCCMSConfig(lazyLoadImageAttr = "src"),
) {
override fun SManga.cleanup() = apply {
title = title.substringBeforeLast(" ligne")
}
}

View File

@ -0,0 +1,9 @@
ext {
extName = 'FR Manga'
extClass = '.FRManga'
themePkg = 'mccms'
baseUrl = 'https://www.frmanga.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.extension.fr.frmanga
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig
class FRManga : MCCMS(
"FR Manga",
"https://www.frmanga.com",
"fr",
MCCMSConfig(lazyLoadImageAttr = "src"),
)

View File

@ -0,0 +1,9 @@
ext {
extName = 'Manhuawu'
extClass = '.Manhuawu'
themePkg = 'mccms'
baseUrl = 'https://www.mhua5.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

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

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.zh.manhuawu
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
class Manhuawu : MCCMS("漫画屋", "https://www.mhua5.com")

View File

@ -0,0 +1,9 @@
ext {
extName = 'Miaoqu Manhua'
extClass = '.Miaoqu'
themePkg = 'mccms'
baseUrl = 'https://www.miaoqumh.org'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,120 @@
package eu.kanade.tachiyomi.extension.zh.miaoqu
import android.util.Base64
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSWeb
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservable
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.util.asJsoup
import keiyoushi.utils.parseAs
import kotlinx.serialization.Serializable
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import kotlin.experimental.xor
// This site shares the same database with 6Manhua (SixMH), but uses manga slug as URL.
class Miaoqu : MCCMSWeb("喵趣漫画", "https://www.miaoqumh.org") {
override fun parseListing(document: Document): MangasPage {
// There's no genre list to parse, so we fetch genres from mobile page in getFilterList()
val entries = document.selectFirst("#mangawrap")!!.children().map { element ->
SManga.create().apply {
val img = element.child(0)
thumbnail_url = img.attr("style").substringBetween("background: url(", ')')
url = img.attr("href")
title = element.selectFirst(".manga-name")!!.text()
author = element.selectFirst(".manga-author")?.text()
}
}
val hasNextPage = run {
val button = document.selectFirst("#next") ?: return@run false
button.attr("href").substringAfterLast('/') != document.location().substringAfterLast('/')
}
return MangasPage(entries, hasNextPage)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters)).asObservable().map { response ->
if (response.code == 404) {
response.close()
throw Exception("服务器错误,无法搜索")
}
searchMangaParse(response)
}
}
// Use mobile page
override fun mangaDetailsRequest(manga: SManga) = GET(getMangaUrl(manga), headers)
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup()
description = document.selectFirst(".text")!!.text()
val infobox = document.selectFirst(".infobox")!!
title = infobox.selectFirst(".title")!!.text()
thumbnail_url = infobox.selectFirst("img")!!.attr("src")
for (element in infobox.select(".tage")) {
val text = element.text()
when (text.substring(0, 3)) {
"作者:" -> author = text.substring(3).trimStart()
"类型:" -> genre = element.select("a").joinToString { it.text() }
"更新于" -> description = "$text\n\n$description"
}
}
}
override fun chapterListRequest(manga: SManga) = GET(getMangaUrl(manga), headers)
override fun chapterListSelector() = "ul.list > li"
// Might return HTTP 500 with page data
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
client.newCall(pageListRequest(chapter)).asObservable().map(::pageListParse)
override fun pageListParse(response: Response): List<Page> {
val cid = response.request.url.pathSegments.last().removeSuffix(".html").toInt()
val key = when (cid % 10) {
0 -> "8-bXd9iN"
1 -> "8-RXyjry"
2 -> "8-oYvwVy"
3 -> "8-4ZY57U"
4 -> "8-mbJpU7"
5 -> "8-6MM2Ei"
6 -> "8-54TiQr"
7 -> "8-Ph5xx9"
8 -> "8-bYgePR"
9 -> "8-Z9A3bW"
else -> throw Exception("Illegal cid: $cid")
}.encodeToByteArray()
check(key.size == 8)
val data = response.body.string().substringBetween("var DATA='", '\'')
val bytes = Base64.decode(data, Base64.DEFAULT)
for (i in bytes.indices) {
bytes[i] = bytes[i] xor key[i and 7]
}
val decrypted = String(Base64.decode(bytes, Base64.DEFAULT))
return decrypted.parseAs<List<Image>>().mapIndexed { i, image -> Page(i, imageUrl = image.url) }
}
@Serializable
private class Image(val url: String)
override fun getFilterList(): FilterList {
config.genreData.fetchGenres(this)
return super.getFilterList()
}
}
private fun String.substringBetween(left: String, right: Char): String {
val index = indexOf(left)
check(index != -1) { "string doesn't match $left[...]$right" }
val startIndex = index + left.length
val endIndex = indexOf(right, startIndex)
check(endIndex != -1) { "string doesn't match $left[...]$right" }
return substring(startIndex, endIndex)
}

View File

@ -1,7 +1,9 @@
ext {
extName = '6Manhua'
extClass = '.SixMH'
extVersionCode = 13
themePkg = 'mccms'
baseUrl = 'https://www.liumanhua.com'
overrideVersionCode = 7
isNsfw = true
}

View File

@ -1,8 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.sixmh
import kotlinx.serialization.Serializable
@Serializable
data class Data(
val images: List<String>,
)

View File

@ -1,27 +0,0 @@
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()
}

View File

@ -1,35 +1,19 @@
package eu.kanade.tachiyomi.extension.zh.sixmh
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSWeb
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.util.asJsoup
import keiyoushi.utils.parseAs
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import kotlinx.serialization.Serializable
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class SixMH : SimpleParsedHttpSource() {
class SixMH : MCCMSWeb("六漫画", "https://www.liumanhua.com") {
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"
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()
@ -38,7 +22,9 @@ class SixMH : SimpleParsedHttpSource() {
}
// Details
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
val document = response.asJsoup()
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")
@ -47,15 +33,12 @@ class SixMH : SimpleParsedHttpSource() {
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()
genre = infoElements[1].select("span:first-child > a").joinToString { it.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()
}
override fun getDescendingChapters(chapters: List<SChapter>) = chapters
// Pages
override fun pageListParse(response: Response): List<Page> {
@ -67,7 +50,8 @@ class SixMH : SimpleParsedHttpSource() {
return images.mapIndexed { index, url -> Page(index, imageUrl = url) }
}
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
@Serializable
private class Data(val images: List<String>)
private fun parseStatus(status: String?): Int {
return when {