Kemono: fix cache and post list, update domain (#10134)
* Kemono: fix cache and post list, update domain * update
This commit is contained in:
parent
e61892ced7
commit
9ea67f22dd
@ -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")
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user