MCCMS: update sources (#956)
|
@ -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
|
||||||
|
}
|
|
@ -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 android.util.Base64
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.extension.zh.kuaikuai3
|
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.GET
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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 |
|
@ -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 |
|
@ -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
|
package eu.kanade.tachiyomi.multisrc.mccms
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
@ -19,7 +17,7 @@ import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.concurrent.thread
|
import java.net.URLEncoder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 漫城CMS http://mccms.cn/
|
* 漫城CMS http://mccms.cn/
|
||||||
|
@ -28,7 +26,7 @@ open class MCCMS(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
override val lang: String = "zh",
|
override val lang: String = "zh",
|
||||||
hasCategoryPage: Boolean = false,
|
private val config: MCCMSConfig = MCCMSConfig(),
|
||||||
) : HttpSource() {
|
) : HttpSource() {
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
@ -37,25 +35,19 @@ open class MCCMS(
|
||||||
override val client by lazy {
|
override val client by lazy {
|
||||||
network.client.newBuilder()
|
network.client.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
.addInterceptor(DecryptInterceptor)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val pcHeaders by lazy { super.headersBuilder().build() }
|
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder()
|
override fun headersBuilder() = Headers.Builder()
|
||||||
.add("User-Agent", System.getProperty("http.agent")!!)
|
.add("User-Agent", System.getProperty("http.agent")!!)
|
||||||
.add("Referer", baseUrl)
|
.add("Referer", baseUrl)
|
||||||
|
|
||||||
protected open fun SManga.cleanup(): SManga = this
|
|
||||||
protected open fun MangaDto.prepare(): MangaDto = this
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
GET("$baseUrl/api/data/comic?page=$page&size=$PAGE_SIZE&order=hits", headers)
|
GET("$baseUrl/api/data/comic?page=$page&size=$PAGE_SIZE&order=hits", headers)
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
val list: List<MangaDto> = response.parseAs()
|
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 =
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
|
@ -68,7 +60,7 @@ open class MCCMS(
|
||||||
add("page=$page")
|
add("page=$page")
|
||||||
add("size=$PAGE_SIZE")
|
add("size=$PAGE_SIZE")
|
||||||
val isTextSearch = query.isNotBlank()
|
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) {
|
for (filter in filters) if (filter is MCCMSFilter) {
|
||||||
if (isTextSearch && filter.isTypeQuery) continue
|
if (isTextSearch && filter.isTypeQuery) continue
|
||||||
val part = filter.query
|
val part = filter.query
|
||||||
|
@ -84,22 +76,24 @@ open class MCCMS(
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
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> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
val url = "$baseUrl/api/data/comic".toHttpUrl().newBuilder()
|
val url = "$baseUrl/api/data/comic".toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("key", manga.title)
|
.addQueryParameter("key", manga.title)
|
||||||
.toString()
|
.toString()
|
||||||
|
val mangaUrl = manga.url
|
||||||
return client.newCall(GET(url, headers))
|
return client.newCall(GET(url, headers))
|
||||||
.asObservableSuccess().map { response ->
|
.asObservableSuccess().map { response ->
|
||||||
val list = response.parseAs<List<MangaDto>>().map { it.prepare() }
|
val list = response.parseAs<List<MangaDto>>()
|
||||||
list.find { it.url == manga.url }!!.toSManga().cleanup()
|
list.first { it.cleanUrl == mangaUrl }.toSManga()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
|
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 dataResponse = client.newCall(GET("$baseUrl/api/data/chapter?mid=$id", headers)).execute()
|
||||||
val dataList: List<ChapterDataDto> = dataResponse.parseAs() // unordered
|
val dataList: List<ChapterDataDto> = dataResponse.parseAs() // unordered
|
||||||
val dateMap = HashMap<Int, Long>(dataList.size * 2)
|
val dateMap = HashMap<Int, Long>(dataList.size * 2)
|
||||||
|
@ -110,48 +104,28 @@ open class MCCMS(
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun getMangaId(url: String) = url.substringAfterLast('/')
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException()
|
override fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request =
|
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> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val document = response.asJsoup()
|
return config.pageListParse(response)
|
||||||
return document.select("img[$lazyLoadImageAttr]").mapIndexed { i, element ->
|
|
||||||
Page(i, imageUrl = element.attr(lazyLoadImageAttr))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// Don't send referer
|
||||||
override fun imageRequest(page: Page) = GET(page.imageUrl!!, pcHeaders)
|
override fun imageRequest(page: Page) = GET(page.imageUrl!!, pcHeaders)
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T = use {
|
private inline fun <reified T> Response.parseAs(): T = use {
|
||||||
json.decodeFromStream<ResultDto<T>>(it.body.byteStream()).data
|
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 {
|
override fun getFilterList(): FilterList {
|
||||||
fetchGenres()
|
val genreData = config.genreData.also { it.fetchGenres(this) }
|
||||||
return getFilters(genreData)
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jsoup.nodes.Entities
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
internal const val PAGE_SIZE = 30
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MangaDto(
|
data class MangaDto(
|
||||||
val id: String,
|
private val id: String,
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val pic: String,
|
private val pic: String,
|
||||||
private val serialize: String,
|
private val serialize: String,
|
||||||
private val author: String,
|
private val author: String,
|
||||||
private val content: String,
|
private val content: String,
|
||||||
private val addtime: String,
|
private val addtime: String,
|
||||||
val url: String,
|
private val url: String,
|
||||||
private val tags: List<String>,
|
private val tags: List<String>,
|
||||||
) {
|
) {
|
||||||
|
val cleanUrl get() = url.removePathPrefix()
|
||||||
|
|
||||||
fun toSManga() = SManga.create().apply {
|
fun toSManga() = SManga.create().apply {
|
||||||
url = this@MangaDto.url
|
url = cleanUrl
|
||||||
title = name
|
title = Entities.unescape(name)
|
||||||
author = this@MangaDto.author
|
author = Entities.unescape(this@MangaDto.author)
|
||||||
description = content
|
description = Entities.unescape(content)
|
||||||
genre = tags.joinToString()
|
genre = tags.joinToString()
|
||||||
val date = dateFormat.parse(addtime)?.time ?: 0
|
|
||||||
val isUpdating = System.currentTimeMillis() - date <= 30L * 24 * 3600 * 1000 // a month
|
|
||||||
status = when {
|
status = when {
|
||||||
'连' in serialize || isUpdating -> SManga.ONGOING
|
'连' in serialize || isUpdating(addtime) -> SManga.ONGOING
|
||||||
'完' in serialize -> SManga.COMPLETED
|
'完' in serialize -> SManga.COMPLETED
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
thumbnail_url = pic
|
thumbnail_url = "$pic#$id"
|
||||||
initialized = true
|
initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val dateFormat by lazy { getDateFormat() }
|
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
|
@Serializable
|
||||||
class ChapterDto(val id: String, private val name: String, private val link: String) {
|
class ChapterDto(val id: String, private val name: String, private val link: String) {
|
||||||
fun toSChapter(date: Long) = SChapter.create().apply {
|
fun toSChapter(date: Long) = SChapter.create().apply {
|
||||||
url = link
|
url = link.removePathPrefix()
|
||||||
name = this@ChapterDto.name
|
name = Entities.unescape(this@ChapterDto.name)
|
||||||
date_upload = date
|
date_upload = date
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.mccms
|
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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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 org.jsoup.nodes.Document
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
open class MCCMSFilter(
|
open class MCCMSFilter(
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -45,6 +50,20 @@ class GenreData(hasCategoryPage: Boolean) {
|
||||||
var status = if (hasCategoryPage) NOT_FETCHED else NO_DATA
|
var status = if (hasCategoryPage) NOT_FETCHED else NO_DATA
|
||||||
lateinit var genreFilter: GenreFilter
|
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 {
|
companion object {
|
||||||
const val NOT_FETCHED = 0
|
const val NOT_FETCHED = 0
|
||||||
const val FETCHING = 1
|
const val FETCHING = 1
|
||||||
|
@ -54,7 +73,13 @@ class GenreData(hasCategoryPage: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun parseGenres(document: Document, genreData: GenreData) {
|
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()) {
|
if (genres.isEmpty()) {
|
||||||
genreData.status = GenreData.NO_DATA
|
genreData.status = GenreData.NO_DATA
|
||||||
return
|
return
|
||||||
|
|
|
@ -17,42 +17,42 @@ class MCCMSGenerator : ThemeSourceGenerator {
|
||||||
overrideVersionCode = 0,
|
overrideVersionCode = 0,
|
||||||
),
|
),
|
||||||
SingleLang(
|
SingleLang(
|
||||||
name = "Manhuawu",
|
name = "6Manhua",
|
||||||
baseUrl = "https://www.mhua5.com",
|
baseUrl = "https://www.liumanhua.com",
|
||||||
lang = "zh",
|
lang = "zh",
|
||||||
className = "Manhuawu",
|
className = "SixMH",
|
||||||
sourceName = "漫画屋",
|
sourceName = "六漫画",
|
||||||
|
overrideVersionCode = 4,
|
||||||
|
),
|
||||||
|
SingleLang(
|
||||||
|
name = "Miaoshang Manhua",
|
||||||
|
baseUrl = "https://www.miaoshangmanhua.com",
|
||||||
|
lang = "zh",
|
||||||
|
className = "Miaoshang",
|
||||||
|
sourceName = "喵上漫画",
|
||||||
overrideVersionCode = 0,
|
overrideVersionCode = 0,
|
||||||
),
|
),
|
||||||
// The following sources are from https://www.yy123.cyou/ and are configured to use MCCMSNsfw
|
// The following sources are from https://www.yy123.cyou/
|
||||||
SingleLang( // 103=校园梦精记, same as: www.hmanwang.com, www.quanman8.com, www.lmmh.cc, www.xinmanba.com
|
SingleLang( // 103=他的那里, same as: www.hmanwang.com, www.lmmh.cc, www.999mh.net
|
||||||
name = "Dida Manhua",
|
name = "Dida Manhua",
|
||||||
baseUrl = "https://www.didamanhua.com",
|
baseUrl = "https://www.didamanhua.com/index.php",
|
||||||
lang = "zh",
|
lang = "zh",
|
||||||
isNsfw = true,
|
isNsfw = true,
|
||||||
className = "DidaManhua",
|
className = "DidaManhua",
|
||||||
sourceName = "嘀嗒漫画",
|
sourceName = "嘀嗒漫画",
|
||||||
overrideVersionCode = 0,
|
overrideVersionCode = 1,
|
||||||
),
|
),
|
||||||
SingleLang( // 103=脱身之法, same as: www.quanmanba.com, www.999mh.net
|
SingleLang( // 103=青春男女(完结), same as: www.hanman.men
|
||||||
name = "Dimanba",
|
name = "Damao Manhua",
|
||||||
baseUrl = "https://www.dimanba.com",
|
baseUrl = "https://www.hanman.cyou/index.php",
|
||||||
lang = "zh",
|
lang = "zh",
|
||||||
isNsfw = true,
|
isNsfw = true,
|
||||||
className = "Dimanba",
|
className = "DamaoManhua",
|
||||||
sourceName = "滴漫吧",
|
sourceName = "大猫漫画",
|
||||||
overrideVersionCode = 0,
|
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 {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
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
|
package eu.kanade.tachiyomi.multisrc.mccms
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.select.Evaluator
|
import org.jsoup.select.Evaluator
|
||||||
import rx.Observable
|
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(
|
open class MCCMSWeb(
|
||||||
name: String,
|
override val name: String,
|
||||||
baseUrl: String,
|
override val baseUrl: String,
|
||||||
lang: String = "zh",
|
override val lang: String = "zh",
|
||||||
hasCategoryPage: Boolean = true,
|
private val config: MCCMSConfig = MCCMSConfig(),
|
||||||
) : MCCMS(name, baseUrl, lang, hasCategoryPage) {
|
) : 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 {
|
val mangas = document.select(Evaluator.Class("common-comic-item")).map {
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
val titleElement = it.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
|
val titleElement = it.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
|
||||||
url = titleElement.attr("href")
|
url = titleElement.attr("href").removePathPrefix()
|
||||||
title = titleElement.ownText()
|
title = titleElement.ownText()
|
||||||
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
|
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
|
||||||
}.cleanup()
|
}
|
||||||
}
|
}
|
||||||
val hasNextPage = run { // default pagination
|
val hasNextPage = run { // default pagination
|
||||||
val buttons = document.selectFirst(Evaluator.Id("Pagination"))!!.select(Evaluator.Tag("a"))
|
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) =
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
if (query.isNotBlank()) {
|
if (query.isNotBlank()) {
|
||||||
val url = "$baseUrl/index.php/search".toHttpUrl().newBuilder()
|
val url = if (config.textSearchOnlyPageOne) {
|
||||||
|
"$baseUrl/search".toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("key", query)
|
.addQueryParameter("key", query)
|
||||||
.toString()
|
.toString()
|
||||||
|
} else {
|
||||||
|
"$baseUrl/search/$query/$page"
|
||||||
|
}
|
||||||
GET(url, pcHeaders)
|
GET(url, pcHeaders)
|
||||||
} else {
|
} else {
|
||||||
val url = buildString {
|
val url = buildString {
|
||||||
|
@ -67,7 +85,7 @@ open class MCCMSWeb(
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
if (document.selectFirst(Evaluator.Id("code-div")) != null) {
|
if (document.selectFirst(Evaluator.Id("code-div")) != null) {
|
||||||
val manga = SManga.create().apply {
|
val manga = SManga.create().apply {
|
||||||
url = "/index.php/search"
|
url = "/search"
|
||||||
title = "验证码"
|
title = "验证码"
|
||||||
description = "请点击 WebView 按钮输入验证码,完成后返回重新搜索"
|
description = "请点击 WebView 按钮输入验证码,完成后返回重新搜索"
|
||||||
initialized = true
|
initialized = true
|
||||||
|
@ -75,19 +93,19 @@ open class MCCMSWeb(
|
||||||
return MangasPage(listOf(manga), false)
|
return MangasPage(listOf(manga), false)
|
||||||
}
|
}
|
||||||
val result = parseListing(document)
|
val result = parseListing(document)
|
||||||
if (document.location().contains("search")) {
|
if (config.textSearchOnlyPageOne && document.location().contains("search")) {
|
||||||
return MangasPage(result.mangas, false)
|
return MangasPage(result.mangas, false)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
if (manga.url == "/index.php/search") return Observable.just(manga)
|
if (manga.url == "/search") return Observable.just(manga)
|
||||||
return client.newCall(GET(baseUrl + manga.url, pcHeaders)).asObservableSuccess().map { response ->
|
return super.fetchMangaDetails(manga)
|
||||||
mangaDetailsParse(response)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
return run {
|
return run {
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
|
@ -97,31 +115,41 @@ open class MCCMSWeb(
|
||||||
author = document.selectFirst(Evaluator.Class("name"))!!.text()
|
author = document.selectFirst(Evaluator.Class("name"))!!.text()
|
||||||
genre = document.selectFirst(Evaluator.Class("comic-status"))!!.select(Evaluator.Tag("a")).joinToString { it.ownText() }
|
genre = document.selectFirst(Evaluator.Class("comic-status"))!!.select(Evaluator.Tag("a")).joinToString { it.ownText() }
|
||||||
description = document.selectFirst(Evaluator.Class("intro-total"))!!.text()
|
description = document.selectFirst(Evaluator.Class("intro-total"))!!.text()
|
||||||
}.cleanup()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
if (manga.url == "/index.php/search") return Observable.just(emptyList())
|
if (manga.url == "/search") return Observable.just(emptyList())
|
||||||
return client.newCall(GET(baseUrl + manga.url, pcHeaders)).asObservableSuccess().map { response ->
|
return super.fetchChapterList(manga)
|
||||||
chapterListParse(response)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
return run {
|
return run {
|
||||||
response.asJsoup().selectFirst(Evaluator.Class("chapter__list-box"))!!.children().map {
|
response.asJsoup().selectFirst(Evaluator.Class("chapter__list-box"))!!.children().map {
|
||||||
val link = it.child(0)
|
val link = it.child(0)
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = link.attr("href")
|
url = link.attr("href").removePathPrefix()
|
||||||
name = link.ownText()
|
name = link.ownText()
|
||||||
}
|
}
|
||||||
}.asReversed()
|
}.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 {
|
override fun getFilterList(): FilterList {
|
||||||
fetchGenres()
|
val genreData = config.genreData
|
||||||
return getWebFilters(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-",
|
|
||||||
)
|
|