Skip to content

Commit

Permalink
Improve the customizable HttpClient configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisKruegerDev committed Mar 4, 2023
1 parent 817b551 commit b183408
Show file tree
Hide file tree
Showing 18 changed files with 357 additions and 220 deletions.
128 changes: 80 additions & 48 deletions tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb3.kt
Original file line number Diff line number Diff line change
@@ -1,53 +1,85 @@
package app.moviebase.tmdb

import app.moviebase.tmdb.api.*
import app.moviebase.tmdb.remote.TmdbHttpClientFactory
import app.moviebase.tmdb.remote.TmdbLogLevel
import app.moviebase.tmdb.remote.TmdbSessionProvider
import io.ktor.client.*

class Tmdb3(
tmdbApiKey: String,
logLevel: TmdbLogLevel = TmdbLogLevel.NONE,
tmdbSessionProvider: TmdbSessionProvider? = null,
httpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null,
) {
private val client: HttpClient = TmdbHttpClientFactory.create(
tmdbApiKey = tmdbApiKey,
logLevel = logLevel,
httpClientConfigBlock = httpClientConfigBlock,
)
private val clientWithSession: HttpClient = TmdbHttpClientFactory.createWithSession(
tmdbApiKey = tmdbApiKey,
logLevel = logLevel,
tmdbSessionProvider = tmdbSessionProvider,
httpClientConfigBlock = httpClientConfigBlock,
)

val account = TmdbAccountApi(clientWithSession)
val authentication = TmdbAuthenticationApi(client)
val certifications = TmdbCertificationsApi(client)
val changes = TmdbChangesApi(client)
val collections = TmdbCollectionsApi(client)
val companies = TmdbCompaniesApi(client)
val configuration = TmdbConfigurationApi(client)
val credits = TmdbCreditsApi(client)
val discover = TmdbDiscoverApi(client)
val find = TmdbFindApi(client)
val genres = TmdbGenresApi(client)
val guestSessions = TmdbGuestSessionsApi(client)
val keywords = TmdbKeywordsApi(client)
val lists = TmdbListsApi(client)
val movies = TmdbMoviesApi(client)
val networks = TmdbNetworksApi(client)
val trending = TmdbTrendingApi(client)
val people = TmdbPeopleApi(client)
val reviews = TmdbReviewsApi(client)
val search = TmdbSearchApi(client)
val show = TmdbShowApi(client)
val showSeasons = TmdbShowSeasonsApi(client)
val showEpisodes = TmdbShowEpisodesApi(client)
val showEpisodeGroups = TmdbShowEpisodeGroupsApi(client)
import app.moviebase.tmdb.api.TmdbAccountApi
import app.moviebase.tmdb.api.TmdbAuthenticationApi
import app.moviebase.tmdb.api.TmdbCertificationsApi
import app.moviebase.tmdb.api.TmdbChangesApi
import app.moviebase.tmdb.api.TmdbCollectionsApi
import app.moviebase.tmdb.api.TmdbCompaniesApi
import app.moviebase.tmdb.api.TmdbConfigurationApi
import app.moviebase.tmdb.api.TmdbCreditsApi
import app.moviebase.tmdb.api.TmdbDiscoverApi
import app.moviebase.tmdb.api.TmdbFindApi
import app.moviebase.tmdb.api.TmdbGenresApi
import app.moviebase.tmdb.api.TmdbGuestSessionsApi
import app.moviebase.tmdb.api.TmdbKeywordsApi
import app.moviebase.tmdb.api.TmdbListsApi
import app.moviebase.tmdb.api.TmdbMoviesApi
import app.moviebase.tmdb.api.TmdbNetworksApi
import app.moviebase.tmdb.api.TmdbPeopleApi
import app.moviebase.tmdb.api.TmdbReviewsApi
import app.moviebase.tmdb.api.TmdbSearchApi
import app.moviebase.tmdb.api.TmdbShowApi
import app.moviebase.tmdb.api.TmdbShowEpisodeGroupsApi
import app.moviebase.tmdb.api.TmdbShowEpisodesApi
import app.moviebase.tmdb.api.TmdbShowSeasonsApi
import app.moviebase.tmdb.api.TmdbTrendingApi
import app.moviebase.tmdb.remote.TmdbDsl
import app.moviebase.tmdb.remote.buildHttpClient
import app.moviebase.tmdb.remote.interceptRequest
import io.ktor.client.HttpClient
import io.ktor.client.request.*

@TmdbDsl
fun Tmdb3(block: TmdbClientConfig.() -> Unit): Tmdb3 {
val config = TmdbClientConfig().apply(block)
return Tmdb3(config)
}

class Tmdb3 internal constructor(private val config: TmdbClientConfig) {

constructor(tmdbApiKey: String) : this(TmdbClientConfig.buildDefault(tmdbApiKey))

init {
requireNotNull(config.tmdbApiKey) {
"TMDB API key unavailable. Set the tmdbApiKey field in the class TmdbClientConfig when instantiate the TMDB client."
}
}

private val client: HttpClient by lazy {
buildHttpClient(TmdbVersion.V3, config).interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, config.tmdbApiKey)

config.tmdbCredentials?.sessionIdProvider?.get()?.let { sessionId ->
it.parameter(TmdbUrlParameter.SESSION_ID, sessionId)
}
}
}

