package com.lagradost.cloudstream3.ui.home import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext import java.util.* import kotlin.collections.set class HomeViewModel : ViewModel() { private var repo: APIRepository? = null private val _apiName = MutableLiveData() val apiName: LiveData = _apiName private val _randomItems = MutableLiveData?>(null) val randomItems: LiveData?> = _randomItems private fun autoloadRepo(): APIRepository { return APIRepository(apis.first { it.hasMainPage }) } private val _availableWatchStatusTypes = MutableLiveData, EnumSet>>() val availableWatchStatusTypes: LiveData, EnumSet>> = _availableWatchStatusTypes private val _bookmarks = MutableLiveData>>() val bookmarks: LiveData>> = _bookmarks private val _resumeWatching = MutableLiveData>() private val _preview = MutableLiveData>() val resumeWatching: LiveData> = _resumeWatching val preview: LiveData> = _preview fun loadResumeWatching() = viewModelScope.launchSafe { val resumeWatching = withContext(Dispatchers.IO) { getAllResumeStateIds()?.mapNotNull { id -> getLastWatched(id) }?.sortedBy { -it.updateTime } } // val resumeWatchingResult = ArrayList() val resumeWatchingResult = withContext(Dispatchers.IO) { resumeWatching?.map { resume -> val data = getKey( DOWNLOAD_HEADER_CACHE, resume.parentId.toString() ) ?: return@map null val watchPos = getViewPos(resume.episodeId) DataStoreHelper.ResumeWatchingResult( data.name, data.url, data.apiName, data.type, data.poster, watchPos, resume.episodeId, resume.parentId, resume.episode, resume.season, resume.isFromDownload ) }?.filterNotNull() } resumeWatchingResult?.let { _resumeWatching.postValue(it) } } fun loadStoredData(preferredWatchStatus: EnumSet?) = viewModelScope.launchSafe { val watchStatusIds = withContext(Dispatchers.IO) { getAllWatchStateIds()?.map { id -> Pair(id, getResultWatchState(id)) } }?.distinctBy { it.first } ?: return@launchSafe val length = WatchType.values().size val currentWatchTypes = EnumSet.noneOf(WatchType::class.java) for (watch in watchStatusIds) { currentWatchTypes.add(watch.second) if (currentWatchTypes.size >= length) { break } } currentWatchTypes.remove(WatchType.NONE) if (currentWatchTypes.size <= 0) { _bookmarks.postValue(Pair(false, ArrayList())) return@launchSafe } val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) //if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first()) _availableWatchStatusTypes.postValue( Pair( watchPrefNotNull, currentWatchTypes, ) ) val list = withContext(Dispatchers.IO) { watchStatusIds.filter { watchPrefNotNull.contains(it.second) } .mapNotNull { getBookmarkedData(it.first) } .sortedBy { -it.latestUpdatedTime } } _bookmarks.postValue(Pair(true, list)) } private var onGoingLoad: Job? = null private fun loadAndCancel(api: MainAPI?) { onGoingLoad?.cancel() onGoingLoad = load(api) } data class ExpandableHomepageList( var list: HomePageList, var currentPage: Int, var hasNext: Boolean, ) private val expandable: MutableMap = mutableMapOf() private val _page = MutableLiveData>>(Resource.Loading()) val page: LiveData>> = _page val lock: MutableSet = mutableSetOf() suspend fun expandAndReturn(name: String): ExpandableHomepageList? { if (lock.contains(name)) return null lock += name repo?.apply { waitForHomeDelay() expandable[name]?.let { current -> debugAssert({ !current.hasNext }) { "Expand called when not needed" } val nextPage = current.currentPage + 1 val next = getMainPage(nextPage, mainPage.indexOfFirst { it.name == name }) if (next is Resource.Success) { next.value.filterNotNull().forEach { main -> main.items.forEach { newList -> val key = newList.name expandable[key]?.apply { hasNext = main.hasNext currentPage = nextPage debugWarning({ newList.list.any { outer -> this.list.list.any { it.url == outer.url } } }) { "Expanded contained an item that was previously already in the list\n${list.name} = ${this.list.list}\n${newList.name} = ${newList.list}" } this.list.list += newList.list this.list.list.distinctBy { it.url } // just to be sure we are not adding the same shit for some reason } ?: debugWarning { "Expanded an item not in main load named $key, current list is ${expandable.keys}" } } } } else { current.hasNext = false } } _page.postValue(Resource.Success(expandable)) } lock -= name return expandable[name] } // this is soo over engineered, but idk how I can make it clean without making the main api harder to use :pensive: fun expand(name: String) = viewModelScope.launchSafe { expandAndReturn(name) } private fun load(api: MainAPI?) = viewModelScope.launchSafe { repo = if (api != null) { APIRepository(api) } else { autoloadRepo() } _apiName.postValue(repo?.name) _randomItems.postValue(listOf()) if (repo?.hasMainPage == true) { _page.postValue(Resource.Loading()) _preview.postValue(Resource.Loading()) 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, 1, home.hasNext) } } val items = data.value.mapNotNull { it?.items }.flatten() items.randomOrNull()?.list?.randomOrNull()?.url?.let { url -> // backup request in case first fails var first = repo?.load(url) if(first == null ||first is Resource.Failure) { first = repo?.load(items.random().list.random().url) } first?.let { _preview.postValue(it) } ?: run { _preview.postValue( Resource.Failure( false, null, null, "No repo found, this should never happen" ) ) } } ?: run { _preview.postValue( Resource.Failure( false, null, null, "No homepage items" ) ) } _page.postValue(Resource.Success(expandable)) //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() _randomItems.postValue(randomItems) } } } catch (e: Exception) { _randomItems.postValue(emptyList()) logError(e) } } is Resource.Failure -> { _page.postValue(data!!) } else -> Unit } } else { _page.postValue(Resource.Success(emptyMap())) _preview.postValue(Resource.Failure(false, null, null, "No homepage")) } } fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) = viewModelScope.launchSafe { // Since plugins are loaded in stages this function can get called multiple times. // The issue with this is that the homepage may be fetched multiple times while the first request is loading val api = getApiFromNameNull(preferredApiName) if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) { return@launchSafe } if (preferredApiName == noneApi.name) { setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { val validAPIs = context?.filterProviderByPreferredMedia() if (validAPIs.isNullOrEmpty()) { // Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded loadAndCancel(noneApi) } else { val apiRandom = validAPIs.random() loadAndCancel(apiRandom) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) } // If the plugin isn't loaded yet. (Does not set the key) } else if (api == null) { loadAndCancel(noneApi) } else { setKey(USER_SELECTED_HOMEPAGE_API, api.name) loadAndCancel(api) } } }