From 7bec47b4d8d6676c4b96bb9828b9eb2cbdc401bc Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 25 Mar 2023 08:40:18 +0200 Subject: [PATCH] Automaticaly switch mirrors on network errors --- app/build.gradle | 2 +- .../org/koitharu/kotatsu/core/AppModule.kt | 2 + .../core/network/MirrorSwitchInterceptor.kt | 88 +++++++++++++++++++ .../core/parser/RemoteMangaRepository.kt | 9 +- .../kotatsu/core/prefs/SourceSettings.kt | 8 ++ 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt diff --git a/app/build.gradle b/app/build.gradle index 73effe75c..b0925c19c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,7 +77,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:93f5f70d79') { + implementation('com.github.KotatsuApp:kotatsu-parsers:cc418570d5') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt index a02e42b42..042f57865 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt @@ -86,6 +86,7 @@ interface AppModule { fun provideOkHttpClient( localStorageManager: LocalStorageManager, commonHeadersInterceptor: CommonHeadersInterceptor, + mirrorSwitchInterceptor: MirrorSwitchInterceptor, cookieJar: CookieJar, settings: AppSettings, ): OkHttpClient { @@ -103,6 +104,7 @@ interface AppModule { addInterceptor(GZipInterceptor()) addInterceptor(commonHeadersInterceptor) addInterceptor(CloudFlareInterceptor()) + addInterceptor(mirrorSwitchInterceptor) if (BuildConfig.DEBUG) { addInterceptor(CurlLoggingInterceptor()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt new file mode 100644 index 000000000..909134479 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt @@ -0,0 +1,88 @@ +package org.koitharu.kotatsu.core.network + +import dagger.Lazy +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okhttp3.internal.canParseAsIpAddress +import okhttp3.internal.closeQuietly +import okhttp3.internal.publicsuffix.PublicSuffixDatabase +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.parsers.model.MangaSource +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MirrorSwitchInterceptor @Inject constructor( + private val mangaRepositoryFactoryLazy: Lazy, +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + return try { + val response = chain.proceed(request) + if (response.isFailed) { + val responseCopy = response.newBuilder().build() + response.close() + trySwitchMirror(request, chain) ?: responseCopy + } else { + response + } + } catch (e: Exception) { + trySwitchMirror(request, chain) ?: throw e + } + } + + private fun trySwitchMirror(request: Request, chain: Interceptor.Chain): Response? { + val source = request.tag(MangaSource::class.java) ?: return null + val repository = mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository ?: return null + val mirrors = repository.getAvailableMirrors() + if (mirrors.isEmpty()) { + return null + } + return tryMirrors(repository, mirrors, chain, request) + } + + private fun tryMirrors( + repository: RemoteMangaRepository, + mirrors: List, + chain: Interceptor.Chain, + request: Request, + ): Response? { + val url = request.url + val currentDomain = url.topPrivateDomain() + if (currentDomain !in mirrors) { + return null + } + val urlBuilder = url.newBuilder() + for (mirror in mirrors) { + if (mirror == currentDomain) { + continue + } + val newHost = hostOf(url.host, mirror) ?: continue + val newRequest = request.newBuilder() + .url(urlBuilder.host(newHost).build()) + .build() + val response = chain.proceed(newRequest) + if (response.isFailed) { + response.closeQuietly() + } else { + repository.domain = mirror + return response + } + } + return null + } + + private val Response.isFailed: Boolean + get() = code in 400..599 + + private fun hostOf(host: String, newDomain: String): String? { + if (newDomain.canParseAsIpAddress()) { + return newDomain + } + val domain = PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) ?: return null + return host.removeSuffix(domain) + newDomain + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 389898767..12420b0ad 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -43,8 +43,11 @@ class RemoteMangaRepository( getConfig().defaultSortOrder = value } - val domain: String + var domain: String get() = parser.domain + set(value) { + getConfig()[parser.configKeyDomain] = value + } val headers: Headers? get() = parser.headers @@ -95,6 +98,10 @@ class RemoteMangaRepository( parser.onCreateConfig(it) } + fun getAvailableMirrors(): List { + return parser.configKeyDomain.presetValues?.toList().orEmpty() + } + private fun getConfig() = parser.config as SourceSettings private suspend fun asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred { 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 5f74b3b2d..1b3af7980 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 @@ -28,4 +28,12 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue) } as T } + + operator fun set(key: ConfigKey, value: T) = prefs.edit { + when (key) { + is ConfigKey.Domain -> putString(key.key, value as String?) + is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean) + is ConfigKey.UserAgent -> putString(key.key, value as String?) + } + } }