Compare commits
85 Commits
1.0.0-beta
...
develop
Author | SHA1 | Date |
---|---|---|
|
7d661712f7 | |
|
8fcf047e99 | |
|
17dbe945e5 | |
|
5f609d4c33 | |
|
6515f657d0 | |
|
c448b44fc4 | |
|
88ebc378d3 | |
|
1a012cba7d | |
|
59a457430e | |
|
0662d656ac | |
|
3549a3d2a7 | |
|
c89ae54929 | |
|
3aa03783a9 | |
|
4bceacf75c | |
|
cf02bee7d4 | |
|
01d026cc7f | |
|
7580093649 | |
|
f266731115 | |
|
a6a23c8560 | |
|
2cb05de810 | |
|
5cf4527a92 | |
|
14ad34138c | |
|
47e1f6bd49 | |
|
fdcb76e26e | |
|
7004d73b9f | |
|
a13eb15adf | |
|
d40ab9519c | |
|
2e7db26d1d | |
|
8b7fb3ac5f | |
|
097383a082 | |
|
9380f98098 | |
|
e0f05169f5 | |
|
e113a9c795 | |
|
8e397e13d2 | |
|
31e7adac03 | |
|
63f5e69094 | |
|
bf6f2d916e | |
|
81a20e0aa9 | |
|
ed8f3fdcda | |
|
fffbeaeb49 | |
|
21caa8eb1b | |
|
bbc819551b | |
|
2004a3f483 | |
|
0a31c2fd88 | |
|
f49b5a2730 | |
|
a95813e91e | |
|
8bdaa8122b | |
|
e2ea0a364e | |
|
777c6e0212 | |
|
71d5c58653 | |
|
6624e71228 | |
|
d33de371d1 | |
|
1ecd25bb06 | |
|
fa28eb35ab | |
|
d3fe81224b | |
|
34c7f9d081 | |
|
e835715b9c | |
|
001141337d | |
|
5cd3d25ebe | |
|
215e01c53a | |
|
1751963574 | |
|
9c3548a866 | |
|
ebd96f9849 | |
|
85b17d7a76 | |
|
f128efea0d | |
|
da94003368 | |
|
3fdc2aff1b | |
|
326da147f1 | |
|
f398c82f62 | |
|
821f8b5590 | |
|
0028cb6dd7 | |
|
127bd030b9 | |
|
3cadaa5c7a | |
|
97966f5ad3 | |
|
4c55bb771f | |
|
8eb737a831 | |
|
522b893dc8 | |
|
69e0b6bcca | |
|
c34b95795f | |
|
9059306e90 | |
|
ed0c0a4c61 | |
|
03a79346b7 | |
|
19552d3950 | |
|
49e0b1ec29 | |
|
af66d968cc |
|
@ -26,4 +26,4 @@ Currently you need to have an Crunchyroll account to contribute to Teapod. Contr
|
|||
#### Why is it called Teapod?
|
||||
Teapod is a Acronym for "The ultimate anime app on demand", hence this project is called Teapod and not Teapot.
|
||||
|
||||
Teapod © 2020-2022 [@Seil0](https://git.mosad.xyz/Seil0)
|
||||
Teapod © 2020-2023 [@Seil0](https://git.mosad.xyz/Seil0)
|
||||
|
|
|
@ -4,16 +4,23 @@ plugins {
|
|||
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain 11
|
||||
sourceSets.configureEach {
|
||||
languageSettings.optIn("kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "30.0.3"
|
||||
compileSdk 34
|
||||
buildToolsVersion = '34.0.0'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.mosad.teapod"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 31
|
||||
versionCode 9010 //00.09.010
|
||||
versionName "1.0.0-beta2"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 100992 //01.00.000
|
||||
versionName "1.1.0-beta3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resValue "string", "build_time", buildTime()
|
||||
|
@ -32,53 +39,46 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
kotlin.sourceSets.all {
|
||||
languageSettings.optIn("kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
namespace 'org.mosad.teapod'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-rc01'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.4.2'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation "com.google.android.exoplayer:exoplayer-core:$exo_version"
|
||||
implementation "com.google.android.exoplayer:exoplayer-hls:$exo_version"
|
||||
implementation "com.google.android.exoplayer:exoplayer-dash:$exo_version"
|
||||
implementation "com.google.android.exoplayer:exoplayer-ui:$exo_version"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:$exo_version"
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.13.1'
|
||||
implementation 'com.facebook.shimmer:shimmer:0.5.0'
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.15.1'
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-android:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-serialization:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
|
||||
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,9 @@
|
|||
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
#misc
|
||||
-dontwarn java.lang.instrument.ClassFileTransformer
|
||||
-dontwarn java.lang.ClassValue
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme.Dark">
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||
|
|
|
@ -25,15 +25,15 @@ package org.mosad.teapod.parser.crunchyroll
|
|||
import android.util.Log
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.features.*
|
||||
import io.ktor.client.features.json.*
|
||||
import io.ktor.client.features.json.serializer.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
|
@ -41,30 +41,28 @@ import kotlinx.serialization.json.put
|
|||
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
object Crunchyroll {
|
||||
private val TAG = javaClass.name
|
||||
|
||||
private val client = HttpClient {
|
||||
install(JsonFeature) {
|
||||
serializer = KotlinxSerializer(json)
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
}
|
||||
private const val baseUrl = "https://beta-api.crunchyroll.com"
|
||||
private const val staticUrl = "https://static.crunchyroll.com"
|
||||
private const val basicApiTokenUrl = "https://gitlab.com/-/snippets/2274956/raw/main/snippetfile1.txt"
|
||||
private var basicApiToken: String = ""
|
||||
|
||||
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 = ""
|
||||
|
||||
private var policy = ""
|
||||
private var signature = ""
|
||||
private var keyPairID = ""
|
||||
private var externalID = ""
|
||||
|
||||
private val browsingCache = hashMapOf<String, BrowseResult>()
|
||||
|
||||
|
@ -76,7 +74,7 @@ object Crunchyroll {
|
|||
*/
|
||||
fun initBasicApiToken() = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
basicApiToken = (client.get(basicApiTokenUrl) as HttpResponse).readText()
|
||||
basicApiToken = client.get { url(basicApiTokenUrl) }.bodyAsText()
|
||||
Log.i(TAG, "basic auth token: $basicApiToken")
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +104,7 @@ object Crunchyroll {
|
|||
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
|
||||
header("Authorization", "Basic $basicApiToken")
|
||||
}
|
||||
token = response.receive()
|
||||
token = response.body()
|
||||
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
|
||||
response.status
|
||||
} catch (ex: ClientRequestException) {
|
||||
|
@ -145,7 +143,7 @@ object Crunchyroll {
|
|||
}
|
||||
|
||||
return@coroutineScope (Dispatchers.IO) {
|
||||
val response: T = client.request(url) {
|
||||
val response = client.request(url) {
|
||||
method = httpMethod
|
||||
header("Authorization", "${token.tokenType} ${token.accessToken}")
|
||||
params.forEach {
|
||||
|
@ -154,21 +152,24 @@ object Crunchyroll {
|
|||
|
||||
// for json set body and content type
|
||||
if (bodyObject is JsonObject) {
|
||||
body = bodyObject
|
||||
setBody(bodyObject)
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
response.body<T>()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTTP GET request with [params] to the [endpoint] at [url], if url is empty use baseUrl
|
||||
*/
|
||||
private suspend inline fun <reified T> requestGet(
|
||||
endpoint: String,
|
||||
params: List<Pair<String, Any?>> = listOf(),
|
||||
url: String = ""
|
||||
): T {
|
||||
val path = url.ifEmpty { "$baseUrl$endpoint" }
|
||||
val path = url.ifEmpty { baseUrl }.plus(endpoint)
|
||||
|
||||
return request(path, HttpMethod.Get, params)
|
||||
}
|
||||
|
@ -207,27 +208,10 @@ object Crunchyroll {
|
|||
}
|
||||
|
||||
/**
|
||||
* Basic functions: index, account
|
||||
* Basic functions: account
|
||||
* Needed for other functions to work properly!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieve the identifiers necessary for streaming. If the identifiers are
|
||||
* retrieved, set the corresponding global var. The identifiers are valid for 24h.
|
||||
*/
|
||||
suspend fun index() {
|
||||
val indexEndpoint = "/index/v2"
|
||||
|
||||
val index: Index = requestGet(indexEndpoint)
|
||||
policy = index.cms.policy
|
||||
signature = index.cms.signature
|
||||
keyPairID = index.cms.keyPairId
|
||||
|
||||
Log.i(TAG, "Policy : $policy")
|
||||
Log.i(TAG, "Signature : $signature")
|
||||
Log.i(TAG, "Key Pair ID : $keyPairID")
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the account id and set the corresponding global var.
|
||||
* The account id is needed for other calls.
|
||||
|
@ -239,12 +223,13 @@ object Crunchyroll {
|
|||
|
||||
val account: Account = try {
|
||||
requestGet(indexEndpoint)
|
||||
} catch (ex: SerializationException) {
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "SerializationException in account(). This is bad!", ex)
|
||||
NoneAccount
|
||||
}
|
||||
|
||||
accountID = account.accountId
|
||||
externalID = account.externalId
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -254,24 +239,30 @@ object Crunchyroll {
|
|||
/**
|
||||
* Browse the media available on crunchyroll.
|
||||
*
|
||||
* @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<Categories> = 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<Categories> = emptyList()
|
||||
): BrowseResult {
|
||||
val browseEndpoint = "/content/v1/browse"
|
||||
val browseEndpoint = "/content/v2/discover/browse"
|
||||
val parameters = mutableListOf(
|
||||
"locale" to Preferences.preferredLocale.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
|
||||
|
@ -291,14 +282,16 @@ object Crunchyroll {
|
|||
Log.d(TAG, "browse result not cached, fetching: $parameters")
|
||||
val browseResult: BrowseResult = try {
|
||||
requestGet(browseEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
}catch (ex: Exception) {
|
||||
Log.e(TAG, "SerializationException in browse().", ex)
|
||||
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
|
||||
if (browsingCache.size > 100) {
|
||||
if (browsingCache.size > 10) {
|
||||
browsingCache.clear()
|
||||
}
|
||||
|
||||
|
@ -315,15 +308,18 @@ object Crunchyroll {
|
|||
*
|
||||
* @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.preferredLocale.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,
|
||||
|
@ -331,8 +327,8 @@ object Crunchyroll {
|
|||
|
||||
return try {
|
||||
requestGet(searchEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in search(), with query = \"$query\".", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in search(), with query = \"$query\".", ex)
|
||||
NoneSearchResult
|
||||
}
|
||||
}
|
||||
|
@ -342,38 +338,22 @@ object Crunchyroll {
|
|||
* Note: episode objects are currently not supported
|
||||
*
|
||||
* @param objects The object IDs as list of Strings
|
||||
* @param ratings add user rating to the objects
|
||||
* @return A **[Collection]** of Panels
|
||||
*/
|
||||
suspend fun objects(objects: List<String>): Collection<Item> {
|
||||
val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}"
|
||||
suspend fun objects(objects: List<String>, ratings: Boolean = false): CollectionV2<Item> {
|
||||
val episodesEndpoint = "/content/v2/cms/objects/${objects.joinToString(",")}"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
"ratings" to ratings,
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(episodesEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in objects().", ex)
|
||||
NoneCollection
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available seasons as **[SeasonListItem]**.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
suspend fun seasonList(): DiscSeasonList {
|
||||
val seasonListEndpoint = "/content/v1/season_list"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
|
||||
return try {
|
||||
requestGet(seasonListEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in seasonList().", ex)
|
||||
NoneDiscSeasonList
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in objects().", ex)
|
||||
NoneCollectionV2
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,18 +365,16 @@ object Crunchyroll {
|
|||
* series id == crunchyroll id?
|
||||
*/
|
||||
suspend fun series(seriesId: String): Series {
|
||||
val seriesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/series/$seriesId"
|
||||
val seriesEndpoint = "/content/v2/cms/series/$seriesId"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(seriesEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in series().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in series() for id $seriesId.", ex)
|
||||
NoneSeries
|
||||
}
|
||||
}
|
||||
|
@ -404,21 +382,29 @@ object Crunchyroll {
|
|||
/**
|
||||
* Get the next episode for a series.
|
||||
*
|
||||
* FIXME up_next returns no content if the is no next episode
|
||||
*
|
||||
* @param seriesId The series id for which to call up next
|
||||
* @return A **[UpNextSeriesItem]** with a Panel representing the up next episode
|
||||
*/
|
||||
suspend fun upNextSeries(seriesId: String): UpNextSeriesItem {
|
||||
val upNextSeriesEndpoint = "/content/v1/up_next_series"
|
||||
suspend fun upNextSeries(seriesId: String): UpNextSeriesList {
|
||||
val upNextSeriesEndpoint = "/content/v2/discover/up_next/$seriesId"
|
||||
val parameters = listOf(
|
||||
"series_id" to seriesId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag()
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(upNextSeriesEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in upNextSeries().", ex)
|
||||
NoneUpNextSeriesItem
|
||||
} catch (ex: NoTransformationFoundException) {
|
||||
// should be 204 No Content
|
||||
NoneUpNextSeriesList
|
||||
} catch (ex: JsonConvertException) {
|
||||
Log.e(TAG, "JsonConvertException in upNextSeries() with seriesId=$seriesId", ex)
|
||||
NoneUpNextSeriesList
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in upNextSeries() for seriesId $seriesId.", ex)
|
||||
NoneUpNextSeriesList
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -429,19 +415,16 @@ object Crunchyroll {
|
|||
* @return A **[Seasons]** object with a list of **[Season]**
|
||||
*/
|
||||
suspend fun seasons(seriesId: String): Seasons {
|
||||
val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons"
|
||||
val seasonsEndpoint = "/content/v2/cms/series/$seriesId/seasons"
|
||||
val parameters = listOf(
|
||||
"series_id" to seriesId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(seasonsEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in seasons().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in seasons() for seriesId $seriesId.", ex)
|
||||
NoneSeasons
|
||||
}
|
||||
}
|
||||
|
@ -453,19 +436,16 @@ object Crunchyroll {
|
|||
* @return A **[Episodes]** object with a list of **[Episode]**
|
||||
*/
|
||||
suspend fun episodes(seasonId: String): Episodes {
|
||||
val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes"
|
||||
val episodesEndpoint = "/content/v2/cms/seasons/$seasonId/episodes"
|
||||
val parameters = listOf(
|
||||
"season_id" to seasonId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(episodesEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in episodes().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in episodes() for seasonId $seasonId.", ex)
|
||||
NoneEpisodes
|
||||
}
|
||||
}
|
||||
|
@ -473,18 +453,28 @@ object Crunchyroll {
|
|||
/**
|
||||
* Get all available subtitles and streams of a episode.
|
||||
*
|
||||
* @param url The playback url of a episode
|
||||
* @return A **[Playback]** object
|
||||
* @param url The streams url of a episode
|
||||
* @return A **[Streams]** object
|
||||
*/
|
||||
suspend fun playback(url: String): Playback {
|
||||
suspend fun streams(url: String): Streams {
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet("", url = url)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in playback(), with url = $url.", ex)
|
||||
NonePlayback
|
||||
requestGet(url, parameters)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in streams() with url $url.", ex)
|
||||
NoneStreams
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun streamsFromMediaGUID(mediaGUID: String): Streams {
|
||||
val streamsEndpoint = "/content/v2/cms/videos/$mediaGUID/streams"
|
||||
return streams(streamsEndpoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional media functions: watchlist (series), playhead, similar to
|
||||
*/
|
||||
|
@ -496,14 +486,18 @@ object Crunchyroll {
|
|||
* @return **[Boolean]**: ture if it was found, else false
|
||||
*/
|
||||
suspend fun isWatchlist(seriesId: String): Boolean {
|
||||
val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val watchlistSeriesEndpoint = "/content/v2/$accountID/watchlist"
|
||||
val parameters = listOf(
|
||||
"content_ids" to seriesId,
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
(requestGet(watchlistSeriesEndpoint, parameters) as JsonObject)
|
||||
.containsKey(seriesId)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in isWatchlist() with seriesId = $seriesId", ex)
|
||||
(requestGet(watchlistSeriesEndpoint, parameters) as CollectionV2<IsWatchlistItem>)
|
||||
.total == 1
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in isWatchlist() with seriesId $seriesId", ex)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -514,14 +508,21 @@ object Crunchyroll {
|
|||
* @param seriesId The crunchyroll series id of the media to check
|
||||
*/
|
||||
suspend fun postWatchlist(seriesId: String) {
|
||||
val watchlistPostEndpoint = "/content/v1/watchlist/$accountID"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val watchlistPostEndpoint = "/content/v2/$accountID/watchlist"
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
val json = buildJsonObject {
|
||||
put("content_id", seriesId)
|
||||
}
|
||||
|
||||
requestPost(watchlistPostEndpoint, parameters, json)
|
||||
try {
|
||||
requestPost(watchlistPostEndpoint, parameters, json)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in postWatchlist() with seriesId $seriesId", ex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -530,10 +531,17 @@ object Crunchyroll {
|
|||
* @param seriesId The crunchyroll series id of the media to check
|
||||
*/
|
||||
suspend fun deleteWatchlist(seriesId: String) {
|
||||
val watchlistDeleteEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val watchlistDeleteEndpoint = "/content/v2/$accountID/watchlist/$seriesId"
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
requestDelete(watchlistDeleteEndpoint, parameters)
|
||||
try {
|
||||
requestDelete(watchlistDeleteEndpoint, parameters)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in deleteWatchlist() with seriesId $seriesId", ex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -544,18 +552,19 @@ object Crunchyroll {
|
|||
* @param episodeIDs A **[List]** of episodes IDs as strings.
|
||||
* @return A **[Map]**<String, **[PlayheadObject]**> containing playback info.
|
||||
*/
|
||||
suspend fun playheads(episodeIDs: List<String>): PlayheadsMap {
|
||||
val playheadsEndpoint = "/content/v1/playheads/$accountID/${episodeIDs.joinToString(",")}"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
suspend fun playheads(episodeIDs: List<String>): Playheads {
|
||||
val playheadsEndpoint = "/content/v2/$accountID/playheads"
|
||||
val parameters = listOf(
|
||||
"content_ids" to episodeIDs.joinToString(","),
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(playheadsEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in playheads().", ex)
|
||||
emptyMap()
|
||||
} catch (ex: Throwable) {
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in playheads().", ex.cause)
|
||||
emptyMap()
|
||||
NonePlayheads
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -567,7 +576,7 @@ object Crunchyroll {
|
|||
*/
|
||||
suspend fun postPlayheads(episodeId: String, playhead: Int) {
|
||||
val playheadsEndpoint = "/content/v1/playheads/$accountID"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val parameters = listOf("locale" to Preferences.preferredSubtitleLocale.toLanguageTag())
|
||||
|
||||
val json = buildJsonObject {
|
||||
put("content_id", episodeId)
|
||||
|
@ -576,30 +585,53 @@ object Crunchyroll {
|
|||
|
||||
try {
|
||||
requestPost(playheadsEndpoint, parameters, json)
|
||||
} catch (ex: Throwable) {
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in postPlayheads()", ex.cause)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the intro meta data including start, end and duration of the intro.
|
||||
*
|
||||
* @param episodeId A episode ID as strings.
|
||||
*/
|
||||
suspend fun datalabIntro(episodeId: String): DatalabIntro {
|
||||
val datalabIntroEndpoint = "/datalab-intro-v2/$episodeId.json"
|
||||
|
||||
/*
|
||||
* wtf crunchyroll, why do you return an xml error message when some data is missing,
|
||||
* this is a json endpoint. For fucks sake, return at least a valid json message.
|
||||
*/
|
||||
return try {
|
||||
val response: HttpResponse = requestGet(datalabIntroEndpoint, url = staticUrl)
|
||||
Json.decodeFromString(response.bodyAsText())
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in datalabIntro(). EpisodeId=$episodeId", ex)
|
||||
NoneDatalabIntro
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get similar media for a show/movie.
|
||||
*
|
||||
* @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,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"n" to n
|
||||
"n" to n,
|
||||
"ratings" to ratings,
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(similarToEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in similarTo().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in similarTo().", ex)
|
||||
NoneSimilarToResult
|
||||
}
|
||||
}
|
||||
|
@ -612,60 +644,69 @@ object Crunchyroll {
|
|||
* List items present in the watchlist.
|
||||
*
|
||||
* @param n Number of items to return, defaults to 20.
|
||||
* @return A **[Watchlist]** containing up to n **[Item]**.
|
||||
* @return A **[Collection]** containing up to n **[Item]**.
|
||||
*/
|
||||
suspend fun watchlist(n: Int = 20): Watchlist {
|
||||
val watchlistEndpoint = "/content/v1/$accountID/watchlist"
|
||||
suspend fun watchlist(n: Int = 20): CollectionV2<Item> {
|
||||
val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"n" to n
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"n" to n,
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
val list: ContinueWatchingList = try {
|
||||
val list: Watchlist = try {
|
||||
requestGet(watchlistEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in watchlist().", ex)
|
||||
NoneContinueWatchingList
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in watchlist().", ex)
|
||||
NoneWatchlist
|
||||
}
|
||||
|
||||
val objects = list.items.map{ it.panel.episodeMetadata.seriesId }
|
||||
val objects = list.data.map{ it.panel.episodeMetadata.seriesId }
|
||||
return objects(objects)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]**.
|
||||
* @param n Number of items to return, default = 20
|
||||
* @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**.
|
||||
*/
|
||||
suspend fun upNextAccount(n: Int = 20): ContinueWatchingList {
|
||||
val watchlistEndpoint = "/content/v1/$accountID/up_next_account"
|
||||
suspend fun upNextAccount(n: Int = 10): HistoryList {
|
||||
val watchlistEndpoint = "/content/v2/discover/$accountID/history"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"n" to n
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(watchlistEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in upNextAccount().", ex)
|
||||
NoneContinueWatchingList
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in upNextAccount().", ex)
|
||||
NoneHistoryList
|
||||
}
|
||||
}
|
||||
|
||||
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.preferredLocale.toLanguageTag(),
|
||||
"n" to n,
|
||||
"start" to start,
|
||||
"variant_id" to 0
|
||||
"n" to n,
|
||||
"ratings" to ratings,
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(recommendationsEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in recommendations().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in recommendations().", ex)
|
||||
NoneRecommendationsList
|
||||
}
|
||||
}
|
||||
|
@ -684,8 +725,8 @@ object Crunchyroll {
|
|||
|
||||
return try {
|
||||
requestGet(profileEndpoint)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in profile().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in profile().", ex)
|
||||
NoneProfile
|
||||
}
|
||||
}
|
||||
|
@ -695,7 +736,7 @@ object Crunchyroll {
|
|||
*
|
||||
* @param languageTag the preferred language as language tag
|
||||
*/
|
||||
suspend fun postPrefSubLanguage(languageTag: String) {
|
||||
suspend fun setPreferredSubtitleLanguage(languageTag: String) {
|
||||
val profileEndpoint = "/accounts/v1/me/profile"
|
||||
val json = buildJsonObject {
|
||||
put("preferred_content_subtitle_language", languageTag)
|
||||
|
@ -704,4 +745,34 @@ object Crunchyroll {
|
|||
requestPatch(profileEndpoint, bodyObject = json)
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the preferred content audio language.
|
||||
*
|
||||
* @param languageTag the preferred language as language tag
|
||||
*/
|
||||
suspend fun setPreferredAudioLanguage(languageTag: String) {
|
||||
val profileEndpoint = "/accounts/v1/me/profile"
|
||||
val json = buildJsonObject {
|
||||
put("preferred_content_audio_language", languageTag)
|
||||
}
|
||||
|
||||
requestPatch(profileEndpoint, bodyObject = json)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional profile (benefits) information for the currently logged in account.
|
||||
*
|
||||
* * @return A **[Profile]** object
|
||||
*/
|
||||
suspend fun benefits(): Benefits {
|
||||
val profileEndpoint = "/subs/v1/subscriptions/$externalID/benefits"
|
||||
|
||||
return try {
|
||||
requestGet(profileEndpoint)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in benefits().", ex)
|
||||
NoneBenefits
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,19 +24,47 @@ package org.mosad.teapod.parser.crunchyroll
|
|||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
val supportedLocals = listOf(
|
||||
val supportedAudioLocals = listOf(
|
||||
Locale.forLanguageTag("ar-SA"),
|
||||
Locale.forLanguageTag("ca-ES"),
|
||||
Locale.forLanguageTag("de-DE"),
|
||||
Locale.forLanguageTag("en-US"),
|
||||
Locale.forLanguageTag("en-IN"),
|
||||
Locale.forLanguageTag("es-419"),
|
||||
Locale.forLanguageTag("es-ES"),
|
||||
Locale.forLanguageTag("fr-FR"),
|
||||
Locale.forLanguageTag("hi-IN"),
|
||||
Locale.forLanguageTag("it-IT"),
|
||||
Locale.forLanguageTag("ko-KR"),
|
||||
Locale.forLanguageTag("pl-PL"),
|
||||
Locale.forLanguageTag("pt-BR"),
|
||||
Locale.forLanguageTag("pt-PT"),
|
||||
Locale.forLanguageTag("ru-RU"),
|
||||
Locale.forLanguageTag("ta-IN"),
|
||||
Locale.forLanguageTag("th-TH"),
|
||||
Locale.forLanguageTag("zh-CN"),
|
||||
Locale.forLanguageTag("zh-TW"),
|
||||
Locale.ROOT
|
||||
)
|
||||
|
||||
val supportedSubtitleLocals = listOf(
|
||||
Locale.forLanguageTag("ar-SA"),
|
||||
Locale.forLanguageTag("ca-ES"),
|
||||
Locale.forLanguageTag("de-DE"),
|
||||
Locale.forLanguageTag("en-US"),
|
||||
Locale.forLanguageTag("es-419"),
|
||||
Locale.forLanguageTag("es-ES"),
|
||||
Locale.forLanguageTag("fr-FR"),
|
||||
Locale.forLanguageTag("hi-IN"),
|
||||
Locale.forLanguageTag("it-IT"),
|
||||
Locale.forLanguageTag("ms-MY"),
|
||||
Locale.forLanguageTag("pl-PL"),
|
||||
Locale.forLanguageTag("pt-BR"),
|
||||
Locale.forLanguageTag("pt-PT"),
|
||||
Locale.forLanguageTag("ru-RU"),
|
||||
Locale.forLanguageTag("tr-TR"),
|
||||
Locale.ROOT
|
||||
)
|
||||
|
||||
|
@ -44,6 +72,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,28 +144,23 @@ val NoneAccount = Account("", "", false, "")
|
|||
*/
|
||||
|
||||
@Serializable
|
||||
data class Collection<T>(
|
||||
data class CollectionV1<T>(
|
||||
@SerialName("total") val total: Int,
|
||||
@SerialName("items") val items: List<T>
|
||||
)
|
||||
|
||||
typealias SearchResult = Collection<SearchCollection>
|
||||
typealias SearchCollection = Collection<Item>
|
||||
typealias BrowseResult = Collection<Item>
|
||||
typealias SimilarToResult = Collection<Item>
|
||||
typealias DiscSeasonList = Collection<SeasonListItem>
|
||||
typealias Watchlist = Collection<Item>
|
||||
typealias ContinueWatchingList = Collection<ContinueWatchingItem>
|
||||
typealias RecommendationsList = Collection<Item>
|
||||
|
||||
@Serializable
|
||||
data class UpNextSeriesItem(
|
||||
@SerialName("playhead") val playhead: Int,
|
||||
@SerialName("fully_watched") val fullyWatched: Boolean,
|
||||
@SerialName("never_watched") val neverWatched: Boolean,
|
||||
@SerialName("panel") val panel: EpisodePanel,
|
||||
data class CollectionV2<T>(
|
||||
@SerialName("total") val total: Int,
|
||||
@SerialName("data") val data: List<T>
|
||||
)
|
||||
|
||||
typealias SearchResult = CollectionV2<SearchTypedList<Item>>
|
||||
typealias BrowseResult = CollectionV2<Item>
|
||||
typealias SimilarToResult = CollectionV2<Item>
|
||||
typealias RecommendationsList = CollectionV2<Item>
|
||||
typealias Benefits = CollectionV1<Benefit>
|
||||
|
||||
/**
|
||||
* panel data classes
|
||||
*/
|
||||
|
@ -160,35 +187,45 @@ data class Images(val poster_tall: List<List<Poster>>, val poster_wide: List<Lis
|
|||
data class Poster(val height: Int, val width: Int, val source: String, val type: String)
|
||||
|