Skip to content
140 changes: 115 additions & 25 deletions app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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?>> {

Copy link
Copy Markdown
Collaborator

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.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(

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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -401,6 +402,7 @@ class HomeParentItemAdapterPreview(
homePreviewTags.isGone =
item.tags.isNullOrEmpty()

homePreviewInfoBtt.isClickable = true
homePreviewInfoBtt.setOnClickListener { view ->
viewModel.click(
LoadClickCallback(0, view, position, item)
Expand Down Expand Up @@ -584,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 {
Expand Down Expand Up @@ -651,8 +653,37 @@ 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<Pair<Boolean, List<LoadResponse>>>) {
if (preview is Resource.Success) {
if (preview is Resource.Success || preview is Resource.Loading) {
homeNonePadding.apply {
val params = layoutParams
params.height = 0
Expand Down Expand Up @@ -685,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)
Expand All @@ -693,6 +727,15 @@ class HomeParentItemAdapterPreview(
}
}

is Resource.Loading -> {
previewAdapter.submitList(listOf())
previewViewpager.setCurrentItem(0, false)
previewViewpager.isInvisible = true
previewViewpagerText.isVisible = true
alternativeAccountPadding?.isVisible = false
resetPreviewDetails()
}

else -> {
previewAdapter.submitList(listOf())
previewViewpager.setCurrentItem(0, false)
Expand All @@ -703,6 +746,7 @@ class HomeParentItemAdapterPreview(
homePreviewInfoBtt.isVisible = false
}
//previewHeader.isVisible = false
resetPreviewDetails()
}
}
}
Expand Down
Loading