migrate more Crunchyroll API endpoints to v2

This commit is contained in:
Jannik 2023-07-21 17:22:45 +02:00
parent 0662d656ac
commit 59a457430e
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
5 changed files with 93 additions and 71 deletions

View File

@ -58,7 +58,7 @@ object Crunchyroll {
private lateinit var token: Token private lateinit var token: Token
private var tokenValidUntil: Long = 0 private var tokenValidUntil: Long = 0
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext") private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
private var accountID = "" private var accountID = ""
@ -260,26 +260,30 @@ object Crunchyroll {
/** /**
* Browse the media available on crunchyroll. * Browse the media available on crunchyroll.
* *
* TODO migrate to v2 * @param start start of the item list, used for pagination, default = 0
* * @param n number of items to return, default = 10
* @param sortBy * @param sortBy the sort order, see **[SortBy]**
* @param n Number of items to return, defaults to 10 * @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. * @return A **[BrowseResult]** object is returned.
*/ */
suspend fun browse( suspend fun browse(
categories: List<Categories> = emptyList(),
sortBy: SortBy = SortBy.ALPHABETICAL,
seasonTag: String = "",
start: Int = 0, start: Int = 0,
n: Int = 10 n: Int = 10,
sortBy: SortBy = SortBy.ALPHABETICAL,
ratings: Boolean = false,
seasonTag: String = "",
categories: List<Categories> = emptyList()
): BrowseResult { ): BrowseResult {
val browseEndpoint = "/content/v1/browse" val browseEndpoint = "/content/v2/discover/browse"
val parameters = mutableListOf( val parameters = mutableListOf(
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"sort_by" to sortBy.str,
"start" to start, "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 // if a season tag is present add it to the parameters
@ -304,9 +308,10 @@ object Crunchyroll {
NoneBrowseResult 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 // 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) { if (browsingCache.size > 10) {
browsingCache.clear() browsingCache.clear()
} }
@ -322,19 +327,20 @@ object Crunchyroll {
* Search fo a query term. * Search fo a query term.
* Note: currently this function only supports series/tv shows. * Note: currently this function only supports series/tv shows.
* *
* TODO migrate to v2
*
* @param query The query term as String * @param query The query term as String
* @param n The maximum number of results to return, default = 10 * @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 * @return A **[SearchResult]** object
*/ */
suspend fun search(query: String, n: Int = 10): SearchResult { suspend fun search(query: String, n: Int = 10, ratings: Boolean = false): SearchResult {
val searchEndpoint = "/content/v1/search" val searchEndpoint = "/content/v2/discover/search"
val parameters = listOf( val parameters = listOf(
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"q" to query, "q" to query,
"n" to n, "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, // 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 * 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
* @param ratings the user rating of the object * @param ratings add user rating to the objects
* @return A **[Collection]** of Panels * @return A **[Collection]** of Panels
*/ */
suspend fun objects(objects: List<String>, ratings: Boolean = false): Collection2<Item> { suspend fun objects(objects: List<String>, ratings: Boolean = false): CollectionV2<Item> {
val episodesEndpoint = "/content/v2/cms/objects/${objects.joinToString(",")}" val episodesEndpoint = "/content/v2/cms/objects/${objects.joinToString(",")}"
val parameters = listOf( val parameters = listOf(
"ratings" to ratings, "ratings" to ratings,
@ -368,7 +374,7 @@ object Crunchyroll {
requestGet(episodesEndpoint, parameters) requestGet(episodesEndpoint, parameters)
} catch (ex: Exception) { } catch (ex: Exception) {
Log.e(TAG, "Exception in objects().", ex) Log.e(TAG, "Exception in objects().", ex)
NoneCollection2 NoneCollectionV2
} }
} }
@ -509,7 +515,7 @@ object Crunchyroll {
) )
return try { return try {
(requestGet(watchlistSeriesEndpoint, parameters) as Collection2<IsWatchlistItem>) (requestGet(watchlistSeriesEndpoint, parameters) as CollectionV2<IsWatchlistItem>)
.total == 1 .total == 1
} catch (ex: Exception) { } catch (ex: Exception) {
Log.e(TAG, "Exception in isWatchlist() with seriesId $seriesId", ex) 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 seriesId The crunchyroll series id of the media
* @param n The maximum number of results to return, default = 10 * @param n The maximum number of results to return, default = 10
* @param ratings add user rating to the objects
* @return A **[SimilarToResult]** object * @return A **[SimilarToResult]** object
*/ */
suspend fun similarTo(seriesId: String, n: Int = 10): SimilarToResult { suspend fun similarTo(seriesId: String, n: Int = 10, ratings: Boolean = false): SimilarToResult {
val similarToEndpoint = "/content/v1/$accountID/similar_to" val similarToEndpoint = "/content/v2/discover/$accountID/similar_to/$seriesId"
val parameters = listOf( val parameters = listOf(
"guid" to seriesId, "n" to n,
"ratings" to ratings,
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"n" to n
) )
return try { return try {
@ -659,7 +667,7 @@ object Crunchyroll {
* @param n Number of items to return, defaults to 20. * @param n Number of items to return, defaults to 20.
* @return A **[Collection]** containing up to n **[Item]**. * @return A **[Collection]** containing up to n **[Item]**.
*/ */
suspend fun watchlist(n: Int = 20): Collection2<Item> { suspend fun watchlist(n: Int = 20): CollectionV2<Item> {
val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist" val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist"
val parameters = listOf( val parameters = listOf(
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), "locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
@ -681,10 +689,10 @@ object Crunchyroll {
/** /**
* List the next up episodes for the logged in account. * 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]**. * @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 watchlistEndpoint = "/content/v2/discover/$accountID/history"
val parameters = listOf( val parameters = listOf(
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(), "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( val parameters = listOf(
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"n" to n,
"start" to start, "start" to start,
"variant_id" to 0 "n" to n,
"ratings" to ratings,
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
) )
return try { return try {

View File

@ -24,7 +24,7 @@ package org.mosad.teapod.parser.crunchyroll
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.* import java.util.Locale
val supportedLocals = listOf( val supportedLocals = listOf(
Locale.forLanguageTag("ar-SA"), Locale.forLanguageTag("ar-SA"),
@ -44,6 +44,10 @@ val supportedLocals = listOf(
* data classes for browse * data classes for browse
* TODO make class names more clear/possibly overlapping for now * TODO make class names more clear/possibly overlapping for now
*/ */
/**
* Enum of all supported sorting orders.
*/
enum class SortBy(val str: String) { enum class SortBy(val str: String) {
ALPHABETICAL("alphabetical"), ALPHABETICAL("alphabetical"),
NEWLY_ADDED("newly_added"), NEWLY_ADDED("newly_added"),
@ -112,23 +116,22 @@ val NoneAccount = Account("", "", false, "")
*/ */
@Serializable @Serializable
data class Collection<T>( data class CollectionV1<T>(
@SerialName("total") val total: Int, @SerialName("total") val total: Int,
@SerialName("items") val items: List<T> @SerialName("items") val items: List<T>
) )
@Serializable @Serializable
data class Collection2<T>( data class CollectionV2<T>(
@SerialName("total") val total: Int, @SerialName("total") val total: Int,
@SerialName("data") val data: List<T> @SerialName("data") val data: List<T>
) )
typealias SearchResult = Collection<SearchCollection> typealias SearchResult = CollectionV2<SearchTypedList<Item>>
typealias SearchCollection = Collection<Item> typealias BrowseResult = CollectionV2<Item>
typealias BrowseResult = Collection<Item> typealias SimilarToResult = CollectionV2<Item>
typealias SimilarToResult = Collection<Item> typealias RecommendationsList = CollectionV2<Item>
typealias RecommendationsList = Collection<Item> typealias Benefits = CollectionV1<Benefit>
typealias Benefits = Collection<Benefit>
/** /**
* panel data classes * 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 * up next & watchlist data classes
*/ */
typealias Watchlist = Collection2<WatchlistItem> typealias Watchlist = CollectionV2<WatchlistItem>
typealias HistoryList = Collection2<UpNextAccountItem> typealias HistoryList = CollectionV2<UpNextAccountItem>
typealias UpNextSeriesList = Collection2<UpNextSeriesItem> typealias UpNextSeriesList = CollectionV2<UpNextSeriesItem>
@Serializable @Serializable
data class WatchlistItem( data class WatchlistItem(
@ -221,8 +224,7 @@ data class EpisodeMetadata(
@SerialName("series_title") val seriesTitle: String, @SerialName("series_title") val seriesTitle: String,
) )
val NoneCollection = Collection<Item>(0, emptyList()) val NoneCollectionV2 = CollectionV2<Item>(0, emptyList())
val NoneCollection2 = Collection2<Item>(0, emptyList())
val NoneSearchResult = SearchResult(0, emptyList()) val NoneSearchResult = SearchResult(0, emptyList())
val NoneBrowseResult = BrowseResult(0, emptyList()) val NoneBrowseResult = BrowseResult(0, emptyList())
val NoneSimilarToResult = SimilarToResult(0, emptyList()) val NoneSimilarToResult = SimilarToResult(0, emptyList())
@ -236,7 +238,7 @@ val NoneBenefits = Benefits(0, emptyList())
* series data class * series data class
*/ */
typealias Series = Collection2<SeriesItem> typealias Series = CollectionV2<SeriesItem>
@Serializable @Serializable
data class SeriesItem( data class SeriesItem(
@ -354,7 +356,7 @@ val NoneVersion = Version(
variant = "" variant = ""
) )
typealias Playheads = Collection2<PlayheadObject> typealias Playheads = CollectionV2<PlayheadObject>
@Serializable @Serializable
data class PlayheadObject( data class PlayheadObject(
@ -450,7 +452,18 @@ data class Benefit(
@SerialName("benefit") val benefit: String, @SerialName("benefit") val benefit: String,
@SerialName("source") val source: String, @SerialName("source") val source: String,
) )
@Suppress("unused")
val NoneBenefit = Benefit( val NoneBenefit = Benefit(
benefit = "", benefit = "",
source = "" source = ""
) )
/**
* search result typed list data class
*/
@Serializable
data class SearchTypedList<T>(
@SerialName("type") val type: String,
@SerialName("count") val count: Int,
@SerialName("items") val items: List<T>
)

View File

@ -66,16 +66,16 @@ class HomeViewModel : ViewModel() {
uiState.emit(UiState.Loading) uiState.emit(UiState.Loading)
try { try {
// run the loading in parallel to speed up the process // 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 watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).data }
val recommendationsJob = viewModelScope.async { val recommendationsJob = viewModelScope.async {
Crunchyroll.recommendations(20).items Crunchyroll.recommendations(n = 20).data
} }
val recentlyAddedJob = viewModelScope.async { 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 { val topTenJob = viewModelScope.async {
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).items Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).data
} }
val recentlyAddedItems = recentlyAddedJob.await() val recentlyAddedItems = recentlyAddedJob.await()
@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
uiState.update { currentUiState -> uiState.update { currentUiState ->
if (currentUiState is UiState.Normal) { if (currentUiState is UiState.Normal) {
val upNextItems = Crunchyroll.upNextAccount().data val upNextItems = Crunchyroll.upNextAccount(n = 20).data
currentUiState.copy(upNextItems = upNextItems) currentUiState.copy(upNextItems = upNextItems)
} else { } else {
currentUiState currentUiState

View File

@ -90,7 +90,7 @@ class LibraryFragmentViewModel : ViewModel() {
delay(250) delay(250)
val results = Crunchyroll.search(query, 50) val results = Crunchyroll.search(query, 50)
.items.firstOrNull()?.items?.toItemMediaList() .data.firstOrNull()?.items?.toItemMediaList()
?: listOf() ?: listOf()
uiState.emit(UiState.Search(results)) uiState.emit(UiState.Search(results))
} }

View File

@ -9,12 +9,11 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.parser.crunchyroll.Collection import org.mosad.teapod.parser.crunchyroll.CollectionV2
import org.mosad.teapod.parser.crunchyroll.Collection2
import org.mosad.teapod.parser.crunchyroll.Item import org.mosad.teapod.parser.crunchyroll.Item
import org.mosad.teapod.parser.crunchyroll.PlayheadObject import org.mosad.teapod.parser.crunchyroll.PlayheadObject
import org.mosad.teapod.ui.activity.player.PlayerActivity 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. * Create a Intent for PlayerActivity with season and episode id.
@ -36,13 +35,7 @@ fun <T> concatenate(vararg lists: List<T>): List<T> {
} }
// TODO move to correct location // TODO move to correct location
fun Collection<Item>.toItemMediaList(): List<ItemMedia> { fun CollectionV2<Item>.toItemMediaList(): List<ItemMedia> {
return this.items.map {
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
}
}
fun Collection2<Item>.toItemMediaList(): List<ItemMedia> {
return this.data.map { return this.data.map {
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source) ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
} }
@ -65,7 +58,7 @@ fun Locale.toDisplayString(fallback: String): String {
} }
} }
fun Collection2<PlayheadObject>.toPlayheadsMap(): Map<String, PlayheadObject> { fun CollectionV2<PlayheadObject>.toPlayheadsMap(): Map<String, PlayheadObject> {
return this.data.associateBy { it.contentId } return this.data.associateBy { it.contentId }
} }