From ba412721acfe4d351fbc29033e1d1591bc3a8995 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sun, 5 Nov 2017 22:14:02 +0100 Subject: [PATCH] Add stream call. Also introduced helper method in SubsonicApiClient that handles error cases. Signed-off-by: Yahor Berdnikau --- .../api/subsonic/CommonFunctions.kt | 2 +- .../subsonic/SubsonicApiGetCoverArtTest.kt | 6 +- .../api/subsonic/SubsonicApiStreamTest.kt | 125 ++++++++++++++++++ .../api/subsonic/SubsonicAPIClient.kt | 21 ++- .../api/subsonic/SubsonicAPIDefinition.kt | 12 ++ .../api/subsonic/response/StreamResponse.kt | 8 +- .../subsonic/response/StreamResponseTest.kt | 20 ++- 7 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.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 5c052f7f..5efa0bf3 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 @@ -46,7 +46,7 @@ fun parseDate(dateAsString: String): Calendar { return result } -fun checkErrorCallParsed(mockWebServerRule: MockWebServerRule, +fun checkErrorCallParsed(mockWebServerRule : MockWebServerRule, apiRequest: () -> Response): T { mockWebServerRule.enqueueResponse("generic_error_response.json") 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 index d0c6d647..3b22d396 100644 --- 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 @@ -19,7 +19,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() { with(response) { stream `should be` null - requestErrorCode `should be` null + responseHttpCode `should equal to` 200 apiError `should equal` SubsonicError.GENERIC } } @@ -33,7 +33,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() { with(response) { stream `should be` null - requestErrorCode `should equal` 404 + responseHttpCode `should equal` 404 apiError `should be` null } } @@ -46,7 +46,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() { val response = client.getCoverArt("some-id") with(response) { - requestErrorCode `should be` null + responseHttpCode `should equal to` 200 apiError `should be` null stream `should not be` null val expectedContent = mockWebServerRule.loadJsonResponse("ping_ok.json") diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt new file mode 100644 index 00000000..b0255d66 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt @@ -0,0 +1,125 @@ +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.stream] call. + */ +class SubsonicApiStreamTest : SubsonicAPIClientTest() { + @Test + fun `Should handle api error response`() { + mockWebServerRule.enqueueResponse("generic_error_response.json") + + val response = client.stream("some-id") + + with(response) { + stream `should be` null + responseHttpCode `should equal to` 200 + apiError `should equal` SubsonicError.GENERIC + } + } + + @Test + fun `Should handle server error`() { + val httpErrorCode = 404 + mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode)) + + val response = client.stream("some-id") + + with(response) { + stream `should be` null + responseHttpCode `should equal to` httpErrorCode + apiError `should be` null + } + } + + @Test + fun `Should return successfull call stream`() { + mockWebServerRule.mockWebServer.enqueue(MockResponse() + .setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))) + + val response = client.stream("some-id") + + with(response) { + responseHttpCode `should equal to` 200 + 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 = "asdo123" + + mockWebServerRule.assertRequestParam("ping_ok.json", id) { + client.api.stream(id = id).execute() + } + } + + @Test + fun `Should pass max bit rate as param`() { + val maxBitRate = 360 + + mockWebServerRule.assertRequestParam("ping_ok.json", + "maxBitRate=$maxBitRate") { + client.api.stream("some-id", maxBitRate = maxBitRate).execute() + } + } + + @Test + fun `Should pass format as param`() { + val format = "aac" + + mockWebServerRule.assertRequestParam("ping_ok.json", + "format=$format") { + client.api.stream("some-id", format = format).execute() + } + } + + @Test + fun `Should pass time offset as param`() { + val timeOffset = 155 + + mockWebServerRule.assertRequestParam("ping_ok.json", + "timeOffset=$timeOffset") { + client.api.stream("some-id", timeOffset = timeOffset).execute() + } + } + + @Test + fun `Should pass video size as param`() { + val videoSize = "44144" + + mockWebServerRule.assertRequestParam("ping_ok.json", + "size=$videoSize") { + client.api.stream("some-id", videoSize = videoSize).execute() + } + } + + @Test + fun `Should pass estimate content length as param`() { + val estimateContentLength = true + + mockWebServerRule.assertRequestParam("ping_ok.json", + "estimateContentLength=$estimateContentLength") { + client.api.stream("some-id", estimateContentLength = estimateContentLength).execute() + } + } + + @Test + fun `Should pass converted as param`() { + val converted = false + + mockWebServerRule.assertRequestParam("ping_ok.json", + "converted=$converted") { + client.api.stream("some-id", converted = converted).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 f0c44510..cd407947 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 @@ -83,6 +83,20 @@ class SubsonicAPIClient(baseUrl: String, api.getCoverArt(id, size).execute() } + /** + * Convenient method to get media stream from api using item [id] and optional [maxBitrate]. + * + * Optionally also you can provide [offset] that stream should start from. + * + * It detects the response `Content-Type` and tries to parse subsonic error if there is one. + * + * Prefer this method over [SubsonicAPIDefinition.stream] as this handles error cases. + */ + fun stream(id: String, maxBitrate: Int? = null, offset: Long? = null): StreamResponse = + handleStreamResponse { + api.stream(id, maxBitrate, offset = offset).execute() + } + private inline fun handleStreamResponse(apiCall: () -> Response): StreamResponse { val response = apiCall() return if (response.isSuccessful) { @@ -92,12 +106,13 @@ class SubsonicAPIClient(baseUrl: String, contentType.type().equals("application", true) && contentType.subtype().equals("json", true)) { val error = jacksonMapper.readValue(responseBody.byteStream()) - StreamResponse(apiError = error.error) + StreamResponse(apiError = error.error, responseHttpCode = response.code()) } else { - StreamResponse(stream = responseBody.byteStream()) + StreamResponse(stream = responseBody.byteStream(), + responseHttpCode = response.code()) } } else { - StreamResponse(requestErrorCode = response.code()) + StreamResponse(responseHttpCode = response.code()) } } 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 35413f1a..d5a778e1 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 @@ -24,6 +24,7 @@ import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import retrofit2.Call import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.Query import retrofit2.http.Streaming @@ -167,4 +168,15 @@ interface SubsonicAPIDefinition { @GET("getCoverArt.view") fun getCoverArt(@Query("id") id: String, @Query("size") size: Long? = null): Call + + @Streaming + @GET("stream.view") + fun stream(@Query("id") id: String, + @Query("maxBitRate") maxBitRate: Int? = null, + @Query("format") format: String? = null, + @Query("timeOffset") timeOffset: Int? = null, + @Query("size") videoSize: String? = null, + @Query("estimateContentLength") estimateContentLength: Boolean? = null, + @Query("converted") converted: Boolean? = null, + @Header("Range") offset: 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 index 47292296..7af5547b 100644 --- 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 @@ -5,15 +5,15 @@ import java.io.InputStream /** * Special response that contains either [stream] of data from api, or [apiError], - * or [requestErrorCode]. + * or [responseHttpCode]. * - * [requestErrorCode] will be only if there problem on http level. + * [responseHttpCode] will be there always. */ class StreamResponse(val stream: InputStream? = null, val apiError: SubsonicError? = null, - val requestErrorCode: Int? = null) { + val responseHttpCode: Int) { /** * Check if this response has error. */ - fun hasError(): Boolean = apiError != null || requestErrorCode != null + fun hasError(): Boolean = apiError != null || responseHttpCode !in 200..300 } 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 index e8a2c694..76085241 100644 --- 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 @@ -10,16 +10,26 @@ import org.moire.ultrasonic.api.subsonic.SubsonicError.GENERIC class StreamResponseTest { @Test fun `Should have error if subsonic error is not null`() { - StreamResponse(apiError = GENERIC).hasError() `should equal to` true + StreamResponse(apiError = GENERIC, responseHttpCode = 200).hasError() `should equal to` true } @Test - fun `Should have error if http error is not null`() { - StreamResponse(requestErrorCode = 500).hasError() `should equal to` true + fun `Should have error if http error is greater then 300`() { + StreamResponse(responseHttpCode = 301).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 + fun `Should have error of http error code is lower then 200`() { + StreamResponse(responseHttpCode = 199).hasError() `should equal to` true + } + + @Test + fun `Should not have error if http code is 200`() { + StreamResponse(responseHttpCode = 200).hasError() `should equal to` false + } + + @Test + fun `Should not have error if http code is 300`() { + StreamResponse(responseHttpCode = 300).hasError() `should equal to` false } }