7 Commits

Author SHA1 Message Date
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
20 changed files with 292 additions and 206 deletions

View File

@ -5,15 +5,15 @@ plugins {
}
android {
compileSdkVersion 33
buildToolsVersion "30.0.3"
compileSdk 33
buildToolsVersion '30.0.3'
defaultConfig {
applicationId "org.mosad.teapod"
minSdkVersion 23
targetSdkVersion 33
versionCode 100991 //01.00.000
versionName "1.1.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()
@ -38,7 +38,7 @@ android {
}
kotlinOptions {
jvmTarget = '1.8'
kotlin.sourceSets.all {
kotlin.sourceSets.configureEach {
languageSettings.optIn("kotlin.RequiresOptIn")
}
}
@ -48,21 +48,21 @@ android {
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.5.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1'
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation 'androidx.core:core-ktx:1.10.1'
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.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
implementation 'androidx.security:security-crypto:1.1.0-alpha05'
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
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.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.google.android.material:material:1.9.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"

View File

@ -34,7 +34,6 @@ import io.ktor.http.*
import io.ktor.serialization.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
@ -59,16 +58,12 @@ object Crunchyroll {
private lateinit var token: Token
private var tokenValidUntil: Long = 0
@OptIn(DelicateCoroutinesApi::class)
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
private val tokenRefreshContext = newSingleThreadContext("TokenRefreshContext")
private var accountID = ""
private var externalID = ""
private var policy = ""
private var signature = ""
private var keyPairID = ""
private val browsingCache = hashMapOf<String, BrowseResult>()
/**
@ -213,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.
@ -261,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.preferredSubtitleLocale.toLanguageTag(),
"sort_by" to sortBy.str,
"start" to start,
"n" to n
"n" to n,
"sort_by" to sortBy.str,
"ratings" to ratings,
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
)
// if a season tag is present add it to the parameters
@ -303,9 +287,10 @@ object Crunchyroll {
NoneBrowseResult
}
// if the cache has more than 100 entries clear it, so it doesn't become a memory problem
// if the cache has more than 10 entries clear it, so it doesn't become a memory problem
// Note: this value is totally guessed and should be replaced by a properly researched value
// TODO 100 is way to high as it's not the number of items but BrowseResults
if (browsingCache.size > 10) {
browsingCache.clear()
}
@ -321,19 +306,20 @@ object Crunchyroll {
* Search fo a query term.
* Note: currently this function only supports series/tv shows.
*
* TODO migrate to v2
*
* @param query The query term as String
* @param n The maximum number of results to return, default = 10
* @param ratings add user rating to the objects, default = false
* @return A **[SearchResult]** object
*/
suspend fun search(query: String, n: Int = 10): SearchResult {
val searchEndpoint = "/content/v1/search"
suspend fun search(query: String, n: Int = 10, ratings: Boolean = false): SearchResult {
val searchEndpoint = "/content/v2/discover/search"
val parameters = listOf(
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"q" to query,
"n" to n,
"type" to "series"
"type" to "series",
"ratings" to ratings,
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
)
// TODO episodes have thumbnails as image, and not poster_tall/poster_tall,
@ -352,22 +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.preferredSubtitleLocale.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: Exception) {
Log.e(TAG, "Exception in objects().", ex)
NoneCollection
NoneCollectionV2
}
}
@ -508,7 +494,7 @@ object Crunchyroll {
)
return try {
(requestGet(watchlistSeriesEndpoint, parameters) as Collection2<IsWatchlistItem>)
(requestGet(watchlistSeriesEndpoint, parameters) as CollectionV2<IsWatchlistItem>)
.total == 1
} catch (ex: Exception) {
Log.e(TAG, "Exception in isWatchlist() with seriesId $seriesId", ex)
@ -630,14 +616,16 @@ object Crunchyroll {
*
* @param seriesId The crunchyroll series id of the media
* @param n The maximum number of results to return, default = 10
* @param ratings add user rating to the objects
* @return A **[SimilarToResult]** object
*/
suspend fun similarTo(seriesId: String, n: Int = 10): SimilarToResult {
val similarToEndpoint = "/content/v1/$accountID/similar_to"
suspend fun similarTo(seriesId: String, n: Int = 10, ratings: Boolean = false): SimilarToResult {
val similarToEndpoint = "/content/v2/discover/$accountID/similar_to/$seriesId"
val parameters = listOf(
"guid" to seriesId,
"n" to n,
"ratings" to ratings,
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"n" to n
)
return try {
@ -658,7 +646,7 @@ object Crunchyroll {
* @param n Number of items to return, defaults to 20.
* @return A **[Collection]** containing up to n **[Item]**.
*/
suspend fun watchlist(n: Int = 20): Collection<Item> {
suspend fun watchlist(n: Int = 20): CollectionV2<Item> {
val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist"
val parameters = listOf(
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
@ -680,10 +668,10 @@ object Crunchyroll {
/**
* List the next up episodes for the logged in account.
*
* @param n Number of items to return, defaults to 20.
* @param n Number of items to return, default = 20
* @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**.
*/
suspend fun upNextAccount(n: Int = 20): HistoryList {
suspend fun upNextAccount(n: Int = 10): HistoryList {
val watchlistEndpoint = "/content/v2/discover/$accountID/history"
val parameters = listOf(
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
@ -698,13 +686,21 @@ object Crunchyroll {
}
}
suspend fun recommendations(n: Int = 20, start: Int = 0): RecommendationsList {
val recommendationsEndpoint = "/content/v1/$accountID/recommendations"
/**
* Returns a collection of recommendations for the currently logged in account.
*
* @param start start of the item list, used for pagination, default = 0
* @param n number of items to return, default = 10
* @param ratings add user rating to the objects, default = false
* @return A **[RecommendationsList]** containing up to n **[Item]**.
*/
suspend fun recommendations(start: Int = 0, n: Int = 10, ratings: Boolean = false): RecommendationsList {
val recommendationsEndpoint = "/content/v2/discover/$accountID/recommendations"
val parameters = listOf(
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
"n" to n,
"start" to start,
"variant_id" to 0
"n" to n,
"ratings" to ratings,
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
)
return try {
@ -740,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)
@ -749,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.
*

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,23 +144,22 @@ val NoneAccount = Account("", "", false, "")
*/
@Serializable
data class Collection<T>(
data class CollectionV1<T>(
@SerialName("total") val total: Int,
@SerialName("items") val items: List<T>
)
@Serializable
data class Collection2<T>(
data class CollectionV2<T>(
@SerialName("total") val total: Int,
@SerialName("data") val data: List<T>
)
typealias SearchResult = Collection<SearchCollection>
typealias SearchCollection = Collection<Item>
typealias BrowseResult = Collection<Item>
typealias SimilarToResult = Collection<Item>
typealias RecommendationsList = Collection<Item>
typealias Benefits = Collection<Benefit>
typealias SearchResult = CollectionV2<SearchTypedList<Item>>
typealias BrowseResult = CollectionV2<Item>
typealias SimilarToResult = CollectionV2<Item>
typealias RecommendationsList = CollectionV2<Item>
typealias Benefits = CollectionV1<Benefit>
/**
* panel data classes
@ -159,9 +190,9 @@ data class Poster(val height: Int, val width: Int, val source: String, val type:
* up next & watchlist data classes
*/
typealias Watchlist = Collection2<WatchlistItem>
typealias HistoryList = Collection2<UpNextAccountItem>
typealias UpNextSeriesList = Collection2<UpNextSeriesItem>
typealias Watchlist = CollectionV2<WatchlistItem>
typealias HistoryList = CollectionV2<UpNextAccountItem>
typealias UpNextSeriesList = CollectionV2<UpNextSeriesItem>
@Serializable
data class WatchlistItem(
@ -221,7 +252,7 @@ data class EpisodeMetadata(
@SerialName("series_title") val seriesTitle: String,
)
val NoneCollection = Collection<Item>(0, emptyList())
val NoneCollectionV2 = CollectionV2<Item>(0, emptyList())
val NoneSearchResult = SearchResult(0, emptyList())
val NoneBrowseResult = BrowseResult(0, emptyList())
val NoneSimilarToResult = SimilarToResult(0, emptyList())
@ -235,7 +266,7 @@ val NoneBenefits = Benefits(0, emptyList())
* series data class
*/
typealias Series = Collection2<SeriesItem>
typealias Series = CollectionV2<SeriesItem>
@Serializable
data class SeriesItem(
@ -353,7 +384,7 @@ val NoneVersion = Version(
variant = ""
)
typealias Playheads = Collection2<PlayheadObject>
typealias Playheads = CollectionV2<PlayheadObject>
@Serializable
data class PlayheadObject(
@ -449,7 +480,18 @@ data class Benefit(
@SerialName("benefit") val benefit: String,
@SerialName("source") val source: String,
)
@Suppress("unused")
val NoneBenefit = Benefit(
benefit = "",
source = ""
)
/**
* search result typed list data class
*/
@Serializable
data class SearchTypedList<T>(
@SerialName("type") val type: String,
@SerialName("count") val count: Int,
@SerialName("items") val items: List<T>
)

View File

@ -12,8 +12,6 @@ object Preferences {
internal set
var preferredSubtitleLocale: Locale = Locale.forLanguageTag("en-US")
internal set
var preferSubbed = false
internal set
var autoplay = true
internal set
var devSettings = false
@ -50,15 +48,6 @@ object Preferences {
this.preferredSubtitleLocale = preferredLocale
}
fun savePreferSecondary(context: Context, preferSubbed: Boolean) {
with(getSharedPref(context).edit()) {
putBoolean(context.getString(R.string.save_key_prefer_secondary), preferSubbed)
apply()
}
this.preferSubbed = preferSubbed
}
fun saveAutoplay(context: Context, autoplay: Boolean) {
with(getSharedPref(context).edit()) {
putBoolean(context.getString(R.string.save_key_autoplay), autoplay)
@ -111,9 +100,6 @@ object Preferences {
context.getString(R.string.save_key_preferred_local), "en-US"
) ?: "en-US"
)
preferSubbed = sharedPref.getBoolean(
context.getString(R.string.save_key_prefer_secondary), false
)
autoplay = sharedPref.getBoolean(
context.getString(R.string.save_key_autoplay), true
)

View File

@ -173,7 +173,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
private fun initCrunchyroll(): List<Job> {
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading"))
return listOf(
scope.launch { Crunchyroll.index() },
scope.launch { Crunchyroll.account() },
scope.launch {
// update the local preferred content language, since it may have changed

View File

@ -15,7 +15,8 @@ import org.mosad.teapod.databinding.FragmentAccountBinding
import org.mosad.teapod.parser.crunchyroll.Benefits
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.parser.crunchyroll.Profile
import org.mosad.teapod.parser.crunchyroll.supportedLocals
import org.mosad.teapod.parser.crunchyroll.supportedAudioLocals
import org.mosad.teapod.parser.crunchyroll.supportedSubtitleLocals
import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.activity.main.MainActivity
@ -61,11 +62,13 @@ class AccountFragment : Fragment() {
// add preferred subtitles
lifecycleScope.launch {
binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag(
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
profile.await().preferredContentAudioLanguage
).displayLanguage
binding.textSettingsSubtitleLanguageDesc.text = Locale.forLanguageTag(
profile.await().preferredContentSubtitleLanguage
).displayLanguage
}
binding.switchSecondary.isChecked = Preferences.preferSubbed
binding.switchAutoplay.isChecked = Preferences.autoplay
binding.textThemeSelected.text = when (Preferences.theme) {
Theme.SYSTEM -> getString(R.string.theme_system)
@ -86,12 +89,12 @@ class AccountFragment : Fragment() {
showLoginDialog()
}
binding.linearSettingsContentLanguage.setOnClickListener {
showContentLanguageSelection()
binding.linearSettingsAudioLanguage.setOnClickListener {
showAudioLanguageSelection()
}
binding.switchSecondary.setOnClickListener {
Preferences.savePreferSecondary(requireContext(), binding.switchSecondary.isChecked)
binding.linearSettingsSubtitleLanguage.setOnClickListener {
showSubtitleLanguageSelection()
}
binding.switchAutoplay.setOnClickListener {
@ -136,43 +139,86 @@ class AccountFragment : Fragment() {
activity?.let { loginModal.show(it.supportFragmentManager, LoginModalBottomSheet.TAG) }
}
private fun showContentLanguageSelection() {
private fun showAudioLanguageSelection() {
// we should be able to use the index of supportedLocals for language selection, items is GUI only
val items = supportedLocals.map {
val items = supportedAudioLocals.map {
it.toDisplayString(getString(R.string.settings_content_language_none))
}.toTypedArray()
var initialSelection: Int
// profile should be completed here, therefore blocking
runBlocking {
initialSelection = supportedLocals.indexOf(Locale.forLanguageTag(
profile.await().preferredContentSubtitleLanguage))
if (initialSelection < 0) initialSelection = supportedLocals.lastIndex
initialSelection = supportedAudioLocals.indexOf(Locale.forLanguageTag(
profile.await().preferredContentAudioLanguage))
if (initialSelection < 0) initialSelection = supportedAudioLocals.lastIndex
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.settings_content_language)
.setTitle(R.string.settings_audio_language)
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
updatePrefContentLanguage(supportedLocals[which])
updateAudioLanguage(supportedAudioLocals[which])
dialog.dismiss()
}
.show()
}
private fun showSubtitleLanguageSelection() {
// we should be able to use the index of supportedLocals for language selection, items is GUI only
val items = supportedSubtitleLocals.map {
it.toDisplayString(getString(R.string.settings_content_language_none))
}.toTypedArray()
var initialSelection: Int
// profile should be completed here, therefore blocking
runBlocking {
initialSelection = supportedSubtitleLocals.indexOf(Locale.forLanguageTag(
profile.await().preferredContentSubtitleLanguage))
if (initialSelection < 0) initialSelection = supportedSubtitleLocals.lastIndex
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.settings_audio_language)
.setSingleChoiceItems(items, initialSelection){ dialog, which ->
updateSubtitleLanguage(supportedSubtitleLocals[which])
dialog.dismiss()
}
.show()
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun updatePrefContentLanguage(preferredLocale: Locale) {
private fun updateAudioLanguage(preferredLocale: Locale) {
lifecycleScope.launch {
Crunchyroll.postPrefSubLanguage(preferredLocale.toLanguageTag())
Crunchyroll.setPreferredAudioLanguage(preferredLocale.toLanguageTag())
}.invokeOnCompletion {
// update the local preferred content language
Preferences.savePreferredSubtitleLocal(requireContext(), preferredLocale)
// update the local preferred audio language
Preferences.savePreferredAudioLocal(requireContext(), preferredLocale)
// update profile since the language selection might have changed
profile = lifecycleScope.async { Crunchyroll.profile() }
profile.invokeOnCompletion {
// update language once loading profile is completed
binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag(
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
profile.getCompleted().preferredContentAudioLanguage
).displayLanguage
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun updateSubtitleLanguage(preferredLocal: Locale) {
lifecycleScope.launch {
Crunchyroll.setPreferredSubtitleLanguage(preferredLocal.toLanguageTag())
}.invokeOnCompletion {
// update the local preferred subtitle language
Preferences.savePreferredSubtitleLocal(requireContext(), preferredLocal)
// update profile since the language selection might have changed
profile = lifecycleScope.async { Crunchyroll.profile() }
profile.invokeOnCompletion {
// update language once loading profile is completed
binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
profile.getCompleted().preferredContentSubtitleLanguage
).displayLanguage
}

View File

@ -66,16 +66,16 @@ class HomeViewModel : ViewModel() {
uiState.emit(UiState.Loading)
try {
// run the loading in parallel to speed up the process
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().data }
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).items }
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount(n = 20).data }
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).data }
val recommendationsJob = viewModelScope.async {
Crunchyroll.recommendations(20).items
Crunchyroll.recommendations(n = 20).data
}
val recentlyAddedJob = viewModelScope.async {
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).items
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).data
}
val topTenJob = viewModelScope.async {
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).items
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).data
}
val recentlyAddedItems = recentlyAddedJob.await()
@ -114,7 +114,7 @@ class HomeViewModel : ViewModel() {
}
// update the watchlist after a item has been added/removed
val watchlistItems = Crunchyroll.watchlist(WATCHLIST_LENGTH).items
val watchlistItems = Crunchyroll.watchlist(WATCHLIST_LENGTH).data
currentUiState.copy(
watchlistItems = watchlistItems,
@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() {
viewModelScope.launch {
uiState.update { currentUiState ->
if (currentUiState is UiState.Normal) {
val upNextItems = Crunchyroll.upNextAccount().data
val upNextItems = Crunchyroll.upNextAccount(n = 20).data
currentUiState.copy(upNextItems = upNextItems)
} else {
currentUiState

View File

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

View File

@ -222,12 +222,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
// needs to be blocking, currentPlayback must be present when calling playCurrentMedia()
joinAll(
viewModelScope.launch(Dispatchers.IO) {
currentVersion = if (Preferences.preferSubbed) {
currentEpisode.versions?.first { it.original } ?: NoneVersion
} else {
currentEpisode.versions?.firstOrNull { it.audioLocale == currentAudioLocale.toLanguageTag() }
?: currentEpisode.versions?.first() ?: NoneVersion
}
currentVersion = currentEpisode.versions?.firstOrNull {
it.audioLocale == currentAudioLocale.toLanguageTag()
} ?: currentEpisode.versions?.first() ?: NoneVersion
// get the current streams object, if no version is set, use streamsLink
currentStreams = if (currentVersion != NoneVersion) {

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,3v9.28c-0.47,-0.17 -0.97,-0.28 -1.5,-0.28C8.01,12 6,14.01 6,16.5S8.01,21 10.5,21c2.31,0 4.2,-1.75 4.45,-4H15V6h4V3h-7z"/>
</vector>

View File

@ -141,7 +141,7 @@
android:textStyle="bold" />
<LinearLayout
android:id="@+id/linear_settings_content_language"
android:id="@+id/linear_settings_audio_language"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
@ -151,12 +151,12 @@
android:id="@+id/imageView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/settings_content_language"
android:contentDescription="@string/settings_audio_language"
android:minWidth="48dp"
android:minHeight="48dp"
android:padding="9dp"
android:scaleType="fitXY"
android:src="@drawable/ic_baseline_language_24" />
android:src="@drawable/ic_baseline_audiotrack_24" />
<LinearLayout
android:layout_width="match_parent"
@ -167,11 +167,11 @@
android:id="@+id/text_settings_content_language"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_content_language"
android:text="@string/settings_audio_language"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView
android:id="@+id/text_settings_content_language_desc"
android:id="@+id/text_settings_audio_language_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_content_language_desc" />
@ -179,67 +179,41 @@
</LinearLayout>
<LinearLayout
android:id="@+id/linear_settings_secondary"
android:id="@+id/linear_settings_subtitle_language"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
android:padding="7dp">
<ImageView
android:id="@+id/imageView3"
android:id="@+id/imageView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/settings_prefer_subbed"
android:contentDescription="@string/settings_subtitle_language"
android:minWidth="48dp"
android:minHeight="48dp"
android:padding="9dp"
android:scaleType="fitXY"
android:src="@drawable/ic_baseline_subtitles_24" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch_secondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text_settings_secondary"
android:id="@+id/text_settings_subtitle_language"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_prefer_subbed"
android:text="@string/settings_subtitle_language"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView
android:id="@+id/text_settings_secondary_desc"
android:id="@+id/text_settings_subtitle_language_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="2"
android:text="@string/settings_prefer_subbed_desc" />
android:text="@string/settings_content_language_desc" />
</LinearLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:contentDescription="@string/settings_prefer_subbed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<LinearLayout

View File

@ -45,7 +45,8 @@
<string name="info">Info</string>
<string name="info_about_desc">Version %1$s (%2$s)</string>
<string name="settings">Einstellungen</string>
<string name="settings_content_language">Bevorzuge Inhaltssprache</string>
<string name="settings_audio_language">Audio Sprache</string>
<string name="settings_subtitle_language">Untertielsprache</string>
<string name="settings_content_language_desc">Englisch</string>
<string name="settings_content_language_none">Keine</string>
<string name="settings_prefer_subbed">Bevorzuge OmU</string>

View File

@ -59,7 +59,8 @@
<string name="account_tier_mega_fan" translatable="false">Mega Fan</string>
<string name="account_tier_ultimate_fan" translatable="false">Ultimate Fan</string>
<string name="settings">Settings</string>
<string name="settings_content_language">Preferred content language</string>
<string name="settings_audio_language">Audio language</string>
<string name="settings_subtitle_language">Subtitle language</string>
<string name="settings_content_language_desc">English</string>
<string name="settings_content_language_none">None</string>
<string name="settings_prefer_subbed">Prefer subbed</string>

View File

@ -1,14 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.8.10"
ext.ktor_version = "2.2.4"
ext.exo_version = "2.18.5"
ext.kotlin_version = "1.8.22"
ext.ktor_version = "2.3.2"
ext.exo_version = "2.18.7"
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.0'
classpath 'com.android.tools.build:gradle:8.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@ -0,0 +1,10 @@
Dies ist der dritte beta Release von Teapod 1.1.
* Neues App Design (Material Design 3)
* Unterstützung für Crunchyroll v2 API
* Intro überspringen hinzugefügt
* Seperaten Screen für "Meine Liste"
* Dynamische Spaltenanzahl für alle Screens um große Bildschirme besser zu unterstützen
* Kleine UI/UX Verbesserungen
Alle Änderungen: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta3

View File

@ -0,0 +1,10 @@
This is the third beta release of Teapod 1.1.
* Migrate to material design 3
* Migrate crunchyroll parser to v2 (fixes crunchyroll)
* Add skip intro function
* Add a separate Watchlist fragment
* Dynamically set column count based on the display size
* Minor UI/UX improvements
Full changelog: https://git.mosad.xyz/Seil0/teapod/compare/1.0.0...1.1.0-beta3

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

7
gradlew vendored
View File

@ -85,9 +85,6 @@ done
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -197,6 +194,10 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in