diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt index 034a6513e..63f70cf61 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt @@ -1,6 +1,9 @@ package org.koitharu.kotatsu.core.network +import androidx.collection.ArraySet import dagger.Lazy +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response @@ -13,6 +16,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.model.MangaSource +import java.util.EnumMap import javax.inject.Inject import javax.inject.Singleton @@ -22,9 +26,15 @@ class MirrorSwitchInterceptor @Inject constructor( private val settings: AppSettings, ) : Interceptor { + private val locks = EnumMap(MangaSource::class.java) + private val blacklist = EnumMap>(MangaSource::class.java) + + val isEnabled: Boolean + get() = settings.isMirrorSwitchingAvailable + override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() - if (!settings.isMirrorSwitchingAvailable) { + if (!isEnabled) { return chain.proceed(request) } return try { @@ -43,6 +53,30 @@ class MirrorSwitchInterceptor @Inject constructor( } } + suspend fun trySwitchMirror(repository: RemoteMangaRepository): Boolean = runInterruptible(Dispatchers.Default) { + if (!isEnabled) { + return@runInterruptible false + } + val mirrors = repository.getAvailableMirrors() + if (mirrors.size <= 1) { + return@runInterruptible false + } + synchronized(obtainLock(repository.source)) { + val currentMirror = repository.domain + addToBlacklist(repository.source, currentMirror) + val newMirror = mirrors.firstOrNull { x -> + x != currentMirror && !isBlacklisted(repository.source, x) + } ?: return@synchronized false + repository.domain = newMirror + true + } + } + + fun rollback(repository: RemoteMangaRepository, oldMirror: String) = synchronized(obtainLock(repository.source)) { + blacklist[repository.source]?.remove(oldMirror) + repository.domain = oldMirror + } + 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 @@ -50,7 +84,9 @@ class MirrorSwitchInterceptor @Inject constructor( if (mirrors.isEmpty()) { return null } - return tryMirrors(repository, mirrors, chain, request) + return synchronized(obtainLock(repository.source)) { + tryMirrors(repository, mirrors, chain, request) + } } private fun tryMirrors( @@ -66,7 +102,7 @@ class MirrorSwitchInterceptor @Inject constructor( } val urlBuilder = url.newBuilder() for (mirror in mirrors) { - if (mirror == currentDomain) { + if (mirror == currentDomain || isBlacklisted(repository.source, mirror)) { continue } val newHost = hostOf(url.host, mirror) ?: continue @@ -75,6 +111,7 @@ class MirrorSwitchInterceptor @Inject constructor( .build() val response = chain.proceed(newRequest) if (response.isFailed) { + addToBlacklist(repository.source, mirror) response.closeQuietly() } else { repository.domain = mirror @@ -104,4 +141,18 @@ class MirrorSwitchInterceptor @Inject constructor( private fun ResponseBody.copy(): ResponseBody { return source().readByteArray().toResponseBody(contentType()) } + + private fun obtainLock(source: MangaSource): Any = locks.getOrPut(source) { + Any() + } + + private fun isBlacklisted(source: MangaSource, domain: String): Boolean { + return blacklist[source]?.contains(domain) == true + } + + private fun addToBlacklist(source: MangaSource, domain: String) { + blacklist.getOrPut(source) { + ArraySet(2) + }.add(domain) + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt index d3df9f404..7335ff5c1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.parser import androidx.annotation.AnyThread import org.koitharu.kotatsu.core.cache.ContentCache +import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.model.Manga @@ -43,6 +44,7 @@ interface MangaRepository { private val localMangaRepository: LocalMangaRepository, private val loaderContext: MangaLoaderContext, private val contentCache: ContentCache, + private val mirrorSwitchInterceptor: MirrorSwitchInterceptor, ) { private val cache = EnumMap>(MangaSource::class.java) @@ -55,7 +57,11 @@ interface MangaRepository { cache[source]?.get()?.let { return it } return synchronized(cache) { cache[source]?.get()?.let { return it } - val repository = RemoteMangaRepository(MangaParser(source, loaderContext), contentCache) + val repository = RemoteMangaRepository( + parser = MangaParser(source, loaderContext), + cache = contentCache, + mirrorSwitchInterceptor = mirrorSwitchInterceptor, + ) cache[source] = WeakReference(repository) repository } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index dc934cf53..949c30415 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -13,11 +13,13 @@ import okhttp3.Response import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.cache.SafeDeferred +import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.Favicons import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter @@ -31,6 +33,7 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable class RemoteMangaRepository( private val parser: MangaParser, private val cache: ContentCache, + private val mirrorSwitchInterceptor: MirrorSwitchInterceptor, ) : MangaRepository, Interceptor { override val source: MangaSource @@ -66,11 +69,15 @@ class RemoteMangaRepository( } override suspend fun getList(offset: Int, query: String): List { - return parser.getList(offset, query) + return mirrorSwitchInterceptor.withMirrorSwitching { + parser.getList(offset, query) + } } override suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List { - return parser.getList(offset, tags, sortOrder) + return mirrorSwitchInterceptor.withMirrorSwitching { + parser.getList(offset, tags, sortOrder) + } } override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, withCache = true) @@ -78,17 +85,25 @@ class RemoteMangaRepository( override suspend fun getPages(chapter: MangaChapter): List { cache.getPages(source, chapter.url)?.let { return it } val pages = asyncSafe { - parser.getPages(chapter).distinctById() + mirrorSwitchInterceptor.withMirrorSwitching { + parser.getPages(chapter).distinctById() + } } cache.putPages(source, chapter.url, pages) return pages.await() } - override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page) + override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching { + parser.getPageUrl(page) + } - override suspend fun getTags(): Set = parser.getTags() + override suspend fun getTags(): Set = mirrorSwitchInterceptor.withMirrorSwitching { + parser.getTags() + } - suspend fun getFavicons(): Favicons = parser.getFavicons() + suspend fun getFavicons(): Favicons = mirrorSwitchInterceptor.withMirrorSwitching { + parser.getFavicons() + } override suspend fun getRelated(seed: Manga): List { cache.getRelatedManga(source, seed.url)?.let { return it } @@ -105,7 +120,9 @@ class RemoteMangaRepository( } cache.getDetails(source, manga.url)?.let { return it } val details = asyncSafe { - parser.getDetails(manga) + mirrorSwitchInterceptor.withMirrorSwitching { + parser.getDetails(manga) + } } cache.putDetails(source, manga.url, details) return details.await() @@ -155,4 +172,33 @@ class RemoteMangaRepository( } return result } + + private suspend fun MirrorSwitchInterceptor.withMirrorSwitching(block: suspend () -> R): R { + if (!isEnabled) { + return block() + } + val initialMirror = domain + val result = runCatchingCancellable { + block() + } + if (result.isValidResult()) { + return result.getOrThrow() + } + return if (trySwitchMirror(this@RemoteMangaRepository)) { + val newResult = runCatchingCancellable { + block() + } + if (newResult.isValidResult()) { + return newResult.getOrThrow() + } else { + rollback(this@RemoteMangaRepository, initialMirror) + return result.getOrThrow() + } + } else { + result.getOrThrow() + } + } + + private fun Result<*>.isValidResult() = exceptionOrNull() !is ParseException + && (getOrNull() as? Collection<*>)?.isEmpty() != true }