Images proxy #357
This commit is contained in:
@@ -45,7 +45,6 @@ import org.koitharu.kotatsu.list.domain.ListExtraProviderImpl
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
@@ -91,7 +90,8 @@ interface AppModule {
|
||||
@ApplicationContext context: Context,
|
||||
@MangaHttpClient okHttpClient: OkHttpClient,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
pagesCache: PagesCache,
|
||||
imageProxyInterceptor: ImageProxyInterceptor,
|
||||
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||
): ImageLoader {
|
||||
val diskCacheFactory = {
|
||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||
@@ -113,7 +113,8 @@ interface AppModule {
|
||||
.add(SvgDecoder.Factory())
|
||||
.add(CbzFetcher.Factory())
|
||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||
.add(MangaPageFetcher.Factory(context, okHttpClient, pagesCache, mangaRepositoryFactory))
|
||||
.add(pageFetcherFactory)
|
||||
.add(imageProxyInterceptor)
|
||||
.build(),
|
||||
).build()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import android.util.Log
|
||||
import androidx.collection.ArraySet
|
||||
import coil.intercept.Interceptor
|
||||
import coil.request.ErrorResult
|
||||
import coil.request.ImageResult
|
||||
import coil.request.SuccessResult
|
||||
import coil.size.Dimension
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.util.Collections
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ImageProxyInterceptor @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
) : Interceptor {
|
||||
|
||||
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
||||
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val request = chain.request
|
||||
if (!settings.isImagesProxyEnabled) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
val url: HttpUrl? = when (val data = request.data) {
|
||||
is HttpUrl -> data
|
||||
is String -> data.toHttpUrlOrNull()
|
||||
else -> null
|
||||
}
|
||||
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
val newUrl = HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host("wsrv.nl")
|
||||
.addQueryParameter("url", url.toString())
|
||||
.addQueryParameter("fit", "outside")
|
||||
.addQueryParameter("we", null)
|
||||
val size = request.sizeResolver.size()
|
||||
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
|
||||
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
|
||||
|
||||
val newRequest = request.newBuilder()
|
||||
.data(newUrl.build())
|
||||
.build()
|
||||
val result = chain.proceed(newRequest)
|
||||
return if (result is SuccessResult) {
|
||||
result
|
||||
} else {
|
||||
logDebug((result as? ErrorResult)?.throwable)
|
||||
chain.proceed(request).also {
|
||||
if (it is SuccessResult) {
|
||||
blacklist.add(url.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
||||
if (!settings.isImagesProxyEnabled) {
|
||||
return okHttp.newCall(request).await()
|
||||
}
|
||||
val sourceUrl = request.url
|
||||
val targetUrl = HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host("wsrv.nl")
|
||||
.addQueryParameter("url", sourceUrl.toString())
|
||||
.addQueryParameter("we", null)
|
||||
val newRequest = request.newBuilder()
|
||||
.url(targetUrl.build())
|
||||
.build()
|
||||
return runCatchingCancellable {
|
||||
okHttp.doCall(newRequest)
|
||||
}.recover {
|
||||
logDebug(it)
|
||||
okHttp.doCall(request).also {
|
||||
blacklist.add(sourceUrl.host)
|
||||
}
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
private suspend fun OkHttpClient.doCall(request: Request): Response {
|
||||
return newCall(request).await().ensureSuccess()
|
||||
}
|
||||
|
||||
private fun logDebug(e: Throwable?) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w("ImageProxy", e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,6 +277,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isReaderSliderEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
||||
|
||||
val isImagesProxyEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_IMAGES_PROXY, false)
|
||||
|
||||
val dnsOverHttps: DoHProvider
|
||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||
|
||||
@@ -448,6 +451,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_PROXY_TYPE = "proxy_type"
|
||||
const val KEY_PROXY_ADDRESS = "proxy_address"
|
||||
const val KEY_PROXY_PORT = "proxy_port"
|
||||
const val KEY_IMAGES_PROXY = "images_proxy"
|
||||
|
||||
// About
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
@@ -23,3 +24,17 @@ fun Response.parseJsonOrNull(): JSONObject? {
|
||||
closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
val HttpUrl.isHttpOrHttps: Boolean
|
||||
get() {
|
||||
val s = scheme.lowercase()
|
||||
return s == "https" || s == "http"
|
||||
}
|
||||
|
||||
fun Response.ensureSuccess() = apply {
|
||||
if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) {
|
||||
val message = "Invalid response: $code $message at ${request.url}"
|
||||
closeQuietly()
|
||||
throw IllegalStateException(message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.core.model.findChapter
|
||||
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
@@ -14,7 +15,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
@@ -28,6 +28,7 @@ class DetectReaderModeUseCase @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(manga: Manga, state: ReaderState?): ReaderMode {
|
||||
@@ -70,7 +71,7 @@ class DetectReaderModeUseCase @Inject constructor(
|
||||
}
|
||||
} else {
|
||||
val request = PageLoader.createPageRequest(page, url)
|
||||
okHttpClient.newCall(request).await().use {
|
||||
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
getBitmapSize(it.body?.byteStream())
|
||||
}
|
||||
|
||||
@@ -22,18 +22,19 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.source
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
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.core.util.RetainedLifecycleCoroutineScope
|
||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||
import org.koitharu.kotatsu.core.util.ext.withProgress
|
||||
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
||||
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
@@ -51,6 +52,7 @@ class PageLoader @Inject constructor(
|
||||
private val cache: PagesCache,
|
||||
private val settings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
) : RetainedLifecycle.OnClearedListener {
|
||||
|
||||
init {
|
||||
@@ -191,10 +193,7 @@ class PageLoader @Inject constructor(
|
||||
}
|
||||
} else {
|
||||
val request = createPageRequest(page, pageUrl)
|
||||
okHttp.newCall(request).await().use { response ->
|
||||
check(response.isSuccessful) {
|
||||
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
||||
}
|
||||
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||
val body = checkNotNull(response.body) {
|
||||
"Null response"
|
||||
}
|
||||
|
||||
@@ -9,21 +9,24 @@ import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.OkHttpClient
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
|
||||
class MangaPageFetcher(
|
||||
private val context: Context,
|
||||
@@ -32,6 +35,7 @@ class MangaPageFetcher(
|
||||
private val options: Options,
|
||||
private val page: MangaPage,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
) : Fetcher {
|
||||
|
||||
override suspend fun fetch(): FetchResult {
|
||||
@@ -66,7 +70,7 @@ class MangaPageFetcher(
|
||||
)
|
||||
} else {
|
||||
val request = PageLoader.createPageRequest(page, pageUrl)
|
||||
okHttpClient.newCall(request).await().use { response ->
|
||||
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
|
||||
check(response.isSuccessful) {
|
||||
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
||||
}
|
||||
@@ -89,11 +93,12 @@ class MangaPageFetcher(
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
class Factory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
||||
private val pagesCache: PagesCache,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
) : Fetcher.Factory<MangaPage> {
|
||||
|
||||
override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
@@ -104,6 +109,7 @@ class MangaPageFetcher(
|
||||
page = data,
|
||||
context = context,
|
||||
mangaRepositoryFactory = mangaRepositoryFactory,
|
||||
imageProxyInterceptor = imageProxyInterceptor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,4 +426,6 @@
|
||||
<string name="invalid_value_message">Invalid value</string>
|
||||
<string name="manga_branch_title_template">%1$s (%2$s)</string>
|
||||
<string name="downloaded">Downloaded</string>
|
||||
<string name="images_proxy_title">Images optimization proxy</string>
|
||||
<string name="images_procy_description">Use the wsrv.nl service to reduce traffic usage and speed up image loading if possible</string>
|
||||
</resources>
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
app:allowDividerAbove="true"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="images_proxy"
|
||||
android:summary="@string/images_procy_description"
|
||||
android:title="@string/images_proxy_title" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.ProxySettingsFragment"
|
||||
android:key="proxy"
|
||||
|
||||
Reference in New Issue
Block a user