Merge Qixi Manhua into 6Manhua and update URL (#14516)
* Merge Qixi Manhua into 6Manhua and update URL * fix NPE
This commit is contained in:
parent
1182b80a0e
commit
b9950dd8da
|
@ -3,10 +3,10 @@ apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = 'QiXiManhua'
|
extName = 'QiXiManhua (Deprecated)'
|
||||||
pkgNameSuffix = 'zh.qiximh'
|
pkgNameSuffix = 'zh.qiximh'
|
||||||
extClass = '.Qiximh'
|
extClass = '.QixiStub'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.zh.qiximh
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class QixiStub : HttpSource() {
|
||||||
|
|
||||||
|
private val migratePrompt = Exception("请迁移到“6漫画”插件,可以在该插件的设置中修改镜像站点")
|
||||||
|
|
||||||
|
override val id get() = 418374491144859437
|
||||||
|
override val name get() = "七夕漫画 (废弃,请使用6漫画)"
|
||||||
|
override val lang get() = "zh"
|
||||||
|
override val supportsLatest get() = false
|
||||||
|
|
||||||
|
override val baseUrl get() = ""
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = throw migratePrompt
|
||||||
|
override fun popularMangaParse(response: Response) = throw migratePrompt
|
||||||
|
override fun latestUpdatesRequest(page: Int) = throw migratePrompt
|
||||||
|
override fun latestUpdatesParse(response: Response) = throw migratePrompt
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw migratePrompt
|
||||||
|
override fun searchMangaParse(response: Response) = throw migratePrompt
|
||||||
|
override fun mangaDetailsRequest(manga: SManga) = throw migratePrompt
|
||||||
|
override fun mangaDetailsParse(response: Response) = throw migratePrompt
|
||||||
|
override fun chapterListRequest(manga: SManga) = throw migratePrompt
|
||||||
|
override fun chapterListParse(response: Response) = throw migratePrompt
|
||||||
|
override fun pageListRequest(chapter: SChapter) = throw migratePrompt
|
||||||
|
override fun pageListParse(response: Response) = throw migratePrompt
|
||||||
|
override fun imageUrlParse(response: Response) = throw migratePrompt
|
||||||
|
}
|
|
@ -1,314 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.zh.qiximh
|
|
||||||
|
|
||||||
import app.cash.quickjs.QuickJs
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
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.int
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class Qiximh : HttpSource() {
|
|
||||||
|
|
||||||
override val lang = "zh"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val name = "七夕漫画"
|
|
||||||
|
|
||||||
override val baseUrl = "http://qiximh1.com"
|
|
||||||
|
|
||||||
// This is hard limit by API
|
|
||||||
private val maxPage = 5
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
// Used in Rank API
|
|
||||||
private enum class RANKTYPE(val rankVal: Int) {
|
|
||||||
DAILY_HOT(1),
|
|
||||||
WEEKLY_HOT(2),
|
|
||||||
MONTHLY_HOT(3),
|
|
||||||
OVERALL_HOT(4),
|
|
||||||
LATEST(5),
|
|
||||||
NEW(6),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used in Sort API (although it looks like genre)
|
|
||||||
private enum class SORTTYPE(val sortVal: Int) {
|
|
||||||
ADVENTURE(1),
|
|
||||||
ACTION(2),
|
|
||||||
MAGIC_SCIFI(3),
|
|
||||||
THRILLER(4),
|
|
||||||
ROMANCE(5),
|
|
||||||
SLICE_OF_LIFE(6),
|
|
||||||
// These are not accurate, hence not included
|
|
||||||
// HIGH_QUALITY(11),
|
|
||||||
// ON_GOING(12),
|
|
||||||
// COMPLETED(13)
|
|
||||||
}
|
|
||||||
|
|
||||||
private open class PairIntFilter(displayName: String, val vals: Array<Pair<String, Int?>>) :
|
|
||||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
|
||||||
fun getVal() = vals[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override
|
|
||||||
private fun FormBody.value(name: String): String {
|
|
||||||
return (0 until size)
|
|
||||||
.first { name(it) == name }
|
|
||||||
.let { value(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun commonRankDataRequest(page: Int, rankTypeVal: Int): Request {
|
|
||||||
return POST(
|
|
||||||
"$baseUrl/rankdata.php",
|
|
||||||
headers,
|
|
||||||
FormBody.Builder()
|
|
||||||
.add("page_num", page.toString())
|
|
||||||
.add("type", rankTypeVal.toString())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun commonSortDataRequest(page: Int, sortTypeVal: Int): Request {
|
|
||||||
return POST(
|
|
||||||
"$baseUrl/sortdata.php",
|
|
||||||
headers,
|
|
||||||
FormBody.Builder()
|
|
||||||
.add("page_num", page.toString())
|
|
||||||
.add("type", sortTypeVal.toString())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun commonDataProcess(origRequest: Request, responseBody: String): MangasPage {
|
|
||||||
val jsonData = json.parseToJsonElement(responseBody).jsonArray
|
|
||||||
|
|
||||||
val mangaArr = jsonData.map {
|
|
||||||
val targetObj = it.jsonObject
|
|
||||||
|
|
||||||
SManga.create().apply {
|
|
||||||
title = targetObj["name"]!!.jsonPrimitive.content
|
|
||||||
status = SManga.UNKNOWN
|
|
||||||
thumbnail_url = targetObj["imgurl"]!!.jsonPrimitive.content
|
|
||||||
// Extension is wrongly adding the baseURL to the SManga.
|
|
||||||
// I kept it as it is to avoid user migrations.
|
|
||||||
url = "$baseUrl/${targetObj["id"]!!.jsonPrimitive.int}/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val requestBody = origRequest.body as FormBody
|
|
||||||
val currentPage: Int = requestBody.value("page_num").toInt()
|
|
||||||
val hasNextPage = currentPage < maxPage
|
|
||||||
|
|
||||||
return MangasPage(mangaArr, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun commonRankDataParse(response: Response): MangasPage {
|
|
||||||
return commonDataProcess(response.request, response.body!!.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Popular Manga
|
|
||||||
override fun popularMangaRequest(page: Int) = commonRankDataRequest(page, RANKTYPE.DAILY_HOT.rankVal)
|
|
||||||
override fun popularMangaParse(response: Response) = commonRankDataParse(response)
|
|
||||||
|
|
||||||
// Latest Updates
|
|
||||||
override fun latestUpdatesRequest(page: Int) = commonRankDataRequest(page, RANKTYPE.LATEST.rankVal)
|
|
||||||
override fun latestUpdatesParse(response: Response) = commonRankDataParse(response)
|
|
||||||
|
|
||||||
// Search
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
return POST(
|
|
||||||
"$baseUrl/search.php",
|
|
||||||
headers,
|
|
||||||
FormBody.Builder()
|
|
||||||
.add("keyword", query)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is RankFilter -> {
|
|
||||||
val filterVal = filter.getVal()
|
|
||||||
if (filterVal != null) {
|
|
||||||
return commonRankDataRequest(page, filterVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is SortFilter -> {
|
|
||||||
val filterVal = filter.getVal()
|
|
||||||
if (filterVal != null) {
|
|
||||||
return commonSortDataRequest(page, filterVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default if no filter set
|
|
||||||
return commonRankDataRequest(page, RANKTYPE.DAILY_HOT.rankVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val responseBody = response.body
|
|
||||||
?: return MangasPage(emptyList(), false)
|
|
||||||
|
|
||||||
val responseString = responseBody.string()
|
|
||||||
|
|
||||||
if (responseString.isNotEmpty()) {
|
|
||||||
if (responseString.startsWith("[")) {
|
|
||||||
// This is to process filter
|
|
||||||
return commonDataProcess(response.request, responseString)
|
|
||||||
} else {
|
|
||||||
val jsonData = json.parseToJsonElement(responseString).jsonObject
|
|
||||||
|
|
||||||
if (jsonData["msg"]!!.jsonPrimitive.content == "success") {
|
|
||||||
val mangaArr = jsonData["search_data"]!!.jsonArray.map {
|
|
||||||
val targetObj = it.jsonObject
|
|
||||||
|
|
||||||
SManga.create().apply {
|
|
||||||
title = targetObj["name"]!!.jsonPrimitive.content
|
|
||||||
thumbnail_url = targetObj["imgs"]!!.jsonPrimitive.content
|
|
||||||
// Extension is wrongly adding the baseURL to the SManga.
|
|
||||||
// I kept it as it is to avoid user migrations.
|
|
||||||
url = "$baseUrl/${targetObj["id"]!!.jsonPrimitive.int}/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(mangaArr, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search does not have pagination
|
|
||||||
return MangasPage(emptyList(), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter
|
|
||||||
private class RankFilter : PairIntFilter(
|
|
||||||
"排行榜",
|
|
||||||
arrayOf(
|
|
||||||
Pair("全部", null),
|
|
||||||
Pair("日热门榜", RANKTYPE.DAILY_HOT.rankVal),
|
|
||||||
Pair("周热门榜", RANKTYPE.WEEKLY_HOT.rankVal),
|
|
||||||
Pair("月热门榜", RANKTYPE.MONTHLY_HOT.rankVal),
|
|
||||||
Pair("总热门榜", RANKTYPE.OVERALL_HOT.rankVal),
|
|
||||||
Pair("最近更新", RANKTYPE.LATEST.rankVal),
|
|
||||||
Pair("新漫入库", RANKTYPE.NEW.rankVal),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private class SortFilter : PairIntFilter(
|
|
||||||
"分类",
|
|
||||||
arrayOf(
|
|
||||||
Pair("全部", null),
|
|
||||||
Pair("冒险热血", SORTTYPE.ADVENTURE.sortVal),
|
|
||||||
Pair("武侠格斗", SORTTYPE.ACTION.sortVal),
|
|
||||||
Pair("玄幻科幻", SORTTYPE.MAGIC_SCIFI.sortVal),
|
|
||||||
Pair("侦探推理", SORTTYPE.THRILLER.sortVal),
|
|
||||||
Pair("耽美爱情", SORTTYPE.ROMANCE.sortVal),
|
|
||||||
Pair("生活漫画", SORTTYPE.SLICE_OF_LIFE.sortVal),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
Filter.Header("注意: 文本搜索,排行榜和分类筛选,不可同时使用"),
|
|
||||||
Filter.Separator(),
|
|
||||||
RankFilter(),
|
|
||||||
SortFilter()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Manga Details
|
|
||||||
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers)
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = document.select("h1.name").text()
|
|
||||||
thumbnail_url = document.select("div.comic_cover").attr("data-original")
|
|
||||||
author = document.select(".author_name").text()
|
|
||||||
|
|
||||||
description = arrayOf(
|
|
||||||
document.select("span.looking_chapter").text(),
|
|
||||||
document.select(".bold_fortime").text(),
|
|
||||||
document.select(".details").first().ownText(),
|
|
||||||
).filter(String::isNotBlank).joinToString("\n")
|
|
||||||
|
|
||||||
genre = arrayOf(
|
|
||||||
document.select(".comic_hot span:last-child").text(),
|
|
||||||
*(document.select(".tags.tags_last").text().split("|").toTypedArray())
|
|
||||||
).filter(String::isNotBlank).joinToString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapter
|
|
||||||
override fun chapterListRequest(manga: SManga) = GET(manga.url, headers)
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
// API does not allow retrieve full chapter list, hence the need to parse
|
|
||||||
// the chapters from both HTML and API
|
|
||||||
val chapterList = document.select(".catalog_list.row_catalog_list a")
|
|
||||||
.map {
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = it.text()
|
|
||||||
url = "$baseUrl${it.attr("href")}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toMutableList()
|
|
||||||
|
|
||||||
val mangaUrl = response.request.url.toString()
|
|
||||||
|
|
||||||
val request = POST(
|
|
||||||
"$baseUrl/bookchapter/",
|
|
||||||
headers,
|
|
||||||
FormBody.Builder()
|
|
||||||
.add("id", mangaUrl.split("/").toTypedArray().filter(String::isNotBlank).last())
|
|
||||||
.add("id2", "1")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
val inlineResponse = client.newCall(request).execute()
|
|
||||||
val jsonData = json.parseToJsonElement(inlineResponse.body!!.string()).jsonArray
|
|
||||||
|
|
||||||
chapterList += jsonData.map {
|
|
||||||
val targetObj = it.jsonObject
|
|
||||||
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = targetObj["chaptername"]!!.jsonPrimitive.content
|
|
||||||
url = "$mangaUrl${targetObj["chapterid"]!!.jsonPrimitive.int}.html"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapterList
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page
|
|
||||||
override fun pageListRequest(chapter: SChapter) = GET(chapter.url, headers)
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
// Special thanks to author who created Mangahere.kt
|
|
||||||
val script = document.select("script:containsData(function(p,a,c,k,e,d))").html().removePrefix("eval")
|
|
||||||
val deobfuscatedScript = QuickJs.create().use { it.evaluate(script).toString() }
|
|
||||||
val imageUrlListString = deobfuscatedScript.substringAfter("newImgs=").trim()
|
|
||||||
val imageUrlList = json.parseToJsonElement(imageUrlListString).jsonArray.map { it.jsonPrimitive.content }
|
|
||||||
|
|
||||||
return imageUrlList.mapIndexed { index, s -> Page(index, imageUrl = s) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unused
|
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Unused")
|
|
||||||
}
|
|
|
@ -3,10 +3,10 @@ apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = '6Manhua'
|
extName = '6Manhua / Qixi Manhua'
|
||||||
pkgNameSuffix = 'zh.sixmh'
|
pkgNameSuffix = 'zh.sixmh'
|
||||||
extClass = '.SixMH'
|
extClass = '.SixMH'
|
||||||
extVersionCode = 4
|
extVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
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,90 +1,112 @@
|
||||||
package eu.kanade.tachiyomi.extension.zh.sixmh
|
package eu.kanade.tachiyomi.extension.zh.sixmh
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.AppInfo
|
import eu.kanade.tachiyomi.AppInfo
|
||||||
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
|
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
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.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import org.jsoup.select.Evaluator
|
import org.jsoup.select.Evaluator
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Single
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class SixMH : ParsedHttpSource() {
|
class SixMH : HttpSource(), ConfigurableSource {
|
||||||
override val name = "6漫画"
|
override val name = "6漫画"
|
||||||
override val lang = "zh"
|
override val lang = "zh"
|
||||||
override val baseUrl = PC_URL
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val mirrorIndex: Int
|
||||||
|
private val pcUrl: String
|
||||||
|
override val baseUrl: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
val preferences = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
val mirrors = MIRRORS
|
||||||
|
val index = preferences.getString(MIRROR_PREF, "0")!!.toInt().coerceAtMost(mirrors.size - 1)
|
||||||
|
val domain = mirrors[index]
|
||||||
|
|
||||||
|
mirrorIndex = index
|
||||||
|
pcUrl = "http://www.$domain"
|
||||||
|
baseUrl = "http://$domain"
|
||||||
|
}
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
override val client = network.client.newBuilder()
|
||||||
.rateLimit(2)
|
.rateLimit(2)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$PC_URL/rank/1-$page.html", headers)
|
override fun popularMangaRequest(page: Int) = GET("$pcUrl/rank/1-$page.html", headers)
|
||||||
override fun popularMangaNextPageSelector() = "li.thisclass:not(:last-of-type)"
|
|
||||||
override fun popularMangaSelector() = "div.cy_list_mh > ul"
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
val document = response.asJsoup()
|
||||||
with(element.child(1).child(0)) {
|
val imgSelector = Evaluator.Tag("img")
|
||||||
url = attr("href")
|
val items = document.selectFirst(Evaluator.Class("cy_list_mh")).children().map {
|
||||||
title = ownText()
|
SManga.create().apply {
|
||||||
|
val link = it.child(1).child(0)
|
||||||
|
url = link.attr("href")
|
||||||
|
title = link.ownText()
|
||||||
|
thumbnail_url = it.selectFirst(imgSelector).attr("src")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
thumbnail_url = element.selectFirst(Evaluator.Tag("img")).attr("src")
|
val hasNextPage = document.selectFirst(Evaluator.Class("thisclass"))?.nextElementSibling() != null
|
||||||
|
return MangasPage(items, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$PC_URL/rank/5-$page.html", headers)
|
override fun latestUpdatesRequest(page: Int) = GET("$pcUrl/rank/5-$page.html", headers)
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
if (query.isNotBlank()) {
|
if (query.isNotBlank()) {
|
||||||
return GET("$PC_URL/search.php?keyword=$query", headers)
|
val url = pcUrl.toHttpUrl().newBuilder()
|
||||||
|
.addEncodedPathSegment("search.php")
|
||||||
|
.addQueryParameter("keyword", query)
|
||||||
|
.toString()
|
||||||
|
return GET(url, headers)
|
||||||
} else {
|
} else {
|
||||||
filters.filterIsInstance<PageFilter>().firstOrNull()?.run {
|
filters.filterIsInstance<PageFilter>().firstOrNull()?.run {
|
||||||
return GET("$PC_URL$path$page.html", headers)
|
return GET("$pcUrl$path$page.html", headers)
|
||||||
}
|
}
|
||||||
return popularMangaRequest(page)
|
return popularMangaRequest(page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pcRequest(manga: SManga) = GET("$PC_URL${manga.url}", headers)
|
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||||
private fun mobileRequest(manga: SManga) = GET("$MOBILE_URL${manga.url}", headers)
|
|
||||||
|
|
||||||
// for WebView
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
override fun mangaDetailsRequest(manga: SManga) = mobileRequest(manga)
|
return client.newCall(GET(pcUrl + manga.url, headers))
|
||||||
override fun mangaDetailsParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
.asObservableSuccess().map(::mangaDetailsParse)
|
||||||
|
}
|
||||||
|
|
||||||
// fetchMangaDetails fetches and parses PC page first, then mobile page
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
// fetchChapterList does in the opposite order, to make use of transparent cache
|
val document = response.asJsoup()
|
||||||
// in this way, the latter requests will be responded with 304 Not Modified (in most cases)
|
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Single.create<SManga> {
|
|
||||||
val document = client.newCall(pcRequest(manga)).execute().asJsoup()
|
|
||||||
val result = SManga.create().apply {
|
val result = SManga.create().apply {
|
||||||
val box = document.selectFirst(Evaluator.Id("intro_l"))
|
val box = document.selectFirst(Evaluator.Class("cy_info"))
|
||||||
val details = box.getElementsByTag("span")
|
val details = box.getElementsByTag("span")
|
||||||
author = details[0].text().removePrefix("作者:")
|
author = details[0].text().removePrefix("作者:")
|
||||||
status = when (details[1].child(0).ownText()) {
|
status = when (details[1].text().removePrefix("状态:").trimStart()) {
|
||||||
"连载中" -> SManga.ONGOING
|
"连载中" -> SManga.ONGOING
|
||||||
"已完结" -> SManga.COMPLETED
|
"已完结" -> SManga.COMPLETED
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
|
@ -95,54 +117,57 @@ class SixMH : ParsedHttpSource() {
|
||||||
.filterTo(this) { it.isNotEmpty() }
|
.filterTo(this) { it.isNotEmpty() }
|
||||||
}.joinToString()
|
}.joinToString()
|
||||||
description = box.selectFirst(Evaluator.Tag("p")).ownText()
|
description = box.selectFirst(Evaluator.Tag("p")).ownText()
|
||||||
thumbnail_url = box.selectFirst(Evaluator.Tag("img")).attr("src")
|
thumbnail_url = box.selectFirst(Evaluator.Tag("img")).run {
|
||||||
|
attr("data-src").ifEmpty { attr("src") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val mobileDocument = client.newCall(mobileRequest(manga)).execute().asJsoup()
|
return result
|
||||||
val details = mobileDocument.selectFirst(Evaluator.Class("author"))
|
}
|
||||||
.ownText().trim().split(Regex(""" +"""))
|
|
||||||
if (details.size >= 3) {
|
|
||||||
result.description = details[2] + '\n' + result.description
|
|
||||||
}
|
|
||||||
it.onSuccess(result)
|
|
||||||
}.toObservable()
|
|
||||||
|
|
||||||
override fun chapterListSelector() = throw UnsupportedOperationException("Not used.")
|
override fun chapterListRequest(manga: SManga) = GET(pcUrl + manga.url, headers)
|
||||||
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Not used.")
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Single.create<List<SChapter>> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val document = client.newCall(mobileRequest(manga)).execute().asJsoup()
|
val document = response.asJsoup()
|
||||||
val list = mutableListOf<SChapter>()
|
|
||||||
|
|
||||||
val tab = document.selectFirst(Evaluator.Class("cartoon-directory")).children()
|
val list = document.selectFirst(Evaluator.Class("cy_plist"))
|
||||||
if (tab.size >= 2) {
|
.child(0).children().map {
|
||||||
tab[1].children().mapTo(list) { element ->
|
val element = it.child(0)
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = element.attr("href")
|
url = element.attr("href")
|
||||||
name = element.text()
|
name = element.text()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tab.size >= 3) {
|
as ArrayList
|
||||||
val element = tab[2]
|
|
||||||
val path = manga.url
|
if (mirrorIndex == 0) { // 6Manhua
|
||||||
|
document.selectFirst(Evaluator.Id("zhankai"))?.let { element ->
|
||||||
|
val path = '/' + response.request.url.pathSegments[0] + '/'
|
||||||
val body = FormBody.Builder().apply {
|
val body = FormBody.Builder().apply {
|
||||||
addEncoded("id", element.attr("data-id"))
|
addEncoded("id", element.attr("data-id"))
|
||||||
addEncoded("id2", element.attr("data-vid"))
|
addEncoded("id2", element.attr("data-vid"))
|
||||||
}.build()
|
}.build()
|
||||||
client.newCall(POST("$MOBILE_URL/bookchapter/", headers, body)).execute()
|
client.newCall(POST("$pcUrl/bookchapter/", headers, body)).execute()
|
||||||
.parseAs<List<ChapterDto>>().mapTo(list) { it.toSChapter(path) }
|
.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 (isNewDateLogic && list.isNotEmpty()) {
|
if (isNewDateLogic && list.isNotEmpty()) {
|
||||||
val pcDocument = client.newCall(pcRequest(manga)).execute().asJsoup()
|
document.selectFirst(".cy_zhangjie_top font")?.run {
|
||||||
pcDocument.selectFirst(".cy_zhangjie_top font")?.run {
|
|
||||||
list[0].date_upload = dateFormat.parse(ownText())?.time ?: 0
|
list[0].date_upload = dateFormat.parse(ownText())?.time ?: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
it.onSuccess(list)
|
return list
|
||||||
}.toObservable()
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter) = GET("$MOBILE_URL${chapter.url}", headers)
|
override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers)
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val result = Unpacker.unpack(response.body!!.string(), "[", "]")
|
val result = Unpacker.unpack(response.body!!.string(), "[", "]")
|
||||||
|
@ -152,8 +177,7 @@ class SixMH : ParsedHttpSource() {
|
||||||
return result.mapIndexed { i, url -> Page(i, imageUrl = url) }
|
return result.mapIndexed { i, url -> Page(i, imageUrl = url) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used.")
|
||||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T = use {
|
private inline fun <reified T> Response.parseAs(): T = use {
|
||||||
json.decodeFromStream(body!!.byteStream())
|
json.decodeFromStream(body!!.byteStream())
|
||||||
|
@ -161,11 +185,26 @@ class SixMH : ParsedHttpSource() {
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(listOf(PageFilter()))
|
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 {
|
companion object {
|
||||||
// redirect URL: http://www.6mh9.com/
|
|
||||||
private const val DOMAIN = "sixmh7.com"
|
const val MIRROR_PREF = "MIRROR"
|
||||||
private const val PC_URL = "http://www.$DOMAIN"
|
|
||||||
private const val MOBILE_URL = "http://m.$DOMAIN"
|
/** Note: mirror index affects [chapterListParse] */
|
||||||
|
val MIRRORS get() = arrayOf("6mh66.com", "qiximh3.com")
|
||||||
|
val MIRROR_NAMES get() = arrayOf("6漫画", "七夕漫画")
|
||||||
|
|
||||||
private val isNewDateLogic = AppInfo.getVersionCode() >= 81
|
private val isNewDateLogic = AppInfo.getVersionCode() >= 81
|
||||||
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
||||||
|
|
|
@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ChapterDto(val chapterid: String, val chaptername: String) {
|
class ChapterDto(private val chapterid: String, private val chaptername: String) {
|
||||||
fun toSChapter(path: String) = SChapter.create().apply {
|
fun toSChapter(path: String) = SChapter.create().apply {
|
||||||
url = "$path$chapterid.html"
|
url = "$path$chapterid.html"
|
||||||
name = chaptername
|
name = chaptername
|
||||||
|
|
Loading…
Reference in New Issue