Compare commits
128 Commits
1.0.0-beta
...
develop
Author | SHA1 | Date | |
---|---|---|---|
0fd7cc964f | |||
b07a6fd407 | |||
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 | |||
ad1e3068cd | |||
de1f19c2b7 | |||
12bbc2ef5f | |||
0186cef79e | |||
bc5509cf93 | |||
ef9a0f00d0 | |||
b85d7ae025 | |||
69c9666d2b | |||
7d6c300f7e | |||
1ebc1194e6 | |||
c48328723b | |||
95c8a72c94 | |||
fc04e8e222 | |||
a898a70653 | |||
58aab72097 | |||
35157b78f5 | |||
c6a00ea061 | |||
80a7fc4398 | |||
dd6ca8b90e | |||
e80e81af0f | |||
f852600dc7 | |||
aa49169034 | |||
7abb5cd3e8 | |||
3a71bdd2c7 | |||
629c144c5b | |||
b2196f11da | |||
5b5a74a1de | |||
7a860a7270 | |||
e97ad9a245 | |||
cf435fdb72 | |||
42895a6fba | |||
eaf1cf78e9 | |||
1af82f8370 | |||
d31a19a4f1 | |||
b27666ee69 | |||
e76cbda04d | |||
7fbf639a70 | |||
ff63b3d7a4 | |||
7d32cecd89 | |||
72280f29d8 | |||
cd4cfb7a0c | |||
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)
|
||||
|
@ -1,20 +1,26 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-android-extensions'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain 17
|
||||
sourceSets.configureEach {
|
||||
languageSettings.optIn("kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
compileSdk 34
|
||||
buildToolsVersion = '34.0.0'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.mosad.teapod"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 9000 //00.09.000
|
||||
versionName "1.0.0-beta1"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 100992 //01.00.000
|
||||
versionName "1.1.0-beta3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resValue "string", "build_time", buildTime()
|
||||
@ -23,6 +29,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -33,51 +40,47 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
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.5.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.3'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.8.3'
|
||||
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.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.6'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6'
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.3.2"
|
||||
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.8' // TODO remove, still used by metadb
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.15.0'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.15.0'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.15.0'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.15.0'
|
||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.15.0'
|
||||
implementation 'com.google.android.material:material:1.12.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.12.0'
|
||||
implementation 'com.facebook.shimmer:shimmer:0.5.0'
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
implementation 'com.afollestad.material-dialogs:core:3.3.0' // TODO remove once unused
|
||||
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0' // TODO remove once unused
|
||||
|
||||
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.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
|
||||
}
|
||||
|
||||
|
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
@ -24,10 +24,6 @@
|
||||
|
||||
-keep class org.json.** { *; }
|
||||
|
||||
#Gson
|
||||
-keepattributes Signature
|
||||
-dontwarn sun.misc.**
|
||||
|
||||
# kotlinx.serialization
|
||||
# Keep `Companion` object fields of serializable classes.
|
||||
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
||||
@ -56,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
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.mosad.teapod">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
@ -11,34 +10,29 @@
|
||||
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:name="org.mosad.teapod.ui.activity.SplashActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:screenOrientation="portrait">
|
||||
android:exported="true"
|
||||
android:name="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.App.Starting">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name="org.mosad.teapod.ui.activity.player.PlayerActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity"
|
||||
android:supportsPictureInPicture="true"
|
||||
|
@ -25,46 +25,46 @@ package org.mosad.teapod.parser.crunchyroll
|
||||
import android.util.Log
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
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
|
||||
import kotlinx.serialization.json.put
|
||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.util.concatenate
|
||||
|
||||
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, ExperimentalCoroutinesApi::class)
|
||||
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
|
||||
|
||||
private var accountID = ""
|
||||
private var externalID = ""
|
||||
|
||||
private var policy = ""
|
||||
private var signature = ""
|
||||
private var keyPairID = ""
|
||||
|
||||
private val browsingCache = arrayListOf<Item>()
|
||||
private val browsingCache = hashMapOf<String, BrowseResult>()
|
||||
|
||||
/**
|
||||
* Load the pai token, see:
|
||||
@ -74,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")
|
||||
}
|
||||
}
|
||||
@ -98,15 +98,27 @@ object Crunchyroll {
|
||||
|
||||
var success = false// is false
|
||||
withContext(Dispatchers.IO) {
|
||||
// TODO handle exceptions
|
||||
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
|
||||
header("Authorization", "Basic $basicApiToken")
|
||||
}
|
||||
token = response.receive()
|
||||
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
|
||||
Log.i(TAG, "getting token ...")
|
||||
|
||||
Log.i(TAG, "login complete with code ${response.status}")
|
||||
success = (response.status == HttpStatusCode.OK)
|
||||
val status = try {
|
||||
val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) {
|
||||
header("Authorization", "Basic $basicApiToken")
|
||||
}
|
||||
token = response.body()
|
||||
tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000)
|
||||
response.status
|
||||
} catch (ex: ClientRequestException) {
|
||||
val status = ex.response.status
|
||||
if (status == HttpStatusCode.Unauthorized) {
|
||||
Log.e(TAG, "Could not complete login: " +
|
||||
"${status.value} ${status.description}. " +
|
||||
"Probably wrong username or password")
|
||||
}
|
||||
|
||||
status
|
||||
}
|
||||
Log.i(TAG, "Login complete with code $status")
|
||||
success = (status == HttpStatusCode.OK)
|
||||
}
|
||||
|
||||
return@runBlocking success
|
||||
@ -126,10 +138,12 @@ object Crunchyroll {
|
||||
params: List<Pair<String, Any?>> = listOf(),
|
||||
bodyObject: Any = Any()
|
||||
): T = coroutineScope {
|
||||
if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
|
||||
withContext(tokenRefreshContext) {
|
||||
if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -138,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)
|
||||
}
|
||||
@ -191,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.
|
||||
@ -223,72 +223,103 @@ 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
|
||||
}
|
||||
|
||||
/**
|
||||
* General element/media functions: browse, search, objects, season_list
|
||||
*/
|
||||
|
||||
// TODO categories
|
||||
/**
|
||||
* 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(
|
||||
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 noneOptParams = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"sort_by" to sortBy.str,
|
||||
val browseEndpoint = "/content/v2/discover/browse"
|
||||
val parameters = mutableListOf(
|
||||
"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
|
||||
val parameters = if (seasonTag.isNotEmpty()) {
|
||||
concatenate(noneOptParams, listOf("season_tag" to seasonTag))
|
||||
if (seasonTag.isNotEmpty()) {
|
||||
parameters.add("season_tag" to seasonTag)
|
||||
}
|
||||
|
||||
// if a season tag is present add it to the parameters
|
||||
if (categories.isNotEmpty()) {
|
||||
parameters.add("categories" to categories.joinToString(",") { it.str })
|
||||
}
|
||||
|
||||
// fetch result if not already cached
|
||||
if (browsingCache.contains(parameters.toString())) {
|
||||
Log.d(TAG, "browse result cached: $parameters")
|
||||
} else {
|
||||
noneOptParams
|
||||
Log.d(TAG, "browse result not cached, fetching: $parameters")
|
||||
val browseResult: BrowseResult = try {
|
||||
requestGet(browseEndpoint, parameters)
|
||||
}catch (ex: Exception) {
|
||||
Log.e(TAG, "SerializationException in browse().", ex)
|
||||
NoneBrowseResult
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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 > 10) {
|
||||
browsingCache.clear()
|
||||
}
|
||||
|
||||
// add results to cache
|
||||
browsingCache[parameters.toString()] = browseResult
|
||||
}
|
||||
|
||||
val browseResult: BrowseResult = try {
|
||||
requestGet(browseEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in browse().", ex)
|
||||
NoneBrowseResult
|
||||
}
|
||||
|
||||
// add results to cache TODO improve
|
||||
browsingCache.clear()
|
||||
browsingCache.addAll(browseResult.items)
|
||||
|
||||
return browseResult
|
||||
return browsingCache[parameters.toString()] ?: NoneBrowseResult
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Search fo a query term.
|
||||
* Note: currently this function only supports series/tv shows.
|
||||
*
|
||||
* @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,
|
||||
@ -296,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
|
||||
}
|
||||
}
|
||||
@ -307,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
|
||||
}
|
||||
}
|
||||
|
||||
@ -350,87 +365,118 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun seasons(seriesId: String): Seasons {
|
||||
val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons"
|
||||
val parameters = listOf(
|
||||
"series_id" to seriesId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(seasonsEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in seasons().", ex)
|
||||
NoneSeasons
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun episodes(seasonId: String): Episodes {
|
||||
val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes"
|
||||
val parameters = listOf(
|
||||
"season_id" to seasonId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(episodesEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in episodes().", ex)
|
||||
NoneEpisodes
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun playback(url: String): Playback {
|
||||
return try {
|
||||
requestGet("", url = url)
|
||||
}catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in playback(), with url = $url.", ex)
|
||||
NonePlayback
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional media functions: watchlist (series), playhead
|
||||
* Get all available seasons for a series.
|
||||
*
|
||||
* @param seriesId The series id for which to get the seasons
|
||||
* @return A **[Seasons]** object with a list of **[Season]**
|
||||
*/
|
||||
suspend fun seasons(seriesId: String): Seasons {
|
||||
val seasonsEndpoint = "/content/v2/cms/series/$seriesId/seasons"
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(seasonsEndpoint, parameters)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in seasons() for seriesId $seriesId.", ex)
|
||||
NoneSeasons
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available episodes for a season.
|
||||
*
|
||||