add upNextSeries
the MediaFragment will show the next episodes title instead for the series title and play the "next up" episode when the play button is clicked
This commit is contained in:
parent
e98e75456e
commit
a10287f747
|
@ -14,7 +14,7 @@ android {
|
|||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 4200 //00.04.200
|
||||
versionName "1.0.0-alpha2"
|
||||
versionName "1.0.0-alpha3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resValue "string", "build_time", buildTime()
|
||||
|
|
|
@ -295,6 +295,23 @@ object Crunchyroll {
|
|||
} ?: NoneSeries
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
suspend fun upNextSeries(seriesId: String): UpNextSeriesItem {
|
||||
val upNextSeriesEndpoint = "/content/v1/up_next_series"
|
||||
val parameters = listOf(
|
||||
"series_id" to seriesId,
|
||||
"locale" to locale
|
||||
)
|
||||
|
||||
val result = request(upNextSeriesEndpoint, parameters)
|
||||
|
||||
return result.component1()?.obj()?.let {
|
||||
json.decodeFromString(it.toString())
|
||||
} ?: NoneUpNextSeriesItem
|
||||
}
|
||||
|
||||
suspend fun seasons(seriesId: String): Seasons {
|
||||
val episodesEndpoint = "/cms/v2/$country/M3/crunchyroll/seasons"
|
||||
val parameters = listOf(
|
||||
|
@ -404,6 +421,18 @@ object Crunchyroll {
|
|||
} ?: emptyMap()
|
||||
}
|
||||
|
||||
suspend fun postPlayheads(episodeId: String, playhead: Int) {
|
||||
val playheadsEndpoint = "/content/v1/playheads/$accountID"
|
||||
val parameters = listOf("locale" to locale)
|
||||
|
||||
val json = buildJsonObject {
|
||||
put("content_id", episodeId)
|
||||
put("playhead", playhead)
|
||||
}
|
||||
|
||||
requestPost(playheadsEndpoint, parameters, json.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing functions: watchlist (list), up_next_account
|
||||
*/
|
||||
|
|
|
@ -31,6 +31,14 @@ typealias DiscSeasonList = Collection<SeasonListItem>
|
|||
typealias Watchlist = Collection<Item>
|
||||
typealias ContinueWatchingList = Collection<ContinueWatchingItem>
|
||||
|
||||
@Serializable
|
||||
data class UpNextSeriesItem(
|
||||
val playhead: Int,
|
||||
val fully_watched: Boolean,
|
||||
val never_watched: Boolean,
|
||||
val panel: EpisodePanel,
|
||||
)
|
||||
|
||||
/**
|
||||
* panel data classes
|
||||
*/
|
||||
|
@ -73,17 +81,18 @@ data class SeasonListLocalization(
|
|||
/**
|
||||
* continue_watching_item data classes
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class ContinueWatchingItem(
|
||||
@SerialName("panel") val panel: EpisodePanel,
|
||||
@SerialName("new") val new: Boolean,
|
||||
@SerialName("new_content") val newContent: Boolean,
|
||||
// not present in up_next_account's continue_watching_item
|
||||
// not present in up_next_account -> continue_watching_item
|
||||
// @SerialName("is_favorite") val isFavorite: Boolean,
|
||||
// @SerialName("never_watched") val neverWatched: Boolean,
|
||||
// @SerialName("completion_status") val completionStatus: Boolean,
|
||||
@SerialName("playhead") val playhead: Int,
|
||||
// not present in watchlist -> continue_watching_item
|
||||
// @SerialName("fully_watched") val fullyWatched: Boolean,
|
||||
)
|
||||
|
||||
// EpisodePanel is used in ContinueWatchingItem
|
||||
|
@ -94,24 +103,31 @@ data class EpisodePanel(
|
|||
@SerialName("type") val type: String,
|
||||
@SerialName("channel_id") val channelId: String,
|
||||
@SerialName("description") val description: String,
|
||||
@SerialName("images") val images: Thumbnail,
|
||||
@SerialName("episode_metadata") val episodeMetadata: EpisodeMetadata,
|
||||
@SerialName("images") val images: Thumbnail,
|
||||
@SerialName("playback") val playback: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EpisodeMetadata(
|
||||
@SerialName("duration_ms") val durationMs: Int,
|
||||
@SerialName("season_id") val seasonId: String,
|
||||
@SerialName("series_id") val seriesId: String,
|
||||
@SerialName("series_title") val seriesTitle: String,
|
||||
)
|
||||
|
||||
val NoneItem = Item("", "", "", "", "", Images(emptyList(), emptyList()))
|
||||
val NoneEpisodeMetadata = EpisodeMetadata(0, "", "", "")
|
||||
val NoneEpisodePanel = EpisodePanel("", "", "", "", "", NoneEpisodeMetadata, Thumbnail(listOf()), "")
|
||||
|
||||
val NoneCollection = Collection<Item>(0, emptyList())
|
||||
val NoneSearchResult = SearchResult(0, emptyList())
|
||||
val NoneBrowseResult = BrowseResult(0, emptyList())
|
||||
val NoneDiscSeasonList = DiscSeasonList(0, emptyList())
|
||||
val NoneWatchlist = Watchlist(0, emptyList())
|
||||
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
|
||||
|
||||
val NoneUpNextSeriesItem =UpNextSeriesItem(0, false, false, NoneEpisodePanel)
|
||||
|
||||
/**
|
||||
* Series data type
|
||||
*/
|
||||
|
@ -163,7 +179,7 @@ data class Season(
|
|||
@SerialName("is_dubbed") val isDubbed: Boolean,
|
||||
)
|
||||
|
||||
val NoneSeasons = Seasons(0, listOf())
|
||||
val NoneSeasons = Seasons(0, emptyList())
|
||||
val NoneSeason = Season("", "", "", 0, isSubbed = false, isDubbed = false)
|
||||
|
||||
|
||||
|
|
|
@ -17,10 +17,10 @@ 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.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentMediaBinding
|
||||
import org.mosad.teapod.parser.crunchyroll.NoneUpNextSeriesItem
|
||||
import org.mosad.teapod.ui.activity.main.MainActivity
|
||||
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||
import org.mosad.teapod.util.tmdb.TMDBApiController
|
||||
|
@ -37,19 +37,23 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||
private lateinit var binding: FragmentMediaBinding
|
||||
private lateinit var pagerAdapter: FragmentStateAdapter
|
||||
|
||||
private val model: MediaFragmentViewModel by activityViewModels()
|
||||
|
||||
private val fragments = arrayListOf<Fragment>()
|
||||
private var watchlistJobRunning = false
|
||||
private var runOnResume = false
|
||||
|
||||
private val model: MediaFragmentViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
println("onViewCreated")
|
||||
|
||||
binding.frameLoading.visibility = View.VISIBLE
|
||||
|
||||
// tab layout and pager
|
||||
|
@ -77,11 +81,21 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// update the next ep text if there is one, since it may have changed
|
||||
// TODO reimplement
|
||||
// if (model.media.getEpisodeById(model.nextEpisodeId).title.isNotEmpty()) {
|
||||
// binding.textTitle.text = model.media.getEpisodeById(model.nextEpisodeId).title
|
||||
// }
|
||||
if (runOnResume) {
|
||||
lifecycleScope.launch {
|
||||
model.updateOnResume()
|
||||
|
||||
if (model.upNextSeries != NoneUpNextSeriesItem) {
|
||||
binding.textTitle.text = model.upNextSeries.panel.title
|
||||
}
|
||||
|
||||
if (fragments.elementAtOrNull(0) is MediaFragmentEpisodes) {
|
||||
(fragments[0] as MediaFragmentEpisodes).updateWatchedState()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runOnResume = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,15 +116,17 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||
.apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
|
||||
.into(binding.imageBackdrop)
|
||||
|
||||
binding.textTitle.text = seriesCrunchy.title
|
||||
binding.textOverview.text = seriesCrunchy.description
|
||||
binding.textAge.text = seriesCrunchy.maturityRatings.firstOrNull()
|
||||
|
||||
binding.textYear.text = when(tmdbResult) {
|
||||
is TMDBTVShow -> (tmdbResult as TMDBTVShow).firstAirDate.substring(0, 4)
|
||||
is TMDBMovie -> (tmdbResult as TMDBMovie).releaseDate.substring(0, 4)
|
||||
else -> ""
|
||||
}
|
||||
binding.textAge.text = seriesCrunchy.maturityRatings.firstOrNull()
|
||||
|
||||
binding.textTitle.text = if (upNextSeries != NoneUpNextSeriesItem) {
|
||||
upNextSeries.panel.title
|
||||
} else seriesCrunchy.title
|
||||
binding.textOverview.text = seriesCrunchy.description
|
||||
|
||||
// set "watchlist" indicator
|
||||
val watchlistIcon = if (isWatchlist) R.drawable.ic_baseline_check_24 else R.drawable.ic_baseline_add_24
|
||||
|
@ -127,8 +143,7 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||
pagerAdapter.notifyItemInserted(fragments.indexOf(it))
|
||||
}
|
||||
|
||||
// TODO reimplement via tmdb/metaDB
|
||||
// specific gui
|
||||
// specific gui (via tmdb)
|
||||
when (tmdbResult) {
|
||||
is TMDBTVShow -> {
|
||||
// episodes count
|
||||
|
@ -156,40 +171,6 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// TODO reimplement
|
||||
// if (media.similar.isNotEmpty()) {
|
||||
|
@ -210,12 +191,9 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
|||
|
||||
private fun initActions() = with(model) {
|
||||
binding.buttonPlay.setOnClickListener {
|
||||
// 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}")
|
||||
// }
|
||||
if (upNextSeries != NoneUpNextSeriesItem) {
|
||||
playEpisode(upNextSeries.panel.episodeMetadata.seasonId, upNextSeries.panel.id)
|
||||
}
|
||||
}
|
||||
|
||||
// add or remove media from myList
|
||||
|
|
|
@ -60,6 +60,7 @@ class MediaFragmentEpisodes : Fragment() {
|
|||
// if adapterRecEpisodes is initialized, update the watched state for the episodes
|
||||
if (this::adapterRecEpisodes.isInitialized) {
|
||||
// TODO reimplement, if needed
|
||||
// update via playheads?
|
||||
// model.media.playlist.forEachIndexed { index, episodeInfo ->
|
||||
// adapterRecEpisodes.updateWatchedState(episodeInfo.watched, index)
|
||||
// }
|
||||
|
@ -67,6 +68,11 @@ class MediaFragmentEpisodes : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateWatchedState() {
|
||||
// TODO update watched state of all episodes
|
||||
// use a mutable list for playheads and notify dataset changed
|
||||
}
|
||||
|
||||
private fun showSeasonSelection(v: View) {
|
||||
// TODO replace with Exposed dropdown menu: https://material.io/components/menus/android#exposed-dropdown-menus
|
||||
val popup = PopupMenu(requireContext(), v)
|
||||
|
|
|
@ -3,9 +3,9 @@ package org.mosad.teapod.ui.activity.main.viewmodel
|
|||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import org.mosad.teapod.parser.crunchyroll.*
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.util.DataTypes.MediaType
|
||||
|
@ -29,9 +29,12 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||
var episodesCrunchy = NoneEpisodes
|
||||
internal set
|
||||
val currentEpisodesCrunchy = arrayListOf<Episode>() // used for EpisodeItemAdapter (easier updates)
|
||||
|
||||
// additional media info
|
||||
var currentPlayheads: PlayheadsMap = emptyMap()
|
||||
var isWatchlist = false
|
||||
internal set
|
||||
var upNextSeries = NoneUpNextSeriesItem
|
||||
|
||||
// TMDB stuff
|
||||
var mediaType = MediaType.OTHER
|
||||
|
@ -52,35 +55,40 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||
listOf(
|
||||
viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId) },
|
||||
viewModelScope.launch { seasonsCrunchy = Crunchyroll.seasons(crunchyId) },
|
||||
viewModelScope.launch { isWatchlist = Crunchyroll.isWatchlist(crunchyId) }
|
||||
viewModelScope.launch { isWatchlist = Crunchyroll.isWatchlist(crunchyId) },
|
||||
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(crunchyId) }
|
||||
).joinAll()
|
||||
|
||||
println("series: $seriesCrunchy")
|
||||
println("seasons: $seasonsCrunchy")
|
||||
|
||||
// TODO load episodes, metaDB and tmdb in parallel
|
||||
// println("series: $seriesCrunchy")
|
||||
// println("seasons: $seasonsCrunchy")
|
||||
println(upNextSeries)
|
||||
|
||||
// load the preferred season (preferred language, language per season, not per stream)
|
||||
currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocal)
|
||||
episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id)
|
||||
|
||||
// load episodes and metaDB in parallel (tmdb needs mediaType, which is set via episodes)
|
||||
listOf(
|
||||
viewModelScope.launch { episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) },
|
||||
viewModelScope.launch { mediaMeta = null }, // TODO metaDB
|
||||
).joinAll()
|
||||
// println("episodes: $episodesCrunchy")
|
||||
|
||||
currentEpisodesCrunchy.clear()
|
||||
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
|
||||
println("episodes: $episodesCrunchy")
|
||||
|
||||
// get playheads (including fully watched state)
|
||||
val episodeIDs = episodesCrunchy.items.map { it.id }
|
||||
currentPlayheads = Crunchyroll.playheads(episodeIDs)
|
||||
|
||||
// set media type
|
||||
mediaType = episodesCrunchy.items.firstOrNull()?.let {
|
||||
if (it.episodeNumber != null) MediaType.TVSHOW else MediaType.MOVIE
|
||||
} ?: MediaType.OTHER
|
||||
|
||||
// TODO check if metaDB knows the title
|
||||
mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media
|
||||
|
||||
// use tmdb search to get media info
|
||||
loadTmdbInfo()
|
||||
// load playheads and tmdb in parallel
|
||||
listOf(
|
||||
viewModelScope.launch {
|
||||
// get playheads (including fully watched state)
|
||||
val episodeIDs = episodesCrunchy.items.map { it.id }
|
||||
currentPlayheads = Crunchyroll.playheads(episodeIDs)
|
||||
},
|
||||
viewModelScope.launch { loadTmdbInfo() } // use tmdb search to get media info
|
||||
).joinAll()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -143,6 +151,16 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun updateOnResume(): List<Job> {
|
||||
return listOf(
|
||||
viewModelScope.launch {
|
||||
val episodeIDs = episodesCrunchy.items.map { it.id }
|
||||
currentPlayheads = Crunchyroll.playheads(episodeIDs)
|
||||
},
|
||||
viewModelScope.launch { upNextSeries = Crunchyroll.upNextSeries(seriesCrunchy.id) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* get the next episode based on episodeId
|
||||
* if no matching is found, use first episode
|
||||
|
|
Loading…
Reference in New Issue