From 14a23bbdc7f61db5a39e01ebb20745a8607b6dbe Mon Sep 17 00:00:00 2001 From: Spring Date: Thu, 27 Aug 2026 12:16:55 +0700 Subject: [PATCH 1/8] feat(cache): add provider cache --- .../cloudstream3/ui/APIRepository.kt | 43 +- .../cloudstream3/ui/home/HomeCache.kt | 304 ++++++++++ .../ui/home/HomeParentItemAdapter.kt | 6 +- .../ui/home/HomeParentItemAdapterPreview.kt | 72 ++- .../cloudstream3/ui/home/HomeViewModel.kt | 215 ++++--- .../ui/settings/SettingsProviders.kt | 65 +++ .../cloudstream3/utils/DataStoreHelper.kt | 8 + app/src/main/res/values/strings.xml | 57 ++ app/src/main/res/xml/settings_providers.xml | 11 + .../cloudstream3/ui/home/HomeCacheTest.kt | 539 ++++++++++++++++++ 10 files changed, 1203 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt create mode 100644 app/src/test/java/com/lagradost/cloudstream3/ui/home/HomeCacheTest.kt 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 8ec082520fb..94d919edf10 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.newSearchResponseList +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope @@ -62,6 +63,10 @@ class APIRepository(val api: MainAPI) { fun getTimeout(desired: Long?): Long { return (desired ?: DEFAULT_TIMEOUT).coerceIn(MIN_TIMEOUT, MAX_TIMEOUT) } + + fun clearCache() { + cache.clear() + } } private fun afterPluginsLoaded(forceReload: Boolean) { @@ -88,31 +93,37 @@ class APIRepository(val api: MainAPI) { if (isInvalidData(url)) throw ErrorLoadingException() val fixedUrl = api.fixUrl(url) val lookingForHash = Pair(api.name, fixedUrl) - - val cached = cache.withLock { - var found: LoadResponse? = null - for (item in cache) { - // 10 min save - if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) { - found = item.response - break + val cacheTtl = DataStoreHelper.cacheTimeSeconds + val isCacheEnabled = DataStoreHelper.isCacheEnabled + + if (isCacheEnabled) { + val cached = cache.withLock { + var found: LoadResponse? = null + for (item in cache) { + if (item.hash == lookingForHash && (unixTime - item.unixTime) < cacheTtl) { + found = item.response + break + } } + found } - found + + if (cached != null) return@withTimeout cached } - if (cached != null) return@withTimeout cached api.load(fixedUrl)?.also { response -> // Remove all blank tags as early as possible response.tags = response.tags?.filter { it.isNotBlank() } val add = SavedLoadResponse(unixTime, response, lookingForHash) - cache.withLock { - if (cache.size > CACHE_SIZE) { - cache[cacheIndex] = add // rolling cache - cacheIndex = (cacheIndex + 1) % CACHE_SIZE - } else { - cache.add(add) + if (isCacheEnabled) { + cache.withLock { + if (cache.size > CACHE_SIZE) { + cache[cacheIndex] = add // rolling cache + cacheIndex = (cacheIndex + 1) % CACHE_SIZE + } else { + cache.add(add) + } } } } ?: throw ErrorLoadingException() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt new file mode 100644 index 00000000000..6da63c617eb --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt @@ -0,0 +1,304 @@ +package com.lagradost.cloudstream3.ui.home + +import android.content.Context +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey +import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey +import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey +import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs +import com.lagradost.cloudstream3.utils.DataStore.removeKeys +import com.lagradost.cloudstream3.HomePageResponse +import com.lagradost.cloudstream3.HomePageList +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.MovieSearchResponse +import com.lagradost.cloudstream3.TvSeriesSearchResponse +import com.lagradost.cloudstream3.AnimeSearchResponse +import com.lagradost.cloudstream3.TorrentSearchResponse +import com.lagradost.cloudstream3.LiveSearchResponse +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.SearchQuality +import com.lagradost.cloudstream3.Score +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.DataStoreHelper +import java.util.concurrent.ConcurrentHashMap + +object HomeCache { + private const val HOME_CACHE_FOLDER = "home_cache" + + @JsonIgnoreProperties(ignoreUnknown = true) + data class CachedSearchResponse( + @JsonProperty("name") val name: String, + @JsonProperty("url") val url: String, + @JsonProperty("apiName") val apiName: String, + @JsonProperty("type") val type: TvType? = null, + @JsonProperty("posterUrl") val posterUrl: String? = null, + @JsonProperty("posterHeaders") val posterHeaders: Map? = null, + @JsonProperty("id") val id: Int? = null, + @JsonProperty("quality") val quality: SearchQuality? = null, + @JsonProperty("scoreDouble") val scoreDouble: Double? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("dubStatus") val dubStatus: Set? = null, + @JsonProperty("animeEpisodes") val animeEpisodes: Map? = null, + @JsonProperty("tvEpisodes") val tvEpisodes: Int? = null, + @JsonProperty("otherName") val otherName: String? = null, + @JsonProperty("lang") val lang: String? = null + ) { + @Suppress("DEPRECATION_ERROR") + fun toSearchResponse(): SearchResponse { + val scoreObj = scoreDouble?.let { Score.from10(it) } + return when (type) { + TvType.Anime, TvType.AnimeMovie, TvType.OVA -> AnimeSearchResponse( + name = name, + url = url, + apiName = apiName, + type = type, + posterUrl = posterUrl, + year = year, + dubStatus = dubStatus?.toMutableSet(), + otherName = otherName, + episodes = animeEpisodes?.toMutableMap() ?: mutableMapOf(), + id = id, + quality = quality, + score = scoreObj, + posterHeaders = posterHeaders, + ) + TvType.TvSeries, TvType.AsianDrama -> TvSeriesSearchResponse( + name = name, + url = url, + apiName = apiName, + type = type, + posterUrl = posterUrl, + year = year, + episodes = tvEpisodes, + id = id, + quality = quality, + score = scoreObj, + posterHeaders = posterHeaders, + ) + TvType.Live -> LiveSearchResponse( + name = name, + url = url, + apiName = apiName, + type = type, + posterUrl = posterUrl, + id = id, + quality = quality, + score = scoreObj, + posterHeaders = posterHeaders, + lang = lang, + ) + TvType.Torrent -> TorrentSearchResponse( + name = name, + url = url, + apiName = apiName, + type = type, + posterUrl = posterUrl, + id = id, + quality = quality, + score = scoreObj, + posterHeaders = posterHeaders, + ) + else -> MovieSearchResponse( + name = name, + url = url, + apiName = apiName, + type = type ?: TvType.Movie, + posterUrl = posterUrl, + year = year, + id = id, + quality = quality, + score = scoreObj, + posterHeaders = posterHeaders, + ) + } + } + + companion object { + fun fromSearchResponse(res: SearchResponse): CachedSearchResponse { + val year = when (res) { + is MovieSearchResponse -> res.year + is TvSeriesSearchResponse -> res.year + is AnimeSearchResponse -> res.year + else -> null + } + val dubStatus = (res as? AnimeSearchResponse)?.dubStatus?.toSet() + val animeEpisodes = (res as? AnimeSearchResponse)?.episodes?.toMap() + val tvEpisodes = (res as? TvSeriesSearchResponse)?.episodes + val otherName = (res as? AnimeSearchResponse)?.otherName + val lang = (res as? LiveSearchResponse)?.lang + val scoreDouble = res.score?.toDouble(10) + + return CachedSearchResponse( + name = res.name, + url = res.url, + apiName = res.apiName, + type = res.type, + posterUrl = res.posterUrl, + posterHeaders = res.posterHeaders, + id = res.id, + quality = res.quality, + scoreDouble = scoreDouble, + year = year, + dubStatus = dubStatus, + animeEpisodes = animeEpisodes, + tvEpisodes = tvEpisodes, + otherName = otherName, + lang = lang + ) + } + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + data class CachedHomePageList( + @JsonProperty("name") val name: String, + @JsonProperty("list") val list: List, + @JsonProperty("isHorizontalImages") val isHorizontalImages: Boolean = false + ) { + @Suppress("DEPRECATION_ERROR") + fun toHomePageList(): HomePageList { + return HomePageList( + name = name, + list = list.map { it.toSearchResponse() }, + isHorizontalImages = isHorizontalImages + ) + } + + companion object { + fun fromHomePageList(homeList: HomePageList): CachedHomePageList { + return CachedHomePageList( + name = homeList.name, + list = homeList.list.map { CachedSearchResponse.fromSearchResponse(it) }, + isHorizontalImages = homeList.isHorizontalImages + ) + } + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + data class CachedHomePageResponse( + @JsonProperty("items") val items: List, + @JsonProperty("hasNext") val hasNext: Boolean = false + ) { + @Suppress("DEPRECATION_ERROR") + fun toHomePageResponse(): HomePageResponse { + return HomePageResponse( + items = items.map { it.toHomePageList() }, + hasNext = hasNext + ) + } + + companion object { + fun fromHomePageResponse(resp: HomePageResponse): CachedHomePageResponse { + return CachedHomePageResponse( + items = resp.items.map { CachedHomePageList.fromHomePageList(it) }, + hasNext = resp.hasNext + ) + } + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + data class CachedHomeData( + @JsonProperty("unixTime") val unixTime: Long, + @JsonProperty("responses") val responses: List + ) + + private val memoryCache = ConcurrentHashMap>>() + + fun getHomeCache(apiName: String): List? { + if (!DataStoreHelper.isCacheEnabled) return null + + val cacheTtlSeconds = DataStoreHelper.cacheTimeSeconds + + memoryCache[apiName]?.let { (savedTime, data) -> + if (unixTime - savedTime < cacheTtlSeconds) { + return data + } else { + memoryCache.remove(apiName) + } + } + + return try { + val diskCached = getKey(HOME_CACHE_FOLDER, apiName) + if (diskCached != null) { + if (unixTime - diskCached.unixTime < cacheTtlSeconds) { + val deserialized = diskCached.responses.map { it.toHomePageResponse() } + memoryCache[apiName] = Pair(diskCached.unixTime, deserialized) + deserialized + } else { + null + } + } else { + null + } + } catch (e: Exception) { + logError(e) + null + } + } + + fun setHomeCache(apiName: String, data: List) { + if (!DataStoreHelper.isCacheEnabled) return + val nonNullData = data.filterNotNull() + if (nonNullData.isEmpty()) return + + memoryCache[apiName] = Pair(unixTime, data) + + try { + val cachedHomeData = CachedHomeData( + unixTime = unixTime, + responses = nonNullData.map { CachedHomePageResponse.fromHomePageResponse(it) } + ) + setKey(HOME_CACHE_FOLDER, apiName, cachedHomeData) + } catch (e: Exception) { + logError(e) + } + } + + fun removeHomeCache(apiName: String) { + memoryCache.remove(apiName) + try { + removeKey(HOME_CACHE_FOLDER, apiName) + } catch (e: Exception) { + logError(e) + } + } + + fun clear() { + memoryCache.clear() + } + + fun clearAll(context: Context? = null) { + memoryCache.clear() + try { + if (context != null) { + context.removeKeys(HOME_CACHE_FOLDER) + } else { + com.lagradost.cloudstream3.CloudStreamApp.removeKeys(HOME_CACHE_FOLDER) + } + } catch (e: Exception) { + logError(e) + } + } + + fun getCacheSize(context: Context?): Long { + if (context == null) return 0L + var totalBytes = 0L + try { + val prefs = context.getSharedPrefs() + val prefix = "${HOME_CACHE_FOLDER}/" + for ((key, value) in prefs.all) { + if (key.startsWith(prefix) && value is String) { + totalBytes += value.toByteArray(Charsets.UTF_8).size.toLong() + } + } + } catch (e: Exception) { + logError(e) + } + return totalBytes + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 6bdd1bf492f..53f4211e400 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -82,7 +82,11 @@ open class ParentItemAdapter( ) { val binding = holder.view if (binding !is HomepageParentBinding) return - (binding.homeChildRecyclerview.adapter as? HomeChildItemAdapter)?.submitList(item.list.list) + (binding.homeChildRecyclerview.adapter as? HomeChildItemAdapter)?.apply { + isHorizontal = item.list.isHorizontalImages + hasNext = item.hasNext + submitList(item.list.list) + } } override fun onBindContent( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 959806e566c..b1c8eb809e3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -11,6 +11,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.view.isGone +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner @@ -547,10 +548,6 @@ class HomeParentItemAdapterPreview( alternateHeadProfilePic?.loadImage(currentAccount?.image) } - headProfilePicCard?.setOnClickListener { - activity?.showAccountSelectLinear() - } - fun showAccountEditBox(context: Context): Boolean { val currentAccount = DataStoreHelper.getCurrentAccount() return if (currentAccount != null) { @@ -580,6 +577,9 @@ class HomeParentItemAdapterPreview( alternateHeadProfilePicCard?.setOnClickListener { activity?.showAccountSelectLinear() } + headProfilePicCard?.setOnClickListener { + activity?.showAccountSelectLinear() + } (binding as? FragmentHomeHeadTvBinding)?.apply { /*homePreviewChangeApi.setOnClickListener { view -> @@ -651,8 +651,35 @@ class HomeParentItemAdapterPreview( } } + private fun clearPreview() { + previewAdapter.submitList(listOf()) + previewViewpager.setCurrentItem(0, false) + (binding as? FragmentHomeHeadTvBinding)?.apply { + homePreviewText.text = "" + homePreviewDescription.text = "" + homePreviewDescription.isGone = true + homePreviewScore.text = "" + homePreviewScore.isGone = true + homePreviewYear.text = "" + homePreviewYear.isGone = true + homePreviewDuration.text = "" + homePreviewDuration.isGone = true + homePreviewCast.text = "" + homePreviewCast.isGone = true + homePreviewTags.isGone = true + homeBackgroundPosterWatermarkBadgeHolder.setImageDrawable(null) + homeBackgroundPosterWatermarkBadgeHolder.isGone = true + homePreviewInfoBtt.setOnClickListener(null) + } + (binding as? FragmentHomeHeadBinding)?.apply { + homePreviewPlay.setOnClickListener(null) + homePreviewInfo.setOnClickListener(null) + homePreviewBookmark.setOnClickListener(null) + } + } + private fun updatePreview(preview: Resource>>) { - if (preview is Resource.Success) { + if (preview is Resource.Success || preview is Resource.Loading) { homeNonePadding.apply { val params = layoutParams params.height = 0 @@ -684,24 +711,49 @@ class HomeParentItemAdapterPreview( alternativeAccountPadding?.isVisible = false (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewInfoBtt.isVisible = true + homePreviewViewpagerText.isVisible = true } - // Explicitly bind the current item to ensure instant loading + (binding as? FragmentHomeHeadBinding)?.apply { + homePreviewTitleHolder.isVisible = true + } + val currentPos = previewViewpager.currentItem - val item = preview.value.second.getOrNull(currentPos) + val items = preview.value.second + val (item, pos) = if (currentPos in items.indices) { + items[currentPos] to currentPos + } else { + items.firstOrNull()?.let { it to 0 } ?: (null to 0) + } if (item != null) { - onSelect(item, currentPos) + onSelect(item, pos) + } + } + + is Resource.Loading -> { + clearPreview() + previewViewpager.isInvisible = true + previewViewpagerText.isVisible = true + alternativeAccountPadding?.isVisible = false + (binding as? FragmentHomeHeadTvBinding)?.apply { + homePreviewInfoBtt.isVisible = true + homePreviewViewpagerText.isInvisible = true + } + (binding as? FragmentHomeHeadBinding)?.apply { + homePreviewTitleHolder.isInvisible = true } } else -> { - previewAdapter.submitList(listOf()) - previewViewpager.setCurrentItem(0, false) + clearPreview() previewViewpager.isVisible = false previewViewpagerText.isVisible = false alternativeAccountPadding?.isVisible = true (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewInfoBtt.isVisible = false } + (binding as? FragmentHomeHeadBinding)?.apply { + homePreviewTitleHolder.isVisible = false + } //previewHeader.isVisible = false } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 8d48f5a6859..d2485e190c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -12,9 +12,10 @@ import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList +import com.lagradost.cloudstream3.HomePageResponse import com.lagradost.cloudstream3.LoadResponse -import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.Resource @@ -195,10 +196,8 @@ class HomeViewModel : ViewModel() { DataStoreHelper.homeBookmarkedList = watchPrefNotNull.map { it.internalId }.toIntArray() _availableWatchStatusTypes.postValue( - watchPrefNotNull to currentWatchTypes, - ) val list = withContext(Dispatchers.IO) { @@ -211,11 +210,11 @@ class HomeViewModel : ViewModel() { private var onGoingLoad: Job? = null private var isCurrentlyLoadingName: String? = null - private fun loadAndCancel(api: MainAPI) { + private fun loadAndCancel(api: MainAPI, forceReload: Boolean = false) { //println("loaded ${api.name}") onGoingLoad?.cancel() isCurrentlyLoadingName = api.name - onGoingLoad = load(api) + onGoingLoad = load(api, forceReload) } data class ExpandableHomepageList( @@ -226,7 +225,7 @@ class HomeViewModel : ViewModel() { private val expandable: MutableMap = mutableMapOf() private val _page = - MutableLiveData>>(Resource.Loading()) + MutableLiveData>>() val page: LiveData>> = _page val lock: MutableSet = mutableSetOf() @@ -268,7 +267,7 @@ class HomeViewModel : ViewModel() { current.hasNext = false } } - _page.postValue(Resource.Success(expandable)) + _page.postValue(Resource.Success(HashMap(expandable))) } lock -= name @@ -316,7 +315,84 @@ class HomeViewModel : ViewModel() { } } - private fun load(api: MainAPI): Job = ioSafe { + private suspend fun processHomePageData(dataValue: List) { + try { + dataValue.forEach { home -> + home?.items?.forEach { list -> + val filteredList = + context?.filterHomePageListByFilmQuality(list) ?: list + val existing = expandable[list.name] + if (existing != null && existing.currentPage > 1) { + val existingUrls = existing.list.list.map { it.url }.toSet() + val newItems = filteredList.list.filter { it.url !in existingUrls } + existing.list.list += newItems + existing.hasNext = home.hasNext + } else { + expandable[list.name] = + ExpandableHomepageList( + filteredList.copy( + list = CopyOnWriteArrayList( + filteredList.list + ) + ), 1, home.hasNext + ) + } + } + } + + _page.postValue(Resource.Success(HashMap(expandable))) + + val items = dataValue.mapNotNull { it?.items }.flatten() + + if (items.isNotEmpty()) { + val currentList = + items.filter { it.list.isNotEmpty() } + .flatMap { it.list } + .distinctBy { it.url } + + if (currentList.isNotEmpty()) { + val existingUrls = currentShuffledList.map { it.url }.toSet() + val newUrls = currentList.map { it.url }.toSet() + val shouldReshuffle = currentShuffledList.isEmpty() || existingUrls != newUrls + + if (shouldReshuffle) { + val shuffled = currentList.shuffled() + val randomItems = + context?.filterSearchResultByFilmQuality(shuffled) + ?: shuffled + + previewResponses.clear() + previewResponsesAdded.clear() + + updatePreviewResponses( + previewResponses, + previewResponsesAdded, + randomItems, + 3 + ) + + _randomItems.postValue(randomItems) + currentShuffledList = randomItems + } + } + } + if (previewResponses.isEmpty()) { + _preview.postValue( + Resource.Failure( + false, + "No homepage responses" + ) + ) + } else { + _preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses)) + } + } catch (e: Exception) { + _randomItems.postValue(emptyList()) + logError(e) + } + } + + private fun load(api: MainAPI, forceReload: Boolean = false): Job = ioSafe { repo = //if (api != null) { APIRepository(api) //} else { @@ -325,94 +401,52 @@ class HomeViewModel : ViewModel() { _apiName.postValue(repo?.name) _randomItems.postValue(listOf()) - - if (repo?.hasMainPage != true) { - _page.postValue(Resource.Success(emptyMap())) - _preview.postValue(Resource.Failure(false, "No homepage")) - return@ioSafe - } - - - _page.postValue(Resource.Loading()) + previewResponses.clear() + previewResponsesAdded.clear() + currentShuffledList = emptyList() _preview.postValue(Resource.Loading()) - // cancel the current preview expand as that is no longer relevant - addJob?.cancel() - - when (val data = repo?.getMainPage(1, null)) { - is Resource.Success -> { - try { - expandable.clear() - data.value.forEach { home -> - home?.items?.forEach { list -> - val filteredList = - context?.filterHomePageListByFilmQuality(list) ?: list - expandable[list.name] = - ExpandableHomepageList( - filteredList.copy( - list = CopyOnWriteArrayList( - filteredList.list - ) - ), 1, home.hasNext - ) - } - } - val items = data.value.mapNotNull { it?.items }.flatten() + expandable.clear() + try { + if (repo?.hasMainPage != true) { + _page.postValue(Resource.Success(emptyMap())) + _preview.postValue(Resource.Failure(false, "No homepage")) + return@ioSafe + } - previewResponses.clear() - previewResponsesAdded.clear() - - //val home = data.value - if (items.isNotEmpty()) { - val currentList = - items.shuffled().filter { it.list.isNotEmpty() } - .flatMap { it.list } - .distinctBy { it.url }.toList() + // cancel the current preview expand as that is no longer relevant + addJob?.cancel() - if (currentList.isNotEmpty()) { - val randomItems = - context?.filterSearchResultByFilmQuality(currentList.shuffled()) - ?: currentList.shuffled() + val cachedData = if (forceReload) null else HomeCache.getHomeCache(api.name) + var hadCachedData = false + if (!cachedData.isNullOrEmpty()) { + hadCachedData = true + processHomePageData(cachedData) + } else { + _page.postValue(Resource.Loading()) + } - updatePreviewResponses( - previewResponses, - previewResponsesAdded, - randomItems, - 3 - ) + when (val data = repo?.getMainPage(1, null)) { + is Resource.Success -> { + HomeCache.setHomeCache(api.name, data.value) + processHomePageData(data.value) + } - _randomItems.postValue(randomItems) - currentShuffledList = randomItems - } + is Resource.Failure -> { + if (!hadCachedData) { + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") + _page.postValue(data!!) + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") + _preview.postValue(data!!) } - if (previewResponses.isEmpty()) { - _preview.postValue( - Resource.Failure( - false, - "No homepage responses" - ) - ) - } else { - _preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses)) - } - _page.postValue(Resource.Success(expandable)) - } catch (e: Exception) { - _randomItems.postValue(emptyList()) - logError(e) } - } - is Resource.Failure -> { - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - _page.postValue(data!!) - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - _preview.postValue(data!!) + else -> Unit } - - else -> Unit + } finally { + isCurrentlyLoadingName = null } - isCurrentlyLoadingName = null } fun click(callback: SearchClickCallback) { @@ -467,6 +501,7 @@ class HomeViewModel : ViewModel() { MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded MainActivity.reloadHomeEvent -= ::reloadHome MainActivity.reloadAccountEvent -= ::reloadAccount + expandable.clear() super.onCleared() } @@ -512,7 +547,7 @@ class HomeViewModel : ViewModel() { // if we don't need to reload and we have a valid homepage or currently loading the same thing then return val currentLoading = isCurrentlyLoadingName - if (!forceReload && (currentPage is Resource.Success && currentPage.value.isNotEmpty() || (currentLoading != null && currentLoading == preferredApiName))) { + if (!forceReload && ((_apiName.value == preferredApiName && currentPage is Resource.Success && currentPage.value.isNotEmpty()) || (currentLoading != null && currentLoading == preferredApiName))) { return@ioSafe } @@ -520,23 +555,23 @@ class HomeViewModel : ViewModel() { if (preferredApiName == noneApi.name) { // just set to random if (fromUI) DataStoreHelper.currentHomePage = noneApi.name - loadAndCancel(noneApi) + loadAndCancel(noneApi, forceReload) } else if (preferredApiName == randomApi.name) { // randomize the api, if none exist like if not loaded or not installed // then use nothing val validAPIs = context?.filterProviderByPreferredMedia() if (validAPIs.isNullOrEmpty()) { - loadAndCancel(noneApi) + loadAndCancel(noneApi, forceReload) } else { val apiRandom = validAPIs.random() - loadAndCancel(apiRandom) + loadAndCancel(apiRandom, forceReload) if (fromUI) DataStoreHelper.currentHomePage = apiRandom.name } } else if (api == null) { // API is not found aka not loaded or removed, post the loading // progress if waiting for plugins, otherwise nothing if (PluginManager.loadedOnlinePlugins || PluginManager.isSafeMode()) { - loadAndCancel(noneApi) + loadAndCancel(noneApi, forceReload) } else { _page.postValue(Resource.Loading()) if (preferredApiName != null) @@ -545,7 +580,7 @@ class HomeViewModel : ViewModel() { } else { // if the api is found, then set it to it and save key if (fromUI) DataStoreHelper.currentHomePage = api.name - loadAndCancel(api) + loadAndCancel(api, forceReload) } reloadAccount() } 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 c8478a84003..0bf624b5c0e 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 @@ -1,14 +1,20 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle +import android.text.format.Formatter.formatShortFileSize import android.view.View +import android.widget.Toast import androidx.core.content.edit import androidx.navigation.fragment.findNavController import androidx.navigation.NavOptions import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.BasePreferenceFragmentCompat +import com.lagradost.cloudstream3.ui.home.HomeCache +import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags @@ -16,6 +22,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpTo import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -33,6 +40,64 @@ class SettingsProviders : BasePreferenceFragmentCompat() { setPreferencesFromResource(R.xml.settings_providers, rootKey) val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val cacheNames = resources.getStringArray(R.array.cache_time_names) + val cacheValues = resources.getIntArray(R.array.cache_time_values) + + fun updateCacheSummary() { + val currentVal = DataStoreHelper.cacheTimeMinutes + val index = cacheValues.indexOf(currentVal) + getPref(R.string.cache_time_key)?.summary = if (index != -1) { + cacheNames.getOrNull(index) + } else { + "${currentVal}m" + } + } + updateCacheSummary() + + getPref(R.string.cache_time_key)?.setOnPreferenceClickListener { + val currentVal = DataStoreHelper.cacheTimeMinutes + val currentIndex = cacheValues.indexOf(currentVal).let { if (it == -1) 0 else it } + + activity?.showBottomDialog( + cacheNames.toList(), + currentIndex, + getString(R.string.cache_time_settings), + false, + {} + ) { selectedIndex -> + val selectedMinutes = cacheValues.getOrNull(selectedIndex) ?: 0 + DataStoreHelper.cacheTimeMinutes = selectedMinutes + updateCacheSummary() + } + return@setOnPreferenceClickListener true + } + + getPref(R.string.clear_provider_cache_key)?.let { pref -> + fun updateSummary() { + try { + val size = HomeCache.getCacheSize(pref.context) + pref.summary = formatShortFileSize(pref.context, size) + } catch (e: Exception) { + logError(e) + } + } + + updateSummary() + + pref.setOnPreferenceClickListener { + try { + HomeCache.clearAll(context) + APIRepository.clearCache() + RepoLinkGenerator.cache.clear() + updateSummary() + showToast(R.string.clear_provider_cache_cleared, Toast.LENGTH_SHORT) + } catch (e: Exception) { + logError(e) + } + return@setOnPreferenceClickListener true + } + } + getPref(R.string.display_sub_key)?.setOnPreferenceClickListener { activity?.getApiDubstatusSettings()?.let { current -> val dublist = DubStatus.entries diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 340266f6c52..6222a24c45b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -160,6 +160,14 @@ object DataStoreHelper { _resultsSortingMode = value.ordinal } + var cacheTimeMinutes: Int by UserPreferenceDelegate( + "cache_time_pref", + 0 + ) + + val isCacheEnabled: Boolean get() = cacheTimeMinutes > 0 + val cacheTimeSeconds: Long get() = cacheTimeMinutes * 60L + @Serializable data class Account( @JsonProperty("keyIndex") @SerialName("keyIndex") val keyIndex: Int, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d530b59f6e..3fdfafdb265 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -792,4 +792,61 @@ %d downloads queued Live + Recently Added + Top 10 + New Season + New Episodes + Leaving Soon + Must Watch + Play Trailer + Add to My List + In My List + Select Season + Trailers & More + More Like This + Cast: + Genres: + This show is: + Creator: + Writers: + About %1$s + Maturity Rating: + Download Episode %1$d + Episode %1$d + Audio Description + TOP\n10 + Close + + cache_time_key + Cache Expiry Time + Control how long Home & Detail data is cached. Set to 0 to disable cache and always fetch latest updates. + + clear_provider_cache_key + Clear Provider Cache + Delete all saved homepage, detail, and extractor cache + Provider cache cleared + + + Off (Default - Always Reload) + 10 Minutes + 30 Minutes + 1 Hour + 3 Hours + 6 Hours + 1 Day + 3 Days + 1 Week + + + + 0 + 10 + 30 + 60 + 180 + 360 + 1440 + 4320 + 10080 + diff --git a/app/src/main/res/xml/settings_providers.xml b/app/src/main/res/xml/settings_providers.xml index b34583e2132..3095dabfaa4 100644 --- a/app/src/main/res/xml/settings_providers.xml +++ b/app/src/main/res/xml/settings_providers.xml @@ -22,6 +22,17 @@ android:title="@string/enable_nsfw_on_providers" app:defaultValue="false" /> + + + + Date: Thu, 27 Aug 2026 14:21:16 +0700 Subject: [PATCH 2/8] feat(cache): lazy load hero banner --- .../cloudstream3/ui/home/HomeCache.kt | 16 +++---- .../ui/home/HomeScrollTransformer.kt | 2 +- .../cloudstream3/ui/home/HomeViewModel.kt | 47 +++++++++++-------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt index 6da63c617eb..f4e0200117d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt @@ -30,9 +30,9 @@ object HomeCache { @JsonIgnoreProperties(ignoreUnknown = true) data class CachedSearchResponse( - @JsonProperty("name") val name: String, - @JsonProperty("url") val url: String, - @JsonProperty("apiName") val apiName: String, + @JsonProperty("name") val name: String = "", + @JsonProperty("url") val url: String = "", + @JsonProperty("apiName") val apiName: String = "", @JsonProperty("type") val type: TvType? = null, @JsonProperty("posterUrl") val posterUrl: String? = null, @JsonProperty("posterHeaders") val posterHeaders: Map? = null, @@ -154,8 +154,8 @@ object HomeCache { @JsonIgnoreProperties(ignoreUnknown = true) data class CachedHomePageList( - @JsonProperty("name") val name: String, - @JsonProperty("list") val list: List, + @JsonProperty("name") val name: String = "", + @JsonProperty("list") val list: List = emptyList(), @JsonProperty("isHorizontalImages") val isHorizontalImages: Boolean = false ) { @Suppress("DEPRECATION_ERROR") @@ -180,7 +180,7 @@ object HomeCache { @JsonIgnoreProperties(ignoreUnknown = true) data class CachedHomePageResponse( - @JsonProperty("items") val items: List, + @JsonProperty("items") val items: List = emptyList(), @JsonProperty("hasNext") val hasNext: Boolean = false ) { @Suppress("DEPRECATION_ERROR") @@ -203,8 +203,8 @@ object HomeCache { @JsonIgnoreProperties(ignoreUnknown = true) data class CachedHomeData( - @JsonProperty("unixTime") val unixTime: Long, - @JsonProperty("responses") val responses: List + @JsonProperty("unixTime") val unixTime: Long = 0L, + @JsonProperty("responses") val responses: List = emptyList() ) private val memoryCache = ConcurrentHashMap>>() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt index 2d6757f9294..f21524c70fd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt @@ -20,4 +20,4 @@ class HomeScrollTransformer : ViewPager2.PageTransformer { -padding, 0 ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index d2485e190c3..c779145bf4d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -361,30 +361,37 @@ class HomeViewModel : ViewModel() { context?.filterSearchResultByFilmQuality(shuffled) ?: shuffled - previewResponses.clear() - previewResponsesAdded.clear() + currentShuffledList = randomItems + _randomItems.postValue(randomItems) - updatePreviewResponses( - previewResponses, - previewResponsesAdded, - randomItems, - 3 - ) + // Lazy Hero Banner: Fetch banner details asynchronously in background + addJob?.cancel() + addJob = ioSafe { + previewResponses.clear() + previewResponsesAdded.clear() + + updatePreviewResponses( + previewResponses, + previewResponsesAdded, + randomItems, + 3 + ) - _randomItems.postValue(randomItems) - currentShuffledList = randomItems + if (previewResponses.isEmpty()) { + _preview.postValue( + Resource.Failure( + false, + "No homepage responses" + ) + ) + } else { + _preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses)) + } + } } } - } - if (previewResponses.isEmpty()) { - _preview.postValue( - Resource.Failure( - false, - "No homepage responses" - ) - ) } else { - _preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses)) + _preview.postValue(Resource.Failure(false, "No homepage responses")) } } catch (e: Exception) { _randomItems.postValue(emptyList()) @@ -535,7 +542,7 @@ class HomeViewModel : ViewModel() { // only save the key if it is from UI, as we don't want internal functions changing the setting fun loadAndCancel( preferredApiName: String?, - forceReload: Boolean = true, + forceReload: Boolean = false, fromUI: Boolean = false ) = ioSafe { From 4e15cee7d9fcc002ef7e8284aa0b62aa1507f049 Mon Sep 17 00:00:00 2001 From: Spring Date: Thu, 27 Aug 2026 21:56:06 +0700 Subject: [PATCH 3/8] fix(cache): simplified cache logic, remove unnecessary mapping, remove unused string --- .../cloudstream3/ui/APIRepository.kt | 48 +- .../cloudstream3/ui/home/HomeCache.kt | 304 ---------- .../ui/home/HomeParentItemAdapterPreview.kt | 65 +-- .../ui/home/HomeScrollTransformer.kt | 2 +- .../cloudstream3/ui/home/HomeViewModel.kt | 235 ++++---- .../ui/settings/SettingsProviders.kt | 56 +- .../cloudstream3/utils/DataStoreHelper.kt | 4 +- app/src/main/res/values/strings.xml | 24 - .../cloudstream3/ui/home/HomeCacheTest.kt | 539 ------------------ 9 files changed, 179 insertions(+), 1098 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt delete mode 100644 app/src/test/java/com/lagradost/cloudstream3/ui/home/HomeCacheTest.kt 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 94d919edf10..cb6a3938039 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -56,22 +56,33 @@ class APIRepository(val api: MainAPI) { val hash: Pair ) + data class SavedHomePageResponse( + val unixTime: Long, + val response: List, + val hash: Pair> + ) + private val cache = atomicListOf() private var cacheIndex: Int = 0 const val CACHE_SIZE = 20 + private val homeCache = atomicListOf() + private var homeCacheIndex: Int = 0 + const val HOME_CACHE_SIZE = 20 + fun getTimeout(desired: Long?): Long { return (desired ?: DEFAULT_TIMEOUT).coerceIn(MIN_TIMEOUT, MAX_TIMEOUT) } fun clearCache() { cache.clear() + homeCache.clear() } } private fun afterPluginsLoaded(forceReload: Boolean) { if (forceReload) { - cache.clear() + clearCache() } } @@ -165,11 +176,30 @@ class APIRepository(val api: MainAPI) { } suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource> { + val lookingForHash = Pair(api.name, Pair(page, nameIndex)) + val cacheTtl = DataStoreHelper.cacheTimeSeconds + val isCacheEnabled = DataStoreHelper.isCacheEnabled + + if (isCacheEnabled) { + val cached = homeCache.withLock { + var found: List? = null + for (item in homeCache) { + if (item.hash == lookingForHash && (unixTime - item.unixTime) < cacheTtl) { + found = item.response + break + } + } + found + } + + if (cached != null) return Resource.Success(cached) + } + return safeApiCall { withTimeout(getTimeout(api.getMainPageTimeoutMs)) { api.lastHomepageRequest = unixTimeMS - nameIndex?.let { api.mainPage.getOrNull(it) }?.let { data -> + val res = nameIndex?.let { api.mainPage.getOrNull(it) }?.let { data -> listOf( api.getMainPage( page, @@ -202,6 +232,20 @@ class APIRepository(val api: MainAPI) { } } } + + if (isCacheEnabled && res.isNotEmpty()) { + val add = SavedHomePageResponse(unixTime, res, lookingForHash) + homeCache.withLock { + if (homeCache.size > HOME_CACHE_SIZE) { + homeCache[homeCacheIndex] = add // rolling cache + homeCacheIndex = (homeCacheIndex + 1) % HOME_CACHE_SIZE + } else { + homeCache.add(add) + } + } + } + + res } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt deleted file mode 100644 index f4e0200117d..00000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeCache.kt +++ /dev/null @@ -1,304 +0,0 @@ -package com.lagradost.cloudstream3.ui.home - -import android.content.Context -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey -import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey -import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey -import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs -import com.lagradost.cloudstream3.utils.DataStore.removeKeys -import com.lagradost.cloudstream3.HomePageResponse -import com.lagradost.cloudstream3.HomePageList -import com.lagradost.cloudstream3.SearchResponse -import com.lagradost.cloudstream3.MovieSearchResponse -import com.lagradost.cloudstream3.TvSeriesSearchResponse -import com.lagradost.cloudstream3.AnimeSearchResponse -import com.lagradost.cloudstream3.TorrentSearchResponse -import com.lagradost.cloudstream3.LiveSearchResponse -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.SearchQuality -import com.lagradost.cloudstream3.Score -import com.lagradost.cloudstream3.DubStatus -import com.lagradost.cloudstream3.APIHolder.unixTime -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.DataStoreHelper -import java.util.concurrent.ConcurrentHashMap - -object HomeCache { - private const val HOME_CACHE_FOLDER = "home_cache" - - @JsonIgnoreProperties(ignoreUnknown = true) - data class CachedSearchResponse( - @JsonProperty("name") val name: String = "", - @JsonProperty("url") val url: String = "", - @JsonProperty("apiName") val apiName: String = "", - @JsonProperty("type") val type: TvType? = null, - @JsonProperty("posterUrl") val posterUrl: String? = null, - @JsonProperty("posterHeaders") val posterHeaders: Map? = null, - @JsonProperty("id") val id: Int? = null, - @JsonProperty("quality") val quality: SearchQuality? = null, - @JsonProperty("scoreDouble") val scoreDouble: Double? = null, - @JsonProperty("year") val year: Int? = null, - @JsonProperty("dubStatus") val dubStatus: Set? = null, - @JsonProperty("animeEpisodes") val animeEpisodes: Map? = null, - @JsonProperty("tvEpisodes") val tvEpisodes: Int? = null, - @JsonProperty("otherName") val otherName: String? = null, - @JsonProperty("lang") val lang: String? = null - ) { - @Suppress("DEPRECATION_ERROR") - fun toSearchResponse(): SearchResponse { - val scoreObj = scoreDouble?.let { Score.from10(it) } - return when (type) { - TvType.Anime, TvType.AnimeMovie, TvType.OVA -> AnimeSearchResponse( - name = name, - url = url, - apiName = apiName, - type = type, - posterUrl = posterUrl, - year = year, - dubStatus = dubStatus?.toMutableSet(), - otherName = otherName, - episodes = animeEpisodes?.toMutableMap() ?: mutableMapOf(), - id = id, - quality = quality, - score = scoreObj, - posterHeaders = posterHeaders, - ) - TvType.TvSeries, TvType.AsianDrama -> TvSeriesSearchResponse( - name = name, - url = url, - apiName = apiName, - type = type, - posterUrl = posterUrl, - year = year, - episodes = tvEpisodes, - id = id, - quality = quality, - score = scoreObj, - posterHeaders = posterHeaders, - ) - TvType.Live -> LiveSearchResponse( - name = name, - url = url, - apiName = apiName, - type = type, - posterUrl = posterUrl, - id = id, - quality = quality, - score = scoreObj, - posterHeaders = posterHeaders, - lang = lang, - ) - TvType.Torrent -> TorrentSearchResponse( - name = name, - url = url, - apiName = apiName, - type = type, - posterUrl = posterUrl, - id = id, - quality = quality, - score = scoreObj, - posterHeaders = posterHeaders, - ) - else -> MovieSearchResponse( - name = name, - url = url, - apiName = apiName, - type = type ?: TvType.Movie, - posterUrl = posterUrl, - year = year, - id = id, - quality = quality, - score = scoreObj, - posterHeaders = posterHeaders, - ) - } - } - - companion object { - fun fromSearchResponse(res: SearchResponse): CachedSearchResponse { - val year = when (res) { - is MovieSearchResponse -> res.year - is TvSeriesSearchResponse -> res.year - is AnimeSearchResponse -> res.year - else -> null - } - val dubStatus = (res as? AnimeSearchResponse)?.dubStatus?.toSet() - val animeEpisodes = (res as? AnimeSearchResponse)?.episodes?.toMap() - val tvEpisodes = (res as? TvSeriesSearchResponse)?.episodes - val otherName = (res as? AnimeSearchResponse)?.otherName - val lang = (res as? LiveSearchResponse)?.lang - val scoreDouble = res.score?.toDouble(10) - - return CachedSearchResponse( - name = res.name, - url = res.url, - apiName = res.apiName, - type = res.type, - posterUrl = res.posterUrl, - posterHeaders = res.posterHeaders, - id = res.id, - quality = res.quality, - scoreDouble = scoreDouble, - year = year, - dubStatus = dubStatus, - animeEpisodes = animeEpisodes, - tvEpisodes = tvEpisodes, - otherName = otherName, - lang = lang - ) - } - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - data class CachedHomePageList( - @JsonProperty("name") val name: String = "", - @JsonProperty("list") val list: List = emptyList(), - @JsonProperty("isHorizontalImages") val isHorizontalImages: Boolean = false - ) { - @Suppress("DEPRECATION_ERROR") - fun toHomePageList(): HomePageList { - return HomePageList( - name = name, - list = list.map { it.toSearchResponse() }, - isHorizontalImages = isHorizontalImages - ) - } - - companion object { - fun fromHomePageList(homeList: HomePageList): CachedHomePageList { - return CachedHomePageList( - name = homeList.name, - list = homeList.list.map { CachedSearchResponse.fromSearchResponse(it) }, - isHorizontalImages = homeList.isHorizontalImages - ) - } - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - data class CachedHomePageResponse( - @JsonProperty("items") val items: List = emptyList(), - @JsonProperty("hasNext") val hasNext: Boolean = false - ) { - @Suppress("DEPRECATION_ERROR") - fun toHomePageResponse(): HomePageResponse { - return HomePageResponse( - items = items.map { it.toHomePageList() }, - hasNext = hasNext - ) - } - - companion object { - fun fromHomePageResponse(resp: HomePageResponse): CachedHomePageResponse { - return CachedHomePageResponse( - items = resp.items.map { CachedHomePageList.fromHomePageList(it) }, - hasNext = resp.hasNext - ) - } - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - data class CachedHomeData( - @JsonProperty("unixTime") val unixTime: Long = 0L, - @JsonProperty("responses") val responses: List = emptyList() - ) - - private val memoryCache = ConcurrentHashMap>>() - - fun getHomeCache(apiName: String): List? { - if (!DataStoreHelper.isCacheEnabled) return null - - val cacheTtlSeconds = DataStoreHelper.cacheTimeSeconds - - memoryCache[apiName]?.let { (savedTime, data) -> - if (unixTime - savedTime < cacheTtlSeconds) { - return data - } else { - memoryCache.remove(apiName) - } - } - - return try { - val diskCached = getKey(HOME_CACHE_FOLDER, apiName) - if (diskCached != null) { - if (unixTime - diskCached.unixTime < cacheTtlSeconds) { - val deserialized = diskCached.responses.map { it.toHomePageResponse() } - memoryCache[apiName] = Pair(diskCached.unixTime, deserialized) - deserialized - } else { - null - } - } else { - null - } - } catch (e: Exception) { - logError(e) - null - } - } - - fun setHomeCache(apiName: String, data: List) { - if (!DataStoreHelper.isCacheEnabled) return - val nonNullData = data.filterNotNull() - if (nonNullData.isEmpty()) return - - memoryCache[apiName] = Pair(unixTime, data) - - try { - val cachedHomeData = CachedHomeData( - unixTime = unixTime, - responses = nonNullData.map { CachedHomePageResponse.fromHomePageResponse(it) } - ) - setKey(HOME_CACHE_FOLDER, apiName, cachedHomeData) - } catch (e: Exception) { - logError(e) - } - } - - fun removeHomeCache(apiName: String) { - memoryCache.remove(apiName) - try { - removeKey(HOME_CACHE_FOLDER, apiName) - } catch (e: Exception) { - logError(e) - } - } - - fun clear() { - memoryCache.clear() - } - - fun clearAll(context: Context? = null) { - memoryCache.clear() - try { - if (context != null) { - context.removeKeys(HOME_CACHE_FOLDER) - } else { - com.lagradost.cloudstream3.CloudStreamApp.removeKeys(HOME_CACHE_FOLDER) - } - } catch (e: Exception) { - logError(e) - } - } - - fun getCacheSize(context: Context?): Long { - if (context == null) return 0L - var totalBytes = 0L - try { - val prefs = context.getSharedPrefs() - val prefix = "${HOME_CACHE_FOLDER}/" - for ((key, value) in prefs.all) { - if (key.startsWith(prefix) && value is String) { - totalBytes += value.toByteArray(Charsets.UTF_8).size.toLong() - } - } - } catch (e: Exception) { - logError(e) - } - return totalBytes - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index b1c8eb809e3..be48d4794dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -548,6 +548,10 @@ class HomeParentItemAdapterPreview( alternateHeadProfilePic?.loadImage(currentAccount?.image) } + headProfilePicCard?.setOnClickListener { + activity?.showAccountSelectLinear() + } + fun showAccountEditBox(context: Context): Boolean { val currentAccount = DataStoreHelper.getCurrentAccount() return if (currentAccount != null) { @@ -577,9 +581,6 @@ class HomeParentItemAdapterPreview( alternateHeadProfilePicCard?.setOnClickListener { activity?.showAccountSelectLinear() } - headProfilePicCard?.setOnClickListener { - activity?.showAccountSelectLinear() - } (binding as? FragmentHomeHeadTvBinding)?.apply { /*homePreviewChangeApi.setOnClickListener { view -> @@ -651,33 +652,6 @@ class HomeParentItemAdapterPreview( } } - private fun clearPreview() { - previewAdapter.submitList(listOf()) - previewViewpager.setCurrentItem(0, false) - (binding as? FragmentHomeHeadTvBinding)?.apply { - homePreviewText.text = "" - homePreviewDescription.text = "" - homePreviewDescription.isGone = true - homePreviewScore.text = "" - homePreviewScore.isGone = true - homePreviewYear.text = "" - homePreviewYear.isGone = true - homePreviewDuration.text = "" - homePreviewDuration.isGone = true - homePreviewCast.text = "" - homePreviewCast.isGone = true - homePreviewTags.isGone = true - homeBackgroundPosterWatermarkBadgeHolder.setImageDrawable(null) - homeBackgroundPosterWatermarkBadgeHolder.isGone = true - homePreviewInfoBtt.setOnClickListener(null) - } - (binding as? FragmentHomeHeadBinding)?.apply { - homePreviewPlay.setOnClickListener(null) - homePreviewInfo.setOnClickListener(null) - homePreviewBookmark.setOnClickListener(null) - } - } - private fun updatePreview(preview: Resource>>) { if (preview is Resource.Success || preview is Resource.Loading) { homeNonePadding.apply { @@ -711,49 +685,32 @@ class HomeParentItemAdapterPreview( alternativeAccountPadding?.isVisible = false (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewInfoBtt.isVisible = true - homePreviewViewpagerText.isVisible = true - } - (binding as? FragmentHomeHeadBinding)?.apply { - homePreviewTitleHolder.isVisible = true } - + // Explicitly bind the current item to ensure instant loading val currentPos = previewViewpager.currentItem - val items = preview.value.second - val (item, pos) = if (currentPos in items.indices) { - items[currentPos] to currentPos - } else { - items.firstOrNull()?.let { it to 0 } ?: (null to 0) - } + val item = preview.value.second.getOrNull(currentPos) if (item != null) { - onSelect(item, pos) + onSelect(item, currentPos) } } is Resource.Loading -> { - clearPreview() + previewAdapter.submitList(listOf()) + previewViewpager.setCurrentItem(0, false) previewViewpager.isInvisible = true previewViewpagerText.isVisible = true alternativeAccountPadding?.isVisible = false - (binding as? FragmentHomeHeadTvBinding)?.apply { - homePreviewInfoBtt.isVisible = true - homePreviewViewpagerText.isInvisible = true - } - (binding as? FragmentHomeHeadBinding)?.apply { - homePreviewTitleHolder.isInvisible = true - } } else -> { - clearPreview() + previewAdapter.submitList(listOf()) + previewViewpager.setCurrentItem(0, false) previewViewpager.isVisible = false previewViewpagerText.isVisible = false alternativeAccountPadding?.isVisible = true (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewInfoBtt.isVisible = false } - (binding as? FragmentHomeHeadBinding)?.apply { - homePreviewTitleHolder.isVisible = false - } //previewHeader.isVisible = false } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt index f21524c70fd..2d6757f9294 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt @@ -20,4 +20,4 @@ class HomeScrollTransformer : ViewPager2.PageTransformer { -padding, 0 ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index c779145bf4d..8bb595cde08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -12,10 +12,9 @@ import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList -import com.lagradost.cloudstream3.HomePageResponse import com.lagradost.cloudstream3.LoadResponse -import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.Resource @@ -196,8 +195,10 @@ class HomeViewModel : ViewModel() { DataStoreHelper.homeBookmarkedList = watchPrefNotNull.map { it.internalId }.toIntArray() _availableWatchStatusTypes.postValue( + watchPrefNotNull to currentWatchTypes, + ) val list = withContext(Dispatchers.IO) { @@ -210,11 +211,11 @@ class HomeViewModel : ViewModel() { private var onGoingLoad: Job? = null private var isCurrentlyLoadingName: String? = null - private fun loadAndCancel(api: MainAPI, forceReload: Boolean = false) { + private fun loadAndCancel(api: MainAPI) { //println("loaded ${api.name}") onGoingLoad?.cancel() isCurrentlyLoadingName = api.name - onGoingLoad = load(api, forceReload) + onGoingLoad = load(api) } data class ExpandableHomepageList( @@ -225,7 +226,7 @@ class HomeViewModel : ViewModel() { private val expandable: MutableMap = mutableMapOf() private val _page = - MutableLiveData>>() + MutableLiveData>>(Resource.Loading()) val page: LiveData>> = _page val lock: MutableSet = mutableSetOf() @@ -267,7 +268,7 @@ class HomeViewModel : ViewModel() { current.hasNext = false } } - _page.postValue(Resource.Success(HashMap(expandable))) + _page.postValue(Resource.Success(expandable)) } lock -= name @@ -315,91 +316,7 @@ class HomeViewModel : ViewModel() { } } - private suspend fun processHomePageData(dataValue: List) { - try { - dataValue.forEach { home -> - home?.items?.forEach { list -> - val filteredList = - context?.filterHomePageListByFilmQuality(list) ?: list - val existing = expandable[list.name] - if (existing != null && existing.currentPage > 1) { - val existingUrls = existing.list.list.map { it.url }.toSet() - val newItems = filteredList.list.filter { it.url !in existingUrls } - existing.list.list += newItems - existing.hasNext = home.hasNext - } else { - expandable[list.name] = - ExpandableHomepageList( - filteredList.copy( - list = CopyOnWriteArrayList( - filteredList.list - ) - ), 1, home.hasNext - ) - } - } - } - - _page.postValue(Resource.Success(HashMap(expandable))) - - val items = dataValue.mapNotNull { it?.items }.flatten() - - if (items.isNotEmpty()) { - val currentList = - items.filter { it.list.isNotEmpty() } - .flatMap { it.list } - .distinctBy { it.url } - - if (currentList.isNotEmpty()) { - val existingUrls = currentShuffledList.map { it.url }.toSet() - val newUrls = currentList.map { it.url }.toSet() - val shouldReshuffle = currentShuffledList.isEmpty() || existingUrls != newUrls - - if (shouldReshuffle) { - val shuffled = currentList.shuffled() - val randomItems = - context?.filterSearchResultByFilmQuality(shuffled) - ?: shuffled - - currentShuffledList = randomItems - _randomItems.postValue(randomItems) - - // Lazy Hero Banner: Fetch banner details asynchronously in background - addJob?.cancel() - addJob = ioSafe { - previewResponses.clear() - previewResponsesAdded.clear() - - updatePreviewResponses( - previewResponses, - previewResponsesAdded, - randomItems, - 3 - ) - - if (previewResponses.isEmpty()) { - _preview.postValue( - Resource.Failure( - false, - "No homepage responses" - ) - ) - } else { - _preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses)) - } - } - } - } - } else { - _preview.postValue(Resource.Failure(false, "No homepage responses")) - } - } catch (e: Exception) { - _randomItems.postValue(emptyList()) - logError(e) - } - } - - private fun load(api: MainAPI, forceReload: Boolean = false): Job = ioSafe { + private fun load(api: MainAPI): Job = ioSafe { repo = //if (api != null) { APIRepository(api) //} else { @@ -408,52 +325,101 @@ class HomeViewModel : ViewModel() { _apiName.postValue(repo?.name) _randomItems.postValue(listOf()) - previewResponses.clear() - previewResponsesAdded.clear() - currentShuffledList = emptyList() - _preview.postValue(Resource.Loading()) - expandable.clear() + if (repo?.hasMainPage != true) { + _page.postValue(Resource.Success(emptyMap())) + _preview.postValue(Resource.Failure(false, "No homepage")) + return@ioSafe + } - try { - if (repo?.hasMainPage != true) { - _page.postValue(Resource.Success(emptyMap())) - _preview.postValue(Resource.Failure(false, "No homepage")) - return@ioSafe - } - // cancel the current preview expand as that is no longer relevant - addJob?.cancel() + if (expandable.isEmpty()) { + _page.postValue(Resource.Loading()) + _preview.postValue(Resource.Loading()) + } + // cancel the current preview expand as that is no longer relevant + addJob?.cancel() + + when (val data = repo?.getMainPage(1, null)) { + is Resource.Success -> { + try { + expandable.clear() + data.value.forEach { home -> + home?.items?.forEach { list -> + val filteredList = + context?.filterHomePageListByFilmQuality(list) ?: list + expandable[list.name] = + ExpandableHomepageList( + filteredList.copy( + list = CopyOnWriteArrayList( + filteredList.list + ) + ), 1, home.hasNext + ) + } + } - val cachedData = if (forceReload) null else HomeCache.getHomeCache(api.name) - var hadCachedData = false - if (!cachedData.isNullOrEmpty()) { - hadCachedData = true - processHomePageData(cachedData) - } else { - _page.postValue(Resource.Loading()) - } + _page.postValue(Resource.Success(expandable)) - when (val data = repo?.getMainPage(1, null)) { - is Resource.Success -> { - HomeCache.setHomeCache(api.name, data.value) - processHomePageData(data.value) - } + val items = data.value.mapNotNull { it?.items }.flatten() + + + previewResponses.clear() + previewResponsesAdded.clear() + + //val home = data.value + if (items.isNotEmpty()) { + val currentList = + items.shuffled().filter { it.list.isNotEmpty() } + .flatMap { it.list } + .distinctBy { it.url }.toList() + + if (currentList.isNotEmpty()) { + val randomItems = + context?.filterSearchResultByFilmQuality(currentList.shuffled()) + ?: currentList.shuffled() - is Resource.Failure -> { - if (!hadCachedData) { - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - _page.postValue(data!!) - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - _preview.postValue(data!!) + _randomItems.postValue(randomItems) + currentShuffledList = randomItems + + addJob?.cancel() + addJob = ioSafe { + updatePreviewResponses( + previewResponses, + previewResponsesAdded, + randomItems, + 3 + ) + + if (previewResponses.isEmpty()) { + _preview.postValue( + Resource.Failure( + false, + "No homepage responses" + ) + ) + } else { + _preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses)) + } + } + } } + } catch (e: Exception) { + _randomItems.postValue(emptyList()) + logError(e) } + } - else -> Unit + is Resource.Failure -> { + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") + _page.postValue(data!!) + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") + _preview.postValue(data!!) } - } finally { - isCurrentlyLoadingName = null + + else -> Unit } + isCurrentlyLoadingName = null } fun click(callback: SearchClickCallback) { @@ -508,7 +474,6 @@ class HomeViewModel : ViewModel() { MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded MainActivity.reloadHomeEvent -= ::reloadHome MainActivity.reloadAccountEvent -= ::reloadAccount - expandable.clear() super.onCleared() } @@ -542,7 +507,7 @@ class HomeViewModel : ViewModel() { // only save the key if it is from UI, as we don't want internal functions changing the setting fun loadAndCancel( preferredApiName: String?, - forceReload: Boolean = false, + forceReload: Boolean = true, fromUI: Boolean = false ) = ioSafe { @@ -558,27 +523,31 @@ class HomeViewModel : ViewModel() { return@ioSafe } + if (forceReload) { + APIRepository.clearCache() + } + val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random if (fromUI) DataStoreHelper.currentHomePage = noneApi.name - loadAndCancel(noneApi, forceReload) + loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { // randomize the api, if none exist like if not loaded or not installed // then use nothing val validAPIs = context?.filterProviderByPreferredMedia() if (validAPIs.isNullOrEmpty()) { - loadAndCancel(noneApi, forceReload) + loadAndCancel(noneApi) } else { val apiRandom = validAPIs.random() - loadAndCancel(apiRandom, forceReload) + loadAndCancel(apiRandom) if (fromUI) DataStoreHelper.currentHomePage = apiRandom.name } } else if (api == null) { // API is not found aka not loaded or removed, post the loading // progress if waiting for plugins, otherwise nothing if (PluginManager.loadedOnlinePlugins || PluginManager.isSafeMode()) { - loadAndCancel(noneApi, forceReload) + loadAndCancel(noneApi) } else { _page.postValue(Resource.Loading()) if (preferredApiName != null) @@ -587,7 +556,7 @@ class HomeViewModel : ViewModel() { } else { // if the api is found, then set it to it and save key if (fromUI) DataStoreHelper.currentHomePage = api.name - loadAndCancel(api, forceReload) + loadAndCancel(api) } reloadAccount() } 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 0bf624b5c0e..d69c06314bf 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 @@ -1,19 +1,17 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle -import android.text.format.Formatter.formatShortFileSize import android.view.View import android.widget.Toast import androidx.core.content.edit -import androidx.navigation.fragment.findNavController import androidx.navigation.NavOptions +import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.BasePreferenceFragmentCompat -import com.lagradost.cloudstream3.ui.home.HomeCache import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom @@ -40,24 +38,18 @@ class SettingsProviders : BasePreferenceFragmentCompat() { setPreferencesFromResource(R.xml.settings_providers, rootKey) val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val cachePref = getPref(R.string.cache_time_key) val cacheNames = resources.getStringArray(R.array.cache_time_names) val cacheValues = resources.getIntArray(R.array.cache_time_values) fun updateCacheSummary() { - val currentVal = DataStoreHelper.cacheTimeMinutes - val index = cacheValues.indexOf(currentVal) - getPref(R.string.cache_time_key)?.summary = if (index != -1) { - cacheNames.getOrNull(index) - } else { - "${currentVal}m" - } + val idx = cacheValues.indexOf(DataStoreHelper.cacheTimeMinutes) + cachePref?.summary = cacheNames.getOrNull(idx) ?: "${DataStoreHelper.cacheTimeMinutes}m" } updateCacheSummary() - getPref(R.string.cache_time_key)?.setOnPreferenceClickListener { - val currentVal = DataStoreHelper.cacheTimeMinutes - val currentIndex = cacheValues.indexOf(currentVal).let { if (it == -1) 0 else it } - + cachePref?.setOnPreferenceClickListener { + val currentIndex = cacheValues.indexOf(DataStoreHelper.cacheTimeMinutes).coerceAtLeast(0) activity?.showBottomDialog( cacheNames.toList(), currentIndex, @@ -65,37 +57,21 @@ class SettingsProviders : BasePreferenceFragmentCompat() { false, {} ) { selectedIndex -> - val selectedMinutes = cacheValues.getOrNull(selectedIndex) ?: 0 - DataStoreHelper.cacheTimeMinutes = selectedMinutes + DataStoreHelper.cacheTimeMinutes = cacheValues.getOrElse(selectedIndex) { 0 } updateCacheSummary() } - return@setOnPreferenceClickListener true + true } - getPref(R.string.clear_provider_cache_key)?.let { pref -> - fun updateSummary() { - try { - val size = HomeCache.getCacheSize(pref.context) - pref.summary = formatShortFileSize(pref.context, size) - } catch (e: Exception) { - logError(e) - } - } - - updateSummary() - - pref.setOnPreferenceClickListener { - try { - HomeCache.clearAll(context) - APIRepository.clearCache() - RepoLinkGenerator.cache.clear() - updateSummary() - showToast(R.string.clear_provider_cache_cleared, Toast.LENGTH_SHORT) - } catch (e: Exception) { - logError(e) - } - return@setOnPreferenceClickListener true + getPref(R.string.clear_provider_cache_key)?.setOnPreferenceClickListener { + try { + APIRepository.clearCache() + RepoLinkGenerator.cache.clear() + showToast(R.string.clear_provider_cache_cleared, Toast.LENGTH_SHORT) + } catch (e: Exception) { + logError(e) } + true } getPref(R.string.display_sub_key)?.setOnPreferenceClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 6222a24c45b..62f4c7d823d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -64,15 +64,17 @@ class UserPreferenceDelegate( private val default: T, ) { private val klass: KClass = default::class + private var cache: T? = null private val realKey get() = "${DataStoreHelper.currentAccount}/$key" operator fun getValue(self: Any?, property: KProperty<*>) = - getKeyClass(realKey, klass.java) ?: default + cache ?: getKeyClass(realKey, klass.java).also { newCache -> cache = newCache } ?: default operator fun setValue( self: Any?, property: KProperty<*>, t: T?, ) { + cache = t if (t == null) { removeKey(realKey) } else { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3fdfafdb265..8aa6dac3804 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -792,30 +792,6 @@ %d downloads queued Live - Recently Added - Top 10 - New Season - New Episodes - Leaving Soon - Must Watch - Play Trailer - Add to My List - In My List - Select Season - Trailers & More - More Like This - Cast: - Genres: - This show is: - Creator: - Writers: - About %1$s - Maturity Rating: - Download Episode %1$d - Episode %1$d - Audio Description - TOP\n10 - Close cache_time_key Cache Expiry Time diff --git a/app/src/test/java/com/lagradost/cloudstream3/ui/home/HomeCacheTest.kt b/app/src/test/java/com/lagradost/cloudstream3/ui/home/HomeCacheTest.kt deleted file mode 100644 index e4449773774..00000000000 --- a/app/src/test/java/com/lagradost/cloudstream3/ui/home/HomeCacheTest.kt +++ /dev/null @@ -1,539 +0,0 @@ -package com.lagradost.cloudstream3.ui.home - -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.lagradost.cloudstream3.AnimeSearchResponse -import com.lagradost.cloudstream3.DubStatus -import com.lagradost.cloudstream3.HomePageList -import com.lagradost.cloudstream3.HomePageResponse -import com.lagradost.cloudstream3.LiveSearchResponse -import com.lagradost.cloudstream3.MovieSearchResponse -import com.lagradost.cloudstream3.Score -import com.lagradost.cloudstream3.SearchQuality -import com.lagradost.cloudstream3.SearchResponse -import com.lagradost.cloudstream3.TorrentSearchResponse -import com.lagradost.cloudstream3.TvSeriesSearchResponse -import com.lagradost.cloudstream3.TvType -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Test - -class HomeCacheTest { - private val mapper = ObjectMapper().apply { - registerModule(KotlinModule.Builder().build()) - configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - } - - @Test - fun testMovieSearchResponseAllFields() { - @Suppress("DEPRECATION_ERROR") - val movie = MovieSearchResponse( - name = "Interstellar & Space: The Odyssey (2014) [4K HDR] \"Special\"", - url = "https://provider.com/watch/interstellar?source=main&lang=en#play", - apiName = "MegaMovieProvider", - type = TvType.Movie, - posterUrl = "https://cdn.provider.com/posters/interstellar.jpg", - year = 2014, - id = 998877, - quality = SearchQuality.FourK, - score = Score.from10(8.9), - posterHeaders = mapOf( - "User-Agent" to "Cloudstream/4.0", - "Referer" to "https://provider.com/", - "Authorization" to "Bearer test_token_123" - ) - ) - - val cached = HomeCache.CachedSearchResponse.fromSearchResponse(movie) - assertEquals(movie.name, cached.name) - assertEquals(movie.url, cached.url) - assertEquals("MegaMovieProvider", cached.apiName) - assertEquals(TvType.Movie, cached.type) - assertEquals(movie.posterUrl, cached.posterUrl) - assertEquals(2014, cached.year) - assertEquals(998877, cached.id) - assertEquals(SearchQuality.FourK, cached.quality) - assertEquals(8.9, cached.scoreDouble ?: 0.0, 0.05) - assertEquals("https://provider.com/", cached.posterHeaders?.get("Referer")) - - val restored = cached.toSearchResponse() - assertTrue(restored is MovieSearchResponse) - val restoredMovie = restored as MovieSearchResponse - assertEquals(movie.name, restoredMovie.name) - assertEquals(movie.url, restoredMovie.url) - assertEquals(2014, restoredMovie.year) - assertEquals(SearchQuality.FourK, restoredMovie.quality) - assertEquals(8.9, restoredMovie.score?.toDouble(10) ?: 0.0, 0.05) - assertEquals("Bearer test_token_123", restoredMovie.posterHeaders?.get("Authorization")) - } - - @Test - fun testTvSeriesAndAsianDramaTypes() { - @Suppress("DEPRECATION_ERROR") - val tvSeries = TvSeriesSearchResponse( - name = "Breaking Bad", - url = "https://provider.com/series/breaking-bad", - apiName = "TvProvider", - type = TvType.TvSeries, - posterUrl = "https://provider.com/bb.jpg", - year = 2008, - episodes = 62, - id = 101, - quality = SearchQuality.HD, - score = Score.from10(9.5) - ) - - val cachedTv = HomeCache.CachedSearchResponse.fromSearchResponse(tvSeries) - val restoredTv = cachedTv.toSearchResponse() - assertTrue(restoredTv is TvSeriesSearchResponse) - assertEquals(62, (restoredTv as TvSeriesSearchResponse).episodes) - - @Suppress("DEPRECATION_ERROR") - val drama = TvSeriesSearchResponse( - name = "Crash Landing on You", - url = "https://dramaprovider.com/cloy", - apiName = "DramaProvider", - type = TvType.AsianDrama, - posterUrl = "https://dramaprovider.com/cloy.jpg", - year = 2019, - episodes = 16, - score = Score.from10(9.1) - ) - - val cachedDrama = HomeCache.CachedSearchResponse.fromSearchResponse(drama) - assertEquals(TvType.AsianDrama, cachedDrama.type) - val restoredDrama = cachedDrama.toSearchResponse() - assertTrue(restoredDrama is TvSeriesSearchResponse) - assertEquals(16, (restoredDrama as TvSeriesSearchResponse).episodes) - assertEquals(TvType.AsianDrama, restoredDrama.type) - } - - @Test - fun testAnimeWithAllDubStatusVariations() { - @Suppress("DEPRECATION_ERROR") - val subAndDubAnime = AnimeSearchResponse( - name = "Demon Slayer", - url = "https://anime.com/demon-slayer", - apiName = "AnimeProvider", - type = TvType.Anime, - posterUrl = "https://anime.com/ds.jpg", - year = 2019, - dubStatus = mutableSetOf(DubStatus.Subbed, DubStatus.Dubbed), - otherName = "Kimetsu no Yaiba", - episodes = mutableMapOf(DubStatus.Subbed to 26, DubStatus.Dubbed to 26), - id = 555, - quality = SearchQuality.HD, - score = Score.from10(8.7) - ) - - val cached = HomeCache.CachedSearchResponse.fromSearchResponse(subAndDubAnime) - val restored = cached.toSearchResponse() as AnimeSearchResponse - assertEquals("Kimetsu no Yaiba", restored.otherName) - assertEquals(2, restored.dubStatus?.size) - assertTrue(restored.dubStatus?.contains(DubStatus.Subbed) == true) - assertTrue(restored.dubStatus?.contains(DubStatus.Dubbed) == true) - assertEquals(26, restored.episodes[DubStatus.Subbed]) - assertEquals(26, restored.episodes[DubStatus.Dubbed]) - - @Suppress("DEPRECATION_ERROR") - val ovaAnime = AnimeSearchResponse( - name = "Fate/Stay Night OVA", - url = "https://anime.com/fate-ova", - apiName = "AnimeProvider", - type = TvType.OVA, - posterUrl = "https://anime.com/fate.jpg", - dubStatus = mutableSetOf(DubStatus.Subbed), - episodes = mutableMapOf(DubStatus.Subbed to 1) - ) - - val cachedOva = HomeCache.CachedSearchResponse.fromSearchResponse(ovaAnime) - val restoredOva = cachedOva.toSearchResponse() as AnimeSearchResponse - assertEquals(TvType.OVA, restoredOva.type) - assertEquals(1, restoredOva.episodes[DubStatus.Subbed]) - - @Suppress("DEPRECATION_ERROR") - val movieAnime = AnimeSearchResponse( - name = "Your Name", - url = "https://anime.com/your-name", - apiName = "AnimeProvider", - type = TvType.AnimeMovie, - posterUrl = "https://anime.com/your-name.jpg", - year = 2016 - ) - val cachedAnimeMovie = HomeCache.CachedSearchResponse.fromSearchResponse(movieAnime) - val restoredAnimeMovie = cachedAnimeMovie.toSearchResponse() as AnimeSearchResponse - assertEquals(TvType.AnimeMovie, restoredAnimeMovie.type) - assertEquals(2016, restoredAnimeMovie.year) - } - - @Test - fun testLiveStreamWithAndWithoutLanguage() { - @Suppress("DEPRECATION_ERROR") - val liveWithLang = LiveSearchResponse( - name = "NHK World Japan", - url = "https://live.tv/nhk", - apiName = "IptvProvider", - type = TvType.Live, - posterUrl = "https://live.tv/nhk.png", - lang = "ja", - id = 1 - ) - val cachedWithLang = HomeCache.CachedSearchResponse.fromSearchResponse(liveWithLang) - val restoredWithLang = cachedWithLang.toSearchResponse() as LiveSearchResponse - assertEquals(TvType.Live, restoredWithLang.type) - assertEquals("ja", restoredWithLang.lang) - - @Suppress("DEPRECATION_ERROR") - val liveNoLang = LiveSearchResponse( - name = "Global Sports HD", - url = "https://live.tv/sports", - apiName = "IptvProvider", - type = TvType.Live, - posterUrl = null, - lang = null - ) - val cachedNoLang = HomeCache.CachedSearchResponse.fromSearchResponse(liveNoLang) - val restoredNoLang = cachedNoLang.toSearchResponse() as LiveSearchResponse - assertEquals(TvType.Live, restoredNoLang.type) - assertNull(restoredNoLang.lang) - assertNull(restoredNoLang.posterUrl) - } - - @Test - fun testTorrentAndQualityEnums() { - val qualities = listOf( - SearchQuality.FourK, - SearchQuality.UHD, - SearchQuality.BlueRay, - SearchQuality.HD, - SearchQuality.HQ, - SearchQuality.SD, - SearchQuality.Cam, - SearchQuality.CamRip, - SearchQuality.Telecine, - SearchQuality.Telesync, - SearchQuality.DVD, - SearchQuality.WorkPrint, - SearchQuality.WebRip, - SearchQuality.HDR, - SearchQuality.SDR - ) - - for (q in qualities) { - @Suppress("DEPRECATION_ERROR") - val torrent = TorrentSearchResponse( - name = "Sample Torrent $q", - url = "magnet:?xt=urn:btih:sample_$q", - apiName = "TorrentProvider", - type = TvType.Torrent, - posterUrl = "https://torrent.org/poster.jpg", - quality = q - ) - val cached = HomeCache.CachedSearchResponse.fromSearchResponse(torrent) - assertEquals(q, cached.quality) - val restored = cached.toSearchResponse() as TorrentSearchResponse - assertEquals(q, restored.quality) - } - } - - @Test - fun testRatingScoreBoundaryValuesAndFormats() { - val testScores = listOf( - 0.0, - 1.5, - 5.0, - 7.65, - 9.99, - 10.0 - ) - - for (scoreVal in testScores) { - @Suppress("DEPRECATION_ERROR") - val item = MovieSearchResponse( - name = "Score Test $scoreVal", - url = "https://test.com/$scoreVal", - apiName = "RatingProvider", - type = TvType.Movie, - score = Score.from10(scoreVal) - ) - val cached = HomeCache.CachedSearchResponse.fromSearchResponse(item) - assertEquals(scoreVal, cached.scoreDouble ?: -1.0, 0.05) - - val restored = cached.toSearchResponse() - val restoredScore = restored.score?.toDouble(10) ?: -1.0 - assertEquals(scoreVal, restoredScore, 0.05) - } - - // Test with null score (providers that don't supply score) - @Suppress("DEPRECATION_ERROR") - val noScoreItem = MovieSearchResponse( - name = "No Score Movie", - url = "https://test.com/no-score", - apiName = "RatingProvider", - type = TvType.Movie, - score = null - ) - val cachedNoScore = HomeCache.CachedSearchResponse.fromSearchResponse(noScoreItem) - assertNull(cachedNoScore.scoreDouble) - val restoredNoScore = cachedNoScore.toSearchResponse() - assertNull(restoredNoScore.score) - } - - @Test - fun testOtherTvTypesAndNullTypeFallback() { - val otherTypes = listOf( - TvType.Cartoon, - TvType.Documentary, - TvType.Music, - TvType.AudioBook, - TvType.Podcast, - TvType.CustomMedia, - TvType.Others, - TvType.NSFW - ) - - for (t in otherTypes) { - @Suppress("DEPRECATION_ERROR") - val item = MovieSearchResponse( - name = "Test $t", - url = "https://test.com/$t", - apiName = "OtherProvider", - type = t - ) - val cached = HomeCache.CachedSearchResponse.fromSearchResponse(item) - assertEquals(t, cached.type) - val restored = cached.toSearchResponse() - assertEquals(t, restored.type) - } - - // When TvType is null, defaults safely to MovieSearchResponse - val nullTypeCached = HomeCache.CachedSearchResponse( - name = "Null Type Item", - url = "https://test.com/null-type", - apiName = "DefaultProvider", - type = null - ) - val restored = nullTypeCached.toSearchResponse() - assertTrue(restored is MovieSearchResponse) - assertEquals(TvType.Movie, restored.type) - } - - @Test - fun testMultiSectionHomePageWithHorizontalAndVerticalLists() { - @Suppress("DEPRECATION_ERROR") - val bannerMovie1 = MovieSearchResponse( - name = "Hero Movie 1", - url = "https://provider.com/hero1", - apiName = "MultiProvider", - type = TvType.Movie, - posterUrl = "https://provider.com/hero1_landscape.jpg", - score = Score.from10(8.5) - ) - @Suppress("DEPRECATION_ERROR") - val bannerMovie2 = MovieSearchResponse( - name = "Hero Movie 2", - url = "https://provider.com/hero2", - apiName = "MultiProvider", - type = TvType.Movie, - posterUrl = "https://provider.com/hero2_landscape.jpg", - score = Score.from10(9.0) - ) - - @Suppress("DEPRECATION_ERROR") - val rowItem1 = TvSeriesSearchResponse( - name = "Popular Series 1", - url = "https://provider.com/series1", - apiName = "MultiProvider", - type = TvType.TvSeries, - posterUrl = "https://provider.com/series1.jpg", - episodes = 24, - year = 2023 - ) - - @Suppress("DEPRECATION_ERROR") - val bannerRow = HomePageList( - name = "Spotlight Banner", - list = listOf(bannerMovie1, bannerMovie2), - isHorizontalImages = true - ) - - @Suppress("DEPRECATION_ERROR") - val seriesRow = HomePageList( - name = "Binge-Worthy Series", - list = listOf(rowItem1), - isHorizontalImages = false - ) - - @Suppress("DEPRECATION_ERROR") - val emptyRow = HomePageList( - name = "Coming Soon", - list = emptyList(), - isHorizontalImages = false - ) - - @Suppress("DEPRECATION_ERROR") - val page1 = HomePageResponse( - items = listOf(bannerRow, seriesRow, emptyRow), - hasNext = true - ) - - @Suppress("DEPRECATION_ERROR") - val page2 = HomePageResponse( - items = emptyList(), - hasNext = false - ) - - val cachedHomeData = HomeCache.CachedHomeData( - unixTime = 1750000000L, - responses = listOf( - HomeCache.CachedHomePageResponse.fromHomePageResponse(page1), - HomeCache.CachedHomePageResponse.fromHomePageResponse(page2) - ) - ) - - // Serialize to JSON - val json = mapper.writeValueAsString(cachedHomeData) - assertNotNull(json) - - // Deserialize back - val deserialized = mapper.readValue(json, HomeCache.CachedHomeData::class.java) - assertEquals(1750000000L, deserialized.unixTime) - assertEquals(2, deserialized.responses.size) - - val restoredPage1 = deserialized.responses[0].toHomePageResponse() - assertTrue(restoredPage1.hasNext) - assertEquals(3, restoredPage1.items.size) - - // Validate banner row - val restoredBanner = restoredPage1.items[0] - assertEquals("Spotlight Banner", restoredBanner.name) - assertTrue(restoredBanner.isHorizontalImages) - assertEquals(2, restoredBanner.list.size) - assertEquals("Hero Movie 1", restoredBanner.list[0].name) - assertEquals(8.5, restoredBanner.list[0].score?.toDouble(10) ?: 0.0, 0.05) - - // Validate series row - val restoredSeries = restoredPage1.items[1] - assertEquals("Binge-Worthy Series", restoredSeries.name) - assertFalse(restoredSeries.isHorizontalImages) - assertEquals(1, restoredSeries.list.size) - val seriesItem = restoredSeries.list[0] as TvSeriesSearchResponse - assertEquals("Popular Series 1", seriesItem.name) - assertEquals(24, seriesItem.episodes) - assertEquals(2023, seriesItem.year) - - // Validate empty row - val restoredEmpty = restoredPage1.items[2] - assertEquals("Coming Soon", restoredEmpty.name) - assertTrue(restoredEmpty.list.isEmpty()) - - // Validate page 2 - val restoredPage2 = deserialized.responses[1].toHomePageResponse() - assertFalse(restoredPage2.hasNext) - assertTrue(restoredPage2.items.isEmpty()) - } - - @Test - fun testJsonFaultToleranceWithFutureAndArbitraryFields() { - val complexUnknownJson = """ - { - "unixTime": 1712345678, - "schemaVersion": "2.5.0", - "customConfig": { - "enableFeatureX": true, - "retryCount": 5 - }, - "responses": [ - { - "hasNext": true, - "extraSectionId": "section_99", - "items": [ - { - "name": "StreamPlay Top 10", - "isHorizontalImages": false, - "badge": "NEW", - "tags": ["Action", "Sci-Fi"], - "list": [ - { - "name": "StreamPlay Exclusive 1", - "url": "https://streamplay.to/movie/100", - "apiName": "StreamPlay", - "type": "Movie", - "posterUrl": "https://streamplay.to/posters/100.webp", - "posterHeaders": { - "X-Auth-Token": "secret_key", - "X-Custom-Header": "custom_val" - }, - "id": 10001, - "quality": "FourK", - "scoreDouble": 9.35, - "year": 2024, - "randomPluginMetadata": "ignored_value", - "extraNestedObject": { "key": 42 } - }, - { - "name": "StreamPlay Drama Series", - "url": "https://streamplay.to/series/200", - "apiName": "StreamPlay", - "type": "AsianDrama", - "posterUrl": "https://streamplay.to/posters/200.webp", - "tvEpisodes": 16, - "year": 2024, - "scoreDouble": 8.8 - }, - { - "name": "StreamPlay Anime", - "url": "https://streamplay.to/anime/300", - "apiName": "StreamPlay", - "type": "Anime", - "otherName": "Kimetsu 2024", - "dubStatus": ["Subbed", "Dubbed"], - "animeEpisodes": { "Subbed": 12, "Dubbed": 12 }, - "scoreDouble": 9.0 - } - ] - } - ] - } - ] - } - """.trimIndent() - - val deserialized = mapper.readValue(complexUnknownJson, HomeCache.CachedHomeData::class.java) - assertEquals(1712345678L, deserialized.unixTime) - assertEquals(1, deserialized.responses.size) - - val pageResponse = deserialized.responses[0].toHomePageResponse() - assertTrue(pageResponse.hasNext) - assertEquals(1, pageResponse.items.size) - - val items = pageResponse.items[0].list - assertEquals(3, items.size) - - // Item 1: Movie - assertTrue(items[0] is MovieSearchResponse) - val movie = items[0] as MovieSearchResponse - assertEquals("StreamPlay Exclusive 1", movie.name) - assertEquals(SearchQuality.FourK, movie.quality) - assertEquals(9.35, movie.score?.toDouble(10) ?: 0.0, 0.05) - assertEquals("secret_key", movie.posterHeaders?.get("X-Auth-Token")) - - // Item 2: AsianDrama - assertTrue(items[1] is TvSeriesSearchResponse) - val drama = items[1] as TvSeriesSearchResponse - assertEquals("StreamPlay Drama Series", drama.name) - assertEquals(16, drama.episodes) - assertEquals(TvType.AsianDrama, drama.type) - - // Item 3: Anime - assertTrue(items[2] is AnimeSearchResponse) - val anime = items[2] as AnimeSearchResponse - assertEquals("StreamPlay Anime", anime.name) - assertEquals("Kimetsu 2024", anime.otherName) - assertEquals(12, anime.episodes[DubStatus.Subbed]) - } -} From 4d7654156601282ec85bb93ee92baa2d2fc5b00d Mon Sep 17 00:00:00 2001 From: Spring Date: Thu, 27 Aug 2026 22:01:07 +0700 Subject: [PATCH 4/8] fix(cache): linter --- .../main/java/com/lagradost/cloudstream3/ui/APIRepository.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 cb6a3938039..609ade26cbc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -111,7 +111,7 @@ class APIRepository(val api: MainAPI) { val cached = cache.withLock { var found: LoadResponse? = null for (item in cache) { - if (item.hash == lookingForHash && (unixTime - item.unixTime) < cacheTtl) { + if (item.hash == lookingForHash && unixTime - item.unixTime < cacheTtl) { found = item.response break } @@ -184,7 +184,7 @@ class APIRepository(val api: MainAPI) { val cached = homeCache.withLock { var found: List? = null for (item in homeCache) { - if (item.hash == lookingForHash && (unixTime - item.unixTime) < cacheTtl) { + if (item.hash == lookingForHash && unixTime - item.unixTime < cacheTtl) { found = item.response break } From 0171382a3bf1e1ba79896506e07c189542dc4260 Mon Sep 17 00:00:00 2001 From: Spring Date: Thu, 27 Aug 2026 22:21:33 +0700 Subject: [PATCH 5/8] fix(cache): revert force reload --- .../com/lagradost/cloudstream3/ui/home/HomeViewModel.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 8bb595cde08..2b274e5f987 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -333,10 +333,8 @@ class HomeViewModel : ViewModel() { } - if (expandable.isEmpty()) { - _page.postValue(Resource.Loading()) - _preview.postValue(Resource.Loading()) - } + _page.postValue(Resource.Loading()) + _preview.postValue(Resource.Loading()) // cancel the current preview expand as that is no longer relevant addJob?.cancel() From fbe5b3495d312b2a08d4247e1a1cfcd20d9ae3c1 Mon Sep 17 00:00:00 2001 From: Spring Date: Thu, 27 Aug 2026 23:02:30 +0700 Subject: [PATCH 6/8] fix(cache): introduce disk cache on api repository --- .../cloudstream3/ui/APIRepository.kt | 31 +++++++++++++++++++ .../cloudstream3/ui/home/HomeViewModel.kt | 8 +++-- 2 files changed, 36 insertions(+), 3 deletions(-) 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 609ade26cbc..d5dacf7349e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.newSearchResponseList +import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.ExtractorLink @@ -69,6 +70,7 @@ class APIRepository(val api: MainAPI) { private val homeCache = atomicListOf() private var homeCacheIndex: Int = 0 const val HOME_CACHE_SIZE = 20 + const val HOME_CACHE_FOLDER = "home_cache" fun getTimeout(desired: Long?): Long { return (desired ?: DEFAULT_TIMEOUT).coerceIn(MIN_TIMEOUT, MAX_TIMEOUT) @@ -77,6 +79,20 @@ class APIRepository(val api: MainAPI) { fun clearCache() { cache.clear() homeCache.clear() + CloudStreamApp.removeKeys(HOME_CACHE_FOLDER) + } + + fun hasHomePageCache(apiName: String, page: Int = 1, nameIndex: Int? = null): Boolean { + if (!DataStoreHelper.isCacheEnabled) return false + val lookingForHash = Pair(apiName, Pair(page, nameIndex)) + val cacheTtl = DataStoreHelper.cacheTimeSeconds + val inRam = homeCache.withLock { + homeCache.any { it.hash == lookingForHash && unixTime - it.unixTime < cacheTtl } + } + if (inRam) return true + val diskKey = "${apiName}_${page}_${nameIndex}" + val onDisk = CloudStreamApp.getKey(HOME_CACHE_FOLDER, diskKey) + return onDisk != null && unixTime - onDisk.unixTime < cacheTtl } } @@ -179,6 +195,7 @@ class APIRepository(val api: MainAPI) { val lookingForHash = Pair(api.name, Pair(page, nameIndex)) val cacheTtl = DataStoreHelper.cacheTimeSeconds val isCacheEnabled = DataStoreHelper.isCacheEnabled + val diskKey = "${api.name}_${page}_${nameIndex}" if (isCacheEnabled) { val cached = homeCache.withLock { @@ -193,6 +210,19 @@ class APIRepository(val api: MainAPI) { } if (cached != null) return Resource.Success(cached) + + val cachedOnDisk = CloudStreamApp.getKey(HOME_CACHE_FOLDER, diskKey) + if (cachedOnDisk != null && unixTime - cachedOnDisk.unixTime < cacheTtl) { + homeCache.withLock { + if (homeCache.size > HOME_CACHE_SIZE) { + homeCache[homeCacheIndex] = cachedOnDisk + homeCacheIndex = (homeCacheIndex + 1) % HOME_CACHE_SIZE + } else { + homeCache.add(cachedOnDisk) + } + } + return Resource.Success(cachedOnDisk.response) + } } return safeApiCall { @@ -243,6 +273,7 @@ class APIRepository(val api: MainAPI) { homeCache.add(add) } } + CloudStreamApp.setKey(HOME_CACHE_FOLDER, diskKey, add) } res diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 2b274e5f987..cc4183e5deb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -226,7 +226,7 @@ class HomeViewModel : ViewModel() { private val expandable: MutableMap = mutableMapOf() private val _page = - MutableLiveData>>(Resource.Loading()) + MutableLiveData>>() val page: LiveData>> = _page val lock: MutableSet = mutableSetOf() @@ -333,8 +333,10 @@ class HomeViewModel : ViewModel() { } - _page.postValue(Resource.Loading()) - _preview.postValue(Resource.Loading()) + if (!APIRepository.hasHomePageCache(api.name, 1, null)) { + _page.postValue(Resource.Loading()) + _preview.postValue(Resource.Loading()) + } // cancel the current preview expand as that is no longer relevant addJob?.cancel() From 2fe33cf21083f8af2c7fc221ad72ee7e6eeaaa60 Mon Sep 17 00:00:00 2001 From: Spring Date: Fri, 28 Aug 2026 00:05:20 +0700 Subject: [PATCH 7/8] fix(cache): add serialization --- .../cloudstream3/ui/APIRepository.kt | 2 ++ .../com/lagradost/cloudstream3/MainAPI.kt | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) 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 d5dacf7349e..424c0b70ba9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.withTimeout +import kotlinx.serialization.Serializable class APIRepository(val api: MainAPI) { companion object { @@ -57,6 +58,7 @@ class APIRepository(val api: MainAPI) { val hash: Pair ) + @Serializable data class SavedHomePageResponse( val unixTime: Long, val response: List, diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index ca024b463a1..3068008eebf 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -39,9 +39,15 @@ import kotlinx.datetime.format.byUnicodePattern import kotlinx.datetime.format.char import kotlinx.datetime.format.parse import kotlinx.datetime.toInstant +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import kotlin.io.encoding.Base64 import kotlin.jvm.JvmName import kotlin.math.absoluteValue @@ -902,6 +908,7 @@ enum class ShowStatus { Ongoing, } +@Serializable enum class DubStatus(val id: Int) { None(-1), Dubbed(1), @@ -1116,6 +1123,7 @@ class Score private constructor( } } +@Serializable @Suppress("UNUSED_PARAMETER") enum class TvType(value: Int?) { Movie(1), @@ -1267,6 +1275,7 @@ suspend fun newAudioFile( * @property items List of [HomePageList] items. * @property hasNext if there is a next page or not. * */ +@Serializable data class HomePageResponse @Deprecated("Use newHomePageResponse method", level = DeprecationLevel.ERROR) constructor( @@ -1279,6 +1288,7 @@ constructor( * @property list list of [SearchResponse] items that will be added to the category. * @property isHorizontalImages here you can control how the items' cards will be appeared on the UI (Horizontal or Vertical) cards. * */ +@Serializable data class HomePageList( val name: String, var list: List, @@ -1289,6 +1299,7 @@ data class HomePageList( * @property items list of [SearchResponse] items that will be added to the search row. * @property hasNext if there is a next page or not. * */ +@Serializable data class SearchResponseList @Deprecated("Use newSearchResponseList method", level = DeprecationLevel.ERROR) constructor( @@ -1299,6 +1310,7 @@ constructor( /** enum class holds search quality. * * [Movie release types](https://en.wikipedia.org/wiki/Pirated_movie_release_types)**/ +@Serializable @Suppress("UNUSED_PARAMETER") enum class SearchQuality(value: Int?) { Cam(1), @@ -1402,7 +1414,22 @@ fun MainAPI.updateUrl(url: String): String { } } +object SearchResponseSerializer : JsonContentPolymorphicSerializer(SearchResponse::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val json = element.jsonObject + val type = json["type"]?.jsonPrimitive?.contentOrNull + return when { + type == "Torrent" -> TorrentSearchResponse.serializer() + type == "Live" || json.containsKey("lang") -> LiveSearchResponse.serializer() + json.containsKey("dubStatus") || type == "Anime" || type == "AnimeMovie" || type == "OVA" -> AnimeSearchResponse.serializer() + json.containsKey("episodes") || type == "TvSeries" || type == "Cartoon" || type == "Documentary" || type == "AsianDrama" -> TvSeriesSearchResponse.serializer() + else -> MovieSearchResponse.serializer() + } + } +} + /** Abstract interface of SearchResponse. */ +@Serializable(with = SearchResponseSerializer::class) interface SearchResponse { val name: String val url: String @@ -1552,6 +1579,7 @@ data class ActorData( /** Data class of [SearchResponse] interface for Anime. * @see newAnimeSearchResponse * */ +@Serializable data class AnimeSearchResponse @Deprecated("Use newAnimeSearchResponse", level = DeprecationLevel.ERROR) constructor( @@ -1618,6 +1646,7 @@ fun AnimeSearchResponse.addDubStatus(status: String, episodes: Int? = null) { /** Data class of [SearchResponse] interface for Torrent. * @see newTorrentSearchResponse * */ +@Serializable data class TorrentSearchResponse @Deprecated("Use newTorrentSearchResponse", level = DeprecationLevel.ERROR) constructor( @@ -1652,6 +1681,7 @@ constructor( /** Data class of [SearchResponse] interface for Movies. * @see newMovieSearchResponse * */ +@Serializable data class MovieSearchResponse @Deprecated("Use newMovieSearchResponse", level = DeprecationLevel.ERROR) constructor( @@ -1688,6 +1718,7 @@ constructor( /** Data class of [SearchResponse] interface for Live streams. * @see newLiveSearchResponse * */ +@Serializable data class LiveSearchResponse @Deprecated("Use newLiveSearchResponse", level = DeprecationLevel.ERROR) constructor( @@ -1724,6 +1755,7 @@ constructor( /** Data class of [SearchResponse] interface for Tv series. * @see newTvSeriesSearchResponse * */ +@Serializable data class TvSeriesSearchResponse @Deprecated("Use newTvSeriesSearchResponse", level = DeprecationLevel.ERROR) constructor( From 055600333e33d2501d5f02b61ef3a0a0dda06198 Mon Sep 17 00:00:00 2001 From: Spring Date: Fri, 28 Aug 2026 00:39:03 +0700 Subject: [PATCH 8/8] fix(cache): handle cache mechanism during switch provider & handle home banner --- .../cloudstream3/ui/APIRepository.kt | 38 ++++++++++--------- .../cloudstream3/ui/home/HomeFragment.kt | 2 +- .../ui/home/HomeParentItemAdapterPreview.kt | 37 +++++++++++++++++- .../cloudstream3/ui/home/HomeViewModel.kt | 36 +++++++++--------- 4 files changed, 76 insertions(+), 37 deletions(-) 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 424c0b70ba9..f589e408f6e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.HomePageResponse import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainAPI -import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainPageRequest import com.lagradost.cloudstream3.SearchResponseList import com.lagradost.cloudstream3.SubtitleFile @@ -78,10 +77,21 @@ class APIRepository(val api: MainAPI) { return (desired ?: DEFAULT_TIMEOUT).coerceIn(MIN_TIMEOUT, MAX_TIMEOUT) } - fun clearCache() { - cache.clear() - homeCache.clear() - CloudStreamApp.removeKeys(HOME_CACHE_FOLDER) + fun clearCache(apiName: String? = null) { + if (apiName == null) { + cache.clear() + homeCache.clear() + CloudStreamApp.removeKeys(HOME_CACHE_FOLDER) + } else { + homeCache.withLock { + homeCache.removeAll { it.hash.first == apiName } + } + CloudStreamApp.getKeys(HOME_CACHE_FOLDER)?.forEach { key -> + if (key.startsWith("${apiName}_")) { + CloudStreamApp.removeKey(HOME_CACHE_FOLDER, key) + } + } + } } fun hasHomePageCache(apiName: String, page: Int = 1, nameIndex: Int? = null): Boolean { @@ -98,16 +108,6 @@ class APIRepository(val api: MainAPI) { } } - private fun afterPluginsLoaded(forceReload: Boolean) { - if (forceReload) { - clearCache() - } - } - - init { - afterPluginsLoadedEvent += ::afterPluginsLoaded - } - val hasMainPage = api.hasMainPage val providerType = api.providerType val name = api.name @@ -193,13 +193,13 @@ class APIRepository(val api: MainAPI) { delay(delta) } - suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource> { + suspend fun getMainPage(page: Int, nameIndex: Int? = null, forceReload: Boolean = false): Resource> { val lookingForHash = Pair(api.name, Pair(page, nameIndex)) val cacheTtl = DataStoreHelper.cacheTimeSeconds val isCacheEnabled = DataStoreHelper.isCacheEnabled val diskKey = "${api.name}_${page}_${nameIndex}" - if (isCacheEnabled) { + if (isCacheEnabled && !forceReload) { val cached = homeCache.withLock { var found: List? = null for (item in homeCache) { @@ -211,7 +211,9 @@ class APIRepository(val api: MainAPI) { found } - if (cached != null) return Resource.Success(cached) + if (cached != null) { + return Resource.Success(cached) + } val cachedOnDisk = CloudStreamApp.getKey(HOME_CACHE_FOLDER, diskKey) if (cachedOnDisk != null && unixTime - cachedOnDisk.unixTime < cacheTtl) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index d0b4b0f209a..61ada27715b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -618,7 +618,7 @@ class HomeFragment : BaseFragment( private val apiChangeClickListener = View.OnClickListener { view -> view.context.selectHomepage(currentApiName) { api -> - homeViewModel.loadAndCancel(api, forceReload = true, fromUI = true) + homeViewModel.loadAndCancel(api, forceReload = false, fromUI = true) } /*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index be48d4794dd..18eab28708b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -402,6 +402,7 @@ class HomeParentItemAdapterPreview( homePreviewTags.isGone = item.tags.isNullOrEmpty() + homePreviewInfoBtt.isClickable = true homePreviewInfoBtt.setOnClickListener { view -> viewModel.click( LoadClickCallback(0, view, position, item) @@ -585,7 +586,7 @@ class HomeParentItemAdapterPreview( (binding as? FragmentHomeHeadTvBinding)?.apply { /*homePreviewChangeApi.setOnClickListener { view -> view.context.selectHomepage(viewModel.repo?.name) { api -> - viewModel.loadAndCancel(api, forceReload = true, fromUI = true) + viewModel.loadAndCancel(api, forceReload = false, fromUI = true) } } homePreviewReloadProvider.setOnClickListener { @@ -652,6 +653,35 @@ class HomeParentItemAdapterPreview( } } + private fun resetPreviewDetails() { + (binding as? FragmentHomeHeadBinding)?.apply { + homePreviewTitleHolder.isVisible = false + homePreviewPlay.setOnClickListener(null) + homePreviewInfo.setOnClickListener(null) + homePreviewBookmark.setOnClickListener(null) + } + (binding as? FragmentHomeHeadTvBinding)?.apply { + homePreviewInfoBtt.isVisible = true + homePreviewInfoBtt.isClickable = false + homePreviewInfoBtt.setOnClickListener(null) + homePreviewText.text = "" + homePreviewDescription.text = "" + homePreviewDescription.isGone = true + homePreviewScore.text = "" + homePreviewScore.isGone = true + homePreviewYear.text = "" + homePreviewYear.isGone = true + homePreviewDuration.text = "" + homePreviewDuration.isGone = true + homePreviewCast.text = "" + homePreviewCast.isVisible = false + homePreviewTags.removeAllViews() + homePreviewTags.isGone = true + homeBackgroundPosterWatermarkBadgeHolder.setImageDrawable(null) + homeBackgroundPosterWatermarkBadgeHolder.isVisible = false + } + } + private fun updatePreview(preview: Resource>>) { if (preview is Resource.Success || preview is Resource.Loading) { homeNonePadding.apply { @@ -686,6 +716,9 @@ class HomeParentItemAdapterPreview( (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewInfoBtt.isVisible = true } + (binding as? FragmentHomeHeadBinding)?.apply { + homePreviewTitleHolder.isVisible = true + } // Explicitly bind the current item to ensure instant loading val currentPos = previewViewpager.currentItem val item = preview.value.second.getOrNull(currentPos) @@ -700,6 +733,7 @@ class HomeParentItemAdapterPreview( previewViewpager.isInvisible = true previewViewpagerText.isVisible = true alternativeAccountPadding?.isVisible = false + resetPreviewDetails() } else -> { @@ -712,6 +746,7 @@ class HomeParentItemAdapterPreview( homePreviewInfoBtt.isVisible = false } //previewHeader.isVisible = false + resetPreviewDetails() } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index cc4183e5deb..84671742c0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -211,11 +211,11 @@ class HomeViewModel : ViewModel() { private var onGoingLoad: Job? = null private var isCurrentlyLoadingName: String? = null - private fun loadAndCancel(api: MainAPI) { + private fun loadAndCancel(api: MainAPI, forceReload: Boolean = false) { //println("loaded ${api.name}") onGoingLoad?.cancel() isCurrentlyLoadingName = api.name - onGoingLoad = load(api) + onGoingLoad = load(api, forceReload) } data class ExpandableHomepageList( @@ -316,7 +316,7 @@ class HomeViewModel : ViewModel() { } } - private fun load(api: MainAPI): Job = ioSafe { + private fun load(api: MainAPI, forceReload: Boolean = false): Job = ioSafe { repo = //if (api != null) { APIRepository(api) //} else { @@ -325,6 +325,11 @@ class HomeViewModel : ViewModel() { _apiName.postValue(repo?.name) _randomItems.postValue(listOf()) + _preview.postValue(Resource.Loading()) + previewResponses.clear() + previewResponsesAdded.clear() + currentShuffledList = emptyList() + addJob?.cancel() if (repo?.hasMainPage != true) { _page.postValue(Resource.Success(emptyMap())) @@ -333,14 +338,11 @@ class HomeViewModel : ViewModel() { } - if (!APIRepository.hasHomePageCache(api.name, 1, null)) { + if (forceReload || !APIRepository.hasHomePageCache(api.name, 1, null)) { _page.postValue(Resource.Loading()) - _preview.postValue(Resource.Loading()) } - // cancel the current preview expand as that is no longer relevant - addJob?.cancel() - when (val data = repo?.getMainPage(1, null)) { + when (val data = repo?.getMainPage(1, null, forceReload)) { is Resource.Success -> { try { expandable.clear() @@ -443,7 +445,7 @@ class HomeViewModel : ViewModel() { } private fun afterPluginsLoaded(forceReload: Boolean) { - loadAndCancel(DataStoreHelper.currentHomePage, forceReload) + loadAndCancel(DataStoreHelper.currentHomePage, false) } private fun afterMainPluginsLoaded(unused: Boolean = false) { @@ -507,7 +509,7 @@ class HomeViewModel : ViewModel() { // only save the key if it is from UI, as we don't want internal functions changing the setting fun loadAndCancel( preferredApiName: String?, - forceReload: Boolean = true, + forceReload: Boolean = false, fromUI: Boolean = false ) = ioSafe { @@ -523,31 +525,31 @@ class HomeViewModel : ViewModel() { return@ioSafe } - if (forceReload) { - APIRepository.clearCache() + if (forceReload && preferredApiName != null) { + APIRepository.clearCache(preferredApiName) } val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random if (fromUI) DataStoreHelper.currentHomePage = noneApi.name - loadAndCancel(noneApi) + loadAndCancel(noneApi, forceReload) } else if (preferredApiName == randomApi.name) { // randomize the api, if none exist like if not loaded or not installed // then use nothing val validAPIs = context?.filterProviderByPreferredMedia() if (validAPIs.isNullOrEmpty()) { - loadAndCancel(noneApi) + loadAndCancel(noneApi, forceReload) } else { val apiRandom = validAPIs.random() - loadAndCancel(apiRandom) + loadAndCancel(apiRandom, forceReload) if (fromUI) DataStoreHelper.currentHomePage = apiRandom.name } } else if (api == null) { // API is not found aka not loaded or removed, post the loading // progress if waiting for plugins, otherwise nothing if (PluginManager.loadedOnlinePlugins || PluginManager.isSafeMode()) { - loadAndCancel(noneApi) + loadAndCancel(noneApi, forceReload) } else { _page.postValue(Resource.Loading()) if (preferredApiName != null) @@ -556,7 +558,7 @@ class HomeViewModel : ViewModel() { } else { // if the api is found, then set it to it and save key if (fromUI) DataStoreHelper.currentHomePage = api.name - loadAndCancel(api) + loadAndCancel(api, forceReload) } reloadAccount() }