package org.mosad.teapod.ui.activity.main.viewmodel import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.async import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import org.mosad.teapod.parser.crunchyroll.* import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.tmdb.* import org.mosad.teapod.util.toPlayheadsMap /** * handle media, next ep and tmdb * TODO this lives in activity, is this correct? */ class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) { var seriesCrunchy = NoneSeriesItem // movies are also series internal set var seasonsCrunchy = NoneSeasons internal set var currentSeasonCrunchy = NoneSeason internal set var episodesCrunchy = NoneEpisodes internal set val currentEpisodesCrunchy = arrayListOf() // used for EpisodeItemAdapter (easier updates) // additional media info, might change during during user interaction // use a map to update the episode adapter values val currentPlayheads: MutableMap = mutableMapOf() var isWatchlist = false internal set var upNextSeries = NoneUpNextSeriesList internal set var similarTo = NoneSimilarToResult internal set // TMDB stuff var mediaType = MediaType.OTHER internal set var tmdbResult: TMDBResult = NoneTMDB // TODO rename internal set var tmdbTVSeason: TMDBTVSeason = NoneTMDBTVSeason internal set /** * @param crunchyId the crunchyroll series id */ suspend fun loadCrunchy(crunchyId: String) { // load series and seasons info in parallel listOf( viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId).data.first() }, viewModelScope.launch { seasonsCrunchy = Crunchyroll.seasons(crunchyId) }, viewModelScope.launch { isWatchlist = Crunchyroll.isWatchlist(crunchyId) }, viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(crunchyId) }, viewModelScope.launch { similarTo = Crunchyroll.similarTo(crunchyId) } ).joinAll() // load the preferred season: // next episode > first season currentSeasonCrunchy = if (upNextSeries != NoneUpNextSeriesList) { seasonsCrunchy.data.firstOrNull{ season -> season.id == upNextSeries.data.first().panel.episodeMetadata.seasonId } ?: seasonsCrunchy.data.first() } else { seasonsCrunchy.data.first() } // Note: if we need to query metaDB, do it now // load episodes and metaDB in parallel (tmdb needs mediaType, which is set via episodes) viewModelScope.launch { episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) }.join() currentEpisodesCrunchy.clear() currentEpisodesCrunchy.addAll(episodesCrunchy.data) // set media type, for movies the episode field is empty mediaType = episodesCrunchy.data.firstOrNull()?.let { if (it.episode.isNotEmpty()) MediaType.TVSHOW else MediaType.MOVIE } ?: MediaType.OTHER // load playheads and tmdb in parallel listOf( updatePlayheadsAsync(), viewModelScope.launch { loadTmdbInfo() } // use tmdb search to get media info ).joinAll() } /** * Load the tmdb info for the selected media. * The TMDB search return a media type, use this to get the details (movie/tv show and season) */ private suspend fun loadTmdbInfo() { val tmdbApiController = TMDBApiController() val tmdbSearchResult = when(mediaType) { MediaType.MOVIE -> tmdbApiController.searchMovie(seriesCrunchy.title) MediaType.TVSHOW -> tmdbApiController.searchTVShow(seriesCrunchy.title) else -> NoneTMDBSearch } tmdbResult = if (tmdbSearchResult.results.isNotEmpty()) { when (val result = tmdbSearchResult.results.first()) { is TMDBSearchResultMovie -> tmdbApiController.getMovieDetails(result.id) is TMDBSearchResultTVShow -> tmdbApiController.getTVShowDetails(result.id) else -> NoneTMDB } } else NoneTMDB // currently not used // tmdbTVSeason = if (tmdbResult is TMDBTVShow) { // tmdbApiController.getTVSeasonDetails(tmdbResult.id, 0) // } else NoneTMDBTVSeason } /** * Get current playheads for all episodes */ private fun updatePlayheadsAsync() = viewModelScope.async { currentPlayheads.clear() currentPlayheads.putAll( Crunchyroll.playheads(episodesCrunchy.data.map { it.id }).toPlayheadsMap() ) } /** * Set currentSeasonCrunchy based on the season id. Also set the new seasons episodes. * * @param seasonId the id of the season to set */ suspend fun setCurrentSeason(seasonId: String) { // return if the id hasn't changed (performance) if (currentSeasonCrunchy.id == seasonId) return // set currentSeasonCrunchy to the new season with id == seasonId, if the id isn't found, // don't change the current season (this should/can never happen) currentSeasonCrunchy = seasonsCrunchy.data.firstOrNull { it.id == seasonId } ?: currentSeasonCrunchy episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) currentEpisodesCrunchy.clear() currentEpisodesCrunchy.addAll(episodesCrunchy.data) // update playheads playheads (including fully watched state) updatePlayheadsAsync().await() } suspend fun setWatchlist() { isWatchlist = if (isWatchlist) { Crunchyroll.deleteWatchlist(seriesCrunchy.id) false } else { Crunchyroll.postWatchlist(seriesCrunchy.id) true } } suspend fun updateOnResume() { joinAll( updatePlayheadsAsync(), viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(seriesCrunchy.id) } ) } }