implement preferred season/languag choosing in MediaFragment

This commit is contained in:
Jannik 2021-12-28 20:32:44 +01:00
parent 51e214d3c1
commit b21e9c7abd
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
9 changed files with 171 additions and 124 deletions

View File

@ -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<Item>()
private val browsingCache = arrayListOf<Item>()
fun login(username: String, password: String): Boolean = runBlocking {
val tokenEndpoint = "/auth/v1/token"

View File

@ -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<Item>)
// 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<Season>)
data class Seasons(
val total: Int,
val items: List<Season>
) {
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

View File

@ -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

View File

@ -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()
// }
}
}

View File

@ -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()
}
}

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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)