Use ktor instead of fuel for http requests [Part 1/2]
This commit is contained in:
		@ -79,6 +79,7 @@ dependencies {
 | 
			
		||||
    // TODO replace fuel with ktor
 | 
			
		||||
    implementation "io.ktor:ktor-client-core:$ktor_version"
 | 
			
		||||
    implementation "io.ktor:ktor-client-android:$ktor_version"
 | 
			
		||||
    implementation "io.ktor:ktor-client-serialization:$ktor_version"
 | 
			
		||||
 | 
			
		||||
    testImplementation 'junit:junit:4.13.2'
 | 
			
		||||
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
 | 
			
		||||
 | 
			
		||||
@ -8,12 +8,15 @@ import com.github.kittinunf.fuel.json.FuelJson
 | 
			
		||||
import com.github.kittinunf.fuel.json.responseJson
 | 
			
		||||
import com.github.kittinunf.result.Result
 | 
			
		||||
import io.ktor.client.*
 | 
			
		||||
import io.ktor.client.features.json.*
 | 
			
		||||
import io.ktor.client.features.json.serializer.*
 | 
			
		||||
import io.ktor.client.request.*
 | 
			
		||||
import io.ktor.client.statement.*
 | 
			
		||||
import io.ktor.http.*
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import kotlinx.serialization.decodeFromString
 | 
			
		||||
import kotlinx.serialization.SerializationException
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import kotlinx.serialization.json.JsonObject
 | 
			
		||||
import kotlinx.serialization.json.buildJsonObject
 | 
			
		||||
import kotlinx.serialization.json.put
 | 
			
		||||
import org.mosad.teapod.preferences.EncryptedPreferences
 | 
			
		||||
@ -25,7 +28,11 @@ private val json = Json { ignoreUnknownKeys = true }
 | 
			
		||||
object Crunchyroll {
 | 
			
		||||
    private val TAG = javaClass.name
 | 
			
		||||
 | 
			
		||||
    private val client = HttpClient()
 | 
			
		||||
    private val client = HttpClient {
 | 
			
		||||
        install(JsonFeature) {
 | 
			
		||||
            serializer = KotlinxSerializer(json)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    private const val baseUrl = "https://beta-api.crunchyroll.com"
 | 
			
		||||
 | 
			
		||||
    private var accessToken = ""
 | 
			
		||||
@ -100,31 +107,56 @@ object Crunchyroll {
 | 
			
		||||
     * Requests: get, post, delete
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    private suspend fun request(
 | 
			
		||||
    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(
 | 
			
		||||
        endpoint: String,
 | 
			
		||||
        params: Parameters = listOf(),
 | 
			
		||||
        url: String = ""
 | 
			
		||||
    ): Result<FuelJson, FuelError> = coroutineScope {
 | 
			
		||||
    ): T = coroutineScope {
 | 
			
		||||
        val path = url.ifEmpty { "$baseUrl$endpoint" }
 | 
			
		||||
        if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
 | 
			
		||||
 | 
			
		||||
        return@coroutineScope (Dispatchers.IO) {
 | 
			
		||||
            val (request, response, result) = Fuel.get(path, params)
 | 
			
		||||
                .header("Authorization", "$tokenType $accessToken")
 | 
			
		||||
                .responseJson()
 | 
			
		||||
 | 
			
		||||
//            println("request request: $request")
 | 
			
		||||
//            println("request response: $response")
 | 
			
		||||
//            println("request result: $result")
 | 
			
		||||
 | 
			
		||||
            result
 | 
			
		||||
            client.request(path) {
 | 
			
		||||
                method = HttpMethod.Get
 | 
			
		||||
                header("Authorization", "$tokenType $accessToken")
 | 
			
		||||
                params.forEach {
 | 
			
		||||
                    parameter(it.first, it.second)
 | 
			
		||||
                }
 | 
			
		||||
            } as T
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun requestPost(
 | 
			
		||||
        endpoint: String,
 | 
			
		||||
        params: Parameters = listOf(),
 | 
			
		||||
        requestBody: String
 | 
			
		||||
        bodyObject: JsonObject
 | 
			
		||||
    ) = coroutineScope {
 | 
			
		||||
        val path = "$baseUrl$endpoint"
 | 
			
		||||
        if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
 | 
			
		||||
@ -132,7 +164,7 @@ object Crunchyroll {
 | 
			
		||||
        withContext(Dispatchers.IO) {
 | 
			
		||||
            val response: HttpResponse = client.request(path) {
 | 
			
		||||
                method = HttpMethod.Post
 | 
			
		||||
                body = requestBody
 | 
			
		||||
                body = bodyObject
 | 
			
		||||
                header("Authorization", "$tokenType $accessToken")
 | 
			
		||||
                contentType(ContentType.Application.Json)
 | 
			
		||||
                params.forEach {
 | 
			
		||||
@ -140,14 +172,14 @@ object Crunchyroll {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Log.i(TAG, "Response status: ${response.status}")
 | 
			
		||||
            Log.i(TAG, "Response: $response")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun requestPatch(
 | 
			
		||||
        endpoint: String,
 | 
			
		||||
        params: Parameters = listOf(),
 | 
			
		||||
        requestBody: String
 | 
			
		||||
        bodyObject: JsonObject
 | 
			
		||||
    ) = coroutineScope {
 | 
			
		||||
        val path = "$baseUrl$endpoint"
 | 
			
		||||
        if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
 | 
			
		||||
@ -155,7 +187,7 @@ object Crunchyroll {
 | 
			
		||||
        withContext(Dispatchers.IO) {
 | 
			
		||||
            val response: HttpResponse = client.request(path) {
 | 
			
		||||
                method = HttpMethod.Patch
 | 
			
		||||
                body = requestBody
 | 
			
		||||
                body = bodyObject
 | 
			
		||||
                header("Authorization", "$tokenType $accessToken")
 | 
			
		||||
                contentType(ContentType.Application.Json)
 | 
			
		||||
                params.forEach {
 | 
			
		||||
@ -163,7 +195,7 @@ object Crunchyroll {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Log.i(TAG, "Response status: ${response.status}")
 | 
			
		||||
            Log.i(TAG, "Response: $response")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -176,9 +208,15 @@ object Crunchyroll {
 | 
			
		||||
        if (System.currentTimeMillis() > tokenValidUntil) refreshToken()
 | 
			
		||||
 | 
			
		||||
        withContext(Dispatchers.IO) {
 | 
			
		||||
            Fuel.delete(path, params)
 | 
			
		||||
                .header("Authorization", "$tokenType $accessToken")
 | 
			
		||||
                .response() // without a response, crunchy doesn't accept the request
 | 
			
		||||
            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")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -193,17 +231,15 @@ object Crunchyroll {
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun index() {
 | 
			
		||||
        val indexEndpoint = "/index/v2"
 | 
			
		||||
        val result = request(indexEndpoint)
 | 
			
		||||
 | 
			
		||||
        result.component1()?.obj()?.getJSONObject("cms")?.let {
 | 
			
		||||
            policy = it.get("policy").toString()
 | 
			
		||||
            signature = it.get("signature").toString()
 | 
			
		||||
            keyPairID = it.get("key_pair_id").toString()
 | 
			
		||||
        }
 | 
			
		||||
        val index: Index = requestGet(indexEndpoint)
 | 
			
		||||
        policy = index.cms.policy
 | 
			
		||||
        signature = index.cms.signature
 | 
			
		||||
        keyPairID = index.cms.keyPairId
 | 
			
		||||
 | 
			
		||||
        println("policy: $policy")
 | 
			
		||||
        println("signature: $signature")
 | 
			
		||||
        println("keyPairID: $keyPairID")
 | 
			
		||||
        Log.i(TAG, "Policy : $policy")
 | 
			
		||||
        Log.i(TAG, "Signature : $signature")
 | 
			
		||||
        Log.i(TAG, "Key Pair ID : $keyPairID")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -214,18 +250,22 @@ object Crunchyroll {
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun account() {
 | 
			
		||||
        val indexEndpoint = "/accounts/v1/me"
 | 
			
		||||
        val result = request(indexEndpoint)
 | 
			
		||||
 | 
			
		||||
        result.component1()?.obj()?.let {
 | 
			
		||||
            accountID = it.get("account_id").toString()
 | 
			
		||||
        val account: Account = try {
 | 
			
		||||
            requestGet(indexEndpoint)
 | 
			
		||||
        } catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in account(). This is bad!", ex)
 | 
			
		||||
            NoneAccount
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        accountID = account.accountId
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * General element/media functions: browse, search, objects, season_list
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    // TODO locale de-DE, categories
 | 
			
		||||
    // TODO categories
 | 
			
		||||
    /**
 | 
			
		||||
     * Browse the media available on crunchyroll.
 | 
			
		||||
     *
 | 
			
		||||
@ -241,7 +281,12 @@ object Crunchyroll {
 | 
			
		||||
        n: Int = 10
 | 
			
		||||
    ): BrowseResult {
 | 
			
		||||
        val browseEndpoint = "/content/v1/browse"
 | 
			
		||||
        val noneOptParams = listOf("sort_by" to sortBy.str, "start" to start, "n" to n)
 | 
			
		||||
        val noneOptParams = listOf(
 | 
			
		||||
            "locale" to locale,
 | 
			
		||||
            "sort_by" to sortBy.str,
 | 
			
		||||
            "start" to start,
 | 
			
		||||
            "n" to n
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        // if a season tag is present add it to the parameters
 | 
			
		||||
        val parameters = if (seasonTag.isNotEmpty()) {
 | 
			
		||||
@ -250,10 +295,12 @@ object Crunchyroll {
 | 
			
		||||
            noneOptParams
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val result = request(browseEndpoint, parameters)
 | 
			
		||||
        val browseResult = result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneBrowseResult
 | 
			
		||||
        val browseResult: BrowseResult = try {
 | 
			
		||||
            requestGet(browseEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in browse().", ex)
 | 
			
		||||
            NoneBrowseResult
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // add results to cache TODO improve
 | 
			
		||||
        browsingCache.clear()
 | 
			
		||||
@ -269,13 +316,15 @@ object Crunchyroll {
 | 
			
		||||
        val searchEndpoint = "/content/v1/search"
 | 
			
		||||
        val parameters = listOf("q" to query, "n" to n, "locale" to locale, "type" to "series")
 | 
			
		||||
 | 
			
		||||
        val result = request(searchEndpoint, parameters)
 | 
			
		||||
        // TODO episodes have thumbnails as image, and not poster_tall/poster_tall,
 | 
			
		||||
        // to work around this, for now only tv shows are supported
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneSearchResult
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(searchEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in search(), with query = \"$query\".", ex)
 | 
			
		||||
            NoneSearchResult
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -294,11 +343,12 @@ object Crunchyroll {
 | 
			
		||||
            "Key-Pair-Id" to keyPairID
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val result = request(episodesEndpoint, parameters)
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneCollection
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(episodesEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in objects().", ex)
 | 
			
		||||
            NoneCollection
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -309,11 +359,12 @@ object Crunchyroll {
 | 
			
		||||
        val seasonListEndpoint = "/content/v1/season_list"
 | 
			
		||||
        val parameters = listOf("locale" to locale)
 | 
			
		||||
 | 
			
		||||
        val result = request(seasonListEndpoint, parameters)
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneDiscSeasonList
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(seasonListEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in seasonList().", ex)
 | 
			
		||||
            NoneDiscSeasonList
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -332,11 +383,12 @@ object Crunchyroll {
 | 
			
		||||
            "Key-Pair-Id" to keyPairID
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val result = request(seriesEndpoint, parameters)
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneSeries
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(seriesEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in series().", ex)
 | 
			
		||||
            NoneSeries
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -349,15 +401,16 @@ object Crunchyroll {
 | 
			
		||||
            "locale" to locale
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val result = request(upNextSeriesEndpoint, parameters)
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneUpNextSeriesItem
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(upNextSeriesEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in upNextSeries().", ex)
 | 
			
		||||
            NoneUpNextSeriesItem
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun seasons(seriesId: String): Seasons {
 | 
			
		||||
        val episodesEndpoint = "/cms/v2/$country/M3/crunchyroll/seasons"
 | 
			
		||||
        val seasonsEndpoint = "/cms/v2/$country/M3/crunchyroll/seasons"
 | 
			
		||||
        val parameters = listOf(
 | 
			
		||||
            "series_id" to seriesId,
 | 
			
		||||
            "locale" to locale,
 | 
			
		||||
@ -366,11 +419,12 @@ object Crunchyroll {
 | 
			
		||||
            "Key-Pair-Id" to keyPairID
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val result = request(episodesEndpoint, parameters)
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneSeasons
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(seasonsEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in seasons().", ex)
 | 
			
		||||
            NoneSeasons
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun episodes(seasonId: String): Episodes {
 | 
			
		||||
@ -383,19 +437,21 @@ object Crunchyroll {
 | 
			
		||||
            "Key-Pair-Id" to keyPairID
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val result = request(episodesEndpoint, parameters)
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneEpisodes
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(episodesEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in episodes().", ex)
 | 
			
		||||
            NoneEpisodes
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun playback(url: String): Playback {
 | 
			
		||||
        val result = request("", url = url)
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NonePlayback
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet("", url = url)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in playback(), with url = $url.", ex)
 | 
			
		||||
            NonePlayback
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -412,10 +468,13 @@ object Crunchyroll {
 | 
			
		||||
        val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
 | 
			
		||||
        val parameters = listOf("locale" to locale)
 | 
			
		||||
 | 
			
		||||
        val result = request(watchlistSeriesEndpoint, parameters)
 | 
			
		||||
        // if needed implement parsing
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.has(seriesId) ?: false
 | 
			
		||||
        return try {
 | 
			
		||||
            (requestGet(watchlistSeriesEndpoint, parameters) as JsonObject)
 | 
			
		||||
                .containsKey(seriesId)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in isWatchlist() with seriesId = $seriesId", ex)
 | 
			
		||||
            false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -431,7 +490,7 @@ object Crunchyroll {
 | 
			
		||||
            put("content_id", seriesId)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        requestPost(watchlistPostEndpoint, parameters, json.toString())
 | 
			
		||||
        requestPost(watchlistPostEndpoint, parameters, json)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -458,11 +517,12 @@ object Crunchyroll {
 | 
			
		||||
        val playheadsEndpoint = "/content/v1/playheads/$accountID/${episodeIDs.joinToString(",")}"
 | 
			
		||||
        val parameters = listOf("locale" to locale)
 | 
			
		||||
 | 
			
		||||
        val result = request(playheadsEndpoint, parameters)
 | 
			
		||||
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: emptyMap()
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(playheadsEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in upNextSeries().", ex)
 | 
			
		||||
            emptyMap()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun postPlayheads(episodeId: String, playhead: Int) {
 | 
			
		||||
@ -474,7 +534,7 @@ object Crunchyroll {
 | 
			
		||||
            put("playhead", playhead)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        requestPost(playheadsEndpoint, parameters, json.toString())
 | 
			
		||||
        requestPost(playheadsEndpoint, parameters, json)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -491,10 +551,12 @@ object Crunchyroll {
 | 
			
		||||
        val watchlistEndpoint = "/content/v1/$accountID/watchlist"
 | 
			
		||||
        val parameters = listOf("locale" to locale, "n" to n)
 | 
			
		||||
 | 
			
		||||
        val result = request(watchlistEndpoint, parameters)
 | 
			
		||||
        val list: ContinueWatchingList = result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneContinueWatchingList
 | 
			
		||||
        val list: ContinueWatchingList = try {
 | 
			
		||||
            requestGet(watchlistEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in watchlist().", ex)
 | 
			
		||||
            NoneContinueWatchingList
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val objects = list.items.map{ it.panel.episodeMetadata.seriesId }
 | 
			
		||||
        return objects(objects)
 | 
			
		||||
@ -510,10 +572,12 @@ object Crunchyroll {
 | 
			
		||||
        val watchlistEndpoint = "/content/v1/$accountID/up_next_account"
 | 
			
		||||
        val parameters = listOf("locale" to locale, "n" to n)
 | 
			
		||||
 | 
			
		||||
        val result = request(watchlistEndpoint, parameters)
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneContinueWatchingList
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(watchlistEndpoint, parameters)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in upNextAccount().", ex)
 | 
			
		||||
            NoneContinueWatchingList
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -523,10 +587,12 @@ object Crunchyroll {
 | 
			
		||||
    suspend fun profile(): Profile {
 | 
			
		||||
        val profileEndpoint = "/accounts/v1/me/profile"
 | 
			
		||||
 | 
			
		||||
        val result = request(profileEndpoint)
 | 
			
		||||
        return result.component1()?.obj()?.let {
 | 
			
		||||
            json.decodeFromString(it.toString())
 | 
			
		||||
        } ?: NoneProfile
 | 
			
		||||
        return try {
 | 
			
		||||
            requestGet(profileEndpoint)
 | 
			
		||||
        }catch (ex: SerializationException) {
 | 
			
		||||
            Log.e(TAG, "SerializationException in profile().", ex)
 | 
			
		||||
            NoneProfile
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun postPrefSubLanguage(languageTag: String) {
 | 
			
		||||
@ -535,7 +601,7 @@ object Crunchyroll {
 | 
			
		||||
            put("preferred_content_subtitle_language", languageTag)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        requestPatch(profileEndpoint, requestBody = json.toString())
 | 
			
		||||
        requestPatch(profileEndpoint, bodyObject = json)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,33 @@ enum class SortBy(val str: String) {
 | 
			
		||||
    POPULARITY("popularity")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * index, account. This must pe present for the app to work!
 | 
			
		||||
 */
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Index(
 | 
			
		||||
    @SerialName("cms") val cms: CMS,
 | 
			
		||||
    @SerialName("service_available") val serviceAvailable: Boolean,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class CMS(
 | 
			
		||||
    @SerialName("bucket") val bucket: String,
 | 
			
		||||
    @SerialName("policy") val policy: String,
 | 
			
		||||
    @SerialName("signature") val signature: String,
 | 
			
		||||
    @SerialName("key_pair_id") val keyPairId: String,
 | 
			
		||||
    @SerialName("expires") val expires: String,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class Account(
 | 
			
		||||
    @SerialName("account_id") val accountId: String,
 | 
			
		||||
    @SerialName("external_id") val externalId: String,
 | 
			
		||||
    @SerialName("email_verified") val emailVerified: Boolean,
 | 
			
		||||
    @SerialName("created") val created: String,
 | 
			
		||||
)
 | 
			
		||||
val NoneAccount = Account("", "", false, "")
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * search, browse, DiscSeasonList, Watchlist, ContinueWatchingList data types all use Collection
 | 
			
		||||
 */
 | 
			
		||||
@ -47,10 +74,10 @@ typealias ContinueWatchingList = Collection<ContinueWatchingItem>
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class UpNextSeriesItem(
 | 
			
		||||
    val playhead: Int,
 | 
			
		||||
    val fully_watched: Boolean,
 | 
			
		||||
    val never_watched: Boolean,
 | 
			
		||||
    val panel: EpisodePanel,
 | 
			
		||||
    @SerialName("playhead") val playhead: Int,
 | 
			
		||||
    @SerialName("fully_watched") val fullyWatched: Boolean,
 | 
			
		||||
    @SerialName("never_watched") val neverWatched: Boolean,
 | 
			
		||||
    @SerialName("panel") val panel: EpisodePanel,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -140,7 +167,7 @@ val NoneBrowseResult = BrowseResult(0, emptyList())
 | 
			
		||||
val NoneDiscSeasonList = DiscSeasonList(0, emptyList())
 | 
			
		||||
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
 | 
			
		||||
 | 
			
		||||
val NoneUpNextSeriesItem =UpNextSeriesItem(0, false, false, NoneEpisodePanel)
 | 
			
		||||
val NoneUpNextSeriesItem = UpNextSeriesItem(0, false, false, NoneEpisodePanel)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Series data type
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user