add support for dedicated subtitle and audio language settings

This commit is contained in:
Jannik 2023-07-21 21:42:55 +02:00
parent 59a457430e
commit 1a012cba7d
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
11 changed files with 150 additions and 115 deletions

View File

@ -12,8 +12,8 @@ android {
applicationId "org.mosad.teapod" applicationId "org.mosad.teapod"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 33 targetSdkVersion 33
versionCode 100991 //01.00.000 versionCode 100992 //01.00.000
versionName "1.1.0-beta2" versionName "1.1.0-beta3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime() resValue "string", "build_time", buildTime()

View File

@ -64,10 +64,6 @@ object Crunchyroll {
private var accountID = "" private var accountID = ""
private var externalID = "" private var externalID = ""
private var policy = ""
private var signature = ""
private var keyPairID = ""
private val browsingCache = hashMapOf<String, BrowseResult>() private val browsingCache = hashMapOf<String, BrowseResult>()
/** /**
@ -212,27 +208,10 @@ object Crunchyroll {
} }
/** /**
* Basic functions: index, account * Basic functions: account
* Needed for other functions to work properly! * 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. * Retrieve the account id and set the corresponding global var.
* The account id is needed for other calls. * The account id is needed for other calls.
@ -757,7 +736,7 @@ object Crunchyroll {
* *
* @param languageTag the preferred language as language tag * @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 profileEndpoint = "/accounts/v1/me/profile"
val json = buildJsonObject { val json = buildJsonObject {
put("preferred_content_subtitle_language", languageTag) put("preferred_content_subtitle_language", languageTag)
@ -766,6 +745,20 @@ object Crunchyroll {
requestPatch(profileEndpoint, bodyObject = json) 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. * Get additional profile (benefits) information for the currently logged in account.
* *

View File

@ -26,17 +26,45 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.Locale import java.util.Locale
val supportedLocals = listOf( val supportedAudioLocals = listOf(
Locale.forLanguageTag("ar-SA"), 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("de-DE"),
Locale.forLanguageTag("en-US"), Locale.forLanguageTag("en-US"),
Locale.forLanguageTag("es-419"), Locale.forLanguageTag("es-419"),
Locale.forLanguageTag("es-ES"), Locale.forLanguageTag("es-ES"),
Locale.forLanguageTag("fr-FR"), Locale.forLanguageTag("fr-FR"),
Locale.forLanguageTag("hi-IN"),
Locale.forLanguageTag("it-IT"), Locale.forLanguageTag("it-IT"),
Locale.forLanguageTag("ms-MY"),
Locale.forLanguageTag("pl-PL"),
Locale.forLanguageTag("pt-BR"), Locale.forLanguageTag("pt-BR"),
Locale.forLanguageTag("pt-PT"), Locale.forLanguageTag("pt-PT"),
Locale.forLanguageTag("ru-RU"), Locale.forLanguageTag("ru-RU"),
Locale.forLanguageTag("tr-TR"),
Locale.ROOT Locale.ROOT
) )

View File

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

View File

@ -173,7 +173,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
private fun initCrunchyroll(): List<Job> { private fun initCrunchyroll(): List<Job> {
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading")) val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading"))
return listOf( return listOf(
scope.launch { Crunchyroll.index() },
scope.launch { Crunchyroll.account() }, scope.launch { Crunchyroll.account() },
scope.launch { scope.launch {
// update the local preferred content language, since it may have changed // 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.Benefits
import org.mosad.teapod.parser.crunchyroll.Crunchyroll import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.parser.crunchyroll.Profile 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.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.MainActivity
@ -61,11 +62,13 @@ class AccountFragment : Fragment() {
// add preferred subtitles // add preferred subtitles
lifecycleScope.launch { lifecycleScope.launch {
binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag( binding.textSettingsAudioLanguageDesc.text = Locale.forLanguageTag(
profile.await().preferredContentAudioLanguage
).displayLanguage
binding.textSettingsSubtitleLanguageDesc.text = Locale.forLanguageTag(
profile.await().preferredContentSubtitleLanguage profile.await().preferredContentSubtitleLanguage
).displayLanguage ).displayLanguage
} }
binding.switchSecondary.isChecked = Preferences.preferSubbed
binding.switchAutoplay.isChecked = Preferences.autoplay binding.switchAutoplay.isChecked = Preferences.autoplay
binding.textThemeSelected.text = when (Preferences.theme) { binding.textThemeSelected.text = when (Preferences.theme) {
Theme.SYSTEM -> getString(R.string.theme_system) Theme.SYSTEM -> getString(R.string.theme_system)
@ -86,12 +89,12 @@ class AccountFragment : Fragment() {
showLoginDialog() showLoginDialog()
} }
binding.linearSettingsContentLanguage.setOnClickListener { binding.linearSettingsAudioLanguage.setOnClickListener {
showContentLanguageSelection() showAudioLanguageSelection()
} }
binding.switchSecondary.setOnClickListener { binding.linearSettingsSubtitleLanguage.setOnClickListener {
Preferences.savePreferSecondary(requireContext(), binding.switchSecondary.isChecked) showSubtitleLanguageSelection()
} }
binding.switchAutoplay.setOnClickListener { binding.switchAutoplay.setOnClickListener {
@ -136,43 +139,86 @@ class AccountFragment : Fragment() {
activity?.let { loginModal.show(it.supportFragmentManager, LoginModalBottomSheet.TAG) } 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 // 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)) it.toDisplayString(getString(R.string.settings_content_language_none))
}.toTypedArray() }.toTypedArray()
var initialSelection: Int var initialSelection: Int
// profile should be completed here, therefore blocking // profile should be completed here, therefore blocking
runBlocking { runBlocking {
initialSelection = supportedLocals.indexOf(Locale.forLanguageTag( initialSelection = supportedAudioLocals.indexOf(Locale.forLanguageTag(
profile.await().preferredContentSubtitleLanguage)) profile.await().preferredContentAudioLanguage))
if (initialSelection < 0) initialSelection = supportedLocals.lastIndex if (initialSelection < 0) initialSelection = supportedAudioLocals.lastIndex
} }
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.settings_content_language) .setTitle(R.string.settings_audio_language)
.setSingleChoiceItems(items, initialSelection){ dialog, which -> .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() dialog.dismiss()
} }
.show() .show()
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun updatePrefContentLanguage(preferredLocale: Locale) { private fun updateAudioLanguage(preferredLocale: Locale) {
lifecycleScope.launch { lifecycleScope.launch {
Crunchyroll.postPrefSubLanguage(preferredLocale.toLanguageTag()) Crunchyroll.setPreferredAudioLanguage(preferredLocale.toLanguageTag())
}.invokeOnCompletion { }.invokeOnCompletion {
// update the local preferred content language // update the local preferred audio language
Preferences.savePreferredSubtitleLocal(requireContext(), preferredLocale) Preferences.savePreferredAudioLocal(requireContext(), preferredLocale)
// update profile since the language selection might have changed // update profile since the language selection might have changed
profile = lifecycleScope.async { Crunchyroll.profile() } profile = lifecycleScope.async { Crunchyroll.profile() }
profile.invokeOnCompletion { profile.invokeOnCompletion {
// update language once loading profile is completed // 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 profile.getCompleted().preferredContentSubtitleLanguage
).displayLanguage ).displayLanguage
} }

View File

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

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

View File

@ -45,7 +45,8 @@
<string name="info">Info</string> <string name="info">Info</string>
<string name="info_about_desc">Version %1$s (%2$s)</string> <string name="info_about_desc">Version %1$s (%2$s)</string>
<string name="settings">Einstellungen</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_desc">Englisch</string>
<string name="settings_content_language_none">Keine</string> <string name="settings_content_language_none">Keine</string>
<string name="settings_prefer_subbed">Bevorzuge OmU</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_mega_fan" translatable="false">Mega Fan</string>
<string name="account_tier_ultimate_fan" translatable="false">Ultimate Fan</string> <string name="account_tier_ultimate_fan" translatable="false">Ultimate Fan</string>
<string name="settings">Settings</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_desc">English</string>
<string name="settings_content_language_none">None</string> <string name="settings_content_language_none">None</string>
<string name="settings_prefer_subbed">Prefer subbed</string> <string name="settings_prefer_subbed">Prefer subbed</string>