Kemono: fix cache and post list, update domain (#10134)

* Kemono: fix cache and post list, update domain

* update
This commit is contained in:
stevenyomi 2025-08-15 18:29:54 +00:00 committed by Draff
parent e61892ced7
commit 9ea67f22dd
Signed by: Draff
GPG Key ID: E8A89F3211677653
5 changed files with 87 additions and 38 deletions

View File

@ -2,4 +2,8 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 22 baseVersionCode = 23
dependencies {
compileOnly("com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.11")
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.kemono package eu.kanade.tachiyomi.multisrc.kemono
import android.app.Application
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
@ -15,14 +16,19 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferences import keiyoushi.utils.getPreferences
import kotlinx.serialization.json.Json import keiyoushi.utils.parseAs
import kotlinx.serialization.json.decodeFromStream import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.brotli.BrotliInterceptor
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.lang.Thread.sleep import java.lang.Thread.sleep
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.TimeUnit
import kotlin.math.min import kotlin.math.min
open class Kemono( open class Kemono(
@ -32,13 +38,35 @@ open class Kemono(
) : HttpSource(), ConfigurableSource { ) : HttpSource(), ConfigurableSource {
override val supportsLatest = true override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder().rateLimit(1).build() override val client = network.cloudflareClient.newBuilder()
.rateLimit(1)
.addInterceptor { chain ->
val request = chain.request()
if (request.url.pathSegments.first() == "api") {
chain.proceed(request.newBuilder().header("Accept", "text/css").build())
} else {
chain.proceed(request)
}
}
.apply {
val index = networkInterceptors().indexOfFirst { it is BrotliInterceptor }
if (index >= 0) interceptors().add(networkInterceptors().removeAt(index))
}
.cache(
Cache(
directory = File(Injekt.get<Application>().externalCacheDir, "network_cache_${name.lowercase()}"),
maxSize = 50L * 1024 * 1024, // 50 MiB
),
)
.build()
private val creatorsClient = client.newBuilder()
.readTimeout(5, TimeUnit.MINUTES)
.build()
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/") .add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
private val preferences = getPreferences() private val preferences = getPreferences()
private val apiPath = "api/v1" private val apiPath = "api/v1"
@ -47,8 +75,6 @@ open class Kemono(
private val imgCdnUrl = baseUrl.replace("//", "//img.") private val imgCdnUrl = baseUrl.replace("//", "//img.")
private var mangasCache: List<KemonoCreatorDto> = emptyList()
private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl) private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl)
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
@ -85,6 +111,7 @@ open class Kemono(
is SortFilter -> { is SortFilter -> {
sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc" sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc"
} }
is TypeFilter -> { is TypeFilter -> {
filter.state.filter { state -> state.isIncluded() }.forEach { tri -> filter.state.filter { state -> state.isIncluded() }.forEach { tri ->
typeIncluded.add(tri.value) typeIncluded.add(tri.value)
@ -94,44 +121,60 @@ open class Kemono(
typeExcluded.add(tri.value) typeExcluded.add(tri.value)
} }
} }
is FavouritesFilter -> {
is FavoritesFilter -> {
fav = when (filter.state[0].state) { fav = when (filter.state[0].state) {
0 -> null 0 -> null
1 -> true 1 -> true
else -> false else -> false
} }
} }
else -> {} else -> {}
} }
} }
var mangas = mangasCache val mangas = run {
if (page == 1 || mangasCache.isEmpty()) { val favorites = if (fav != null) {
var favourites: List<KemonoFavouritesDto> = emptyList() val response = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
if (fav != null) {
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
if (favores.code == 401) throw Exception("You are not Logged In") if (response.isSuccessful) {
favourites = favores.parseAs<List<KemonoFavouritesDto>>().filterNot { it.service.lowercase() == "discord" } response.parseAs<List<KemonoFavoritesDto>>().filterNot { it.service.lowercase() == "discord" }
} else {
response.close()
val message = if (response.code == 401) "You are not logged in" else "HTTP error ${response.code}"
throw Exception("Failed to fetch favorites: $message")
}
} else {
emptyList()
} }
val response = client.newCall(GET("$baseUrl/$apiPath/creators", headers)).execute() val request = GET(
"$baseUrl/$apiPath/creators",
headers,
CacheControl.Builder().maxStale(30, TimeUnit.MINUTES).build(),
)
val response = creatorsClient.newCall(request).execute()
if (!response.isSuccessful) {
response.close()
throw Exception("HTTP error ${response.code}")
}
val allCreators = response.parseAs<List<KemonoCreatorDto>>().filterNot { it.service.lowercase() == "discord" } val allCreators = response.parseAs<List<KemonoCreatorDto>>().filterNot { it.service.lowercase() == "discord" }
mangas = allCreators.filter { allCreators.filter {
val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase()) val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase())
val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase()) val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase())
val regularSearch = it.name.contains(title, true) val regularSearch = it.name.contains(title, true)
val isFavourited = when (fav) { val isFavorited = when (fav) {
true -> favourites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } } true -> favorites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } }
false -> favourites.none { f -> f.id == it.id } false -> favorites.none { f -> f.id == it.id }
else -> true else -> true
} }
includeType && !excludeType && isFavourited && includeType && !excludeType && isFavorited &&
regularSearch regularSearch
}.also { mangasCache = it } }
} }
val sorted = when (sort.first) { val sorted = when (sort.first) {
@ -142,6 +185,7 @@ open class Kemono(
mangas.sortedBy { it.favorited } mangas.sortedBy { it.favorited }
} }
} }
"tit" -> { "tit" -> {
if (sort.second == "desc") { if (sort.second == "desc") {
mangas.sortedByDescending { it.name } mangas.sortedByDescending { it.name }
@ -149,6 +193,7 @@ open class Kemono(
mangas.sortedBy { it.name } mangas.sortedBy { it.name }
} }
} }
"new" -> { "new" -> {
if (sort.second == "desc") { if (sort.second == "desc") {
mangas.sortedByDescending { it.id } mangas.sortedByDescending { it.id }
@ -156,14 +201,16 @@ open class Kemono(
mangas.sortedBy { it.id } mangas.sortedBy { it.id }
} }
} }
"fav" -> { "fav" -> {
if (fav != true) throw Exception("Please check 'Favourites Only' Filter") if (fav != true) throw Exception("Please check 'Favorites Only' Filter")
if (sort.second == "desc") { if (sort.second == "desc") {
mangas.sortedByDescending { it.fav } mangas.sortedByDescending { it.fav }
} else { } else {
mangas.sortedBy { it.fav } mangas.sortedBy { it.fav }
} }
} }
else -> { else -> {
if (sort.second == "desc") { if (sort.second == "desc") {
mangas.sortedByDescending { it.updatedDate } mangas.sortedByDescending { it.updatedDate }
@ -203,7 +250,7 @@ open class Kemono(
var hasNextPage = true var hasNextPage = true
val result = ArrayList<SChapter>() val result = ArrayList<SChapter>()
while (offset < prefMaxPost && hasNextPage) { while (offset < prefMaxPost && hasNextPage) {
val request = GET("$baseUrl/$apiPath${manga.url}?o=$offset", headers) val request = GET("$baseUrl/$apiPath${manga.url}/posts?o=$offset", headers)
val page: List<KemonoPostDto> = retry(request).parseAs() val page: List<KemonoPostDto> = retry(request).parseAs()
page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) } page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) }
offset += PAGE_POST_LIMIT offset += PAGE_POST_LIMIT
@ -252,10 +299,6 @@ open class Kemono(
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
}
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = POST_PAGES_PREF key = POST_PAGES_PREF
@ -284,7 +327,7 @@ open class Kemono(
getSortsList, getSortsList,
), ),
TypeFilter("Types", getTypes), TypeFilter("Types", getTypes),
FavouritesFilter(), FavoritesFilter(),
) )
open val getTypes: List<String> = emptyList() open val getTypes: List<String> = emptyList()
@ -295,7 +338,7 @@ open class Kemono(
Pair("Date Updated", "lat"), Pair("Date Updated", "lat"),
Pair("Alphabetical Order", "tit"), Pair("Alphabetical Order", "tit"),
Pair("Service", "serv"), Pair("Service", "serv"),
Pair("Date Favourited", "fav"), Pair("Date Favorited", "fav"),
) )
internal open class TypeFilter(name: String, vals: List<String>) : internal open class TypeFilter(name: String, vals: List<String>) :
@ -304,17 +347,19 @@ open class Kemono(
vals.map { TriFilter(it, it.lowercase()) }, vals.map { TriFilter(it, it.lowercase()) },
) )
internal class FavouritesFilter() : internal class FavoritesFilter() :
Filter.Group<TriFilter>( Filter.Group<TriFilter>(
"Favourites", "Favorites",
listOf(TriFilter("Favourites Only", "fav")), listOf(TriFilter("Favorites Only", "fav")),
) )
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name) internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) : internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) { Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
fun getValue() = vals[state!!.index].second fun getValue() = vals[state!!.index].second
} }
companion object { companion object {
private const val PAGE_POST_LIMIT = 50 private const val PAGE_POST_LIMIT = 50
private const val PAGE_CREATORS_LIMIT = 50 private const val PAGE_CREATORS_LIMIT = 50

View File

@ -10,7 +10,7 @@ import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@Serializable @Serializable
class KemonoFavouritesDto( class KemonoFavoritesDto(
val id: String, val id: String,
val name: String, val name: String,
val service: String, val service: String,

View File

@ -2,7 +2,7 @@ ext {
extName = 'Coomer' extName = 'Coomer'
extClass = '.Coomer' extClass = '.Coomer'
themePkg = 'kemono' themePkg = 'kemono'
baseUrl = 'https://coomer.su' baseUrl = 'https://coomer.st'
overrideVersionCode = 0 overrideVersionCode = 0
isNsfw = true isNsfw = true
} }

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.extension.all.coomer
import eu.kanade.tachiyomi.multisrc.kemono.Kemono import eu.kanade.tachiyomi.multisrc.kemono.Kemono
class Coomer : Kemono("Coomer", "https://coomer.su", "all") { class Coomer : Kemono("Coomer", "https://coomer.st", "all") {
override val getTypes = listOf( override val getTypes = listOf(
"OnlyFans", "OnlyFans",
"Fansly", "Fansly",