val account: TmdbAccountApi by buildApi(::TmdbAccountApi)
val authentication by buildApi(::TmdbAuthenticationApi)
val certifications by buildApi(::TmdbCertificationsApi)
val changes by buildApi(::TmdbChangesApi)
val collections by buildApi(::TmdbCollectionsApi)
val companies by buildApi(::TmdbCompaniesApi)
val configuration by buildApi(::TmdbConfigurationApi)
val credits by buildApi(::TmdbCreditsApi)
val discover by buildApi(::TmdbDiscoverApi)
val find by buildApi(::TmdbFindApi)
val genres by buildApi(::TmdbGenresApi)
val guestSessions by buildApi(::TmdbGuestSessionsApi)
val keywords by buildApi(::TmdbKeywordsApi)
val lists by buildApi(::TmdbListsApi)
val movies by buildApi(::TmdbMoviesApi)
val networks by buildApi(::TmdbNetworksApi)
val trending by buildApi(::TmdbTrendingApi)
val people by buildApi(::TmdbPeopleApi)
val reviews by buildApi(::TmdbReviewsApi)
val search by buildApi(::TmdbSearchApi)
val show by buildApi(::TmdbShowApi)
val showSeasons by buildApi(::TmdbShowSeasonsApi)
val showEpisodes by buildApi(::TmdbShowEpisodesApi)
val showEpisodeGroups by buildApi(::TmdbShowEpisodeGroupsApi)

private fun <T> buildApi(builder: (HttpClient) -> T) = lazy { builder(client) }
}
57 changes: 36 additions & 21 deletions tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,57 @@ package app.moviebase.tmdb
import app.moviebase.tmdb.api.Tmdb4AccountApi
import app.moviebase.tmdb.api.Tmdb4AuthenticationApi
import app.moviebase.tmdb.api.Tmdb4ListApi
import app.moviebase.tmdb.remote.TmdbLogLevel
import app.moviebase.tmdb.remote.TmdbDsl
import app.moviebase.tmdb.remote.buildHttpClient
import app.moviebase.tmdb.remote.interceptRequest
import io.ktor.client.*
import io.ktor.client.request.*

