Merge pull request #158 from Blatzar/master

Added DNS over HTTPS and cache
This commit is contained in:
Osten 2021-10-19 16:08:57 +02:00 committed by GitHub
commit 12977e1788
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 242 additions and 14 deletions

View file

@ -63,6 +63,9 @@ android {
debuggable true debuggable true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
debug {
applicationIdSuffix ".debug"
}
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -106,6 +109,7 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.12.0' implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0' kapt 'com.github.bumptech.glide:compiler:4.12.0'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.12.0'
implementation 'jp.wasabeef:glide-transformations:4.0.0' implementation 'jp.wasabeef:glide-transformations:4.0.0'
@ -141,5 +145,6 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:2.7.0-rc01" implementation "androidx.work:work-runtime-ktx:2.7.0-rc01"
// Networking // Networking
implementation "com.squareup.okhttp3:okhttp:4.9.0" implementation "com.squareup.okhttp3:okhttp:4.9.1"
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
} }

View file

@ -26,6 +26,9 @@ import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.restrictedApis import com.lagradost.cloudstream3.APIHolder.restrictedApis
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.get
import com.lagradost.cloudstream3.network.initRequestClient
import com.lagradost.cloudstream3.network.text
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
@ -46,6 +49,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.fragment_result.*
import java.util.* import java.util.*
import java.util.zip.GZIPInputStream
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -290,6 +294,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
true true
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW ) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
updateLocale() updateLocale()
initRequestClient()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
try { try {
if (isCastApiAvailable()) { if (isCastApiAvailable()) {
@ -334,7 +339,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}*/ }*/
// Fucks up anime info layout since that has its own layout // Fucks up anime info layout since that has its own layout
cast_mini_controller_holder?.isVisible = !listOf(R.id.navigation_results,R.id.navigation_player).contains(destination.id) cast_mini_controller_holder?.isVisible =
!listOf(R.id.navigation_results, R.id.navigation_player).contains(destination.id)
nav_view.isVisible = listOf( nav_view.isVisible = listOf(
R.id.navigation_home, R.id.navigation_home,

View file

@ -183,7 +183,7 @@ class TrailersToProvider : MainAPI() {
return isSucc return isSucc
} else if (url.contains("/episode/")) { } else if (url.contains("/episode/")) {
val response = get(url).text val response = get(url, params = mapOf("preview" to "1")).text
val document = Jsoup.parse(response) val document = Jsoup.parse(response)
// val qSub = document.select("subtitle-content") // val qSub = document.select("subtitle-content")
val subUrl = document.select("subtitle-content")?.attr("data-url") ?: "" val subUrl = document.select("subtitle-content")?.attr("data-url") ?: ""

View file

@ -0,0 +1,67 @@
package com.lagradost.cloudstream3.network
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import java.net.InetAddress
/**
* Based on https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt
*/
fun OkHttpClient.Builder.addGenericDns(url: String, ips: List<String>) = dns(
DnsOverHttps
.Builder()
.client(build())
.url(url.toHttpUrl())
.bootstrapDnsHosts(
ips.map { InetAddress.getByName(it) }
)
.build()
)
fun OkHttpClient.Builder.addGoogleDns() = (
addGenericDns(
"https://dns.google/dns-query",
listOf(
"8.8.4.4",
"8.8.8.8"
)
))
fun OkHttpClient.Builder.addCloudFlareDns() = (
addGenericDns(
"https://cloudflare-dns.com/dns-query",
// https://www.cloudflare.com/ips/
listOf(
"1.1.1.1",
"1.0.0.1",
"2606:4700:4700::1111",
"2606:4700:4700::1001"
)
))
// Commented out as it doesn't work
//fun OkHttpClient.Builder.addOpenDns() = (
// addGenericDns(
// "https://doh.opendns.com/dns-query",
// // https://support.opendns.com/hc/en-us/articles/360038086532-Using-DNS-over-HTTPS-DoH-with-OpenDNS
// listOf(
// "208.67.222.222",
// "208.67.220.220",
// "2620:119:35::35",
// "2620:119:53::53",
// )
// ))
fun OkHttpClient.Builder.addAdGuardDns() = (
addGenericDns(
"https://dns.adguard.com/dns-query",
// https://github.com/AdguardTeam/AdGuardDNS
listOf(
// "Non-filtering"
"94.140.14.140",
"94.140.14.141",
)
))

View file

@ -1,12 +1,20 @@
package com.lagradost.cloudstream3.network package com.lagradost.cloudstream3.network
import android.content.Context
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import okhttp3.* import okhttp3.*
import okhttp3.Headers.Companion.toHeaders import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.dnsoverhttps.DnsOverHttps
import java.io.File
import java.net.InetAddress
import java.net.URI import java.net.URI
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
var baseClient = OkHttpClient()
private const val DEFAULT_TIME = 10 private const val DEFAULT_TIME = 10
private val DEFAULT_TIME_UNIT = TimeUnit.MINUTES private val DEFAULT_TIME_UNIT = TimeUnit.MINUTES
private const val DEFAULT_USER_AGENT = USER_AGENT private const val DEFAULT_USER_AGENT = USER_AGENT
@ -15,6 +23,29 @@ private val DEFAULT_DATA: Map<String, String> = mapOf()
private val DEFAULT_COOKIES: Map<String, String> = mapOf() private val DEFAULT_COOKIES: Map<String, String> = mapOf()
private val DEFAULT_REFERER: String? = null private val DEFAULT_REFERER: String? = null
fun Context.initRequestClient(): OkHttpClient {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val dns = settingsManager.getInt(this.getString(R.string.dns_pref), 0)
baseClient = OkHttpClient.Builder()
.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(cacheDir, "http_cache"),
maxSize = 50L * 1024L * 1024L // 50 MiB
)
).apply {
when (dns) {
1 -> addGoogleDns()
2 -> addCloudFlareDns()
// 3 -> addOpenDns()
4 -> addAdGuardDns()
}
}
// Needs to be build as otherwise the other builders will change this object
.build()
return baseClient
}
/** WARNING! CAN ONLY BE READ ONCE */ /** WARNING! CAN ONLY BE READ ONCE */
val Response.text: String val Response.text: String
@ -93,14 +124,13 @@ fun get(
timeout: Long = 0L, timeout: Long = 0L,
interceptor: Interceptor? = null interceptor: Interceptor? = null
): Response { ): Response {
val client = baseClient
val client = OkHttpClient().newBuilder() .newBuilder()
.followRedirects(allowRedirects) .followRedirects(allowRedirects)
.followSslRedirects(allowRedirects) .followSslRedirects(allowRedirects)
.callTimeout(timeout, TimeUnit.SECONDS) .callTimeout(timeout, TimeUnit.SECONDS)
if (interceptor != null) client.addInterceptor(interceptor) if (interceptor != null) client.addInterceptor(interceptor)
val request = getRequestCreator(url, headers, referer, params, cookies, cacheTime, cacheUnit) val request = getRequestCreator(url, headers, referer, params, cookies, cacheTime, cacheUnit)
return client.build().newCall(request).execute() return client.build().newCall(request).execute()
} }
@ -118,7 +148,8 @@ fun post(
cacheUnit: TimeUnit = DEFAULT_TIME_UNIT, cacheUnit: TimeUnit = DEFAULT_TIME_UNIT,
timeout: Long = 0L timeout: Long = 0L
): Response { ): Response {
val client = OkHttpClient().newBuilder() val client = baseClient
.newBuilder()
.followRedirects(allowRedirects) .followRedirects(allowRedirects)
.followSslRedirects(allowRedirects) .followSslRedirects(allowRedirects)
.callTimeout(timeout, TimeUnit.SECONDS) .callTimeout(timeout, TimeUnit.SECONDS)

View file

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.network
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.net.http.SslError import android.net.http.SslError
import android.webkit.* import android.webkit.*
import androidx.core.view.contains
import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -10,6 +11,7 @@ import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import java.net.URI
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class WebViewResolver(val interceptUrl: Regex) : Interceptor { class WebViewResolver(val interceptUrl: Regex) : Interceptor {
@ -56,7 +58,7 @@ class WebViewResolver(val interceptUrl: Regex) : Interceptor {
request: WebResourceRequest request: WebResourceRequest
): WebResourceResponse? { ): WebResourceResponse? {
val webViewUrl = request.url.toString() val webViewUrl = request.url.toString()
// println("Override url $webViewUrl")
if (interceptUrl.containsMatchIn(webViewUrl)) { if (interceptUrl.containsMatchIn(webViewUrl)) {
fixedRequest = getRequestCreator( fixedRequest = getRequestCreator(
webViewUrl, webViewUrl,
@ -71,7 +73,27 @@ class WebViewResolver(val interceptUrl: Regex) : Interceptor {
println("Web-view request finished: $webViewUrl") println("Web-view request finished: $webViewUrl")
destroyWebView() destroyWebView()
} }
return super.shouldInterceptRequest(view, request)
return try {
when {
// suppress favicon requests as we don't display them anywhere
webViewUrl.endsWith("/favicon.ico") -> WebResourceResponse("image/png", null, null)
webViewUrl.contains("recaptcha") -> super.shouldInterceptRequest(view, request)
request.method == "GET" -> get(
webViewUrl,
headers = request.requestHeaders
).toWebResourceResponse()
request.method == "POST" -> post(
webViewUrl,
headers = request.requestHeaders
).toWebResourceResponse()
else -> return super.shouldInterceptRequest(view, request)
}
} catch (e: Exception) {
null
}
} }
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) { override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
@ -99,4 +121,18 @@ class WebViewResolver(val interceptUrl: Regex) : Interceptor {
return null return null
} }
fun Response.toWebResourceResponse(): WebResourceResponse {
val contentTypeValue = this.header("Content-Type")
// 1. contentType. 2. charset
val typeRegex = Regex("""(.*);(?:.*charset=(.*)(?:|;)|)""")
return if (contentTypeValue != null) {
val found = typeRegex.find(contentTypeValue ?: "")
val contentType = found?.groupValues?.getOrNull(1)?.ifBlank { null } ?: contentTypeValue
val charset = found?.groupValues?.getOrNull(2)?.ifBlank { null }
WebResourceResponse(contentType, charset, this.body?.byteStream())
} else {
WebResourceResponse("application/octet-stream", null, this.body?.byteStream())
}
}
} }

View file

@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.MainActivity.Companion.setLocale
import com.lagradost.cloudstream3.MainActivity.Companion.showToast import com.lagradost.cloudstream3.MainActivity.Companion.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initRequestClient
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
@ -53,6 +54,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
val localePreference = findPreference<Preference>(getString(R.string.locale_key))!! val localePreference = findPreference<Preference>(getString(R.string.locale_key))!!
val benenePreference = findPreference<Preference>(getString(R.string.benene_count))!! val benenePreference = findPreference<Preference>(getString(R.string.benene_count))!!
val watchQualityPreference = findPreference<Preference>(getString(R.string.quality_pref_key))!! val watchQualityPreference = findPreference<Preference>(getString(R.string.quality_pref_key))!!
val dnsPreference = findPreference<Preference>(getString(R.string.dns_key))!!
val legalPreference = findPreference<Preference>(getString(R.string.legal_notice_key))!! val legalPreference = findPreference<Preference>(getString(R.string.legal_notice_key))!!
val subdubPreference = findPreference<Preference>(getString(R.string.display_sub_key))!! val subdubPreference = findPreference<Preference>(getString(R.string.display_sub_key))!!
val providerLangPreference = findPreference<Preference>(getString(R.string.provider_lang_key))!! val providerLangPreference = findPreference<Preference>(getString(R.string.provider_lang_key))!!
@ -154,6 +156,25 @@ class SettingsFragment : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }
dnsPreference.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.dns_pref)
val prefValues = resources.getIntArray(R.array.dns_pref_values)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val currentDns =
settingsManager.getInt(getString(R.string.dns_pref), 0)
context?.showBottomDialog(
prefNames.toList(),
prefValues.indexOf(currentDns),
getString(R.string.dns_pref),
true,
{}) {
settingsManager.edit().putInt(getString(R.string.dns_pref), prefValues[it]).apply()
context?.initRequestClient()
}
return@setOnPreferenceClickListener true
}
try { try {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)

View file

@ -1,12 +1,18 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import android.content.Context import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import com.lagradost.cloudstream3.network.initRequestClient
import java.io.InputStream
@GlideModule @GlideModule
class GlideModule : AppGlideModule() { class GlideModule : AppGlideModule() {
@ -18,4 +24,16 @@ class GlideModule : AppGlideModule() {
.signature(ObjectKey(System.currentTimeMillis().toShort())) .signature(ObjectKey(System.currentTimeMillis().toShort()))
} }
} }
// Needed for DOH
// https://stackoverflow.com/a/61634041
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val client = context.initRequestClient()
registry.replace(
GlideUrl::class.java,
InputStream::class.java,
OkHttpUrlLoader.Factory(client)
)
super.registerComponents(context, glide, registry)
}
} }

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M20,13H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1v-6c0,-0.55 -0.45,-1 -1,-1zM7,19c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM20,3H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1V4c0,-0.55 -0.45,-1 -1,-1zM7,9c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>

