Cleaner separation of API result handling.

* CallResponseChecker functionality are now Kotlin Extensions
* Removed unfitting shortcuts from SubsonicAPIClient
* Increase timeout to get a video url
* Fix a bug, that the Rest API version was checked twice on each call
This commit is contained in:
tzugen 2021-06-09 12:19:34 +02:00
parent dd1f55a927
commit d9a7fa2413
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
21 changed files with 340 additions and 364 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<out ResponseBody>.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<SubsonicResponse>(
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<out SubsonicResponse>.throwOnFailure(): Response<out SubsonicResponse> {
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<out SubsonicResponse>.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
}

View File

@ -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<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, 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())
}
}

View File

@ -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`.

View File

@ -63,7 +63,6 @@ class VersionAwareJacksonConverterFactory(
}
}
@Suppress("SwallowedException")
class VersionAwareResponseBodyConverter<T> (
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
}
}

View File

@ -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;

View File

@ -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

View File

@ -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<HttpLoggingInterceptor.Logger> { TimberOkHttpLogger() }
single { SubsonicAPIClient(get(), get()) }
single { ApiCallResponseChecker(get(), get()) }
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
CachedMusicService(RESTMusicService(get(), get(), get(), get()))
CachedMusicService(RESTMusicService(get(), get(), get()))
}
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {

View File

@ -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<out SubsonicResponse>): Boolean {
return try {
val response = function()
ApiCallResponseChecker.checkResponseSuccessful(response)
true
function().falseOnFailure()
} catch (_: IOException) {
false
} catch (_: SubsonicRESTException) {

View File

@ -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 {

View File

@ -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)
}

View File

@ -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

View File

@ -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 <T : Response<out SubsonicResponse>> 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<out SubsonicResponse>) {
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())
}
}
}
}

View File

@ -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

View File

@ -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<Playlist> {
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<PodcastsChannel> {
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<InputStream, Boolean> {
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<String>?
): 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<Share> {
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<Genre>? {
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<ChatMessage> {
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<Bookmark> {
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<Share> {
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
)
}
}
}
}
}

View File

@ -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 {

View File

@ -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(

View File

@ -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