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'
|
||||
|
||||
ext {
|
||||
extName = 'QiXiManhua'
|
||||
extName = 'QiXiManhua (Deprecated)'
|
||||
pkgNameSuffix = 'zh.qiximh'
|
||||
extClass = '.Qiximh'
|
||||
extVersionCode = 5
|
||||
extClass = '.QixiStub'
|
||||
extVersionCode = 6
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
ext {
|
||||
extName = '6Manhua'
|
||||
extName = '6Manhua / Qixi Manhua'
|
||||
pkgNameSuffix = 'zh.sixmh'
|
||||
extClass = '.SixMH'
|
||||
extVersionCode = 4
|
||||
extVersionCode = 5
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.AppInfo
|
||||
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.ParsedHttpSource
|
||||
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.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import rx.Single
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class SixMH : ParsedHttpSource() {
|
||||
class SixMH : HttpSource(), ConfigurableSource {
|
||||
override val name = "6漫画"
|
||||
override val lang = "zh"
|
||||
override val baseUrl = PC_URL
|
||||
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()
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$PC_URL/rank/1-$page.html", headers)
|
||||
override fun popularMangaNextPageSelector() = "li.thisclass:not(:last-of-type)"
|
||||
override fun popularMangaSelector() = "div.cy_list_mh > ul"
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
with(element.child(1).child(0)) {
|
||||
url = attr("href")
|
||||
title = ownText()
|
||||
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")
|
||||
}
|
||||
}
|
||||
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 latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$pcUrl/rank/5-$page.html", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
filters.filterIsInstance<PageFilter>().firstOrNull()?.run {
|
||||
return GET("$PC_URL$path$page.html", headers)
|
||||
return GET("$pcUrl$path$page.html", headers)
|
||||
}
|
||||
return popularMangaRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pcRequest(manga: SManga) = GET("$PC_URL${manga.url}", headers)
|
||||
private fun mobileRequest(manga: SManga) = GET("$MOBILE_URL${manga.url}", headers)
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// for WebView
|
||||
override fun mangaDetailsRequest(manga: SManga) = mobileRequest(manga)
|
||||
override fun mangaDetailsParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(GET(pcUrl + manga.url, headers))
|
||||
.asObservableSuccess().map(::mangaDetailsParse)
|
||||
}
|
||||
|
||||
// fetchMangaDetails fetches and parses PC page first, then mobile page
|
||||
// fetchChapterList does in the opposite order, to make use of transparent cache
|
||||
// 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()
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
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")
|
||||
author = details[0].text().removePrefix("作者:")
|
||||
status = when (details[1].child(0).ownText()) {
|
||||
status = when (details[1].text().removePrefix("状态:").trimStart()) {
|
||||
"连载中" -> SManga.ONGOING
|
||||
"已完结" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
|
@ -95,54 +117,57 @@ class SixMH : ParsedHttpSource() {
|
|||
.filterTo(this) { it.isNotEmpty() }
|
||||
}.joinToString()
|
||||
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()
|
||||
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()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = throw UnsupportedOperationException("Not used.")
|
||||
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Not used.")
|
||||
override fun chapterListRequest(manga: SManga) = GET(pcUrl + manga.url, headers)
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Single.create<List<SChapter>> {
|
||||
val document = client.newCall(mobileRequest(manga)).execute().asJsoup()
|
||||
val list = mutableListOf<SChapter>()
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val tab = document.selectFirst(Evaluator.Class("cartoon-directory")).children()
|
||||
if (tab.size >= 2) {
|
||||
tab[1].children().mapTo(list) { element ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
if (tab.size >= 3) {
|
||||
val element = tab[2]
|
||||
val path = manga.url
|
||||
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("$MOBILE_URL/bookchapter/", headers, body)).execute()
|
||||
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 (isNewDateLogic && list.isNotEmpty()) {
|
||||
val pcDocument = client.newCall(pcRequest(manga)).execute().asJsoup()
|
||||
pcDocument.selectFirst(".cy_zhangjie_top font")?.run {
|
||||
document.selectFirst(".cy_zhangjie_top font")?.run {
|
||||
list[0].date_upload = dateFormat.parse(ownText())?.time ?: 0
|
||||
}
|
||||
}
|
||||
it.onSuccess(list)
|
||||
}.toObservable()
|
||||
return list
|
||||
}
|
||||
|
||||
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> {
|
||||
val result = Unpacker.unpack(response.body!!.string(), "[", "]")
|
||||
|
@ -152,8 +177,7 @@ class SixMH : ParsedHttpSource() {
|
|||
return result.mapIndexed { i, url -> Page(i, imageUrl = url) }
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
json.decodeFromStream(body!!.byteStream())
|
||||
|
@ -161,11 +185,26 @@ class SixMH : ParsedHttpSource() {
|
|||
|
||||
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 {
|
||||
// redirect URL: http://www.6mh9.com/
|
||||
private const val DOMAIN = "sixmh7.com"
|
||||
private const val PC_URL = "http://www.$DOMAIN"
|
||||
private const val MOBILE_URL = "http://m.$DOMAIN"
|
||||
|
||||
const val MIRROR_PREF = "MIRROR"
|
||||
|
||||
/** 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 dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
||||
|
|
|
@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
import kotlinx.serialization.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 {
|
||||
url = "$path$chapterid.html"
|
||||
name = chaptername
|
||||
|
|
Loading…
Reference in New Issue