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:
oalieno 2024-10-14 19:43:35 +08:00 committed by Draff
parent 1cfbe474eb
commit e1538a3be9
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
4 changed files with 117 additions and 138 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'Roumanwu'
extClass = '.Roumanwu'
extVersionCode = 10
extVersionCode = 11
isNsfw = true
}

View File

@ -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
}

View File

@ -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)

View File

@ -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