Roumanwu: Rewrite to make it works on new website (#5492)
* Roumanwu: Rewrite to make it works on new website * Roumanwu: refactor and change MIRROR url
This commit is contained in:
parent
1cfbe474eb
commit
e1538a3be9
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Roumanwu'
|
||||
extClass = '.Roumanwu'
|
||||
extVersionCode = 10
|
||||
extVersionCode = 11
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -7,19 +7,27 @@ import eu.kanade.tachiyomi.network.GET
|
|||
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.online.HttpSource
|
||||
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.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.math.max
|
||||
|
||||
class Roumanwu : HttpSource(), ConfigurableSource {
|
||||
class Roumanwu : ParsedHttpSource(), ConfigurableSource {
|
||||
override val name = "肉漫屋"
|
||||
override val lang = "zh"
|
||||
override val supportsLatest = true
|
||||
|
@ -35,13 +43,40 @@ class Roumanwu : HttpSource(), ConfigurableSource {
|
|||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val imageUrlRegex = """(?<=\[1,").*(?="\])""".toRegex()
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
|
||||
override fun popularMangaParse(response: Response) = response.nextjsData<HomePage>().getPopular().toMangasPage()
|
||||
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("\")")
|
||||
}
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
val uniqueMangas = mangas.distinctBy { it.url }
|
||||
|
||||
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(uniqueMangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = popularMangaRequest(page)
|
||||
override fun latestUpdatesParse(response: Response) = response.nextjsData<HomePage>().recentUpdatedBooks.toMangasPage()
|
||||
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 searchMangaParse(response: Response) = response.nextjsData<BookList>().toMangasPage()
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
if (query.isNotBlank()) {
|
||||
GET("$baseUrl/search?term=$query&page=${page - 1}", headers)
|
||||
|
@ -49,28 +84,79 @@ class Roumanwu : HttpSource(), ConfigurableSource {
|
|||
val parts = filters.filterIsInstance<UriPartFilter>().joinToString("") { it.toUriPart() }
|
||||
GET("$baseUrl/books?page=${page - 1}$parts", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = response.nextjsData<BookDetails>().book.toSManga()
|
||||
|
||||
override fun chapterListParse(response: Response) = response.nextjsData<BookDetails>().book.getChapterList().reversed()
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val chapter = response.nextjsData<Chapter>()
|
||||
if (chapter.statusCode != null) throw Exception("服务器错误: ${chapter.statusCode}")
|
||||
return if (chapter.images != null) {
|
||||
chapter.getPageList()
|
||||
} else {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val response = client.newCall(GET(baseUrl + chapter.chapterAPIPath!!, headers)).execute()
|
||||
if (!response.isSuccessful) throw Exception("服务器错误: ${response.code}")
|
||||
response.parseAs<ChapterWrapper>().chapter.getPageList()
|
||||
}
|
||||
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 imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
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
|
||||
}
|
||||
genre = document.select("div.basis-3\\/5 > div:nth-child(6) span").text().replace(",", ", ")
|
||||
description = document.select("p:contains(簡介:)").text().substring(3)
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): String = "a[href~=/books/.*/\\d+]"
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
url = element.attr("href")
|
||||
name = element.text()
|
||||
}
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return super.chapterListParse(response).reversed()
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val jsonString = document.selectFirst("script:containsData(imageUrl)")?.data()
|
||||
?.let { content ->
|
||||
imageUrlRegex
|
||||
.find(content)
|
||||
?.value
|
||||
?.substring(2)
|
||||
?.dropLast(2)
|
||||
?.replace("\\\"", "\"")
|
||||
}
|
||||
|
||||
return jsonString?.let { str ->
|
||||
val jo = json.parseToJsonElement(str)
|
||||
val pagesJson = jo.jsonArray
|
||||
.getOrNull(3)?.jsonObject
|
||||
?.get("children")?.jsonArray
|
||||
?.getOrNull(6)?.jsonArray
|
||||
?.getOrNull(3)?.jsonObject
|
||||
?.get("children")?.jsonArray
|
||||
|
||||
pagesJson?.mapNotNull { pageElement ->
|
||||
val pageData = pageElement.jsonArray
|
||||
.getOrNull(3)?.jsonObject
|
||||
?.get("children")?.jsonArray
|
||||
?.getOrNull(3)?.jsonObject
|
||||
|
||||
val index = pageData?.get("ind")?.jsonPrimitive?.intOrNull
|
||||
val imageUrl = pageData?.get("imageUrl")?.jsonPrimitive?.contentOrNull
|
||||
|
||||
if (index != null && imageUrl != null) {
|
||||
Page(index, imageUrl = imageUrl)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: emptyList()
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("提示:搜索时筛选无效"),
|
||||
Filter.Header("提示:搜尋時篩選無效"),
|
||||
TagFilter(),
|
||||
StatusFilter(),
|
||||
SortFilter(),
|
||||
|
@ -112,19 +198,14 @@ class Roumanwu : HttpSource(), ConfigurableSource {
|
|||
|
||||
companion object {
|
||||
private const val MIRROR_PREF = "MIRROR"
|
||||
private const val MIRROR_PREF_TITLE = "使用镜像网址"
|
||||
private const val MIRROR_PREF_SUMMARY = "使用镜像网址。重启软件生效。"
|
||||
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://roum16.xyz")
|
||||
private val MIRRORS_DESC get() = arrayOf("主站", "镜像")
|
||||
private val MIRRORS get() = arrayOf("https://rouman5.com", "https://roum18.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")
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = json.decodeFromStream(this.body.byteStream())
|
||||
|
||||
private inline fun <reified T> Response.nextjsData() =
|
||||
json.decodeFromString<NextData<T>>(this.asJsoup().select("#__NEXT_DATA__").html()).props.pageProps
|
||||
}
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.roumanwu
|
||||
|
||||
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 kotlinx.serialization.Serializable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class NextData<T>(val props: Props<T>)
|
||||
|
||||
@Serializable
|
||||
data class Props<T>(val pageProps: T)
|
||||
|
||||
@Serializable
|
||||
data class Book(
|
||||
val id: String,
|
||||
val name: String,
|
||||
// val alias: List<String>,
|
||||
val description: String,
|
||||
val coverUrl: String,
|
||||
val author: String,
|
||||
val continued: Boolean,
|
||||
val tags: List<String>,
|
||||
val updatedAt: String? = null,
|
||||
val activeResource: Resource? = null,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = "/books/$id"
|
||||
title = name
|
||||
author = this@Book.author
|
||||
description = this@Book.description
|
||||
genre = tags.joinToString(", ")
|
||||
status = if (continued) SManga.ONGOING else SManga.COMPLETED
|
||||
thumbnail_url = coverUrl
|
||||
}
|
||||
|
||||
/** 正序 */
|
||||
fun getChapterList() = activeResource!!.chapters.mapIndexed { i, it ->
|
||||
SChapter.create().apply {
|
||||
url = "/books/$id/$i"
|
||||
name = it
|
||||
}
|
||||
}.apply {
|
||||
if (!updatedAt.isNullOrBlank()) {
|
||||
this[lastIndex].date_upload = DATE_FORMAT.parse(updatedAt)?.time ?: 0L
|
||||
}
|
||||
}
|
||||
|
||||
private val uuid by lazy { UUID.fromString(id) }
|
||||
override fun hashCode() = uuid.hashCode()
|
||||
override fun equals(other: Any?) = other is Book && uuid == other.uuid
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Resource(val chapters: List<String>)
|
||||
|
||||
@Serializable
|
||||
data class BookList(val books: List<Book>, val hasNextPage: Boolean) {
|
||||
fun toMangasPage() = MangasPage(books.map(Book::toSManga), hasNextPage)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class HomePage(
|
||||
val headline: Book,
|
||||
val best: List<Book>,
|
||||
val hottest: List<Book>,
|
||||
val daily: List<Book>,
|
||||
val recentUpdatedBooks: List<Book>,
|
||||
val endedBooks: List<Book>,
|
||||
) {
|
||||
fun getPopular() = (listOf(headline) + best + hottest + daily + endedBooks).distinct()
|
||||
}
|
||||
|
||||
fun List<Book>.toMangasPage() = MangasPage(this.map(Book::toSManga), false)
|
||||
|
||||
@Serializable
|
||||
data class BookDetails(val book: Book)
|
||||
|
||||
@Serializable
|
||||
data class Chapter(
|
||||
val statusCode: Int? = null,
|
||||
val images: List<Image>? = null,
|
||||
val chapterAPIPath: String? = null,
|
||||
) {
|
||||
fun getPageList() = images!!.mapIndexed { i, it ->
|
||||
Page(i, imageUrl = it.src + if (it.scramble) ScrambledImageInterceptor.SCRAMBLED_SUFFIX else "")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChapterWrapper(val chapter: Chapter)
|
||||
|
||||
@Serializable
|
||||
data class Image(val src: String, val scramble: Boolean)
|
|
@ -17,7 +17,7 @@ object ScrambledImageInterceptor : Interceptor {
|
|||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
val url = request.url.toString()
|
||||
if (!url.endsWith(SCRAMBLED_SUFFIX)) return response
|
||||
if ("sr:1" !in url) return response
|
||||
val image = BitmapFactory.decodeStream(response.body.byteStream())
|
||||
val width = image.width
|
||||
val height = image.height
|
||||
|
|
Loading…
Reference in New Issue