MCCMS: update sources (#956)

This commit is contained in:
stevenyomi 2024-02-04 01:54:46 +08:00 committed by GitHub
parent 393920d33c
commit 951774b0b3
34 changed files with 250 additions and 464 deletions

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.extension.zh.damaomanhua
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
class DamaoManhua : MCCMS(
"大猫漫画",
"https://www.hanman.cyou/index.php",
"zh",
MCCMSConfig(useMobilePageList = true),
) {
// Details and chapter pages are broken
override fun getMangaUrl(manga: SManga) = baseUrl
override fun getChapterUrl(chapter: SChapter) = baseUrl
}

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.extension.zh.didamanhua
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
class DidaManhua : MCCMS(
"嘀嗒漫画",
"https://www.didamanhua.com/index.php",
"zh",
MCCMSConfig(useMobilePageList = true),
) {
// Details and chapter pages are broken
override fun getMangaUrl(manga: SManga) = baseUrl
override fun getChapterUrl(chapter: SChapter) = baseUrl
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.multisrc.mccms
package eu.kanade.tachiyomi.extension.zh.kuaikuai3
import android.util.Base64
import okhttp3.Interceptor

View File

@ -1,6 +1,5 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

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

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,20 @@
package eu.kanade.tachiyomi.extension.zh.miaoshang
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import okhttp3.HttpUrl.Companion.toHttpUrl
class Miaoshang : MCCMS(
"喵上漫画",
"https://www.miaoshangmanhua.com",
"zh",
MCCMSConfig(
textSearchOnlyPageOne = true,
lazyLoadImageAttr = "data-src",
),
) {
override val client = network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
}

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.extension.zh.sixmh
import android.app.Application
import android.os.Build
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SixMH : MCCMS("六漫画", "https://www.liumanhua.com") {
override val versionId get() = 2
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Delete old preferences for "6漫画/zh/1"
Injekt.get<Application>().deleteSharedPreferences("source_7259486566651312186")
}
}
override fun getMangaUrl(manga: SManga) = "https://m.liumanhua.com" + manga.url
override fun getChapterUrl(chapter: SChapter) = "https://m.liumanhua.com" + chapter.url
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.mccms
import android.util.Log
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
@ -10,7 +9,6 @@ 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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.Headers
@ -19,7 +17,7 @@ import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import kotlin.concurrent.thread
import java.net.URLEncoder
/**
* 漫城CMS http://mccms.cn/
@ -28,7 +26,7 @@ open class MCCMS(
override val name: String,
override val baseUrl: String,
override val lang: String = "zh",
hasCategoryPage: Boolean = false,
private val config: MCCMSConfig = MCCMSConfig(),
) : HttpSource() {
override val supportsLatest = true
@ -37,25 +35,19 @@ open class MCCMS(
override val client by lazy {
network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.addInterceptor(DecryptInterceptor)
.build()
}
val pcHeaders by lazy { super.headersBuilder().build() }
override fun headersBuilder() = Headers.Builder()
.add("User-Agent", System.getProperty("http.agent")!!)
.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.prepare().toSManga().cleanup() }, list.size >= PAGE_SIZE)
return MangasPage(list.map { it.toSManga() }, list.size >= PAGE_SIZE)
}
override fun latestUpdatesRequest(page: Int): Request =
@ -68,7 +60,7 @@ open class MCCMS(
add("page=$page")
add("size=$PAGE_SIZE")
val isTextSearch = query.isNotBlank()
if (isTextSearch) add("key=$query")
if (isTextSearch) add("key=" + URLEncoder.encode(query, "UTF-8"))
for (filter in filters) if (filter is MCCMSFilter) {
if (isTextSearch && filter.isTypeQuery) continue
val part = filter.query
@ -84,22 +76,24 @@ open class MCCMS(
override fun searchMangaParse(response: Response) = popularMangaParse(response)
// preserve mangaDetailsRequest for WebView
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
val url = "$baseUrl/api/data/comic".toHttpUrl().newBuilder()
.addQueryParameter("key", manga.title)
.toString()
val mangaUrl = manga.url
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val list = response.parseAs<List<MangaDto>>().map { it.prepare() }
list.find { it.url == manga.url }!!.toSManga().cleanup()
val list = response.parseAs<List<MangaDto>>()
list.first { it.cleanUrl == mangaUrl }.toSManga()
}
}
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
val id = getMangaId(manga.url)
val id = manga.thumbnail_url!!.substringAfterLast('#', missingDelimiterValue = "").ifEmpty { throw Exception("请刷新漫画") }
val dataResponse = client.newCall(GET("$baseUrl/api/data/chapter?mid=$id", headers)).execute()
val dataList: List<ChapterDataDto> = dataResponse.parseAs() // unordered
val dateMap = HashMap<Int, Long>(dataList.size * 2)
@ -110,48 +104,28 @@ open class MCCMS(
result
}
protected open fun getMangaId(url: String) = url.substringAfterLast('/')
override fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException()
override fun pageListRequest(chapter: SChapter): Request =
GET(baseUrl + chapter.url, pcHeaders)
GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders)
protected open val lazyLoadImageAttr = "data-original"
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("img[$lazyLoadImageAttr]").mapIndexed { i, element ->
Page(i, imageUrl = element.attr(lazyLoadImageAttr))
}
return config.pageListParse(response)
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
// 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
}
val genreData = GenreData(hasCategoryPage)
fun fetchGenres() {
if (genreData.status != GenreData.NOT_FETCHED) return
genreData.status = GenreData.FETCHING
thread {
try {
val response = client.newCall(GET("$baseUrl/category/", pcHeaders)).execute()
parseGenres(response.asJsoup(), genreData)
} catch (e: Exception) {
genreData.status = GenreData.NOT_FETCHED
Log.e("MCCMS/$name", "failed to fetch genres", e)
}
}
}
override fun getFilterList(): FilterList {
fetchGenres()
val genreData = config.genreData.also { it.fetchGenres(this) }
return getFilters(genreData)
}
}

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.multisrc.mccms
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.Response
import org.jsoup.select.Evaluator
const val PAGE_SIZE = 30
val pcHeaders = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0")
fun String.removePathPrefix() = removePrefix("/index.php")
open class MCCMSConfig(
hasCategoryPage: Boolean = true,
val textSearchOnlyPageOne: Boolean = false,
val useMobilePageList: Boolean = false,
private val lazyLoadImageAttr: String = "data-original",
) {
val genreData = GenreData(hasCategoryPage)
fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return if (useMobilePageList) {
val container = document.selectFirst(Evaluator.Class("comic-list"))!!
container.select(Evaluator.Tag("img")).mapIndexed { i, img ->
Page(i, imageUrl = img.attr("src"))
}
} else {
document.select("img[$lazyLoadImageAttr]").mapIndexed { i, img ->
Page(i, imageUrl = img.attr(lazyLoadImageAttr))
}
}
}
}

View File

@ -3,50 +3,54 @@ package eu.kanade.tachiyomi.multisrc.mccms
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
import org.jsoup.nodes.Entities
import java.text.SimpleDateFormat
import java.util.Locale
internal const val PAGE_SIZE = 30
@Serializable
data class MangaDto(
val id: String,
private val id: String,
private val name: String,
private val pic: String,
private val serialize: String,
private val author: String,
private val content: String,
private val addtime: String,
val url: String,
private val url: String,
private val tags: List<String>,
) {
val cleanUrl get() = url.removePathPrefix()
fun toSManga() = SManga.create().apply {
url = this@MangaDto.url
title = name
author = this@MangaDto.author
description = content
url = cleanUrl
title = Entities.unescape(name)
author = Entities.unescape(this@MangaDto.author)
description = Entities.unescape(content)
genre = tags.joinToString()
val date = dateFormat.parse(addtime)?.time ?: 0
val isUpdating = System.currentTimeMillis() - date <= 30L * 24 * 3600 * 1000 // a month
status = when {
'连' in serialize || isUpdating -> SManga.ONGOING
'连' in serialize || isUpdating(addtime) -> SManga.ONGOING
'完' in serialize -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
thumbnail_url = pic
thumbnail_url = "$pic#$id"
initialized = true
}
companion object {
private val dateFormat by lazy { getDateFormat() }
private fun isUpdating(dateStr: String): Boolean {
val date = dateFormat.parse(dateStr) ?: return false
return System.currentTimeMillis() - date.time <= 30L * 24 * 3600 * 1000 // a month
}
}
}
@Serializable
class ChapterDto(val id: String, private val name: String, private val link: String) {
fun toSChapter(date: Long) = SChapter.create().apply {
url = link
name = this@ChapterDto.name
url = link.removePathPrefix()
name = Entities.unescape(this@ChapterDto.name)
date_upload = date
}
}

View File

@ -1,8 +1,13 @@
package eu.kanade.tachiyomi.multisrc.mccms
import android.util.Log
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import org.jsoup.nodes.Document
import kotlin.concurrent.thread
open class MCCMSFilter(
name: String,
@ -45,6 +50,20 @@ class GenreData(hasCategoryPage: Boolean) {
var status = if (hasCategoryPage) NOT_FETCHED else NO_DATA
lateinit var genreFilter: GenreFilter
fun fetchGenres(source: HttpSource) {
if (status != NOT_FETCHED) return
status = FETCHING
thread {
try {
val response = source.client.newCall(GET("${source.baseUrl}/category/", pcHeaders)).execute()
parseGenres(response.asJsoup(), this)
} catch (e: Exception) {
status = NOT_FETCHED
Log.e("MCCMS/${source.name}", "failed to fetch genres", e)
}
}
}
companion object {
const val NOT_FETCHED = 0
const val FETCHING = 1
@ -54,7 +73,13 @@ class GenreData(hasCategoryPage: Boolean) {
}
internal fun parseGenres(document: Document, genreData: GenreData) {
val genres = document.select("a[href^=/category/tags/]")
if (genreData.status == GenreData.FETCHED || genreData.status == GenreData.NO_DATA) return
val box = document.selectFirst(".cate-selector, .cy_list_l")
if (box == null || "/tags/" in document.location()) {
genreData.status = GenreData.NOT_FETCHED
return
}
val genres = box.select("a[href*=/tags/]")
if (genres.isEmpty()) {
genreData.status = GenreData.NO_DATA
return

View File

@ -17,42 +17,42 @@ class MCCMSGenerator : ThemeSourceGenerator {
overrideVersionCode = 0,
),
SingleLang(
name = "Manhuawu",
baseUrl = "https://www.mhua5.com",
name = "6Manhua",
baseUrl = "https://www.liumanhua.com",
lang = "zh",
className = "Manhuawu",
sourceName = "漫画屋",
className = "SixMH",
sourceName = "六漫画",
overrideVersionCode = 4,
),
SingleLang(
name = "Miaoshang Manhua",
baseUrl = "https://www.miaoshangmanhua.com",
lang = "zh",
className = "Miaoshang",
sourceName = "喵上漫画",
overrideVersionCode = 0,
),
// The following sources are from https://www.yy123.cyou/ and are configured to use MCCMSNsfw
SingleLang( // 103=校园梦精记, same as: www.hmanwang.com, www.quanman8.com, www.lmmh.cc, www.xinmanba.com
// The following sources are from https://www.yy123.cyou/
SingleLang( // 103=他的那里, same as: www.hmanwang.com, www.lmmh.cc, www.999mh.net
name = "Dida Manhua",
baseUrl = "https://www.didamanhua.com",
baseUrl = "https://www.didamanhua.com/index.php",
lang = "zh",
isNsfw = true,
className = "DidaManhua",
sourceName = "嘀嗒漫画",
overrideVersionCode = 0,
overrideVersionCode = 1,
),
SingleLang( // 103=脱身之法, same as: www.quanmanba.com, www.999mh.net
name = "Dimanba",
baseUrl = "https://www.dimanba.com",
SingleLang( // 103=青春男女(完结), same as: www.hanman.men
name = "Damao Manhua",
baseUrl = "https://www.hanman.cyou/index.php",
lang = "zh",
isNsfw = true,
className = "Dimanba",
sourceName = "滴漫吧",
className = "DamaoManhua",
sourceName = "大猫漫画",
overrideVersionCode = 0,
),
)
override fun createAll() {
val userDir = System.getProperty("user.dir")!!
sources.forEach {
val themeClass = if (it.isNsfw) "MCCMSNsfw" else themeClass
ThemeSourceGenerator.createGradleProject(it, themePkg, themeClass, baseVersionCode, userDir)
}
}
companion object {
@JvmStatic
fun main(args: Array<String>) {

View File

@ -1,36 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mccms
import eu.kanade.tachiyomi.network.GET
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.util.asJsoup
import okhttp3.Request
import okhttp3.Response
import org.jsoup.select.Evaluator
open class MCCMSNsfw(
name: String,
baseUrl: String,
lang: String = "zh",
) : MCCMSWeb(name, baseUrl, lang, hasCategoryPage = false) {
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
if (query.isNotBlank()) {
GET("$baseUrl/search/$query/$page", pcHeaders)
} else {
super.searchMangaRequest(page, query, filters)
}
override fun searchMangaParse(response: Response) = parseListing(response.asJsoup())
override fun pageListRequest(chapter: SChapter): Request =
GET(baseUrl + chapter.url, headers)
override fun pageListParse(response: Response): List<Page> {
val container = response.asJsoup().selectFirst(Evaluator.Class("comic-list"))!!
return container.select(Evaluator.Tag("img")).mapIndexed { index, img ->
Page(index, imageUrl = img.attr("src"))
}
}
}

View File

@ -1,34 +1,48 @@
package eu.kanade.tachiyomi.multisrc.mccms
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.select.Evaluator
import rx.Observable
// https://github.com/tachiyomiorg/tachiyomi-extensions/blob/e0b4fcbce8aa87742da22e7fa60b834313f53533/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt
open class MCCMSWeb(
name: String,
baseUrl: String,
lang: String = "zh",
hasCategoryPage: Boolean = true,
) : MCCMS(name, baseUrl, lang, hasCategoryPage) {
override val name: String,
override val baseUrl: String,
override val lang: String = "zh",
private val config: MCCMSConfig = MCCMSConfig(),
) : HttpSource() {
override val supportsLatest get() = true
protected open fun parseListing(document: Document): MangasPage {
override val client by lazy {
network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
}
override fun headersBuilder() = Headers.Builder()
.add("User-Agent", System.getProperty("http.agent")!!)
private 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")
url = titleElement.attr("href").removePathPrefix()
title = titleElement.ownText()
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
}.cleanup()
}
}
val hasNextPage = run { // default pagination
val buttons = document.selectFirst(Evaluator.Id("Pagination"))!!.select(Evaluator.Tag("a"))
@ -49,9 +63,13 @@ open class MCCMSWeb(
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
if (query.isNotBlank()) {
val url = "$baseUrl/index.php/search".toHttpUrl().newBuilder()
.addQueryParameter("key", query)
.toString()
val url = if (config.textSearchOnlyPageOne) {
"$baseUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("key", query)
.toString()
} else {
"$baseUrl/search/$query/$page"
}
GET(url, pcHeaders)
} else {
val url = buildString {
@ -67,7 +85,7 @@ open class MCCMSWeb(
val document = response.asJsoup()
if (document.selectFirst(Evaluator.Id("code-div")) != null) {
val manga = SManga.create().apply {
url = "/index.php/search"
url = "/search"
title = "验证码"
description = "请点击 WebView 按钮输入验证码,完成后返回重新搜索"
initialized = true
@ -75,19 +93,19 @@ open class MCCMSWeb(
return MangasPage(listOf(manga), false)
}
val result = parseListing(document)
if (document.location().contains("search")) {
if (config.textSearchOnlyPageOne && document.location().contains("search")) {
return MangasPage(result.mangas, false)
}
return result
}
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)
}
if (manga.url == "/search") return Observable.just(manga)
return super.fetchMangaDetails(manga)
}
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
override fun mangaDetailsParse(response: Response): SManga {
return run {
SManga.create().apply {
@ -97,31 +115,41 @@ open class MCCMSWeb(
author = document.selectFirst(Evaluator.Class("name"))!!.text()
genre = document.selectFirst(Evaluator.Class("comic-status"))!!.select(Evaluator.Tag("a")).joinToString { it.ownText() }
description = document.selectFirst(Evaluator.Class("intro-total"))!!.text()
}.cleanup()
}
}
}
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)
}
if (manga.url == "/search") return Observable.just(emptyList())
return super.fetchChapterList(manga)
}
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 {
val link = it.child(0)
SChapter.create().apply {
url = link.attr("href")
url = link.attr("href").removePathPrefix()
name = link.ownText()
}
}.asReversed()
}
}
override fun pageListRequest(chapter: SChapter): Request =
GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders)
override fun pageListParse(response: Response) = config.pageListParse(response)
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
// Don't send referer
override fun imageRequest(page: Page) = GET(page.imageUrl!!, pcHeaders)
override fun getFilterList(): FilterList {
fetchGenres()
val genreData = config.genreData
return getWebFilters(genreData)
}
}

View File

@ -1,11 +0,0 @@
ext {
extName = '6Manhua / Qixi Manhua'
extClass = '.SixMH'
extVersionCode = 9
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation project(':lib:unpacker')
}

View File

@ -1,32 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.sixmh
import eu.kanade.tachiyomi.network.POST
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.Request
/** Documentation of unused APIs originally used in `zh.qiximh`. */
object Api {
fun getRankRequest(baseUrl: String, headers: Headers, page: Int, type: Int) =
getListingRequest("$baseUrl/rankdata.php", headers, page, type)
fun getSortRequest(baseUrl: String, headers: Headers, page: Int, type: Int) =
getListingRequest("$baseUrl/sortdata.php", headers, page, type)
/** @param page 1-5. Website allows 1-10 and contains more items per page. */
fun getListingRequest(url: String, headers: Headers, page: Int, type: Int): Request {
val body = FormBody.Builder()
.add("page_num", page.toString())
.add("type", type.toString())
.build()
return POST(url, headers, body)
}
fun getSearchRequest(baseUrl: String, headers: Headers, query: String): Request {
val body = FormBody.Builder()
.add("keyword", query)
.build()
return POST("$baseUrl/search.php", headers, body)
}
}

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.sixmh
import eu.kanade.tachiyomi.source.model.SChapter
import kotlinx.serialization.Serializable
@Serializable
class QixiChapterDto(private val id: String, private val name: String) {
fun toSChapter(path: String) = SChapter.create().apply {
url = "$path$id.html"
name = this@QixiChapterDto.name
}
}
@Serializable
class QixiDataDto(val list: List<QixiChapterDto>)
@Serializable
class QixiResponseDto(val data: QixiDataDto)

View File

@ -1,222 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.sixmh
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.select.Evaluator
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.random.Random
class SixMH : HttpSource(), ConfigurableSource {
override val name = "6漫画"
override val lang = "zh"
override val supportsLatest = true
private val isCi = System.getenv("CI") == "true"
override val baseUrl get() = when {
isCi -> MIRRORS.zip(MIRROR_NAMES) { domain, name -> "http://www.$domain#$name" }.joinToString()
else -> _baseUrl
}
private val mirrorIndex: Int
private val pcUrl: String
private val _baseUrl: String
init {
val preferences = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
val mirrors = MIRRORS
var index = preferences.getString(MIRROR_PREF, "-1")!!.toInt()
if (index !in mirrors.indices) {
index = Random.nextInt(0, mirrors.size)
preferences.edit().putString(MIRROR_PREF, index.toString()).apply()
}
val domain = mirrors[index]
mirrorIndex = index
pcUrl = "http://www.$domain"
_baseUrl = "http://$domain"
}
private val json: Json by injectLazy()
override val client = network.client.newBuilder()
.rateLimit(2)
.build()
override fun popularMangaRequest(page: Int) = GET("$pcUrl/rank/1-$page.html", headers)
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val imgSelector = Evaluator.Tag("img")
val items = document.selectFirst(Evaluator.Class("cy_list_mh"))!!.children().map {
SManga.create().apply {
val link = it.child(1).child(0)
url = link.attr("href")
title = link.ownText()
thumbnail_url = it.selectFirst(imgSelector)!!.attr("src")
}
}
val hasNextPage = document.selectFirst(Evaluator.Class("thisclass"))?.nextElementSibling() != null
return MangasPage(items, hasNextPage)
}
override fun latestUpdatesRequest(page: Int) = GET("$pcUrl/rank/5-$page.html", headers)
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotBlank()) {
val url = pcUrl.toHttpUrl().newBuilder()
.addEncodedPathSegment("search.php")
.addQueryParameter("keyword", query)
.toString()
return GET(url, headers)
} else {
filters.filterIsInstance<PageFilter>().firstOrNull()?.run {
return GET("$pcUrl$path$page.html", headers)
}
return popularMangaRequest(page)
}
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(GET(pcUrl + manga.url, headers))
.asObservableSuccess().map(::mangaDetailsParse)
}
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
val result = SManga.create().apply {
val box = document.selectFirst(Evaluator.Class("cy_info"))!!
val details = box.getElementsByTag("span")
author = details[0].text().removePrefix("作者:")
status = when (details[1].text().removePrefix("状态:").trimStart()) {
"连载中" -> 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"))!!.run {
attr("data-src").ifEmpty { attr("src") }
}
}
return result
}
override fun chapterListRequest(manga: SManga) = GET(pcUrl + manga.url, headers)
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val list = document.selectFirst(Evaluator.Class("cy_plist"))!!
.child(0).children().map {
val element = it.child(0)
SChapter.create().apply {
url = element.attr("href")
name = element.text()
}
}
as ArrayList
if (mirrorIndex == 0) { // 6Manhua
document.selectFirst(Evaluator.Id("zhankai"))?.let { element ->
val path = '/' + response.request.url.pathSegments[0] + '/'
val body = FormBody.Builder().apply {
addEncoded("id", element.attr("data-id"))
addEncoded("id2", element.attr("data-vid"))
}.build()
client.newCall(POST("$pcUrl/bookchapter/", headers, body)).execute()
.parseAs<List<ChapterDto>>().mapTo(list) { it.toSChapter(path) }
}
} else { // Qixi Manhua
if (document.selectFirst(Evaluator.Class("morechp")) != null) {
val id = response.request.url.pathSegments[0]
val path = "/$id/"
val body = FormBody.Builder().addEncoded("id", id).build()
client.newCall(POST("$pcUrl/chapterlist/", headers, body)).execute()
.parseAs<QixiResponseDto>().data.list.mapTo(list) { it.toSChapter(path) }
}
}
if (list.isNotEmpty()) {
document.selectFirst(".cy_zhangjie_top font")?.run {
list[0].date_upload = dateFormat.parse(ownText())?.time ?: 0
}
}
return list
}
override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers)
override fun pageListParse(response: Response): List<Page> {
val result = Unpacker.unpack(response.body.string(), "[", "]")
.ifEmpty { return emptyList() }
.replace("\\u0026", "&")
.replace("\\", "")
.removeSurrounding("\"").split("\",\"")
return result.mapIndexed { i, url -> Page(i, imageUrl = url) }
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(body.byteStream())
}
override fun getFilterList() = FilterList(listOf(PageFilter()))
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
val names = MIRROR_NAMES
key = MIRROR_PREF
title = "镜像站点(重启生效)"
summary = "%s"
entries = names
entryValues = Array(names.size, Int::toString)
setDefaultValue("0")
}.let(screen::addPreference)
}
companion object {
const val MIRROR_PREF = "MIRROR"
/** Note: mirror index affects [chapterListParse] */
val MIRRORS get() = arrayOf("sixmanhua.com", "qiximh3.com")
val MIRROR_NAMES get() = arrayOf("6漫画", "七夕漫画")
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
}
}

View File

@ -1,29 +0,0 @@
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(private val chapterid: String, private 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-",
)