diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 675ce3b2f77..bf499d2a77e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,7 +27,7 @@ jobs: cache-read-only: false - name: Run Gradle - run: ./gradlew assemblePrereleaseDebug lint + run: ./gradlew assemblePrereleaseDebug - name: Upload Artifact uses: actions/upload-artifact@v7 diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt index 9efa88a37f6..f9c76ad19ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -6,7 +6,8 @@ import androidx.annotation.AnyThread import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugWarning import com.lagradost.cloudstream3.mvvm.safe -import com.lagradost.nicehttp.Requests.Companion.await +import com.lagradost.cloudstream3.okHttpClient +import com.lagradost.nicehttp.RequestsCompat.await import com.lagradost.nicehttp.cookies import kotlinx.coroutines.runBlocking import okhttp3.Headers @@ -102,7 +103,8 @@ class CloudflareKiller : Interceptor { val headers = getHeaders(request.headers.toMap() + userAgentMap, cookies + request.cookies) - return app.baseClient.newCall( + @Suppress("DEPRECATION_ERROR") + return okHttpClient.newCall( request.newBuilder() .headers(headers) .build() @@ -135,4 +137,4 @@ class CloudflareKiller : Interceptor { val cookies = savedCookies[request.url.host] ?: return null return proceed(request, cookies) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/DdosGuardKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/DdosGuardKiller.kt index b5783f78c6e..5c1ae75b2af 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/DdosGuardKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/DdosGuardKiller.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.network import androidx.annotation.AnyThread import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.okHttpClient import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.cookies import kotlinx.coroutines.runBlocking @@ -48,10 +49,10 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor { } val headers = getHeaders(request.headers.toMap(), cookies + request.cookies) - return app.baseClient.newCall( + return okHttpClient.newCall( request.newBuilder() .headers(headers) .build() ).execute() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt index 6234297d080..6302d02217e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt @@ -8,6 +8,10 @@ import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ignoreAllSSLErrors +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.cache.* import okhttp3.Cache import okhttp3.Headers import okhttp3.Headers.Companion.toHeaders @@ -18,48 +22,42 @@ import java.security.Security // Backwards compatible constructor, mark as deprecated later fun Requests.initClient(context: Context) { - this.baseClient = buildDefaultClient(context) + this.baseClient = buildDefaultKtorClient(context) } -/** Only use ignoreSSL if you know what you are doing*/ +/** Only use ignoreSSL if you know what you are doing */ @Prerelease fun Requests.initClient(context: Context, ignoreSSL: Boolean = false) { - this.baseClient = buildDefaultClient(context, ignoreSSL) + this.baseClient = buildDefaultKtorClient(context, ignoreSSL) } - // Backwards compatible constructor, mark as deprecated later fun buildDefaultClient(context: Context): OkHttpClient { return buildDefaultClient(context, false) } -/** Only use ignoreSSL if you know what you are doing*/ +/** Only use ignoreSSL if you know what you are doing */ @Prerelease fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClient { safe { Security.insertProviderAt(Conscrypt.newProvider(), 1) } - + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0) - val baseClient = OkHttpClient.Builder() + + return OkHttpClient.Builder() .followRedirects(true) .followSslRedirects(true) - .apply { - if (ignoreSSL) { - ignoreAllSSLErrors() - } - } + .apply { if (ignoreSSL) ignoreAllSSLErrors() } .cache( - // Note that you need to add a ResponseInterceptor to make this 100% active. - // The server response dictates if and when stuff should be cached. Cache( directory = File(context.cacheDir, "http_cache"), maxSize = 50L * 1024L * 1024L // 50 MiB ) - ).apply { + ) + .apply { when (dns) { 1 -> addGoogleDns() 2 -> addCloudFlareDns() -// 3 -> addOpenDns() 4 -> addAdGuardDns() 5 -> addDNSWatchDns() 6 -> addQuad9Dns() @@ -67,9 +65,23 @@ fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClie 8 -> addCanadianShieldDns() } } - // Needs to be build as otherwise the other builders will change this object .build() - return baseClient +} + +/** + * Builds a Ktor [HttpClient] using the OkHttp engine configured with the same + * settings as [buildDefaultClient] — cache, DNS, SSL, etc. + */ +fun buildDefaultKtorClient(context: Context, ignoreSSL: Boolean = false): HttpClient { + val okHttpClient = buildDefaultClient(context, ignoreSSL) + return HttpClient(OkHttp) { + install(HttpTimeout) + install(HttpCache) + install(HttpRequestRetry) { noRetry() } + engine { + preconfigured = okHttpClient + } + } } private val DEFAULT_HEADERS = mapOf("user-agent" to USER_AGENT) @@ -89,4 +101,4 @@ fun getHeaders( }) else mapOf() val tempHeaders = (DEFAULT_HEADERS + headers + cookieMap) return tempHeaders.toHeaders() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index aa44b92359b..902f24135ec 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -86,6 +86,7 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe +import com.lagradost.cloudstream3.okHttpClient import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.fixSubtitleAlignment import com.lagradost.cloudstream3.ui.player.live.LiveHelper import com.lagradost.cloudstream3.ui.player.live.PREFERRED_LIVE_OFFSET @@ -732,18 +733,19 @@ class CS3IPlayer : IPlayer { private var simpleCache: SimpleCache? = null - /// Create a small factory for small things, no cache, no cronet + // Create a small factory for small things, no cache, no cronet private fun createOnlineSource( headers: Map?, interceptor: Interceptor? ): HttpDataSource.Factory { val client = if (interceptor == null) { - app.baseClient + okHttpClient } else { - app.baseClient.newBuilder() + okHttpClient.newBuilder() .addInterceptor(interceptor) .build() } + val source = OkHttpDataSource.Factory(client).setUserAgent(USER_AGENT) if (!headers.isNullOrEmpty()) { @@ -798,7 +800,7 @@ class CS3IPlayer : IPlayer { val source = if (interceptor == null) { if (engine == null) { Log.d(TAG, "Using DefaultHttpDataSource for $link") - OkHttpDataSource.Factory(app.baseClient).setUserAgent(userAgent) + OkHttpDataSource.Factory(okHttpClient).setUserAgent(userAgent) } else { Log.d(TAG, "Using CronetDataSource for $link") CronetDataSource.Factory(engine, Executors.newSingleThreadExecutor()) @@ -810,13 +812,13 @@ class CS3IPlayer : IPlayer { } } else { Log.d(TAG, "Using OkHttpDataSource for $link") - val client = app.baseClient.newBuilder() + val client = okHttpClient.newBuilder() .addInterceptor(interceptor) .build() OkHttpDataSource.Factory(client).setUserAgent(userAgent) } - // Do no include empty referer, if the provider wants those they can use the header map. + // Do not include empty referer, if the provider wants those they can use the header map. val refererMap = if (link.referer.isBlank()) emptyMap() else mapOf("referer" to link.referer) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt index 8dbd7817898..382b7918406 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt @@ -57,7 +57,7 @@ object SearchSuggestionApi { * Parses the TMDB search response and extracts movie/TV show titles. * Filters to only include movies, TV shows, and anime. */ - private fun parseSuggestions(response: NiceResponse): List { + private suspend fun parseSuggestions(response: NiceResponse): List { return try { val parsed = response.parsed() parsed.results diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12fd8525744..088af8aca63 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ media3 = "1.9.3" navigationKtx = "2.9.7" newpipeextractor = "v0.26.0" nextlibMedia3 = "1.9.3-0.12.0" -nicehttp = "0.4.18" +nicehttp = "ea97cfd14d" overlappingpanels = "0.1.5" paletteKtx = "1.0.0" preferenceKtx = "1.2.1" @@ -103,7 +103,7 @@ navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version. newpipeextractor = { module = "com.github.teamnewpipe:NewPipeExtractor", version.ref = "newpipeextractor" } nextlib-media3ext = { module = "io.github.anilbeesetti:nextlib-media3ext", version.ref = "nextlibMedia3" } nextlib-mediainfo = { module = "io.github.anilbeesetti:nextlib-mediainfo", version.ref = "nextlibMedia3" } -nicehttp = { module = "com.github.Blatzar:NiceHttp", version.ref = "nicehttp" } +nicehttp = { module = "com.github.Luna712.NiceHttp-kmp:library", version.ref = "nicehttp" } overlappingpanels = { module = "com.github.discord:OverlappingPanels", version.ref = "overlappingpanels" } palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "paletteKtx" } preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt index 975572d054c..6fbb20da0a2 100644 --- a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt @@ -14,6 +14,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.nicehttp.NiceResponse import com.lagradost.nicehttp.requestCreator import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking @@ -293,7 +294,7 @@ fun WebResourceRequest.toRequest(): Request? { } } -fun Response.toWebResourceResponse(): WebResourceResponse { +fun NiceResponse.toWebResourceResponse(): WebResourceResponse { val contentTypeValue = this.header("Content-Type") // 1. contentType. 2. charset val typeRegex = Regex("""(.*);(?:.*charset=(.*)(?:|;)|)""") diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt index a74097fd5f4..8c64dc7ceff 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt @@ -8,6 +8,8 @@ import com.fleeksoft.ksoup.nodes.Document import com.lagradost.nicehttp.NiceResponse import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser +import io.ktor.client.engine.okhttp.OkHttpEngine +import okhttp3.OkHttpClient import kotlin.reflect.KClass // Short name for requests client to make it nicer to use @@ -40,6 +42,11 @@ var app = Requests(responseParser = jacksonResponseParser).apply { defaultHeaders = mapOf("user-agent" to USER_AGENT) } +// TODO: Remove usage of this by migrating interceptors and media3 to ktor +@InternalAPI +val okHttpClient = (app.baseClient.engine as? OkHttpEngine) + ?.config?.preconfigured ?: OkHttpClient() + /** Parses the response body as a Ksoup Document. */ val NiceResponse.ksoupDocument: Document get() = Ksoup.parse(text) @@ -50,4 +57,4 @@ val NiceResponse.ksoupDocument: Document @UnsafeSSL var insecureApp = Requests(responseParser = jacksonResponseParser).apply { defaultHeaders = mapOf("user-agent" to USER_AGENT) -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt index cf3e28a8de0..4982a77e2fc 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt @@ -28,8 +28,8 @@ import com.lagradost.cloudstream3.newTvSeriesLoadResponse import com.lagradost.cloudstream3.newTvSeriesSearchResponse import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson -import okhttp3.Interceptor -import okhttp3.Response +import com.lagradost.nicehttp.HeadersInterceptor +import io.ktor.http.HttpHeaders import java.text.SimpleDateFormat import java.util.Locale @@ -49,19 +49,9 @@ abstract class MyDramaListAPI : MainAPI() { val API_KEY: String = BuildConfig.MDL_API_KEY const val API_HOST = "https://api.mydramalist.com/v1" const val SITE_HOST = "https://mydramalist.com" - private val headerInterceptor = MyDramaListInterceptor() - } - - /** Automatically adds required api headers */ - private class MyDramaListInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - return chain.proceed( - chain.request().newBuilder() - .removeHeader("user-agent") - .addHeader("user-agent", "Dart/3.6 (dart:io)") - .addHeader("mdl-api-key", API_KEY) - .build() - ) + private val headerInterceptor = HeadersInterceptor { + header(HttpHeaders.UserAgent, "Dart/3.6 (dart:io)") + header("mdl-api-key", API_KEY) } } @@ -75,21 +65,19 @@ abstract class MyDramaListAPI : MainAPI() { ) override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { - val list = app.get( - url = "${request.data}&limit=20&page=$page&lang=en-US", + val list = app.get("${request.data}&limit=20&page=$page&lang=en-US") { interceptor = headerInterceptor - ).parsed().map { element -> + }.parsed().map { element -> element.toSearchResponse() } return newHomePageResponse(request.name, list) } override suspend fun search(query: String): List? { - return app.post( - url = "$API_HOST/search/titles", - data = mapOf("q" to query), + return app.post("$API_HOST/search/titles") { + data = mapOf("q" to query) interceptor = headerInterceptor - ).parsed().map { element -> + }.parsed().map { element -> element.toSearchResponse() } } @@ -126,10 +114,9 @@ abstract class MyDramaListAPI : MainAPI() { override suspend fun load(url: String): LoadResponse { val data = parseJson(url) - return app.get( - url = "$API_HOST/titles/${data.media?.id}", + return app.get("$API_HOST/titles/${data.media?.id}") { interceptor = headerInterceptor - ).parsed().toLoadResponse(data) + }.parsed().toLoadResponse(data) } private suspend fun Media.toLoadResponse(data: Data): LoadResponse { @@ -287,10 +274,9 @@ abstract class MyDramaListAPI : MainAPI() { @JsonProperty("updated_at") val updatedAt: Long, ) { suspend fun fetchCredits(): List { - val actors = app.get( - url = "$API_HOST/titles/$id/credits", + val actors = app.get("$API_HOST/titles/$id/credits") { interceptor = headerInterceptor - ).parsed().cast.map { + }.parsed().cast.map { ActorData( Actor( name = it.name, @@ -303,17 +289,15 @@ abstract class MyDramaListAPI : MainAPI() { } suspend fun fetchRecommendations(): Recommendations { - return app.get( - url = "$API_HOST/titles/$id/recommendations", + return app.get("$API_HOST/titles/$id/recommendations") { interceptor = headerInterceptor - ).parsed() + }.parsed() } suspend fun fetchTrailer(): String? { - return app.get( - url = "$SITE_HOST/v1/trailers/${trailer?.id}", + return app.get("$SITE_HOST/v1/trailers/${trailer?.id}") { interceptor = headerInterceptor - ).parsedSafe()?.trailer?.trailerDetails?.source + }.parsedSafe()?.trailer?.trailerDetails?.source } fun fixGenres(): List { @@ -328,10 +312,9 @@ abstract class MyDramaListAPI : MainAPI() { } private suspend fun Media.fetchEpisodes(): List { - return app.get( - url = "$API_HOST/titles/${this.id}/episodes", + return app.get("$API_HOST/titles/${this.id}/episodes") { interceptor = headerInterceptor - ).parsed().map { + }.parsed().map { it.episodes }.flatten().map { ep -> val link = LinkData( @@ -451,4 +434,4 @@ abstract class MyDramaListAPI : MainAPI() { @JsonProperty("date") val date: String? = null, @JsonProperty("airedDate") val airedDate: String? = null, ) -} \ No newline at end of file +}