Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 104 additions & 27 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,62 @@ 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)
}
}
}
}

fun getEffectiveHomepageCacheTtl(maxHomepageCacheTimeMs: Long?): Long {
val userTtl = DataStoreHelper.cacheTimeSeconds.coerceAtLeast(0L)
return maxHomepageCacheTimeMs?.let { minOf(userTtl, it / 1000L).coerceAtLeast(0L) } ?: userTtl
}
}

init {
afterPluginsLoadedEvent += ::afterPluginsLoaded
fun hasHomePageCache(
apiName: String,
maxHomepageCacheTimeMs: Long? = null,
page: Int = 1,
nameIndex: Int? = null
): Boolean {
val cacheTtl = getEffectiveHomepageCacheTtl(maxHomepageCacheTimeMs)
if (cacheTtl <= 0L) return false
val lookingForHash = Pair(apiName, Pair(page, nameIndex))
return homeCache.withLock {
homeCache.any { it.hash == lookingForHash && unixTime - it.unixTime < cacheTtl }
} || CloudStreamApp.getKey<SavedHomePageResponse>(HOME_CACHE_FOLDER, "${apiName}_${page}_${nameIndex}")
?.let { unixTime - it.unixTime < cacheTtl } == true
}
}

val hasMainPage = api.hasMainPage
Expand All @@ -88,31 +129,28 @@ 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
}
}
found
if (isCacheEnabled) {
cache.withLock {
cache.firstOrNull { item -> item.hash == lookingForHash && unixTime - item.unixTime < cacheTtl }?.response
}?.let { return@withTimeout it }
}

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 +191,36 @@ 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?>> {
Comment thread
iAm-an-iA marked this conversation as resolved.
val lookingForHash = Pair(api.name, Pair(page, nameIndex))
val cacheTtl = getEffectiveHomepageCacheTtl(api.maxHomepageCacheTime)
val isCacheEnabled = cacheTtl > 0L
val diskKey = "${api.name}_${page}_${nameIndex}"

if (isCacheEnabled && !forceReload) {
homeCache.withLock {
homeCache.firstOrNull { item -> item.hash == lookingForHash && unixTime - item.unixTime < cacheTtl }?.response
}?.let { return Resource.Success(it) }

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 +253,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 @@ -620,7 +620,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