OAuth2 authentication is working. Details and edge cases still pending.
This commit is contained in:
parent
0f7703be70
commit
93af0e416e
|
@ -21,6 +21,7 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
|
|||
else "/api/v1/albums/?playable=true&artist=$artistId&ordering=release_date"
|
||||
|
||||
HttpUpstream<Album, OtterResponse<Album>>(
|
||||
context,
|
||||
HttpUpstream.Behavior.Progressive,
|
||||
url,
|
||||
object : TypeToken<AlbumsResponse>() {}.type
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.io.BufferedReader
|
|||
|
||||
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "tracks-artist-$artistId"
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(context, HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.io.BufferedReader
|
|||
|
||||
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
||||
override val cacheId = "artists"
|
||||
override val upstream = HttpUpstream<Artist, OtterResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Artist, OtterResponse<Artist>>(context, HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -16,7 +16,7 @@ import java.io.BufferedReader
|
|||
|
||||
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "favorites.v2"
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(context, HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
@ -81,7 +81,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
|||
|
||||
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
|
||||
override val cacheId = "favorited"
|
||||
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(context, HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Int>) = FavoritedCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritedCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
|
@ -18,47 +19,49 @@ import java.io.Reader
|
|||
import java.lang.reflect.Type
|
||||
import kotlin.math.ceil
|
||||
|
||||
class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
|
||||
class HttpUpstream<D : Any, R : OtterResponse<D>>(val context: Context?, val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
|
||||
enum class Behavior {
|
||||
Single, AtOnce, Progressive
|
||||
}
|
||||
|
||||
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow {
|
||||
if (behavior == Behavior.Single && size != 0) return@flow
|
||||
context?.let {
|
||||
if (behavior == Behavior.Single && size != 0) return@flow
|
||||
|
||||
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
|
||||
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
|
||||
|
||||
val url =
|
||||
Uri.parse(url)
|
||||
.buildUpon()
|
||||
.appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString())
|
||||
.appendQueryParameter("page", page.toString())
|
||||
.appendQueryParameter("scope", Settings.getScope())
|
||||
.build()
|
||||
.toString()
|
||||
val url =
|
||||
Uri.parse(url)
|
||||
.buildUpon()
|
||||
.appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString())
|
||||
.appendQueryParameter("page", page.toString())
|
||||
.appendQueryParameter("scope", Settings.getScope())
|
||||
.build()
|
||||
.toString()
|
||||
|
||||
get(url).fold(
|
||||
{ response ->
|
||||
val data = response.getData()
|
||||
get(context, url).fold(
|
||||
{ response ->
|
||||
val data = response.getData()
|
||||
|
||||
when (behavior) {
|
||||
Behavior.Single -> emit(Repository.Response(Repository.Origin.Network, data, page, false))
|
||||
Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
|
||||
when (behavior) {
|
||||
Behavior.Single -> emit(Repository.Response(Repository.Origin.Network, data, page, false))
|
||||
Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
|
||||
|
||||
else -> {
|
||||
emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
|
||||
else -> {
|
||||
emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
|
||||
|
||||
if (response.next != null) fetch(size + data.size).collect { emit(it) }
|
||||
if (response.next != null) fetch(size + data.size).collect { emit(it) }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
when (error.exception) {
|
||||
is RefreshError -> EventBus.send(Event.LogOut)
|
||||
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), page, false))
|
||||
}
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
when (error.exception) {
|
||||
is RefreshError -> EventBus.send(Event.LogOut)
|
||||
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), page, false))
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}.flowOn(IO)
|
||||
|
||||
class GenericDeserializer<T : OtterResponse<*>>(val type: Type) : ResponseDeserializable<T> {
|
||||
|
@ -67,18 +70,16 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, privat
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun get(url: String): Result<R, FuelError> {
|
||||
suspend fun get(context: Context, url: String): Result<R, FuelError> {
|
||||
return try {
|
||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
header("Authorization", "Bearer ${Settings.getAccessToken()}")
|
||||
}
|
||||
authorize(context)
|
||||
}
|
||||
|
||||
val (_, response, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
return retryGet(url)
|
||||
return Result.Failure(FuelError.wrap(RefreshError))
|
||||
}
|
||||
|
||||
result
|
||||
|
@ -86,22 +87,4 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, privat
|
|||
Result.error(FuelError.wrap(e))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun retryGet(url: String): Result<R, FuelError> {
|
||||
return try {
|
||||
return if (HTTP.refresh()) {
|
||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
header("Authorization", "Bearer ${Settings.getAccessToken()}")
|
||||
}
|
||||
}
|
||||
|
||||
request.awaitObjectResult(GenericDeserializer(type))
|
||||
} else {
|
||||
Result.Failure(FuelError.wrap(RefreshError))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.error(FuelError.wrap(e))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ import java.io.BufferedReader
|
|||
|
||||
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) : Repository<PlaylistTrack, PlaylistTracksCache>() {
|
||||
override val cacheId = "tracks-playlist-$playlistId"
|
||||
override val upstream = HttpUpstream<PlaylistTrack, OtterResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<PlaylistTrack, OtterResponse<PlaylistTrack>>(context, HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<PlaylistTrack>) = PlaylistTracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.io.BufferedReader
|
|||
|
||||
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
|
||||
override val cacheId = "tracks-playlists"
|
||||
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(context, HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.io.BufferedReader
|
|||
|
||||
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
|
||||
override val cacheId = "radios"
|
||||
override val upstream = HttpUpstream<Radio, OtterResponse<Radio>>(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?ordering=name", object : TypeToken<RadiosResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Radio, OtterResponse<Radio>>(context, HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?ordering=name", object : TypeToken<RadiosResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Radio>) = RadiosCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(RadiosCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -13,7 +13,7 @@ import java.io.BufferedReader
|
|||
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<Track, TracksCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream: Upstream<Track>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
|
||||
get() = HttpUpstream(context, HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
@ -44,7 +44,7 @@ class TracksSearchRepository(override val context: Context?, var query: String)
|
|||
class ArtistsSearchRepository(override val context: Context?, var query: String) : Repository<Artist, ArtistsCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream: Upstream<Artist>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
get() = HttpUpstream(context, HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
|
||||
|
@ -53,7 +53,7 @@ class ArtistsSearchRepository(override val context: Context?, var query: String)
|
|||
class AlbumsSearchRepository(override val context: Context?, var query: String) : Repository<Album, AlbumsCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream: Upstream<Album>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", object : TypeToken<AlbumsResponse>() {}.type)
|
||||
get() = HttpUpstream(context, HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", object : TypeToken<AlbumsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Album>) = AlbumsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(AlbumsCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -13,7 +13,7 @@ import java.io.BufferedReader
|
|||
|
||||
class TracksRepository(override val context: Context?, albumId: Int) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "tracks-album-$albumId"
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(context, HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -34,36 +34,6 @@ object HTTP {
|
|||
{ false }
|
||||
)
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> get(url: String): Result<T, FuelError> {
|
||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
header("Authorization", "Bearer ${Settings.getAccessToken()}")
|
||||
}
|
||||
}
|
||||
|
||||
val (_, response, result) = request.awaitObjectResponseResult(gsonDeserializerOf(T::class.java))
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
return retryGet(url)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> retryGet(url: String): Result<T, FuelError> {
|
||||
return if (refresh()) {
|
||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
header("Authorization", "Bearer ${Settings.getAccessToken()}")
|
||||
}
|
||||
}
|
||||
|
||||
request.awaitObjectResult(gsonDeserializerOf(T::class.java))
|
||||
} else {
|
||||
Result.Failure(FuelError.wrap(RefreshError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Cache {
|
||||
|
|
|
@ -11,11 +11,11 @@ import com.google.android.exoplayer2.offline.Download
|
|||
import com.google.gson.Gson
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.squareup.picasso.RequestCreator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import net.openid.appauth.ClientSecretPost
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, isCache: Boolean, page: Int, hasMore: Boolean) -> Unit) {
|
||||
|
@ -70,10 +70,27 @@ fun Picasso.maybeLoad(url: String?): RequestCreator {
|
|||
}
|
||||
|
||||
fun Request.authorize(context: Context): Request {
|
||||
return this.apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
OAuth.state().performActionWithFreshTokens(OAuth.service(context)) { token, _, _ ->
|
||||
header("Authorization", "Bearer $token")
|
||||
return runBlocking {
|
||||
this@authorize.apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
OAuth.state().let { state ->
|
||||
val old = state.accessToken
|
||||
val auth = ClientSecretPost(OAuth.state().clientSecret)
|
||||
val done = CompletableDeferred<Boolean>()
|
||||
|
||||
state.performActionWithFreshTokens(OAuth.service(context), auth) { token, _, _ ->
|
||||
if (token != old && token != null) {
|
||||
state.save()
|
||||
}
|
||||
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
done.complete(true)
|
||||
}
|
||||
|
||||
done.await()
|
||||
|
||||
return@runBlocking this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,18 +126,7 @@ object OAuth {
|
|||
ResponseTypeValues.CODE,
|
||||
REDIRECT_URI
|
||||
)
|
||||
.setScopes(
|
||||
/* "read:profile",
|
||||
"read:libraries",
|
||||
"write:libraries",
|
||||
"read:favorites",
|
||||
"write:favorites",
|
||||
"read:playlists",
|
||||
"write:playlists",
|
||||
"read:radios",
|
||||
"write:listenings" */
|
||||
"read", "write"
|
||||
)
|
||||
.setScopes("read", "write")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue