From 59a457430e9f0d3c45e01dd1ad75b51c52ac71d1 Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 21 Jul 2023 17:22:45 +0200 Subject: [PATCH] migrate more Crunchyroll API endpoints to v2 --- .../teapod/parser/crunchyroll/Crunchyroll.kt | 92 +++++++++++-------- .../teapod/parser/crunchyroll/DataTypes.kt | 45 +++++---- .../activity/main/viewmodel/HomeViewModel.kt | 10 +- .../viewmodel/LibraryFragmentViewModel.kt | 2 +- .../main/java/org/mosad/teapod/util/Utils.kt | 15 +-- 5 files changed, 93 insertions(+), 71 deletions(-) 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 08464ba..b7c1be5 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 @@ -58,7 +58,7 @@ object Crunchyroll { private lateinit var token: Token private var tokenValidUntil: Long = 0 - @OptIn(DelicateCoroutinesApi::class) + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext") private var accountID = "" @@ -260,26 +260,30 @@ object Crunchyroll { /** * Browse the media available on crunchyroll. * - * TODO migrate to v2 - * - * @param sortBy - * @param n Number of items to return, defaults to 10 - * + * @param start start of the item list, used for pagination, default = 0 + * @param n number of items to return, default = 10 + * @param sortBy the sort order, see **[SortBy]** + * @param ratings add user rating to the objects, default = false + * @param seasonTag filter by season tag, if present + * @param categories filter by category, if present * @return A **[BrowseResult]** object is returned. */ suspend fun browse( - categories: List = emptyList(), - sortBy: SortBy = SortBy.ALPHABETICAL, - seasonTag: String = "", start: Int = 0, - n: Int = 10 + n: Int = 10, + sortBy: SortBy = SortBy.ALPHABETICAL, + ratings: Boolean = false, + seasonTag: String = "", + categories: List = emptyList() ): BrowseResult { - val browseEndpoint = "/content/v1/browse" + val browseEndpoint = "/content/v2/discover/browse" val parameters = mutableListOf( - "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), - "sort_by" to sortBy.str, "start" to start, - "n" to n + "n" to n, + "sort_by" to sortBy.str, + "ratings" to ratings, + "preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(), + "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), ) // if a season tag is present add it to the parameters @@ -304,9 +308,10 @@ object Crunchyroll { NoneBrowseResult } - // if the cache has more than 100 entries clear it, so it doesn't become a memory problem + + + // if the cache has more than 10 entries clear it, so it doesn't become a memory problem // Note: this value is totally guessed and should be replaced by a properly researched value - // TODO 100 is way to high as it's not the number of items but BrowseResults if (browsingCache.size > 10) { browsingCache.clear() } @@ -322,19 +327,20 @@ object Crunchyroll { * Search fo a query term. * Note: currently this function only supports series/tv shows. * - * TODO migrate to v2 - * * @param query The query term as String * @param n The maximum number of results to return, default = 10 + * @param ratings add user rating to the objects, default = false * @return A **[SearchResult]** object */ - suspend fun search(query: String, n: Int = 10): SearchResult { - val searchEndpoint = "/content/v1/search" + suspend fun search(query: String, n: Int = 10, ratings: Boolean = false): SearchResult { + val searchEndpoint = "/content/v2/discover/search" val parameters = listOf( - "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), "q" to query, "n" to n, - "type" to "series" + "type" to "series", + "ratings" to ratings, + "preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(), + "locale" to Preferences.preferredSubtitleLocale.toLanguageTag() ) // TODO episodes have thumbnails as image, and not poster_tall/poster_tall, @@ -353,10 +359,10 @@ object Crunchyroll { * Note: episode objects are currently not supported * * @param objects The object IDs as list of Strings - * @param ratings the user rating of the object + * @param ratings add user rating to the objects * @return A **[Collection]** of Panels */ - suspend fun objects(objects: List, ratings: Boolean = false): Collection2 { + suspend fun objects(objects: List, ratings: Boolean = false): CollectionV2 { val episodesEndpoint = "/content/v2/cms/objects/${objects.joinToString(",")}" val parameters = listOf( "ratings" to ratings, @@ -368,7 +374,7 @@ object Crunchyroll { requestGet(episodesEndpoint, parameters) } catch (ex: Exception) { Log.e(TAG, "Exception in objects().", ex) - NoneCollection2 + NoneCollectionV2 } } @@ -509,7 +515,7 @@ object Crunchyroll { ) return try { - (requestGet(watchlistSeriesEndpoint, parameters) as Collection2) + (requestGet(watchlistSeriesEndpoint, parameters) as CollectionV2) .total == 1 } catch (ex: Exception) { Log.e(TAG, "Exception in isWatchlist() with seriesId $seriesId", ex) @@ -631,14 +637,16 @@ object Crunchyroll { * * @param seriesId The crunchyroll series id of the media * @param n The maximum number of results to return, default = 10 + * @param ratings add user rating to the objects * @return A **[SimilarToResult]** object */ - suspend fun similarTo(seriesId: String, n: Int = 10): SimilarToResult { - val similarToEndpoint = "/content/v1/$accountID/similar_to" + suspend fun similarTo(seriesId: String, n: Int = 10, ratings: Boolean = false): SimilarToResult { + val similarToEndpoint = "/content/v2/discover/$accountID/similar_to/$seriesId" val parameters = listOf( - "guid" to seriesId, + "n" to n, + "ratings" to ratings, + "preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(), "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), - "n" to n ) return try { @@ -659,7 +667,7 @@ object Crunchyroll { * @param n Number of items to return, defaults to 20. * @return A **[Collection]** containing up to n **[Item]**. */ - suspend fun watchlist(n: Int = 20): Collection2 { + suspend fun watchlist(n: Int = 20): CollectionV2 { val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist" val parameters = listOf( "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), @@ -681,10 +689,10 @@ object Crunchyroll { /** * List the next up episodes for the logged in account. * - * @param n Number of items to return, defaults to 20. + * @param n Number of items to return, default = 20 * @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**. */ - suspend fun upNextAccount(n: Int = 20): HistoryList { + suspend fun upNextAccount(n: Int = 10): HistoryList { val watchlistEndpoint = "/content/v2/discover/$accountID/history" val parameters = listOf( "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), @@ -699,13 +707,21 @@ object Crunchyroll { } } - suspend fun recommendations(n: Int = 20, start: Int = 0): RecommendationsList { - val recommendationsEndpoint = "/content/v1/$accountID/recommendations" + /** + * Returns a collection of recommendations for the currently logged in account. + * + * @param start start of the item list, used for pagination, default = 0 + * @param n number of items to return, default = 10 + * @param ratings add user rating to the objects, default = false + * @return A **[RecommendationsList]** containing up to n **[Item]**. + */ + suspend fun recommendations(start: Int = 0, n: Int = 10, ratings: Boolean = false): RecommendationsList { + val recommendationsEndpoint = "/content/v2/discover/$accountID/recommendations" val parameters = listOf( - "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), - "n" to n, "start" to start, - "variant_id" to 0 + "n" to n, + "ratings" to ratings, + "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), ) return try { 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 28f6314..6cc85c4 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 @@ -24,7 +24,7 @@ package org.mosad.teapod.parser.crunchyroll import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import java.util.* +import java.util.Locale val supportedLocals = listOf( Locale.forLanguageTag("ar-SA"), @@ -44,6 +44,10 @@ val supportedLocals = listOf( * data classes for browse * TODO make class names more clear/possibly overlapping for now */ + +/** + * Enum of all supported sorting orders. + */ enum class SortBy(val str: String) { ALPHABETICAL("alphabetical"), NEWLY_ADDED("newly_added"), @@ -112,23 +116,22 @@ val NoneAccount = Account("", "", false, "") */ @Serializable -data class Collection( +data class CollectionV1( @SerialName("total") val total: Int, @SerialName("items") val items: List ) @Serializable -data class Collection2( +data class CollectionV2( @SerialName("total") val total: Int, @SerialName("data") val data: List ) -typealias SearchResult = Collection -typealias SearchCollection = Collection -typealias BrowseResult = Collection -typealias SimilarToResult = Collection -typealias RecommendationsList = Collection -typealias Benefits = Collection +typealias SearchResult = CollectionV2> +typealias BrowseResult = CollectionV2 +typealias SimilarToResult = CollectionV2 +typealias RecommendationsList = CollectionV2 +typealias Benefits = CollectionV1 /** * panel data classes @@ -159,9 +162,9 @@ data class Poster(val height: Int, val width: Int, val source: String, val type: * up next & watchlist data classes */ -typealias Watchlist = Collection2 -typealias HistoryList = Collection2 -typealias UpNextSeriesList = Collection2 +typealias Watchlist = CollectionV2 +typealias HistoryList = CollectionV2 +typealias UpNextSeriesList = CollectionV2 @Serializable data class WatchlistItem( @@ -221,8 +224,7 @@ data class EpisodeMetadata( @SerialName("series_title") val seriesTitle: String, ) -val NoneCollection = Collection(0, emptyList()) -val NoneCollection2 = Collection2(0, emptyList()) +val NoneCollectionV2 = CollectionV2(0, emptyList()) val NoneSearchResult = SearchResult(0, emptyList()) val NoneBrowseResult = BrowseResult(0, emptyList()) val NoneSimilarToResult = SimilarToResult(0, emptyList()) @@ -236,7 +238,7 @@ val NoneBenefits = Benefits(0, emptyList()) * series data class */ -typealias Series = Collection2 +typealias Series = CollectionV2 @Serializable data class SeriesItem( @@ -354,7 +356,7 @@ val NoneVersion = Version( variant = "" ) -typealias Playheads = Collection2 +typealias Playheads = CollectionV2 @Serializable data class PlayheadObject( @@ -450,7 +452,18 @@ data class Benefit( @SerialName("benefit") val benefit: String, @SerialName("source") val source: String, ) +@Suppress("unused") val NoneBenefit = Benefit( benefit = "", source = "" ) + +/** + * search result typed list data class + */ +@Serializable +data class SearchTypedList( + @SerialName("type") val type: String, + @SerialName("count") val count: Int, + @SerialName("items") val items: List +) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/HomeViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/HomeViewModel.kt index 3bfbce0..96c9b68 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/HomeViewModel.kt @@ -66,16 +66,16 @@ class HomeViewModel : ViewModel() { uiState.emit(UiState.Loading) try { // run the loading in parallel to speed up the process - val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().data } + val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount(n = 20).data } val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).data } val recommendationsJob = viewModelScope.async { - Crunchyroll.recommendations(20).items + Crunchyroll.recommendations(n = 20).data } val recentlyAddedJob = viewModelScope.async { - Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).items + Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).data } val topTenJob = viewModelScope.async { - Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).items + Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).data } val recentlyAddedItems = recentlyAddedJob.await() @@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() { viewModelScope.launch { uiState.update { currentUiState -> if (currentUiState is UiState.Normal) { - val upNextItems = Crunchyroll.upNextAccount().data + val upNextItems = Crunchyroll.upNextAccount(n = 20).data currentUiState.copy(upNextItems = upNextItems) } else { currentUiState diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/LibraryFragmentViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/LibraryFragmentViewModel.kt index 45e784b..bc90b8a 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/LibraryFragmentViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/LibraryFragmentViewModel.kt @@ -90,7 +90,7 @@ class LibraryFragmentViewModel : ViewModel() { delay(250) val results = Crunchyroll.search(query, 50) - .items.firstOrNull()?.items?.toItemMediaList() + .data.firstOrNull()?.items?.toItemMediaList() ?: listOf() uiState.emit(UiState.Search(results)) } diff --git a/app/src/main/java/org/mosad/teapod/util/Utils.kt b/app/src/main/java/org/mosad/teapod/util/Utils.kt index 7c7b884..b0be878 100644 --- a/app/src/main/java/org/mosad/teapod/util/Utils.kt +++ b/app/src/main/java/org/mosad/teapod/util/Utils.kt @@ -9,12 +9,11 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.Fragment import org.mosad.teapod.R -import org.mosad.teapod.parser.crunchyroll.Collection -import org.mosad.teapod.parser.crunchyroll.Collection2 +import org.mosad.teapod.parser.crunchyroll.CollectionV2 import org.mosad.teapod.parser.crunchyroll.Item import org.mosad.teapod.parser.crunchyroll.PlayheadObject import org.mosad.teapod.ui.activity.player.PlayerActivity -import java.util.* +import java.util.Locale /** * Create a Intent for PlayerActivity with season and episode id. @@ -36,13 +35,7 @@ fun concatenate(vararg lists: List): List { } // TODO move to correct location -fun Collection.toItemMediaList(): List { - return this.items.map { - ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source) - } -} - -fun Collection2.toItemMediaList(): List { +fun CollectionV2.toItemMediaList(): List { return this.data.map { ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source) } @@ -65,7 +58,7 @@ fun Locale.toDisplayString(fallback: String): String { } } -fun Collection2.toPlayheadsMap(): Map { +fun CollectionV2.toPlayheadsMap(): Map { return this.data.associateBy { it.contentId } }