diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/details/MovieTrailersLoader.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/details/MovieTrailersLoader.kt index dc4967541e..bce51195a1 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/movies/details/MovieTrailersLoader.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/details/MovieTrailersLoader.kt @@ -4,13 +4,8 @@ package com.battlelancer.seriesguide.movies.details import android.content.Context -import com.battlelancer.seriesguide.SgApp -import com.battlelancer.seriesguide.movies.MoviesSettings.getMoviesLanguage -import com.battlelancer.seriesguide.util.Errors +import com.battlelancer.seriesguide.tmdbapi.TmdbTools2 import com.uwetrottmann.androidutils.GenericSimpleLoader -import com.uwetrottmann.tmdb2.entities.Videos -import com.uwetrottmann.tmdb2.enumerations.VideoType -import timber.log.Timber /** * Loads a YouTube movie trailer from TMDb. Tries to get a local trailer, if not falls back to @@ -20,48 +15,7 @@ class MovieTrailersLoader(context: Context, private val tmdbId: Int) : GenericSimpleLoader(context) { override fun loadInBackground(): String? { - // try to get a local trailer - val trailer = getTrailerVideoId( - getMoviesLanguage(context), "get local movie trailer" - ) - if (trailer != null) { - return trailer - } - Timber.d("Did not find a local movie trailer.") - - // fall back to default language trailer - return getTrailerVideoId(null, "get default movie trailer") - } - - private fun getTrailerVideoId(language: String?, action: String): String? { - val moviesService = SgApp.getServicesComponent(context).moviesService() - try { - val response = moviesService.videos(tmdbId, language).execute() - if (response.isSuccessful) { - return extractTrailer(response.body()) - } else { - Errors.logAndReport(action, response) - } - } catch (e: Exception) { - Errors.logAndReport(action, e) - } - return null + return TmdbTools2().getMovieTrailerYoutubeId(context, tmdbId) } - private fun extractTrailer(videos: Videos?): String? { - val results = videos?.results - if (results == null || results.size == 0) { - return null - } - - // Pick the first YouTube trailer - for (video in results) { - val videoId = video.key - if (video.type == VideoType.TRAILER && "YouTube" == video.site - && !videoId.isNullOrEmpty()) { - return videoId - } - } - return null - } } \ No newline at end of file diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/ShowFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/ShowFragment.kt index c3d4b144c9..612e411174 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/ShowFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/ShowFragment.kt @@ -102,6 +102,7 @@ class ShowFragment() : Fragment() { val buttonShortcut: Button val buttonLanguage: Button val buttonRate: View + val buttonTrailer: Button val buttonSimilar: Button val buttonImdb: Button val buttonShowMetacritic: Button @@ -140,6 +141,7 @@ class ShowFragment() : Fragment() { buttonShortcut = view.findViewById(R.id.buttonShowShortcut) buttonLanguage = view.findViewById(R.id.buttonShowLanguage) buttonRate = view.findViewById(R.id.containerRatings) + buttonTrailer = view.findViewById(R.id.buttonShowTrailer) buttonSimilar = view.findViewById(R.id.buttonShowSimilar) buttonImdb = view.findViewById(R.id.buttonShowImdb) buttonShowMetacritic = view.findViewById(R.id.buttonShowMetacritic) @@ -406,6 +408,19 @@ class ShowFragment() : Fragment() { binding.textViewRatingVotes.text = showForUi.traktVotes binding.textViewRatingUser.text = showForUi.traktUserRating + // Trailer button + binding.buttonTrailer.apply { + if (showForUi.trailerVideoId != null) { + setOnClickListener { + ServiceUtils.openYoutube(showForUi.trailerVideoId, requireContext()) + } + isEnabled = true + } else { + setOnClickListener(null) + isEnabled = false + } + } + // Similar shows button. binding.buttonSimilar.setOnClickListener { show.tmdbId?.also { diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/ShowViewModel.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/ShowViewModel.kt index 575d935952..0cbeca158b 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/ShowViewModel.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/ShowViewModel.kt @@ -20,6 +20,7 @@ import com.battlelancer.seriesguide.util.LanguageTools import com.battlelancer.seriesguide.util.TextTools import com.battlelancer.seriesguide.util.TimeTools import com.battlelancer.seriesguide.util.Utils +import com.github.michaelbull.result.onSuccess import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -45,7 +46,8 @@ class ShowViewModel(application: Application) : AndroidViewModel(application) { val genres: String, val traktRating: String, val traktVotes: String, - val traktUserRating: String + val traktUserRating: String, + val trailerVideoId: String? ) // Mediator to compute some additional data for the UI in the background. @@ -99,20 +101,38 @@ class ShowViewModel(application: Application) : AndroidViewModel(application) { val traktUserRating = TraktTools.buildUserRatingString(application, show.ratingUser) + val databaseValues = ShowForUi( + show, + timeOrNull, + baseInfo, + overviewStyled, + languageData, + country, + releaseYear, + lastUpdated, + genres, + traktRating, traktVotes, traktUserRating, + null + ) + withContext(Dispatchers.Main) { - showForUi.value = - ShowForUi( - show, - timeOrNull, - baseInfo, - overviewStyled, - languageData, - country, - releaseYear, - lastUpdated, - genres, - traktRating, traktVotes, traktUserRating - ) + showForUi.value = databaseValues + } + + // Do network request after returning data from the database + val showTmdbId = show.tmdbId + if (showTmdbId != null && languageData != null) { + TmdbTools2().getShowTrailerYoutubeId( + application, + show.tmdbId, + languageData.languageCode + ).onSuccess { + if (it != null) { + withContext(Dispatchers.Main) { + showForUi.value = databaseValues.copy(trailerVideoId = it) + } + } + } } } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogFragment.kt index 656a8b53dd..908e483f48 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogFragment.kt @@ -26,6 +26,7 @@ import com.battlelancer.seriesguide.ui.OverviewActivity import com.battlelancer.seriesguide.ui.dialogs.L10nDialogFragment import com.battlelancer.seriesguide.util.ImageTools import com.battlelancer.seriesguide.util.LanguageTools +import com.battlelancer.seriesguide.util.ServiceUtils import com.battlelancer.seriesguide.util.TextTools import com.battlelancer.seriesguide.util.TimeTools import com.battlelancer.seriesguide.util.ViewTools @@ -152,6 +153,17 @@ class AddShowDialogFragment : AppCompatDialogFragment() { this.binding?.textViewAddDescription?.isGone = false populateShowViews(show) } + model.trailer.observe(this) { videoId -> + this.binding?.buttonAddTrailer?.apply { + if (videoId != null) { + setOnClickListener { ServiceUtils.openYoutube(videoId, requireContext()) } + isEnabled = true + } else { + setOnClickListener(null) + isEnabled = false + } + } + } model.watchProvider.observe(this) { watchInfo -> this.binding?.buttonAddStreamingSearch?.let { val providerInfo = StreamingSearch.configureButton(it, watchInfo, false) diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogViewModel.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogViewModel.kt index f860b81cf6..e190266d6e 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogViewModel.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogViewModel.kt @@ -17,6 +17,7 @@ import com.battlelancer.seriesguide.SgApp import com.battlelancer.seriesguide.shows.database.SgShow2 import com.battlelancer.seriesguide.shows.tools.GetShowTools.GetShowError.GetShowDoesNotExist import com.battlelancer.seriesguide.streaming.StreamingSearch +import com.battlelancer.seriesguide.tmdbapi.TmdbTools2 import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import kotlinx.coroutines.Dispatchers @@ -35,6 +36,7 @@ class AddShowDialogViewModel( val languageCode = MutableLiveData() val showDetails: LiveData + val trailer: LiveData init { // Set original value for region. @@ -66,6 +68,12 @@ class AddShowDialogViewModel( } } } + this.trailer = languageCode.switchMap { languageCode -> + liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) { + TmdbTools2().getShowTrailerYoutubeId(application, showTmdbId, languageCode) + .onSuccess { emit(it) } + } + } } private val watchInfoMediator = MediatorLiveData().apply { diff --git a/app/src/main/java/com/battlelancer/seriesguide/tmdbapi/TmdbTools2.kt b/app/src/main/java/com/battlelancer/seriesguide/tmdbapi/TmdbTools2.kt index ef298f158e..2f41f8127a 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/tmdbapi/TmdbTools2.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/tmdbapi/TmdbTools2.kt @@ -5,6 +5,7 @@ package com.battlelancer.seriesguide.tmdbapi import android.content.Context import com.battlelancer.seriesguide.SgApp +import com.battlelancer.seriesguide.movies.MoviesSettings import com.battlelancer.seriesguide.provider.SgRoomDatabase import com.battlelancer.seriesguide.util.Errors import com.battlelancer.seriesguide.util.isRetryError @@ -26,16 +27,19 @@ import com.uwetrottmann.tmdb2.entities.TmdbDate import com.uwetrottmann.tmdb2.entities.TvEpisode import com.uwetrottmann.tmdb2.entities.TvShow import com.uwetrottmann.tmdb2.entities.TvShowResultsPage +import com.uwetrottmann.tmdb2.entities.Videos import com.uwetrottmann.tmdb2.entities.WatchProviders import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem import com.uwetrottmann.tmdb2.enumerations.ExternalSource import com.uwetrottmann.tmdb2.enumerations.SortBy +import com.uwetrottmann.tmdb2.enumerations.VideoType import com.uwetrottmann.tmdb2.services.PeopleService import com.uwetrottmann.tmdb2.services.TvEpisodesService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import retrofit2.awaitResponse import retrofit2.create +import timber.log.Timber import java.util.Calendar import java.util.Date @@ -224,6 +228,95 @@ class TmdbTools2 { return null } + fun getShowTrailerYoutubeId( + context: Context, + showTmdbId: Int, + languageCode: String + ): Result { + val action = "get show trailer" + val tmdb = SgApp.getServicesComponent(context.applicationContext).tmdb() + return runCatching { + tmdb.tvService() + .videos(showTmdbId, languageCode) + .execute() + }.mapError { + Errors.logAndReport(action, it) + if (it.isRetryError()) TmdbRetry else TmdbStop + }.andThen { + if (it.isSuccessful) { + val results = it.body()?.results + if (results != null) { + return@andThen Ok(extractTrailer(it.body())) + } else { + Errors.logAndReport(action, it, "results is null") + } + } else { + Errors.logAndReport(action, it) + } + return@andThen Err(TmdbStop) + } + } + + /** + * Loads a YouTube movie trailer from TMDb. Tries to get a local trailer, if not falls back to + * English. + */ + fun getMovieTrailerYoutubeId( + context: Context, + movieTmdbId: Int + ): String? { + // try to get a local trailer + val trailer = getMovieTrailerYoutubeId( + context, movieTmdbId, MoviesSettings.getMoviesLanguage(context), "get local movie trailer" + ) + if (trailer != null) { + return trailer + } + Timber.d("Did not find a local movie trailer.") + + // fall back to default language trailer + return getMovieTrailerYoutubeId( + context, movieTmdbId, null, "get default movie trailer" + ) + } + + private fun getMovieTrailerYoutubeId( + context: Context, + movieTmdbId: Int, + languageCode: String?, + action: String + ): String? { + val moviesService = SgApp.getServicesComponent(context).moviesService() + try { + val response = moviesService.videos(movieTmdbId, languageCode).execute() + if (response.isSuccessful) { + return extractTrailer(response.body()) + } else { + Errors.logAndReport(action, response) + } + } catch (e: Exception) { + Errors.logAndReport(action, e) + } + return null + } + + private fun extractTrailer(videos: Videos?): String? { + val results = videos?.results + if (results == null || results.size == 0) { + return null + } + + // Pick the first YouTube trailer + for (video in results) { + val videoId = video.key + if (video.type == VideoType.TRAILER && "YouTube" == video.site + && !videoId.isNullOrEmpty()) { + return videoId + } + } + return null + } + suspend fun getShowWatchProviders( tmdb: Tmdb, language: String, diff --git a/app/src/main/java/com/battlelancer/seriesguide/util/LanguageTools.kt b/app/src/main/java/com/battlelancer/seriesguide/util/LanguageTools.kt index bd111237d5..09daeda1d9 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/util/LanguageTools.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/util/LanguageTools.kt @@ -91,7 +91,7 @@ object LanguageTools { return null } - data class LanguageData(val languageCode: String?, val languageString: String) + data class LanguageData(val languageCode: String, val languageString: String) /** * Based on the first two letters gets the language display name. Except for diff --git a/app/src/main/res/layout/dialog_addshow.xml b/app/src/main/res/layout/dialog_addshow.xml index ffc6488018..cf1b40d7dc 100644 --- a/app/src/main/res/layout/dialog_addshow.xml +++ b/app/src/main/res/layout/dialog_addshow.xml @@ -1,4 +1,8 @@ + + + + +