Update MCCMS sources (#9631)
* Add back Manhuawu, closes #1567 * Clean up 6Manhua * Add Miaoqu Manhua, closes #4482 * Add 2 French sources
@ -0,0 +1,77 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.mccms
|
||||||
|
|
||||||
|
object Intl {
|
||||||
|
var lang = "zh"
|
||||||
|
|
||||||
|
val sort
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "排序"
|
||||||
|
else -> "Sort by"
|
||||||
|
}
|
||||||
|
|
||||||
|
val popular
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "热门人气"
|
||||||
|
else -> "Popular"
|
||||||
|
}
|
||||||
|
|
||||||
|
val latest
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "更新时间"
|
||||||
|
else -> "Latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
val score
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "评分"
|
||||||
|
else -> "Score"
|
||||||
|
}
|
||||||
|
|
||||||
|
val status
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "进度"
|
||||||
|
else -> "Status"
|
||||||
|
}
|
||||||
|
|
||||||
|
val all
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "全部"
|
||||||
|
else -> "All"
|
||||||
|
}
|
||||||
|
|
||||||
|
val ongoing
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "连载"
|
||||||
|
else -> "Ongoing"
|
||||||
|
}
|
||||||
|
|
||||||
|
val completed
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "完结"
|
||||||
|
else -> "Completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
val genreWeb
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "标签"
|
||||||
|
else -> "Genre"
|
||||||
|
}
|
||||||
|
|
||||||
|
val genreApi
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "标签(搜索文本时无效)"
|
||||||
|
else -> "Genre (ignored for text search)"
|
||||||
|
}
|
||||||
|
|
||||||
|
val categoryWeb
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "分类筛选(搜索时无效)"
|
||||||
|
else -> "Category filters (ignored for text search)"
|
||||||
|
}
|
||||||
|
|
||||||
|
val tapReset
|
||||||
|
get() = when (lang) {
|
||||||
|
"zh" -> "点击“重置”尝试刷新标签分类"
|
||||||
|
else -> "Tap 'Reset' to load genres"
|
||||||
|
}
|
||||||
|
}
|
@ -9,15 +9,13 @@ 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 kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
import keiyoushi.utils.parseAs as parseAsRaw
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 漫城CMS http://mccms.cn/
|
* 漫城CMS http://mccms.cn/
|
||||||
@ -25,16 +23,26 @@ import java.net.URLEncoder
|
|||||||
open class MCCMS(
|
open class MCCMS(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
override val lang: String = "zh",
|
final override val lang: String = "zh",
|
||||||
private val config: MCCMSConfig = MCCMSConfig(),
|
private val config: MCCMSConfig = MCCMSConfig(),
|
||||||
) : HttpSource() {
|
) : HttpSource() {
|
||||||
override val supportsLatest = true
|
override val supportsLatest get() = true
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
init {
|
||||||
|
Intl.lang = lang
|
||||||
|
}
|
||||||
|
|
||||||
override val client by lazy {
|
override val client by lazy {
|
||||||
network.cloudflareClient.newBuilder()
|
network.cloudflareClient.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
|
.addInterceptor { chain -> // for thumbnail requests
|
||||||
|
var request = chain.request()
|
||||||
|
val referer = request.header("Referer")
|
||||||
|
if (referer != null && !request.url.toString().startsWith(referer)) {
|
||||||
|
request = request.newBuilder().removeHeader("Referer").build()
|
||||||
|
}
|
||||||
|
chain.proceed(request)
|
||||||
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,12 +50,14 @@ open class MCCMS(
|
|||||||
.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
|
||||||
|
|
||||||
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.toSManga() }, list.size >= PAGE_SIZE)
|
return MangasPage(list.map { it.toSManga().cleanup() }, list.size >= PAGE_SIZE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
@ -86,7 +96,7 @@ open class MCCMS(
|
|||||||
return client.newCall(GET(url, headers))
|
return client.newCall(GET(url, headers))
|
||||||
.asObservableSuccess().map { response ->
|
.asObservableSuccess().map { response ->
|
||||||
val list = response.parseAs<List<MangaDto>>()
|
val list = response.parseAs<List<MangaDto>>()
|
||||||
list.first { it.cleanUrl == mangaUrl }.toSManga()
|
list.first { it.cleanUrl == mangaUrl }.toSManga().cleanup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,9 +130,7 @@ open class MCCMS(
|
|||||||
// Don't send referer
|
// 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 = parseAsRaw<ResultDto<T>>().data
|
||||||
json.decodeFromStream<ResultDto<T>>(it.body.byteStream()).data
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
override fun getFilterList(): FilterList {
|
||||||
val genreData = config.genreData.also { it.fetchGenres(this) }
|
val genreData = config.genreData.also { it.fetchGenres(this) }
|
||||||
|
@ -12,6 +12,8 @@ val pcHeaders = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; W
|
|||||||
|
|
||||||
fun String.removePathPrefix() = removePrefix("/index.php")
|
fun String.removePathPrefix() = removePrefix("/index.php")
|
||||||
|
|
||||||
|
fun String.mobileUrl() = replace("//www.", "//m.")
|
||||||
|
|
||||||
open class MCCMSConfig(
|
open class MCCMSConfig(
|
||||||
hasCategoryPage: Boolean = true,
|
hasCategoryPage: Boolean = true,
|
||||||
val textSearchOnlyPageOne: Boolean = false,
|
val textSearchOnlyPageOne: Boolean = false,
|
||||||
|
@ -26,11 +26,11 @@ data class MangaDto(
|
|||||||
title = Entities.unescape(name)
|
title = Entities.unescape(name)
|
||||||
author = Entities.unescape(this@MangaDto.author)
|
author = Entities.unescape(this@MangaDto.author)
|
||||||
description = Entities.unescape(content)
|
description = Entities.unescape(content)
|
||||||
genre = tags.joinToString()
|
genre = Entities.unescape(tags.joinToString())
|
||||||
status = when {
|
status = when (serialize) {
|
||||||
'连' in serialize || isUpdating(addtime) -> SManga.ONGOING
|
"连载", "連載中", "En cours", "OnGoing" -> SManga.ONGOING
|
||||||
'完' in serialize -> SManga.COMPLETED
|
"完结", "已完結", "Terminé", "Complete", "Complété" -> SManga.COMPLETED
|
||||||
else -> SManga.UNKNOWN
|
else -> if (isUpdating(addtime)) SManga.ONGOING else SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
thumbnail_url = "$pic#$id"
|
thumbnail_url = "$pic#$id"
|
||||||
initialized = true
|
initialized = true
|
||||||
|
@ -18,32 +18,31 @@ open class MCCMSFilter(
|
|||||||
val query get() = queries[state]
|
val query get() = queries[state]
|
||||||
}
|
}
|
||||||
|
|
||||||
class SortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES)
|
class SortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES)
|
||||||
class WebSortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES_WEB)
|
class WebSortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES_WEB)
|
||||||
|
|
||||||
private val SORT_NAMES = arrayOf("热门人气", "更新时间", "评分")
|
private val SORT_NAMES get() = arrayOf(Intl.popular, Intl.latest, Intl.score)
|
||||||
private val SORT_QUERIES = arrayOf("order=hits", "order=addtime", "order=score")
|
private val SORT_QUERIES get() = arrayOf("order=hits", "order=addtime", "order=score")
|
||||||
private val SORT_QUERIES_WEB = arrayOf("order/hits", "order/addtime", "order/score")
|
private val SORT_QUERIES_WEB get() = arrayOf("order/hits", "order/addtime", "order/score")
|
||||||
|
|
||||||
class StatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES)
|
class StatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES)
|
||||||
class WebStatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES_WEB)
|
class WebStatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES_WEB)
|
||||||
|
|
||||||
private val STATUS_NAMES = arrayOf("全部", "连载", "完结")
|
private val STATUS_NAMES get() = arrayOf(Intl.all, Intl.ongoing, Intl.completed)
|
||||||
private val STATUS_QUERIES = arrayOf("", "serialize=连载", "serialize=完结")
|
private val STATUS_QUERIES get() = arrayOf("", "serialize=连载", "serialize=完结")
|
||||||
private val STATUS_QUERIES_WEB = arrayOf("", "finish/1", "finish/2")
|
private val STATUS_QUERIES_WEB get() = arrayOf("", "finish/1", "finish/2")
|
||||||
|
|
||||||
class GenreFilter(private val values: Array<String>, private val queries: Array<String>) {
|
class GenreFilter(private val values: Array<String>, private val queries: Array<String>) {
|
||||||
|
|
||||||
private val apiQueries get() = queries.run {
|
private val apiQueries get() = queries.run {
|
||||||
Array(size) { i -> "type[tags]=" + this[i] }
|
Array(size) { i -> "type[tags]=" + this[i] }.apply { this[0] = "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val webQueries get() = queries.run {
|
private val webQueries get() = queries.run {
|
||||||
Array(size) { i -> "tags/" + this[i] }
|
Array(size) { i -> "tags/" + this[i] }.apply { this[0] = "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
val filter get() = MCCMSFilter("标签(搜索文本时无效)", values, apiQueries, isTypeQuery = true)
|
val filter get() = MCCMSFilter(Intl.genreApi, values, apiQueries, isTypeQuery = true)
|
||||||
val webFilter get() = MCCMSFilter("标签", values, webQueries, isTypeQuery = true)
|
val webFilter get() = MCCMSFilter(Intl.genreWeb, values, webQueries, isTypeQuery = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenreData(hasCategoryPage: Boolean) {
|
class GenreData(hasCategoryPage: Boolean) {
|
||||||
@ -55,7 +54,12 @@ class GenreData(hasCategoryPage: Boolean) {
|
|||||||
status = FETCHING
|
status = FETCHING
|
||||||
thread {
|
thread {
|
||||||
try {
|
try {
|
||||||
val response = source.client.newCall(GET("${source.baseUrl}/category/", pcHeaders)).execute()
|
val request = when (source) {
|
||||||
|
// Web sources parse listings whenever possible. They call this function for mobile pages.
|
||||||
|
is MCCMSWeb -> GET("${source.baseUrl.mobileUrl()}/category/", source.headers)
|
||||||
|
else -> GET("${source.baseUrl}/category/", pcHeaders)
|
||||||
|
}
|
||||||
|
val response = source.client.newCall(request).execute()
|
||||||
parseGenres(response.asJsoup(), this)
|
parseGenres(response.asJsoup(), this)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
status = NOT_FETCHED
|
status = NOT_FETCHED
|
||||||
@ -74,7 +78,7 @@ class GenreData(hasCategoryPage: Boolean) {
|
|||||||
|
|
||||||
internal fun parseGenres(document: Document, genreData: GenreData) {
|
internal fun parseGenres(document: Document, genreData: GenreData) {
|
||||||
if (genreData.status == GenreData.FETCHED || genreData.status == GenreData.NO_DATA) return
|
if (genreData.status == GenreData.FETCHED || genreData.status == GenreData.NO_DATA) return
|
||||||
val box = document.selectFirst(".cate-selector, .cy_list_l")
|
val box = document.selectFirst(".cate-selector, .cy_list_l, .ticai, .stui-screen__list")
|
||||||
if (box == null || "/tags/" in document.location()) {
|
if (box == null || "/tags/" in document.location()) {
|
||||||
genreData.status = GenreData.NOT_FETCHED
|
genreData.status = GenreData.NOT_FETCHED
|
||||||
return
|
return
|
||||||
@ -85,7 +89,7 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val result = buildList(genres.size + 1) {
|
val result = buildList(genres.size + 1) {
|
||||||
add(Pair("全部", ""))
|
add(Pair(Intl.all, ""))
|
||||||
genres.mapTo(this) {
|
genres.mapTo(this) {
|
||||||
val tagId = it.attr("href").substringAfterLast('/')
|
val tagId = it.attr("href").substringAfterLast('/')
|
||||||
Pair(it.text(), tagId)
|
Pair(it.text(), tagId)
|
||||||
@ -100,14 +104,14 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
|
|||||||
|
|
||||||
internal fun getFilters(genreData: GenreData): FilterList {
|
internal fun getFilters(genreData: GenreData): FilterList {
|
||||||
val list = buildList(4) {
|
val list = buildList(4) {
|
||||||
add(StatusFilter())
|
if (Intl.lang == "zh") add(StatusFilter())
|
||||||
add(SortFilter())
|
add(SortFilter())
|
||||||
if (genreData.status == GenreData.NO_DATA) return@buildList
|
if (genreData.status == GenreData.NO_DATA) return@buildList
|
||||||
add(Filter.Separator())
|
add(Filter.Separator())
|
||||||
if (genreData.status == GenreData.FETCHED) {
|
if (genreData.status == GenreData.FETCHED) {
|
||||||
add(genreData.genreFilter.filter)
|
add(genreData.genreFilter.filter)
|
||||||
} else {
|
} else {
|
||||||
add(Filter.Header("点击“重置”尝试刷新标签分类"))
|
add(Filter.Header(Intl.tapReset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return FilterList(list)
|
return FilterList(list)
|
||||||
@ -115,13 +119,13 @@ internal fun getFilters(genreData: GenreData): FilterList {
|
|||||||
|
|
||||||
internal fun getWebFilters(genreData: GenreData): FilterList {
|
internal fun getWebFilters(genreData: GenreData): FilterList {
|
||||||
val list = buildList(4) {
|
val list = buildList(4) {
|
||||||
add(Filter.Header("分类筛选(搜索时无效)"))
|
add(Filter.Header(Intl.categoryWeb))
|
||||||
add(WebStatusFilter())
|
add(WebStatusFilter())
|
||||||
add(WebSortFilter())
|
add(WebSortFilter())
|
||||||
when (genreData.status) {
|
when (genreData.status) {
|
||||||
GenreData.NO_DATA -> return@buildList
|
GenreData.NO_DATA -> return@buildList
|
||||||
GenreData.FETCHED -> add(genreData.genreFilter.webFilter)
|
GenreData.FETCHED -> add(genreData.genreFilter.webFilter)
|
||||||
else -> add(Filter.Header("点击“重置”尝试刷新标签分类"))
|
else -> add(Filter.Header(Intl.tapReset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return FilterList(list)
|
return FilterList(list)
|
||||||
|
@ -13,39 +13,45 @@ import okhttp3.Headers
|
|||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okio.IOException
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.select.Evaluator
|
import org.jsoup.select.Evaluator
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class MCCMSWeb(
|
open class MCCMSWeb(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
override val lang: String = "zh",
|
final override val lang: String = "zh",
|
||||||
private val config: MCCMSConfig = MCCMSConfig(),
|
protected val config: MCCMSConfig = MCCMSConfig(),
|
||||||
) : HttpSource() {
|
) : HttpSource() {
|
||||||
override val supportsLatest get() = true
|
override val supportsLatest get() = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
Intl.lang = lang
|
||||||
|
}
|
||||||
|
|
||||||
override val client by lazy {
|
override val client by lazy {
|
||||||
network.cloudflareClient.newBuilder()
|
network.cloudflareClient.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
val response = chain.proceed(chain.request())
|
||||||
|
if (response.request.url.encodedPath == "/err/comic") {
|
||||||
|
throw IOException(response.body.string().substringBefore('\n'))
|
||||||
|
}
|
||||||
|
response
|
||||||
|
}
|
||||||
.build()
|
.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")!!)
|
||||||
|
|
||||||
private fun parseListing(document: Document): MangasPage {
|
open fun parseListing(document: Document): MangasPage {
|
||||||
parseGenres(document, config.genreData)
|
parseGenres(document, config.genreData)
|
||||||
val mangas = document.select(Evaluator.Class("common-comic-item")).map {
|
val mangas = document.select(simpleMangaSelector()).map(::simpleMangaFromElement)
|
||||||
SManga.create().apply {
|
|
||||||
val titleElement = it.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
|
|
||||||
url = titleElement.attr("href").removePathPrefix()
|
|
||||||
title = titleElement.ownText()
|
|
||||||
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val hasNextPage = run { // default pagination
|
val hasNextPage = run { // default pagination
|
||||||
val buttons = document.selectFirst(Evaluator.Id("Pagination"))!!.select(Evaluator.Tag("a"))
|
val buttons = document.selectFirst("#Pagination, .NewPages")!!.select(Evaluator.Tag("a"))
|
||||||
val count = buttons.size
|
val count = buttons.size
|
||||||
// Next page != Last page
|
// Next page != Last page
|
||||||
buttons[count - 1].attr("href") != buttons[count - 2].attr("href")
|
buttons[count - 1].attr("href") != buttons[count - 2].attr("href")
|
||||||
@ -53,6 +59,15 @@ open class MCCMSWeb(
|
|||||||
return MangasPage(mangas, hasNextPage)
|
return MangasPage(mangas, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open fun simpleMangaSelector() = ".common-comic-item"
|
||||||
|
|
||||||
|
open fun simpleMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
val titleElement = element.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
|
||||||
|
url = titleElement.attr("href").removePathPrefix()
|
||||||
|
title = titleElement.ownText()
|
||||||
|
thumbnail_url = element.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/order/hits/page/$page", pcHeaders)
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/order/hits/page/$page", pcHeaders)
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response) = parseListing(response.asJsoup())
|
override fun popularMangaParse(response: Response) = parseListing(response.asJsoup())
|
||||||
@ -104,6 +119,8 @@ open class MCCMSWeb(
|
|||||||
return super.fetchMangaDetails(manga)
|
return super.fetchMangaDetails(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga) = baseUrl.mobileUrl() + manga.url
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
|
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
@ -127,17 +144,23 @@ open class MCCMSWeb(
|
|||||||
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
|
override fun 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 getDescendingChapters(
|
||||||
response.asJsoup().selectFirst(Evaluator.Class("chapter__list-box"))!!.children().map {
|
response.asJsoup().select(chapterListSelector()).map {
|
||||||
val link = it.child(0)
|
val link = it.child(0)
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = link.attr("href").removePathPrefix()
|
url = link.attr("href").removePathPrefix()
|
||||||
name = link.ownText()
|
name = link.text()
|
||||||
}
|
}
|
||||||
}.asReversed()
|
},
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open fun chapterListSelector() = ".chapter__list-box > li"
|
||||||
|
|
||||||
|
open fun getDescendingChapters(chapters: List<SChapter>) = chapters.asReversed()
|
||||||
|
|
||||||
|
override fun getChapterUrl(chapter: SChapter) = baseUrl.mobileUrl() + chapter.url
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request =
|
override fun pageListRequest(chapter: SChapter): Request =
|
||||||
GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders)
|
GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders)
|
||||||
|
|
||||||
|
9
src/fr/enlignemanga/build.gradle
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'En Ligne Manga'
|
||||||
|
extClass = '.EnLigneManga'
|
||||||
|
themePkg = 'mccms'
|
||||||
|
baseUrl = 'https://www.enlignemanga.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
@ -0,0 +1,16 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.fr.enlignemanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
|
||||||
|
class EnLigneManga : MCCMS(
|
||||||
|
"En Ligne Manga",
|
||||||
|
"https://www.enlignemanga.com",
|
||||||
|
"fr",
|
||||||
|
MCCMSConfig(lazyLoadImageAttr = "src"),
|
||||||
|
) {
|
||||||
|
override fun SManga.cleanup() = apply {
|
||||||
|
title = title.substringBeforeLast(" ligne")
|
||||||
|
}
|
||||||
|
}
|
9
src/fr/frmanga/build.gradle
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'FR Manga'
|
||||||
|
extClass = '.FRManga'
|
||||||
|
themePkg = 'mccms'
|
||||||
|
baseUrl = 'https://www.frmanga.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.fr.frmanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig
|
||||||
|
|
||||||
|
class FRManga : MCCMS(
|
||||||
|
"FR Manga",
|
||||||
|
"https://www.frmanga.com",
|
||||||
|
"fr",
|
||||||
|
MCCMSConfig(lazyLoadImageAttr = "src"),
|
||||||
|
)
|
9
src/zh/manhuawu/build.gradle
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Manhuawu'
|
||||||
|
extClass = '.Manhuawu'
|
||||||
|
themePkg = 'mccms'
|
||||||
|
baseUrl = 'https://www.mhua5.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/zh/manhuawu/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/zh/manhuawu/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/zh/manhuawu/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/zh/manhuawu/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
src/zh/manhuawu/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.zh.manhuawu
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
|
||||||
|
|
||||||
|
class Manhuawu : MCCMS("漫画屋", "https://www.mhua5.com")
|
9
src/zh/miaoqu/build.gradle
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Miaoqu Manhua'
|
||||||
|
extClass = '.Miaoqu'
|
||||||
|
themePkg = 'mccms'
|
||||||
|
baseUrl = 'https://www.miaoqumh.org'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/zh/miaoqu/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/zh/miaoqu/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/zh/miaoqu/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/zh/miaoqu/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/zh/miaoqu/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
@ -0,0 +1,120 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.zh.miaoqu
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSWeb
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservable
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import rx.Observable
|
||||||
|
import kotlin.experimental.xor
|
||||||
|
|
||||||
|
// This site shares the same database with 6Manhua (SixMH), but uses manga slug as URL.
|
||||||
|
class Miaoqu : MCCMSWeb("喵趣漫画", "https://www.miaoqumh.org") {
|
||||||
|
override fun parseListing(document: Document): MangasPage {
|
||||||
|
// There's no genre list to parse, so we fetch genres from mobile page in getFilterList()
|
||||||
|
val entries = document.selectFirst("#mangawrap")!!.children().map { element ->
|
||||||
|
SManga.create().apply {
|
||||||
|
val img = element.child(0)
|
||||||
|
thumbnail_url = img.attr("style").substringBetween("background: url(", ')')
|
||||||
|
url = img.attr("href")
|
||||||
|
title = element.selectFirst(".manga-name")!!.text()
|
||||||
|
author = element.selectFirst(".manga-author")?.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val hasNextPage = run {
|
||||||
|
val button = document.selectFirst("#next") ?: return@run false
|
||||||
|
button.attr("href").substringAfterLast('/') != document.location().substringAfterLast('/')
|
||||||
|
}
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
return client.newCall(searchMangaRequest(page, query, filters)).asObservable().map { response ->
|
||||||
|
if (response.code == 404) {
|
||||||
|
response.close()
|
||||||
|
throw Exception("服务器错误,无法搜索")
|
||||||
|
}
|
||||||
|
searchMangaParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use mobile page
|
||||||
|
override fun mangaDetailsRequest(manga: SManga) = GET(getMangaUrl(manga), headers)
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
description = document.selectFirst(".text")!!.text()
|
||||||
|
|
||||||
|
val infobox = document.selectFirst(".infobox")!!
|
||||||
|
title = infobox.selectFirst(".title")!!.text()
|
||||||
|
thumbnail_url = infobox.selectFirst("img")!!.attr("src")
|
||||||
|
|
||||||
|
for (element in infobox.select(".tage")) {
|
||||||
|
val text = element.text()
|
||||||
|
when (text.substring(0, 3)) {
|
||||||
|
"作者:" -> author = text.substring(3).trimStart()
|
||||||
|
"类型:" -> genre = element.select("a").joinToString { it.text() }
|
||||||
|
"更新于" -> description = "$text\n\n$description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) = GET(getMangaUrl(manga), headers)
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "ul.list > li"
|
||||||
|
|
||||||
|
// Might return HTTP 500 with page data
|
||||||
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
||||||
|
client.newCall(pageListRequest(chapter)).asObservable().map(::pageListParse)
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val cid = response.request.url.pathSegments.last().removeSuffix(".html").toInt()
|
||||||
|
val key = when (cid % 10) {
|
||||||
|
0 -> "8-bXd9iN"
|
||||||
|
1 -> "8-RXyjry"
|
||||||
|
2 -> "8-oYvwVy"
|
||||||
|
3 -> "8-4ZY57U"
|
||||||
|
4 -> "8-mbJpU7"
|
||||||
|
5 -> "8-6MM2Ei"
|
||||||
|
6 -> "8-54TiQr"
|
||||||
|
7 -> "8-Ph5xx9"
|
||||||
|
8 -> "8-bYgePR"
|
||||||
|
9 -> "8-Z9A3bW"
|
||||||
|
else -> throw Exception("Illegal cid: $cid")
|
||||||
|
}.encodeToByteArray()
|
||||||
|
check(key.size == 8)
|
||||||
|
val data = response.body.string().substringBetween("var DATA='", '\'')
|
||||||
|
val bytes = Base64.decode(data, Base64.DEFAULT)
|
||||||
|
for (i in bytes.indices) {
|
||||||
|
bytes[i] = bytes[i] xor key[i and 7]
|
||||||
|
}
|
||||||
|
val decrypted = String(Base64.decode(bytes, Base64.DEFAULT))
|
||||||
|
return decrypted.parseAs<List<Image>>().mapIndexed { i, image -> Page(i, imageUrl = image.url) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private class Image(val url: String)
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
config.genreData.fetchGenres(this)
|
||||||
|
return super.getFilterList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.substringBetween(left: String, right: Char): String {
|
||||||
|
val index = indexOf(left)
|
||||||
|
check(index != -1) { "string doesn't match $left[...]$right" }
|
||||||
|
val startIndex = index + left.length
|
||||||
|
val endIndex = indexOf(right, startIndex)
|
||||||
|
check(endIndex != -1) { "string doesn't match $left[...]$right" }
|
||||||
|
return substring(startIndex, endIndex)
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = '6Manhua'
|
extName = '6Manhua'
|
||||||
extClass = '.SixMH'
|
extClass = '.SixMH'
|
||||||
extVersionCode = 13
|
themePkg = 'mccms'
|
||||||
|
baseUrl = 'https://www.liumanhua.com'
|
||||||
|
overrideVersionCode = 7
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.sixmh
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Data(
|
|
||||||
val images: List<String>,
|
|
||||||
)
|
|
@ -1,27 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.sixmh
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
abstract class SimpleParsedHttpSource : ParsedHttpSource() {
|
|
||||||
|
|
||||||
abstract fun simpleMangaSelector(): String
|
|
||||||
abstract fun simpleMangaFromElement(element: Element): SManga
|
|
||||||
abstract fun simpleNextPageSelector(): String?
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = simpleMangaSelector()
|
|
||||||
override fun popularMangaFromElement(element: Element) = simpleMangaFromElement(element)
|
|
||||||
override fun popularMangaNextPageSelector() = simpleNextPageSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = simpleMangaSelector()
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = simpleMangaFromElement(element)
|
|
||||||
override fun latestUpdatesNextPageSelector() = simpleNextPageSelector()
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = simpleMangaSelector()
|
|
||||||
override fun searchMangaFromElement(element: Element) = simpleMangaFromElement(element)
|
|
||||||
override fun searchMangaNextPageSelector() = simpleNextPageSelector()
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
|
||||||
}
|
|
@ -1,35 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.sixmh
|
package eu.kanade.tachiyomi.extension.zh.sixmh
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSWeb
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
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.util.asJsoup
|
||||||
import keiyoushi.utils.parseAs
|
import keiyoushi.utils.parseAs
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import kotlinx.serialization.Serializable
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
class SixMH : SimpleParsedHttpSource() {
|
class SixMH : MCCMSWeb("六漫画", "https://www.liumanhua.com") {
|
||||||
private val paramsRegex = Regex("params = '([A-Za-z0-9+/=]+)'")
|
private val paramsRegex = Regex("params = '([A-Za-z0-9+/=]+)'")
|
||||||
override val versionId get() = 3
|
override val versionId get() = 3
|
||||||
override val name: String = "六漫画"
|
|
||||||
override val lang: String = "zh"
|
|
||||||
override val supportsLatest: Boolean = true
|
|
||||||
override val baseUrl: String = "https://www.liumanhua.com"
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/order/hits/page/$page", headers)
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/category/order/addtime/page/$page", headers)
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/index.php/search".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("key", query)
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun simpleNextPageSelector(): String? = null
|
|
||||||
override fun simpleMangaSelector(): String = "div.cy_list_mh ul"
|
override fun simpleMangaSelector(): String = "div.cy_list_mh ul"
|
||||||
override fun simpleMangaFromElement(element: Element): SManga = SManga.create().apply {
|
override fun simpleMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
title = element.selectFirst("li.title > a")!!.text()
|
title = element.selectFirst("li.title > a")!!.text()
|
||||||
@ -38,7 +22,9 @@ class SixMH : SimpleParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Details
|
// Details
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
val element = document.selectFirst("div.cy_info")!!
|
val element = document.selectFirst("div.cy_info")!!
|
||||||
title = element.selectFirst("div.cy_title")!!.text()
|
title = element.selectFirst("div.cy_title")!!.text()
|
||||||
thumbnail_url = element.selectFirst("div.cy_info_cover > a > img.pic")?.absUrl("src")
|
thumbnail_url = element.selectFirst("div.cy_info_cover > a > img.pic")?.absUrl("src")
|
||||||
@ -47,15 +33,12 @@ class SixMH : SimpleParsedHttpSource() {
|
|||||||
val infoElements = element.select("div.cy_xinxi")
|
val infoElements = element.select("div.cy_xinxi")
|
||||||
author = infoElements[0].selectFirst("span:first-child > a")?.text()
|
author = infoElements[0].selectFirst("span:first-child > a")?.text()
|
||||||
status = parseStatus(infoElements[0].selectFirst("span:nth-child(2)")?.text())
|
status = parseStatus(infoElements[0].selectFirst("span:nth-child(2)")?.text())
|
||||||
genre = infoElements[1].selectFirst("span:first-child > a")?.text()
|
genre = infoElements[1].select("span:first-child > a").joinToString { it.text() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chapters
|
// Chapters
|
||||||
override fun chapterListSelector(): String = "ul#mh-chapter-list-ol-0 li.chapter__item"
|
override fun chapterListSelector(): String = "ul#mh-chapter-list-ol-0 li.chapter__item"
|
||||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
override fun getDescendingChapters(chapters: List<SChapter>) = chapters
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
|
||||||
name = element.selectFirst("a > p")!!.text()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
@ -67,7 +50,8 @@ class SixMH : SimpleParsedHttpSource() {
|
|||||||
return images.mapIndexed { index, url -> Page(index, imageUrl = url) }
|
return images.mapIndexed { index, url -> Page(index, imageUrl = url) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
|
@Serializable
|
||||||
|
private class Data(val images: List<String>)
|
||||||
|
|
||||||
private fun parseStatus(status: String?): Int {
|
private fun parseStatus(status: String?): Int {
|
||||||
return when {
|
return when {
|
||||||
|