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?>> {
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
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