Skip to content
This repository has been archived by the owner on Nov 12, 2024. It is now read-only.

Fix more login issues #2026

Merged
merged 1 commit into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/trakt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ kotlin {

implementation(libs.ktor.client.core)
implementation(libs.ktor.client.auth)
implementation(libs.ktor.client.contentnegotiation)

api(libs.kotlin.coroutines.core)

Expand Down
124 changes: 124 additions & 0 deletions api/trakt/src/commonMain/kotlin/app/tivi/trakt/TiviTrakt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2024, Christopher Banes and the Tivi project contributors
// SPDX-License-Identifier: Apache-2.0

@file:Suppress("invisible_reference", "invisible_member")

package app.tivi.trakt

import app.moviebase.trakt.TraktBearerTokens
import app.moviebase.trakt.TraktClientConfig
import app.moviebase.trakt.api.TraktAuthApi
import app.moviebase.trakt.api.TraktCheckinApi
import app.moviebase.trakt.api.TraktCommentsApi
import app.moviebase.trakt.api.TraktEpisodesApi
import app.moviebase.trakt.api.TraktMoviesApi
import app.moviebase.trakt.api.TraktRecommendationsApi
import app.moviebase.trakt.api.TraktSearchApi
import app.moviebase.trakt.api.TraktSeasonsApi
import app.moviebase.trakt.api.TraktShowsApi
import app.moviebase.trakt.api.TraktSyncApi
import app.moviebase.trakt.api.TraktUsersApi
import app.moviebase.trakt.core.HttpClientFactory
import app.moviebase.trakt.core.TraktDsl
import app.moviebase.trakt.core.interceptRequest
import app.tivi.app.ApplicationInfo
import app.tivi.data.traktauth.TraktAuthRepository
import app.tivi.data.traktauth.TraktOAuthInfo
import co.touchlab.kermit.Logger
import io.ktor.client.HttpClient
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.pluginOrNull
import io.ktor.client.request.header

@TraktDsl
fun TiviTrakt(block: TraktClientConfig.() -> Unit): TiviTrakt {
val config = TraktClientConfig().apply(block)
return TiviTrakt(config)
}

class TiviTrakt internal constructor(private val config: TraktClientConfig) {

private val client: HttpClient by lazy {
HttpClientFactory.create(config = config, useAuthentication = true).apply {
interceptRequest {
it.header(TraktHeader.API_KEY, config.traktApiKey)
it.header(TraktHeader.API_VERSION, TraktWebConfig.VERSION)
}
}
}

init {
requireNotNull(config.traktApiKey) {
"Trakt API key unavailable. Set the traktApiKey field in the class TraktClientConfig " +
"when instantiate the Trakt client."
}
}

val auth by lazy { TraktAuthApi(client, config) }
val movies by buildApi(::TraktMoviesApi)
val shows by buildApi(::TraktShowsApi)
val seasons by buildApi(::TraktSeasonsApi)
val episodes by buildApi(::TraktEpisodesApi)
val checkin by buildApi(::TraktCheckinApi)
val search by buildApi(::TraktSearchApi)
val users by buildApi(::TraktUsersApi)
val sync by buildApi(::TraktSyncApi)
val recommendations by buildApi(::TraktRecommendationsApi)
val comments by buildApi(::TraktCommentsApi)

fun invalidateAuth() {
// Force Ktor to re-fetch bearer tokens
// https://youtrack.jetbrains.com/issue/KTOR-4759
client.pluginOrNull(Auth)
?.providers
?.filterIsInstance<BearerAuthProvider>()
?.firstOrNull()
?.clearToken()
}

private inline fun <T> buildApi(crossinline builder: (HttpClient) -> T) = lazy { builder(client) }
}

internal object TraktWebConfig {
const val VERSION = "2"
}

internal object TraktHeader {
const val API_KEY = "trakt-api-key"
const val API_VERSION = "trakt-api-version"
}

internal fun TraktClientConfig.applyTiviConfig(
oauthInfo: TraktOAuthInfo,
applicationInfo: ApplicationInfo,
traktAuthRepository: () -> TraktAuthRepository,
) {
traktApiKey = oauthInfo.clientId
maxRetries = 3

logging {
logger = object : io.ktor.client.plugins.logging.Logger {
override fun log(message: String) {
Logger.d("trakt-ktor") { message }
}
}
level = when {
applicationInfo.debugBuild -> LogLevel.HEADERS
else -> LogLevel.NONE
}
}

userAuthentication {
loadTokens {
traktAuthRepository().getAuthState()
?.let { TraktBearerTokens(it.accessToken, it.refreshToken) }
}

refreshTokens {
traktAuthRepository().refreshTokens()
?.let { TraktBearerTokens(it.accessToken, it.refreshToken) }
}
}
}
30 changes: 16 additions & 14 deletions api/trakt/src/commonMain/kotlin/app/tivi/trakt/TraktComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

package app.tivi.trakt

import app.moviebase.trakt.Trakt
import app.moviebase.trakt.api.TraktEpisodesApi
import app.moviebase.trakt.api.TraktRecommendationsApi
import app.moviebase.trakt.api.TraktSearchApi
Expand All @@ -12,18 +11,14 @@ import app.moviebase.trakt.api.TraktShowsApi
import app.moviebase.trakt.api.TraktSyncApi
import app.moviebase.trakt.api.TraktUsersApi
import app.tivi.app.ApplicationInfo
import app.tivi.data.traktauth.TraktClient
import app.tivi.data.traktauth.TraktOAuthInfo
import app.tivi.inject.ApplicationScope
import me.tatarka.inject.annotations.Provides

interface TraktComponent :
TraktCommonComponent,
TraktPlatformComponent

expect interface TraktPlatformComponent

interface TraktCommonComponent {

interface TraktComponent : TraktPlatformComponent {
@ApplicationScope
@Provides
fun provideTraktOAuthInfo(
Expand All @@ -48,23 +43,30 @@ interface TraktCommonComponent {
)

@Provides
fun provideTraktUsersService(trakt: Trakt): TraktUsersApi = trakt.users
fun provideTraktUsersService(trakt: TiviTrakt): TraktUsersApi = trakt.users

@Provides
fun provideTraktShowsService(trakt: TiviTrakt): TraktShowsApi = trakt.shows

@Provides
fun provideTraktShowsService(trakt: Trakt): TraktShowsApi = trakt.shows
fun provideTraktEpisodesService(trakt: TiviTrakt): TraktEpisodesApi = trakt.episodes

@Provides
fun provideTraktEpisodesService(trakt: Trakt): TraktEpisodesApi = trakt.episodes
fun provideTraktSeasonsService(trakt: TiviTrakt): TraktSeasonsApi = trakt.seasons

@Provides
fun provideTraktSeasonsService(trakt: Trakt): TraktSeasonsApi = trakt.seasons
fun provideTraktSyncService(trakt: TiviTrakt): TraktSyncApi = trakt.sync

@Provides
fun provideTraktSyncService(trakt: Trakt): TraktSyncApi = trakt.sync
fun provideTraktSearchService(trakt: TiviTrakt): TraktSearchApi = trakt.search

@Provides
fun provideTraktSearchService(trakt: Trakt): TraktSearchApi = trakt.search
fun provideTraktRecommendationsService(trakt: TiviTrakt): TraktRecommendationsApi = trakt.recommendations

@Provides
fun provideTraktRecommendationsService(trakt: Trakt): TraktRecommendationsApi = trakt.recommendations
fun provideTraktClient(trakt: Lazy<TiviTrakt>): TraktClient = object : TraktClient {
override fun invalidateAuthTokens() {
trakt.value.invalidateAuth()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,11 @@

package app.tivi.trakt

import app.moviebase.trakt.Trakt
import app.tivi.app.ApplicationInfo
import app.tivi.data.traktauth.TraktAuthRepository
import app.tivi.data.traktauth.TraktOAuthInfo
import app.tivi.inject.ApplicationScope
import co.touchlab.kermit.Logger
import io.ktor.client.engine.darwin.Darwin
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.http.HttpStatusCode
import me.tatarka.inject.annotations.Provides

actual interface TraktPlatformComponent {
Expand All @@ -25,56 +17,19 @@ actual interface TraktPlatformComponent {
oauthInfo: TraktOAuthInfo,
applicationInfo: ApplicationInfo,
traktAuthRepository: Lazy<TraktAuthRepository>,
): Trakt = Trakt {
traktApiKey = oauthInfo.clientId
maxRetries = 3

logging {
logger = object : io.ktor.client.plugins.logging.Logger {
override fun log(message: String) {
Logger.d("trakt-ktor") { message }
}
}
level = when {
applicationInfo.debugBuild -> LogLevel.HEADERS
else -> LogLevel.NONE
}
}
): TiviTrakt = TiviTrakt {
applyTiviConfig(
oauthInfo = oauthInfo,
applicationInfo = applicationInfo,
traktAuthRepository = traktAuthRepository::value,
)

httpClient(Darwin) {
engine {
configureRequest {
setAllowsCellularAccess(true)
}
}

install(HttpRequestRetry) {
retryIf(5) { _, httpResponse ->
when {
httpResponse.status.value in 500..599 -> true
httpResponse.status == HttpStatusCode.TooManyRequests -> true
else -> false
}
}
}

install(Auth) {
bearer {
loadTokens {
traktAuthRepository.value.getAuthState()
?.let { BearerTokens(it.accessToken, it.refreshToken) }
}

refreshTokens {
traktAuthRepository.value.refreshTokens()
?.let { BearerTokens(it.accessToken, it.refreshToken) }
}

sendWithoutRequest { request ->
request.url.host == "api.trakt.tv"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@

package app.tivi.trakt

import app.moviebase.trakt.Trakt
import app.tivi.app.ApplicationInfo
import app.tivi.data.traktauth.TraktAuthRepository
import app.tivi.data.traktauth.TraktOAuthInfo
import app.tivi.data.traktauth.store.AuthStore
import app.tivi.inject.ApplicationScope
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.http.HttpStatusCode
import me.tatarka.inject.annotations.Provides
import okhttp3.OkHttpClient

Expand All @@ -22,46 +16,21 @@ actual interface TraktPlatformComponent {
@Provides
fun provideTrakt(
client: OkHttpClient,
authStore: AuthStore,
oauthInfo: TraktOAuthInfo,
applicationInfo: ApplicationInfo,
traktAuthRepository: Lazy<TraktAuthRepository>,
): Trakt = Trakt {
traktApiKey = oauthInfo.clientId
maxRetries = 3
): TiviTrakt = TiviTrakt {
applyTiviConfig(
oauthInfo = oauthInfo,
applicationInfo = applicationInfo,
traktAuthRepository = traktAuthRepository::value,
)

httpClient(OkHttp) {
// Probably want to move to using Ktor's caching, timeouts, etc eventually
engine {
preconfigured = client
}

install(HttpRequestRetry) {
retryIf(5) { _, httpResponse ->
when {
httpResponse.status.value in 500..599 -> true
httpResponse.status == HttpStatusCode.TooManyRequests -> true
else -> false
}
}
}

install(Auth) {
bearer {
loadTokens {
traktAuthRepository.value.getAuthState()
?.let { BearerTokens(it.accessToken, it.refreshToken) }
}

refreshTokens {
traktAuthRepository.value.refreshTokens()
?.let { BearerTokens(it.accessToken, it.refreshToken) }
}

sendWithoutRequest { request ->
request.url.host == "api.trakt.tv"
}
}
}
}
}
}
Loading
Loading