View file

@ -31,6 +31,21 @@
<item>-2</item> <item>-2</item>
</array> </array>
<array name="dns_pref">
<item>None</item>
<item>Google</item>
<item>Cloudflare</item>
<!-- <item>OpenDns</item>-->
<item>AdGuard</item>
</array>
<array name="dns_pref_values">
<item>0</item>
<item>1</item>
<item>2</item>
<!-- <item>3</item>-->
<item>4</item>
</array>
<array name="episode_long_click_options"> <array name="episode_long_click_options">
<item>@string/episode_action_chomecast_episode</item> <item>@string/episode_action_chomecast_episode</item>
<item>@string/episode_action_chomecast_mirror</item> <item>@string/episode_action_chomecast_mirror</item>

View file

@ -23,6 +23,7 @@
<string name="display_sub_key" translatable="false">display_sub_key</string> <string name="display_sub_key" translatable="false">display_sub_key</string>
<string name="show_fillers_key" translatable="false">show_fillers_key</string> <string name="show_fillers_key" translatable="false">show_fillers_key</string>
<string name="provider_lang_key" translatable="false">provider_lang_key</string> <string name="provider_lang_key" translatable="false">provider_lang_key</string>
<string name="dns_key" translatable="false">dns_key</string>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG --> <!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
@ -166,7 +167,9 @@
<string name="double_tap_to_seek_settings_des">Tap twice on the right or left side to seek forwards or backwards <string name="double_tap_to_seek_settings_des">Tap twice on the right or left side to seek forwards or backwards
</string> </string>
<string name="use_system_brightness_settings">Use system brightness</string> <string name="use_system_brightness_settings">Use system brightness</string>
<string name="use_system_brightness_settings_des">Use system brightness in the app player instead of an dark overlay</string> <string name="use_system_brightness_settings_des">Use system brightness in the app player instead of an dark
overlay
</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="settings_info">Info</string> <string name="settings_info">Info</string>
@ -261,6 +264,10 @@
<string name="dont_show_again">Don\'t show again</string> <string name="dont_show_again">Don\'t show again</string>
<string name="update">Update</string> <string name="update">Update</string>
<string name="watch_quality_pref">Preferred watch quality</string> <string name="watch_quality_pref">Preferred watch quality</string>
<string name="dns_pref">DNS over HTTPS</string>
<string name="dns_pref_summary">Useful for bypassing ISP blocks</string>
<string name="display_subbed_dubbed_settings">Display Dubbed/Subbed Anime</string> <string name="display_subbed_dubbed_settings">Display Dubbed/Subbed Anime</string>
<string name="resize_fit">Fit to screen</string> <string name="resize_fit">Fit to screen</string>
@ -269,15 +276,21 @@
<string name="legal_notice" translatable="false">Disclaimer</string> <string name="legal_notice" translatable="false">Disclaimer</string>
<string name="legal_notice_key" translatable="false">legal_notice_key</string> <string name="legal_notice_key" translatable="false">legal_notice_key</string>
<string name="legal_notice_text" translatable="false">Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. <string name="legal_notice_text" translatable="false">Any legal issues regarding the content on this application
should be taken up with the actual file hosts and providers themselves as we are not affiliated with them.
In case of copyright infringement, please directly contact the responsible parties or the streaming websites. In case of copyright infringement, please directly contact the responsible parties or the streaming websites.
The app is purely for educational and personal use. The app is purely for educational and personal use.
CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down.
CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or
manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient,
user-friendly interface.
It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the
responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use
CloudStream 3 at your own risk.
</string> </string>
<string name="general">General</string> <string name="general">General</string>
<string name="provider_lang_settings">Provider Languages</string> <string name="provider_lang_settings">Provider Languages</string>

View file

@ -93,6 +93,12 @@
android:icon="@drawable/ic_baseline_skip_next_24" android:icon="@drawable/ic_baseline_skip_next_24"
android:title="@string/show_fillers_settings" android:title="@string/show_fillers_settings"
android:defaultValue="true"/> android:defaultValue="true"/>
<Preference
android:key="@string/dns_key"
android:title="@string/dns_pref"
android:summary="@string/dns_pref_summary"
android:icon="@drawable/ic_baseline_dns_24">
</Preference>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="search" android:key="search"