Fix Roumanwu (#10271)
This commit is contained in:
parent
c6bad74c45
commit
c514b4fc04
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Roumanwu'
|
||||
extClass = '.Roumanwu'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 17
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.roumanwu
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
@ -10,15 +11,19 @@ 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 keiyoushi.utils.getPreferences
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParsePosition
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
|
||||
class Roumanwu : ParsedHttpSource(), ConfigurableSource {
|
||||
class Roumanwu : HttpSource(), ConfigurableSource {
|
||||
override val name = "肉漫屋"
|
||||
override val lang = "zh"
|
||||
override val supportsLatest = true
|
||||
@ -29,40 +34,42 @@ class Roumanwu : ParsedHttpSource(), ConfigurableSource {
|
||||
max(MIRRORS.size - 1, preferences.getString(MIRROR_PREF, MIRROR_DEFAULT)!!.toInt()),
|
||||
]
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder().addInterceptor(ScrambledImageInterceptor).build()
|
||||
|
||||
private val imageUrlRegex = """\\"imageUrl\\":\\"([^\\]+)""".toRegex()
|
||||
override val client = network.cloudflareClient.newBuilder().addInterceptor(ScrambledImageInterceptor()).build()
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
override fun popularMangaSelector(): String = "div.px-1 > div:matches(正熱門|今日最佳|本週熱門) .grid a[href*=/books/]"
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.select("div.truncate").text()
|
||||
url = element.attr("href")
|
||||
thumbnail_url = element.select("div.bg-cover").attr("style").substringAfter("background-image:url(\"").substringBefore("\")")
|
||||
|
||||
private fun parseEntries(container: Element): List<SManga> {
|
||||
return container.select("a[href*=/books/]").map {
|
||||
SManga.create().apply {
|
||||
title = it.selectFirst("div.truncate")!!.text()
|
||||
url = it.attr("href")
|
||||
thumbnail_url = it.selectFirst("div.bg-cover")!!.attr("style").substringAfter("background-image:url(\"").substringBefore("\")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
return parseHomePage(document, Regex("正熱門|今日最佳|本週熱門"))
|
||||
}
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
val uniqueMangas = mangas.distinctBy { it.url }
|
||||
private fun parseHomePage(document: Document, sections: Regex): MangasPage {
|
||||
val entries = document.selectFirst("div.px-1")!!.children().flatMap { section ->
|
||||
if (section.child(0).text().contains(sections)) {
|
||||
parseEntries(section)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}.distinctBy { it.url }
|
||||
|
||||
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(uniqueMangas, hasNextPage)
|
||||
return MangasPage(entries, false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = popularMangaRequest(page)
|
||||
override fun latestUpdatesNextPageSelector(): String? = null
|
||||
override fun latestUpdatesSelector(): String = "div.px-1 > div:contains(最近更新) .grid a[href*=/books/]"
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.select("div.truncate").text()
|
||||
url = element.attr("href")
|
||||
thumbnail_url = element.select("div.bg-cover").attr("style").substringAfter("background-image:url(\"").substringBefore("\")")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
return parseHomePage(document, Regex("最近更新"))
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
@ -72,67 +79,94 @@ class Roumanwu : ParsedHttpSource(), ConfigurableSource {
|
||||
val parts = filters.filterIsInstance<UriPartFilter>().joinToString("") { it.toUriPart() }
|
||||
GET("$baseUrl/books?page=${page - 1}$parts", headers)
|
||||
}
|
||||
override fun searchMangaNextPageSelector(): String? = null
|
||||
override fun searchMangaSelector(): String = "a[href*=/books/]"
|
||||
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.select("div.truncate").text()
|
||||
url = element.attr("href")
|
||||
thumbnail_url = element.select("div.bg-cover").attr("style").substringAfter("background-image:url(\"").substringBefore("\")")
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val entries = parseEntries(document)
|
||||
val thisPage = response.request.url.queryParameter("page")!!
|
||||
val nextPage = document.selectFirst("div.justify-end > a:contains(下一頁)")!!
|
||||
.absUrl("href").toHttpUrl().queryParameter("page")!!
|
||||
return MangasPage(entries, thisPage != nextPage)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
title = document.select("div.basis-3\\/5 > div.text-xl").text()
|
||||
thumbnail_url = baseUrl + document.select("main > div:first-child img").attr("src")
|
||||
author = document.select("div.basis-3\\/5 > div:nth-child(3) span").text()
|
||||
artist = author
|
||||
status = when (document.select("div.basis-3\\/5 > div:nth-child(4) span").text()) {
|
||||
"連載中" -> SManga.ONGOING
|
||||
"已完結" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
||||
val document = response.asJsoup()
|
||||
val infobox = parseInfobox(document).iterator()
|
||||
|
||||
title = infobox.next()
|
||||
thumbnail_url = document.selectFirst("div.basis-2\\/5 img")!!.absUrl("src")
|
||||
.run { toHttpUrl().queryParameter("url") ?: this }
|
||||
description = document.selectFirst("p:contains(簡介:)")!!.text().substring(3)
|
||||
|
||||
val genres = ArrayList<String>()
|
||||
for (text in infobox) {
|
||||
val value = text.drop(3).trimStart()
|
||||
if (value.isEmpty()) continue
|
||||
when (text.take(3)) {
|
||||
"別名:" -> if (value != title) description = "$text\n\n$description"
|
||||
"作者:" -> author = value
|
||||
"狀態:" -> status = when (value) {
|
||||
"連載中" -> SManga.ONGOING
|
||||
"已完結" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
"地區:" -> genres.add(value)
|
||||
"標籤:" -> genres.addAll(value.split(","))
|
||||
}
|
||||
}
|
||||
genre = document.select("div.basis-3\\/5 > div:nth-child(6) span").text().replace(",", ", ")
|
||||
description = document.select("p:contains(簡介:)").text().substring(3)
|
||||
genre = genres.joinToString()
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): String = "a[href~=/books/.*/\\d+]"
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
url = element.attr("href")
|
||||
name = element.text()
|
||||
private fun parseInfobox(document: Document): List<String> {
|
||||
val infobox = document.selectFirst("div.basis-3\\/5")!!.children()
|
||||
check(infobox.size >= 6 && infobox[0].hasClass("text-xl"))
|
||||
return infobox.map { it.text() }
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return super.chapterListParse(response).reversed()
|
||||
val document = response.asJsoup()
|
||||
val chapters = document.select("a[href~=/books/.*/\\d+]").map {
|
||||
SChapter.create().apply {
|
||||
url = it.attr("href")
|
||||
name = it.text()
|
||||
}
|
||||
}.asReversed()
|
||||
if (chapters.isNotEmpty()) {
|
||||
val dateFormat = SimpleDateFormat("M/d/yyyy", Locale.US)
|
||||
for (text in parseInfobox(document).asReversed()) {
|
||||
val date = dateFormat.parse(text, ParsePosition(0)) ?: continue
|
||||
chapters[0].date_upload = date.time
|
||||
break
|
||||
}
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val images = document.selectFirst("script:containsData(imageUrl)")?.data()
|
||||
?.let { content ->
|
||||
imageUrlRegex
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val images = response.asJsoup().selectFirst("script:containsData(imageUrl)")!!.data()
|
||||
.let { content ->
|
||||
"""\\"imageUrl\\":\\"([^\\]+)""".toRegex()
|
||||
.findAll(content).map { it.groups[1]?.value }
|
||||
.toList()
|
||||
} ?: return emptyList()
|
||||
}
|
||||
|
||||
return images.mapIndexed { index, imageUrl ->
|
||||
Page(index, imageUrl = imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("提示:搜尋時篩選無效"),
|
||||
TagFilter(),
|
||||
StatusFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
|
||||
private abstract class UriPartFilter(name: String, values: Array<String>) : Filter.Select<String>(name, values) {
|
||||
abstract fun toUriPart(): String
|
||||
}
|
||||
|
||||
private class TagFilter : UriPartFilter("標籤", TAGS) {
|
||||
override fun toUriPart() = if (state == 0) "" else "&tag=${values[state]}"
|
||||
}
|
||||
|
||||
private class StatusFilter : UriPartFilter("狀態", arrayOf("全部", "連載中", "已完結")) {
|
||||
override fun toUriPart() =
|
||||
when (state) {
|
||||
@ -142,17 +176,13 @@ class Roumanwu : ParsedHttpSource(), ConfigurableSource {
|
||||
}
|
||||
}
|
||||
|
||||
private class SortFilter : UriPartFilter("排序", arrayOf("更新日期", "評分")) {
|
||||
override fun toUriPart() = if (state == 0) "" else "&sort=rating"
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val mirrorPref = androidx.preference.ListPreference(screen.context).apply {
|
||||
val mirrorPref = ListPreference(screen.context).apply {
|
||||
key = MIRROR_PREF
|
||||
title = MIRROR_PREF_TITLE
|
||||
title = "使用鏡像網址"
|
||||
entries = MIRRORS_DESC
|
||||
entryValues = MIRRORS.indices.map(Int::toString).toTypedArray()
|
||||
summary = MIRROR_PREF_SUMMARY
|
||||
entryValues = Array(MIRRORS.size, Int::toString)
|
||||
summary = "使用鏡像網址。重啟軟體生效。"
|
||||
|
||||
setDefaultValue(MIRROR_DEFAULT)
|
||||
}
|
||||
@ -161,14 +191,10 @@ class Roumanwu : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
companion object {
|
||||
private const val MIRROR_PREF = "MIRROR"
|
||||
private const val MIRROR_PREF_TITLE = "使用鏡像網址"
|
||||
private const val MIRROR_PREF_SUMMARY = "使用鏡像網址。重啟軟體生效。"
|
||||
|
||||
// 地址: https://rou.pub/dizhi
|
||||
private val MIRRORS get() = arrayOf("https://rouman5.com", "https://roum20.xyz")
|
||||
// 地址: https://rou.pub/dizhi or https://rdz1.xyz/dizhi
|
||||
private val MIRRORS get() = arrayOf("https://rouman5.com", "https://roum22.xyz")
|
||||
private val MIRRORS_DESC get() = arrayOf("主站", "鏡像")
|
||||
private const val MIRROR_DEFAULT = 1.toString() // use mirror
|
||||
|
||||
private val TAGS get() = arrayOf("全部", "\u6B63\u59B9", "\u604B\u7231", "\u51FA\u7248\u6F2B\u753B", "\u8089\u617E", "\u6D6A\u6F2B", "\u5927\u5C3A\u5EA6", "\u5DE8\u4E73", "\u6709\u592B\u4E4B\u5A66", "\u5973\u5927\u751F", "\u72D7\u8840\u5287", "\u540C\u5C45", "\u597D\u53CB", "\u8ABF\u6559", "\u52A8\u4F5C", "\u5F8C\u5BAE", "\u4E0D\u502B")
|
||||
}
|
||||
}
|
||||
|
@ -8,25 +8,25 @@ import android.util.Base64
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import java.io.ByteArrayOutputStream
|
||||
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||
import okio.Buffer
|
||||
import java.security.MessageDigest
|
||||
|
||||
object ScrambledImageInterceptor : Interceptor {
|
||||
class ScrambledImageInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
val url = request.url.toString()
|
||||
if ("sr:1" !in url) return response
|
||||
val image = BitmapFactory.decodeStream(response.body.byteStream())
|
||||
val url = request.url
|
||||
if ("sr:1" !in url.pathSegments) return response
|
||||
val image = response.body.use { BitmapFactory.decodeStream(it.byteStream()) }
|
||||
val width = image.width
|
||||
val height = image.height
|
||||
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
|
||||
// /_next/static/chunks/pages/books/%5Bbookid%5D/%5Bid%5D-6f60a589e82dc8db.js
|
||||
// /_next/static/chunks/app/books/[bookId]/[ind]/page-eaacab94dbec1fa4.js
|
||||
// Scrambled images are reversed by blocks. Remainder is included in the bottom (scrambled) block.
|
||||
val blocks = url.removeSuffix(SCRAMBLED_SUFFIX).substringAfterLast('/').removeSuffix(".jpg")
|
||||
val blocks = url.pathSegments.last().substringBeforeLast('.')
|
||||
.let { Base64.decode(it, Base64.DEFAULT) }
|
||||
.let { MessageDigest.getInstance("MD5").digest(it) } // thread-safe
|
||||
.let { it.last().toPositiveInt() % 10 + 5 }
|
||||
@ -42,13 +42,12 @@ object ScrambledImageInterceptor : Interceptor {
|
||||
cy += h
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
|
||||
val responseBody = output.toByteArray().toResponseBody(jpegMediaType)
|
||||
val responseBody = Buffer().run {
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 90, outputStream())
|
||||
asResponseBody("image/jpeg".toMediaType())
|
||||
}
|
||||
return response.newBuilder().body(responseBody).build()
|
||||
}
|
||||
|
||||
private val jpegMediaType = "image/jpeg".toMediaType()
|
||||
private fun Byte.toPositiveInt() = toInt() and 0xFF
|
||||
const val SCRAMBLED_SUFFIX = "#scrambled"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user