MCCMS: update sources (#956)
17
multisrc/overrides/mccms/damaomanhua/src/DamaoManhua.kt
Normal 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
|
||||
}
|
17
multisrc/overrides/mccms/didamanhua/src/DidaManhua.kt
Normal 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mccms
|
||||
package eu.kanade.tachiyomi.extension.zh.kuaikuai3
|
||||
|
||||
import android.util.Base64
|
||||
import okhttp3.Interceptor
|
@ -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
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 7.8 KiB |
@ -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('.')
|
||||
}
|
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 7.3 KiB |
20
multisrc/overrides/mccms/miaoshang/src/Miaoshang.kt
Normal 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()
|
||||
}
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
24
multisrc/overrides/mccms/sixmh/src/SixMH.kt
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>) {
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
ext {
|
||||
extName = '6Manhua / Qixi Manhua'
|
||||
extClass = '.SixMH'
|
||||
extVersionCode = 9
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(':lib:unpacker')
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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-",
|
||||
)
|