diff --git a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/Intl.kt b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/Intl.kt new file mode 100644 index 000000000..d5d3e6598 --- /dev/null +++ b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/Intl.kt @@ -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" + } +} diff --git a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt index e368ecfd7..01724d1c1 100644 --- a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt +++ b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt @@ -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 = 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.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 Response.parseAs(): T = use { - json.decodeFromStream>(it.body.byteStream()).data - } + private inline fun Response.parseAs(): T = parseAsRaw>().data override fun getFilterList(): FilterList { val genreData = config.genreData.also { it.fetchGenres(this) } diff --git a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSConfig.kt b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSConfig.kt index e0036b985..68e005ba9 100644 --- a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSConfig.kt +++ b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSConfig.kt @@ -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, diff --git a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSDto.kt b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSDto.kt index 8d873ba10..934025dd8 100644 --- a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSDto.kt +++ b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSDto.kt @@ -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 diff --git a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSFilters.kt b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSFilters.kt index eb20c1faf..3c0a34bc2 100644 --- a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSFilters.kt +++ b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSFilters.kt @@ -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, private val queries: Array) { - 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) diff --git a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSWeb.kt b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSWeb.kt index 481d72208..19744c607 100644 --- a/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSWeb.kt +++ b/lib-multisrc/mccms/src/eu/kanade/tachiyomi/multisrc/mccms/MCCMSWeb.kt @@ -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 { - 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) = 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) diff --git a/src/fr/enlignemanga/build.gradle b/src/fr/enlignemanga/build.gradle new file mode 100644 index 000000000..5809da2d0 --- /dev/null +++ b/src/fr/enlignemanga/build.gradle @@ -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" diff --git a/src/fr/enlignemanga/src/eu/kanade/tachiyomi/extension/fr/enlignemanga/EnLigneManga.kt b/src/fr/enlignemanga/src/eu/kanade/tachiyomi/extension/fr/enlignemanga/EnLigneManga.kt new file mode 100644 index 000000000..5583d95d5 --- /dev/null +++ b/src/fr/enlignemanga/src/eu/kanade/tachiyomi/extension/fr/enlignemanga/EnLigneManga.kt @@ -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") + } +} diff --git a/src/fr/frmanga/build.gradle b/src/fr/frmanga/build.gradle new file mode 100644 index 000000000..c38267edc --- /dev/null +++ b/src/fr/frmanga/build.gradle @@ -0,0 +1,9 @@ +ext { + extName = 'FR Manga' + extClass = '.FRManga' + themePkg = 'mccms' + baseUrl = 'https://www.frmanga.com' + overrideVersionCode = 0 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/fr/frmanga/src/eu/kanade/tachiyomi/extension/fr/frmanga/FRManga.kt b/src/fr/frmanga/src/eu/kanade/tachiyomi/extension/fr/frmanga/FRManga.kt new file mode 100644 index 000000000..397f4bf6a --- /dev/null +++ b/src/fr/frmanga/src/eu/kanade/tachiyomi/extension/fr/frmanga/FRManga.kt @@ -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"), +) diff --git a/src/zh/manhuawu/build.gradle b/src/zh/manhuawu/build.gradle new file mode 100644 index 000000000..fc5784415 --- /dev/null +++ b/src/zh/manhuawu/build.gradle @@ -0,0 +1,9 @@ +ext { + extName = 'Manhuawu' + extClass = '.Manhuawu' + themePkg = 'mccms' + baseUrl = 'https://www.mhua5.com' + overrideVersionCode = 0 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/zh/manhuawu/res/mipmap-hdpi/ic_launcher.png b/src/zh/manhuawu/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..7f9656e60 Binary files /dev/null and b/src/zh/manhuawu/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/zh/manhuawu/res/mipmap-mdpi/ic_launcher.png b/src/zh/manhuawu/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..2937585cb Binary files /dev/null and b/src/zh/manhuawu/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/zh/manhuawu/res/mipmap-xhdpi/ic_launcher.png b/src/zh/manhuawu/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..b4d91407e Binary files /dev/null and b/src/zh/manhuawu/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/zh/manhuawu/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/manhuawu/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..0dcdb8298 Binary files /dev/null and b/src/zh/manhuawu/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/zh/manhuawu/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/manhuawu/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..7f3305374 Binary files /dev/null and b/src/zh/manhuawu/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/zh/manhuawu/src/eu/kanade/tachiyomi/extension/zh/manhuawu/Manhuawu.kt b/src/zh/manhuawu/src/eu/kanade/tachiyomi/extension/zh/manhuawu/Manhuawu.kt new file mode 100644 index 000000000..2718ba16c --- /dev/null +++ b/src/zh/manhuawu/src/eu/kanade/tachiyomi/extension/zh/manhuawu/Manhuawu.kt @@ -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") diff --git a/src/zh/miaoqu/build.gradle b/src/zh/miaoqu/build.gradle new file mode 100644 index 000000000..b1c025c93 --- /dev/null +++ b/src/zh/miaoqu/build.gradle @@ -0,0 +1,9 @@ +ext { + extName = 'Miaoqu Manhua' + extClass = '.Miaoqu' + themePkg = 'mccms' + baseUrl = 'https://www.miaoqumh.org' + overrideVersionCode = 0 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/zh/miaoqu/res/mipmap-hdpi/ic_launcher.png b/src/zh/miaoqu/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..d2e30c2f1 Binary files /dev/null and b/src/zh/miaoqu/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/zh/miaoqu/res/mipmap-mdpi/ic_launcher.png b/src/zh/miaoqu/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..2efbb790e Binary files /dev/null and b/src/zh/miaoqu/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/zh/miaoqu/res/mipmap-xhdpi/ic_launcher.png b/src/zh/miaoqu/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..882f424a1 Binary files /dev/null and b/src/zh/miaoqu/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/zh/miaoqu/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/miaoqu/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b459a45a5 Binary files /dev/null and b/src/zh/miaoqu/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/zh/miaoqu/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/miaoqu/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..dbe4b84e0 Binary files /dev/null and b/src/zh/miaoqu/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/zh/miaoqu/src/eu/kanade/tachiyomi/extension/zh/miaoqu/Miaoqu.kt b/src/zh/miaoqu/src/eu/kanade/tachiyomi/extension/zh/miaoqu/Miaoqu.kt new file mode 100644 index 000000000..990cfd56b --- /dev/null +++ b/src/zh/miaoqu/src/eu/kanade/tachiyomi/extension/zh/miaoqu/Miaoqu.kt @@ -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 { + 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> = + client.newCall(pageListRequest(chapter)).asObservable().map(::pageListParse) + + override fun pageListParse(response: Response): List { + 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>().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) +} diff --git a/src/zh/sixmh/build.gradle b/src/zh/sixmh/build.gradle index 5c517bb89..2dfea4adf 100644 --- a/src/zh/sixmh/build.gradle +++ b/src/zh/sixmh/build.gradle @@ -1,7 +1,9 @@ ext { extName = '6Manhua' extClass = '.SixMH' - extVersionCode = 13 + themePkg = 'mccms' + baseUrl = 'https://www.liumanhua.com' + overrideVersionCode = 7 isNsfw = true } diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Data.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Data.kt deleted file mode 100644 index 60091e4ae..000000000 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Data.kt +++ /dev/null @@ -1,8 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.sixmh - -import kotlinx.serialization.Serializable - -@Serializable -data class Data( - val images: List, -) diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SimpleParsedHttpSource.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SimpleParsedHttpSource.kt deleted file mode 100644 index 2e93443ab..000000000 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SimpleParsedHttpSource.kt +++ /dev/null @@ -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() -} diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMH.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMH.kt index f3d524fcc..de005d3f7 100644 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMH.kt +++ b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMH.kt @@ -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) = chapters // Pages override fun pageListParse(response: Response): List { @@ -67,7 +50,8 @@ class SixMH : SimpleParsedHttpSource() { return images.mapIndexed { index, url -> Page(index, imageUrl = url) } } - override fun pageListParse(document: Document): List = throw UnsupportedOperationException() + @Serializable + private class Data(val images: List) private fun parseStatus(status: String?): Int { return when {