Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support displaying show trailer #958

Merged
merged 5 commits into from
Nov 17, 2023
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,48 +15,7 @@ class MovieTrailersLoader(context: Context, private val tmdbId: Int) :
GenericSimpleLoader<String?>(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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
}
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,6 +36,7 @@ class AddShowDialogViewModel(

val languageCode = MutableLiveData<String>()
val showDetails: LiveData<ShowDetails>
val trailer: LiveData<String?>

init {
// Set original value for region.
Expand Down Expand Up @@ -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<StreamingSearch.WatchInfo>().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -224,6 +228,95 @@ class TmdbTools2 {
return null
}

fun getShowTrailerYoutubeId(
context: Context,
showTmdbId: Int,
languageCode: String
): Result<String?, TmdbError> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading