Added 0ms.dev images proxy support #771
parent
6055776329
commit
745972a717
@ -1,106 +0,0 @@
|
|||||||
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 coil.size.isOriginal
|
|
||||||
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("we", null)
|
|
||||||
val size = request.sizeResolver.size()
|
|
||||||
if (!size.isOriginal) {
|
|
||||||
newUrl.addQueryParameter("crop", "cover")
|
|
||||||
(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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import coil.intercept.Interceptor
|
||||||
|
import coil.network.HttpException
|
||||||
|
import coil.request.ErrorResult
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.request.ImageResult
|
||||||
|
import coil.request.SuccessResult
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.HttpStatusException
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
||||||
|
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.net.HttpURLConnection
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
|
abstract class BaseImageProxyInterceptor : ImageProxyInterceptor {
|
||||||
|
|
||||||
|
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
||||||
|
|
||||||
|
final override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
|
val request = chain.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 newRequest = onInterceptImageRequest(request, url)
|
||||||
|
return when (val result = chain.proceed(newRequest)) {
|
||||||
|
is SuccessResult -> result
|
||||||
|
is ErrorResult -> {
|
||||||
|
logDebug(result.throwable, newRequest.data)
|
||||||
|
chain.proceed(request).also {
|
||||||
|
if (it is SuccessResult && result.throwable.isBlockedByServer()) {
|
||||||
|
blacklist.add(url.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
||||||
|
val newRequest = onInterceptPageRequest(request)
|
||||||
|
return runCatchingCancellable {
|
||||||
|
okHttp.doCall(newRequest)
|
||||||
|
}.recover { error ->
|
||||||
|
logDebug(error, newRequest.url)
|
||||||
|
okHttp.doCall(request).also {
|
||||||
|
if (error.isBlockedByServer()) {
|
||||||
|
blacklist.add(request.url.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest
|
||||||
|
|
||||||
|
protected abstract suspend fun onInterceptPageRequest(request: Request): Request
|
||||||
|
|
||||||
|
private suspend fun OkHttpClient.doCall(request: Request): Response {
|
||||||
|
return newCall(request).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logDebug(e: Throwable, url: Any) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w("ImageProxy", "${e.message}: $url", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Throwable.isBlockedByServer(): Boolean {
|
||||||
|
return this is CloudFlareBlockedException
|
||||||
|
|| (this is HttpException && response.code == HttpURLConnection.HTTP_FORBIDDEN)
|
||||||
|
|| (this is HttpStatusException && statusCode == HttpURLConnection.HTTP_FORBIDDEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import coil.intercept.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
interface ImageProxyInterceptor : Interceptor {
|
||||||
|
|
||||||
|
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import coil.intercept.Interceptor
|
||||||
|
import coil.request.ImageResult
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class RealImageProxyInterceptor @Inject constructor(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : ImageProxyInterceptor {
|
||||||
|
|
||||||
|
private val delegate = settings.observeAsStateFlow(
|
||||||
|
scope = processLifecycleScope + Dispatchers.Default,
|
||||||
|
key = AppSettings.KEY_IMAGES_PROXY,
|
||||||
|
valueProducer = { createDelegate() },
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
|
return delegate.value?.intercept(chain) ?: chain.proceed(chain.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
||||||
|
return delegate.value?.interceptPageRequest(request, okHttp) ?: okHttp.newCall(request).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDelegate(): ImageProxyInterceptor? = when (val proxy = settings.imagesProxy) {
|
||||||
|
-1 -> null
|
||||||
|
0 -> WsrvNlProxyInterceptor()
|
||||||
|
1 -> ZeroMsProxyInterceptor()
|
||||||
|
else -> error("Unsupported images proxy $proxy")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.size.Dimension
|
||||||
|
import coil.size.isOriginal
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
|
||||||
|
class WsrvNlProxyInterceptor : BaseImageProxyInterceptor() {
|
||||||
|
|
||||||
|
override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest {
|
||||||
|
val newUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", url.toString())
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
val size = request.sizeResolver.size()
|
||||||
|
if (!size.isOriginal) {
|
||||||
|
newUrl.addQueryParameter("crop", "cover")
|
||||||
|
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
|
||||||
|
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.newBuilder()
|
||||||
|
.data(newUrl.build())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onInterceptPageRequest(request: Request): Request {
|
||||||
|
val sourceUrl = request.url
|
||||||
|
val targetUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", sourceUrl.toString())
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
return request.newBuilder()
|
||||||
|
.url(targetUrl.build())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
|
||||||
|
class ZeroMsProxyInterceptor : BaseImageProxyInterceptor() {
|
||||||
|
|
||||||
|
override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest {
|
||||||
|
if (url.host == "x.0ms.dev" || url.host == "0ms.dev") {
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
val newUrl = ("https://x.0ms.dev/q70/$url").toHttpUrl()
|
||||||
|
return request.newBuilder()
|
||||||
|
.data(newUrl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onInterceptPageRequest(request: Request): Request {
|
||||||
|
val newUrl = ("https://x.0ms.dev/q70/${request.url}").toHttpUrl()
|
||||||
|
return request.newBuilder()
|
||||||
|
.url(newUrl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue