From 75204e522d220a6c805697f023a1eacb9b17e370 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 5 Mar 2022 20:41:39 +0100 Subject: [PATCH] Use ktor instead of fuel for http requests [Part 2/2] * update preferred locale in preferences, is is the actual locale implementation * update token handling for crunchy (country via token) * update TMDBApiController to use ktor * add parsable dates to NoneTMDBTVShow and NoneTMDBMovie --- app/build.gradle | 7 +- .../teapod/parser/crunchyroll/Crunchyroll.kt | 210 ++++++++---------- .../teapod/parser/crunchyroll/DataTypes.kt | 35 ++- .../mosad/teapod/preferences/Preferences.kt | 16 +- .../main/fragments/AccountFragment.kt | 9 +- .../main/viewmodel/MediaFragmentViewModel.kt | 2 +- .../ui/activity/player/PlayerViewModel.kt | 2 +- .../teapod/util/tmdb/TMDBApiController.kt | 91 +++++--- .../mosad/teapod/util/tmdb/TMDBDataTypes.kt | 4 +- app/src/main/res/values/strings.xml | 1 + 10 files changed, 205 insertions(+), 172 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6ea4001..a53fb20 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,7 +14,7 @@ android { minSdkVersion 23 targetSdkVersion 30 versionCode 4200 //00.04.200 - versionName "1.0.0-alpha4" + versionName "1.0.0-alpha5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_time", buildTime() @@ -72,11 +72,6 @@ dependencies { implementation 'com.afollestad.material-dialogs:core:3.3.0' implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0' - implementation 'com.github.kittinunf.fuel:fuel:2.3.1' - implementation 'com.github.kittinunf.fuel:fuel-android:2.3.1' - implementation 'com.github.kittinunf.fuel:fuel-json:2.3.1' - - // 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" diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt index 68d41ff..d2b24bb 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt @@ -1,16 +1,34 @@ +/** + * Teapod + * + * Copyright 2020-2022 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + package org.mosad.teapod.parser.crunchyroll import android.util.Log -import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.core.FuelError -import com.github.kittinunf.fuel.core.Parameters -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.call.* import io.ktor.client.features.json.* import io.ktor.client.features.json.serializer.* import io.ktor.client.request.* +import io.ktor.client.request.forms.* import io.ktor.client.statement.* import io.ktor.http.* import kotlinx.coroutines.* @@ -35,8 +53,7 @@ object Crunchyroll { } private const val baseUrl = "https://beta-api.crunchyroll.com" - private var accessToken = "" - private var tokenType = "" + private lateinit var token: Token private var tokenValidUntil: Long = 0 private var accountID = "" @@ -45,10 +62,6 @@ object Crunchyroll { private var signature = "" private var keyPairID = "" - // TODO temp helper vary - private var locale: String = Preferences.preferredLocal.toLanguageTag() - private var country: String = Preferences.preferredLocal.country - private val browsingCache = arrayListOf() /** @@ -61,39 +74,24 @@ object Crunchyroll { */ 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" - ) + val formData = Parameters.build { + append("username", username) + append("password", password) + append("grant_type", "password") + append("scope", "offline_access") + } - var success: Boolean // is false + var success = false// is false 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() - - // TODO fix JSONException: No value for - result.component1()?.obj()?.let { - accessToken = it.get("access_token").toString() - tokenType = it.get("token_type").toString() - - // token will be invalid 1 sec - val expiresIn = (it.get("expires_in").toString().toLong() - 1) - tokenValidUntil = System.currentTimeMillis() + (expiresIn * 1000) + // TODO handle exceptions + val response: HttpResponse = client.submitForm("$baseUrl$tokenEndpoint", formParameters = formData) { + header("Authorization", "Basic ") } + token = response.receive() + tokenValidUntil = System.currentTimeMillis() + (token.expiresIn * 1000) -// println("request: $request") -// println("response: $response") -// println("response: $result") - - Log.i(TAG, "login complete with code ${response.statusCode}") - success = (response.statusCode == 200) + Log.i(TAG, "login complete with code ${response.status}") + success = (response.status == HttpStatusCode.OK) } return@runBlocking success @@ -110,22 +108,22 @@ object Crunchyroll { private suspend inline fun request( url: String, httpMethod: HttpMethod, - params: Parameters = listOf(), - bodyA: Any = Any() + params: List> = listOf(), + bodyObject: 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") + header("Authorization", "${token.tokenType} ${token.accessToken}") params.forEach { parameter(it.first, it.second) } - // for json body set content type - if (bodyA is JsonObject) { + // for json set body and content type + if (bodyObject is JsonObject) { + body = bodyObject contentType(ContentType.Application.Json) } } @@ -136,88 +134,45 @@ object Crunchyroll { private suspend inline fun requestGet( endpoint: String, - params: Parameters = listOf(), + params: List> = listOf(), url: String = "" - ): T = coroutineScope { + ): T { val path = url.ifEmpty { "$baseUrl$endpoint" } - if (System.currentTimeMillis() > tokenValidUntil) refreshToken() - return@coroutineScope (Dispatchers.IO) { - client.request(path) { - method = HttpMethod.Get - header("Authorization", "$tokenType $accessToken") - params.forEach { - parameter(it.first, it.second) - } - } as T - } + return request(path, HttpMethod.Get, params) } private suspend fun requestPost( endpoint: String, - params: Parameters = listOf(), + params: List> = listOf(), bodyObject: JsonObject - ) = coroutineScope { + ) { val path = "$baseUrl$endpoint" - if (System.currentTimeMillis() > tokenValidUntil) refreshToken() - withContext(Dispatchers.IO) { - val response: HttpResponse = client.request(path) { - method = HttpMethod.Post - body = bodyObject - header("Authorization", "$tokenType $accessToken") - contentType(ContentType.Application.Json) - params.forEach { - parameter(it.first, it.second) - } - } - - Log.i(TAG, "Response: $response") - } + val response: HttpResponse = request(path, HttpMethod.Post, params, bodyObject) + Log.i(TAG, "Response: $response") } private suspend fun requestPatch( endpoint: String, - params: Parameters = listOf(), + params: List> = listOf(), bodyObject: JsonObject - ) = coroutineScope { + ) { val path = "$baseUrl$endpoint" - if (System.currentTimeMillis() > tokenValidUntil) refreshToken() - withContext(Dispatchers.IO) { - val response: HttpResponse = client.request(path) { - method = HttpMethod.Patch - body = bodyObject - header("Authorization", "$tokenType $accessToken") - contentType(ContentType.Application.Json) - params.forEach { - parameter(it.first, it.second) - } - } - - Log.i(TAG, "Response: $response") - } + val response: HttpResponse = request(path, HttpMethod.Patch, params, bodyObject) + Log.i(TAG, "Response: $response") } private suspend fun requestDelete( endpoint: String, - params: Parameters = listOf(), + params: List> = listOf(), url: String = "" ) = coroutineScope { val path = url.ifEmpty { "$baseUrl$endpoint" } - if (System.currentTimeMillis() > tokenValidUntil) refreshToken() - withContext(Dispatchers.IO) { - 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") - } + val response: HttpResponse = request(path, HttpMethod.Delete, params) + Log.i(TAG, "Response: $response") } /** @@ -282,7 +237,7 @@ object Crunchyroll { ): BrowseResult { val browseEndpoint = "/content/v1/browse" val noneOptParams = listOf( - "locale" to locale, + "locale" to Preferences.preferredLocale.toLanguageTag(), "sort_by" to sortBy.str, "start" to start, "n" to n @@ -314,7 +269,12 @@ object Crunchyroll { */ suspend fun search(query: String, n: Int = 10): SearchResult { val searchEndpoint = "/content/v1/search" - val parameters = listOf("q" to query, "n" to n, "locale" to locale, "type" to "series") + val parameters = listOf( + "locale" to Preferences.preferredLocale.toLanguageTag(), + "q" to query, + "n" to n, + "type" to "series" + ) // TODO episodes have thumbnails as image, and not poster_tall/poster_tall, // to work around this, for now only tv shows are supported @@ -337,7 +297,7 @@ object Crunchyroll { suspend fun objects(objects: List): Collection { val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}" val parameters = listOf( - "locale" to locale, + "locale" to Preferences.preferredLocale.toLanguageTag(), "Signature" to signature, "Policy" to policy, "Key-Pair-Id" to keyPairID @@ -357,7 +317,7 @@ object Crunchyroll { @Suppress("unused") suspend fun seasonList(): DiscSeasonList { val seasonListEndpoint = "/content/v1/season_list" - val parameters = listOf("locale" to locale) + val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag()) return try { requestGet(seasonListEndpoint, parameters) @@ -375,9 +335,9 @@ object Crunchyroll { * series id == crunchyroll id? */ suspend fun series(seriesId: String): Series { - val seriesEndpoint = "/cms/v2/$country/M3/crunchyroll/series/$seriesId" + val seriesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/series/$seriesId" val parameters = listOf( - "locale" to locale, + "locale" to Preferences.preferredLocale.toLanguageTag(), "Signature" to signature, "Policy" to policy, "Key-Pair-Id" to keyPairID @@ -398,7 +358,7 @@ object Crunchyroll { val upNextSeriesEndpoint = "/content/v1/up_next_series" val parameters = listOf( "series_id" to seriesId, - "locale" to locale + "locale" to Preferences.preferredLocale.toLanguageTag() ) return try { @@ -410,10 +370,10 @@ object Crunchyroll { } suspend fun seasons(seriesId: String): Seasons { - val seasonsEndpoint = "/cms/v2/$country/M3/crunchyroll/seasons" + val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons" val parameters = listOf( "series_id" to seriesId, - "locale" to locale, + "locale" to Preferences.preferredLocale.toLanguageTag(), "Signature" to signature, "Policy" to policy, "Key-Pair-Id" to keyPairID @@ -428,10 +388,10 @@ object Crunchyroll { } suspend fun episodes(seasonId: String): Episodes { - val episodesEndpoint = "/cms/v2/$country/M3/crunchyroll/episodes" + val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes" val parameters = listOf( "season_id" to seasonId, - "locale" to locale, + "locale" to Preferences.preferredLocale.toLanguageTag(), "Signature" to signature, "Policy" to policy, "Key-Pair-Id" to keyPairID @@ -466,7 +426,7 @@ object Crunchyroll { */ suspend fun isWatchlist(seriesId: String): Boolean { val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId" - val parameters = listOf("locale" to locale) + val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag()) return try { (requestGet(watchlistSeriesEndpoint, parameters) as JsonObject) @@ -484,7 +444,7 @@ object Crunchyroll { */ suspend fun postWatchlist(seriesId: String) { val watchlistPostEndpoint = "/content/v1/watchlist/$accountID" - val parameters = listOf("locale" to locale) + val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag()) val json = buildJsonObject { put("content_id", seriesId) @@ -500,7 +460,7 @@ object Crunchyroll { */ suspend fun deleteWatchlist(seriesId: String) { val watchlistDeleteEndpoint = "/content/v1/watchlist/$accountID/$seriesId" - val parameters = listOf("locale" to locale) + val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag()) requestDelete(watchlistDeleteEndpoint, parameters) } @@ -515,7 +475,7 @@ object Crunchyroll { */ suspend fun playheads(episodeIDs: List): PlayheadsMap { val playheadsEndpoint = "/content/v1/playheads/$accountID/${episodeIDs.joinToString(",")}" - val parameters = listOf("locale" to locale) + val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag()) return try { requestGet(playheadsEndpoint, parameters) @@ -527,7 +487,7 @@ object Crunchyroll { suspend fun postPlayheads(episodeId: String, playhead: Int) { val playheadsEndpoint = "/content/v1/playheads/$accountID" - val parameters = listOf("locale" to locale) + val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag()) val json = buildJsonObject { put("content_id", episodeId) @@ -549,7 +509,10 @@ object Crunchyroll { */ suspend fun watchlist(n: Int = 20): Watchlist { val watchlistEndpoint = "/content/v1/$accountID/watchlist" - val parameters = listOf("locale" to locale, "n" to n) + val parameters = listOf( + "locale" to Preferences.preferredLocale.toLanguageTag(), + "n" to n + ) val list: ContinueWatchingList = try { requestGet(watchlistEndpoint, parameters) @@ -570,7 +533,10 @@ object Crunchyroll { */ suspend fun upNextAccount(n: Int = 20): ContinueWatchingList { val watchlistEndpoint = "/content/v1/$accountID/up_next_account" - val parameters = listOf("locale" to locale, "n" to n) + val parameters = listOf( + "locale" to Preferences.preferredLocale.toLanguageTag(), + "n" to n + ) return try { requestGet(watchlistEndpoint, parameters) diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt index 75cacac..4702969 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt @@ -1,3 +1,25 @@ +/** + * Teapod + * + * Copyright 2020-2022 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + package org.mosad.teapod.parser.crunchyroll import kotlinx.serialization.SerialName @@ -29,8 +51,19 @@ enum class SortBy(val str: String) { } /** - * index, account. This must pe present for the app to work! + * token, index, account. This must pe present for the app to work! */ +@Serializable +data class Token( + @SerialName("access_token") val accessToken: String, + @SerialName("refresh_token") val refreshToken: String, + @SerialName("expires_in") val expiresIn: Int, + @SerialName("token_type") val tokenType: String, + @SerialName("scope") val scope: String, + @SerialName("country") val country: String, + @SerialName("account_id") val accountId: String, +) + @Serializable data class Index( @SerialName("cms") val cms: CMS, diff --git a/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt b/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt index 96440ca..fd06dbe 100644 --- a/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt +++ b/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt @@ -10,7 +10,7 @@ object Preferences { var preferSecondary = false internal set - var preferredLocal = Locale.GERMANY + var preferredLocale: Locale = Locale.forLanguageTag("en-US") // TODO this should be saved (potential offline usage) but fetched on start internal set var autoplay = true internal set @@ -35,6 +35,15 @@ object Preferences { this.preferSecondary = preferSecondary } + fun savePreferredLocal(context: Context, preferredLocale: Locale) { + with(getSharedPref(context).edit()) { + putString(context.getString(R.string.save_key_preferred_local), preferredLocale.toLanguageTag()) + apply() + } + + this.preferredLocale = preferredLocale + } + fun saveAutoplay(context: Context, autoplay: Boolean) { with(getSharedPref(context).edit()) { putBoolean(context.getString(R.string.save_key_autoplay), autoplay) @@ -71,6 +80,11 @@ object Preferences { preferSecondary = sharedPref.getBoolean( context.getString(R.string.save_key_prefer_secondary), false ) + preferredLocale = Locale.forLanguageTag( + sharedPref.getString( + context.getString(R.string.save_key_preferred_local), "en-US" + ) ?: "en-US" + ) autoplay = sharedPref.getBoolean( context.getString(R.string.save_key_autoplay), true ) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt index 68b86bb..38b4bf3 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt @@ -180,18 +180,21 @@ class AccountFragment : Fragment() { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.settings_content_language) .setSingleChoiceItems(items, initialSelection){ dialog, which -> - updatePrefContentLanguage(supportedLocals[which].toLanguageTag()) + updatePrefContentLanguage(supportedLocals[which]) dialog.dismiss() } .show() } @kotlinx.coroutines.ExperimentalCoroutinesApi - private fun updatePrefContentLanguage(languageTag: String) { + private fun updatePrefContentLanguage(preferredLocale: Locale) { lifecycleScope.launch { - Crunchyroll.postPrefSubLanguage(languageTag) + Crunchyroll.postPrefSubLanguage(preferredLocale.toLanguageTag()) }.invokeOnCompletion { + // update the local preferred content language + Preferences.savePreferredLocal(requireContext(), preferredLocale) + // update profile since the language selection might have changed profile = lifecycleScope.async { Crunchyroll.profile() } profile.invokeOnCompletion { diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt index 4c0d265..2e4a260 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt @@ -62,7 +62,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic println(upNextSeries) // load the preferred season (preferred language, language per season, not per stream) - currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocal) + currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocale) // load episodes and metaDB in parallel (tmdb needs mediaType, which is set via episodes) listOf( diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt index 087c5c0..003c944 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt @@ -83,7 +83,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) var currentPlayback = NonePlayback // current playback settings - var currentLanguage: Locale = Preferences.preferredLocal + var currentLanguage: Locale = Preferences.preferredLocale internal set init { diff --git a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt index 3a1b347..fbdcc1c 100644 --- a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt +++ b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt @@ -22,15 +22,17 @@ package org.mosad.teapod.util.tmdb -import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.core.FuelError -import com.github.kittinunf.fuel.core.Parameters -import com.github.kittinunf.fuel.json.FuelJson -import com.github.kittinunf.fuel.json.responseJson -import com.github.kittinunf.result.Result -import kotlinx.coroutines.* -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.decodeFromString +import android.util.Log +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.invoke +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import org.mosad.teapod.util.concatenate @@ -41,8 +43,14 @@ import org.mosad.teapod.util.concatenate * */ class TMDBApiController { + private val classTag = javaClass.name private val json = Json { ignoreUnknownKeys = true } + private val client = HttpClient { + install(JsonFeature) { + serializer = KotlinxSerializer(json) + } + } private val apiUrl = "https://api.themoviedb.org/3" private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2" @@ -52,19 +60,22 @@ class TMDBApiController { const val imageUrl = "https://image.tmdb.org/t/p/w500" } - private suspend fun request( + private suspend inline fun request( endpoint: String, - parameters: Parameters = emptyList() - ): Result = coroutineScope { + parameters: List> = emptyList() + ): T = coroutineScope { val path = "$apiUrl$endpoint" val params = concatenate(listOf("api_key" to apiKey, "language" to language), parameters) // TODO handle FileNotFoundException return@coroutineScope (Dispatchers.IO) { - val (_, _, result) = Fuel.get(path, params) - .responseJson() + val response: HttpResponse = client.get(path) { + params.forEach { + parameter(it.first, it.second) + } + } - result + response.receive() } } @@ -78,10 +89,12 @@ class TMDBApiController { val searchEndpoint = "/search/multi" val parameters = listOf("query" to query, "include_adult" to false) - val result = request(searchEndpoint, parameters) - return result.component1()?.obj()?.let { - json.decodeFromString(it.toString()) - } ?: NoneTMDBSearchMovie + return try { + request(searchEndpoint, parameters) + }catch (ex: SerializationException) { + Log.e(classTag, "SerializationException in searchMovie(), with query = $query.", ex) + NoneTMDBSearchMovie + } } /** @@ -94,10 +107,12 @@ class TMDBApiController { val searchEndpoint = "/search/tv" val parameters = listOf("query" to query, "include_adult" to false) - val result = request(searchEndpoint, parameters) - return result.component1()?.obj()?.let { - json.decodeFromString(it.toString()) - } ?: NoneTMDBSearchTVShow + return try { + request(searchEndpoint, parameters) + }catch (ex: SerializationException) { + Log.e(classTag, "SerializationException in searchTVShow(), with query = $query.", ex) + NoneTMDBSearchTVShow + } } /** @@ -109,10 +124,12 @@ class TMDBApiController { val movieEndpoint = "/movie/$movieId" // TODO is FileNotFoundException handling needed? - val result = request(movieEndpoint) - return result.component1()?.obj()?.let { - json.decodeFromString(it.toString()) - } ?: NoneTMDBMovie + return try { + request(movieEndpoint) + }catch (ex: SerializationException) { + Log.e(classTag, "SerializationException in getMovieDetails(), with movieId = $movieId.", ex) + NoneTMDBMovie + } } /** @@ -124,10 +141,12 @@ class TMDBApiController { val tvShowEndpoint = "/tv/$tvId" // TODO is FileNotFoundException handling needed? - val result = request(tvShowEndpoint) - return result.component1()?.obj()?.let { - json.decodeFromString(it.toString()) - } ?: NoneTMDBTVShow + return try { + request(tvShowEndpoint) + }catch (ex: SerializationException) { + Log.e(classTag, "SerializationException in getTVShowDetails(), with tvId = $tvId.", ex) + NoneTMDBTVShow + } } @Suppress("unused") @@ -141,10 +160,12 @@ class TMDBApiController { val tvShowSeasonEndpoint = "/tv/$tvId/season/$seasonNumber" // TODO is FileNotFoundException handling needed? - val result = request(tvShowSeasonEndpoint) - return result.component1()?.obj()?.let { - json.decodeFromString(it.toString()) - } ?: NoneTMDBTVSeason + return try { + request(tvShowSeasonEndpoint) + }catch (ex: SerializationException) { + Log.e(classTag, "SerializationException in getTVSeasonDetails(), with tvId = $tvId, seasonNumber = $seasonNumber.", ex) + NoneTMDBTVSeason + } } } diff --git a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt index 1c2a568..10432d9 100644 --- a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt @@ -110,8 +110,8 @@ data class TMDBTVShow( // use null for nullable types, the gui needs to handle/implement a fallback for null values val NoneTMDB = TMDBBase(0, "", "", null, null) -val NoneTMDBMovie = TMDBMovie(0, "", "", null, null, "", null, "") -val NoneTMDBTVShow = TMDBTVShow(0, "", "", null, null, "", "", "") +val NoneTMDBMovie = TMDBMovie(0, "", "", null, null, "1970-01-01", null, "") +val NoneTMDBTVShow = TMDBTVShow(0, "", "", null, null, "1970-01-01", "1970-01-01", "") @Serializable data class TMDBTVSeason( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8630404..abb68f3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -132,6 +132,7 @@ org.mosad.teapod.user_login org.mosad.teapod.user_password org.mosad.teapod.prefer_secondary + org.mosad.teapod.preferred_local org.mosad.teapod.autoplay org.mosad.teapod.dev.settings org.mosad.teapod.theme