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 87d8da8..be456da 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 @@ -14,6 +14,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.mosad.teapod.preferences.Preferences +import org.mosad.teapod.util.concatenate import java.util.* private val json = Json { ignoreUnknownKeys = true } @@ -179,9 +180,21 @@ object Crunchyroll { * * @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 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 browseResult = result.component1()?.obj()?.let { @@ -216,7 +229,7 @@ object Crunchyroll { * Note: episode objects are currently not supported * * @param objects The object IDs as list of Strings - * @return A Collection of Panels + * @return A **[Collection]** of Panels */ suspend fun objects(objects: List): Collection { val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}" @@ -228,7 +241,6 @@ object Crunchyroll { ) val result = request(episodesEndpoint, parameters) - println(result.component1()?.obj()?.toString()) return result.component1()?.obj()?.let { json.decodeFromString(it.toString()) @@ -304,7 +316,7 @@ object Crunchyroll { * Check if a media is in the user's watchlist. * * @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 { 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]** containing playback info. */ - suspend fun playhead() { - // implement + suspend fun playheads(episodeIDs: List): PlayheadsMap { + 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 */ + /** + * 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 { val watchlistEndpoint = "/content/v1/$accountID/watchlist" 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 { val watchlistEndpoint = "/content/v1/$accountID/up_next_account" val parameters = listOf("locale" to locale, "n" to n) val resultUpNextAccount = request(watchlistEndpoint, parameters) - val list: ContinueWatchingList = resultUpNextAccount.component1()?.obj()?.let { + return resultUpNextAccount.component1()?.obj()?.let { json.decodeFromString(it.toString()) } ?: NoneContinueWatchingList - -// val objects = list.items.map{ it.panel.episodeMetadata.seriesId } -// return objects(objects) - return list } } 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 3690e58..0c08afe 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 @@ -29,7 +29,6 @@ data class Collection( typealias SearchCollection = Collection typealias BrowseResult = Collection typealias Watchlist = Collection -typealias UpNextAccount = Collection @Serializable data class SearchResult( @@ -112,7 +111,6 @@ data class Series( ) val NoneSeries = Series("", "", "", Images(emptyList(), emptyList()), emptyList()) - /** * Seasons data type */ @@ -209,6 +207,16 @@ val NoneEpisode = Episode( playback = "" ) +typealias PlayheadsMap = Map + +@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 */ 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 f680356..4a43ded 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 @@ -31,7 +31,11 @@ class MediaFragmentEpisodes : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - adapterRecEpisodes = EpisodeItemAdapter(model.currentEpisodesCrunchy, model.tmdbTVSeason.episodes) + adapterRecEpisodes = EpisodeItemAdapter( + model.currentEpisodesCrunchy, + model.tmdbTVSeason.episodes, + model.currentPlayheads + ) binding.recyclerEpisodes.adapter = adapterRecEpisodes // set onItemClick, adapter is initialized 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 5372d45..b6f0afd 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 @@ -29,6 +29,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic var episodesCrunchy = NoneEpisodes internal set val currentEpisodesCrunchy = arrayListOf() // used for EpisodeItemAdapter (easier updates) + var currentPlayheads: PlayheadsMap = emptyMap() var isWatchlist = false internal set @@ -66,6 +67,10 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic 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 diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt index 2220c60..f4c9368 100644 --- a/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt +++ b/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt @@ -2,8 +2,10 @@ package org.mosad.teapod.util.adapter import android.graphics.Color import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide 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.databinding.ItemEpisodeBinding import org.mosad.teapod.parser.crunchyroll.Episode +import org.mosad.teapod.parser.crunchyroll.PlayheadsMap import org.mosad.teapod.util.tmdb.TMDBTVEpisode -class EpisodeItemAdapter(private val episodes: List, private val tmdbEpisodes: List?) : RecyclerView.Adapter() { +class EpisodeItemAdapter( + private val episodes: List, + private val tmdbEpisodes: List?, + private val playheads: PlayheadsMap +) : RecyclerView.Adapter() { var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null @@ -53,16 +60,13 @@ class EpisodeItemAdapter(private val episodes: List, private val tmdbEp .into(holder.binding.imageEpisode) } - // TODO -// if (ep.watched) { -// holder.binding.imageWatched.setImageDrawable( -// ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24) -// ) -// } else { -// holder.binding.imageWatched.setImageDrawable(null) -// } - // disable watched icon until implemented - holder.binding.imageWatched.setImageDrawable(null) + // add watched icon to episode, if the episode id is present in playheads and fullyWatched + val watchedImage: Drawable? = if (playheads[ep.id]?.fullyWatched == true) { + ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24) + } else { + null + } + holder.binding.imageWatched.setImageDrawable(watchedImage) } override fun getItemCount(): Int {