diff --git a/multisrc/overrides/mccms/damaomanhua/src/DamaoManhua.kt b/multisrc/overrides/mccms/damaomanhua/src/DamaoManhua.kt new file mode 100644 index 000000000..1fca6a881 --- /dev/null +++ b/multisrc/overrides/mccms/damaomanhua/src/DamaoManhua.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.extension.zh.damaomanhua + +import eu.kanade.tachiyomi.multisrc.mccms.MCCMS +import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga + +class DamaoManhua : MCCMS( + "大猫漫画", + "https://www.hanman.cyou/index.php", + "zh", + MCCMSConfig(useMobilePageList = true), +) { + // Details and chapter pages are broken + override fun getMangaUrl(manga: SManga) = baseUrl + override fun getChapterUrl(chapter: SChapter) = baseUrl +} diff --git a/multisrc/overrides/mccms/didamanhua/src/DidaManhua.kt b/multisrc/overrides/mccms/didamanhua/src/DidaManhua.kt new file mode 100644 index 000000000..7a0ffdb9a --- /dev/null +++ b/multisrc/overrides/mccms/didamanhua/src/DidaManhua.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.extension.zh.didamanhua + +import eu.kanade.tachiyomi.multisrc.mccms.MCCMS +import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga + +class DidaManhua : MCCMS( + "嘀嗒漫画", + "https://www.didamanhua.com/index.php", + "zh", + MCCMSConfig(useMobilePageList = true), +) { + // Details and chapter pages are broken + override fun getMangaUrl(manga: SManga) = baseUrl + override fun getChapterUrl(chapter: SChapter) = baseUrl +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/DecryptInterceptor.kt b/multisrc/overrides/mccms/kuaikuai3/src/DecryptInterceptor.kt similarity index 97% rename from multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/DecryptInterceptor.kt rename to multisrc/overrides/mccms/kuaikuai3/src/DecryptInterceptor.kt index c0021b3dc..b1f5eac76 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/DecryptInterceptor.kt +++ b/multisrc/overrides/mccms/kuaikuai3/src/DecryptInterceptor.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.multisrc.mccms +package eu.kanade.tachiyomi.extension.zh.kuaikuai3 import android.util.Base64 import okhttp3.Interceptor diff --git a/multisrc/overrides/mccms/kuaikuai3/src/MCCMSReduced.kt b/multisrc/overrides/mccms/kuaikuai3/src/MCCMSReduced.kt index 278022b98..303e9fc4b 100644 --- a/multisrc/overrides/mccms/kuaikuai3/src/MCCMSReduced.kt +++ b/multisrc/overrides/mccms/kuaikuai3/src/MCCMSReduced.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.extension.zh.kuaikuai3 -import eu.kanade.tachiyomi.multisrc.mccms.DecryptInterceptor import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.interceptor.rateLimitHost import eu.kanade.tachiyomi.source.model.FilterList diff --git a/multisrc/overrides/mccms/manhuawu/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/mccms/manhuawu/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 7f9656e60..000000000 Binary files a/multisrc/overrides/mccms/manhuawu/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/mccms/manhuawu/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/mccms/manhuawu/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 2937585cb..000000000 Binary files a/multisrc/overrides/mccms/manhuawu/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/mccms/manhuawu/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/mccms/manhuawu/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index b4d91407e..000000000 Binary files a/multisrc/overrides/mccms/manhuawu/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/mccms/manhuawu/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/mccms/manhuawu/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 0dcdb8298..000000000 Binary files a/multisrc/overrides/mccms/manhuawu/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/mccms/manhuawu/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/mccms/manhuawu/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 7f3305374..000000000 Binary files a/multisrc/overrides/mccms/manhuawu/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/mccms/manhuawu/src/Manhuawu.kt b/multisrc/overrides/mccms/manhuawu/src/Manhuawu.kt deleted file mode 100644 index d3138e9aa..000000000 --- a/multisrc/overrides/mccms/manhuawu/src/Manhuawu.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.manhuawu - -import eu.kanade.tachiyomi.multisrc.mccms.MCCMS -import eu.kanade.tachiyomi.multisrc.mccms.MangaDto - -class Manhuawu : MCCMS("漫画屋", "https://www.mhua5.com", hasCategoryPage = true) { - - override fun MangaDto.prepare() = copy(url = "/comic-$id.html") - - override fun getMangaId(url: String) = url.substringAfterLast('-').substringBeforeLast('.') -} diff --git a/multisrc/overrides/mccms/miaoshang/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/mccms/miaoshang/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..d2e30c2f1 Binary files /dev/null and b/multisrc/overrides/mccms/miaoshang/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mccms/miaoshang/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/mccms/miaoshang/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..2efbb790e Binary files /dev/null and b/multisrc/overrides/mccms/miaoshang/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mccms/miaoshang/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/mccms/miaoshang/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..882f424a1 Binary files /dev/null and b/multisrc/overrides/mccms/miaoshang/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mccms/miaoshang/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/mccms/miaoshang/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b459a45a5 Binary files /dev/null and b/multisrc/overrides/mccms/miaoshang/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mccms/miaoshang/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/mccms/miaoshang/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..dbe4b84e0 Binary files /dev/null and b/multisrc/overrides/mccms/miaoshang/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mccms/miaoshang/src/Miaoshang.kt b/multisrc/overrides/mccms/miaoshang/src/Miaoshang.kt new file mode 100644 index 000000000..6a4503e50 --- /dev/null +++ b/multisrc/overrides/mccms/miaoshang/src/Miaoshang.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.extension.zh.miaoshang + +import eu.kanade.tachiyomi.multisrc.mccms.MCCMS +import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +import okhttp3.HttpUrl.Companion.toHttpUrl + +class Miaoshang : MCCMS( + "喵上漫画", + "https://www.miaoshangmanhua.com", + "zh", + MCCMSConfig( + textSearchOnlyPageOne = true, + lazyLoadImageAttr = "data-src", + ), +) { + override val client = network.cloudflareClient.newBuilder() + .rateLimitHost(baseUrl.toHttpUrl(), 2) + .build() +} diff --git a/src/zh/sixmh/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/mccms/sixmh/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/zh/sixmh/res/mipmap-hdpi/ic_launcher.png rename to multisrc/overrides/mccms/sixmh/res/mipmap-hdpi/ic_launcher.png diff --git a/src/zh/sixmh/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/mccms/sixmh/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/zh/sixmh/res/mipmap-mdpi/ic_launcher.png rename to multisrc/overrides/mccms/sixmh/res/mipmap-mdpi/ic_launcher.png diff --git a/src/zh/sixmh/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/mccms/sixmh/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/zh/sixmh/res/mipmap-xhdpi/ic_launcher.png rename to multisrc/overrides/mccms/sixmh/res/mipmap-xhdpi/ic_launcher.png diff --git a/src/zh/sixmh/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/mccms/sixmh/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/zh/sixmh/res/mipmap-xxhdpi/ic_launcher.png rename to multisrc/overrides/mccms/sixmh/res/mipmap-xxhdpi/ic_launcher.png diff --git a/src/zh/sixmh/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/mccms/sixmh/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/zh/sixmh/res/mipmap-xxxhdpi/ic_launcher.png rename to multisrc/overrides/mccms/sixmh/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/multisrc/overrides/mccms/sixmh/src/SixMH.kt b/multisrc/overrides/mccms/sixmh/src/SixMH.kt new file mode 100644 index 000000000..836eb1858 --- /dev/null +++ b/multisrc/overrides/mccms/sixmh/src/SixMH.kt @@ -0,0 +1,24 @@ +package eu.kanade.tachiyomi.extension.zh.sixmh + +import android.app.Application +import android.os.Build +import eu.kanade.tachiyomi.multisrc.mccms.MCCMS +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SixMH : MCCMS("六漫画", "https://www.liumanhua.com") { + + override val versionId get() = 2 + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // Delete old preferences for "6漫画/zh/1" + Injekt.get().deleteSharedPreferences("source_7259486566651312186") + } + } + + override fun getMangaUrl(manga: SManga) = "https://m.liumanhua.com" + manga.url + override fun getChapterUrl(chapter: SChapter) = "https://m.liumanhua.com" + chapter.url +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt index e32403e12..6462ba75a 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.multisrc.mccms -import android.util.Log import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.interceptor.rateLimitHost @@ -10,7 +9,6 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.util.asJsoup import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import okhttp3.Headers @@ -19,7 +17,7 @@ import okhttp3.Request import okhttp3.Response import rx.Observable import uy.kohesive.injekt.injectLazy -import kotlin.concurrent.thread +import java.net.URLEncoder /** * 漫城CMS http://mccms.cn/ @@ -28,7 +26,7 @@ open class MCCMS( override val name: String, override val baseUrl: String, override val lang: String = "zh", - hasCategoryPage: Boolean = false, + private val config: MCCMSConfig = MCCMSConfig(), ) : HttpSource() { override val supportsLatest = true @@ -37,25 +35,19 @@ open class MCCMS( override val client by lazy { network.client.newBuilder() .rateLimitHost(baseUrl.toHttpUrl(), 2) - .addInterceptor(DecryptInterceptor) .build() } - val pcHeaders by lazy { super.headersBuilder().build() } - override fun headersBuilder() = Headers.Builder() .add("User-Agent", System.getProperty("http.agent")!!) .add("Referer", baseUrl) - protected open fun SManga.cleanup(): SManga = this - protected open fun MangaDto.prepare(): MangaDto = this - override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/api/data/comic?page=$page&size=$PAGE_SIZE&order=hits", headers) override fun popularMangaParse(response: Response): MangasPage { val list: List = response.parseAs() - return MangasPage(list.map { it.prepare().toSManga().cleanup() }, list.size >= PAGE_SIZE) + return MangasPage(list.map { it.toSManga() }, list.size >= PAGE_SIZE) } override fun latestUpdatesRequest(page: Int): Request = @@ -68,7 +60,7 @@ open class MCCMS( add("page=$page") add("size=$PAGE_SIZE") val isTextSearch = query.isNotBlank() - if (isTextSearch) add("key=$query") + if (isTextSearch) add("key=" + URLEncoder.encode(query, "UTF-8")) for (filter in filters) if (filter is MCCMSFilter) { if (isTextSearch && filter.isTypeQuery) continue val part = filter.query @@ -84,22 +76,24 @@ open class MCCMS( override fun searchMangaParse(response: Response) = popularMangaParse(response) - // preserve mangaDetailsRequest for WebView + override fun getMangaUrl(manga: SManga) = baseUrl + manga.url + override fun fetchMangaDetails(manga: SManga): Observable { val url = "$baseUrl/api/data/comic".toHttpUrl().newBuilder() .addQueryParameter("key", manga.title) .toString() + val mangaUrl = manga.url return client.newCall(GET(url, headers)) .asObservableSuccess().map { response -> - val list = response.parseAs>().map { it.prepare() } - list.find { it.url == manga.url }!!.toSManga().cleanup() + val list = response.parseAs>() + list.first { it.cleanUrl == mangaUrl }.toSManga() } } override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException() override fun fetchChapterList(manga: SManga): Observable> = Observable.fromCallable { - val id = getMangaId(manga.url) + val id = manga.thumbnail_url!!.substringAfterLast('#', missingDelimiterValue = "").ifEmpty { throw Exception("请刷新漫画") } val dataResponse = client.newCall(GET("$baseUrl/api/data/chapter?mid=$id", headers)).execute() val dataList: List = dataResponse.parseAs() // unordered val dateMap = HashMap(dataList.size * 2) @@ -110,48 +104,28 @@ open class MCCMS( result } - protected open fun getMangaId(url: String) = url.substringAfterLast('/') - override fun chapterListParse(response: Response): List = throw UnsupportedOperationException() override fun pageListRequest(chapter: SChapter): Request = - GET(baseUrl + chapter.url, pcHeaders) + GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders) - protected open val lazyLoadImageAttr = "data-original" + override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url override fun pageListParse(response: Response): List { - val document = response.asJsoup() - return document.select("img[$lazyLoadImageAttr]").mapIndexed { i, element -> - Page(i, imageUrl = element.attr(lazyLoadImageAttr)) - } + return config.pageListParse(response) } override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + // Don't send referer override fun imageRequest(page: Page) = GET(page.imageUrl!!, pcHeaders) private inline fun Response.parseAs(): T = use { json.decodeFromStream>(it.body.byteStream()).data } - val genreData = GenreData(hasCategoryPage) - - fun fetchGenres() { - if (genreData.status != GenreData.NOT_FETCHED) return - genreData.status = GenreData.FETCHING - thread { - try { - val response = client.newCall(GET("$baseUrl/category/", pcHeaders)).execute() - parseGenres(response.asJsoup(), genreData) - } catch (e: Exception) { - genreData.status = GenreData.NOT_FETCHED - Log.e("MCCMS/$name", "failed to fetch genres", e) - } - } - } - override fun getFilterList(): FilterList { - fetchGenres() + val genreData = config.genreData.also { it.fetchGenres(this) } return getFilters(genreData) } } diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSConfig.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSConfig.kt new file mode 100644 index 000000000..e0036b985 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSConfig.kt @@ -0,0 +1,37 @@ +package eu.kanade.tachiyomi.multisrc.mccms + +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Headers +import okhttp3.Response +import org.jsoup.select.Evaluator + +const val PAGE_SIZE = 30 + +val pcHeaders = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0") + +fun String.removePathPrefix() = removePrefix("/index.php") + +open class MCCMSConfig( + hasCategoryPage: Boolean = true, + val textSearchOnlyPageOne: Boolean = false, + val useMobilePageList: Boolean = false, + private val lazyLoadImageAttr: String = "data-original", +) { + val genreData = GenreData(hasCategoryPage) + + fun pageListParse(response: Response): List { + val document = response.asJsoup() + + return if (useMobilePageList) { + val container = document.selectFirst(Evaluator.Class("comic-list"))!! + container.select(Evaluator.Tag("img")).mapIndexed { i, img -> + Page(i, imageUrl = img.attr("src")) + } + } else { + document.select("img[$lazyLoadImageAttr]").mapIndexed { i, img -> + Page(i, imageUrl = img.attr(lazyLoadImageAttr)) + } + } + } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSDto.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSDto.kt index 739982867..8d873ba10 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSDto.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSDto.kt @@ -3,50 +3,54 @@ package eu.kanade.tachiyomi.multisrc.mccms import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import kotlinx.serialization.Serializable +import org.jsoup.nodes.Entities import java.text.SimpleDateFormat import java.util.Locale -internal const val PAGE_SIZE = 30 - @Serializable data class MangaDto( - val id: String, + private val id: String, private val name: String, private val pic: String, private val serialize: String, private val author: String, private val content: String, private val addtime: String, - val url: String, + private val url: String, private val tags: List, ) { + val cleanUrl get() = url.removePathPrefix() + fun toSManga() = SManga.create().apply { - url = this@MangaDto.url - title = name - author = this@MangaDto.author - description = content + url = cleanUrl + title = Entities.unescape(name) + author = Entities.unescape(this@MangaDto.author) + description = Entities.unescape(content) genre = tags.joinToString() - val date = dateFormat.parse(addtime)?.time ?: 0 - val isUpdating = System.currentTimeMillis() - date <= 30L * 24 * 3600 * 1000 // a month status = when { - '连' in serialize || isUpdating -> SManga.ONGOING + '连' in serialize || isUpdating(addtime) -> SManga.ONGOING '完' in serialize -> SManga.COMPLETED else -> SManga.UNKNOWN } - thumbnail_url = pic + thumbnail_url = "$pic#$id" initialized = true } companion object { private val dateFormat by lazy { getDateFormat() } + + private fun isUpdating(dateStr: String): Boolean { + val date = dateFormat.parse(dateStr) ?: return false + return System.currentTimeMillis() - date.time <= 30L * 24 * 3600 * 1000 // a month + } } } @Serializable class ChapterDto(val id: String, private val name: String, private val link: String) { fun toSChapter(date: Long) = SChapter.create().apply { - url = link - name = this@ChapterDto.name + url = link.removePathPrefix() + name = Entities.unescape(this@ChapterDto.name) date_upload = date } } diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSFilters.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSFilters.kt index 357cec884..eb20c1faf 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSFilters.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSFilters.kt @@ -1,8 +1,13 @@ package eu.kanade.tachiyomi.multisrc.mccms +import android.util.Log +import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.asJsoup import org.jsoup.nodes.Document +import kotlin.concurrent.thread open class MCCMSFilter( name: String, @@ -45,6 +50,20 @@ class GenreData(hasCategoryPage: Boolean) { var status = if (hasCategoryPage) NOT_FETCHED else NO_DATA lateinit var genreFilter: GenreFilter + fun fetchGenres(source: HttpSource) { + if (status != NOT_FETCHED) return + status = FETCHING + thread { + try { + val response = source.client.newCall(GET("${source.baseUrl}/category/", pcHeaders)).execute() + parseGenres(response.asJsoup(), this) + } catch (e: Exception) { + status = NOT_FETCHED + Log.e("MCCMS/${source.name}", "failed to fetch genres", e) + } + } + } + companion object { const val NOT_FETCHED = 0 const val FETCHING = 1 @@ -54,7 +73,13 @@ class GenreData(hasCategoryPage: Boolean) { } internal fun parseGenres(document: Document, genreData: GenreData) { - val genres = document.select("a[href^=/category/tags/]") + if (genreData.status == GenreData.FETCHED || genreData.status == GenreData.NO_DATA) return + val box = document.selectFirst(".cate-selector, .cy_list_l") + if (box == null || "/tags/" in document.location()) { + genreData.status = GenreData.NOT_FETCHED + return + } + val genres = box.select("a[href*=/tags/]") if (genres.isEmpty()) { genreData.status = GenreData.NO_DATA return diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSGenerator.kt index 06bc01d82..2cb5f6c72 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSGenerator.kt @@ -17,42 +17,42 @@ class MCCMSGenerator : ThemeSourceGenerator { overrideVersionCode = 0, ), SingleLang( - name = "Manhuawu", - baseUrl = "https://www.mhua5.com", + name = "6Manhua", + baseUrl = "https://www.liumanhua.com", lang = "zh", - className = "Manhuawu", - sourceName = "漫画屋", + className = "SixMH", + sourceName = "六漫画", + overrideVersionCode = 4, + ), + SingleLang( + name = "Miaoshang Manhua", + baseUrl = "https://www.miaoshangmanhua.com", + lang = "zh", + className = "Miaoshang", + sourceName = "喵上漫画", overrideVersionCode = 0, ), - // The following sources are from https://www.yy123.cyou/ and are configured to use MCCMSNsfw - SingleLang( // 103=校园梦精记, same as: www.hmanwang.com, www.quanman8.com, www.lmmh.cc, www.xinmanba.com + // The following sources are from https://www.yy123.cyou/ + SingleLang( // 103=他的那里, same as: www.hmanwang.com, www.lmmh.cc, www.999mh.net name = "Dida Manhua", - baseUrl = "https://www.didamanhua.com", + baseUrl = "https://www.didamanhua.com/index.php", lang = "zh", isNsfw = true, className = "DidaManhua", sourceName = "嘀嗒漫画", - overrideVersionCode = 0, + overrideVersionCode = 1, ), - SingleLang( // 103=脱身之法, same as: www.quanmanba.com, www.999mh.net - name = "Dimanba", - baseUrl = "https://www.dimanba.com", + SingleLang( // 103=青春男女(完结), same as: www.hanman.men + name = "Damao Manhua", + baseUrl = "https://www.hanman.cyou/index.php", lang = "zh", isNsfw = true, - className = "Dimanba", - sourceName = "滴漫吧", + className = "DamaoManhua", + sourceName = "大猫漫画", overrideVersionCode = 0, ), ) - override fun createAll() { - val userDir = System.getProperty("user.dir")!! - sources.forEach { - val themeClass = if (it.isNsfw) "MCCMSNsfw" else themeClass - ThemeSourceGenerator.createGradleProject(it, themePkg, themeClass, baseVersionCode, userDir) - } - } - companion object { @JvmStatic fun main(args: Array) { diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSNsfw.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSNsfw.kt deleted file mode 100644 index f5494e607..000000000 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSNsfw.kt +++ /dev/null @@ -1,36 +0,0 @@ -package eu.kanade.tachiyomi.multisrc.mccms - -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.util.asJsoup -import okhttp3.Request -import okhttp3.Response -import org.jsoup.select.Evaluator - -open class MCCMSNsfw( - name: String, - baseUrl: String, - lang: String = "zh", -) : MCCMSWeb(name, baseUrl, lang, hasCategoryPage = false) { - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = - if (query.isNotBlank()) { - GET("$baseUrl/search/$query/$page", pcHeaders) - } else { - super.searchMangaRequest(page, query, filters) - } - - override fun searchMangaParse(response: Response) = parseListing(response.asJsoup()) - - override fun pageListRequest(chapter: SChapter): Request = - GET(baseUrl + chapter.url, headers) - - override fun pageListParse(response: Response): List { - val container = response.asJsoup().selectFirst(Evaluator.Class("comic-list"))!! - return container.select(Evaluator.Tag("img")).mapIndexed { index, img -> - Page(index, imageUrl = img.attr("src")) - } - } -} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSWeb.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSWeb.kt index 78dc6a174..8974fd175 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSWeb.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMSWeb.kt @@ -1,34 +1,48 @@ package eu.kanade.tachiyomi.multisrc.mccms import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.select.Evaluator import rx.Observable -// https://github.com/tachiyomiorg/tachiyomi-extensions/blob/e0b4fcbce8aa87742da22e7fa60b834313f53533/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mccms/MCCMS.kt open class MCCMSWeb( - name: String, - baseUrl: String, - lang: String = "zh", - hasCategoryPage: Boolean = true, -) : MCCMS(name, baseUrl, lang, hasCategoryPage) { + override val name: String, + override val baseUrl: String, + override val lang: String = "zh", + private val config: MCCMSConfig = MCCMSConfig(), +) : HttpSource() { + override val supportsLatest get() = true - protected open fun parseListing(document: Document): MangasPage { + override val client by lazy { + network.client.newBuilder() + .rateLimitHost(baseUrl.toHttpUrl(), 2) + .build() + } + + override fun headersBuilder() = Headers.Builder() + .add("User-Agent", System.getProperty("http.agent")!!) + + private fun parseListing(document: Document): MangasPage { + parseGenres(document, config.genreData) val mangas = document.select(Evaluator.Class("common-comic-item")).map { SManga.create().apply { val titleElement = it.selectFirst(Evaluator.Class("comic__title"))!!.child(0) - url = titleElement.attr("href") + url = titleElement.attr("href").removePathPrefix() title = titleElement.ownText() thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("data-original") - }.cleanup() + } } val hasNextPage = run { // default pagination val buttons = document.selectFirst(Evaluator.Id("Pagination"))!!.select(Evaluator.Tag("a")) @@ -49,9 +63,13 @@ open class MCCMSWeb( override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = if (query.isNotBlank()) { - val url = "$baseUrl/index.php/search".toHttpUrl().newBuilder() - .addQueryParameter("key", query) - .toString() + val url = if (config.textSearchOnlyPageOne) { + "$baseUrl/search".toHttpUrl().newBuilder() + .addQueryParameter("key", query) + .toString() + } else { + "$baseUrl/search/$query/$page" + } GET(url, pcHeaders) } else { val url = buildString { @@ -67,7 +85,7 @@ open class MCCMSWeb( val document = response.asJsoup() if (document.selectFirst(Evaluator.Id("code-div")) != null) { val manga = SManga.create().apply { - url = "/index.php/search" + url = "/search" title = "验证码" description = "请点击 WebView 按钮输入验证码,完成后返回重新搜索" initialized = true @@ -75,19 +93,19 @@ open class MCCMSWeb( return MangasPage(listOf(manga), false) } val result = parseListing(document) - if (document.location().contains("search")) { + if (config.textSearchOnlyPageOne && document.location().contains("search")) { return MangasPage(result.mangas, false) } return result } override fun fetchMangaDetails(manga: SManga): Observable { - if (manga.url == "/index.php/search") return Observable.just(manga) - return client.newCall(GET(baseUrl + manga.url, pcHeaders)).asObservableSuccess().map { response -> - mangaDetailsParse(response) - } + if (manga.url == "/search") return Observable.just(manga) + return super.fetchMangaDetails(manga) } + override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders) + override fun mangaDetailsParse(response: Response): SManga { return run { SManga.create().apply { @@ -97,31 +115,41 @@ open class MCCMSWeb( author = document.selectFirst(Evaluator.Class("name"))!!.text() genre = document.selectFirst(Evaluator.Class("comic-status"))!!.select(Evaluator.Tag("a")).joinToString { it.ownText() } description = document.selectFirst(Evaluator.Class("intro-total"))!!.text() - }.cleanup() + } } } override fun fetchChapterList(manga: SManga): Observable> { - if (manga.url == "/index.php/search") return Observable.just(emptyList()) - return client.newCall(GET(baseUrl + manga.url, pcHeaders)).asObservableSuccess().map { response -> - chapterListParse(response) - } + if (manga.url == "/search") return Observable.just(emptyList()) + return super.fetchChapterList(manga) } + override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders) + override fun chapterListParse(response: Response): List { return run { response.asJsoup().selectFirst(Evaluator.Class("chapter__list-box"))!!.children().map { val link = it.child(0) SChapter.create().apply { - url = link.attr("href") + url = link.attr("href").removePathPrefix() name = link.ownText() } }.asReversed() } } + override fun pageListRequest(chapter: SChapter): Request = + GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders) + + override fun pageListParse(response: Response) = config.pageListParse(response) + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + // Don't send referer + override fun imageRequest(page: Page) = GET(page.imageUrl!!, pcHeaders) + override fun getFilterList(): FilterList { - fetchGenres() + val genreData = config.genreData return getWebFilters(genreData) } } diff --git a/src/zh/sixmh/build.gradle b/src/zh/sixmh/build.gradle deleted file mode 100644 index dce0b2676..000000000 --- a/src/zh/sixmh/build.gradle +++ /dev/null @@ -1,11 +0,0 @@ -ext { - extName = '6Manhua / Qixi Manhua' - extClass = '.SixMH' - extVersionCode = 9 -} - -apply from: "$rootDir/common.gradle" - -dependencies { - implementation project(':lib:unpacker') -} diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Api.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Api.kt deleted file mode 100644 index a840d6755..000000000 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/Api.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.sixmh - -import eu.kanade.tachiyomi.network.POST -import okhttp3.FormBody -import okhttp3.Headers -import okhttp3.Request - -/** Documentation of unused APIs originally used in `zh.qiximh`. */ -object Api { - - fun getRankRequest(baseUrl: String, headers: Headers, page: Int, type: Int) = - getListingRequest("$baseUrl/rankdata.php", headers, page, type) - - fun getSortRequest(baseUrl: String, headers: Headers, page: Int, type: Int) = - getListingRequest("$baseUrl/sortdata.php", headers, page, type) - - /** @param page 1-5. Website allows 1-10 and contains more items per page. */ - fun getListingRequest(url: String, headers: Headers, page: Int, type: Int): Request { - val body = FormBody.Builder() - .add("page_num", page.toString()) - .add("type", type.toString()) - .build() - return POST(url, headers, body) - } - - fun getSearchRequest(baseUrl: String, headers: Headers, query: String): Request { - val body = FormBody.Builder() - .add("keyword", query) - .build() - return POST("$baseUrl/search.php", headers, body) - } -} diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/QixiDto.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/QixiDto.kt deleted file mode 100644 index e1c4bff08..000000000 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/QixiDto.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.sixmh - -import eu.kanade.tachiyomi.source.model.SChapter -import kotlinx.serialization.Serializable - -@Serializable -class QixiChapterDto(private val id: String, private val name: String) { - fun toSChapter(path: String) = SChapter.create().apply { - url = "$path$id.html" - name = this@QixiChapterDto.name - } -} - -@Serializable -class QixiDataDto(val list: List) - -@Serializable -class QixiResponseDto(val data: QixiDataDto) 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 deleted file mode 100644 index 21a4909a0..000000000 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMH.kt +++ /dev/null @@ -1,222 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.sixmh - -import android.app.Application -import androidx.preference.ListPreference -import androidx.preference.PreferenceScreen -import eu.kanade.tachiyomi.lib.unpacker.Unpacker -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.network.interceptor.rateLimit -import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.util.asJsoup -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromStream -import okhttp3.FormBody -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request -import okhttp3.Response -import org.jsoup.select.Evaluator -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -import java.text.SimpleDateFormat -import java.util.Locale -import kotlin.random.Random - -class SixMH : HttpSource(), ConfigurableSource { - override val name = "6漫画" - override val lang = "zh" - override val supportsLatest = true - - private val isCi = System.getenv("CI") == "true" - override val baseUrl get() = when { - isCi -> MIRRORS.zip(MIRROR_NAMES) { domain, name -> "http://www.$domain#$name" }.joinToString() - else -> _baseUrl - } - - private val mirrorIndex: Int - private val pcUrl: String - private val _baseUrl: String - - init { - val preferences = Injekt.get().getSharedPreferences("source_$id", 0x0000) - val mirrors = MIRRORS - var index = preferences.getString(MIRROR_PREF, "-1")!!.toInt() - if (index !in mirrors.indices) { - index = Random.nextInt(0, mirrors.size) - preferences.edit().putString(MIRROR_PREF, index.toString()).apply() - } - val domain = mirrors[index] - - mirrorIndex = index - pcUrl = "http://www.$domain" - _baseUrl = "http://$domain" - } - - private val json: Json by injectLazy() - - override val client = network.client.newBuilder() - .rateLimit(2) - .build() - - override fun popularMangaRequest(page: Int) = GET("$pcUrl/rank/1-$page.html", headers) - - override fun popularMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - val imgSelector = Evaluator.Tag("img") - val items = document.selectFirst(Evaluator.Class("cy_list_mh"))!!.children().map { - SManga.create().apply { - val link = it.child(1).child(0) - url = link.attr("href") - title = link.ownText() - thumbnail_url = it.selectFirst(imgSelector)!!.attr("src") - } - } - val hasNextPage = document.selectFirst(Evaluator.Class("thisclass"))?.nextElementSibling() != null - return MangasPage(items, hasNextPage) - } - - override fun latestUpdatesRequest(page: Int) = GET("$pcUrl/rank/5-$page.html", headers) - - override fun latestUpdatesParse(response: Response) = popularMangaParse(response) - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - if (query.isNotBlank()) { - val url = pcUrl.toHttpUrl().newBuilder() - .addEncodedPathSegment("search.php") - .addQueryParameter("keyword", query) - .toString() - return GET(url, headers) - } else { - filters.filterIsInstance().firstOrNull()?.run { - return GET("$pcUrl$path$page.html", headers) - } - return popularMangaRequest(page) - } - } - - override fun searchMangaParse(response: Response) = popularMangaParse(response) - - override fun fetchMangaDetails(manga: SManga): Observable { - return client.newCall(GET(pcUrl + manga.url, headers)) - .asObservableSuccess().map(::mangaDetailsParse) - } - - override fun mangaDetailsParse(response: Response): SManga { - val document = response.asJsoup() - val result = SManga.create().apply { - val box = document.selectFirst(Evaluator.Class("cy_info"))!! - val details = box.getElementsByTag("span") - author = details[0].text().removePrefix("作者:") - status = when (details[1].text().removePrefix("状态:").trimStart()) { - "连载中" -> SManga.ONGOING - "已完结" -> SManga.COMPLETED - else -> SManga.UNKNOWN - } - genre = buildList { - add(details[2].ownText().removePrefix("类别:")) - details[3].ownText().removePrefix("标签:").split(Regex("[ -~]+")) - .filterTo(this) { it.isNotEmpty() } - }.joinToString() - description = box.selectFirst(Evaluator.Tag("p"))!!.ownText() - thumbnail_url = box.selectFirst(Evaluator.Tag("img"))!!.run { - attr("data-src").ifEmpty { attr("src") } - } - } - return result - } - - override fun chapterListRequest(manga: SManga) = GET(pcUrl + manga.url, headers) - - override fun chapterListParse(response: Response): List { - val document = response.asJsoup() - - val list = document.selectFirst(Evaluator.Class("cy_plist"))!! - .child(0).children().map { - val element = it.child(0) - SChapter.create().apply { - url = element.attr("href") - name = element.text() - } - } - as ArrayList - - if (mirrorIndex == 0) { // 6Manhua - document.selectFirst(Evaluator.Id("zhankai"))?.let { element -> - val path = '/' + response.request.url.pathSegments[0] + '/' - val body = FormBody.Builder().apply { - addEncoded("id", element.attr("data-id")) - addEncoded("id2", element.attr("data-vid")) - }.build() - client.newCall(POST("$pcUrl/bookchapter/", headers, body)).execute() - .parseAs>().mapTo(list) { it.toSChapter(path) } - } - } else { // Qixi Manhua - if (document.selectFirst(Evaluator.Class("morechp")) != null) { - val id = response.request.url.pathSegments[0] - val path = "/$id/" - val body = FormBody.Builder().addEncoded("id", id).build() - client.newCall(POST("$pcUrl/chapterlist/", headers, body)).execute() - .parseAs().data.list.mapTo(list) { it.toSChapter(path) } - } - } - - if (list.isNotEmpty()) { - document.selectFirst(".cy_zhangjie_top font")?.run { - list[0].date_upload = dateFormat.parse(ownText())?.time ?: 0 - } - } - return list - } - - override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers) - - override fun pageListParse(response: Response): List { - val result = Unpacker.unpack(response.body.string(), "[", "]") - .ifEmpty { return emptyList() } - .replace("\\u0026", "&") - .replace("\\", "") - .removeSurrounding("\"").split("\",\"") - return result.mapIndexed { i, url -> Page(i, imageUrl = url) } - } - - override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() - - private inline fun Response.parseAs(): T = use { - json.decodeFromStream(body.byteStream()) - } - - override fun getFilterList() = FilterList(listOf(PageFilter())) - - override fun setupPreferenceScreen(screen: PreferenceScreen) { - ListPreference(screen.context).apply { - val names = MIRROR_NAMES - - key = MIRROR_PREF - title = "镜像站点(重启生效)" - summary = "%s" - entries = names - entryValues = Array(names.size, Int::toString) - setDefaultValue("0") - }.let(screen::addPreference) - } - - companion object { - - const val MIRROR_PREF = "MIRROR" - - /** Note: mirror index affects [chapterListParse] */ - val MIRRORS get() = arrayOf("sixmanhua.com", "qiximh3.com") - val MIRROR_NAMES get() = arrayOf("6漫画", "七夕漫画") - - private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) } - } -} diff --git a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMHUtils.kt b/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMHUtils.kt deleted file mode 100644 index 9dee2df54..000000000 --- a/src/zh/sixmh/src/eu/kanade/tachiyomi/extension/zh/sixmh/SixMHUtils.kt +++ /dev/null @@ -1,29 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.sixmh - -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.source.model.SChapter -import kotlinx.serialization.Serializable - -@Serializable -class ChapterDto(private val chapterid: String, private val chaptername: String) { - fun toSChapter(path: String) = SChapter.create().apply { - url = "$path$chapterid.html" - name = chaptername - } -} - -internal class PageFilter : Filter.Select("排行榜/分类", PAGE_NAMES) { - val path get() = PAGE_PATHS[state] -} - -private val PAGE_NAMES = arrayOf( - "人气榜", "周读榜", "月读榜", "火爆榜", "更新榜", "新漫榜", - "冒险热血", "武侠格斗", "科幻魔幻", "侦探推理", "耽美爱情", "生活漫画", - "推荐漫画", "完结漫画", "连载漫画", -) - -private val PAGE_PATHS = arrayOf( - "/rank/1-", "/rank/2-", "/rank/3-", "/rank/4-", "/rank/5-", "/rank/6-", - "/sort/1-", "/sort/2-", "/sort/3-", "/sort/4-", "/sort/5-", "/sort/6-", - "/sort/11-", "/sort/12-", "/sort/13-", -)