2021-12-04 19:55:26 +01:00
package org.mosad.teapod.parser.crunchyroll
2021-12-05 00:42:56 +01:00
import android.util.Log
2021-12-04 19:55:26 +01:00
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.core.FuelError
2021-12-05 00:42:56 +01:00
import com.github.kittinunf.fuel.core.Parameters
2021-12-04 19:55:26 +01:00
import com.github.kittinunf.fuel.json.FuelJson
import com.github.kittinunf.fuel.json.responseJson
import com.github.kittinunf.result.Result
2022-03-04 20:29:37 +01:00
import io.ktor.client.*
2022-03-05 19:22:47 +01:00
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
2022-03-04 20:29:37 +01:00
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
2021-12-04 19:55:26 +01:00
import kotlinx.coroutines.*
2022-03-05 19:22:47 +01:00
import kotlinx.serialization.SerializationException
2021-12-04 19:55:26 +01:00
import kotlinx.serialization.json.Json
2022-03-05 19:22:47 +01:00
import kotlinx.serialization.json.JsonObject
2022-01-02 22:39:31 +01:00
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
2022-01-30 12:22:25 +01:00
import org.mosad.teapod.preferences.EncryptedPreferences
2021-12-28 20:32:44 +01:00
import org.mosad.teapod.preferences.Preferences
2022-01-05 01:28:39 +01:00
import org.mosad.teapod.util.concatenate
2021-12-04 19:55:26 +01:00
private val json = Json { ignoreUnknownKeys = true }
2021-12-05 00:42:56 +01:00
object Crunchyroll {
2022-03-04 20:29:37 +01:00
private val TAG = javaClass . name
2021-12-04 19:55:26 +01:00
2022-03-05 19:22:47 +01:00
private val client = HttpClient {
install ( JsonFeature ) {
serializer = KotlinxSerializer ( json )
}
}
2021-12-20 22:14:58 +01:00
private const val baseUrl = " https://beta-api.crunchyroll.com "
2021-12-04 19:55:26 +01:00
private var accessToken = " "
private var tokenType = " "
2022-01-30 12:22:25 +01:00
private var tokenValidUntil : Long = 0
2021-12-04 19:55:26 +01:00
2022-01-02 22:39:31 +01:00
private var accountID = " "
2021-12-05 01:34:06 +01:00
private var policy = " "
private var signature = " "
private var keyPairID = " "
2021-12-20 22:14:58 +01:00
// TODO temp helper vary
2021-12-28 20:32:44 +01:00
private var locale : String = Preferences . preferredLocal . toLanguageTag ( )
private var country : String = Preferences . preferredLocal . country
2021-12-20 22:14:58 +01:00
2021-12-28 20:32:44 +01:00
private val browsingCache = arrayListOf < Item > ( )
2021-12-20 22:14:58 +01:00
2022-01-08 19:20:21 +01:00
/ * *
* Login to the crunchyroll API .
*
* @param username The Username / Email of the user to log in
* @param password The Accounts Password
*
* @return Boolean : True if login was successful , else false
* /
2021-12-04 19:55:26 +01:00
fun login ( username : String , password : String ) : Boolean = runBlocking {
val tokenEndpoint = " /auth/v1/token "
val formData = listOf (
" username " to username ,
" password " to password ,
" grant_type " to " password " ,
" scope " to " offline_access "
)
2022-01-08 19:20:21 +01:00
var success : Boolean // is false
2021-12-04 19:55:26 +01:00
withContext ( Dispatchers . IO ) {
val ( request , response , result ) = Fuel . post ( " $baseUrl $tokenEndpoint " , parameters = formData )
. header ( " Content-Type " , " application/x-www-form-urlencoded " )
. appendHeader (
" Authorization " ,
" Basic "
)
. responseJson ( )
2021-12-05 01:34:06 +01:00
// TODO fix JSONException: No value for
2021-12-04 19:55:26 +01:00
result . component1 ( ) ?. obj ( ) ?. let {
accessToken = it . get ( " access_token " ) . toString ( )
tokenType = it . get ( " token_type " ) . toString ( )
2022-01-30 12:22:25 +01:00
// token will be invalid 1 sec
val expiresIn = ( it . get ( " expires_in " ) . toString ( ) . toLong ( ) - 1 )
tokenValidUntil = System . currentTimeMillis ( ) + ( expiresIn * 1000 )
2021-12-04 19:55:26 +01:00
}
// println("request: $request")
// println("response: $response")
// println("response: $result")
2022-03-04 20:29:37 +01:00
Log . i ( TAG , " login complete with code ${response.statusCode} " )
2022-01-08 19:20:21 +01:00
success = ( response . statusCode == 200 )
2021-12-04 19:55:26 +01:00
}
2022-01-08 19:20:21 +01:00
return @runBlocking success
2021-12-04 19:55:26 +01:00
}
2022-01-30 12:22:25 +01:00
private fun refreshToken ( ) {
login ( EncryptedPreferences . login , EncryptedPreferences . password )
}
2022-01-06 18:39:23 +01:00
/ * *
* Requests : get , post , delete
* /
2022-03-05 19:22:47 +01:00
private suspend inline fun < reified T > request (
url : String ,
httpMethod : HttpMethod ,
params : Parameters = listOf ( ) ,
bodyA : Any = Any ( )
) : T = coroutineScope {
if ( System . currentTimeMillis ( ) > tokenValidUntil ) refreshToken ( )
return @coroutineScope ( Dispatchers . IO ) {
val response : T = client . request ( url ) {
method = httpMethod
body = bodyA
header ( " Authorization " , " $tokenType $accessToken " )
params . forEach {
parameter ( it . first , it . second )
}
// for json body set content type
if ( bodyA is JsonObject ) {
contentType ( ContentType . Application . Json )
}
}
response
}
}
private suspend inline fun < reified T > requestGet (
2021-12-20 22:14:58 +01:00
endpoint : String ,
params : Parameters = listOf ( ) ,
url : String = " "
2022-03-05 19:22:47 +01:00
) : T = coroutineScope {
2022-01-30 12:22:25 +01:00
val path = url . ifEmpty { " $baseUrl $endpoint " }
if ( System . currentTimeMillis ( ) > tokenValidUntil ) refreshToken ( )
2021-12-20 22:14:58 +01:00
2021-12-04 19:55:26 +01:00
return @coroutineScope ( Dispatchers . IO ) {
2022-03-05 19:22:47 +01:00
client . request ( path ) {
method = HttpMethod . Get
header ( " Authorization " , " $tokenType $accessToken " )
params . forEach {
parameter ( it . first , it . second )
}
} as T
2021-12-04 19:55:26 +01:00
}
}
2022-01-02 22:39:31 +01:00
private suspend fun requestPost (
endpoint : String ,
params : Parameters = listOf ( ) ,
2022-03-05 19:22:47 +01:00
bodyObject : JsonObject
2022-01-02 22:39:31 +01:00
) = coroutineScope {
val path = " $baseUrl $endpoint "
2022-01-30 12:22:25 +01:00
if ( System . currentTimeMillis ( ) > tokenValidUntil ) refreshToken ( )
2022-01-02 22:39:31 +01:00
withContext ( Dispatchers . IO ) {
2022-03-04 20:29:37 +01:00
val response : HttpResponse = client . request ( path ) {
method = HttpMethod . Post
2022-03-05 19:22:47 +01:00
body = bodyObject
2022-03-04 20:29:37 +01:00
header ( " Authorization " , " $tokenType $accessToken " )
contentType ( ContentType . Application . Json )
params . forEach {
parameter ( it . first , it . second )
}
}
2022-03-05 19:22:47 +01:00
Log . i ( TAG , " Response: $response " )
2022-03-04 20:29:37 +01:00
}
}
private suspend fun requestPatch (
endpoint : String ,
params : Parameters = listOf ( ) ,
2022-03-05 19:22:47 +01:00
bodyObject : JsonObject
2022-03-04 20:29:37 +01:00
) = coroutineScope {
val path = " $baseUrl $endpoint "
if ( System . currentTimeMillis ( ) > tokenValidUntil ) refreshToken ( )
withContext ( Dispatchers . IO ) {
val response : HttpResponse = client . request ( path ) {
method = HttpMethod . Patch
2022-03-05 19:22:47 +01:00
body = bodyObject
2022-03-04 20:29:37 +01:00
header ( " Authorization " , " $tokenType $accessToken " )
contentType ( ContentType . Application . Json )
params . forEach {
parameter ( it . first , it . second )
}
}
2022-03-05 19:22:47 +01:00
Log . i ( TAG , " Response: $response " )
2022-01-02 22:39:31 +01:00
}
}
private suspend fun requestDelete (
endpoint : String ,
params : Parameters = listOf ( ) ,
url : String = " "
) = coroutineScope {
2022-01-30 12:22:25 +01:00
val path = url . ifEmpty { " $baseUrl $endpoint " }
if ( System . currentTimeMillis ( ) > tokenValidUntil ) refreshToken ( )
2022-01-02 22:39:31 +01:00
withContext ( Dispatchers . IO ) {
2022-03-05 19:22:47 +01:00
val response : HttpResponse = client . request ( path ) {
method = HttpMethod . Delete
header ( " Authorization " , " $tokenType $accessToken " )
params . forEach {
parameter ( it . first , it . second )
}
}
Log . i ( TAG , " Response : $response " )
2022-01-02 22:39:31 +01:00
}
}
/ * *
* Basic functions : index , account
* Needed for other functions to work properly !
* /
2021-12-20 22:14:58 +01:00
/ * *
* Retrieve the identifiers necessary for streaming . If the identifiers are
* retrieved , set the corresponding global var . The identifiers are valid for 24 h .
* /
suspend fun index ( ) {
val indexEndpoint = " /index/v2 "
2022-03-05 19:22:47 +01:00
val index : Index = requestGet ( indexEndpoint )
policy = index . cms . policy
signature = index . cms . signature
keyPairID = index . cms . keyPairId
2021-12-20 22:14:58 +01:00
2022-03-05 19:22:47 +01:00
Log . i ( TAG , " Policy : $policy " )
Log . i ( TAG , " Signature : $signature " )
Log . i ( TAG , " Key Pair ID : $keyPairID " )
2021-12-20 22:14:58 +01:00
}
2021-12-04 19:55:26 +01:00
2022-01-02 22:39:31 +01:00
/ * *
* Retrieve the account id and set the corresponding global var .
* The account id is needed for other calls .
*
* This must be execute on every start for teapod to work properly !
* /
suspend fun account ( ) {
val indexEndpoint = " /accounts/v1/me "
2022-03-05 19:22:47 +01:00
val account : Account = try {
requestGet ( indexEndpoint )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in account(). This is bad! " , ex )
NoneAccount
2022-01-02 22:39:31 +01:00
}
2022-03-05 19:22:47 +01:00
accountID = account . accountId
2022-01-02 22:39:31 +01:00
}
/ * *
2022-01-06 18:39:23 +01:00
* General element / media functions : browse , search , objects , season _list
2022-01-02 22:39:31 +01:00
* /
2021-12-04 19:55:26 +01:00
2022-03-05 19:22:47 +01:00
// TODO categories
2021-12-05 01:34:06 +01:00
/ * *
* Browse the media available on crunchyroll .
*
* @param sortBy
* @param n Number of items to return , defaults to 10
*
* @return A * * [ BrowseResult ] * * object is returned .
* /
2022-01-05 01:28:39 +01:00
suspend fun browse (
sortBy : SortBy = SortBy . ALPHABETICAL ,
seasonTag : String = " " ,
start : Int = 0 ,
n : Int = 10
) : BrowseResult {
2021-12-04 19:55:26 +01:00
val browseEndpoint = " /content/v1/browse "
2022-03-05 19:22:47 +01:00
val noneOptParams = listOf (
" locale " to locale ,
" sort_by " to sortBy . str ,
" start " to start ,
" n " to n
)
2022-01-05 01:28:39 +01:00
// if a season tag is present add it to the parameters
2022-01-06 18:39:23 +01:00
val parameters = if ( seasonTag . isNotEmpty ( ) ) {
2022-01-05 01:28:39 +01:00
concatenate ( noneOptParams , listOf ( " season_tag " to seasonTag ) )
} else {
noneOptParams
}
2021-12-04 19:55:26 +01:00
2022-03-05 19:22:47 +01:00
val browseResult : BrowseResult = try {
requestGet ( browseEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in browse(). " , ex )
NoneBrowseResult
}
2021-12-04 19:55:26 +01:00
2021-12-20 22:14:58 +01:00
// add results to cache TODO improve
browsingCache . clear ( )
browsingCache . addAll ( browseResult . items )
2021-12-04 19:55:26 +01:00
2021-12-20 22:14:58 +01:00
return browseResult
2021-12-04 19:55:26 +01:00
}
2021-12-27 22:50:29 +01:00
/ * *
* TODO
* /
suspend fun search ( query : String , n : Int = 10 ) : SearchResult {
2021-12-04 19:55:26 +01:00
val searchEndpoint = " /content/v1/search "
2021-12-27 22:50:29 +01:00
val parameters = listOf ( " q " to query , " n " to n , " locale " to locale , " type " to " series " )
2021-12-04 19:55:26 +01:00
2021-12-27 22:50:29 +01:00
// TODO episodes have thumbnails as image, and not poster_tall/poster_tall,
// to work around this, for now only tv shows are supported
2021-12-04 19:55:26 +01:00
2022-03-05 19:22:47 +01:00
return try {
requestGet ( searchEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in search(), with query = \" $query \" . " , ex )
NoneSearchResult
}
2021-12-04 19:55:26 +01:00
}
2022-01-03 14:10:41 +01:00
/ * *
* Get a collection of series objects .
* Note : episode objects are currently not supported
*
* @param objects The object IDs as list of Strings
2022-01-05 01:28:39 +01:00
* @return A * * [ Collection ] * * of Panels
2022-01-03 14:10:41 +01:00
* /
2022-01-06 18:39:23 +01:00
suspend fun objects ( objects : List < String > ) : Collection < Item > {
2022-01-03 14:10:41 +01:00
val episodesEndpoint = " /cms/v2/DE/M3/crunchyroll/objects/ ${objects.joinToString(",")} "
val parameters = listOf (
" locale " to locale ,
" Signature " to signature ,
" Policy " to policy ,
" Key-Pair-Id " to keyPairID
)
2022-03-05 19:22:47 +01:00
return try {
requestGet ( episodesEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in objects(). " , ex )
NoneCollection
}
2022-01-03 14:10:41 +01:00
}
2022-01-06 18:39:23 +01:00
/ * *
* List all available seasons as * * [ SeasonListItem ] * * .
* /
@Suppress ( " unused " )
suspend fun seasonList ( ) : DiscSeasonList {
val seasonListEndpoint = " /content/v1/season_list "
val parameters = listOf ( " locale " to locale )
2022-03-05 19:22:47 +01:00
return try {
requestGet ( seasonListEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in seasonList(). " , ex )
NoneDiscSeasonList
}
2022-01-06 18:39:23 +01:00
}
/ * *
* Main media functions : series , season , episodes , playback
* /
2021-12-05 01:34:06 +01:00
/ * *
2021-12-20 22:14:58 +01:00
* series id == crunchyroll id ?
2021-12-05 01:34:06 +01:00
* /
2021-12-20 22:14:58 +01:00
suspend fun series ( seriesId : String ) : Series {
val seriesEndpoint = " /cms/v2/ $country /M3/crunchyroll/series/ $seriesId "
val parameters = listOf (
" locale " to locale ,
" Signature " to signature ,
" Policy " to policy ,
" Key-Pair-Id " to keyPairID
)
2021-12-05 01:34:06 +01:00
2022-03-05 19:22:47 +01:00
return try {
requestGet ( seriesEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in series(). " , ex )
NoneSeries
}
2021-12-20 22:14:58 +01:00
}
2022-01-09 18:41:23 +01:00
/ * *
* TODO
* /
suspend fun upNextSeries ( seriesId : String ) : UpNextSeriesItem {
val upNextSeriesEndpoint = " /content/v1/up_next_series "
val parameters = listOf (
" series_id " to seriesId ,
" locale " to locale
)
2022-03-05 19:22:47 +01:00
return try {
requestGet ( upNextSeriesEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in upNextSeries(). " , ex )
NoneUpNextSeriesItem
}
2022-01-09 18:41:23 +01:00
}
2021-12-20 22:14:58 +01:00
suspend fun seasons ( seriesId : String ) : Seasons {
2022-03-05 19:22:47 +01:00
val seasonsEndpoint = " /cms/v2/ $country /M3/crunchyroll/seasons "
2021-12-20 22:14:58 +01:00
val parameters = listOf (
" series_id " to seriesId ,
" locale " to locale ,
" Signature " to signature ,
" Policy " to policy ,
" Key-Pair-Id " to keyPairID
)
2022-03-05 19:22:47 +01:00
return try {
requestGet ( seasonsEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in seasons(). " , ex )
NoneSeasons
}
2021-12-20 22:14:58 +01:00
}
suspend fun episodes ( seasonId : String ) : Episodes {
val episodesEndpoint = " /cms/v2/ $country /M3/crunchyroll/episodes "
val parameters = listOf (
" season_id " to seasonId ,
" locale " to locale ,
" Signature " to signature ,
" Policy " to policy ,
" Key-Pair-Id " to keyPairID
)
2022-03-05 19:22:47 +01:00
return try {
requestGet ( episodesEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in episodes(). " , ex )
NoneEpisodes
}
2021-12-20 22:14:58 +01:00
}
suspend fun playback ( url : String ) : Playback {
2022-03-05 19:22:47 +01:00
return try {
requestGet ( " " , url = url )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in playback(), with url = $url . " , ex )
NonePlayback
}
2021-12-05 01:34:06 +01:00
}
2022-01-02 22:39:31 +01:00
/ * *
2022-01-03 14:10:41 +01:00
* Additional media functions : watchlist ( series ) , playhead
2022-01-02 22:39:31 +01:00
* /
/ * *
* Check if a media is in the user ' s watchlist .
*
* @param seriesId The crunchyroll series id of the media to check
2022-01-05 01:28:39 +01:00
* @return * * [ Boolean ] * * : ture if it was found , else false
2022-01-02 22:39:31 +01:00
* /
suspend fun isWatchlist ( seriesId : String ) : Boolean {
2022-01-03 14:10:41 +01:00
val watchlistSeriesEndpoint = " /content/v1/watchlist/ $accountID / $seriesId "
2022-01-02 22:39:31 +01:00
val parameters = listOf ( " locale " to locale )
2022-03-05 19:22:47 +01:00
return try {
( requestGet ( watchlistSeriesEndpoint , parameters ) as JsonObject )
. containsKey ( seriesId )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in isWatchlist() with seriesId = $seriesId " , ex )
false
}
2022-01-02 22:39:31 +01:00
}
/ * *
* Add a media to the user ' s watchlist .
*
* @param seriesId The crunchyroll series id of the media to check
* /
suspend fun postWatchlist ( seriesId : String ) {
2022-01-03 14:10:41 +01:00
val watchlistPostEndpoint = " /content/v1/watchlist/ $accountID "
2022-01-02 22:39:31 +01:00
val parameters = listOf ( " locale " to locale )
val json = buildJsonObject {
put ( " content_id " , seriesId )
}
2022-03-05 19:22:47 +01:00
requestPost ( watchlistPostEndpoint , parameters , json )
2022-01-02 22:39:31 +01:00
}
/ * *
* Remove a media from the user ' s watchlist .
*
* @param seriesId The crunchyroll series id of the media to check
* /
suspend fun deleteWatchlist ( seriesId : String ) {
2022-01-03 14:10:41 +01:00
val watchlistDeleteEndpoint = " /content/v1/watchlist/ $accountID / $seriesId "
2022-01-02 22:39:31 +01:00
val parameters = listOf ( " locale " to locale )
2022-01-03 14:10:41 +01:00
requestDelete ( watchlistDeleteEndpoint , parameters )
2022-01-02 22:39:31 +01:00
}
/ * *
2022-01-05 01:28:39 +01:00
* Get playhead information for all episodes in episodeIDs .
* The Information returned contains the playhead position , watched state
* and last modified date .
*
* @param episodeIDs A * * [ List ] * * of episodes IDs as strings .
* @return A * * [ Map ] * * < String , * * [ PlayheadObject ] * * > containing playback info .
2022-01-02 22:39:31 +01:00
* /
2022-01-05 01:28:39 +01:00
suspend fun playheads ( episodeIDs : List < String > ) : PlayheadsMap {
val playheadsEndpoint = " /content/v1/playheads/ $accountID / ${episodeIDs.joinToString(",")} "
val parameters = listOf ( " locale " to locale )
2022-03-05 19:22:47 +01:00
return try {
requestGet ( playheadsEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in upNextSeries(). " , ex )
emptyMap ( )
}
2022-01-02 22:39:31 +01:00
}
2022-01-09 18:41:23 +01:00
suspend fun postPlayheads ( episodeId : String , playhead : Int ) {
val playheadsEndpoint = " /content/v1/playheads/ $accountID "
val parameters = listOf ( " locale " to locale )
val json = buildJsonObject {
put ( " content_id " , episodeId )
put ( " playhead " , playhead )
}
2022-03-05 19:22:47 +01:00
requestPost ( playheadsEndpoint , parameters , json )
2022-01-09 18:41:23 +01:00
}
2022-01-03 14:10:41 +01:00
/ * *
* Listing functions : watchlist ( list ) , up _next _account
* /
2022-01-05 01:28:39 +01:00
/ * *
* List items present in the watchlist .
*
* @param n Number of items to return , defaults to 20.
* @return A * * [ Watchlist ] * * containing up to n * * [ Item ] * * .
* /
2022-01-03 14:10:41 +01:00
suspend fun watchlist ( n : Int = 20 ) : Watchlist {
val watchlistEndpoint = " /content/v1/ $accountID /watchlist "
val parameters = listOf ( " locale " to locale , " n " to n )
2022-03-05 19:22:47 +01:00
val list : ContinueWatchingList = try {
requestGet ( watchlistEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in watchlist(). " , ex )
NoneContinueWatchingList
}
2022-01-03 14:10:41 +01:00
val objects = list . items . map { it . panel . episodeMetadata . seriesId }
return objects ( objects )
}
2022-01-05 00:28:49 +01:00
/ * *
2022-01-05 01:28:39 +01:00
* 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 ] * * .
2022-01-05 00:28:49 +01:00
* /
suspend fun upNextAccount ( n : Int = 20 ) : ContinueWatchingList {
val watchlistEndpoint = " /content/v1/ $accountID /up_next_account "
val parameters = listOf ( " locale " to locale , " n " to n )
2022-03-05 19:22:47 +01:00
return try {
requestGet ( watchlistEndpoint , parameters )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in upNextAccount(). " , ex )
NoneContinueWatchingList
}
2022-01-05 00:28:49 +01:00
}
2022-02-05 20:02:33 +01:00
/ * *
* Account / Profile functions
* /
suspend fun profile ( ) : Profile {
val profileEndpoint = " /accounts/v1/me/profile "
2022-03-05 19:22:47 +01:00
return try {
requestGet ( profileEndpoint )
} catch ( ex : SerializationException ) {
Log . e ( TAG , " SerializationException in profile(). " , ex )
NoneProfile
}
2022-02-05 20:02:33 +01:00
}
2022-03-04 20:29:37 +01:00
suspend fun postPrefSubLanguage ( languageTag : String ) {
val profileEndpoint = " /accounts/v1/me/profile "
val json = buildJsonObject {
put ( " preferred_content_subtitle_language " , languageTag )
}
2022-03-05 19:22:47 +01:00
requestPatch ( profileEndpoint , bodyObject = json )
2022-03-04 20:29:37 +01:00
}
2022-01-02 17:59:23 +01:00
}