AquaStream/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt

498 lines
20 KiB
Kotlin

package com.lagradost.cloudstream3.ui.home
import android.os.Build
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.CommonActivity.activity
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.debugWarning
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PluginManager
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.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
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.EnumSet
import kotlin.collections.set
class HomeViewModel : ViewModel() {
companion object {
suspend fun getResumeWatching(): List<DataStoreHelper.ResumeWatchingResult>? {
val resumeWatching = withContext(Dispatchers.IO) {
getAllResumeStateIds()?.mapNotNull { id ->
getLastWatched(id)
}?.sortedBy { -it.updateTime }
}
val resumeWatchingResult = withContext(Dispatchers.IO) {
resumeWatching?.mapNotNull { resume ->
val data = getKey<VideoDownloadHelper.DownloadHeaderCached>(
DOWNLOAD_HEADER_CACHE,
resume.parentId.toString()
) ?: return@mapNotNull 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
)
}
}
return resumeWatchingResult
}
}
var repo: APIRepository? = null
private val _apiName = MutableLiveData<String>()
val apiName: LiveData<String> = _apiName
private val _randomItems = MutableLiveData<List<SearchResponse>?>(null)
val randomItems: LiveData<List<SearchResponse>?> = _randomItems
private var currentShuffledList: List<SearchResponse> = listOf()
private fun autoloadRepo(): APIRepository {
return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }})
}
private val _availableWatchStatusTypes =
MutableLiveData<Pair<Set<WatchType>, Set<WatchType>>>()
val availableWatchStatusTypes: LiveData<Pair<Set<WatchType>, Set<WatchType>>> =
_availableWatchStatusTypes
private val _bookmarks = MutableLiveData<Pair<Boolean, List<SearchResponse>>>()
val bookmarks: LiveData<Pair<Boolean, List<SearchResponse>>> = _bookmarks
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
private val previewResponses = mutableListOf<LoadResponse>()
private val previewResponsesAdded = mutableSetOf<String>()
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
private fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatchingResult = getResumeWatching()
if (isTrueTvSettings() && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ioSafe {
// this WILL crash on non tvs, so keep this inside a try catch
activity?.addProgramsToContinueWatching(resumeWatchingResult)
}
}
resumeWatchingResult?.let {
_resumeWatching.postValue(it)
}
}
fun loadStoredData(preferredWatchStatus: Set<WatchType>?) = 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 = mutableSetOf<WatchType>()
for (watch in watchStatusIds) {
currentWatchTypes.add(watch.second)
if (currentWatchTypes.size >= length) {
break
}
}
currentWatchTypes.remove(WatchType.NONE)
if (currentWatchTypes.size <= 0) {
setKey(
HOME_BOOKMARK_VALUE_LIST,
intArrayOf()
)
_availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf())
_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())
setKey(
HOME_BOOKMARK_VALUE_LIST,
watchPrefNotNull.map { it.internalId }.toIntArray()
)
_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 var isCurrentlyLoadingName : String? = null
private fun loadAndCancel(api: MainAPI) {
onGoingLoad?.cancel()
isCurrentlyLoadingName = api.name
onGoingLoad = load(api)
}
data class ExpandableHomepageList(
var list: HomePageList,
var currentPage: Int,
var hasNext: Boolean,
)
private val expandable: MutableMap<String, ExpandableHomepageList> = mutableMapOf()
private val _page =
MutableLiveData<Resource<Map<String, ExpandableHomepageList>>>(Resource.Loading())
val page: LiveData<Resource<Map<String, ExpandableHomepageList>>> = _page
val lock: MutableSet<String> = 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)
}
// returns the amount of items added and modifies current
private suspend fun updatePreviewResponses(
current: MutableList<LoadResponse>,
alreadyAdded: MutableSet<String>,
shuffledList: List<SearchResponse>,
size: Int
): Int {
var count = 0
val addItems = arrayListOf<SearchResponse>()
for (searchResponse in shuffledList) {
if (!alreadyAdded.contains(searchResponse.url)) {
addItems.add(searchResponse)
previewResponsesAdded.add(searchResponse.url)
if (++count >= size) {
break
}
}
}
val add = addItems.amap { searchResponse ->
repo?.load(searchResponse.url)
}.mapNotNull { if (it != null && it is Resource.Success) it.value else null }
current.addAll(add)
return add.size
}
private var addJob: Job? = null
fun loadMoreHomeScrollResponses() {
addJob = ioSafe {
updatePreviewResponses(previewResponses, previewResponsesAdded, currentShuffledList, 1)
_preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses))
}
}
private fun load(api: MainAPI) : Job = ioSafe {
repo = //if (api != null) {
APIRepository(api)
//} else {
// autoloadRepo()
//}
_apiName.postValue(repo?.name)
_randomItems.postValue(listOf())
if (repo?.hasMainPage != true) {
_page.postValue(Resource.Success(emptyMap()))
_preview.postValue(Resource.Failure(false, null, null, "No homepage"))
return@ioSafe
}
_page.postValue(Resource.Loading())
_preview.postValue(Resource.Loading())
// cancel the current preview expand as that is no longer relevant
addJob?.cancel()
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()
previewResponses.clear()
previewResponsesAdded.clear()
//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()
updatePreviewResponses(
previewResponses,
previewResponsesAdded,
randomItems,
3
)
_randomItems.postValue(randomItems)
currentShuffledList = randomItems
}
}
if (previewResponses.isEmpty()) {
_preview.postValue(
Resource.Failure(
false,
null,
null,
"No homepage responses"
)
)
} else {
_preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses))
}
_page.postValue(Resource.Success(expandable))
} catch (e: Exception) {
_randomItems.postValue(emptyList())
logError(e)
}
}
is Resource.Failure -> {
_page.postValue(data!!)
_preview.postValue(data!!)
}
else -> Unit
}
isCurrentlyLoadingName = null
}
fun click(callback: SearchClickCallback) {
if (callback.action == SEARCH_ACTION_FOCUSED) {
//focusCallback(callback.card)
} else {
SearchHelper.handleSearchClickCallback(callback)
}
}
private val _popup = MutableLiveData<ExpandableHomepageList?>(null)
val popup: LiveData<ExpandableHomepageList?> = _popup
fun popup(list: ExpandableHomepageList?) {
_popup.postValue(list)
}
private fun bookmarksUpdated(unused: Boolean) {
reloadStored()
}
private fun afterPluginsLoaded(forceReload: Boolean) {
loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), forceReload)
}
private fun afterMainPluginsLoaded(unused: Boolean = false) {
loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false)
}
init {
MainActivity.bookmarksUpdatedEvent += ::bookmarksUpdated
MainActivity.afterPluginsLoadedEvent += ::afterPluginsLoaded
MainActivity.mainPluginsLoadedEvent += ::afterMainPluginsLoaded
}
override fun onCleared() {
MainActivity.bookmarksUpdatedEvent -= ::bookmarksUpdated
MainActivity.afterPluginsLoadedEvent -= ::afterPluginsLoaded
MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded
super.onCleared()
}
fun queryTextSubmit(query: String) {
QuickSearchFragment.pushSearch(
query,
repo?.name?.let { arrayOf(it) })
}
fun queryTextChange(newText: String) {
// do nothing
}
fun reloadStored() {
loadResumeWatching()
val list = EnumSet.noneOf(WatchType::class.java)
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
list.addAll(it)
}
loadStoredData(list)
}
fun click(load: LoadClickCallback) {
loadResult(load.response.url, load.response.apiName, load.action)
}
// only save the key if it is from UI, as we don't want internal functions changing the setting
fun loadAndCancel(
preferredApiName: String?,
forceReload: Boolean = true,
fromUI: Boolean = false
) =
ioSafe {
// 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)
// api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true
val currentPage = page.value
// if we don't need to reload and we have a valid homepage or currently loading the same thing then return
val currentLoading = isCurrentlyLoadingName
if (!forceReload && (currentPage is Resource.Success && currentPage.value.isNotEmpty() || (currentLoading != null && currentLoading == preferredApiName))) {
return@ioSafe
}
if (preferredApiName == noneApi.name) {
// just set to random
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
loadAndCancel(noneApi)
} else if (preferredApiName == randomApi.name) {
// randomize the api, if none exist like if not loaded or not installed
// then use nothing
val validAPIs = context?.filterProviderByPreferredMedia()
if (validAPIs.isNullOrEmpty()) {
loadAndCancel(noneApi)
} else {
val apiRandom = validAPIs.random()
loadAndCancel(apiRandom)
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name)
}
} else if (api == null) {
// API is not found aka not loaded or removed, post the loading
// progress if waiting for plugins, otherwise nothing
if(PluginManager.loadedLocalPlugins) {
loadAndCancel(noneApi)
} else {
_page.postValue(Resource.Loading())
}
} else {
// if the api is found, then set it to it and save key
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name)
loadAndCancel(api)
}
}
}