From 99037dd046c61e3af7af72b7323719089ff38882 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 6 Jul 2022 15:12:34 +0300 Subject: [PATCH] Automatic switch mirrors on server error --- .../core/network/MirrorsInterceptor.kt | 105 ++++++++++++++++++ .../kotatsu/core/network/NetworkModule.kt | 3 + .../kotatsu/core/parser/MangaRepository.kt | 9 +- .../kotatsu/core/prefs/SourceSettings.kt | 12 ++ 4 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/network/MirrorsInterceptor.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/MirrorsInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/MirrorsInterceptor.kt new file mode 100644 index 000000000..d2edd29e0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/MirrorsInterceptor.kt @@ -0,0 +1,105 @@ +package org.koitharu.kotatsu.core.network + +import android.content.Context +import androidx.collection.ArrayMap +import okhttp3.Interceptor +import okhttp3.Response +import org.koitharu.kotatsu.core.prefs.SourceSettings +import org.koitharu.kotatsu.parsers.MangaParser +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.model.MangaSource + +class MirrorsInterceptor( + private val context: Context, +) : Interceptor { + + private val mirrorsMap = ArrayMap() + + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + return if (response.isServerError) { + trySwitchMirror(chain) ?: response + } else { + response + } + } + + fun register(parser: MangaParser) { + val configKeys = ArrayList>() + parser.onCreateConfig(configKeys) + for (key in configKeys) { + if (key is ConfigKey.Domain) { + val mirrors = key.presetValues ?: continue + mirrorsMap[parser.getDomain()] = Mirrors(parser.source, mirrors) + } + } + } + + private fun trySwitchMirror(chain: Interceptor.Chain): Response? { + val url = chain.request().url + var mirrors = mirrorsMap[url.host] + val domain = if (mirrors != null) { + url.host + } else { + mirrors = mirrorsMap[url.topPrivateDomain()] + url.topPrivateDomain() + } + if (domain == null || mirrors == null) { + return null + } + synchronized(mirrors) { + for (mirror in mirrors.mirrors) { + val request = chain.request() + .newBuilder() + .url(url.newBuilder().host(mirror).build()) + .build() + val response = chain.proceed(request) + if (!response.isServerError) { + switchMirror(domain, mirrors.source, mirror) + return response + } + } + return null + } + } + + private fun switchMirror(oldDomain: String, source: MangaSource, newDomain: String) { + val mirrors = mirrorsMap[oldDomain]?.mirrors?.toMutableList() + if (mirrors != null) { + mirrors.remove(newDomain) + mirrors.add(oldDomain) + } + mirrorsMap[newDomain] = Mirrors(source, (mirrors ?: listOf(oldDomain)).toTypedArray()) + val settings = SourceSettings(context, source) + settings[ConfigKey.Domain(oldDomain, null)] = newDomain + } + + private val Response.isServerError: Boolean + get() { + return code in 500..599 + } + + private class Mirrors( + val source: MangaSource, + val mirrors: Array, + ) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Mirrors + + if (source != other.source) return false + if (!mirrors.contentEquals(other.mirrors)) return false + + return true + } + + override fun hashCode(): Int { + var result = source.hashCode() + result = 31 * result + mirrors.contentHashCode() + return result + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt index 2af4c215e..445f8855f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.network import okhttp3.CookieJar import okhttp3.OkHttpClient +import org.koin.android.ext.koin.androidContext import org.koin.dsl.bind import org.koin.dsl.module import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl @@ -12,6 +13,7 @@ import java.util.concurrent.TimeUnit val networkModule get() = module { single { AndroidCookieJar() } bind CookieJar::class + single { MirrorsInterceptor(androidContext()) } single { val cache = get().createHttpCache() OkHttpClient.Builder().apply { @@ -23,6 +25,7 @@ val networkModule cache(cache) addInterceptor(UserAgentInterceptor()) addInterceptor(CloudFlareInterceptor()) + addInterceptor(get()) }.build() } single { MangaLoaderContextImpl(get(), get(), get()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt index 90c84d5a8..ed7e48745 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -1,11 +1,12 @@ package org.koitharu.kotatsu.core.parser -import java.lang.ref.WeakReference -import java.util.* import org.koin.core.component.KoinComponent import org.koin.core.component.get +import org.koitharu.kotatsu.core.network.MirrorsInterceptor import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.* +import java.lang.ref.WeakReference +import java.util.* interface MangaRepository { @@ -36,7 +37,9 @@ interface MangaRepository { cache[source]?.get()?.let { return it } return synchronized(cache) { cache[source]?.get()?.let { return it } - val repository = RemoteMangaRepository(MangaParser(source, get())) + val parser = MangaParser(source, get()) + get().register(parser) + val repository = RemoteMangaRepository(parser) cache[source] = WeakReference(repository) repository } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt index ea14c7342..652d5efd5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt @@ -26,4 +26,16 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue } } as T } + + operator fun set(key: ConfigKey, value: T?) { + val editor = prefs.edit() + when (key) { + is ConfigKey.Domain -> if (value == null) { + editor.remove(key.key) + } else { + editor.putString(key.key, value as String) + } + } + editor.apply() + } } \ No newline at end of file