Add MMLook multisrc (#9624)
* Add MMLook multisrc * show updated time * fix updated text * tweak manga url logic * Use cloudflareClient
This commit is contained in:
parent
03b8b9b4ca
commit
3993e7349b
9
lib-multisrc/mmlook/build.gradle.kts
Normal file
9
lib-multisrc/mmlook/build.gradle.kts
Normal file
@ -0,0 +1,9 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:unpacker"))
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mmlook
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ResponseDto(val data: List<ChapterDto>)
|
||||
|
||||
@Serializable
|
||||
class ChapterDto(
|
||||
private val chapterid: String,
|
||||
private val chaptername: String,
|
||||
) {
|
||||
fun toSChapter(mangaId: String) = SChapter.create().apply {
|
||||
url = "$mangaId/$chapterid"
|
||||
name = chaptername
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mmlook
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class Option(val name: String, val value: String)
|
||||
|
||||
open class SelectFilter(name: String, val options: Array<Option>) :
|
||||
Filter.Select<String>(name, Array(options.size) { options[it].name })
|
||||
|
||||
class RankingFilter : SelectFilter(
|
||||
"排行榜",
|
||||
arrayOf(
|
||||
Option("不查看", ""),
|
||||
Option("精品榜", "1"),
|
||||
Option("人气榜", "2"),
|
||||
Option("推荐榜", "3"),
|
||||
Option("黑马榜", "4"),
|
||||
Option("最近更新", "5"),
|
||||
Option("新漫画", "6"),
|
||||
),
|
||||
)
|
||||
|
||||
class CategoryFilter : SelectFilter(
|
||||
"分类",
|
||||
arrayOf(
|
||||
Option("全部", ""),
|
||||
Option("冒险", "1"),
|
||||
Option("热血", "2"),
|
||||
Option("都市", "3"),
|
||||
Option("玄幻", "4"),
|
||||
Option("悬疑", "5"),
|
||||
Option("耽美", "6"),
|
||||
Option("恋爱", "7"),
|
||||
Option("生活", "8"),
|
||||
Option("搞笑", "9"),
|
||||
Option("穿越", "10"),
|
||||
Option("修真", "11"),
|
||||
Option("后宫", "12"),
|
||||
Option("女主", "13"),
|
||||
Option("古风", "14"),
|
||||
Option("连载", "15"),
|
||||
Option("完结", "16"),
|
||||
),
|
||||
)
|
@ -0,0 +1,200 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mmlook
|
||||
|
||||
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
|
||||
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 keiyoushi.utils.parseAs
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
// Rumanhua legacy preference:
|
||||
// const val APP_CUSTOMIZATION_URL = "APP_CUSTOMIZATION_URL"
|
||||
|
||||
/** 漫漫看 */
|
||||
open class MMLook(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
private val desktopUrl: String,
|
||||
private val useLegacyMangaUrl: Boolean,
|
||||
) : HttpSource() {
|
||||
override val lang: String get() = "zh"
|
||||
override val supportsLatest: Boolean get() = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.followRedirects(false)
|
||||
.hostnameVerifier { _, _ -> true }
|
||||
.build()
|
||||
|
||||
private fun String.certificateWorkaround() = replace("https:", "http:")
|
||||
|
||||
private fun SManga.formatUrl() = apply { if (useLegacyMangaUrl) url = "/$url/" }
|
||||
|
||||
private fun rankingRequest(id: String) = GET("$desktopUrl/rank/$id", headers)
|
||||
|
||||
override fun popularMangaRequest(page: Int) = rankingRequest("1")
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val entries = response.asJsoup().select(".likedata").map { element ->
|
||||
SManga.create().apply {
|
||||
url = element.select("a").attr("href").mustRemoveSurrounding("/", "/")
|
||||
title = element.selectFirst(".le-t")!!.text()
|
||||
author = element.selectFirst(".likeinfo > p")!!.text()
|
||||
.mustRemoveSurrounding("作者:", "")
|
||||
description = element.selectFirst(".le-j")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
|
||||
}.formatUrl()
|
||||
}
|
||||
return MangasPage(entries, false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = rankingRequest("5")
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
RankingFilter(),
|
||||
Filter.Separator(),
|
||||
Filter.Header("分类(搜索文本、查看排行榜时无效)"),
|
||||
CategoryFilter(),
|
||||
)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (query.isNotBlank()) {
|
||||
return POST(
|
||||
"$desktopUrl/s",
|
||||
headers,
|
||||
FormBody.Builder().add("k", query.take(12)).build(),
|
||||
)
|
||||
}
|
||||
for (filter in filters) {
|
||||
when (filter) {
|
||||
is RankingFilter -> if (filter.state > 0) {
|
||||
return rankingRequest(filter.options[filter.state].value)
|
||||
}
|
||||
|
||||
is CategoryFilter -> if (filter.state > 0) {
|
||||
val id = filter.options[filter.state].value
|
||||
return GET("$desktopUrl/sort/$id", headers)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return popularMangaRequest(page)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (response.request.method == "GET") return popularMangaParse(response)
|
||||
|
||||
val entries = response.asJsoup().select(".col-auto").map { element ->
|
||||
SManga.create().apply {
|
||||
url = element.selectFirst("a")!!.attr("href").mustRemoveSurrounding("/", "/")
|
||||
title = element.selectFirst(".e-title")!!.text()
|
||||
author = element.selectFirst(".tip")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
|
||||
}.formatUrl()
|
||||
}
|
||||
return MangasPage(entries, false)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
val id = manga.url.removeSurrounding("/")
|
||||
return "$baseUrl/$id/".certificateWorkaround()
|
||||
}
|
||||
|
||||
// Desktop page has consistent template and more initial chapters
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val id = manga.url.removeSurrounding("/")
|
||||
return GET("$desktopUrl/$id/", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val comicInfo = response.asJsoup().selectFirst(".comicInfo")!!
|
||||
thumbnail_url = comicInfo.selectFirst("img")!!.attr("data-src")
|
||||
|
||||
val container = comicInfo.selectFirst(".detinfo")!!
|
||||
title = container.selectFirst("h1")!!.text()
|
||||
|
||||
var updated = ""
|
||||
for (span in container.select("span")) {
|
||||
val text = span.ownText()
|
||||
val value = text.substring(4).trimStart()
|
||||
when (val key = text.substring(0, 4)) {
|
||||
"作 者:" -> author = value
|
||||
"更新时间" -> updated = "$text\n\n"
|
||||
"标 签:" -> genre = value.replace(" ", ", ")
|
||||
"状 态:" -> status = when (value) {
|
||||
"连载中" -> SManga.ONGOING
|
||||
"已完结" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
else -> throw Exception("Unknown field: $key")
|
||||
}
|
||||
}
|
||||
|
||||
description = updated + container.selectFirst(".content")!!.text()
|
||||
}
|
||||
|
||||
// Desktop page contains more initial chapters
|
||||
// "more chapter" request must be sent to the same domain
|
||||
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val container = response.asJsoup().selectFirst(".chapterlistload")!!
|
||||
val chapters = container.child(0).children().mapTo(ArrayList()) { element ->
|
||||
SChapter.create().apply {
|
||||
url = element.attr("href").mustRemoveSurrounding("/", ".html")
|
||||
name = element.text()
|
||||
}
|
||||
}
|
||||
if (container.selectFirst(".chaplist-more") != null) {
|
||||
val mangaId = response.request.url.pathSegments[0]
|
||||
val request = POST(
|
||||
"$desktopUrl/morechapter",
|
||||
headers,
|
||||
FormBody.Builder().addEncoded("id", mangaId).build(),
|
||||
)
|
||||
client.newCall(request).execute().parseAs<ResponseDto>().data
|
||||
.mapTo(chapters) { it.toSChapter(mangaId) }
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
private fun SChapter.fullUrl(): String {
|
||||
val url = this.url
|
||||
if (url.startsWith('/')) throw Exception("请刷新章节列表")
|
||||
return "$baseUrl/$url.html"
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter) = chapter.fullUrl().certificateWorkaround()
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request = GET(chapter.fullUrl(), headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val id = document.selectFirst(".readerContainer")!!.attr("data-id").toInt()
|
||||
return document.selectFirst("script:containsData(eval)")!!.data()
|
||||
.let(Unpacker::unpack)
|
||||
.mustRemoveSurrounding("var __c0rst96=\"", "\"")
|
||||
.let { decrypt(it, id) }
|
||||
.parseAs<List<String>>()
|
||||
.mapIndexed { i, imageUrl -> Page(i, imageUrl = imageUrl) }
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private fun String.mustRemoveSurrounding(prefix: String, suffix: String): String {
|
||||
check(startsWith(prefix) && endsWith(suffix)) { "string doesn't match $prefix[...]$suffix" }
|
||||
return substring(prefix.length, length - suffix.length)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mmlook
|
||||
|
||||
import android.util.Base64
|
||||
import kotlin.experimental.xor
|
||||
|
||||
// all2.js?v=2.3
|
||||
fun decrypt(data: String, index: Int): String {
|
||||
val key = when (index) {
|
||||
0 -> "smkhy258"
|
||||
1 -> "smkd95fv"
|
||||
2 -> "md496952"
|
||||
3 -> "cdcsdwq"
|
||||
4 -> "vbfsa256"
|
||||
5 -> "cawf151c"
|
||||
6 -> "cd56cvda"
|
||||
7 -> "8kihnt9"
|
||||
8 -> "dso15tlo"
|
||||
9 -> "5ko6plhy"
|
||||
else -> throw Exception("Unknown index: $index")
|
||||
}.encodeToByteArray()
|
||||
val keyLength = key.size
|
||||
val bytes = Base64.decode(data, Base64.DEFAULT)
|
||||
for (i in bytes.indices) {
|
||||
bytes[i] = bytes[i] xor key[i % keyLength]
|
||||
}
|
||||
return String(Base64.decode(bytes, Base64.DEFAULT))
|
||||
}
|
9
src/zh/dumanwu/build.gradle
Normal file
9
src/zh/dumanwu/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
ext {
|
||||
extName = 'Dumanwu'
|
||||
extClass = '.Dumanwu'
|
||||
themePkg = 'mmlook'
|
||||
baseUrl = 'https://m.dumanwu1.com'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/zh/dumanwu/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/zh/dumanwu/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
src/zh/dumanwu/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/zh/dumanwu/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
src/zh/dumanwu/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/zh/dumanwu/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
BIN
src/zh/dumanwu/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/zh/dumanwu/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
src/zh/dumanwu/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/zh/dumanwu/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dumanwu
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mmlook.MMLook
|
||||
|
||||
class Dumanwu : MMLook(
|
||||
"读漫屋",
|
||||
"https://m.dumanwu1.com",
|
||||
"https://www.dumanwu1.com",
|
||||
useLegacyMangaUrl = false,
|
||||
)
|
@ -1,8 +1,9 @@
|
||||
ext {
|
||||
extName = 'Rumanhua'
|
||||
extClass = '.Rumanhua'
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
themePkg = 'mmlook'
|
||||
baseUrl = 'https://m.rumanhua1.com'
|
||||
overrideVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,111 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.rumanhua
|
||||
|
||||
import android.util.Base64
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
// all2.js?v=2.3
|
||||
class PageDecrypt {
|
||||
private val SCRIPT_PATTERN = "eval(function(p,a,c,k,e,d)"
|
||||
private val CONTENT_MARKER = "var __c0rst96=\""
|
||||
|
||||
fun toDecrypt(document: Document): String {
|
||||
document.head().select("script").forEach { script ->
|
||||
val scriptStr = script.data().trim()
|
||||
if (scriptStr.startsWith(SCRIPT_PATTERN)) {
|
||||
val obf = obfuscate(extractPackerParams(scriptStr)).substringAfter(CONTENT_MARKER)
|
||||
.trimEnd('"')
|
||||
|
||||
val selectedIndex =
|
||||
document.selectFirst("div.readerContainer")?.attr("data-id")?.toIntOrNull()
|
||||
?: throw IllegalArgumentException("Invalid container index")
|
||||
|
||||
return decryptToString(obf, selectedIndex)
|
||||
}
|
||||
}
|
||||
throw IllegalArgumentException("No valid script found for decryption")
|
||||
}
|
||||
|
||||
private fun decryptToString(encodedContent: String, selectedIndex: Int): String {
|
||||
val boundedIndex = selectedIndex.coerceIn(0, 9)
|
||||
val xorKeyEncoded = getEncodedKeyForIndex(boundedIndex)
|
||||
val xorKey = base64Decode(xorKeyEncoded)
|
||||
val encryptedContent = base64Decode(encodedContent)
|
||||
val keyLength = xorKey.length
|
||||
val xoredString = StringBuilder(encryptedContent.length)
|
||||
|
||||
for (i in encryptedContent.indices) {
|
||||
val k = i % keyLength
|
||||
val xoredChar = encryptedContent[i].code xor xorKey[k].code
|
||||
xoredString.append(xoredChar.toChar())
|
||||
}
|
||||
|
||||
return base64Decode(xoredString.toString())
|
||||
}
|
||||
|
||||
private fun getEncodedKeyForIndex(index: Int): String {
|
||||
return when (index) {
|
||||
0 -> "c21raHkyNTg="
|
||||
1 -> "c21rZDk1ZnY="
|
||||
2 -> "bWQ0OTY5NTI="
|
||||
3 -> "Y2Rjc2R3cQ=="
|
||||
4 -> "dmJmc2EyNTY="
|
||||
5 -> "Y2F3ZjE1MWM="
|
||||
6 -> "Y2Q1NmN2ZGE="
|
||||
7 -> "OGtpaG50OQ=="
|
||||
8 -> "ZHNvMTV0bG8="
|
||||
9 -> "NWtvNnBsaHk="
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun base64Decode(input: String): String {
|
||||
if (input.isEmpty()) return input
|
||||
return String(Base64.decode(input, Base64.DEFAULT))
|
||||
}
|
||||
|
||||
private fun obfuscate(pl: PackerPayload): String {
|
||||
fun eFunction(c: Int): String {
|
||||
return if (c < pl.a) {
|
||||
if (c > 35) {
|
||||
(c + 29).toChar().toString()
|
||||
} else {
|
||||
c.toString(36)
|
||||
}
|
||||
} else {
|
||||
eFunction(c / pl.a) + if (c % pl.a > 35) {
|
||||
(c % pl.a + 29).toChar().toString()
|
||||
} else {
|
||||
(c % pl.a).toString(36)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val d = mutableMapOf<String, String>()
|
||||
var tempC = pl.c
|
||||
while (tempC-- > 0) {
|
||||
d[eFunction(tempC)] = pl.k.getOrElse(tempC) { eFunction(tempC) }
|
||||
}
|
||||
|
||||
var result = pl.p
|
||||
tempC = pl.c
|
||||
while (tempC-- > 0) {
|
||||
val key = eFunction(tempC)
|
||||
val replacement = d[key] ?: ""
|
||||
result = result.replace(Regex("\\b${Regex.escape(key)}\\b"), replacement)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private class PackerPayload(val p: String, val a: Int, val c: Int, val k: List<String>)
|
||||
|
||||
private fun extractPackerParams(source: String): PackerPayload {
|
||||
val args = source.substringAfter("}(").substringBefore(".split('|')").split(",")
|
||||
return PackerPayload(
|
||||
args[0].trim('\''),
|
||||
args[1].toInt(),
|
||||
args[2].toInt(),
|
||||
args[3].trim('\'').split("|"),
|
||||
)
|
||||
}
|
||||
}
|
@ -1,323 +1,10 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.rumanhua
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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 keiyoushi.utils.getPreferences
|
||||
import keiyoushi.utils.jsonInstance
|
||||
import keiyoushi.utils.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
import eu.kanade.tachiyomi.multisrc.mmlook.MMLook
|
||||
|
||||
class Rumanhua : HttpSource(), ConfigurableSource {
|
||||
override val lang: String = "zh"
|
||||
override val name: String = "如漫画"
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences = getPreferences()
|
||||
|
||||
override val baseUrl: String = getTargetUrl()
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private fun getTargetUrl(): String {
|
||||
val defaultUrl = "http://www.rumanhua1.com"
|
||||
val url = preferences.getString(APP_CUSTOMIZATION_URL, defaultUrl)!!
|
||||
if (url.isNotBlank()) {
|
||||
return url
|
||||
}
|
||||
return defaultUrl
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val lis = mutableListOf<SChapter>()
|
||||
document.select("div.forminfo > div.chapterList > div.chapterlistload > ul > a")
|
||||
.forEach { element ->
|
||||
lis.add(
|
||||
SChapter.create().apply {
|
||||
name = element.text()
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// get more chapter ...
|
||||
val bid = response.request.url.pathSegments[0]
|
||||
val body = FormBody.Builder().add("id", bid).build()
|
||||
val moreRequest = POST("$baseUrl/morechapter", headers, body)
|
||||
val moreResponse = client.newCall(moreRequest).execute()
|
||||
if (!moreResponse.isSuccessful) {
|
||||
throw IOException("Request failed: ${moreRequest.url}")
|
||||
}
|
||||
|
||||
val moreChapter = moreResponse.parseAs<MoreChapter>()
|
||||
if (moreChapter.code == "200") {
|
||||
jsonInstance.decodeFromJsonElement<List<MoreChapterInfo>>(moreChapter.data).forEach {
|
||||
lis.add(
|
||||
SChapter.create().apply {
|
||||
name = it.chaptername
|
||||
url = "/$bid/${it.chapterid}.html"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return lis
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private class MoreChapter(
|
||||
val code: String,
|
||||
val msg: String,
|
||||
val data: JsonElement,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private class MoreChapterInfo(
|
||||
val chapterid: String,
|
||||
val chaptername: String,
|
||||
)
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/rank/5", headers)
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val info = document.selectFirst("div.forminfo > div.comicInfo")!!
|
||||
|
||||
return SManga.create().apply {
|
||||
info.select("div.detinfo > p").forEach { element ->
|
||||
when (element.attr("class")) {
|
||||
"gray" -> {
|
||||
element.select("span").forEach { span ->
|
||||
val spanText = span.text()
|
||||
val dgenre =
|
||||
removePrefixAndCheck(spanText, "标 签:")?.replace(" ", ", ")
|
||||
if (dgenre != null) {
|
||||
genre = dgenre
|
||||
} else {
|
||||
status = when (removePrefixAndCheck(spanText, "状 态:")) {
|
||||
"连载中" -> SManga.ONGOING
|
||||
"已完结" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"content" -> {
|
||||
description = element.text()
|
||||
}
|
||||
|
||||
else -> {
|
||||
element.select("span").forEach { span ->
|
||||
val dauthor = removePrefixAndCheck(span.text(), "作 者:")
|
||||
if (dauthor != null) {
|
||||
author = dauthor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
title = info.selectFirst("div.detinfo > h1")!!.text()
|
||||
thumbnail_url = document.selectFirst("div.mhcover > div.himg > img")?.absUrl("data-src")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removePrefixAndCheck(input: String, prefix: String): String? {
|
||||
if (input.isEmpty() || prefix.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
if (input.startsWith(prefix)) {
|
||||
return input.substring(prefix.length).trim()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
private val pageDecrypt = PageDecrypt()
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val lis = mutableListOf<Page>()
|
||||
pageDecrypt.toDecrypt(document).parseAs<List<String>>().forEachIndexed { index, img ->
|
||||
lis.add(Page(index, imageUrl = img))
|
||||
}
|
||||
|
||||
return lis
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val lis = mutableListOf<SManga>()
|
||||
document.select("div.float-r.rs-p > div.wholike > div.likedata").forEach { element ->
|
||||
lis.add(
|
||||
SManga.create().apply {
|
||||
title = element.selectFirst("div.likeinfo > a")!!.text()
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("data-src")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return MangasPage(lis, false)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/rank/1", headers)
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (response.request.url.encodedPath != "/s") {
|
||||
return popularMangaParse(response)
|
||||
}
|
||||
|
||||
val document = response.asJsoup()
|
||||
|
||||
val lis = mutableListOf<SManga>()
|
||||
document.select("div.item-data.s-data > div.col-auto").forEach { element ->
|
||||
lis.add(
|
||||
SManga.create().apply {
|
||||
title = element.selectFirst("a > p.e-title")!!.text()
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
thumbnail_url =
|
||||
element.selectFirst("a > div.edit-top > img")?.absUrl("data-src")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return MangasPage(lis, false)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||
|
||||
if (query != "" && !query.contains("-")) {
|
||||
val body = FormBody.Builder().add("k", query.take(12)).build()
|
||||
return POST(
|
||||
urlBuilder.encodedPath("/s").build().toString(),
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
} else {
|
||||
// RankGroup or CategoryGroup take one and reset the other
|
||||
var url: String? = null
|
||||
for (filter in filters.filterIsInstance<UriPartFilter>()) {
|
||||
if (url != null) {
|
||||
filter.reset()
|
||||
} else {
|
||||
val path = filter.toUriPart()
|
||||
if (path != "") {
|
||||
url = path
|
||||
}
|
||||
}
|
||||
}
|
||||
if (url != null) {
|
||||
return GET(urlBuilder.encodedPath(url).build().toString(), headers)
|
||||
}
|
||||
throw IOException("Invalid filter types")
|
||||
}
|
||||
}
|
||||
|
||||
// Filter
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
RankGroup(),
|
||||
CategoryGroup(),
|
||||
)
|
||||
|
||||
private class RankGroup : UriPartFilter(
|
||||
"排行榜",
|
||||
arrayOf(
|
||||
Pair("None", ""),
|
||||
Pair("精品榜", "/rank/1"),
|
||||
Pair("人气榜", "/rank/2"),
|
||||
Pair("推荐榜", "/rank/3"),
|
||||
Pair("黑马榜", "/rank/4"),
|
||||
Pair("最近更新", "/rank/5"),
|
||||
Pair("新漫画", "/rank/6"),
|
||||
),
|
||||
)
|
||||
|
||||
private class CategoryGroup : UriPartFilter(
|
||||
"按类型",
|
||||
arrayOf(
|
||||
Pair("None", ""),
|
||||
Pair("冒险", "/sort/1"),
|
||||
Pair("热血", "/sort/2"),
|
||||
Pair("都市", "/sort/3"),
|
||||
Pair("玄幻", "/sort/4"),
|
||||
Pair("悬疑", "/sort/5"),
|
||||
Pair("耽美", "/sort/6"),
|
||||
Pair("恋爱", "/sort/7"),
|
||||
Pair("生活", "/sort/8"),
|
||||
Pair("搞笑", "/sort/9"),
|
||||
Pair("穿越", "/sort/10"),
|
||||
Pair("修真", "/sort/11"),
|
||||
Pair("后宫", "/sort/12"),
|
||||
Pair("女主", "/sort/13"),
|
||||
Pair("古风", "/sort/14"),
|
||||
Pair("连载", "/sort/15"),
|
||||
Pair("完结", "/sort/16"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
defaultValue: Int = 0,
|
||||
) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), defaultValue) {
|
||||
open fun toUriPart() = vals[state].second
|
||||
open fun reset() {
|
||||
state = 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = APP_CUSTOMIZATION_URL
|
||||
title = "自定义url"
|
||||
summary = "修改后需要重启应用生效"
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putString(APP_CUSTOMIZATION_URL, newValue as String).commit()
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
||||
const val APP_CUSTOMIZATION_URL = "APP_CUSTOMIZATION_URL"
|
||||
class Rumanhua : MMLook(
|
||||
"如漫画",
|
||||
"https://m.rumanhua1.com",
|
||||
"https://www.rumanhua1.com",
|
||||
useLegacyMangaUrl = true,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user