From 48e67882249fe49ccaf66673a5e3b7e16c8065f2 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Tue, 17 Oct 2017 21:31:33 +0200 Subject: [PATCH] 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 --- .../api/subsonic/CommonFunctions.kt | 9 ++- .../subsonic/SubsonicApiGetCoverArtTest.kt | 74 +++++++++++++++++++ .../api/subsonic/SubsonicAPIClient.kt | 34 +++++++++ .../api/subsonic/SubsonicAPIDefinition.kt | 7 ++ .../api/subsonic/response/StreamResponse.kt | 19 +++++ .../subsonic/response/StreamResponseTest.kt | 25 +++++++ 6 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/StreamResponse.kt create mode 100644 subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/response/StreamResponseTest.kt diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt index 09b47447..5c052f7f 100644 --- a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt @@ -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) { + apiRequest: () -> Response) { this.enqueueResponse(responseResourceName) apiRequest() diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt new file mode 100644 index 00000000..d0c6d647 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt @@ -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() + } + } +} diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index dd89c8f4..11fc9041 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -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): 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(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) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt index 28d1c952..35413f1a 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt @@ -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 + + @Streaming + @GET("getCoverArt.view") + fun getCoverArt(@Query("id") id: String, + @Query("size") size: Long? = null): Call } diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/StreamResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/StreamResponse.kt new file mode 100644 index 00000000..47292296 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/StreamResponse.kt @@ -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 +} diff --git a/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/response/StreamResponseTest.kt b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/response/StreamResponseTest.kt new file mode 100644 index 00000000..e8a2c694 --- /dev/null +++ b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/response/StreamResponseTest.kt @@ -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 + } +}