From b21e9c7abdfe5496e418d51871784185a5b4a825 Mon Sep 17 00:00:00 2001 From: Jannik Date: Tue, 28 Dec 2021 20:32:44 +0100 Subject: [PATCH] implement preferred season/languag choosing in MediaFragment --- .../teapod/parser/crunchyroll/Crunchyroll.kt | 8 +- .../teapod/parser/crunchyroll/DataTypes.kt | 38 +++- .../mosad/teapod/preferences/Preferences.kt | 3 + .../activity/main/fragments/MediaFragment.kt | 169 ++++++++++-------- .../main/fragments/MediaFragmentEpisodes.kt | 9 +- .../main/fragments/MediaFragmentSimilar.kt | 2 +- .../main/viewmodel/MediaFragmentViewModel.kt | 60 ++++--- .../ui/activity/player/PlayerViewModel.kt | 4 +- .../ui/components/EpisodesListPlayer.kt | 2 +- 9 files changed, 171 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt index 21f9e64..1fc542f 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt @@ -8,9 +8,9 @@ import com.github.kittinunf.fuel.json.FuelJson import com.github.kittinunf.fuel.json.responseJson import com.github.kittinunf.result.Result import kotlinx.coroutines.* -import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import org.mosad.teapod.preferences.Preferences import java.util.* private val json = Json { ignoreUnknownKeys = true } @@ -27,10 +27,10 @@ object Crunchyroll { private var keyPairID = "" // TODO temp helper vary - var locale = "${Locale.GERMANY.language}-${Locale.GERMANY.country}" - var country = Locale.GERMANY.country + private var locale: String = Preferences.preferredLocal.toLanguageTag() + private var country: String = Preferences.preferredLocal.country - val browsingCache = arrayListOf() + private val browsingCache = arrayListOf() fun login(username: String, password: String): Boolean = runBlocking { val tokenEndpoint = "/auth/v1/token" diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt index 1f9edba..4ea22dd 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt @@ -2,6 +2,7 @@ package org.mosad.teapod.parser.crunchyroll import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.util.* /** * data classes for browse @@ -36,6 +37,7 @@ val NoneSearchResult = SearchResult(0, emptyList()) data class BrowseResult(val total: Int, val items: List) // the data class Item is used in browse and search +// TODO rename to MediaPanel @Serializable data class Item( val id: String, @@ -74,14 +76,38 @@ val NoneSeries = Series("", "", "", Images(listOf(), listOf())) * Seasons data type */ @Serializable -data class Seasons(val total: Int, val items: List) +data class Seasons( + val total: Int, + val items: List +) { + fun getPreferredSeasonId(local: Locale): String { + // try to get the the first seasons which matches the preferred local + items.forEach { season -> + if (season.title.startsWith("(${local.language})", true)) { + return season.id + } + } + + // if there is no season with the preferred local, try to find a subbed season + items.forEach { season -> + if (season.isSubbed) { + return season.id + } + } + + // if there is no preferred language season and no sub, use the first season + return items.first().id + } +} @Serializable data class Season( - val id: String, - val title: String, - val series_id: String, - val season_number: Int + @SerialName("id") val id: String, + @SerialName("title") val title: String, + @SerialName("series_id") val seriesId: String, + @SerialName("season_number") val seasonNumber: Int, + @SerialName("is_subbed") val isSubbed: Boolean, + @SerialName("is_dubbed") val isDubbed: Boolean, ) val NoneSeasons = Seasons(0, listOf()) @@ -101,7 +127,7 @@ data class Episode( @SerialName("season_id") val seasonId: String, @SerialName("season_number") val seasonNumber: Int, @SerialName("episode") val episode: String, - @SerialName("episode_number") val episodeNumber: Int, + @SerialName("episode_number") val episodeNumber: Int? = null, @SerialName("description") val description: String, @SerialName("next_episode_id") val nextEpisodeId: String? = null, // default/nullable value since optional @SerialName("next_episode_title") val nextEpisodeTitle: String? = null, // default/nullable value since optional diff --git a/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt b/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt index b5c1d60..96440ca 100644 --- a/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt +++ b/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt @@ -4,11 +4,14 @@ import android.content.Context import android.content.SharedPreferences import org.mosad.teapod.R import org.mosad.teapod.util.DataTypes +import java.util.* object Preferences { var preferSecondary = false internal set + var preferredLocal = Locale.GERMANY + internal set var autoplay = true internal set var devSettings = false diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt index 87408e7..f9caed4 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt @@ -15,6 +15,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.tabs.TabLayoutMediator import jp.wasabeef.glide.transformations.BlurTransformation import kotlinx.coroutines.launch import org.mosad.teapod.R @@ -56,14 +57,14 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : // fix material components issue #1878, if more tabs are added increase binding.pagerEpisodesSimilar.offscreenPageLimit = 2 binding.pagerEpisodesSimilar.adapter = pagerAdapter - // TODO implement for cr media items -// TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position -> -// tab.text = if (model.media.type == MediaType.TVSHOW && position == 0) { -// getString(R.string.episodes) -// } else { -// getString(R.string.similar_titles) -// } -// }.attach() + // TODO is position 0 always episodes? (and 1 always similar titles) + TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position -> + tab.text = when(position) { + 0 -> getString(R.string.episodes) + 1 -> getString(R.string.similar_titles) + else -> "" + } + }.attach() lifecycleScope.launch { model.loadCrunchy(mediaIdStr) @@ -77,9 +78,10 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : super.onResume() // update the next ep text if there is one, since it may have changed - if (model.media.getEpisodeById(model.nextEpisodeId).title.isNotEmpty()) { - binding.textTitle.text = model.media.getEpisodeById(model.nextEpisodeId).title - } + // TODO reimplement +// if (model.media.getEpisodeById(model.nextEpisodeId).title.isNotEmpty()) { +// binding.textTitle.text = model.media.getEpisodeById(model.nextEpisodeId).title +// } } /** @@ -88,9 +90,9 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : private fun updateGUI() = with(model) { // generic gui val backdropUrl = tmdbResult?.backdropPath?.let { TMDBApiController.imageUrl + it } - ?: mediaCrunchy.images.poster_wide[0][2].source + ?: seriesCrunchy.images.poster_wide[0][2].source val posterUrl = tmdbResult?.posterPath?.let { TMDBApiController.imageUrl + it } - ?: mediaCrunchy.images.poster_tall[0][2].source + ?: seriesCrunchy.images.poster_tall[0][2].source // load poster and backdrop Glide.with(requireContext()).load(posterUrl) @@ -100,65 +102,74 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3))) .into(binding.imageBackdrop) - binding.textTitle.text = mediaCrunchy.title - //binding.textYear.text = media.year.toString() // TODO - //binding.textAge.text = media.age.toString() // TODO - binding.textOverview.text = mediaCrunchy.description + binding.textTitle.text = seriesCrunchy.title + //binding.textYear.text = media.year.toString() // TODO get from tmdb + //binding.textAge.text = media.age.toString() // TODO get from tmdb + binding.textOverview.text = seriesCrunchy.description // TODO set "my list" indicator - if (StorageController.myList.contains(media.aodId)) { - Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) - } else { - Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction) - } +// if (StorageController.myList.contains(media.aodId)) { +// Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) +// } else { +// Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction) +// } // clear fragments, since it lives in onCreate scope (don't do this in onPause/onStop -> FragmentManager transaction) val fragmentsSize = if (fragments.lastIndex < 0) 0 else fragments.lastIndex fragments.clear() pagerAdapter.notifyItemRangeRemoved(0, fragmentsSize) - // specific gui - if (mediaCrunchy.type == MediaType.TVSHOW.str) { - // TODO get next episode -// nextEpisodeId = media.playlist.firstOrNull{ !it.watched }?.mediaId -// ?: media.playlist.first().mediaId - // TODO title is the next episodes title -// binding.textTitle.text = media.getEpisodeById(nextEpisodeId).title - - // episodes count - binding.textEpisodesOrRuntime.text = resources.getQuantityString( - R.plurals.text_episodes_count, - episodesCrunchy.total, - episodesCrunchy.total - ) - - // episodes - MediaFragmentEpisodes().also { - fragments.add(it) - pagerAdapter.notifyItemInserted(fragments.indexOf(it)) - } - } else if (media.type == MediaType.MOVIE) { - val tmdbMovie = (tmdbResult as TMDBMovie?) - - if (tmdbMovie?.runtime != null) { - binding.textEpisodesOrRuntime.text = resources.getQuantityString( - R.plurals.text_runtime, - tmdbMovie.runtime, - tmdbMovie.runtime - ) - } else { - binding.textEpisodesOrRuntime.visibility = View.GONE - } + // add the episodes fragment (as tab) + MediaFragmentEpisodes().also { + fragments.add(it) + pagerAdapter.notifyItemInserted(fragments.indexOf(it)) } + // TODO reimplement via tmdb/metaDB + // specific gui +// if (mediaCrunchy.type == MediaType.TVSHOW.str) { +// // TODO get next episode +//// nextEpisodeId = media.playlist.firstOrNull{ !it.watched }?.mediaId +//// ?: media.playlist.first().mediaId +// +// // TODO title is the next episodes title +//// binding.textTitle.text = media.getEpisodeById(nextEpisodeId).title +// +// // episodes count +// binding.textEpisodesOrRuntime.text = resources.getQuantityString( +// R.plurals.text_episodes_count, +// episodesCrunchy.total, +// episodesCrunchy.total +// ) +// +// // episodes +// MediaFragmentEpisodes().also { +// fragments.add(it) +// pagerAdapter.notifyItemInserted(fragments.indexOf(it)) +// } +// } else if (media.type == MediaType.MOVIE) { +// val tmdbMovie = (tmdbResult as TMDBMovie?) +// +// if (tmdbMovie?.runtime != null) { +// binding.textEpisodesOrRuntime.text = resources.getQuantityString( +// R.plurals.text_runtime, +// tmdbMovie.runtime, +// tmdbMovie.runtime +// ) +// } else { +// binding.textEpisodesOrRuntime.visibility = View.GONE +// } +// } + // if has similar titles - if (media.similar.isNotEmpty()) { - MediaFragmentSimilar().also { - fragments.add(it) - pagerAdapter.notifyItemInserted(fragments.indexOf(it)) - } - } + // TODO reimplement +// if (media.similar.isNotEmpty()) { +// MediaFragmentSimilar().also { +// fragments.add(it) +// pagerAdapter.notifyItemInserted(fragments.indexOf(it)) +// } +// } // disable scrolling on appbar, if no tabs where added if(fragments.isEmpty()) { @@ -171,28 +182,30 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : private fun initActions() = with(model) { binding.buttonPlay.setOnClickListener { - when (media.type) { - //MediaType.MOVIE -> playEpisode(media.playlist.first().mediaId) // TODO - //MediaType.TVSHOW -> playEpisode(nextEpisodeId) // TODO - else -> Log.e(javaClass.name, "Wrong Type: ${media.type}") - } + // TODO reimplement +// when (media.type) { +// MediaType.MOVIE -> playEpisode(media.playlist.first().mediaId) +// MediaType.TVSHOW -> playEpisode(nextEpisodeId) +// else -> Log.e(javaClass.name, "Wrong Type: ${media.type}") +// } } // add or remove media from myList binding.linearMyListAction.setOnClickListener { - if (StorageController.myList.contains(media.aodId)) { - StorageController.myList.remove(media.aodId) - Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction) - } else { - StorageController.myList.add(media.aodId) - Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) - } - StorageController.saveMyList(requireContext()) - - // notify home fragment on change - parentFragmentManager.findFragmentByTag("HomeFragment")?.let { - (it as HomeFragment).updateMyListMedia() - } + // TODO reimplement +// if (StorageController.myList.contains(media.aodId)) { +// StorageController.myList.remove(media.aodId) +// Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction) +// } else { +// StorageController.myList.add(media.aodId) +// Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) +// } +// StorageController.saveMyList(requireContext()) +// +// // notify home fragment on change +// parentFragmentManager.findFragmentByTag("HomeFragment")?.let { +// (it as HomeFragment).updateMyListMedia() +// } } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt index a0985ce..0d47b65 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt @@ -44,10 +44,11 @@ class MediaFragmentEpisodes : Fragment() { // if adapterRecEpisodes is initialized, update the watched state for the episodes if (this::adapterRecEpisodes.isInitialized) { - model.media.playlist.forEachIndexed { index, episodeInfo -> - adapterRecEpisodes.updateWatchedState(episodeInfo.watched, index) - } - adapterRecEpisodes.notifyDataSetChanged() + // TODO reimplement, if needed +// model.media.playlist.forEachIndexed { index, episodeInfo -> +// adapterRecEpisodes.updateWatchedState(episodeInfo.watched, index) +// } +// adapterRecEpisodes.notifyDataSetChanged() } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt index c57770b..052ec89 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt @@ -27,7 +27,7 @@ class MediaFragmentSimilar : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - adapterSimilar = MediaItemAdapter(model.media.similar) + adapterSimilar = MediaItemAdapter(emptyList()) //(model.media.similar) binding.recyclerMediaSimilar.adapter = adapterSimilar binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9)) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt index ac73a6e..b3a26fb 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt @@ -3,9 +3,16 @@ package org.mosad.teapod.ui.activity.main.viewmodel import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel -import org.mosad.teapod.parser.crunchyroll.* -import org.mosad.teapod.util.* +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import org.mosad.teapod.parser.crunchyroll.Crunchyroll +import org.mosad.teapod.parser.crunchyroll.NoneEpisodes +import org.mosad.teapod.parser.crunchyroll.NoneSeasons +import org.mosad.teapod.parser.crunchyroll.NoneSeries +import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.util.DataTypes.MediaType +import org.mosad.teapod.util.Meta import org.mosad.teapod.util.tmdb.TMDBApiController import org.mosad.teapod.util.tmdb.TMDBResult import org.mosad.teapod.util.tmdb.TMDBTVSeason @@ -16,12 +23,9 @@ import org.mosad.teapod.util.tmdb.TMDBTVSeason */ class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) { - var media = AoDMediaNone - internal set - var nextEpisodeId = -1 - internal set - - var mediaCrunchy = NoneItem +// var mediaCrunchy = NoneItem +// internal set + var seriesCrunchy = NoneSeries // TODO it seems movies also series? internal set var seasonsCrunchy = NoneSeasons internal set @@ -35,34 +39,31 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic var mediaMeta: Meta? = null internal set + /** + * @param crunchyId the crunchyroll series id + */ suspend fun loadCrunchy(crunchyId: String) { val tmdbApiController = TMDBApiController() - println("loading crunchyroll media $crunchyId") + // load series and seasons info in parallel + listOf( + viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId) }, + viewModelScope.launch { seasonsCrunchy = Crunchyroll.seasons(crunchyId) } + ).joinAll() - // TODO info also in browse result item - // TODO doesn't support search - mediaCrunchy = Crunchyroll.browsingCache.find { it -> - it.id == crunchyId - } ?: NoneItem - println("media: $mediaCrunchy") - - // load seasons - seasonsCrunchy = Crunchyroll.seasons(crunchyId) + println("series: $seriesCrunchy") println("seasons: $seasonsCrunchy") - // load first season - // TODO make sure to load the preferred season (language), language is set per season, not per stream - episodesCrunchy = Crunchyroll.episodes(seasonsCrunchy.items.first().id) + // load the preferred season (preferred language, language per season, not per stream) + val preferredSeasonId = seasonsCrunchy.getPreferredSeasonId(Preferences.preferredLocal) + episodesCrunchy = Crunchyroll.episodes(preferredSeasonId) println("episodes: $episodesCrunchy") - - // TODO check if metaDB knows the title - // use tmdb search to get media info TODO media type is hardcoded, use type info from browse result once implemented + // use tmdb search to get media info TODO media type is hardcoded, use episodeNumber? (if null it should be a movie) mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media - val tmdbId = tmdbApiController.search(mediaCrunchy.title, MediaType.TVSHOW) + val tmdbId = tmdbApiController.search(seriesCrunchy.title, MediaType.TVSHOW) tmdbResult = when (MediaType.TVSHOW) { MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) @@ -122,10 +123,11 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic * if no matching is found, use first episode */ fun updateNextEpisode(episodeId: Int) { - if (media.type == MediaType.MOVIE) return // return if movie - - nextEpisodeId = media.playlist.firstOrNull { it.index > media.getEpisodeById(episodeId).index }?.mediaId - ?: media.playlist.first().mediaId + // TODO reimplement if needed +// if (media.type == MediaType.MOVIE) return // return if movie +// +// nextEpisodeId = media.playlist.firstOrNull { it.index > media.getEpisodeById(episodeId).index }?.mediaId +// ?: media.playlist.first().mediaId } // remove unneeded info from the media title before searching diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt index c812342..615bea1 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt @@ -168,7 +168,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) currentEpisodeChangedListener.forEach { it() } // get preferred stream url TODO implement - val url = currentPlayback.streams.adaptive_hls["en-US"]?.url ?: "" + val localeKey = Preferences.preferredLocal.toLanguageTag() + val url = currentPlayback.streams.adaptive_hls[localeKey]?.url + ?: currentPlayback.streams.adaptive_hls[""]?.url ?: "" println("stream url: $url") // create the media source object diff --git a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt index fd139aa..ce182f2 100644 --- a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt +++ b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt @@ -34,7 +34,7 @@ class EpisodesListPlayer @JvmOverloads constructor( model.setCurrentEpisode(episodeId, startPlayback = true) } // episodeNumber starts at 1, we need the episode index -> - 1 - adapterRecEpisodes.currentSelected = (model.currentEpisode.episodeNumber - 1) + adapterRecEpisodes.currentSelected = model.currentEpisode.episodeNumber?.minus(1) ?: 0 binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)