diff --git a/lib-multisrc/kemono/build.gradle.kts b/lib-multisrc/kemono/build.gradle.kts index 58b20df9b..9d4cd0971 100644 --- a/lib-multisrc/kemono/build.gradle.kts +++ b/lib-multisrc/kemono/build.gradle.kts @@ -2,4 +2,8 @@ plugins { id("lib-multisrc") } -baseVersionCode = 22 +baseVersionCode = 23 + +dependencies { + compileOnly("com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.11") +} diff --git a/lib-multisrc/kemono/src/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt b/lib-multisrc/kemono/src/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt index 1a4078e82..02b976a9c 100644 --- a/lib-multisrc/kemono/src/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt +++ b/lib-multisrc/kemono/src/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.multisrc.kemono +import android.app.Application import androidx.preference.ListPreference import androidx.preference.PreferenceScreen 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.online.HttpSource import keiyoushi.utils.getPreferences -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromStream +import keiyoushi.utils.parseAs +import okhttp3.Cache +import okhttp3.CacheControl import okhttp3.Request import okhttp3.Response +import okhttp3.brotli.BrotliInterceptor 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.util.TimeZone +import java.util.concurrent.TimeUnit import kotlin.math.min open class Kemono( @@ -32,13 +38,35 @@ open class Kemono( ) : HttpSource(), ConfigurableSource { 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().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() .add("Referer", "$baseUrl/") - private val json: Json by injectLazy() - private val preferences = getPreferences() private val apiPath = "api/v1" @@ -47,8 +75,6 @@ open class Kemono( private val imgCdnUrl = baseUrl.replace("//", "//img.") - private var mangasCache: List = emptyList() - private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl) override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() @@ -85,6 +111,7 @@ open class Kemono( is SortFilter -> { sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc" } + is TypeFilter -> { filter.state.filter { state -> state.isIncluded() }.forEach { tri -> typeIncluded.add(tri.value) @@ -94,44 +121,60 @@ open class Kemono( typeExcluded.add(tri.value) } } - is FavouritesFilter -> { + + is FavoritesFilter -> { fav = when (filter.state[0].state) { 0 -> null 1 -> true else -> false } } + else -> {} } } - var mangas = mangasCache - if (page == 1 || mangasCache.isEmpty()) { - var favourites: List = emptyList() - if (fav != null) { - val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute() + val mangas = run { + val favorites = if (fav != null) { + val response = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute() - if (favores.code == 401) throw Exception("You are not Logged In") - favourites = favores.parseAs>().filterNot { it.service.lowercase() == "discord" } + if (response.isSuccessful) { + response.parseAs>().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>().filterNot { it.service.lowercase() == "discord" } - mangas = allCreators.filter { + allCreators.filter { val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase()) val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase()) val regularSearch = it.name.contains(title, true) - val isFavourited = when (fav) { - true -> favourites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } } - false -> favourites.none { f -> f.id == it.id } + val isFavorited = when (fav) { + true -> favorites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } } + false -> favorites.none { f -> f.id == it.id } else -> true } - includeType && !excludeType && isFavourited && + includeType && !excludeType && isFavorited && regularSearch - }.also { mangasCache = it } + } } val sorted = when (sort.first) { @@ -142,6 +185,7 @@ open class Kemono( mangas.sortedBy { it.favorited } } } + "tit" -> { if (sort.second == "desc") { mangas.sortedByDescending { it.name } @@ -149,6 +193,7 @@ open class Kemono( mangas.sortedBy { it.name } } } + "new" -> { if (sort.second == "desc") { mangas.sortedByDescending { it.id } @@ -156,14 +201,16 @@ open class Kemono( mangas.sortedBy { it.id } } } + "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") { mangas.sortedByDescending { it.fav } } else { mangas.sortedBy { it.fav } } } + else -> { if (sort.second == "desc") { mangas.sortedByDescending { it.updatedDate } @@ -203,7 +250,7 @@ open class Kemono( var hasNextPage = true val result = ArrayList() 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 = retry(request).parseAs() page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) } offset += PAGE_POST_LIMIT @@ -252,10 +299,6 @@ open class Kemono( override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() - private inline fun Response.parseAs(): T = use { - json.decodeFromStream(it.body.byteStream()) - } - override fun setupPreferenceScreen(screen: PreferenceScreen) { ListPreference(screen.context).apply { key = POST_PAGES_PREF @@ -284,7 +327,7 @@ open class Kemono( getSortsList, ), TypeFilter("Types", getTypes), - FavouritesFilter(), + FavoritesFilter(), ) open val getTypes: List = emptyList() @@ -295,7 +338,7 @@ open class Kemono( Pair("Date Updated", "lat"), Pair("Alphabetical Order", "tit"), Pair("Service", "serv"), - Pair("Date Favourited", "fav"), + Pair("Date Favorited", "fav"), ) internal open class TypeFilter(name: String, vals: List) : @@ -304,17 +347,19 @@ open class Kemono( vals.map { TriFilter(it, it.lowercase()) }, ) - internal class FavouritesFilter() : + internal class FavoritesFilter() : Filter.Group( - "Favourites", - listOf(TriFilter("Favourites Only", "fav")), + "Favorites", + listOf(TriFilter("Favorites Only", "fav")), ) + internal open class TriFilter(name: String, val value: String) : Filter.TriState(name) internal open class SortFilter(name: String, selection: Selection, private val vals: List>) : Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) { fun getValue() = vals[state!!.index].second } + companion object { private const val PAGE_POST_LIMIT = 50 private const val PAGE_CREATORS_LIMIT = 50 diff --git a/lib-multisrc/kemono/src/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt b/lib-multisrc/kemono/src/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt index 4f015f68f..fa68403f8 100644 --- a/lib-multisrc/kemono/src/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt +++ b/lib-multisrc/kemono/src/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt @@ -10,7 +10,7 @@ import java.text.SimpleDateFormat import java.util.Locale @Serializable -class KemonoFavouritesDto( +class KemonoFavoritesDto( val id: String, val name: String, val service: String, diff --git a/src/all/coomer/build.gradle b/src/all/coomer/build.gradle index 6434d25f9..c6744255e 100644 --- a/src/all/coomer/build.gradle +++ b/src/all/coomer/build.gradle @@ -2,7 +2,7 @@ ext { extName = 'Coomer' extClass = '.Coomer' themePkg = 'kemono' - baseUrl = 'https://coomer.su' + baseUrl = 'https://coomer.st' overrideVersionCode = 0 isNsfw = true } diff --git a/src/all/coomer/src/eu/kanade/tachiyomi/extension/all/coomer/Coomer.kt b/src/all/coomer/src/eu/kanade/tachiyomi/extension/all/coomer/Coomer.kt index 97cf0e861..92704ffac 100644 --- a/src/all/coomer/src/eu/kanade/tachiyomi/extension/all/coomer/Coomer.kt +++ b/src/all/coomer/src/eu/kanade/tachiyomi/extension/all/coomer/Coomer.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.extension.all.coomer 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( "OnlyFans", "Fansly",