Kemono: support new design and paginate result (#14014)
This commit is contained in:
parent
c8ed15f451
commit
da1348c813
|
@ -15,15 +15,20 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okio.blackholeSink
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.select.Evaluator
|
import org.jsoup.select.Evaluator
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.IOException
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
open class Kemono(
|
open class Kemono(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
|
@ -32,6 +37,8 @@ open class Kemono(
|
||||||
) : HttpSource(), ConfigurableSource {
|
) : HttpSource(), ConfigurableSource {
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val isNewDesign get() = name == "Kemono"
|
||||||
|
|
||||||
override val client = network.client.newBuilder().rateLimit(2).build()
|
override val client = network.client.newBuilder().rateLimit(2).build()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
@ -71,15 +78,86 @@ open class Kemono(
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||||
|
|
||||||
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
|
if (!isNewDesign) return super.fetchPopularManga(page)
|
||||||
|
return Observable.fromCallable {
|
||||||
|
fetchNewDesignListing(page, "/artists", compareByDescending { it.favorited })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||||
|
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<KemonoCreatorDto>,
|
||||||
|
): 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<MangasPage> = Observable.fromCallable {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = 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<KemonoCreatorDto>) -> List<KemonoCreatorDto>,
|
||||||
|
): MangasPage {
|
||||||
val baseUrl = this.baseUrl
|
val baseUrl = this.baseUrl
|
||||||
val response = client.newCall(GET("$baseUrl/api/creators", headers)).execute()
|
val response = client.newCall(GET("$baseUrl/api/creators", headers)).execute()
|
||||||
val result = response.parseAs<List<KemonoCreatorDto>>()
|
val allCreators = block(response.parseAs())
|
||||||
.filter { it.name.contains(query, ignoreCase = true) }
|
val count = allCreators.size
|
||||||
.sortedByDescending { it.updatedDate }
|
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) }
|
.map { it.toSManga(baseUrl) }
|
||||||
.filterUnsupported()
|
.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.")
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Not used.")
|
||||||
|
@ -140,6 +218,7 @@ open class Kemono(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PAGE_SIZE = 25
|
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."
|
const val PROMPT = "You can change how many posts to load in the extension preferences."
|
||||||
|
|
||||||
private const val POST_PAGE_SIZE = 50
|
private const val POST_PAGE_SIZE = 50
|
||||||
|
|
|
@ -14,6 +14,7 @@ class KemonoCreatorDto(
|
||||||
val name: String,
|
val name: String,
|
||||||
private val service: String,
|
private val service: String,
|
||||||
private val updated: JsonPrimitive,
|
private val updated: JsonPrimitive,
|
||||||
|
val favorited: Int = -1,
|
||||||
) {
|
) {
|
||||||
val updatedDate get() = when {
|
val updatedDate get() = when {
|
||||||
updated.isString -> dateFormat.parse(updated.content)?.time ?: 0
|
updated.isString -> dateFormat.parse(updated.content)?.time ?: 0
|
||||||
|
|
|
@ -6,7 +6,7 @@ import generator.ThemeSourceGenerator
|
||||||
class KemonoGenerator : ThemeSourceGenerator {
|
class KemonoGenerator : ThemeSourceGenerator {
|
||||||
override val themeClass = "Kemono"
|
override val themeClass = "Kemono"
|
||||||
override val themePkg = "kemono"
|
override val themePkg = "kemono"
|
||||||
override val baseVersionCode = 3
|
override val baseVersionCode = 4
|
||||||
override val sources = listOf(
|
override val sources = listOf(
|
||||||
SingleLang("Kemono", "https://kemono.party", "all", isNsfw = true),
|
SingleLang("Kemono", "https://kemono.party", "all", isNsfw = true),
|
||||||
SingleLang("Coomer", "https://coomer.party", "all", isNsfw = true)
|
SingleLang("Coomer", "https://coomer.party", "all", isNsfw = true)
|
||||||
|
|
Loading…
Reference in New Issue