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 clickedpull/49/head
parent
e652c001d3
commit
4fed3ddb91
|
@ -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