-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: instant home screen loading via two-level cache and lazy hero banner #3142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
iAm-an-iA
wants to merge
8
commits into
recloudstream:master
Choose a base branch
from
iAm-an-iA:feat/home-provider-cache
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
14a23bb
feat(cache): add provider cache
sdlcspring-glitch ef98bba
feat(cache): lazy load hero banner
sdlcspring-glitch 4e15cee
fix(cache): simplified cache logic, remove unnecessary mapping, remov…
sdlcspring-glitch 4d76541
fix(cache): linter
sdlcspring-glitch 0171382
fix(cache): revert force reload
sdlcspring-glitch fbe5b34
fix(cache): introduce disk cache on api repository
sdlcspring-glitch 2fe33cf
fix(cache): add serialization
sdlcspring-glitch 0556003
fix(cache): handle cache mechanism during switch provider & handle ho…
sdlcspring-glitch File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -17,12 +16,15 @@ 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 | ||
| 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 { | ||
|
|
@@ -55,23 +57,55 @@ class APIRepository(val api: MainAPI) { | |
| val hash: Pair<String, String> | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class SavedHomePageResponse( | ||
| val unixTime: Long, | ||
| val response: List<HomePageResponse?>, | ||
| val hash: Pair<String, Pair<Int, Int?>> | ||
| ) | ||
|
|
||
| private val cache = atomicListOf<SavedLoadResponse>() | ||
| private var cacheIndex: Int = 0 | ||
| const val CACHE_SIZE = 20 | ||
|
|
||
| private val homeCache = atomicListOf<SavedHomePageResponse>() | ||
| 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) | ||
| } | ||
| } | ||
|
|
||
| private fun afterPluginsLoaded(forceReload: Boolean) { | ||
| if (forceReload) { | ||
| cache.clear() | ||
| 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) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| init { | ||
| afterPluginsLoadedEvent += ::afterPluginsLoaded | ||
| 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<SavedHomePageResponse>(HOME_CACHE_FOLDER, diskKey) | ||
| return onDisk != null && unixTime - onDisk.unixTime < cacheTtl | ||
| } | ||
| } | ||
|
|
||
| val hasMainPage = api.hasMainPage | ||
|
|
@@ -88,31 +122,37 @@ class APIRepository(val api: MainAPI) { | |
| if (isInvalidData(url)) throw ErrorLoadingException() | ||
| val fixedUrl = api.fixUrl(url) | ||
| val lookingForHash = Pair(api.name, fixedUrl) | ||
| val cacheTtl = DataStoreHelper.cacheTimeSeconds | ||
| val isCacheEnabled = DataStoreHelper.isCacheEnabled | ||
|
|
||
| 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 | ||
| 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() | ||
|
|
@@ -153,12 +193,47 @@ class APIRepository(val api: MainAPI) { | |
| delay(delta) | ||
| } | ||
|
|
||
| suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource<List<HomePageResponse?>> { | ||
| suspend fun getMainPage(page: Int, nameIndex: Int? = null, forceReload: Boolean = false): Resource<List<HomePageResponse?>> { | ||
| val lookingForHash = Pair(api.name, Pair(page, nameIndex)) | ||
| val cacheTtl = DataStoreHelper.cacheTimeSeconds | ||
| val isCacheEnabled = DataStoreHelper.isCacheEnabled | ||
| val diskKey = "${api.name}_${page}_${nameIndex}" | ||
|
|
||
| if (isCacheEnabled && !forceReload) { | ||
| val cached = homeCache.withLock { | ||
| var found: List<HomePageResponse?>? = null | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I recommend you take advantage of Kotlin functions to make your code simpler, this could be: val found = homeCache.firstOrNull { item ->
item.hash == lookingForHash && unixTime - item.unixTime < cacheTtl
}?.response |
||
| for (item in homeCache) { | ||
| if (item.hash == lookingForHash && unixTime - item.unixTime < cacheTtl) { | ||
| found = item.response | ||
| break | ||
| } | ||
| } | ||
| found | ||
| } | ||
|
|
||
| if (cached != null) { | ||
| return Resource.Success(cached) | ||
| } | ||
|
|
||
| val cachedOnDisk = CloudStreamApp.getKey<SavedHomePageResponse>(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 { | ||
| 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, | ||
|
|
@@ -191,6 +266,21 @@ 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) | ||
| } | ||
| } | ||
| CloudStreamApp.setKey(HOME_CACHE_FOLDER, diskKey, add) | ||
| } | ||
|
|
||
| res | ||
| } | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This system can be further streamlined by returning the cached response, but simultaneously fetching the home page in the background to update the cache. This lets users get an up to date cache without sacrificing load times.
However, feature is not necessary in this pull request.