Skip to content

Commit

Permalink
feat: support MusicBrainz OAuth refresh so that user will stay logged…
Browse files Browse the repository at this point in the history
… in until they click log out

resolves #415
  • Loading branch information
David Ly committed Sep 3, 2023
1 parent f9a5633 commit ad79187
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import ly.david.data.core.network.RecoverableNetworkException
import ly.david.data.coverart.api.CoverArtArchiveApi
import ly.david.data.musicbrainz.MusicBrainzAuthState
import ly.david.data.musicbrainz.api.MusicBrainzApi
import ly.david.data.musicbrainz.api.MusicBrainzOAuthApi
import ly.david.data.musicbrainz.api.MusicBrainzOAuthInfo
import ly.david.data.spotify.api.SpotifyApi
import ly.david.data.spotify.api.auth.SpotifyAuthApi
import ly.david.data.spotify.api.auth.SpotifyAuthState
Expand Down Expand Up @@ -94,14 +96,29 @@ object NetworkModule {
httpClient = httpClient,
)

@Singleton
@Provides
fun provideMusicBrainzOAuthApi(
httpClient: HttpClient,
): MusicBrainzOAuthApi {
return MusicBrainzOAuthApi.create(
httpClient = httpClient,
)
}

// TODO: pass repository instead of all these params
@Singleton
@Provides
fun provideMusicBrainzApi(
httpClient: HttpClient,
musicBrainzOAuthInfo: MusicBrainzOAuthInfo,
musicBrainzOAuthApi: MusicBrainzOAuthApi,
musicBrainzAuthState: MusicBrainzAuthState,
): MusicBrainzApi {
return MusicBrainzApi.create(
httpClient = httpClient,
musicBrainzOAuthInfo = musicBrainzOAuthInfo,
musicBrainzOAuthApi = musicBrainzOAuthApi,
musicBrainzAuthState = musicBrainzAuthState,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface MusicBrainzApi : SearchApi, BrowseApi, LookupApi, CollectionApi, Music
companion object {
fun create(
httpClient: HttpClient,
musicBrainzOAuthInfo: MusicBrainzOAuthInfo,
musicBrainzOAuthApi: MusicBrainzOAuthApi,
musicBrainzAuthState: MusicBrainzAuthState,
): MusicBrainzApi {
val extendedClient = httpClient.config {
Expand All @@ -36,19 +38,47 @@ interface MusicBrainzApi : SearchApi, BrowseApi, LookupApi, CollectionApi, Music
val refreshToken = musicBrainzAuthState.getRefreshToken()
if (refreshToken.isNullOrEmpty()) return@loadTokens null

BearerTokens(accessToken, refreshToken)
val newAccessTokenResponse = musicBrainzOAuthApi.getAccessToken(
clientId = musicBrainzOAuthInfo.clientId,
clientSecret = musicBrainzOAuthInfo.clientSecret,
grantType = REFRESH_TOKEN,
refreshToken = refreshToken,
)
val newAccessToken = newAccessTokenResponse.accessToken
val newRefreshToken = newAccessTokenResponse.refreshToken

musicBrainzAuthState.saveTokens(
newAccessToken,
newRefreshToken
)

BearerTokens(newAccessToken, newRefreshToken)
}
// TODO: this block is never executed unlike for spotify
refreshTokens {
// TODO: handle refresh
val accessToken = musicBrainzAuthState.getAccessToken() ?: return@refreshTokens null
val refreshToken = musicBrainzAuthState.getRefreshToken() ?: return@refreshTokens null
BearerTokens(accessToken, refreshToken)

val newAccessTokenResponse = musicBrainzOAuthApi.getAccessToken(
clientId = musicBrainzOAuthInfo.clientId,
clientSecret = musicBrainzOAuthInfo.clientSecret,
grantType = REFRESH_TOKEN,
refreshToken = refreshToken,
)
val newAccessToken = newAccessTokenResponse.accessToken
val newRefreshToken = newAccessTokenResponse.refreshToken

musicBrainzAuthState.saveTokens(
newAccessToken,
newRefreshToken
)

BearerTokens(newAccessToken, newRefreshToken)
}
// TODO: handle collection browse, one way to do it is to split up the
// api that requires auth and just return true here
// sendWithoutRequest { request ->
// request.url.pathSegments.contains(USER_INFO)
// }
sendWithoutRequest { request ->
request.url.pathSegments.contains(USER_INFO)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package ly.david.data.musicbrainz.api

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.forms.submitForm
import io.ktor.http.parameters
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

// TODO: could be generic and reuse for mb and spotify
@Serializable
data class MusicBrainzOAuthAccessToken(
@SerialName("access_token") val accessToken: String,
@SerialName("refresh_token") val refreshToken: String,
)

internal const val REFRESH_TOKEN = "refresh_token"

interface MusicBrainzOAuthApi {

companion object {
fun create(
httpClient: HttpClient,
): MusicBrainzOAuthApi {
return MusicBrainzOAuthApiImpl(
httpClient = httpClient,
)
}
}

suspend fun getAccessToken(
clientId: String,
clientSecret: String,
grantType: String,
refreshToken: String,
): MusicBrainzOAuthAccessToken
}

class MusicBrainzOAuthApiImpl(
val httpClient: HttpClient,
) : MusicBrainzOAuthApi {
override suspend fun getAccessToken(
clientId: String,
clientSecret: String,
grantType: String,
refreshToken: String,
): MusicBrainzOAuthAccessToken {
return httpClient.submitForm(
url = "$MUSIC_BRAINZ_BASE_URL/oauth2/token",
formParameters = parameters {
append("client_id", clientId)
append("client_secret", clientSecret)
append("grant_type", grantType)
append("refresh_token", refreshToken)
},
).body()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface SpotifyAuthApi {
httpClient: HttpClient,
): SpotifyAuthApi {
return SpotifyAuthApiImpl(
client = httpClient
httpClient = httpClient,
)
}
}
Expand All @@ -28,14 +28,14 @@ interface SpotifyAuthApi {
}

class SpotifyAuthApiImpl(
private val client: HttpClient,
private val httpClient: HttpClient,
) : SpotifyAuthApi {
override suspend fun getAccessToken(
clientId: String,
clientSecret: String,
grantType: String,
): SpotifyAccessToken {
return client.submitForm(
return httpClient.submitForm(
url = SPOTIFY_AUTH,
formParameters = parameters {
append("client_id", clientId)
Expand Down

0 comments on commit ad79187

Please sign in to comment.