Compare commits

...

60 Commits

Author SHA1 Message Date
b07a6fd407
update gradle wrapper, agp, kotlin and libraries 2024-03-03 21:21:12 +01:00
7d661712f7
update to kotlin 1.9.0 2023-11-15 15:50:13 +01:00
8fcf047e99
update agp and libraries
* agp 8.1.2 -> 8.1.3
* ktor 2.3.4 -> 2.3.6
* androidx.core 1.10.1 -> 1.12.0
* androidx.navigation 2.6.0 -> 2.7.5
* androidx.lifecycle 2.6.1 -> 2.6.1
* com.google.android.material 1.9.0 -> 1.10.0
* gradle wrapper 8.2.1 -> 8.4
2023-11-15 15:37:44 +01:00
17dbe945e5
mograte MyListFragment to use a simple ViewModel
fixes crashes in MyListFragment if the User closes the fragment with a loading job still running, part of #56
2023-10-15 18:44:22 +02:00
5f609d4c33
update agp and libraries
* agp 8.1.0 -> 8.1.2
* ktor 2.3.2 -> 2.3.4
* kotlinx-serialization-json 1.5.1 -> 1.6.0
2023-09-29 23:47:47 +02:00
6515f657d0
partially revert c448b44fc4 2023-08-11 15:07:33 +02:00
c448b44fc4
updatelibraries and agp
* agp 8.0.2 -> 8.1.0
* kotlinx-coroutines-android 1.7.2 -> 1.7.3
* navigation-fragment-ktx 2.6.0 -> 2.7.0
* navigation-ui-ktx 2.6.0 -> 2.7.0
2023-08-11 14:58:17 +02:00
88ebc378d3
add changelog for beta3; update gradle wrapper to 8.2.1 2023-08-11 14:41:55 +02:00
1a012cba7d
add support for dedicated subtitle and audio language settings 2023-07-21 21:42:55 +02:00
59a457430e
migrate more Crunchyroll API endpoints to v2 2023-07-21 17:22:45 +02:00
0662d656ac
update libraries, agp and kotlin
* kotlin 1.8.10 -> 1.8.22
* kotlinx-coroutines-android 1.6.4 -> 1.7.2
* kotlinx-serialization-json 1.5.0 -> 1.5.1
* core-ktx 1.10.0 -> 1.10.1
* core-splashscreen 1.0.0 -> 1.0.1
* navigation-fragment-ktx 2.5.3 -> 2.6.0
* navigation-ui-ktx 2.5.3 -> 2.6.0
* security-crypto 1.1.0-alpha05 -> 1.1.0-alpha06
* material 1.8.0 -> 1.9.0
* ktor 2.2.4 -> 2.3.2
* exo-player 2.18.5 -> 2.18.7
* agp 8.0.0 -> 8.0.2
2023-07-21 11:43:38 +02:00
3549a3d2a7
migrate Crunchyroll.objects() to new v2 endpoint
fixes #71
2023-07-21 11:39:48 +02:00
c89ae54929
fix typo in changelog for 1.1.0-beta2 2023-04-16 16:51:59 +02:00
3aa03783a9
add changelogs for 1.1.0-beta2 2023-04-16 16:49:09 +02:00
4bceacf75c
make versions in DataTypes -> Episodes -> Episode nullable since it is in fact nullable 2023-04-16 16:24:28 +02:00
cf02bee7d4
minor fixes
* fix episode count in MediaFragement
* fix tmdb language tag
* update media type detection to use the episode field as episodeNumber may be messinging from certain episodes of tv shows
2023-04-16 13:49:22 +02:00
01d026cc7f
update libraries, agp and gradle
* core-ktx 1.9.0 -> 1.10.0
* lifecycle-runtime-ktx 2.5.1 -> 2.6.1
* lifecycle-viewmodel-ktx 2.5.1 -> 2.6.1
* glide 4.15.0 -> 4.15.1
* exoplayer 2.18.3 -> 2.18.5
* agp 7.4.2 -> 8.0.0
* gradle wrapper 7.6 -> 8.1
2023-04-16 00:06:40 +02:00
7580093649 Merge pull request 'migrate to material 3' (#70) from feature/material-3 into develop
Reviewed-on: #70
2023-04-15 23:51:08 +02:00
f266731115
remove old theme definition 2023-04-15 23:50:40 +02:00
a6a23c8560
fix onboarding colors for light/dark theme 2023-04-15 23:46:13 +02:00
2cb05de810
fix theme selection dialog to work with system theme also use system as new default 2023-04-15 22:48:59 +02:00
5cf4527a92
clean up color and theme definitions
also use separate theme definition for light/dark
2023-04-15 22:35:19 +02:00
14ad34138c
fix onboarding fragments and bottom sheet login 2023-04-15 22:02:49 +02:00
47e1f6bd49
initial migration to material 3 2023-03-29 16:16:31 +02:00
fdcb76e26e
update glide and kotlinx-serialization
* glide 4.14.2 -> 4.15.0
* kotlinx-serialization 1.4.1 -> 1.5.0
2023-03-01 17:24:02 +01:00
7004d73b9f
update libraries and kotlin
* kotlin 1.7.20 -> 1.8.10
* ktor 2.2.1 -> 2.2.4
* exo player 2.18.2 -> 2.18.3
* androidx.appcompat 1.6.0 -> 1.6.1
* androidx.security:security-crypto 1.1.0-alpha04 -> 1.1.0-alpha05
* com.google.android.material 1.7.0 -> 1.8.0
2023-03-01 17:17:23 +01:00
a13eb15adf
add changelog for 1.1.0-beta1 2023-02-19 17:06:33 +01:00
d40ab9519c
migrate playheads() to crunchyroll v2 api 2023-02-19 16:53:54 +01:00
2e7db26d1d
migrate more api calls to v2 2023-02-19 15:13:31 +01:00
8b7fb3ac5f
fix crunchyroll parser to work with the latest api changes 2023-02-19 14:21:46 +01:00
097383a082
fix playback & update to agp 7.4.0
updated the crunchyroll parser to use the new streams endpoint to retrieve the media streams
2023-01-25 19:51:38 +01:00
9380f98098
add watchlist to MyListsFragment 2022-12-26 19:43:40 +01:00
e0f05169f5
fix shimmer items having the wrong size, update MediaFragmentSimilar to not depend on a specific view model 2022-12-26 19:40:03 +01:00
e113a9c795
Merge library and search into one fragment
closes #55
2022-12-26 16:10:38 +01:00
8e397e13d2
fix padding for ItemMedia
fixes missing shadows in light theme
2022-12-22 18:08:32 +01:00
31e7adac03
update gradle wrapper to version 7.6 2022-12-22 16:16:28 +01:00
63f5e69094
update ktor
ktor 2.1.3 -> 2.2.1
2022-12-11 20:00:39 +01:00
bf6f2d916e
make MeidaFragment poster and backdrop responsive 2022-12-04 15:25:05 +01:00
81a20e0aa9 Merge pull request 'add dynamic spanCount for library/search fragemnt and MediaFragmentSimilar' (#68) from feature/dynamic_span_count into develop
Reviewed-on: #68
2022-12-04 15:24:18 +01:00
ed8f3fdcda
set spanCount according to screen width 2022-12-04 14:48:25 +01:00
fffbeaeb49
make MediaItem width fully dynamic, based on the parents width (50% of parent width) and update SearchFragment to use MediaItemListAdapter and remove now unused MediaItemAdapter 2022-12-04 13:51:29 +01:00
21caa8eb1b
update MediaItem to suport dynamic size
* this is needed for dynamic span count to correctly work
* this also fixes issues with poster image cropping when the MediaItem size was < 195dp
2022-12-03 00:05:57 +01:00
bbc819551b
disable platform diagnostics for exo player 2022-12-02 23:59:39 +01:00
2004a3f483
replace runBlocking{} in setCurrentEpisode with suspend
this fixes the player frezzing for a few 100ms when loading a new episode
2022-11-26 18:34:32 +01:00
0a31c2fd88
update dependencies
* exoplayer 2.17.1 -> 2.18.2
* security-crypto 1.1.0-alpha03 -> 1.1.0-alpha04
* androidx:junit 1.1.3 -> 1.1.4
* androidx:espresso-core 3.4.0 -> 3.5.0
2022-11-26 18:09:50 +01:00
f49b5a2730
rework the player activity starting behaviour
* add callbacks on player finish to update episode watch head progress in gui
* directly start the player from the fragment and not from MainActivity
2022-11-26 17:46:25 +01:00
a95813e91e
use the series id of upNextSeries to select the current season and only fall back to preferred local if not found 2022-11-26 15:52:20 +01:00
8bdaa8122b
replace usage of private exo_white with player_white 2022-11-05 11:58:41 +01:00
e2ea0a364e
update agp, kotlin and libraries 2022-11-05 11:57:35 +01:00
777c6e0212
add ScrollView to player language/subtitles selection 2022-11-05 11:24:16 +01:00
71d5c58653
add crunchy intro metadata to parser and update the skip intro function, closes #66 2022-10-28 23:03:21 +02:00
6624e71228
add more items to the shimmer layout on the home screen 2022-10-14 17:08:51 +02:00
d33de371d1 Merge pull request 'version 1.0.0' (#67) from develop into master
Reviewed-on: #67
2022-10-12 15:36:38 +02:00
1ecd25bb06
update version and changelog for 1.0.0 release 2022-10-12 15:25:48 +02:00
fa28eb35ab
fix crash in TMDBApiController when searchMovie() returns no title
* make title/name optional
* for movies use the movie search endpoint instead of multi

fixes #65
2022-09-21 21:06:52 +02:00
d3fe81224b
add missing play button functionality for highlight media in HomeFragment 2022-09-20 19:47:42 +02:00
34c7f9d081
replace TextView in shimmer items with dummy ImageView with rounded corners 2022-09-20 15:20:49 +02:00
19552d3950 Merge pull request 'version 0.4.2' (#44) from develop into master
Reviewed-on: #44
2021-07-09 18:56:34 +02:00
49e0b1ec29 Merge pull request 'release 0.4.1' (#37) from develop into master
Reviewed-on: #37
2021-03-13 22:19:29 +01:00
af66d968cc Merge pull request 'release 0.4.0' (#34) from develop into master
Reviewed-on: #34
2021-03-04 20:38:28 +01:00
98 changed files with 2165 additions and 1445 deletions

View File

@ -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)

View File

@ -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 33
buildToolsVersion "30.0.3"
compileSdk 34
buildToolsVersion = '34.0.0'
defaultConfig {
applicationId "org.mosad.teapod"
minSdkVersion 23
targetSdkVersion 32
versionCode 9020 //00.09.020
versionName "1.0.0-beta3"
minSdk 23
targetSdk 33
versionCode 100992 //01.00.000
versionName "1.1.0-beta3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
@ -22,6 +29,7 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
buildTypes {
@ -32,37 +40,28 @@ 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.4'
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.9.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
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.5.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.2'
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
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.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation "androidx.paging:paging-runtime-ktx:3.2.1"
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.google.android.material:material:1.11.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"
@ -71,7 +70,7 @@ dependencies {
implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation 'com.github.bumptech.glide:glide:4.13.2'
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"
@ -80,8 +79,8 @@ dependencies {
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'
}

View File

@ -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

View File

@ -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"

View File

@ -31,9 +31,9 @@ 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
@ -52,21 +52,18 @@ object Crunchyroll {
}
}
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 externalID = ""
private var policy = ""
private var signature = ""
private var keyPairID = ""
private val browsingCache = hashMapOf<String, BrowseResult>()
/**
@ -146,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 {
@ -158,18 +155,21 @@ object Crunchyroll {
setBody(bodyObject)
contentType(ContentType.Application.Json)
}
}.body()
}
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)
}
@ -208,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.
@ -240,7 +223,7 @@ 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
}
@ -256,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
@ -293,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()
}
@ -317,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,
@ -333,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
}
}
@ -344,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
}
}
@ -387,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
}
}
@ -406,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
}
}
@ -431,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
}
}
@ -455,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
}
}
@ -475,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
*/
@ -498,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
}
}
@ -516,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)
}
}
/**
@ -532,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)
}
}
/**
@ -546,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
}
}
@ -569,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)
@ -578,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
}
}
@ -614,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
}
}
@ -686,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
}
}
@ -697,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)
@ -706,6 +745,20 @@ 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.
*
@ -716,8 +769,8 @@ object Crunchyroll {
return try {
requestGet(profileEndpoint)
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in benefits().", ex)
} catch (ex: Exception) {
Log.e(TAG, "Exception in benefits().", ex)
NoneBenefits
}
}

View File

@ -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,29 +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>
typealias Benefits = Collection<Benefit>
@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
*/
@ -161,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)
/**
* season list data classes
* up next & watchlist data classes
*/
@Serializable
data class SeasonListItem(
@SerialName("id") val id: String,
@SerialName("localization") val localization: SeasonListLocalization
)
typealias Watchlist = CollectionV2<WatchlistItem>
typealias HistoryList = CollectionV2<UpNextAccountItem>
typealias UpNextSeriesList = CollectionV2<UpNextSeriesItem>
@Serializable
data class SeasonListLocalization(
@SerialName("title") val title: String,
@SerialName("description") val description: String,
)
/**
* continue_watching_item data classes
*/
@Serializable
data class ContinueWatchingItem(
data class WatchlistItem(
@SerialName("panel") val panel: EpisodePanel,
@SerialName("new") val new: Boolean,
@SerialName("new_content") val newContent: Boolean,
// not present in up_next_account -> continue_watching_item
// @SerialName("is_favorite") val isFavorite: Boolean,
// @SerialName("never_watched") val neverWatched: Boolean,
// @SerialName("completion_status") val completionStatus: Boolean,
@SerialName("playhead") val playhead: Int,
// not present in watchlist -> continue_watching_item
@SerialName("fully_watched") val fullyWatched: Boolean = false,
@SerialName("never_watched") val neverWatched: Boolean = false,
@SerialName("is_favorite") val isFavorite: Boolean,
)
@Serializable
data class IsWatchlistItem(
@SerialName("id") val id: String,
@SerialName("is_favorite") val isFavorite: Boolean,
@SerialName("date_added") val dateAdded: String
)