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.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import keiyoushi.utils.parseAs as parseAsRaw
|
||||
|
||||
/**
|
||||
* 漫城CMS http://mccms.cn/
|
||||
@ -25,16 +23,26 @@ import java.net.URLEncoder
|
||||
open class MCCMS(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String = "zh",
|
||||
final override val lang: String = "zh",
|
||||
private val config: MCCMSConfig = MCCMSConfig(),
|
||||
) : HttpSource() {
|
||||
override val supportsLatest = true
|
||||
override val supportsLatest get() = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
init {
|
||||
Intl.lang = lang
|
||||
}
|
||||
|
||||
override val client by lazy {
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.addInterceptor { chain -> // for thumbnail requests
|
||||
var request = chain.request()
|
||||
val referer = request.header("Referer")
|
||||
if (referer != null && !request.url.toString().startsWith(referer)) {
|
||||
request = request.newBuilder().removeHeader("Referer").build()
|
||||
}
|
||||
chain.proceed(request)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
@ -42,12 +50,14 @@ open class MCCMS(
|
||||
.add("User-Agent", System.getProperty("http.agent")!!)
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
protected open fun SManga.cleanup(): SManga = this
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/api/data/comic?page=$page&size=$PAGE_SIZE&order=hits", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val list: List<MangaDto> = response.parseAs()
|
||||
return MangasPage(list.map { it.toSManga() }, list.size >= PAGE_SIZE)
|
||||
return MangasPage(list.map { it.toSManga().cleanup() }, list.size >= PAGE_SIZE)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
@ -86,7 +96,7 @@ open class MCCMS(
|
||||
return client.newCall(GET(url, headers))
|
||||
.asObservableSuccess().map { response ->
|
||||
val list = response.parseAs<List<MangaDto>>()
|
||||
list.first { it.cleanUrl == mangaUrl }.toSManga()
|
||||
list.first { it.cleanUrl == mangaUrl }.toSManga().cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,9 +130,7 @@ open class MCCMS(
|
||||
// Don't send referer
|
||||
override fun imageRequest(page: Page) = GET(page.imageUrl!!, pcHeaders)
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
json.decodeFromStream<ResultDto<T>>(it.body.byteStream()).data
|
||||
}
|
||||
private inline fun <reified T> Response.parseAs(): T = parseAsRaw<ResultDto<T>>().data
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val genreData = config.genreData.also { it.fetchGenres(this) }
|
||||
|
@ -12,6 +12,8 @@ val pcHeaders = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; W
|
||||
|
||||
fun String.removePathPrefix() = removePrefix("/index.php")
|
||||
|
||||
fun String.mobileUrl() = replace("//www.", "//m.")
|
||||
|
||||
open class MCCMSConfig(
|
||||
hasCategoryPage: Boolean = true,
|
||||
val textSearchOnlyPageOne: Boolean = false,
|
||||
|
@ -26,11 +26,11 @@ data class MangaDto(
|
||||
title = Entities.unescape(name)
|
||||
author = Entities.unescape(this@MangaDto.author)
|
||||
description = Entities.unescape(content)
|
||||
genre = tags.joinToString()
|
||||
status = when {
|
||||
'连' in serialize || isUpdating(addtime) -> SManga.ONGOING
|
||||
'完' in serialize -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
genre = Entities.unescape(tags.joinToString())
|
||||
status = when (serialize) {
|
||||
"连载", "連載中", "En cours", "OnGoing" -> SManga.ONGOING
|
||||
"完结", "已完結", "Terminé", "Complete", "Complété" -> SManga.COMPLETED
|
||||
else -> if (isUpdating(addtime)) SManga.ONGOING else SManga.UNKNOWN
|
||||
}
|
||||
thumbnail_url = "$pic#$id"
|
||||
initialized = true
|
||||
|
@ -18,32 +18,31 @@ open class MCCMSFilter(
|
||||
val query get() = queries[state]
|
||||
}
|
||||
|
||||
class SortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES)
|
||||
class WebSortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES_WEB)
|
||||
class SortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES)
|
||||
class WebSortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES_WEB)
|
||||
|
||||
private val SORT_NAMES = arrayOf("热门人气", "更新时间", "评分")
|
||||
private val SORT_QUERIES = arrayOf("order=hits", "order=addtime", "order=score")
|
||||
private val SORT_QUERIES_WEB = arrayOf("order/hits", "order/addtime", "order/score")
|
||||
private val SORT_NAMES get() = arrayOf(Intl.popular, Intl.latest, Intl.score)
|
||||
private val SORT_QUERIES get() = arrayOf("order=hits", "order=addtime", "order=score")
|
||||
private val SORT_QUERIES_WEB get() = arrayOf("order/hits", "order/addtime", "order/score")
|
||||
|
||||
class StatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES)
|
||||
class WebStatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES_WEB)
|
||||
class StatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES)
|
||||
class WebStatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES_WEB)
|
||||
|
||||
private val STATUS_NAMES = arrayOf("全部", "连载", "完结")
|
||||
private val STATUS_QUERIES = arrayOf("", "serialize=连载", "serialize=完结")
|
||||
private val STATUS_QUERIES_WEB = arrayOf("", "finish/1", "finish/2")
|
||||
private val STATUS_NAMES get() = arrayOf(Intl.all, Intl.ongoing, Intl.completed)
|
||||
private val STATUS_QUERIES get() = arrayOf("", "serialize=连载", "serialize=完结")
|
||||
private val STATUS_QUERIES_WEB get() = arrayOf("", "finish/1", "finish/2")
|
||||
|
||||
class GenreFilter(private val values: Array<String>, private val queries: Array<String>) {
|
||||
|
||||
private val apiQueries get() = queries.run {
|
||||
Array(size) { i -> "type[tags]=" + this[i] }
|
||||
Array(size) { i -> "type[tags]=" + this[i] }.apply { this[0] = "" }
|
||||
}
|
||||
|
||||
private val webQueries get() = queries.run {
|
||||
Array(size) { i -> "tags/" + this[i] }
|
||||
Array(size) { i -> "tags/" + this[i] }.apply { this[0] = "" }
|
||||
}
|
||||
|
||||
val filter get() = MCCMSFilter("标签(搜索文本时无效)", values, apiQueries, isTypeQuery = true)
|
||||
val webFilter get() = MCCMSFilter("标签", values, webQueries, isTypeQuery = true)
|
||||
val filter get() = MCCMSFilter(Intl.genreApi, values, apiQueries, isTypeQuery = true)
|
||||
val webFilter get() = MCCMSFilter(Intl.genreWeb, values, webQueries, isTypeQuery = true)
|
||||
}
|
||||
|
||||
class GenreData(hasCategoryPage: Boolean) {
|
||||
@ -55,7 +54,12 @@ class GenreData(hasCategoryPage: Boolean) {
|
||||
status = FETCHING
|
||||
thread {
|
||||
try {
|
||||
val response = source.client.newCall(GET("${source.baseUrl}/category/", pcHeaders)).execute()
|
||||
val request = when (source) {
|
||||
// Web sources parse listings whenever possible. They call this function for mobile pages.
|
||||
is MCCMSWeb -> GET("${source.baseUrl.mobileUrl()}/category/", source.headers)
|
||||
else -> GET("${source.baseUrl}/category/", pcHeaders)
|
||||
}
|
||||
val response = source.client.newCall(request).execute()
|
||||
parseGenres(response.asJsoup(), this)
|
||||
} catch (e: Exception) {
|
||||
status = NOT_FETCHED
|
||||
@ -74,7 +78,7 @@ class GenreData(hasCategoryPage: Boolean) {
|
||||
|
||||
internal fun parseGenres(document: Document, genreData: GenreData) {
|
||||
if (genreData.status == GenreData.FETCHED || genreData.status == GenreData.NO_DATA) return
|
||||
val box = document.selectFirst(".cate-selector, .cy_list_l")
|
||||
val box = document.selectFirst(".cate-selector, .cy_list_l, .ticai, .stui-screen__list")
|
||||
if (box == null || "/tags/" in document.location()) {
|
||||
genreData.status = GenreData.NOT_FETCHED
|
||||
return
|
||||
@ -85,7 +89,7 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
|
||||
return
|
||||
}
|
||||
val result = buildList(genres.size + 1) {
|
||||
add(Pair("全部", ""))
|
||||
add(Pair(Intl.all, ""))
|
||||
genres.mapTo(this) {
|
||||
val tagId = it.attr("href").substringAfterLast('/')
|
||||
Pair(it.text(), tagId)
|
||||
@ -100,14 +104,14 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
|
||||
|
||||
internal fun getFilters(genreData: GenreData): FilterList {
|
||||
val list = buildList(4) {
|
||||
add(StatusFilter())
|
||||
if (Intl.lang == "zh") add(StatusFilter())
|
||||
add(SortFilter())
|
||||
if (genreData.status == GenreData.NO_DATA) return@buildList
|
||||
add(Filter.Separator())
|
||||
if (genreData.status == GenreData.FETCHED) {
|
||||
add(genreData.genreFilter.filter)
|
||||
} else {
|
||||
add(Filter.Header("点击“重置”尝试刷新标签分类"))
|
||||
add(Filter.Header(Intl.tapReset))
|
||||
}
|
||||
}
|
||||
return FilterList(list)
|
||||
@ -115,13 +119,13 @@ internal fun getFilters(genreData: GenreData): FilterList {
|
||||
|
||||
internal fun getWebFilters(genreData: GenreData): FilterList {
|
||||
val list = buildList(4) {
|
||||
add(Filter.Header("分类筛选(搜索时无效)"))
|
||||
add(Filter.Header(Intl.categoryWeb))
|
||||
add(WebStatusFilter())
|
||||
add(WebSortFilter())
|
||||
when (genreData.status) {
|
||||
GenreData.NO_DATA -> return@buildList
|
||||
GenreData.FETCHED -> add(genreData.genreFilter.webFilter)
|
||||
else -> add(Filter.Header("点击“重置”尝试刷新标签分类"))
|
||||
else -> add(Filter.Header(Intl.tapReset))
|
||||
}
|
||||
}
|
||||
return FilterList(list)
|
||||
|
@ -13,39 +13,45 @@ import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
|
||||
open class MCCMSWeb(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String = "zh",
|
||||
private val config: MCCMSConfig = MCCMSConfig(),
|
||||
final override val lang: String = "zh",
|
||||
protected val config: MCCMSConfig = MCCMSConfig(),
|
||||
) : HttpSource() {
|
||||
override val supportsLatest get() = true
|
||||
|
||||
init {
|
||||
Intl.lang = lang
|
||||
}
|
||||
|
||||
override val client by lazy {
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.addInterceptor { chain ->
|
||||
val response = chain.proceed(chain.request())
|
||||
if (response.request.url.encodedPath == "/err/comic") {
|
||||
throw IOException(response.body.string().substringBefore('\n'))
|
||||
}
|
||||
response
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun headersBuilder() = Headers.Builder()
|
||||
.add("User-Agent", System.getProperty("http.agent")!!)
|
||||
|
||||
private fun parseListing(document: Document): MangasPage {
|
||||
open fun parseListing(document: Document): MangasPage {
|
||||
parseGenres(document, config.genreData)
|
||||
val mangas = document.select(Evaluator.Class("common-comic-item")).map {
|
||||
SManga.create().apply {
|
||||
val titleElement = it.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
|
||||
url = titleElement.attr("href").removePathPrefix()
|
||||
title = titleElement.ownText()
|
||||
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
|
||||
}
|
||||
}
|
||||
val mangas = document.select(simpleMangaSelector()).map(::simpleMangaFromElement)
|
||||
val hasNextPage = run { // default pagination
|
||||
val buttons = document.selectFirst(Evaluator.Id("Pagination"))!!.select(Evaluator.Tag("a"))
|
||||
val buttons = document.selectFirst("#Pagination, .NewPages")!!.select(Evaluator.Tag("a"))
|
||||
val count = buttons.size
|
||||
// Next page != Last page
|
||||
buttons[count - 1].attr("href") != buttons[count - 2].attr("href")
|
||||
@ -53,6 +59,15 @@ open class MCCMSWeb(
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
open fun simpleMangaSelector() = ".common-comic-item"
|
||||
|
||||
open fun simpleMangaFromElement(element: Element) = SManga.create().apply {
|
||||
val titleElement = element.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
|
||||
url = titleElement.attr("href").removePathPrefix()
|
||||
title = titleElement.ownText()
|
||||
thumbnail_url = element.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/order/hits/page/$page", pcHeaders)
|
||||
|
||||
override fun popularMangaParse(response: Response) = parseListing(response.asJsoup())
|
||||
@ -104,6 +119,8 @@ open class MCCMSWeb(
|
||||
return super.fetchMangaDetails(manga)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = baseUrl.mobileUrl() + manga.url
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
@ -127,17 +144,23 @@ open class MCCMSWeb(
|
||||
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return run {
|
||||
response.asJsoup().selectFirst(Evaluator.Class("chapter__list-box"))!!.children().map {
|
||||
return getDescendingChapters(
|
||||
response.asJsoup().select(chapterListSelector()).map {
|
||||
val link = it.child(0)
|
||||
SChapter.create().apply {
|
||||
url = link.attr("href").removePathPrefix()
|
||||
name = link.ownText()
|
||||
name = link.text()
|
||||
}
|
||||
}.asReversed()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
open fun chapterListSelector() = ".chapter__list-box > li"
|
||||
|
||||
open fun getDescendingChapters(chapters: List<SChapter>) = chapters.asReversed()
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter) = baseUrl.mobileUrl() + chapter.url
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request =
|
||||
GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders)
|
||||
|
||||
|
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 {
|
||||
extName = '6Manhua'
|
||||
extClass = '.SixMH'
|
||||
extVersionCode = 13
|
||||
themePkg = 'mccms'
|
||||
baseUrl = 'https://www.liumanhua.com'
|
||||
overrideVersionCode = 7
|
||||
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
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSWeb
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.parseAs
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class SixMH : SimpleParsedHttpSource() {
|
||||
class SixMH : MCCMSWeb("六漫画", "https://www.liumanhua.com") {
|
||||
private val paramsRegex = Regex("params = '([A-Za-z0-9+/=]+)'")
|
||||
override val versionId get() = 3
|
||||
override val name: String = "六漫画"
|
||||
override val lang: String = "zh"
|
||||
override val supportsLatest: Boolean = true
|
||||
override val baseUrl: String = "https://www.liumanhua.com"
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/order/hits/page/$page", headers)
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/category/order/addtime/page/$page", headers)
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/index.php/search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("key", query)
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun simpleNextPageSelector(): String? = null
|
||||
override fun simpleMangaSelector(): String = "div.cy_list_mh ul"
|
||||
override fun simpleMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.selectFirst("li.title > a")!!.text()
|
||||
@ -38,7 +22,9 @@ class SixMH : SimpleParsedHttpSource() {
|
||||
}
|
||||
|
||||
// Details
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val element = document.selectFirst("div.cy_info")!!
|
||||
title = element.selectFirst("div.cy_title")!!.text()
|
||||
thumbnail_url = element.selectFirst("div.cy_info_cover > a > img.pic")?.absUrl("src")
|
||||
@ -47,15 +33,12 @@ class SixMH : SimpleParsedHttpSource() {
|
||||
val infoElements = element.select("div.cy_xinxi")
|
||||
author = infoElements[0].selectFirst("span:first-child > a")?.text()
|
||||
status = parseStatus(infoElements[0].selectFirst("span:nth-child(2)")?.text())
|
||||
genre = infoElements[1].selectFirst("span:first-child > a")?.text()
|
||||
genre = infoElements[1].select("span:first-child > a").joinToString { it.text() }
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override fun chapterListSelector(): String = "ul#mh-chapter-list-ol-0 li.chapter__item"
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
name = element.selectFirst("a > p")!!.text()
|
||||
}
|
||||
override fun getDescendingChapters(chapters: List<SChapter>) = chapters
|
||||
|
||||
// Pages
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
@ -67,7 +50,8 @@ class SixMH : SimpleParsedHttpSource() {
|
||||
return images.mapIndexed { index, url -> Page(index, imageUrl = url) }
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
|
||||
@Serializable
|
||||
private class Data(val images: List<String>)
|
||||
|
||||
private fun parseStatus(status: String?): Int {
|
||||
return when {
|
||||
|