class Tmdb4(
tmdbApiKey: String,
authenticationToken: String? = null,
logLevel: TmdbLogLevel = TmdbLogLevel.NONE,
httpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null,
@TmdbDsl
fun Tmdb4(block: TmdbClientConfig.() -> Unit): Tmdb4 {
val config = TmdbClientConfig().apply(block)
return Tmdb4(config)
}

class Tmdb4 internal constructor(
private val config: TmdbClientConfig
) {

var accessToken: String? = null
var requestToken: String? = null
constructor(
tmdbApiKey: String,
authenticationToken: String? = null
) : this(TmdbClientConfig.buildDefault(tmdbApiKey, authenticationToken))

init {
check(!config.tmdbApiKey.isNullOrBlank()) {
"TMDB API key is unavailable. Set the tmdbApiKey when instantiate the TMDB client."
}
}

private val client = buildHttpClient(logLevel, httpClientConfigBlock).apply {
interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, tmdbApiKey)
accessToken?.let { token ->
private val client by lazy {
buildHttpClient(TmdbVersion.V4, config).interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, config.tmdbApiKey)

config.tmdbCredentials?.accessTokenProvider?.let { token ->
it.header("Authorization", "Bearer $token")
}
}
}

private val authClient = buildHttpClient(logLevel, httpClientConfigBlock).apply {
interceptRequest {
// TODO: Install Auth client https://ktor.io/docs/auth.html
it.parameter(TmdbUrlParameter.API_KEY, tmdbApiKey)
requireNotNull(authenticationToken) { "authentication token not set for request auth endpoints" }
it.header("Authorization", "Bearer $authenticationToken")
private val authClient by lazy {
// TODO: Install new Auth plugin https://ktor.io/docs/auth.html
buildHttpClient(TmdbVersion.V4, config).interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, config.tmdbApiKey)

config.tmdbAuthenticationToken?.let { token ->
it.header("Authorization", "Bearer $token")
}
}
}

val account = Tmdb4AccountApi(client)
val auth = Tmdb4AuthenticationApi(authClient)
val list = Tmdb4ListApi(client)
val account by buildApi(::Tmdb4AccountApi)
val auth by lazy { Tmdb4AuthenticationApi(authClient) }
val list by buildApi(::Tmdb4ListApi)

private fun <T> buildApi(builder: (HttpClient) -> T) = lazy { builder(client) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package app.moviebase.tmdb

import app.moviebase.tmdb.remote.TmdbDsl
import io.ktor.client.*
import io.ktor.client.engine.*

@TmdbDsl
class TmdbClientConfig {

var tmdbApiKey: String? = null
var tmdbAuthenticationToken: String? = null
internal var tmdbCredentials: TmdbCredentials? = null

var useCache: Boolean = false
var useTimeout: Boolean = false
var maxRetriesOnException: Int? = null

internal var httpClientConfigBlock: HttpClientConfigExecution<*>? = null
internal var httpClientBuilder: (() -> HttpClient)? = null

fun tmdbAccountCredentials(block: TmdbCredentials.() -> Unit) {
tmdbCredentials = TmdbCredentials().apply(block)
}

/**
* Set custom HttpClient configuration for the default HttpClient.
*/
fun httpClient(block: HttpClientConfigExecution<*>) {
this.httpClientConfigBlock = block
}

/**
* Creates an custom [HttpClient] with the specified [HttpClientEngineFactory] and optional [block] configuration.
* Note that the TMDB config will be added afterwards.
*/
fun <T : HttpClientEngineConfig> httpClient(
engineFactory: HttpClientEngineFactory<T>,
block: HttpClientConfigExecution<T> = {}
) {
httpClientBuilder = {
HttpClient(engineFactory, block)
}
}

companion object {

internal fun buildDefault(
tmdbApiKey: String,
tmdbAuthenticationToken: String? = null
) = TmdbClientConfig().apply {
this.tmdbApiKey = tmdbApiKey
this.tmdbAuthenticationToken = tmdbAuthenticationToken
}
}
}

@TmdbDsl
class TmdbCredentials {

internal var sessionIdProvider: TmdbCredentialProvider? = null
internal var accessTokenProvider: TmdbCredentialProvider? = null
internal var requestTokenProvider: TmdbCredentialProvider? = null

fun sessionId(provider: TmdbCredentialProvider) {
sessionIdProvider = provider
}

fun accessToken(provider: TmdbCredentialProvider) {
accessTokenProvider = provider
}

fun requestToken(provider: TmdbCredentialProvider) {
requestTokenProvider = provider
}
}

@TmdbDsl
fun interface TmdbCredentialProvider {
fun get(): String?
}

@TmdbDsl
typealias HttpClientConfigExecution<T> = HttpClientConfig<T>.() -> Unit
18 changes: 12 additions & 6 deletions tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/TmdbWebConfig.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package app.moviebase.tmdb

object TmdbWebConfig {

internal object TmdbWebConfig {
const val BASE_URL_TMDB = "https://api.themoviedb.org"
const val WEBSITE_BASE_URL = "https://www.themoviedb.org"

const val VERSION_PATH_V3 = "3"
const val VERSION_PATH_V4 = "4"
const val BASE_URL_TMDB_IMAGE = "https://image.tmdb.org/t/p/"
const val BASE_URL_YOUTUBE_IMAGE = "https://img.youtube.com/vi"
const val LOGO_FILTER = "_filter(negate,000,666)"

const val VERSION_PATH_V3 = "3"
const val VERSION_PATH_V4 = "4"

const val LOGO_FILTER = "_filter(negate,000,666)"
}

object TmdbUrlParameter {
internal object TmdbUrlParameter {
const val API_KEY = "api_key"
const val SESSION_ID = "session_id"
const val ACCESS_TOKEN = "access_token"
}

enum class TmdbVersion(val path: String) {
V3(TmdbWebConfig.VERSION_PATH_V3),
V4(TmdbWebConfig.VERSION_PATH_V4)
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*

class TmdbAuthenticationApi(private val client: HttpClient) {
class TmdbAuthenticationApi internal constructor(private val client: HttpClient) {

suspend fun requestToken(): TmdbRequestToken = client.get {
endPointV3("authentication/token/new")
Expand Down Expand Up @@ -42,5 +42,4 @@ class TmdbAuthenticationApi(private val client: HttpClient) {
val requestToken = requestToken().requestToken
return "https://www.themoviedb.org/authenticate/${requestToken}?redirect_to=$redirectTo"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import app.moviebase.tmdb.model.TmdbEpisodeDetail
import app.moviebase.tmdb.model.TmdbExternalIds
import app.moviebase.tmdb.remote.endPointV3
import app.moviebase.tmdb.remote.parameterAppendResponses
import app.moviebase.tmdb.remote.parameterIncludeImageLanguage
import app.moviebase.tmdb.remote.parameterLanguage
import io.ktor.client.*
import io.ktor.client.call.*
Expand All @@ -23,7 +24,7 @@ class TmdbShowEpisodesApi(private val client: HttpClient) {
endPointEpisode(showId, seasonNumber, episodeNumber)
parameterLanguage(language)
parameterAppendResponses(appendResponses)
includeImageLanguages?.let { parameter("include_image_language", it) }
parameterIncludeImageLanguage(includeImageLanguages)
}.body()

suspend fun getExternalIds(showId: Int, seasonNumber: Int, episodeNumber: Int): TmdbExternalIds = client.get {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.moviebase.tmdb.api
import app.moviebase.tmdb.model.*
import app.moviebase.tmdb.remote.endPointV3
import app.moviebase.tmdb.remote.parameterAppendResponses
import app.moviebase.tmdb.remote.parameterIncludeImageLanguage
import app.moviebase.tmdb.remote.parameterLanguage
import io.ktor.client.*
import io.ktor.client.call.*
Expand All @@ -20,7 +21,7 @@ class TmdbShowSeasonsApi(private val client: HttpClient) {
endPointSeason(showId, seasonNumber)
parameterLanguage(language)
parameterAppendResponses(appendResponses)
includeImageLanguages?.let { parameter("include_image_language", it) }
parameterIncludeImageLanguage(includeImageLanguages)
}.body()

suspend fun getVideos(showId: Int, seasonNumber: Int, language: String? = null): TmdbResult<TmdbVideo> = client.get {
Expand All @@ -39,5 +40,4 @@ class TmdbShowSeasonsApi(private val client: HttpClient) {
private fun HttpRequestBuilder.endPointSeason(showId: Int, seasonNumber: Int, vararg paths: String) {
endPointV3("tv", showId.toString(), "season", seasonNumber.toString(), *paths)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ sealed class TmdbPersonCredit : TmdbAnyMedia, TmdbBackdropMedia, TmdbPosterMedia
@SerialName("vote_count") override val voteCount: Int,
@SerialName("name") override val name: String,
@SerialName("original_name") override val originalName: String,

@SerialName("character") val character: String,
@SerialName("credit_id") val creditId: String,
@SerialName("order") val order: Int? = null,
Expand Down Expand Up @@ -330,6 +329,4 @@ sealed class TmdbPersonCredit : TmdbAnyMedia, TmdbBackdropMedia, TmdbPosterMedia
) : TmdbPersonCredit.Show()

}

}

Loading

0 comments on commit b183408

Please sign in to comment.