diff --git a/src/zh/roumanwu/build.gradle b/src/zh/roumanwu/build.gradle index 8ffe53d70..4fb360371 100644 --- a/src/zh/roumanwu/build.gradle +++ b/src/zh/roumanwu/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Roumanwu' extClass = '.Roumanwu' - extVersionCode = 10 + extVersionCode = 11 isNsfw = true } diff --git a/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/Roumanwu.kt b/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/Roumanwu.kt index 37fe29968..082940521 100644 --- a/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/Roumanwu.kt +++ b/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/Roumanwu.kt @@ -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().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().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().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().joinToString("") { it.toUriPart() } GET("$baseUrl/books?page=${page - 1}$parts", headers) } - - override fun mangaDetailsParse(response: Response) = response.nextjsData().book.toSManga() - - override fun chapterListParse(response: Response) = response.nextjsData().book.getChapterList().reversed() - - override fun pageListParse(response: Response): List { - val chapter = response.nextjsData() - 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().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 { + return super.chapterListParse(response).reversed() + } + + override fun pageListParse(document: Document): List { + 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 Response.parseAs(): T = json.decodeFromStream(this.body.byteStream()) - - private inline fun Response.nextjsData() = - json.decodeFromString>(this.asJsoup().select("#__NEXT_DATA__").html()).props.pageProps } diff --git a/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/RoumanwuDto.kt b/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/RoumanwuDto.kt deleted file mode 100644 index 4e7140b43..000000000 --- a/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/RoumanwuDto.kt +++ /dev/null @@ -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(val props: Props) - -@Serializable -data class Props(val pageProps: T) - -@Serializable -data class Book( - val id: String, - val name: String, -// val alias: List, - val description: String, - val coverUrl: String, - val author: String, - val continued: Boolean, - val tags: List, - 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) - -@Serializable -data class BookList(val books: List, val hasNextPage: Boolean) { - fun toMangasPage() = MangasPage(books.map(Book::toSManga), hasNextPage) -} - -@Serializable -data class HomePage( - val headline: Book, - val best: List, - val hottest: List, - val daily: List, - val recentUpdatedBooks: List, - val endedBooks: List, -) { - fun getPopular() = (listOf(headline) + best + hottest + daily + endedBooks).distinct() -} - -fun List.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? = 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) diff --git a/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/ScrambledImageInterceptor.kt b/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/ScrambledImageInterceptor.kt index e71df4043..bfabcbb69 100644 --- a/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/ScrambledImageInterceptor.kt +++ b/src/zh/roumanwu/src/eu/kanade/tachiyomi/extension/zh/roumanwu/ScrambledImageInterceptor.kt @@ -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