diff --git a/data-android/src/main/java/ly/david/data/di/network/NetworkModule.kt b/data-android/src/main/java/ly/david/data/di/network/NetworkModule.kt index e1e44b26a..d3dfde500 100644 --- a/data-android/src/main/java/ly/david/data/di/network/NetworkModule.kt +++ b/data-android/src/main/java/ly/david/data/di/network/NetworkModule.kt @@ -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 @@ -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, ) } diff --git a/data/musicbrainz/src/main/java/ly/david/data/musicbrainz/api/MusicBrainzApi.kt b/data/musicbrainz/src/main/java/ly/david/data/musicbrainz/api/MusicBrainzApi.kt index 1e7963824..6585d3455 100644 --- a/data/musicbrainz/src/main/java/ly/david/data/musicbrainz/api/MusicBrainzApi.kt +++ b/data/musicbrainz/src/main/java/ly/david/data/musicbrainz/api/MusicBrainzApi.kt @@ -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 { @@ -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) + } } } } diff --git a/data/musicbrainz/src/main/java/ly/david/data/musicbrainz/api/MusicBrainzOAuthApi.kt b/data/musicbrainz/src/main/java/ly/david/data/musicbrainz/api/MusicBrainzOAuthApi.kt new file mode 100644 index 000000000..c879eeceb --- /dev/null +++ b/data/musicbrainz/src/main/java/ly/david/data/musicbrainz/api/MusicBrainzOAuthApi.kt @@ -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() + } +} diff --git a/data/spotify/src/main/java/ly/david/data/spotify/api/auth/SpotifyAuthApi.kt b/data/spotify/src/main/java/ly/david/data/spotify/api/auth/SpotifyAuthApi.kt index b4bd2bd61..9165607a1 100644 --- a/data/spotify/src/main/java/ly/david/data/spotify/api/auth/SpotifyAuthApi.kt +++ b/data/spotify/src/main/java/ly/david/data/spotify/api/auth/SpotifyAuthApi.kt @@ -15,7 +15,7 @@ interface SpotifyAuthApi { httpClient: HttpClient, ): SpotifyAuthApi { return SpotifyAuthApiImpl( - client = httpClient + httpClient = httpClient, ) } } @@ -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)