add playheads to crunchyroll parser

* show watched icon, if episode has been fully watched
* add seasonTag to browse()
This commit is contained in:
Jannik 2022-01-05 01:28:39 +01:00
parent 2fa5a0aacd
commit 04b1ac5a53
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
5 changed files with 78 additions and 28 deletions

View File

@ -14,6 +14,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.concatenate
import java.util.* import java.util.*
private val json = Json { ignoreUnknownKeys = true } private val json = Json { ignoreUnknownKeys = true }
@ -179,9 +180,21 @@ object Crunchyroll {
* *
* @return A **[BrowseResult]** object is returned. * @return A **[BrowseResult]** object is returned.
*/ */
suspend fun browse(sortBy: SortBy = SortBy.ALPHABETICAL, start: Int = 0, n: Int = 10): BrowseResult { suspend fun browse(
sortBy: SortBy = SortBy.ALPHABETICAL,
seasonTag: String = "",
start: Int = 0,
n: Int = 10
): BrowseResult {
val browseEndpoint = "/content/v1/browse" val browseEndpoint = "/content/v1/browse"
val parameters = listOf("sort_by" to sortBy.str, "start" to start, "n" to n) val noneOptParams = listOf("sort_by" to sortBy.str, "start" to start, "n" to n)
// if a season tag is present add it to the parameters
val parameters = if (seasonTag.isEmpty()) {
concatenate(noneOptParams, listOf("season_tag" to seasonTag))
} else {
noneOptParams
}
val result = request(browseEndpoint, parameters) val result = request(browseEndpoint, parameters)
val browseResult = result.component1()?.obj()?.let { val browseResult = result.component1()?.obj()?.let {
@ -216,7 +229,7 @@ object Crunchyroll {
* Note: episode objects are currently not supported * Note: episode objects are currently not supported
* *
* @param objects The object IDs as list of Strings * @param objects The object IDs as list of Strings
* @return A Collection of Panels * @return A **[Collection]** of Panels
*/ */
suspend fun objects(objects: List<String>): Collection { suspend fun objects(objects: List<String>): Collection {
val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}" val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}"
@ -228,7 +241,6 @@ object Crunchyroll {
) )
val result = request(episodesEndpoint, parameters) val result = request(episodesEndpoint, parameters)
println(result.component1()?.obj()?.toString())
return result.component1()?.obj()?.let { return result.component1()?.obj()?.let {
json.decodeFromString(it.toString()) json.decodeFromString(it.toString())
@ -304,7 +316,7 @@ object Crunchyroll {
* Check if a media is in the user's watchlist. * Check if a media is in the user's watchlist.
* *
* @param seriesId The crunchyroll series id of the media to check * @param seriesId The crunchyroll series id of the media to check
* @return Boolean: ture if it was found, else false * @return **[Boolean]**: ture if it was found, else false
*/ */
suspend fun isWatchlist(seriesId: String): Boolean { suspend fun isWatchlist(seriesId: String): Boolean {
val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId" val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
@ -345,16 +357,34 @@ object Crunchyroll {
} }
/** /**
* TODO * Get playhead information for all episodes in episodeIDs.
* The Information returned contains the playhead position, watched state
* and last modified date.
*
* @param episodeIDs A **[List]** of episodes IDs as strings.
* @return A **[Map]**<String, **[PlayheadObject]**> containing playback info.
*/ */
suspend fun playhead() { suspend fun playheads(episodeIDs: List<String>): PlayheadsMap {
// implement val playheadsEndpoint = "/content/v1/playheads/$accountID/${episodeIDs.joinToString(",")}"
val parameters = listOf("locale" to locale)
val result = request(playheadsEndpoint, parameters)
return result.component1()?.obj()?.let {
json.decodeFromString(it.toString())
} ?: emptyMap()
} }
/** /**
* Listing functions: watchlist (list), up_next_account * Listing functions: watchlist (list), up_next_account
*/ */
/**
* List items present in the watchlist.
*
* @param n Number of items to return, defaults to 20.
* @return A **[Watchlist]** containing up to n **[Item]**.
*/
suspend fun watchlist(n: Int = 20): Watchlist { suspend fun watchlist(n: Int = 20): Watchlist {
val watchlistEndpoint = "/content/v1/$accountID/watchlist" val watchlistEndpoint = "/content/v1/$accountID/watchlist"
val parameters = listOf("locale" to locale, "n" to n) val parameters = listOf("locale" to locale, "n" to n)
@ -369,20 +399,19 @@ object Crunchyroll {
} }
/** /**
* TODO * List the next up episodes for the logged in account.
*
* @param n Number of items to return, defaults to 20.
* @return A **[ContinueWatchingList]** containing up to n **[ContinueWatchingItem]**.
*/ */
suspend fun upNextAccount(n: Int = 20): ContinueWatchingList { suspend fun upNextAccount(n: Int = 20): ContinueWatchingList {
val watchlistEndpoint = "/content/v1/$accountID/up_next_account" val watchlistEndpoint = "/content/v1/$accountID/up_next_account"
val parameters = listOf("locale" to locale, "n" to n) val parameters = listOf("locale" to locale, "n" to n)
val resultUpNextAccount = request(watchlistEndpoint, parameters) val resultUpNextAccount = request(watchlistEndpoint, parameters)
val list: ContinueWatchingList = resultUpNextAccount.component1()?.obj()?.let { return resultUpNextAccount.component1()?.obj()?.let {
json.decodeFromString(it.toString()) json.decodeFromString(it.toString())
} ?: NoneContinueWatchingList } ?: NoneContinueWatchingList
// val objects = list.items.map{ it.panel.episodeMetadata.seriesId }
// return objects(objects)
return list
} }
} }

View File

@ -29,7 +29,6 @@ data class Collection(
typealias SearchCollection = Collection typealias SearchCollection = Collection
typealias BrowseResult = Collection typealias BrowseResult = Collection
typealias Watchlist = Collection typealias Watchlist = Collection
typealias UpNextAccount = Collection
@Serializable @Serializable
data class SearchResult( data class SearchResult(
@ -112,7 +111,6 @@ data class Series(
) )
val NoneSeries = Series("", "", "", Images(emptyList(), emptyList()), emptyList()) val NoneSeries = Series("", "", "", Images(emptyList(), emptyList()), emptyList())
/** /**
* Seasons data type * Seasons data type
*/ */
@ -209,6 +207,16 @@ val NoneEpisode = Episode(
playback = "" playback = ""
) )
typealias PlayheadsMap = Map<String, PlayheadObject>
@Serializable
data class PlayheadObject(
@SerialName("playhead") val playhead: Int,
@SerialName("content_id") val contentId: String,
@SerialName("fully_watched") val fullyWatched: Boolean,
@SerialName("last_modified") val lastModified: String,
)
/** /**
* Playback/stream data type * Playback/stream data type
*/ */

View File

@ -31,7 +31,11 @@ class MediaFragmentEpisodes : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapterRecEpisodes = EpisodeItemAdapter(model.currentEpisodesCrunchy, model.tmdbTVSeason.episodes) adapterRecEpisodes = EpisodeItemAdapter(
model.currentEpisodesCrunchy,
model.tmdbTVSeason.episodes,
model.currentPlayheads
)
binding.recyclerEpisodes.adapter = adapterRecEpisodes binding.recyclerEpisodes.adapter = adapterRecEpisodes
// set onItemClick, adapter is initialized // set onItemClick, adapter is initialized

View File

@ -29,6 +29,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
var episodesCrunchy = NoneEpisodes var episodesCrunchy = NoneEpisodes
internal set internal set
val currentEpisodesCrunchy = arrayListOf<Episode>() // used for EpisodeItemAdapter (easier updates) val currentEpisodesCrunchy = arrayListOf<Episode>() // used for EpisodeItemAdapter (easier updates)
var currentPlayheads: PlayheadsMap = emptyMap()
var isWatchlist = false var isWatchlist = false
internal set internal set
@ -66,6 +67,10 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
currentEpisodesCrunchy.addAll(episodesCrunchy.items) currentEpisodesCrunchy.addAll(episodesCrunchy.items)
println("episodes: $episodesCrunchy") println("episodes: $episodesCrunchy")
// get playheads (including fully watched state)
val episodeIDs = episodesCrunchy.items.map { it.id }
currentPlayheads = Crunchyroll.playheads(episodeIDs)
// set media type // set media type
mediaType = episodesCrunchy.items.firstOrNull()?.let { mediaType = episodesCrunchy.items.firstOrNull()?.let {
if (it.episodeNumber != null) MediaType.TVSHOW else MediaType.MOVIE if (it.episodeNumber != null) MediaType.TVSHOW else MediaType.MOVIE

View File

@ -2,8 +2,10 @@ package org.mosad.teapod.util.adapter
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
@ -11,9 +13,14 @@ import jp.wasabeef.glide.transformations.RoundedCornersTransformation
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.ItemEpisodeBinding import org.mosad.teapod.databinding.ItemEpisodeBinding
import org.mosad.teapod.parser.crunchyroll.Episode import org.mosad.teapod.parser.crunchyroll.Episode
import org.mosad.teapod.parser.crunchyroll.PlayheadsMap
import org.mosad.teapod.util.tmdb.TMDBTVEpisode import org.mosad.teapod.util.tmdb.TMDBTVEpisode
class EpisodeItemAdapter(private val episodes: List<Episode>, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() { class EpisodeItemAdapter(
private val episodes: List<Episode>,
private val tmdbEpisodes: List<TMDBTVEpisode>?,
private val playheads: PlayheadsMap
) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null
@ -53,16 +60,13 @@ class EpisodeItemAdapter(private val episodes: List<Episode>, private val tmdbEp
.into(holder.binding.imageEpisode) .into(holder.binding.imageEpisode)
} }
// TODO // add watched icon to episode, if the episode id is present in playheads and fullyWatched
// if (ep.watched) { val watchedImage: Drawable? = if (playheads[ep.id]?.fullyWatched == true) {
// holder.binding.imageWatched.setImageDrawable( ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
// ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24) } else {
// ) null
// } else { }
// holder.binding.imageWatched.setImageDrawable(null) holder.binding.imageWatched.setImageDrawable(watchedImage)
// }
// disable watched icon until implemented
holder.binding.imageWatched.setImageDrawable(null)
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {