Add method to get cover art.

As this call should return byte stream: I've introduced helper method
in client to do the actual call and parse possible json error response.

Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
This commit is contained in:
Yahor Berdnikau 2017-10-17 21:31:33 +02:00
parent d08beff1f4
commit 48e6788224
6 changed files with 164 additions and 4 deletions

View File

@ -25,11 +25,12 @@ val dateFormat by lazy(LazyThreadSafetyMode.NONE, {
fun MockWebServerRule.enqueueResponse(resourceName: String) {
this.mockWebServer.enqueue(MockResponse()
.setBody(loadJsonResponse(this, resourceName)))
.setBody(loadJsonResponse(resourceName))
.setHeader("Content-Type", "application/json;charset=UTF-8"))
}
private fun loadJsonResponse(rule: MockWebServerRule, name: String): String {
val source = Okio.buffer(Okio.source(rule.javaClass.classLoader.getResourceAsStream(name)))
fun MockWebServerRule.loadJsonResponse(name: String): String {
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
return source.readString(Charset.forName("UTF-8"))
}
@ -67,7 +68,7 @@ fun SubsonicResponse.assertBaseResponseOk() {
fun MockWebServerRule.assertRequestParam(responseResourceName: String,
expectedParam: String,
apiRequest: () -> Response<out SubsonicResponse>) {
apiRequest: () -> Response<out Any>) {
this.enqueueResponse(responseResourceName)
apiRequest()

View File

@ -0,0 +1,74 @@
package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient] for [SubsonicAPIDefinition.getCoverArt] call.
*/
class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
@Test
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("generic_error_response.json")
val response = client.getCoverArt("some-id")
with(response) {
stream `should be` null
requestErrorCode `should be` null
apiError `should equal` SubsonicError.GENERIC
}
}
@Test
fun `Should handle server error`() {
val httpErrorCode = 404
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
val response = client.getCoverArt("some-id")
with(response) {
stream `should be` null
requestErrorCode `should equal` 404
apiError `should be` null
}
}
@Test
fun `Should return successful call stream`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse()
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")))
val response = client.getCoverArt("some-id")
with(response) {
requestErrorCode `should be` null
apiError `should be` null
stream `should not be` null
val expectedContent = mockWebServerRule.loadJsonResponse("ping_ok.json")
stream!!.bufferedReader().readText() `should equal to` expectedContent
}
}
@Test
fun `Should pass id as parameter`() {
val id = "ca123994"
mockWebServerRule.assertRequestParam("ping_ok.json", id) {
client.api.getCoverArt(id).execute()
}
}
@Test
fun `Should pass size as a parameter`() {
val size = 45600L
mockWebServerRule.assertRequestParam("ping_ok.json", size.toString()) {
client.api.getCoverArt("some-id", size).execute()
}
}
}

View File

@ -3,9 +3,14 @@ package org.moire.ultrasonic.api.subsonic
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import java.lang.IllegalStateException
@ -65,6 +70,35 @@ class SubsonicAPIClient(baseUrl: String,
val api: SubsonicAPIDefinition = retrofit.create(SubsonicAPIDefinition::class.java)
/**
* Convenient method to get cover art from api using item [id] and optional maximum [size].
*
* It detects the response `Content-Type` and tries to parse subsonic error if there is one.
*
* Prefer this method over [SubsonicAPIDefinition.getCoverArt] as this handles error cases.
*/
fun getCoverArt(id: String, size: Long? = null): StreamResponse = handleStreamResponse {
api.getCoverArt(id, size).execute()
}
private inline fun handleStreamResponse(apiCall: () -> Response<ResponseBody>): StreamResponse {
val response = apiCall()
return if (response.isSuccessful) {
val responseBody = response.body()
val contentType = responseBody.contentType()
if (contentType != null &&
contentType.type().equals("application", true) &&
contentType.subtype().equals("json", true)) {
val error = jacksonMapper.readValue<SubsonicResponse>(responseBody.byteStream())
StreamResponse(apiError = error.error)
} else {
StreamResponse(stream = responseBody.byteStream())
}
} else {
StreamResponse(requestErrorCode = response.code())
}
}
private val salt: String by lazy {
val secureRandom = SecureRandom()
BigInteger(130, secureRandom).toString(32)

View File

@ -1,5 +1,6 @@
package org.moire.ultrasonic.api.subsonic
import okhttp3.ResponseBody
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.api.subsonic.response.GetAlbumList2Response
import org.moire.ultrasonic.api.subsonic.response.GetAlbumListResponse
@ -24,6 +25,7 @@ import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.Streaming
/**
* Subsonic API calls.
@ -160,4 +162,9 @@ interface SubsonicAPIDefinition {
@GET("getStarred2.view")
fun getStarred2(@Query("musicFolderId") musicFolderId: Long? = null): Call<GetStarredTwoResponse>
@Streaming
@GET("getCoverArt.view")
fun getCoverArt(@Query("id") id: String,
@Query("size") size: Long? = null): Call<ResponseBody>
}

View File

@ -0,0 +1,19 @@
package org.moire.ultrasonic.api.subsonic.response
import org.moire.ultrasonic.api.subsonic.SubsonicError
import java.io.InputStream
/**
* Special response that contains either [stream] of data from api, or [apiError],
* or [requestErrorCode].
*
* [requestErrorCode] will be only if there problem on http level.
*/
class StreamResponse(val stream: InputStream? = null,
val apiError: SubsonicError? = null,
val requestErrorCode: Int? = null) {
/**
* Check if this response has error.
*/
fun hasError(): Boolean = apiError != null || requestErrorCode != null
}

View File

@ -0,0 +1,25 @@
package org.moire.ultrasonic.api.subsonic.response
import org.amshove.kluent.`should equal to`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.SubsonicError.GENERIC
/**
* Unit test for [StreamResponse].
*/
class StreamResponseTest {
@Test
fun `Should have error if subsonic error is not null`() {
StreamResponse(apiError = GENERIC).hasError() `should equal to` true
}
@Test
fun `Should have error if http error is not null`() {
StreamResponse(requestErrorCode = 500).hasError() `should equal to` true
}
@Test
fun `Should not have error if subsonic error and http error is null`() {
StreamResponse().hasError() `should equal to` false
}
}