diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 93a79689e50..3a46d1c79f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -20,10 +20,13 @@ import com.lagradost.cloudstream3.newSearchResponseList import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.withTimeout +class LinkLoadingLimitReached : Error(null, null, false, false) + class APIRepository(val api: MainAPI) { companion object { // 2 minute timeout to prevent bad extensions/extractors from hogging the resources @@ -210,9 +213,13 @@ class APIRepository(val api: MainAPI) { withTimeout(getTimeout(api.loadLinksTimeoutMs)) { api.loadLinks(data, isCasting, subtitleCallback, callback) } + } catch (_: LinkLoadingLimitReached) { + true + } catch (throwable: CancellationException) { + throw throwable } catch (throwable: Throwable) { logError(throwable) return false } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 0668a194bc3..fc561661266 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -1,16 +1,38 @@ package com.lagradost.cloudstream3.ui.player import android.util.Log +import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.AnimeLoadResponse +import com.lagradost.cloudstream3.Episode +import com.lagradost.cloudstream3.LiveStreamLoadResponse import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MovieLoadResponse +import com.lagradost.cloudstream3.ProviderType +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TorrentLoadResponse +import com.lagradost.cloudstream3.TvSeriesLoadResponse +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.isEpisodeBased +import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.ui.APIRepository +import com.lagradost.cloudstream3.ui.LinkLoadingLimitReached +import com.lagradost.cloudstream3.ui.result.buildResultEpisode +import com.lagradost.cloudstream3.ui.result.getId import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.withTimeoutOrNull import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger +import kotlin.coroutines.coroutineContext data class Cache( val linkCache: MutableSet, @@ -19,16 +41,96 @@ data class Cache( var lastCachedTimestamp: Long = unixTime, /** If it has fully loaded */ var saturated: Boolean, + /** If matching sources from other providers have fully loaded */ + var alternateSaturated: Boolean = false, ) class RepoLinkGenerator( episodes: List, val page: LoadResponse? = null, + private val includeAllProviderSources: Boolean = false, + private val allProviderSourceNames: Set = emptySet(), ) : VideoGenerator(episodes) { companion object { const val TAG = "RepoLink" + private const val MAX_ALTERNATE_PROVIDER_COUNT = 5 + private const val MAX_ALTERNATE_LINKS_PER_PROVIDER = 5 + private val NON_ALPHANUMERIC_REGEX = Regex("[^a-z0-9]+") val cache: HashMap, Cache> = hashMapOf() + + private fun normalizeTitle(title: String?): String { + return title + ?.lowercase() + ?.replace(NON_ALPHANUMERIC_REGEX, "") + .orEmpty() + } + + private fun titlesMatch(first: String?, second: String?): Boolean { + val normalizedFirst = normalizeTitle(first) + val normalizedSecond = normalizeTitle(second) + if (normalizedFirst.isBlank() || normalizedSecond.isBlank()) return false + val allowsPartialMatch = minOf(normalizedFirst.length, normalizedSecond.length) >= 6 + return normalizedFirst == normalizedSecond || + (allowsPartialMatch && ( + normalizedFirst.contains(normalizedSecond) || + normalizedSecond.contains(normalizedFirst) + )) + } + + private fun typesMatch(first: TvType?, second: TvType?): Boolean { + if (first == null || second == null) return true + return first == second || + (first.isMovieType() && second.isMovieType()) || + (first.isEpisodeBased() && second.isEpisodeBased()) + } + + private fun yearsMatch(first: Int?, second: Int?): Boolean { + return first == null || second == null || kotlin.math.abs(first - second) <= 1 + } + + @Suppress("DEPRECATION", "DEPRECATION_ERROR") + private fun ExtractorLink.withProviderDisplayName(providerName: String): ExtractorLink { + val cleanProviderName = providerName.trim().takeIf { it.isNotBlank() } ?: return this + if (name.startsWith("$cleanProviderName - ")) return this + + val cleanSourceName = name + .removePrefix(cleanProviderName) + .trimStart(' ', '-', ':') + .ifBlank { name } + val displayName = "$cleanProviderName - $cleanSourceName" + + return when (this) { + is ExtractorLinkPlayList -> copy(name = displayName) + is DrmExtractorLink -> DrmExtractorLink( + source = source, + name = displayName, + url = url, + referer = referer, + quality = quality, + type = type, + headers = headers, + extractorData = extractorData, + kid = kid, + key = key, + uuid = uuid, + kty = kty, + keyRequestParameters = keyRequestParameters, + licenseUrl = licenseUrl, + ) + else -> ExtractorLink( + source = source, + name = displayName, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type, + audioTracks = audioTracks, + ) + } + } } override val hasCache = true @@ -76,6 +178,7 @@ class RepoLinkGenerator( currentCache.linkCache.clear() currentCache.subtitleCache.clear() currentCache.saturated = false + currentCache.alternateSaturated = false } else if (currentCache.linkCache.isNotEmpty()) { Log.d( TAG, @@ -87,7 +190,7 @@ class RepoLinkGenerator( currentCache.linkCache.forEach { link -> currentLinksUrls.add(link.url) if (sourceTypes.contains(link.type)) { - callback(link to null) + callback(link.withProviderDisplayName(link.source) to null) } } @@ -99,63 +202,297 @@ class RepoLinkGenerator( // this stops all execution if links are cached // no extra get requests - if (currentCache.saturated) { + if (currentCache.saturated && (!includeAllProviderSources || currentCache.alternateSaturated)) { return true } } - val result = APIRepository( - getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist") - ).loadLinks( - current.data, - isCasting = isCasting, - subtitleCallback = { file -> - Log.d(TAG, "Loaded SubtitleFile: $file") - val correctFile = PlayerSubtitleHelper.getSubtitleData(file) - if (correctFile.url.isBlank() || !currentSubsUrls.add(correctFile.url)) { - return@loadLinks - } + suspend fun loadEpisodeLinks(episode: ResultEpisode, maxLinks: Int? = null): Boolean { + coroutineContext.ensureActive() + val api = getApiFromNameNull(episode.apiName) ?: return false + val acceptedLinks = AtomicInteger(0) + return APIRepository(api).loadLinks( + episode.data, + isCasting = isCasting, + subtitleCallback = { file -> + Log.d(TAG, "Loaded SubtitleFile: $file") + val correctFile = PlayerSubtitleHelper.getSubtitleData(file) + if (correctFile.url.isBlank() || !currentSubsUrls.add(correctFile.url)) { + return@loadLinks + } - // this part makes sure that all names are unique for UX - val nameDecoded = correctFile.originalName.html().toString() - .trim() // `%3Ch1%3Esub%20name…` → `

sub name…` → `sub name…` - val suffixCount = - lastCountedSuffix.getOrPut(nameDecoded) { AtomicInteger(0) }.incrementAndGet() + // Keep subtitle labels unique when multiple providers expose the same file name. + val nameDecoded = correctFile.originalName.html().toString() + .trim() // `%3Ch1%3Esub%20name…` → `

sub name…` → `sub name…` + val suffixCount = + lastCountedSuffix.getOrPut(nameDecoded) { AtomicInteger(0) }.incrementAndGet() - val updatedFile = - correctFile.copy(originalName = nameDecoded, nameSuffix = "$suffixCount") + val updatedFile = + correctFile.copy(originalName = nameDecoded, nameSuffix = "$suffixCount") - synchronized(currentCache) { - if (currentCache.subtitleCache.add(updatedFile)) { - subtitleCallback(updatedFile) - currentCache.lastCachedTimestamp = unixTime + synchronized(currentCache) { + if (currentCache.subtitleCache.add(updatedFile)) { + subtitleCallback(updatedFile) + currentCache.lastCachedTimestamp = unixTime + } } - } - }, - callback = { link -> - Log.d(TAG, "Loaded ExtractorLink: $link") - if (link.url.isBlank() || !currentLinksUrls.add(link.url)) { - return@loadLinks - } + }, + callback = { link -> + if (maxLinks != null && acceptedLinks.get() >= maxLinks) { + throw LinkLoadingLimitReached() + } + val displayLink = link.withProviderDisplayName(link.source.ifBlank { episode.apiName }) + Log.d(TAG, "Loaded ExtractorLink: $displayLink") + if (displayLink.url.isBlank() || !currentLinksUrls.add(displayLink.url)) { + return@loadLinks + } + acceptedLinks.incrementAndGet() - synchronized(currentCache) { - if (currentCache.linkCache.add(link)) { - if (sourceTypes.contains(link.type)) { - callback(Pair(link, null)) - } + synchronized(currentCache) { + if (currentCache.linkCache.add(displayLink)) { + if (sourceTypes.contains(displayLink.type)) { + callback(Pair(displayLink, null)) + } - currentCache.linkCache.add(link) - currentCache.lastCachedTimestamp = unixTime + currentCache.linkCache.add(displayLink) + currentCache.lastCachedTimestamp = unixTime + } } } - } + ) + } + + val result = loadEpisodeLinks( + current, + maxLinks = if (includeAllProviderSources) MAX_ALTERNATE_LINKS_PER_PROVIDER else null ) + var alternateResult = false + if (includeAllProviderSources) { + for (episode in getAlternateProviderEpisodes(current)) { + coroutineContext.ensureActive() + alternateResult = withTimeoutOrNull(20_000L) { + loadEpisodeLinks( + episode, + maxLinks = MAX_ALTERNATE_LINKS_PER_PROVIDER + ) + } == true || alternateResult + } + } synchronized(currentCache) { currentCache.saturated = currentCache.linkCache.isNotEmpty() + if (includeAllProviderSources) { + currentCache.alternateSaturated = true + } currentCache.lastCachedTimestamp = unixTime } - return result + return result || alternateResult + } + + private suspend fun getAlternateProviderEpisodes(current: ResultEpisode): List { + coroutineContext.ensureActive() + val currentPage = page ?: return emptyList() + if (allProviderSourceNames.isEmpty()) return emptyList() + + val providers = synchronized(APIHolder.apis) { + APIHolder.apis.filter { api -> + api.name != current.apiName && + allProviderSourceNames.contains(api.name) && + api.providerType != ProviderType.MetaProvider && + !api.usesWebView && + api.supportedTypes.any { typesMatch(it, currentPage.type) } + }.take(MAX_ALTERNATE_PROVIDER_COUNT) + } + + val matches = mutableListOf() + for (api in providers) { + coroutineContext.ensureActive() + withTimeoutOrNull(20_000L) { + findAlternateProviderEpisode(api, current, currentPage) + }?.let { matches += it } + } + return matches + } + + private suspend fun findAlternateProviderEpisode( + api: MainAPI, + current: ResultEpisode, + currentPage: LoadResponse, + ): ResultEpisode? { + coroutineContext.ensureActive() + val repo = APIRepository(api) + val search = repo.search(currentPage.name, 1) as? Resource.Success ?: return null + coroutineContext.ensureActive() + val match = search.value.items.firstOrNull { it.matchesCurrentPage(currentPage, strictTitle = true) } + ?: search.value.items.firstOrNull { it.matchesCurrentPage(currentPage, strictTitle = false) } + ?: return null + val load = repo.load(match.url) as? Resource.Success ?: return null + coroutineContext.ensureActive() + val response = load.value + if (!response.matchesCurrentPage(currentPage, strictTitle = false)) return null + + return response.toMatchingResultEpisode(current) + } + + private fun SearchResponse.matchesCurrentPage( + currentPage: LoadResponse, + strictTitle: Boolean, + ): Boolean { + val responseType = this.type + val titleMatches = if (strictTitle) { + normalizeTitle(name) == normalizeTitle(currentPage.name) + } else { + titlesMatch(name, currentPage.name) + } + return titleMatches && typesMatch(responseType, currentPage.type) && yearsMatch(getSearchYear(), currentPage.year) + } + + private fun SearchResponse.getSearchYear(): Int? { + return when (this) { + is com.lagradost.cloudstream3.MovieSearchResponse -> year + is com.lagradost.cloudstream3.TvSeriesSearchResponse -> year + is com.lagradost.cloudstream3.AnimeSearchResponse -> year + else -> null + } + } + + private fun LoadResponse.matchesCurrentPage( + currentPage: LoadResponse, + strictTitle: Boolean, + ): Boolean { + val titleMatches = if (strictTitle) { + normalizeTitle(name) == normalizeTitle(currentPage.name) + } else { + titlesMatch(name, currentPage.name) + } + return titleMatches && typesMatch(type, currentPage.type) && yearsMatch(year, currentPage.year) + } + + private fun LoadResponse.toMatchingResultEpisode(current: ResultEpisode): ResultEpisode? { + val mainId = getId() + return when (this) { + is MovieLoadResponse -> if (current.tvType.isMovieType()) { + buildResultEpisode( + name, + name, + null, + 0, + null, + null, + dataUrl, + apiName, + mainId, + 0, + null, + null, + null, + type, + mainId, + null, + ) + } else { + null + } + + is LiveStreamLoadResponse -> if (current.tvType.isMovieType()) { + buildResultEpisode( + name, + name, + null, + 0, + null, + null, + dataUrl, + apiName, + mainId, + 0, + null, + null, + null, + type, + mainId, + null, + ) + } else { + null + } + + is TorrentLoadResponse -> if (current.tvType.isMovieType()) { + buildResultEpisode( + name, + name, + null, + 0, + null, + null, + torrent ?: magnet ?: "", + apiName, + mainId, + 0, + null, + null, + null, + type, + mainId, + null, + ) + } else { + null + } + + is TvSeriesLoadResponse -> episodes.sortedByEpisode().withIndex() + .firstOrNull { (_, episode) -> episode.matchesCurrentEpisode(current) } + ?.let { (index, episode) -> buildEpisodeResult(this, episode, current, mainId, index) } + + is AnimeLoadResponse -> episodes.values.flatten().sortedByEpisode().withIndex() + .firstOrNull { (_, episode) -> episode.matchesCurrentEpisode(current) } + ?.let { (index, episode) -> buildEpisodeResult(this, episode, current, mainId, index) } + + else -> null + } + } + + private fun List.sortedByEpisode(): List { + return sortedBy { (it.season ?: 0) * 100_000 + (it.episode ?: 0) } + } + + private fun Episode.matchesCurrentEpisode(current: ResultEpisode): Boolean { + val episodeMatches = episode == null || episode == current.episode + val seasonMatches = season == null || current.seasonIndex == null || + season == current.seasonIndex || season == current.season + return episodeMatches && seasonMatches + } + + private fun buildEpisodeResult( + response: LoadResponse, + episode: Episode, + current: ResultEpisode, + mainId: Int, + index: Int, + ): ResultEpisode { + val episodeIndex = episode.episode ?: current.episode + val seasonIndex = episode.season ?: current.seasonIndex + val id = mainId + (seasonIndex?.times(100_000) ?: 0) + episodeIndex + 1 + return buildResultEpisode( + response.name, + episode.name, + episode.posterUrl, + episodeIndex, + seasonIndex, + episode.season, + episode.data, + response.apiName, + id, + index, + episode.score, + episode.description, + current.isFiller, + response.type, + mainId, + current.totalEpisodeIndex, + airDate = episode.date, + runTime = episode.runTime, + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 7dfe3cf5988..9dc9636beba 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -80,8 +80,10 @@ import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.getSearchAllProviderSourceNames import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.shouldSearchAllProviderSources import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -1268,7 +1270,12 @@ class ResultViewModel2 : ViewModel() { clearCache: Boolean = false, isCasting: Boolean = false ): LinkLoadingResult { - val tempGenerator = RepoLinkGenerator(listOf(result)) + val tempGenerator = RepoLinkGenerator( + listOf(result), + page = currentResponse, + includeAllProviderSources = context?.shouldSearchAllProviderSources() == true, + allProviderSourceNames = context?.getSearchAllProviderSourceNames().orEmpty() + ) val links: MutableSet = mutableSetOf() val subs: MutableSet = mutableSetOf() @@ -2074,14 +2081,26 @@ class ResultViewModel2 : ViewModel() { preferDubStatus = indexer.dubStatus generator = if (isMovie) { - getMovie()?.let { RepoLinkGenerator(listOf(it), page = currentResponse) } + getMovie()?.let { + RepoLinkGenerator( + listOf(it), + page = currentResponse, + includeAllProviderSources = context?.shouldSearchAllProviderSources() == true, + allProviderSourceNames = context?.getSearchAllProviderSourceNames().orEmpty() + ) + } } else { val episodes = currentEpisodes.filter { it.key.dubStatus == indexer.dubStatus } .toList() .sortedBy { it.first.season } .flatMap { it.second } - RepoLinkGenerator(episodes, page = currentResponse) + RepoLinkGenerator( + episodes, + page = currentResponse, + includeAllProviderSources = context?.shouldSearchAllProviderSources() == true, + allProviderSourceNames = context?.getSearchAllProviderSourceNames().orEmpty() + ) } if (isMovie) { @@ -2706,4 +2725,4 @@ class ResultViewModel2 : ViewModel() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 076f17a0aaf..97f23a883c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setTool import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.getSearchAllProviderSourceNames import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji @@ -109,6 +110,38 @@ class SettingsProviders : BasePreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.search_all_provider_sources_list_key)?.setOnPreferenceClickListener { + val providers = synchronized(APIHolder.apis) { + APIHolder.apis.filter { api -> api.providerType != ProviderType.MetaProvider } + .sortedBy { api -> api.name.lowercase() } + } + val selectedProviderNames = requireContext().getSearchAllProviderSourceNames() + val currentList = providers.mapIndexedNotNull { index, api -> + if (selectedProviderNames.contains(api.name)) index else null + } + + activity?.showMultiDialog( + providers.map { api -> "${api.name} (${api.lang})" }, + currentList, + getString(R.string.search_all_provider_sources_list), + {} + ) { selectedList -> + val limitedSelection = selectedList.take(5) + if (selectedList.size > limitedSelection.size) { + CommonActivity.showToast(R.string.search_all_provider_sources_limit) + } + + settingsManager.edit { + putStringSet( + getString(R.string.search_all_provider_sources_list_key), + limitedSelection.map { providers[it].name }.toSet() + ) + } + } + + return@setOnPreferenceClickListener true + } + getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { activity?.getApiProviderLangSettings()?.let { currentLangTags -> val languagesTagName = synchronized(APIHolder.apis) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 1377ccd08ad..b9fe5c2986f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -422,6 +422,19 @@ object AppContextUtils { return list.toHashSet() } + fun Context.shouldSearchAllProviderSources(): Boolean { + return PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(getString(R.string.search_all_provider_sources_key), false) + } + + fun Context.getSearchAllProviderSourceNames(): Set { + return PreferenceManager.getDefaultSharedPreferences(this) + .getStringSet(getString(R.string.search_all_provider_sources_list_key), emptySet()) + ?.take(5) + ?.toSet() + ?: emptySet() + } + fun Context.getApiTypeSettings(): HashSet { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val hashSet = HashSet() @@ -916,4 +929,4 @@ object AppContextUtils { } else null return currentAudioFocusRequest } -} \ No newline at end of file +} diff --git a/app/src/main/res/values-b+tr/strings.xml b/app/src/main/res/values-b+tr/strings.xml index d711505a821..753d7f3e955 100644 --- a/app/src/main/res/values-b+tr/strings.xml +++ b/app/src/main/res/values-b+tr/strings.xml @@ -281,6 +281,11 @@ Uygulama düzeni Tercih edilen medya Desteklenen uzantılarda NSFW etkinleştir + Tüm uzantılarda kaynak ara + Film veya bölüm kaynakları yüklenirken seçili uzantılardaki eşleşen kaynakları da listele. + Kaynağı birleştirilecek uzantılar + En fazla 5 uzantı seç. Her seçili uzantıdan en fazla 5 kaynak eklenir. + En fazla 5 uzantı seçebilirsin. Altyazı kodlaması Sağlayıcılar Düzen diff --git a/app/src/main/res/values/donottranslate-strings.xml b/app/src/main/res/values/donottranslate-strings.xml index 6a4c8271341..88fb8f973a4 100644 --- a/app/src/main/res/values/donottranslate-strings.xml +++ b/app/src/main/res/values/donottranslate-strings.xml @@ -73,6 +73,8 @@ filter_sub_lang_key pref_filter_search_quality_key enable_nsfw_on_providers_key + search_all_provider_sources_key + search_all_provider_sources_list_key skip_startup_account_select_key enable_skip_op_from_database rotate_video_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31cf951cf5f..a8d5307f77e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -380,6 +380,11 @@ App Layout Preferred media Enable NSFW on supported Extensions + Search sources in all extensions + When loading a movie or episode, also list matching sources from selected extensions. + Extensions to merge sources from + Select up to 5 extensions. Up to 5 sources are added from each selected extension. + You can select up to 5 extensions. Subtitle encoding Providers Provider test diff --git a/app/src/main/res/xml/settings_providers.xml b/app/src/main/res/xml/settings_providers.xml index b34583e2132..273bede9504 100644 --- a/app/src/main/res/xml/settings_providers.xml +++ b/app/src/main/res/xml/settings_providers.xml @@ -22,10 +22,24 @@ android:title="@string/enable_nsfw_on_providers" app:defaultValue="false" /> + + + + - \ No newline at end of file +