diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt index 259f714b..7cdf4a69 100644 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt +++ b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt @@ -10,7 +10,7 @@ import org.moire.ultrasonic.api.subsonic.interceptors.toHexBytes import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule /** - * Integration test for [SubsonicAPIClient.getStreamUrl] method. + * Integration test for [getStreamUrl] method. */ class GetStreamUrlTest { @JvmField @Rule val mockWebServerRule = MockWebServerRule() @@ -30,7 +30,7 @@ class GetStreamUrlTest { ) client = SubsonicAPIClient(config) val baseExpectedUrl = mockWebServerRule.mockWebServer.url("").toString() - expectedUrl = "$baseExpectedUrl/rest/stream.view?id=$id&u=$USERNAME" + + expectedUrl = "$baseExpectedUrl/rest/stream.view?id=$id&format=raw&u=$USERNAME" + "&c=$CLIENT_ID&f=json&v=${V1_6_0.restApiVersion}&p=enc:${PASSWORD.toHexBytes()}" } @@ -38,7 +38,7 @@ class GetStreamUrlTest { fun `Should return valid stream url`() { mockWebServerRule.enqueueResponse("ping_ok.json") - val streamUrl = client.getStreamUrl(id) + val streamUrl = client.api.getStreamUrl(id) streamUrl `should be equal to` expectedUrl } @@ -47,7 +47,7 @@ class GetStreamUrlTest { fun `Should still return stream url if connection failed`() { mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(500)) - val streamUrl = client.getStreamUrl(id) + val streamUrl = client.api.getStreamUrl(id) streamUrl `should be equal to` expectedUrl } diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAvatarTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAvatarTest.kt index 3bb637ac..d6a11332 100644 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAvatarTest.kt +++ b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAvatarTest.kt @@ -7,14 +7,14 @@ import org.amshove.kluent.`should not be` import org.junit.Test /** - * Integration test for [SubsonicAPIClient.getAvatar] call. + * Integration test for [SubsonicAPIDefinition.getAvatar] call. */ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() { @Test fun `Should handle api error response`() { mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json") - val response = client.getAvatar("some") + val response = client.api.getAvatar("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -28,7 +28,7 @@ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() { val httpErrorCode = 500 mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode)) - val response = client.getAvatar("some") + val response = client.api.getAvatar("some-id").execute().toStreamResponse() with(response) { stream `should be equal to` null @@ -44,7 +44,7 @@ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() { .setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")) ) - val response = client.stream("some") + val response = client.api.stream("some-id").execute().toStreamResponse() with(response) { responseHttpCode `should be equal to` 200 diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt index f760f98a..2c047ee8 100644 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt +++ b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt @@ -14,7 +14,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() { fun `Should handle api error response`() { mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json") - val response = client.getCoverArt("some-id") + val response = client.api.getCoverArt("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -28,7 +28,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() { val httpErrorCode = 404 mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode)) - val response = client.getCoverArt("some-id") + val response = client.api.getCoverArt("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -44,7 +44,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() { .setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")) ) - val response = client.getCoverArt("some-id") + val response = client.api.getCoverArt("some-id").execute().toStreamResponse() with(response) { responseHttpCode `should be equal to` 200 diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt index b0bd27db..ad16462b 100644 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt +++ b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt @@ -14,7 +14,7 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() { fun `Should handle api error response`() { mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json") - val response = client.stream("some-id") + val response = client.api.stream("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -28,7 +28,7 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() { val httpErrorCode = 404 mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode)) - val response = client.stream("some-id") + val response = client.api.stream("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -38,13 +38,13 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() { } @Test - fun `Should return successfull call stream`() { + fun `Should return successful call stream`() { mockWebServerRule.mockWebServer.enqueue( MockResponse() .setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")) ) - val response = client.stream("some-id") + val response = client.api.stream("some-id").execute().toStreamResponse() with(response) { responseHttpCode `should be equal to` 200 diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt new file mode 100644 index 00000000..08c6c3f4 --- /dev/null +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt @@ -0,0 +1,97 @@ +package org.moire.ultrasonic.api.subsonic + +import com.fasterxml.jackson.module.kotlin.readValue +import java.io.IOException +import okhttp3.ResponseBody +import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse +import retrofit2.Response + +/** + * Converts a Response to a StreamResponse + */ +fun Response.toStreamResponse(): StreamResponse { + val response = this + 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 = SubsonicAPIClient.jacksonMapper.readValue( + responseBody.byteStream() + ) + StreamResponse(apiError = error.error, responseHttpCode = response.code()) + } else { + StreamResponse( + stream = responseBody?.byteStream(), + responseHttpCode = response.code() + ) + } + } else { + StreamResponse(responseHttpCode = response.code()) + } +} + +/** + * This call wraps Subsonic API calls so their results can be checked for errors, API version, etc + * It creates Exceptions from the results returned by the Subsonic API + */ +@Suppress("ThrowsCount") +fun Response.throwOnFailure(): Response { + val response = this + + if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) { + return this + } + if (!response.isSuccessful) { + throw IOException("Server error, code: " + response.code()) + } else if ( + response.body()!!.status === SubsonicResponse.Status.ERROR && + response.body()!!.error != null + ) { + throw SubsonicRESTException(response.body()!!.error!!) + } else { + throw IOException("Failed to perform request: " + response.code()) + } +} + +fun Response.falseOnFailure(): Boolean { + return (this.isSuccessful && this.body()!!.status === SubsonicResponse.Status.OK) +} + +/** + * This call wraps Subsonic API calls so their results can be checked for errors, API version, etc + * It creates Exceptions from a StreamResponse + */ +fun StreamResponse.throwOnFailure(): StreamResponse { + val response = this + if (response.hasError() || response.stream == null) { + if (response.apiError != null) { + throw SubsonicRESTException(response.apiError) + } else { + throw IOException( + "Failed to make endpoint request, code: " + response.responseHttpCode + ) + } + } + return this +} + +/** + * Gets a stream url. + * + * Calling this method do actual connection to the backend, though not downloading all content. + * + * Consider do not use this method, but [SubsonicAPIDefinition.stream] call. + */ +fun SubsonicAPIDefinition.getStreamUrl(id: String): String { + val response = this.stream(id, format = "raw").execute() + val url = response.raw().request().url().toString() + if (response.isSuccessful) { + response.body()?.close() + } + return url +} diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index 6484208e..c111716b 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -3,23 +3,18 @@ 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 java.security.SecureRandom import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit.MILLISECONDS import javax.net.ssl.SSLContext import javax.net.ssl.X509TrustManager import okhttp3.OkHttpClient -import okhttp3.ResponseBody import okhttp3.logging.HttpLoggingInterceptor import org.moire.ultrasonic.api.subsonic.interceptors.PasswordHexInterceptor import org.moire.ultrasonic.api.subsonic.interceptors.PasswordMD5Interceptor import org.moire.ultrasonic.api.subsonic.interceptors.ProxyPasswordInterceptor import org.moire.ultrasonic.api.subsonic.interceptors.RangeHeaderInterceptor import org.moire.ultrasonic.api.subsonic.interceptors.VersionInterceptor -import org.moire.ultrasonic.api.subsonic.response.StreamResponse -import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse -import retrofit2.Response import retrofit2.Retrofit private const val READ_TIMEOUT = 60_000L @@ -48,8 +43,11 @@ class SubsonicAPIClient( config.enableLdapUserSupport ) + var onProtocolChange: (SubsonicAPIVersions) -> Unit = {} + /** - * Get currently used protocol version. + * The currently used protocol version. + * The setter also updates the interceptors and callback (if registered) */ var protocolVersion = config.minimalProtocolVersion private set(value) { @@ -57,6 +55,7 @@ class SubsonicAPIClient( proxyPasswordInterceptor.apiVersion = field wrappedApi.currentApiVersion = field versionInterceptor.protocolVersion = field + onProtocolChange(field) } private val okHttpClient = baseOkClient.newBuilder() @@ -78,18 +77,19 @@ class SubsonicAPIClient( .apply { if (config.debug) addLogging() } .build() - private val jacksonMapper = ObjectMapper() - .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) - .registerModule(KotlinModule()) - + // Create the Retrofit instance, and register a special converter factory + // It will update our protocol version to the correct version, once we made a successful call private val retrofit = Retrofit.Builder() .baseUrl("${config.baseUrl}/rest/") .client(okHttpClient) .addConverterFactory( VersionAwareJacksonConverterFactory.create( - { protocolVersion = it }, + { + // Only trigger update on change + if (protocolVersion != it) { + protocolVersion = it + } + }, jacksonMapper ) ) @@ -102,85 +102,6 @@ class SubsonicAPIClient( val api: SubsonicAPIDefinition get() = wrappedApi - /** - * TODO: Remove this in favour of handling the stream response inside RESTService - * 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() - } - - /** - * TODO: Remove this in favour of handling the stream response inside RESTService - * 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() - } - - /** - * TODO: Remove this in favour of handling the stream response inside RESTService - * Convenient method to get user avatar using [username]. - * - * It detects the response `Content-Type` and tries to parse subsonic error if there is one. - * - * Prefer this method over [SubsonicAPIDefinition.getAvatar] as this handles error cases. - */ - fun getAvatar(username: String): StreamResponse = handleStreamResponse { - api.getAvatar(username).execute() - } - - // TODO: Move this to response checker - 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, responseHttpCode = response.code()) - } else { - StreamResponse( - stream = responseBody?.byteStream(), - responseHttpCode = response.code() - ) - } - } else { - StreamResponse(responseHttpCode = response.code()) - } - } - - /** - * Get stream url. - * - * Calling this method do actual connection to the backend, though not downloading all content. - * - * Consider do not use this method, but [stream] call. - */ - fun getStreamUrl(id: String): String { - val request = api.stream(id).execute() - val url = request.raw().request().url().toString() - if (request.isSuccessful) { - request.body()?.close() - } - return url - } - private fun OkHttpClient.Builder.addLogging() { val loggingInterceptor = HttpLoggingInterceptor(okLogger) loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY @@ -202,4 +123,12 @@ class SubsonicAPIClient( hostnameVerifier { _, _ -> true } } + + companion object { + val jacksonMapper: ObjectMapper = ObjectMapper() + .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) + .registerModule(KotlinModule()) + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/SubsonicRESTException.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicRESTException.kt similarity index 67% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/SubsonicRESTException.kt rename to core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicRESTException.kt index 4d315d0a..0ce8861c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/SubsonicRESTException.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicRESTException.kt @@ -1,6 +1,4 @@ -package org.moire.ultrasonic.service - -import org.moire.ultrasonic.api.subsonic.SubsonicError +package org.moire.ultrasonic.api.subsonic /** * Exception returned by API with given `code`. diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/VersionAwareJacksonConverterFactory.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/VersionAwareJacksonConverterFactory.kt index 66ba8d8b..7863d7a1 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/VersionAwareJacksonConverterFactory.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/VersionAwareJacksonConverterFactory.kt @@ -63,7 +63,6 @@ class VersionAwareJacksonConverterFactory( } } - @Suppress("SwallowedException") class VersionAwareResponseBodyConverter ( private val notifier: (SubsonicAPIVersions) -> Unit = {}, private val adapter: ObjectReader @@ -77,7 +76,7 @@ class VersionAwareJacksonConverterFactory( if (response is SubsonicResponse) { try { notifier(response.version) - } catch (e: IllegalArgumentException) { + } catch (ignored: IllegalArgumentException) { // no-op } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java index f7c24cd8..3c14ad65 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java @@ -30,6 +30,7 @@ import android.widget.Toast; import org.jetbrains.annotations.NotNull; import org.moire.ultrasonic.R; import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; +import org.moire.ultrasonic.api.subsonic.SubsonicRESTException; import org.moire.ultrasonic.app.UApp; import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.JukeboxStatus; diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index a8e9b819..d9c11c86 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -37,12 +37,15 @@ class ActiveServerProvider( cachedServer = repository.findById(serverId) } Timber.d( - "getActiveServer retrieved from DataBase, id: $serverId; " + - "cachedServer: $cachedServer" + "getActiveServer retrieved from DataBase, id: %s cachedServer: %s", + serverId, cachedServer ) } - if (cachedServer != null) return cachedServer!! + if (cachedServer != null) { + return cachedServer!! + } + setActiveServerId(0) } @@ -105,7 +108,7 @@ class ActiveServerProvider( * @param method: The Rest resource to use * @return The Rest Url of the method on the server */ - fun getRestUrl(method: String?): String? { + fun getRestUrl(method: String?): String { val builder = StringBuilder(8192) val activeServer = getActiveServer() val serverUrl: String = activeServer.url diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index c2c0b6d2..fb26fa50 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -14,7 +14,6 @@ import org.moire.ultrasonic.cache.PermanentFileStorage import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.log.TimberOkHttpLogger -import org.moire.ultrasonic.service.ApiCallResponseChecker import org.moire.ultrasonic.service.CachedMusicService import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.OfflineMusicService @@ -68,10 +67,9 @@ val musicServiceModule = module { single { TimberOkHttpLogger() } single { SubsonicAPIClient(get(), get()) } - single { ApiCallResponseChecker(get(), get()) } single(named(ONLINE_MUSIC_SERVICE)) { - CachedMusicService(RESTMusicService(get(), get(), get(), get())) + CachedMusicService(RESTMusicService(get(), get(), get())) } single(named(OFFLINE_MUSIC_SERVICE)) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index 39dc892d..077a7587 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -22,12 +22,13 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration +import org.moire.ultrasonic.api.subsonic.SubsonicRESTException +import org.moire.ultrasonic.api.subsonic.falseOnFailure import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse +import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting -import org.moire.ultrasonic.service.ApiCallResponseChecker import org.moire.ultrasonic.service.MusicServiceFactory -import org.moire.ultrasonic.service.SubsonicRESTException import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.ModalBackgroundTask @@ -360,7 +361,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { // Execute a ping to check the authentication, now using the correct API version. pingResponse = subsonicApiClient.api.ping().execute() - ApiCallResponseChecker.checkResponseSuccessful(pingResponse) + pingResponse.throwOnFailure() currentServerSetting!!.chatSupport = isServerFunctionAvailable { subsonicApiClient.api.getChatMessages().execute() @@ -387,7 +388,8 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { updateProgress(getProgress()) val licenseResponse = subsonicApiClient.api.getLicense().execute() - ApiCallResponseChecker.checkResponseSuccessful(licenseResponse) + licenseResponse.throwOnFailure() + if (!licenseResponse.body()!!.license.valid) { return getProgress() + "\n" + resources.getString(R.string.settings_testing_unlicensed) @@ -438,9 +440,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { private fun isServerFunctionAvailable(function: () -> Response): Boolean { return try { - val response = function() - ApiCallResponseChecker.checkResponseSuccessful(response) - true + function().falseOnFailure() } catch (_: IOException) { false } catch (_: SubsonicRESTException) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt index e009f58c..0c32e2d3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt @@ -6,6 +6,7 @@ import com.squareup.picasso.RequestHandler import java.io.IOException import okio.Okio import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.toStreamResponse /** * Loads avatars from subsonic api. @@ -23,7 +24,7 @@ class AvatarRequestHandler( val username = request.uri.getQueryParameter(QUERY_USERNAME) ?: throw IllegalArgumentException("Nullable username") - val response = apiClient.getAvatar(username) + val response = apiClient.api.getAvatar(username).execute().toStreamResponse() if (response.hasError() || response.stream == null) { throw IOException("${response.apiError}") } else { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt index a6aeb048..fbfad62f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt @@ -7,13 +7,14 @@ import com.squareup.picasso.RequestHandler import java.io.IOException import okio.Okio import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.toStreamResponse import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL /** * Loads cover arts from subsonic api. */ -class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : RequestHandler() { +class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { return with(data.uri) { scheme == SCHEME && @@ -38,7 +39,9 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request } // Try to fetch the image from the API - val response = apiClient.getCoverArt(id, size) + val response = client.api.getCoverArt(id, size).execute().toStreamResponse() + + // Handle the response if (!response.hasError() && response.stream != null) { return Result(Okio.source(response.stream!!), NETWORK) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 52f7e5ef..9d40ad28 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -13,8 +13,9 @@ import java.io.OutputStream import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.throwOnFailure +import org.moire.ultrasonic.api.subsonic.toStreamResponse import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.service.RESTMusicService import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -24,9 +25,12 @@ import timber.log.Timber */ class ImageLoader( context: Context, - private val apiClient: SubsonicAPIClient, + apiClient: SubsonicAPIClient, private val config: ImageLoaderConfig ) { + // Shortcut + @Suppress("VariableNaming", "PropertyName") + val API = apiClient.api private val picasso = Picasso.Builder(context) .addRequestHandler(CoverArtRequestHandler(apiClient)) @@ -143,8 +147,8 @@ class ImageLoader( // Query the API Timber.d("Loading cover art for: %s", entry) - val response = apiClient.getCoverArt(id!!, size.toLong()) - RESTMusicService.checkStreamResponseError(response) + val response = API.getCoverArt(id!!, size.toLong()).execute().toStreamResponse() + response.throwOnFailure() // Check for failure if (response.stream == null) return diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt deleted file mode 100644 index 51bd4b0a..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.moire.ultrasonic.service - -import java.io.IOException -import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient -import org.moire.ultrasonic.api.subsonic.SubsonicAPIDefinition -import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse -import org.moire.ultrasonic.data.ActiveServerProvider -import retrofit2.Response -import timber.log.Timber - -/** - * This call wraps Subsonic API calls so their results can be checked for errors, API version, etc - */ -class ApiCallResponseChecker( - private val subsonicAPIClient: SubsonicAPIClient, - private val activeServerProvider: ActiveServerProvider -) { - /** - * Executes a Subsonic API call with response check - */ - @Throws(SubsonicRESTException::class, IOException::class) - fun > callWithResponseCheck( - call: (SubsonicAPIDefinition) -> T - ): T { - // Check for API version when first contacting the server - if (activeServerProvider.getActiveServer().minimumApiVersion == null) { - try { - val response = subsonicAPIClient.api.ping().execute() - if (response.body() != null) { - val restApiVersion = response.body()!!.version.restApiVersion - Timber.i("Server minimum API version set to %s", restApiVersion) - activeServerProvider.setMinimumApiVersion(restApiVersion) - } - } catch (ignored: Exception) { - // This Ping is only used to get the API Version, if it fails, that's no problem. - } - } - - // This call will be now executed with the correct API Version, so it shouldn't fail - val result = call.invoke(subsonicAPIClient.api) - checkResponseSuccessful(result) - return result - } - - /** - * Creates Exceptions from the results returned by the Subsonic API - */ - companion object { - @Throws(SubsonicRESTException::class, IOException::class) - fun checkResponseSuccessful(response: Response) { - if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) { - return - } - if (!response.isSuccessful) { - throw IOException("Server error, code: " + response.code()) - } else if ( - response.body()!!.status === SubsonicResponse.Status.ERROR && - response.body()!!.error != null - ) { - throw SubsonicRESTException(response.body()!!.error!!) - } else { - throw IOException("Failed to perform request: " + response.code()) - } - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt index 588ac266..1b876fb5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt @@ -28,6 +28,7 @@ import java.security.cert.CertificateException import javax.net.ssl.SSLException import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException +import org.moire.ultrasonic.api.subsonic.SubsonicRESTException import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage import org.moire.ultrasonic.util.Util import timber.log.Timber diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index d3e3e708..ab7b0717 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -13,11 +13,14 @@ import java.io.IOException import java.io.InputStream import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.getStreamUrl import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName import org.moire.ultrasonic.api.subsonic.models.JukeboxAction -import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.moire.ultrasonic.api.subsonic.throwOnFailure +import org.moire.ultrasonic.api.subsonic.toStreamResponse import org.moire.ultrasonic.cache.PermanentFileStorage import org.moire.ultrasonic.cache.serializers.getIndexesSerializer import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer @@ -50,20 +53,24 @@ import timber.log.Timber */ @Suppress("LargeClass") open class RESTMusicService( - private val subsonicAPIClient: SubsonicAPIClient, + subsonicAPIClient: SubsonicAPIClient, private val fileStorage: PermanentFileStorage, - private val activeServerProvider: ActiveServerProvider, - private val responseChecker: ApiCallResponseChecker + private val activeServerProvider: ActiveServerProvider ) : MusicService { + // Shortcut to the API + @Suppress("VariableNaming", "PropertyName") + val API = subsonicAPIClient.api + @Throws(Exception::class) override fun ping() { - responseChecker.callWithResponseCheck { api -> api.ping().execute() } + API.ping().execute().throwOnFailure() } @Throws(Exception::class) override fun isLicenseValid(): Boolean { - val response = responseChecker.callWithResponseCheck { api -> api.getLicense().execute() } + val response = API.getLicense().execute() + response.throwOnFailure() return response.body()!!.license.valid } @@ -78,9 +85,8 @@ open class RESTMusicService( if (cachedMusicFolders != null && !refresh) return cachedMusicFolders - val response = responseChecker.callWithResponseCheck { api -> - api.getMusicFolders().execute() - } + val response = API.getMusicFolders().execute() + response.throwOnFailure() val musicFolders = response.body()!!.musicFolders.toDomainEntityList() fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer()) @@ -98,9 +104,8 @@ open class RESTMusicService( val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer()) if (cachedIndexes != null && !refresh) return cachedIndexes - val response = responseChecker.callWithResponseCheck { api -> - api.getIndexes(musicFolderId, null).execute() - } + val response = API.getIndexes(musicFolderId, null).execute() + response.throwOnFailure() val indexes = response.body()!!.indexes.toDomainEntity() fileStorage.store(indexName, indexes, getIndexesSerializer()) @@ -114,9 +119,8 @@ open class RESTMusicService( val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer()) if (cachedArtists != null && !refresh) return cachedArtists - val response = responseChecker.callWithResponseCheck { api -> - api.getArtists(null).execute() - } + val response = API.getArtists(null).execute() + response.throwOnFailure() val indexes = response.body()!!.indexes.toDomainEntity() fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer()) @@ -129,7 +133,7 @@ open class RESTMusicService( albumId: String?, artistId: String? ) { - responseChecker.callWithResponseCheck { api -> api.star(id, albumId, artistId).execute() } + API.star(id, albumId, artistId).execute().throwOnFailure() } @Throws(Exception::class) @@ -138,7 +142,7 @@ open class RESTMusicService( albumId: String?, artistId: String? ) { - responseChecker.callWithResponseCheck { api -> api.unstar(id, albumId, artistId).execute() } + API.unstar(id, albumId, artistId).execute().throwOnFailure() } @Throws(Exception::class) @@ -146,7 +150,7 @@ open class RESTMusicService( id: String, rating: Int ) { - responseChecker.callWithResponseCheck { api -> api.setRating(id, rating).execute() } + API.setRating(id, rating).execute().throwOnFailure() } @Throws(Exception::class) @@ -155,9 +159,8 @@ open class RESTMusicService( name: String?, refresh: Boolean ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> - api.getMusicDirectory(id).execute() - } + val response = API.getMusicDirectory(id).execute() + response.throwOnFailure() return response.body()!!.musicDirectory.toDomainEntity() } @@ -168,7 +171,8 @@ open class RESTMusicService( name: String?, refresh: Boolean ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> api.getArtist(id).execute() } + val response = API.getArtist(id).execute() + response.throwOnFailure() return response.body()!!.artist.toMusicDirectoryDomainEntity() } @@ -179,7 +183,8 @@ open class RESTMusicService( name: String?, refresh: Boolean ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> api.getAlbum(id).execute() } + val response = API.getAlbum(id).execute() + response.throwOnFailure() return response.body()!!.album.toMusicDirectoryDomainEntity() } @@ -207,10 +212,10 @@ open class RESTMusicService( private fun searchOld( criteria: SearchCriteria ): SearchResult { - val response = responseChecker.callWithResponseCheck { api -> - api.search(null, null, null, criteria.query, criteria.songCount, null, null) + val response = + API.search(null, null, null, criteria.query, criteria.songCount, null, null) .execute() - } + response.throwOnFailure() return response.body()!!.searchResult.toDomainEntity() } @@ -223,12 +228,12 @@ open class RESTMusicService( criteria: SearchCriteria ): SearchResult { requireNotNull(criteria.query) { "Query param is null" } - val response = responseChecker.callWithResponseCheck { api -> - api.search2( - criteria.query, criteria.artistCount, null, criteria.albumCount, null, - criteria.songCount, null - ).execute() - } + val response = API.search2( + criteria.query, criteria.artistCount, null, criteria.albumCount, null, + criteria.songCount, null + ).execute() + + response.throwOnFailure() return response.body()!!.searchResult.toDomainEntity() } @@ -238,12 +243,12 @@ open class RESTMusicService( criteria: SearchCriteria ): SearchResult { requireNotNull(criteria.query) { "Query param is null" } - val response = responseChecker.callWithResponseCheck { api -> - api.search3( - criteria.query, criteria.artistCount, null, criteria.albumCount, null, - criteria.songCount, null - ).execute() - } + val response = API.search3( + criteria.query, criteria.artistCount, null, criteria.albumCount, null, + criteria.songCount, null + ).execute() + + response.throwOnFailure() return response.body()!!.searchResult.toDomainEntity() } @@ -253,9 +258,8 @@ open class RESTMusicService( id: String, name: String ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> - api.getPlaylist(id).execute() - } + val response = API.getPlaylist(id).execute() + response.throwOnFailure() val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity() savePlaylist(name, playlist) @@ -300,9 +304,8 @@ open class RESTMusicService( override fun getPlaylists( refresh: Boolean ): List { - val response = responseChecker.callWithResponseCheck { api -> - api.getPlaylists(null).execute() - } + val response = API.getPlaylists(null).execute() + response.throwOnFailure() return response.body()!!.playlists.toDomainEntitiesList() } @@ -318,16 +321,15 @@ open class RESTMusicService( for ((id1) in entries) { pSongIds.add(id1) } - responseChecker.callWithResponseCheck { api -> - api.createPlaylist(id, name, pSongIds.toList()).execute() - } + + API.createPlaylist(id, name, pSongIds.toList()).execute().throwOnFailure() } @Throws(Exception::class) override fun deletePlaylist( id: String ) { - responseChecker.callWithResponseCheck { api -> api.deletePlaylist(id).execute() } + API.deletePlaylist(id).execute().throwOnFailure() } @Throws(Exception::class) @@ -337,19 +339,16 @@ open class RESTMusicService( comment: String?, pub: Boolean ) { - responseChecker.callWithResponseCheck { api -> - api.updatePlaylist(id, name, comment, pub, null, null) - .execute() - } + API.updatePlaylist(id, name, comment, pub, null, null) + .execute().throwOnFailure() } @Throws(Exception::class) override fun getPodcastsChannels( refresh: Boolean ): List { - val response = responseChecker.callWithResponseCheck { api -> - api.getPodcasts(false, null).execute() - } + val response = API.getPodcasts(false, null).execute() + response.throwOnFailure() return response.body()!!.podcastChannels.toDomainEntitiesList() } @@ -358,9 +357,8 @@ open class RESTMusicService( override fun getPodcastEpisodes( podcastChannelId: String? ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> - api.getPodcasts(true, podcastChannelId).execute() - } + val response = API.getPodcasts(true, podcastChannelId).execute() + response.throwOnFailure() val podcastEntries = response.body()!!.podcastChannels[0].episodeList val musicDirectory = MusicDirectory() @@ -384,9 +382,8 @@ open class RESTMusicService( artist: String, title: String ): Lyrics { - val response = responseChecker.callWithResponseCheck { api -> - api.getLyrics(artist, title).execute() - } + val response = API.getLyrics(artist, title).execute() + response.throwOnFailure() return response.body()!!.lyrics.toDomainEntity() } @@ -396,9 +393,7 @@ open class RESTMusicService( id: String, submission: Boolean ) { - responseChecker.callWithResponseCheck { api -> - api.scrobble(id, null, submission).execute() - } + API.scrobble(id, null, submission).execute().throwOnFailure() } @Throws(Exception::class) @@ -408,10 +403,17 @@ open class RESTMusicService( offset: Int, musicFolderId: String? ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> - api.getAlbumList(fromName(type), size, offset, null, null, null, musicFolderId) - .execute() - } + val response = API.getAlbumList( + fromName(type), + size, + offset, + null, + null, + null, + musicFolderId + ).execute() + + response.throwOnFailure() val childList = response.body()!!.albumList.toDomainEntityList() val result = MusicDirectory() @@ -427,17 +429,17 @@ open class RESTMusicService( offset: Int, musicFolderId: String? ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> - api.getAlbumList2( - fromName(type), - size, - offset, - null, - null, - null, - musicFolderId - ).execute() - } + val response = API.getAlbumList2( + fromName(type), + size, + offset, + null, + null, + null, + musicFolderId + ).execute() + + response.throwOnFailure() val result = MusicDirectory() result.addAll(response.body()!!.albumList.toDomainEntityList()) @@ -449,15 +451,15 @@ open class RESTMusicService( override fun getRandomSongs( size: Int ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> - api.getRandomSongs( - size, - null, - null, - null, - null - ).execute() - } + val response = API.getRandomSongs( + size, + null, + null, + null, + null + ).execute() + + response.throwOnFailure() val result = MusicDirectory() result.addAll(response.body()!!.songsList.toDomainEntityList()) @@ -467,18 +469,18 @@ open class RESTMusicService( @Throws(Exception::class) override fun getStarred(): SearchResult { - val response = responseChecker.callWithResponseCheck { api -> - api.getStarred(null).execute() - } + val response = API.getStarred(null).execute() + + response.throwOnFailure() return response.body()!!.starred.toDomainEntity() } @Throws(Exception::class) override fun getStarred2(): SearchResult { - val response = responseChecker.callWithResponseCheck { api -> - api.getStarred2(null).execute() - } + val response = API.getStarred2(null).execute() + + response.throwOnFailure() return response.body()!!.starred2.toDomainEntity() } @@ -491,8 +493,10 @@ open class RESTMusicService( ): Pair { val songOffset = if (offset < 0) 0 else offset - val response = subsonicAPIClient.stream(song.id, maxBitrate, songOffset) - checkStreamResponseError(response) + val response = API.stream(song.id, maxBitrate, offset = songOffset) + .execute().toStreamResponse() + + response.throwOnFailure() if (response.stream == null) { throw IOException("Null stream response") @@ -518,13 +522,18 @@ open class RESTMusicService( Thread( { - expectedResult[0] = subsonicAPIClient.getStreamUrl(id) + "&format=raw" + expectedResult[0] = API.getStreamUrl(id) latch.countDown() }, "Get-Video-Url" ).start() - latch.await(5, TimeUnit.SECONDS) + // Getting the stream can take a long time on some servers + latch.await(1, TimeUnit.MINUTES) + + if (expectedResult[0] == null) { + throw TimeoutException("Server didn't respond in time") + } return expectedResult[0]!! } @@ -533,10 +542,10 @@ open class RESTMusicService( override fun updateJukeboxPlaylist( ids: List? ): JukeboxStatus { - val response = responseChecker.callWithResponseCheck { api -> - api.jukeboxControl(JukeboxAction.SET, null, null, ids, null) - .execute() - } + val response = API.jukeboxControl(JukeboxAction.SET, null, null, ids, null) + .execute() + + response.throwOnFailure() return response.body()!!.jukebox.toDomainEntity() } @@ -546,40 +555,40 @@ open class RESTMusicService( index: Int, offsetSeconds: Int ): JukeboxStatus { - val response = responseChecker.callWithResponseCheck { api -> - api.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null) - .execute() - } + val response = API.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null) + .execute() + + response.throwOnFailure() return response.body()!!.jukebox.toDomainEntity() } @Throws(Exception::class) override fun stopJukebox(): JukeboxStatus { - val response = responseChecker.callWithResponseCheck { api -> - api.jukeboxControl(JukeboxAction.STOP, null, null, null, null) - .execute() - } + val response = API.jukeboxControl(JukeboxAction.STOP, null, null, null, null) + .execute() + + response.throwOnFailure() return response.body()!!.jukebox.toDomainEntity() } @Throws(Exception::class) override fun startJukebox(): JukeboxStatus { - val response = responseChecker.callWithResponseCheck { api -> - api.jukeboxControl(JukeboxAction.START, null, null, null, null) - .execute() - } + val response = API.jukeboxControl(JukeboxAction.START, null, null, null, null) + .execute() + + response.throwOnFailure() return response.body()!!.jukebox.toDomainEntity() } @Throws(Exception::class) override fun getJukeboxStatus(): JukeboxStatus { - val response = responseChecker.callWithResponseCheck { api -> - api.jukeboxControl(JukeboxAction.STATUS, null, null, null, null) - .execute() - } + val response = API.jukeboxControl(JukeboxAction.STATUS, null, null, null, null) + .execute() + + response.throwOnFailure() return response.body()!!.jukebox.toDomainEntity() } @@ -588,10 +597,10 @@ open class RESTMusicService( override fun setJukeboxGain( gain: Float ): JukeboxStatus { - val response = responseChecker.callWithResponseCheck { api -> - api.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain) - .execute() - } + val response = API.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain) + .execute() + + response.throwOnFailure() return response.body()!!.jukebox.toDomainEntity() } @@ -600,7 +609,8 @@ open class RESTMusicService( override fun getShares( refresh: Boolean ): List { - val response = responseChecker.callWithResponseCheck { api -> api.getShares().execute() } + val response = API.getShares().execute() + response.throwOnFailure() return response.body()!!.shares.toDomainEntitiesList() } @@ -609,7 +619,8 @@ open class RESTMusicService( override fun getGenres( refresh: Boolean ): List? { - val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() } + val response = API.getGenres().execute() + response.throwOnFailure() return response.body()!!.genresList.toDomainEntityList() } @@ -620,9 +631,8 @@ open class RESTMusicService( count: Int, offset: Int ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> - api.getSongsByGenre(genre, count, offset, null).execute() - } + val response = API.getSongsByGenre(genre, count, offset, null).execute() + response.throwOnFailure() val result = MusicDirectory() result.addAll(response.body()!!.songsList.toDomainEntityList()) @@ -634,9 +644,9 @@ open class RESTMusicService( override fun getUser( username: String ): UserInfo { - val response = responseChecker.callWithResponseCheck { api -> - api.getUser(username).execute() - } + val response = API.getUser(username).execute() + + response.throwOnFailure() return response.body()!!.user.toDomainEntity() } @@ -645,9 +655,9 @@ open class RESTMusicService( override fun getChatMessages( since: Long? ): List { - val response = responseChecker.callWithResponseCheck { api -> - api.getChatMessages(since).execute() - } + val response = API.getChatMessages(since).execute() + + response.throwOnFailure() return response.body()!!.chatMessages.toDomainEntitiesList() } @@ -656,12 +666,13 @@ open class RESTMusicService( override fun addChatMessage( message: String ) { - responseChecker.callWithResponseCheck { api -> api.addChatMessage(message).execute() } + API.addChatMessage(message).execute().throwOnFailure() } @Throws(Exception::class) override fun getBookmarks(): List { - val response = responseChecker.callWithResponseCheck { api -> api.getBookmarks().execute() } + val response = API.getBookmarks().execute() + response.throwOnFailure() return response.body()!!.bookmarkList.toDomainEntitiesList() } @@ -671,23 +682,22 @@ open class RESTMusicService( id: String, position: Int ) { - responseChecker.callWithResponseCheck { api -> - api.createBookmark(id, position.toLong(), null).execute() - } + API.createBookmark(id, position.toLong(), null).execute().throwOnFailure() } @Throws(Exception::class) override fun deleteBookmark( id: String ) { - responseChecker.callWithResponseCheck { api -> api.deleteBookmark(id).execute() } + API.deleteBookmark(id).execute().throwOnFailure() } @Throws(Exception::class) override fun getVideos( refresh: Boolean ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> api.getVideos().execute() } + val response = API.getVideos().execute() + response.throwOnFailure() val musicDirectory = MusicDirectory() musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList()) @@ -701,9 +711,8 @@ open class RESTMusicService( description: String?, expires: Long? ): List { - val response = responseChecker.callWithResponseCheck { api -> - api.createShare(ids, description, expires).execute() - } + val response = API.createShare(ids, description, expires).execute() + response.throwOnFailure() return response.body()!!.shares.toDomainEntitiesList() } @@ -712,7 +721,7 @@ open class RESTMusicService( override fun deleteShare( id: String ) { - responseChecker.callWithResponseCheck { api -> api.deleteShare(id).execute() } + API.deleteShare(id).execute().throwOnFailure() } @Throws(Exception::class) @@ -726,8 +735,15 @@ open class RESTMusicService( expiresValue = null } - responseChecker.callWithResponseCheck { api -> - api.updateShare(id, description, expiresValue).execute() + API.updateShare(id, description, expiresValue).execute().throwOnFailure() + } + + init { + // The client will notice if the minimum supported API version has changed + // By registering a callback we ensure this info is saved in the database as well + subsonicAPIClient.onProtocolChange = { + Timber.i("Server minimum API version set to %s", it) + activeServerProvider.setMinimumApiVersion(it.toString()) } } @@ -735,19 +751,5 @@ open class RESTMusicService( private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder" private const val INDEXES_STORAGE_NAME = "indexes" private const val ARTISTS_STORAGE_NAME = "artists" - - // TODO: Move to response checker - @Throws(SubsonicRESTException::class, IOException::class) - fun checkStreamResponseError(response: StreamResponse) { - if (response.hasError() || response.stream == null) { - if (response.apiError != null) { - throw SubsonicRESTException(response.apiError!!) - } else { - throw IOException( - "Failed to make endpoint request, code: " + response.responseHttpCode - ) - } - } - } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/RestErrorMapper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/RestErrorMapper.kt index 1ab7f043..2f2e2304 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/RestErrorMapper.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/RestErrorMapper.kt @@ -12,7 +12,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicError.TokenAuthNotSupportedForL import org.moire.ultrasonic.api.subsonic.SubsonicError.TrialPeriodIsOver import org.moire.ultrasonic.api.subsonic.SubsonicError.UserNotAuthorizedForOperation import org.moire.ultrasonic.api.subsonic.SubsonicError.WrongUsernameOrPassword -import org.moire.ultrasonic.service.SubsonicRESTException +import org.moire.ultrasonic.api.subsonic.SubsonicRESTException /** * Extension for [SubsonicRESTException] that returns localized error string, that can used to @@ -21,7 +21,7 @@ import org.moire.ultrasonic.service.SubsonicRESTException fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String = when (error) { is Generic -> { - val message = error.message + val message = (error as Generic).message val errorMessage = if (message == "") { context.getString(R.string.api_subsonic_generic_no_message) } else { diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt index 0b27cbcf..5ad84cff 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt @@ -14,6 +14,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.moire.ultrasonic.api.subsonic.toStreamResponse import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -59,7 +60,7 @@ class AvatarRequestHandlerTest { apiError = null, responseHttpCode = 200 ) - whenever(mockApiClient.getAvatar(any())) + whenever(mockApiClient.api.getAvatar(any()).execute().toStreamResponse()) .thenReturn(streamResponse) val response = handler.load( diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt index 17dda53e..442eba3c 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt @@ -16,6 +16,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.moire.ultrasonic.api.subsonic.toStreamResponse import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -56,7 +57,9 @@ class CoverArtRequestHandlerTest { fun `Should throw IOException when request to api failed`() { val streamResponse = StreamResponse(null, null, 500) - whenever(mockApiClient.getCoverArt(any(), anyOrNull())).thenReturn(streamResponse) + whenever( + mockApiClient.api.getCoverArt(any(), anyOrNull()).execute().toStreamResponse() + ).thenReturn(streamResponse) val fail = { handler.load(createLoadCoverArtRequest("some").buildRequest(), 0) @@ -73,7 +76,9 @@ class CoverArtRequestHandlerTest { responseHttpCode = 200 ) - whenever(mockApiClient.getCoverArt(any(), anyOrNull())).thenReturn(streamResponse) + whenever( + mockApiClient.api.getCoverArt(any(), anyOrNull()).execute().toStreamResponse() + ).thenReturn(streamResponse) val response = handler.load( createLoadCoverArtRequest("some").buildRequest(), 0