From da1348c8130d93aa34abfab02a364e9db4c00f81 Mon Sep 17 00:00:00 2001 From: stevenyomi <95685115+stevenyomi@users.noreply.github.com> Date: Fri, 28 Oct 2022 21:53:15 +0800 Subject: [PATCH] Kemono: support new design and paginate result (#14014) --- .../tachiyomi/multisrc/kemono/Kemono.kt | 87 ++++++++++++++++++- .../tachiyomi/multisrc/kemono/KemonoDto.kt | 1 + .../multisrc/kemono/KemonoGenerator.kt | 2 +- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt index 9b5555abe..21d3b35a0 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt @@ -15,15 +15,20 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.asJsoup import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream +import okhttp3.Call +import okhttp3.Callback import okhttp3.Request import okhttp3.Response +import okio.blackholeSink import org.jsoup.nodes.Element import org.jsoup.select.Evaluator import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy +import java.io.IOException import java.util.TimeZone +import kotlin.math.min open class Kemono( override val name: String, @@ -32,6 +37,8 @@ open class Kemono( ) : HttpSource(), ConfigurableSource { override val supportsLatest = true + private val isNewDesign get() = name == "Kemono" + override val client = network.client.newBuilder().rateLimit(2).build() override fun headersBuilder() = super.headersBuilder() @@ -71,15 +78,86 @@ open class Kemono( override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + override fun fetchPopularManga(page: Int): Observable { + if (!isNewDesign) return super.fetchPopularManga(page) + return Observable.fromCallable { + fetchNewDesignListing(page, "/artists", compareByDescending { it.favorited }) + } + } + + override fun fetchLatestUpdates(page: Int): Observable { + if (!isNewDesign) return super.fetchLatestUpdates(page) + return Observable.fromCallable { + fetchNewDesignListing(page, "/artists/updated", compareByDescending { it.updatedDate }) + } + } + + private fun fetchNewDesignListing( + page: Int, + path: String, + comparator: Comparator, + ): MangasPage { + val baseUrl = baseUrl + return if (page == 1) { + val document = client.newCall(GET(baseUrl + path, headers)).execute().asJsoup() + val cardList = document.selectFirst(Evaluator.Class("card-list__items")) + val creators = cardList.children().map { + SManga.create().apply { + url = it.attr("href") + title = it.selectFirst(Evaluator.Class("user-card__name")).ownText() + author = it.selectFirst(Evaluator.Class("user-card__service")).ownText() + thumbnail_url = baseUrl + it.selectFirst(Evaluator.Tag("img")).attr("src") + description = PROMPT + initialized = true + } + }.filterUnsupported() + MangasPage(creators, true).also { cacheCreators() } + } else { + fetchCreatorsPage(page) { it.apply { sortWith(comparator) } } + } + } + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { + if (query.isBlank()) throw Exception("Query is empty") + fetchCreatorsPage(page) { all -> + val result = all.filterTo(ArrayList()) { it.name.contains(query, ignoreCase = true) } + if (result.isEmpty()) return@fetchCreatorsPage emptyList() + if (result[0].favorited != -1) { + result.sortByDescending { it.favorited } + } else { + result.sortByDescending { it.updatedDate } + } + result + } + } + + private fun fetchCreatorsPage( + page: Int, + block: (ArrayList) -> List, + ): MangasPage { val baseUrl = this.baseUrl val response = client.newCall(GET("$baseUrl/api/creators", headers)).execute() - val result = response.parseAs>() - .filter { it.name.contains(query, ignoreCase = true) } - .sortedByDescending { it.updatedDate } + val allCreators = block(response.parseAs()) + val count = allCreators.size + val fromIndex = (page - 1) * NEW_PAGE_SIZE + val toIndex = min(count, fromIndex + NEW_PAGE_SIZE) + val creators = allCreators.subList(fromIndex, toIndex) .map { it.toSManga(baseUrl) } .filterUnsupported() - MangasPage(result, false) + return MangasPage(creators, toIndex < count) + } + + private fun cacheCreators() { + val callback = object : Callback { + override fun onResponse(call: Call, response: Response) = + response.body!!.source().run { + readAll(blackholeSink()) + close() + } + + override fun onFailure(call: Call, e: IOException) = Unit + } + client.newCall(GET("$baseUrl/api/creators", headers)).enqueue(callback) } override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Not used.") @@ -140,6 +218,7 @@ open class Kemono( companion object { private const val PAGE_SIZE = 25 + private const val NEW_PAGE_SIZE = 50 const val PROMPT = "You can change how many posts to load in the extension preferences." private const val POST_PAGE_SIZE = 50 diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt index d7ed5622e..05612e37f 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt @@ -14,6 +14,7 @@ class KemonoCreatorDto( val name: String, private val service: String, private val updated: JsonPrimitive, + val favorited: Int = -1, ) { val updatedDate get() = when { updated.isString -> dateFormat.parse(updated.content)?.time ?: 0 diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoGenerator.kt index 24509c546..9bc59187d 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoGenerator.kt @@ -6,7 +6,7 @@ import generator.ThemeSourceGenerator class KemonoGenerator : ThemeSourceGenerator { override val themeClass = "Kemono" override val themePkg = "kemono" - override val baseVersionCode = 3 + override val baseVersionCode = 4 override val sources = listOf( SingleLang("Kemono", "https://kemono.party", "all", isNsfw = true), SingleLang("Coomer", "https://coomer.party", "all", isNsfw = true)