diff --git a/src/en/kagane/build.gradle b/src/en/kagane/build.gradle index 3fd0ef87a..d050ed9d8 100644 --- a/src/en/kagane/build.gradle +++ b/src/en/kagane/build.gradle @@ -1,8 +1,12 @@ ext { extName = 'Kagane' extClass = '.Kagane' - extVersionCode = 7 + extVersionCode = 8 isNsfw = true } apply from: "$rootDir/common.gradle" + +dependencies { + compileOnly("com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.11") +} diff --git a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Dto.kt b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Dto.kt index 8e28cea7c..6e868479a 100644 --- a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Dto.kt +++ b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Dto.kt @@ -83,12 +83,14 @@ class ChapterDto( val releaseDate: String?, @SerialName("pages_count") val pagesCount: Int, + @SerialName("number_sort") + val number: Float, ) { - fun toSChapter(index: Int): SChapter = SChapter.create().apply { + fun toSChapter(): SChapter = SChapter.create().apply { url = "$seriesId;$id;$pagesCount" name = title date_upload = dateFormat.tryParse(releaseDate) - chapter_number = index.toFloat() + chapter_number = number } } diff --git a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Filters.kt b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Filters.kt index c23748ebd..813905a97 100644 --- a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Filters.kt +++ b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Filters.kt @@ -1,9 +1,7 @@ package eu.kanade.tachiyomi.extension.en.kagane import eu.kanade.tachiyomi.extension.en.kagane.Kagane.Companion.CONTENT_RATINGS -import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.Filter -import keiyoushi.utils.parseAs import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.add @@ -11,40 +9,19 @@ import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject -private var metadataFetchAttempts: Int = 0 -private var metadataFetched = false - -private var genresList: List = emptyList() -private var tagsList: List = emptyList() -private var sourcesList: List = emptyList() - -fun fetchMetadata(apiUrl: String, client: okhttp3.OkHttpClient) { - if (metadataFetchAttempts < 3 && !metadataFetched) { - try { - client.newCall(GET("$apiUrl/api/v1/metadata")) - .execute().parseAs() - .let { metadata -> - genresList = metadata.getGenresList() - tagsList = metadata.getTagsList() - sourcesList = metadata.getSourcesList() - metadataFetched = true - } - } catch (_: Exception) { - } finally { - metadataFetchAttempts++ - } - } -} - @Serializable data class MetadataDto( val genres: List, val tags: List, val sources: List, ) { - fun getGenresList() = genres.map { it.name } - fun getTagsList() = tags.sortedByDescending { it.count }.slice(0..200).map { it.name } - fun getSourcesList() = sources.map { it.name } + fun getGenresList() = genres + .map { FilterData(it.name, it.name) } + fun getTagsList() = tags.sortedByDescending { it.count } + .slice(0..200) + .map { FilterData(it.name, it.name.replaceFirstChar { c -> c.uppercase() }) } + fun getSourcesList() = sources + .map { FilterData(it.name, it.name) } } @Serializable @@ -96,7 +73,7 @@ internal class ContentRatingFilter( ) internal class GenresFilter( - genres: List = genresList.map { FilterData(it, it) }, + genres: List, ) : JsonMultiSelectTriFilter( "Genres", "genres", @@ -106,7 +83,7 @@ internal class GenresFilter( ) internal class TagsFilter( - tags: List = tagsList.map { FilterData(it, it.replaceFirstChar { c -> c.uppercase() }) }, + tags: List, ) : JsonMultiSelectTriFilter( "Tags", "tags", @@ -116,7 +93,7 @@ internal class TagsFilter( ) internal class SourcesFilter( - sources: List = sourcesList.map { FilterData(it, it) }, + sources: List, ) : JsonMultiSelectFilter( "Sources", "sources", @@ -127,7 +104,7 @@ internal class SourcesFilter( internal class ScanlationsFilter() : Filter.CheckBox("Show scanlations", true) -internal class FilterData( +class FilterData( val id: String, val name: String, ) diff --git a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Kagane.kt b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Kagane.kt index 17b298808..a1aa97895 100644 --- a/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Kagane.kt +++ b/src/en/kagane/src/eu/kanade/tachiyomi/extension/en/kagane/Kagane.kt @@ -1,10 +1,12 @@ package eu.kanade.tachiyomi.extension.en.kagane +import android.annotation.SuppressLint import android.app.Application import android.content.SharedPreferences import android.os.Handler import android.os.Looper import android.util.Base64 +import android.util.Log import android.view.View import android.webkit.JavascriptInterface import android.webkit.PermissionRequest @@ -15,6 +17,7 @@ import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.Filter @@ -30,14 +33,18 @@ import keiyoushi.utils.toJsonString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put +import okhttp3.CacheControl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response +import okhttp3.brotli.BrotliInterceptor +import okhttp3.internal.closeQuietly import okio.IOException import rx.Observable import uy.kohesive.injekt.Injekt @@ -66,6 +73,11 @@ class Kagane : HttpSource(), ConfigurableSource { .addInterceptor(ImageInterceptor()) .addInterceptor(::refreshTokenInterceptor) .rateLimit(2) + // fix disk cache + .apply { + val index = networkInterceptors().indexOfFirst { it is BrotliInterceptor } + if (index >= 0) interceptors().add(networkInterceptors().removeAt(index)) + } .build() private fun refreshTokenInterceptor(chain: Interceptor.Chain): Response { @@ -202,7 +214,7 @@ class Kagane : HttpSource(), ConfigurableSource { override fun chapterListParse(response: Response): List { val dto = response.parseAs() - return dto.content.mapIndexed { i, it -> it.toSChapter(i + 1) }.reversed() + return dto.content.map { it -> it.toSChapter() }.reversed() } override fun chapterListRequest(manga: SManga): Request { @@ -250,6 +262,8 @@ class Kagane : HttpSource(), ConfigurableSource { private var cacheUrl = "https://kazana.$domain" private var accessToken: String = "" + + @SuppressLint("SetJavaScriptEnabled") private fun getChallengeResponse(seriesId: String, chapterId: String): ChallengeDto { val f = "$seriesId:$chapterId".sha256().sliceArray(0 until 16) @@ -453,21 +467,75 @@ class Kagane : HttpSource(), ConfigurableSource { // ============================= Filters ============================== - private val scope = CoroutineScope(Dispatchers.IO) - private fun launchIO(block: () -> Unit) = scope.launch { block() } + private val metadataClient = client.newBuilder() + .addNetworkInterceptor { chain -> + chain.proceed(chain.request()).newBuilder() + .header("Cache-Control", "max-age=${24 * 60 * 60}") + .removeHeader("Pragma") + .removeHeader("Expires") + .build() + }.build() - override fun getFilterList(): FilterList { - launchIO { fetchMetadata(apiUrl, client) } - return FilterList( + override fun getFilterList(): FilterList = runBlocking(Dispatchers.IO) { + val filters: MutableList> = mutableListOf( SortFilter(), ContentRatingFilter( preferences.contentRating.toSet(), ), - GenresFilter(), - TagsFilter(), - SourcesFilter(), + // GenresFilter(), + // TagsFilter(), + // SourcesFilter(), Filter.Separator(), ScanlationsFilter(), ) + + val response = metadataClient.newCall( + GET("$apiUrl/api/v1/metadata", headers, CacheControl.FORCE_CACHE), + ).await() + + // the cache only request fails if it was not cached already + if (!response.isSuccessful) { + CoroutineScope(Dispatchers.IO).launch { + metadataClient.newCall( + GET("$apiUrl/api/v1/metadata", headers, CacheControl.FORCE_NETWORK), + ).await().closeQuietly() + } + + filters.addAll( + index = 0, + listOf( + Filter.Header("Press 'reset' to load more filters"), + Filter.Separator(), + ), + ) + + return@runBlocking FilterList(filters) + } + + val metadata = try { + response.parseAs() + } catch (e: Throwable) { + Log.e(name, "Unable to parse filters", e) + + filters.addAll( + index = 0, + listOf( + Filter.Header("Failed to parse additional filters"), + Filter.Separator(), + ), + ) + return@runBlocking FilterList(filters) + } + + filters.addAll( + index = 2, + listOf( + GenresFilter(metadata.getGenresList()), + TagsFilter(metadata.getTagsList()), + SourcesFilter(metadata.getSourcesList()), + ), + ) + + FilterList(filters) } }