MCCMS: update sources (#956)
							
								
								
									
										17
									
								
								multisrc/overrides/mccms/damaomanhua/src/DamaoManhua.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								multisrc/overrides/mccms/didamanhua/src/DidaManhua.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,17 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.didamanhua
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
 | 
			
		||||
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
 | 
			
		||||
class DidaManhua : MCCMS(
 | 
			
		||||
    "嘀嗒漫画",
 | 
			
		||||
    "https://www.didamanhua.com/index.php",
 | 
			
		||||
    "zh",
 | 
			
		||||
    MCCMSConfig(useMobilePageList = true),
 | 
			
		||||
) {
 | 
			
		||||
    // Details and chapter pages are broken
 | 
			
		||||
    override fun getMangaUrl(manga: SManga) = baseUrl
 | 
			
		||||
    override fun getChapterUrl(chapter: SChapter) = baseUrl
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
package eu.kanade.tachiyomi.multisrc.mccms
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.kuaikuai3
 | 
			
		||||
 | 
			
		||||
import android.util.Base64
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 2.6 KiB  | 
| 
		 Before Width: | Height: | Size: 1.5 KiB  | 
| 
		 Before Width: | Height: | Size: 3.2 KiB  | 
| 
		 Before Width: | Height: | Size: 6.0 KiB  | 
| 
		 Before Width: | Height: | Size: 7.8 KiB  | 
@ -1,11 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.manhuawu
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
 | 
			
		||||
import eu.kanade.tachiyomi.multisrc.mccms.MangaDto
 | 
			
		||||
 | 
			
		||||
class Manhuawu : MCCMS("漫画屋", "https://www.mhua5.com", hasCategoryPage = true) {
 | 
			
		||||
 | 
			
		||||
    override fun MangaDto.prepare() = copy(url = "/comic-$id.html")
 | 
			
		||||
 | 
			
		||||
    override fun getMangaId(url: String) = url.substringAfterLast('-').substringBeforeLast('.')
 | 
			
		||||
}
 | 
			
		||||
| 
		 After Width: | Height: | Size: 2.4 KiB  | 
| 
		 After Width: | Height: | Size: 1.4 KiB  | 
| 
		 After Width: | Height: | Size: 3.1 KiB  | 
| 
		 After Width: | Height: | Size: 5.6 KiB  | 
| 
		 After Width: | Height: | Size: 7.3 KiB  | 
							
								
								
									
										20
									
								
								multisrc/overrides/mccms/miaoshang/src/Miaoshang.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,20 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.miaoshang
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
 | 
			
		||||
import eu.kanade.tachiyomi.multisrc.mccms.MCCMSConfig
 | 
			
		||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
			
		||||
 | 
			
		||||
class Miaoshang : MCCMS(
 | 
			
		||||
    "喵上漫画",
 | 
			
		||||
    "https://www.miaoshangmanhua.com",
 | 
			
		||||
    "zh",
 | 
			
		||||
    MCCMSConfig(
 | 
			
		||||
        textSearchOnlyPageOne = true,
 | 
			
		||||
        lazyLoadImageAttr = "data-src",
 | 
			
		||||
    ),
 | 
			
		||||
) {
 | 
			
		||||
    override val client = network.cloudflareClient.newBuilder()
 | 
			
		||||
        .rateLimitHost(baseUrl.toHttpUrl(), 2)
 | 
			
		||||
        .build()
 | 
			
		||||
}
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB  | 
| 
		 Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB  | 
| 
		 Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB  | 
| 
		 Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB  | 
| 
		 Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB  | 
							
								
								
									
										24
									
								
								multisrc/overrides/mccms/sixmh/src/SixMH.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,24 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.sixmh
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import eu.kanade.tachiyomi.multisrc.mccms.MCCMS
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
class SixMH : MCCMS("六漫画", "https://www.liumanhua.com") {
 | 
			
		||||
 | 
			
		||||
    override val versionId get() = 2
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 | 
			
		||||
            // Delete old preferences for "6漫画/zh/1"
 | 
			
		||||
            Injekt.get<Application>().deleteSharedPreferences("source_7259486566651312186")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getMangaUrl(manga: SManga) = "https://m.liumanhua.com" + manga.url
 | 
			
		||||
    override fun getChapterUrl(chapter: SChapter) = "https://m.liumanhua.com" + chapter.url
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.multisrc.mccms
 | 
			
		||||
 | 
			
		||||
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<MangaDto> = response.parseAs()
 | 
			
		||||
        return MangasPage(list.map { it.prepare().toSManga().cleanup() }, list.size >= PAGE_SIZE)
 | 
			
		||||
        return MangasPage(list.map { it.toSManga() }, list.size >= PAGE_SIZE)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int): Request =
 | 
			
		||||
@ -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<SManga> {
 | 
			
		||||
        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<List<MangaDto>>().map { it.prepare() }
 | 
			
		||||
                list.find { it.url == manga.url }!!.toSManga().cleanup()
 | 
			
		||||
                val list = response.parseAs<List<MangaDto>>()
 | 
			
		||||
                list.first { it.cleanUrl == mangaUrl }.toSManga()
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
 | 
			
		||||
 | 
			
		||||
    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
 | 
			
		||||
        val id = getMangaId(manga.url)
 | 
			
		||||
        val id = manga.thumbnail_url!!.substringAfterLast('#', missingDelimiterValue = "").ifEmpty { throw Exception("请刷新漫画") }
 | 
			
		||||
        val dataResponse = client.newCall(GET("$baseUrl/api/data/chapter?mid=$id", headers)).execute()
 | 
			
		||||
        val dataList: List<ChapterDataDto> = dataResponse.parseAs() // unordered
 | 
			
		||||
        val dateMap = HashMap<Int, Long>(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<SChapter> = 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<Page> {
 | 
			
		||||
        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 <reified T> Response.parseAs(): T = use {
 | 
			
		||||
        json.decodeFromStream<ResultDto<T>>(it.body.byteStream()).data
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val genreData = GenreData(hasCategoryPage)
 | 
			
		||||
 | 
			
		||||
    fun fetchGenres() {
 | 
			
		||||
        if (genreData.status != GenreData.NOT_FETCHED) return
 | 
			
		||||
        genreData.status = GenreData.FETCHING
 | 
			
		||||
        thread {
 | 
			
		||||
            try {
 | 
			
		||||
                val response = client.newCall(GET("$baseUrl/category/", pcHeaders)).execute()
 | 
			
		||||
                parseGenres(response.asJsoup(), genreData)
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                genreData.status = GenreData.NOT_FETCHED
 | 
			
		||||
                Log.e("MCCMS/$name", "failed to fetch genres", e)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getFilterList(): FilterList {
 | 
			
		||||
        fetchGenres()
 | 
			
		||||
        val genreData = config.genreData.also { it.fetchGenres(this) }
 | 
			
		||||
        return getFilters(genreData)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,37 @@
 | 
			
		||||
package eu.kanade.tachiyomi.multisrc.mccms
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import okhttp3.Headers
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import org.jsoup.select.Evaluator
 | 
			
		||||
 | 
			
		||||
const val PAGE_SIZE = 30
 | 
			
		||||
 | 
			
		||||
val pcHeaders = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0")
 | 
			
		||||
 | 
			
		||||
fun String.removePathPrefix() = removePrefix("/index.php")
 | 
			
		||||
 | 
			
		||||
open class MCCMSConfig(
 | 
			
		||||
    hasCategoryPage: Boolean = true,
 | 
			
		||||
    val textSearchOnlyPageOne: Boolean = false,
 | 
			
		||||
    val useMobilePageList: Boolean = false,
 | 
			
		||||
    private val lazyLoadImageAttr: String = "data-original",
 | 
			
		||||
) {
 | 
			
		||||
    val genreData = GenreData(hasCategoryPage)
 | 
			
		||||
 | 
			
		||||
    fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
 | 
			
		||||
        return if (useMobilePageList) {
 | 
			
		||||
            val container = document.selectFirst(Evaluator.Class("comic-list"))!!
 | 
			
		||||
            container.select(Evaluator.Tag("img")).mapIndexed { i, img ->
 | 
			
		||||
                Page(i, imageUrl = img.attr("src"))
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            document.select("img[$lazyLoadImageAttr]").mapIndexed { i, img ->
 | 
			
		||||
                Page(i, imageUrl = img.attr(lazyLoadImageAttr))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -3,50 +3,54 @@ package eu.kanade.tachiyomi.multisrc.mccms
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.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<String>,
 | 
			
		||||
) {
 | 
			
		||||
    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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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<String>) {
 | 
			
		||||
 | 
			
		||||
@ -1,36 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.multisrc.mccms
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import org.jsoup.select.Evaluator
 | 
			
		||||
 | 
			
		||||
open class MCCMSNsfw(
 | 
			
		||||
    name: String,
 | 
			
		||||
    baseUrl: String,
 | 
			
		||||
    lang: String = "zh",
 | 
			
		||||
) : MCCMSWeb(name, baseUrl, lang, hasCategoryPage = false) {
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
 | 
			
		||||
        if (query.isNotBlank()) {
 | 
			
		||||
            GET("$baseUrl/search/$query/$page", pcHeaders)
 | 
			
		||||
        } else {
 | 
			
		||||
            super.searchMangaRequest(page, query, filters)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaParse(response: Response) = parseListing(response.asJsoup())
 | 
			
		||||
 | 
			
		||||
    override fun pageListRequest(chapter: SChapter): Request =
 | 
			
		||||
        GET(baseUrl + chapter.url, headers)
 | 
			
		||||
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val container = response.asJsoup().selectFirst(Evaluator.Class("comic-list"))!!
 | 
			
		||||
        return container.select(Evaluator.Tag("img")).mapIndexed { index, img ->
 | 
			
		||||
            Page(index, imageUrl = img.attr("src"))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,34 +1,48 @@
 | 
			
		||||
package eu.kanade.tachiyomi.multisrc.mccms
 | 
			
		||||
 | 
			
		||||
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<SManga> {
 | 
			
		||||
        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<List<SChapter>> {
 | 
			
		||||
        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<SChapter> {
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
ext {
 | 
			
		||||
    extName = '6Manhua / Qixi Manhua'
 | 
			
		||||
    extClass = '.SixMH'
 | 
			
		||||
    extVersionCode = 9
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$rootDir/common.gradle"
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation project(':lib:unpacker')
 | 
			
		||||
}
 | 
			
		||||
@ -1,32 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.sixmh
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.Headers
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
 | 
			
		||||
/** Documentation of unused APIs originally used in `zh.qiximh`. */
 | 
			
		||||
object Api {
 | 
			
		||||
 | 
			
		||||
    fun getRankRequest(baseUrl: String, headers: Headers, page: Int, type: Int) =
 | 
			
		||||
        getListingRequest("$baseUrl/rankdata.php", headers, page, type)
 | 
			
		||||
 | 
			
		||||
    fun getSortRequest(baseUrl: String, headers: Headers, page: Int, type: Int) =
 | 
			
		||||
        getListingRequest("$baseUrl/sortdata.php", headers, page, type)
 | 
			
		||||
 | 
			
		||||
    /** @param page 1-5. Website allows 1-10 and contains more items per page. */
 | 
			
		||||
    fun getListingRequest(url: String, headers: Headers, page: Int, type: Int): Request {
 | 
			
		||||
        val body = FormBody.Builder()
 | 
			
		||||
            .add("page_num", page.toString())
 | 
			
		||||
            .add("type", type.toString())
 | 
			
		||||
            .build()
 | 
			
		||||
        return POST(url, headers, body)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getSearchRequest(baseUrl: String, headers: Headers, query: String): Request {
 | 
			
		||||
        val body = FormBody.Builder()
 | 
			
		||||
            .add("keyword", query)
 | 
			
		||||
            .build()
 | 
			
		||||
        return POST("$baseUrl/search.php", headers, body)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.sixmh
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class QixiChapterDto(private val id: String, private val name: String) {
 | 
			
		||||
    fun toSChapter(path: String) = SChapter.create().apply {
 | 
			
		||||
        url = "$path$id.html"
 | 
			
		||||
        name = this@QixiChapterDto.name
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class QixiDataDto(val list: List<QixiChapterDto>)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class QixiResponseDto(val data: QixiDataDto)
 | 
			
		||||
@ -1,222 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.sixmh
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import androidx.preference.ListPreference
 | 
			
		||||
import androidx.preference.PreferenceScreen
 | 
			
		||||
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
 | 
			
		||||
import eu.kanade.tachiyomi.source.ConfigurableSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import kotlinx.serialization.json.decodeFromStream
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import org.jsoup.select.Evaluator
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
import kotlin.random.Random
 | 
			
		||||
 | 
			
		||||
class SixMH : HttpSource(), ConfigurableSource {
 | 
			
		||||
    override val name = "6漫画"
 | 
			
		||||
    override val lang = "zh"
 | 
			
		||||
    override val supportsLatest = true
 | 
			
		||||
 | 
			
		||||
    private val isCi = System.getenv("CI") == "true"
 | 
			
		||||
    override val baseUrl get() = when {
 | 
			
		||||
        isCi -> MIRRORS.zip(MIRROR_NAMES) { domain, name -> "http://www.$domain#$name" }.joinToString()
 | 
			
		||||
        else -> _baseUrl
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val mirrorIndex: Int
 | 
			
		||||
    private val pcUrl: String
 | 
			
		||||
    private val _baseUrl: String
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val preferences = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
 | 
			
		||||
        val mirrors = MIRRORS
 | 
			
		||||
        var index = preferences.getString(MIRROR_PREF, "-1")!!.toInt()
 | 
			
		||||
        if (index !in mirrors.indices) {
 | 
			
		||||
            index = Random.nextInt(0, mirrors.size)
 | 
			
		||||
            preferences.edit().putString(MIRROR_PREF, index.toString()).apply()
 | 
			
		||||
        }
 | 
			
		||||
        val domain = mirrors[index]
 | 
			
		||||
 | 
			
		||||
        mirrorIndex = index
 | 
			
		||||
        pcUrl = "http://www.$domain"
 | 
			
		||||
        _baseUrl = "http://$domain"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val json: Json by injectLazy()
 | 
			
		||||
 | 
			
		||||
    override val client = network.client.newBuilder()
 | 
			
		||||
        .rateLimit(2)
 | 
			
		||||
        .build()
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaRequest(page: Int) = GET("$pcUrl/rank/1-$page.html", headers)
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaParse(response: Response): MangasPage {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
        val imgSelector = Evaluator.Tag("img")
 | 
			
		||||
        val items = document.selectFirst(Evaluator.Class("cy_list_mh"))!!.children().map {
 | 
			
		||||
            SManga.create().apply {
 | 
			
		||||
                val link = it.child(1).child(0)
 | 
			
		||||
                url = link.attr("href")
 | 
			
		||||
                title = link.ownText()
 | 
			
		||||
                thumbnail_url = it.selectFirst(imgSelector)!!.attr("src")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        val hasNextPage = document.selectFirst(Evaluator.Class("thisclass"))?.nextElementSibling() != null
 | 
			
		||||
        return MangasPage(items, hasNextPage)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int) = GET("$pcUrl/rank/5-$page.html", headers)
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
			
		||||
        if (query.isNotBlank()) {
 | 
			
		||||
            val url = pcUrl.toHttpUrl().newBuilder()
 | 
			
		||||
                .addEncodedPathSegment("search.php")
 | 
			
		||||
                .addQueryParameter("keyword", query)
 | 
			
		||||
                .toString()
 | 
			
		||||
            return GET(url, headers)
 | 
			
		||||
        } else {
 | 
			
		||||
            filters.filterIsInstance<PageFilter>().firstOrNull()?.run {
 | 
			
		||||
                return GET("$pcUrl$path$page.html", headers)
 | 
			
		||||
            }
 | 
			
		||||
            return popularMangaRequest(page)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaParse(response: Response) = popularMangaParse(response)
 | 
			
		||||
 | 
			
		||||
    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
 | 
			
		||||
        return client.newCall(GET(pcUrl + manga.url, headers))
 | 
			
		||||
            .asObservableSuccess().map(::mangaDetailsParse)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun mangaDetailsParse(response: Response): SManga {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
        val result = SManga.create().apply {
 | 
			
		||||
            val box = document.selectFirst(Evaluator.Class("cy_info"))!!
 | 
			
		||||
            val details = box.getElementsByTag("span")
 | 
			
		||||
            author = details[0].text().removePrefix("作者:")
 | 
			
		||||
            status = when (details[1].text().removePrefix("状态:").trimStart()) {
 | 
			
		||||
                "连载中" -> SManga.ONGOING
 | 
			
		||||
                "已完结" -> SManga.COMPLETED
 | 
			
		||||
                else -> SManga.UNKNOWN
 | 
			
		||||
            }
 | 
			
		||||
            genre = buildList {
 | 
			
		||||
                add(details[2].ownText().removePrefix("类别:"))
 | 
			
		||||
                details[3].ownText().removePrefix("标签:").split(Regex("[ -~]+"))
 | 
			
		||||
                    .filterTo(this) { it.isNotEmpty() }
 | 
			
		||||
            }.joinToString()
 | 
			
		||||
            description = box.selectFirst(Evaluator.Tag("p"))!!.ownText()
 | 
			
		||||
            thumbnail_url = box.selectFirst(Evaluator.Tag("img"))!!.run {
 | 
			
		||||
                attr("data-src").ifEmpty { attr("src") }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun chapterListRequest(manga: SManga) = GET(pcUrl + manga.url, headers)
 | 
			
		||||
 | 
			
		||||
    override fun chapterListParse(response: Response): List<SChapter> {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
 | 
			
		||||
        val list = document.selectFirst(Evaluator.Class("cy_plist"))!!
 | 
			
		||||
            .child(0).children().map {
 | 
			
		||||
                val element = it.child(0)
 | 
			
		||||
                SChapter.create().apply {
 | 
			
		||||
                    url = element.attr("href")
 | 
			
		||||
                    name = element.text()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            as ArrayList
 | 
			
		||||
 | 
			
		||||
        if (mirrorIndex == 0) { // 6Manhua
 | 
			
		||||
            document.selectFirst(Evaluator.Id("zhankai"))?.let { element ->
 | 
			
		||||
                val path = '/' + response.request.url.pathSegments[0] + '/'
 | 
			
		||||
                val body = FormBody.Builder().apply {
 | 
			
		||||
                    addEncoded("id", element.attr("data-id"))
 | 
			
		||||
                    addEncoded("id2", element.attr("data-vid"))
 | 
			
		||||
                }.build()
 | 
			
		||||
                client.newCall(POST("$pcUrl/bookchapter/", headers, body)).execute()
 | 
			
		||||
                    .parseAs<List<ChapterDto>>().mapTo(list) { it.toSChapter(path) }
 | 
			
		||||
            }
 | 
			
		||||
        } else { // Qixi Manhua
 | 
			
		||||
            if (document.selectFirst(Evaluator.Class("morechp")) != null) {
 | 
			
		||||
                val id = response.request.url.pathSegments[0]
 | 
			
		||||
                val path = "/$id/"
 | 
			
		||||
                val body = FormBody.Builder().addEncoded("id", id).build()
 | 
			
		||||
                client.newCall(POST("$pcUrl/chapterlist/", headers, body)).execute()
 | 
			
		||||
                    .parseAs<QixiResponseDto>().data.list.mapTo(list) { it.toSChapter(path) }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (list.isNotEmpty()) {
 | 
			
		||||
            document.selectFirst(".cy_zhangjie_top font")?.run {
 | 
			
		||||
                list[0].date_upload = dateFormat.parse(ownText())?.time ?: 0
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return list
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers)
 | 
			
		||||
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val result = Unpacker.unpack(response.body.string(), "[", "]")
 | 
			
		||||
            .ifEmpty { return emptyList() }
 | 
			
		||||
            .replace("\\u0026", "&")
 | 
			
		||||
            .replace("\\", "")
 | 
			
		||||
            .removeSurrounding("\"").split("\",\"")
 | 
			
		||||
        return result.mapIndexed { i, url -> Page(i, imageUrl = url) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
 | 
			
		||||
 | 
			
		||||
    private inline fun <reified T> Response.parseAs(): T = use {
 | 
			
		||||
        json.decodeFromStream(body.byteStream())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getFilterList() = FilterList(listOf(PageFilter()))
 | 
			
		||||
 | 
			
		||||
    override fun setupPreferenceScreen(screen: PreferenceScreen) {
 | 
			
		||||
        ListPreference(screen.context).apply {
 | 
			
		||||
            val names = MIRROR_NAMES
 | 
			
		||||
 | 
			
		||||
            key = MIRROR_PREF
 | 
			
		||||
            title = "镜像站点(重启生效)"
 | 
			
		||||
            summary = "%s"
 | 
			
		||||
            entries = names
 | 
			
		||||
            entryValues = Array(names.size, Int::toString)
 | 
			
		||||
            setDefaultValue("0")
 | 
			
		||||
        }.let(screen::addPreference)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 | 
			
		||||
        const val MIRROR_PREF = "MIRROR"
 | 
			
		||||
 | 
			
		||||
        /** Note: mirror index affects [chapterListParse] */
 | 
			
		||||
        val MIRRORS get() = arrayOf("sixmanhua.com", "qiximh3.com")
 | 
			
		||||
        val MIRROR_NAMES get() = arrayOf("6漫画", "七夕漫画")
 | 
			
		||||
 | 
			
		||||
        private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,29 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.sixmh
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class ChapterDto(private val chapterid: String, private val chaptername: String) {
 | 
			
		||||
    fun toSChapter(path: String) = SChapter.create().apply {
 | 
			
		||||
        url = "$path$chapterid.html"
 | 
			
		||||
        name = chaptername
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
internal class PageFilter : Filter.Select<String>("排行榜/分类", PAGE_NAMES) {
 | 
			
		||||
    val path get() = PAGE_PATHS[state]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val PAGE_NAMES = arrayOf(
 | 
			
		||||
    "人气榜", "周读榜", "月读榜", "火爆榜", "更新榜", "新漫榜",
 | 
			
		||||
    "冒险热血", "武侠格斗", "科幻魔幻", "侦探推理", "耽美爱情", "生活漫画",
 | 
			
		||||
    "推荐漫画", "完结漫画", "连载漫画",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
private val PAGE_PATHS = arrayOf(
 | 
			
		||||
    "/rank/1-", "/rank/2-", "/rank/3-", "/rank/4-", "/rank/5-", "/rank/6-",
 | 
			
		||||
    "/sort/1-", "/sort/2-", "/sort/3-", "/sort/4-", "/sort/5-", "/sort/6-",
 | 
			
		||||
    "/sort/11-", "/sort/12-", "/sort/13-",
 | 
			
		||||
)
 | 
			
		||||