mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-17 04:00:39 +01:00
Merge pull request #517 from tzugen/cleanupStreamHandling
Cleaner separation of API result handling.
This commit is contained in:
commit
6ea4ac5829
@ -10,7 +10,7 @@ import org.moire.ultrasonic.api.subsonic.interceptors.toHexBytes
|
|||||||
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration test for [SubsonicAPIClient.getStreamUrl] method.
|
* Integration test for [getStreamUrl] method.
|
||||||
*/
|
*/
|
||||||
class GetStreamUrlTest {
|
class GetStreamUrlTest {
|
||||||
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
||||||
@ -30,7 +30,7 @@ class GetStreamUrlTest {
|
|||||||
)
|
)
|
||||||
client = SubsonicAPIClient(config)
|
client = SubsonicAPIClient(config)
|
||||||
val baseExpectedUrl = mockWebServerRule.mockWebServer.url("").toString()
|
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()}"
|
"&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`() {
|
fun `Should return valid stream url`() {
|
||||||
mockWebServerRule.enqueueResponse("ping_ok.json")
|
mockWebServerRule.enqueueResponse("ping_ok.json")
|
||||||
|
|
||||||
val streamUrl = client.getStreamUrl(id)
|
val streamUrl = client.api.getStreamUrl(id)
|
||||||
|
|
||||||
streamUrl `should be equal to` expectedUrl
|
streamUrl `should be equal to` expectedUrl
|
||||||
}
|
}
|
||||||
@ -47,7 +47,7 @@ class GetStreamUrlTest {
|
|||||||
fun `Should still return stream url if connection failed`() {
|
fun `Should still return stream url if connection failed`() {
|
||||||
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(500))
|
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(500))
|
||||||
|
|
||||||
val streamUrl = client.getStreamUrl(id)
|
val streamUrl = client.api.getStreamUrl(id)
|
||||||
|
|
||||||
streamUrl `should be equal to` expectedUrl
|
streamUrl `should be equal to` expectedUrl
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@ import org.amshove.kluent.`should not be`
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration test for [SubsonicAPIClient.getAvatar] call.
|
* Integration test for [SubsonicAPIDefinition.getAvatar] call.
|
||||||
*/
|
*/
|
||||||
class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
|
class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `Should handle api error response`() {
|
fun `Should handle api error response`() {
|
||||||
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
|
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) {
|
with(response) {
|
||||||
stream `should be` null
|
stream `should be` null
|
||||||
@ -28,7 +28,7 @@ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
|
|||||||
val httpErrorCode = 500
|
val httpErrorCode = 500
|
||||||
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
|
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
|
||||||
|
|
||||||
val response = client.getAvatar("some")
|
val response = client.api.getAvatar("some-id").execute().toStreamResponse()
|
||||||
|
|
||||||
with(response) {
|
with(response) {
|
||||||
stream `should be equal to` null
|
stream `should be equal to` null
|
||||||
@ -44,7 +44,7 @@ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
|
|||||||
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
|
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = client.stream("some")
|
val response = client.api.stream("some-id").execute().toStreamResponse()
|
||||||
|
|
||||||
with(response) {
|
with(response) {
|
||||||
responseHttpCode `should be equal to` 200
|
responseHttpCode `should be equal to` 200
|
||||||
|
@ -14,7 +14,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
|
|||||||
fun `Should handle api error response`() {
|
fun `Should handle api error response`() {
|
||||||
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
|
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) {
|
with(response) {
|
||||||
stream `should be` null
|
stream `should be` null
|
||||||
@ -28,7 +28,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
|
|||||||
val httpErrorCode = 404
|
val httpErrorCode = 404
|
||||||
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
|
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
|
||||||
|
|
||||||
val response = client.getCoverArt("some-id")
|
val response = client.api.getCoverArt("some-id").execute().toStreamResponse()
|
||||||
|
|
||||||
with(response) {
|
with(response) {
|
||||||
stream `should be` null
|
stream `should be` null
|
||||||
@ -44,7 +44,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
|
|||||||
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
|
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = client.getCoverArt("some-id")
|
val response = client.api.getCoverArt("some-id").execute().toStreamResponse()
|
||||||
|
|
||||||
with(response) {
|
with(response) {
|
||||||
responseHttpCode `should be equal to` 200
|
responseHttpCode `should be equal to` 200
|
||||||
|
@ -14,7 +14,7 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
|
|||||||
fun `Should handle api error response`() {
|
fun `Should handle api error response`() {
|
||||||
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
|
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) {
|
with(response) {
|
||||||
stream `should be` null
|
stream `should be` null
|
||||||
@ -28,7 +28,7 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
|
|||||||
val httpErrorCode = 404
|
val httpErrorCode = 404
|
||||||
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
|
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
|
||||||
|
|
||||||
val response = client.stream("some-id")
|
val response = client.api.stream("some-id").execute().toStreamResponse()
|
||||||
|
|
||||||
with(response) {
|
with(response) {
|
||||||
stream `should be` null
|
stream `should be` null
|
||||||
@ -38,13 +38,13 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should return successfull call stream`() {
|
fun `Should return successful call stream`() {
|
||||||
mockWebServerRule.mockWebServer.enqueue(
|
mockWebServerRule.mockWebServer.enqueue(
|
||||||
MockResponse()
|
MockResponse()
|
||||||
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
|
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = client.stream("some-id")
|
val response = client.api.stream("some-id").execute().toStreamResponse()
|
||||||
|
|
||||||
with(response) {
|
with(response) {
|
||||||
responseHttpCode `should be equal to` 200
|
responseHttpCode `should be equal to` 200
|
||||||
|
@ -45,7 +45,8 @@ import retrofit2.Call
|
|||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
internal class ApiVersionCheckWrapper(
|
internal class ApiVersionCheckWrapper(
|
||||||
val api: SubsonicAPIDefinition,
|
val api: SubsonicAPIDefinition,
|
||||||
var currentApiVersion: SubsonicAPIVersions
|
var currentApiVersion: SubsonicAPIVersions,
|
||||||
|
var isRealProtocolVersion: Boolean = false
|
||||||
) : SubsonicAPIDefinition by api {
|
) : SubsonicAPIDefinition by api {
|
||||||
override fun getArtists(musicFolderId: String?): Call<GetArtistsResponse> {
|
override fun getArtists(musicFolderId: String?): Call<GetArtistsResponse> {
|
||||||
checkVersion(V1_8_0)
|
checkVersion(V1_8_0)
|
||||||
@ -325,10 +326,15 @@ internal class ApiVersionCheckWrapper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
||||||
if (currentApiVersion < expectedVersion) throw ApiNotSupportedException(currentApiVersion)
|
// If it is true, it is probably the first call with this server
|
||||||
|
if (!isRealProtocolVersion) return
|
||||||
|
if (currentApiVersion < expectedVersion)
|
||||||
|
throw ApiNotSupportedException(currentApiVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
||||||
|
// If it is true, it is probably the first call with this server
|
||||||
|
if (!isRealProtocolVersion) return
|
||||||
if (param != null) {
|
if (param != null) {
|
||||||
checkVersion(expectedVersion)
|
checkVersion(expectedVersion)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
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 extension checks API call results for errors, API version, etc
|
||||||
|
* It creates Exceptions from the results returned by the Subsonic API
|
||||||
|
*/
|
||||||
|
@Suppress("ThrowsCount")
|
||||||
|
fun <T : SubsonicResponse> Response<out T>.throwOnFailure(): Response<out T> {
|
||||||
|
val response = this
|
||||||
|
|
||||||
|
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
|
||||||
|
return this as Response<T>
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This extension checks API call results for errors, API version, etc
|
||||||
|
* @return Boolean: True if everything was ok, false if an error was found
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
@ -3,7 +3,6 @@ package org.moire.ultrasonic.api.subsonic
|
|||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||||
@ -18,7 +17,6 @@ import org.moire.ultrasonic.api.subsonic.interceptors.ProxyPasswordInterceptor
|
|||||||
import org.moire.ultrasonic.api.subsonic.interceptors.RangeHeaderInterceptor
|
import org.moire.ultrasonic.api.subsonic.interceptors.RangeHeaderInterceptor
|
||||||
import org.moire.ultrasonic.api.subsonic.interceptors.VersionInterceptor
|
import org.moire.ultrasonic.api.subsonic.interceptors.VersionInterceptor
|
||||||
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
|
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
|
||||||
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
@ -48,15 +46,20 @@ class SubsonicAPIClient(
|
|||||||
config.enableLdapUserSupport
|
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
|
var protocolVersion = config.minimalProtocolVersion
|
||||||
private set(value) {
|
private set(value) {
|
||||||
field = value
|
field = value
|
||||||
proxyPasswordInterceptor.apiVersion = field
|
proxyPasswordInterceptor.apiVersion = field
|
||||||
wrappedApi.currentApiVersion = field
|
wrappedApi.currentApiVersion = field
|
||||||
|
wrappedApi.isRealProtocolVersion = true
|
||||||
versionInterceptor.protocolVersion = field
|
versionInterceptor.protocolVersion = field
|
||||||
|
onProtocolChange(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val okHttpClient = baseOkClient.newBuilder()
|
private val okHttpClient = baseOkClient.newBuilder()
|
||||||
@ -78,18 +81,19 @@ class SubsonicAPIClient(
|
|||||||
.apply { if (config.debug) addLogging() }
|
.apply { if (config.debug) addLogging() }
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val jacksonMapper = ObjectMapper()
|
// Create the Retrofit instance, and register a special converter factory
|
||||||
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
|
// It will update our protocol version to the correct version, once we made a successful call
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
|
||||||
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
|
|
||||||
.registerModule(KotlinModule())
|
|
||||||
|
|
||||||
private val retrofit = Retrofit.Builder()
|
private val retrofit = Retrofit.Builder()
|
||||||
.baseUrl("${config.baseUrl}/rest/")
|
.baseUrl("${config.baseUrl}/rest/")
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
.addConverterFactory(
|
.addConverterFactory(
|
||||||
VersionAwareJacksonConverterFactory.create(
|
VersionAwareJacksonConverterFactory.create(
|
||||||
{ protocolVersion = it },
|
{
|
||||||
|
// Only trigger update on change, or if still using the default
|
||||||
|
if (protocolVersion != it || !config.isRealProtocolVersion) {
|
||||||
|
protocolVersion = it
|
||||||
|
}
|
||||||
|
},
|
||||||
jacksonMapper
|
jacksonMapper
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -97,90 +101,12 @@ class SubsonicAPIClient(
|
|||||||
|
|
||||||
private val wrappedApi = ApiVersionCheckWrapper(
|
private val wrappedApi = ApiVersionCheckWrapper(
|
||||||
retrofit.create(SubsonicAPIDefinition::class.java),
|
retrofit.create(SubsonicAPIDefinition::class.java),
|
||||||
config.minimalProtocolVersion
|
config.minimalProtocolVersion,
|
||||||
|
config.isRealProtocolVersion
|
||||||
)
|
)
|
||||||
|
|
||||||
val api: SubsonicAPIDefinition get() = wrappedApi
|
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() {
|
private fun OkHttpClient.Builder.addLogging() {
|
||||||
val loggingInterceptor = HttpLoggingInterceptor(okLogger)
|
val loggingInterceptor = HttpLoggingInterceptor(okLogger)
|
||||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
|
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||||
@ -202,4 +128,19 @@ class SubsonicAPIClient(
|
|||||||
|
|
||||||
hostnameVerifier { _, _ -> true }
|
hostnameVerifier { _, _ -> true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is necessary because Mockito has problems with stubbing chained calls
|
||||||
|
*/
|
||||||
|
fun toStreamResponse(call: Response<ResponseBody>): StreamResponse {
|
||||||
|
return call.toStreamResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,5 +11,6 @@ data class SubsonicClientConfiguration(
|
|||||||
val clientID: String,
|
val clientID: String,
|
||||||
val allowSelfSignedCertificate: Boolean = false,
|
val allowSelfSignedCertificate: Boolean = false,
|
||||||
val enableLdapUserSupport: Boolean = false,
|
val enableLdapUserSupport: Boolean = false,
|
||||||
val debug: Boolean = false
|
val debug: Boolean = false,
|
||||||
|
val isRealProtocolVersion: Boolean = false
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.api.subsonic
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicError
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exception returned by API with given `code`.
|
* Exception returned by API with given `code`.
|
@ -63,7 +63,6 @@ class VersionAwareJacksonConverterFactory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("SwallowedException")
|
|
||||||
class VersionAwareResponseBodyConverter<T> (
|
class VersionAwareResponseBodyConverter<T> (
|
||||||
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
||||||
private val adapter: ObjectReader
|
private val adapter: ObjectReader
|
||||||
@ -77,7 +76,7 @@ class VersionAwareJacksonConverterFactory(
|
|||||||
if (response is SubsonicResponse) {
|
if (response is SubsonicResponse) {
|
||||||
try {
|
try {
|
||||||
notifier(response.version)
|
notifier(response.version)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (ignored: IllegalArgumentException) {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import org.moire.ultrasonic.api.subsonic.models.AlbumListType.BY_GENRE
|
|||||||
*/
|
*/
|
||||||
class ApiVersionCheckWrapperTest {
|
class ApiVersionCheckWrapperTest {
|
||||||
private val apiMock = mock<SubsonicAPIDefinition>()
|
private val apiMock = mock<SubsonicAPIDefinition>()
|
||||||
private val wrapper = ApiVersionCheckWrapper(apiMock, V1_1_0)
|
private val wrapper = ApiVersionCheckWrapper(apiMock, V1_1_0, isRealProtocolVersion = true)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should just call real api for ping`() {
|
fun `Should just call real api for ping`() {
|
||||||
|
@ -30,6 +30,7 @@ import android.widget.Toast;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.moire.ultrasonic.R;
|
import org.moire.ultrasonic.R;
|
||||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
|
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException;
|
||||||
import org.moire.ultrasonic.app.UApp;
|
import org.moire.ultrasonic.app.UApp;
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||||
import org.moire.ultrasonic.domain.JukeboxStatus;
|
import org.moire.ultrasonic.domain.JukeboxStatus;
|
||||||
|
@ -37,12 +37,15 @@ class ActiveServerProvider(
|
|||||||
cachedServer = repository.findById(serverId)
|
cachedServer = repository.findById(serverId)
|
||||||
}
|
}
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"getActiveServer retrieved from DataBase, id: $serverId; " +
|
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
|
||||||
"cachedServer: $cachedServer"
|
serverId, cachedServer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachedServer != null) return cachedServer!!
|
if (cachedServer != null) {
|
||||||
|
return cachedServer!!
|
||||||
|
}
|
||||||
|
|
||||||
setActiveServerId(0)
|
setActiveServerId(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +108,7 @@ class ActiveServerProvider(
|
|||||||
* @param method: The Rest resource to use
|
* @param method: The Rest resource to use
|
||||||
* @return The Rest Url of the method on the server
|
* @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 builder = StringBuilder(8192)
|
||||||
val activeServer = getActiveServer()
|
val activeServer = getActiveServer()
|
||||||
val serverUrl: String = activeServer.url
|
val serverUrl: String = activeServer.url
|
||||||
|
@ -14,7 +14,6 @@ import org.moire.ultrasonic.cache.PermanentFileStorage
|
|||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||||
import org.moire.ultrasonic.log.TimberOkHttpLogger
|
import org.moire.ultrasonic.log.TimberOkHttpLogger
|
||||||
import org.moire.ultrasonic.service.ApiCallResponseChecker
|
|
||||||
import org.moire.ultrasonic.service.CachedMusicService
|
import org.moire.ultrasonic.service.CachedMusicService
|
||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
import org.moire.ultrasonic.service.OfflineMusicService
|
import org.moire.ultrasonic.service.OfflineMusicService
|
||||||
@ -50,28 +49,29 @@ val musicServiceModule = module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
|
val server = get<ActiveServerProvider>().getActiveServer()
|
||||||
|
|
||||||
return@single SubsonicClientConfiguration(
|
return@single SubsonicClientConfiguration(
|
||||||
baseUrl = get<ActiveServerProvider>().getActiveServer().url,
|
baseUrl = server.url,
|
||||||
username = get<ActiveServerProvider>().getActiveServer().userName,
|
username = server.userName,
|
||||||
password = get<ActiveServerProvider>().getActiveServer().password,
|
password = server.password,
|
||||||
minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion(
|
minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion(
|
||||||
get<ActiveServerProvider>().getActiveServer().minimumApiVersion
|
server.minimumApiVersion
|
||||||
?: Constants.REST_PROTOCOL_VERSION
|
?: Constants.REST_PROTOCOL_VERSION
|
||||||
),
|
),
|
||||||
clientID = Constants.REST_CLIENT_ID,
|
clientID = Constants.REST_CLIENT_ID,
|
||||||
allowSelfSignedCertificate = get<ActiveServerProvider>()
|
allowSelfSignedCertificate = server.allowSelfSignedCertificate,
|
||||||
.getActiveServer().allowSelfSignedCertificate,
|
enableLdapUserSupport = server.ldapSupport,
|
||||||
enableLdapUserSupport = get<ActiveServerProvider>().getActiveServer().ldapSupport,
|
debug = BuildConfig.DEBUG,
|
||||||
debug = BuildConfig.DEBUG
|
isRealProtocolVersion = server.minimumApiVersion != null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
single<HttpLoggingInterceptor.Logger> { TimberOkHttpLogger() }
|
single<HttpLoggingInterceptor.Logger> { TimberOkHttpLogger() }
|
||||||
single { SubsonicAPIClient(get(), get()) }
|
single { SubsonicAPIClient(get(), get()) }
|
||||||
single { ApiCallResponseChecker(get(), get()) }
|
|
||||||
|
|
||||||
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
|
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
|
||||||
CachedMusicService(RESTMusicService(get(), get(), get(), get()))
|
CachedMusicService(RESTMusicService(get(), get(), get()))
|
||||||
}
|
}
|
||||||
|
|
||||||
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
||||||
|
@ -22,12 +22,13 @@ import org.moire.ultrasonic.R
|
|||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
|
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.response.SubsonicResponse
|
||||||
|
import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.data.ServerSetting
|
import org.moire.ultrasonic.data.ServerSetting
|
||||||
import org.moire.ultrasonic.service.ApiCallResponseChecker
|
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
import org.moire.ultrasonic.service.SubsonicRESTException
|
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.ErrorDialog
|
import org.moire.ultrasonic.util.ErrorDialog
|
||||||
import org.moire.ultrasonic.util.ModalBackgroundTask
|
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.
|
// Execute a ping to check the authentication, now using the correct API version.
|
||||||
pingResponse = subsonicApiClient.api.ping().execute()
|
pingResponse = subsonicApiClient.api.ping().execute()
|
||||||
ApiCallResponseChecker.checkResponseSuccessful(pingResponse)
|
pingResponse.throwOnFailure()
|
||||||
|
|
||||||
currentServerSetting!!.chatSupport = isServerFunctionAvailable {
|
currentServerSetting!!.chatSupport = isServerFunctionAvailable {
|
||||||
subsonicApiClient.api.getChatMessages().execute()
|
subsonicApiClient.api.getChatMessages().execute()
|
||||||
@ -387,7 +388,8 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
|
|||||||
updateProgress(getProgress())
|
updateProgress(getProgress())
|
||||||
|
|
||||||
val licenseResponse = subsonicApiClient.api.getLicense().execute()
|
val licenseResponse = subsonicApiClient.api.getLicense().execute()
|
||||||
ApiCallResponseChecker.checkResponseSuccessful(licenseResponse)
|
licenseResponse.throwOnFailure()
|
||||||
|
|
||||||
if (!licenseResponse.body()!!.license.valid) {
|
if (!licenseResponse.body()!!.license.valid) {
|
||||||
return getProgress() + "\n" +
|
return getProgress() + "\n" +
|
||||||
resources.getString(R.string.settings_testing_unlicensed)
|
resources.getString(R.string.settings_testing_unlicensed)
|
||||||
@ -438,9 +440,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
|
|||||||
|
|
||||||
private fun isServerFunctionAvailable(function: () -> Response<out SubsonicResponse>): Boolean {
|
private fun isServerFunctionAvailable(function: () -> Response<out SubsonicResponse>): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val response = function()
|
function().falseOnFailure()
|
||||||
ApiCallResponseChecker.checkResponseSuccessful(response)
|
|
||||||
true
|
|
||||||
} catch (_: IOException) {
|
} catch (_: IOException) {
|
||||||
false
|
false
|
||||||
} catch (_: SubsonicRESTException) {
|
} catch (_: SubsonicRESTException) {
|
||||||
|
@ -10,8 +10,6 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import java.net.ConnectException
|
|
||||||
import java.net.UnknownHostException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -89,10 +87,8 @@ open class GenericListModel(application: Application) :
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
load(isOffline, useId3Tags, musicService, refresh, bundle)
|
load(isOffline, useId3Tags, musicService, refresh, bundle)
|
||||||
} catch (exception: ConnectException) {
|
} catch (all: Exception) {
|
||||||
handleException(exception, swipe.context)
|
handleException(all, swipe.context)
|
||||||
} catch (exception: UnknownHostException) {
|
|
||||||
handleException(exception, swipe.context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
|||||||
* Loads avatars from subsonic api.
|
* Loads avatars from subsonic api.
|
||||||
*/
|
*/
|
||||||
class AvatarRequestHandler(
|
class AvatarRequestHandler(
|
||||||
private val apiClient: SubsonicAPIClient
|
private val client: SubsonicAPIClient
|
||||||
) : RequestHandler() {
|
) : RequestHandler() {
|
||||||
override fun canHandleRequest(data: Request): Boolean {
|
override fun canHandleRequest(data: Request): Boolean {
|
||||||
return with(data.uri) {
|
return with(data.uri) {
|
||||||
@ -23,7 +23,9 @@ class AvatarRequestHandler(
|
|||||||
val username = request.uri.getQueryParameter(QUERY_USERNAME)
|
val username = request.uri.getQueryParameter(QUERY_USERNAME)
|
||||||
?: throw IllegalArgumentException("Nullable username")
|
?: throw IllegalArgumentException("Nullable username")
|
||||||
|
|
||||||
val response = apiClient.getAvatar(username)
|
// Inverted call order, because Mockito has problems with chained calls.
|
||||||
|
val response = client.toStreamResponse(client.api.getAvatar(username).execute())
|
||||||
|
|
||||||
if (response.hasError() || response.stream == null) {
|
if (response.hasError() || response.stream == null) {
|
||||||
throw IOException("${response.apiError}")
|
throw IOException("${response.apiError}")
|
||||||
} else {
|
} else {
|
||||||
|
@ -7,13 +7,14 @@ import com.squareup.picasso.RequestHandler
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import okio.Okio
|
import okio.Okio
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
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_LARGE
|
||||||
import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL
|
import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads cover arts from subsonic api.
|
* 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 {
|
override fun canHandleRequest(data: Request): Boolean {
|
||||||
return with(data.uri) {
|
return with(data.uri) {
|
||||||
scheme == SCHEME &&
|
scheme == SCHEME &&
|
||||||
@ -38,7 +39,10 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to fetch the image from the API
|
// Try to fetch the image from the API
|
||||||
val response = apiClient.getCoverArt(id, size)
|
// Inverted call order, because Mockito has problems with chained calls.
|
||||||
|
val response = client.toStreamResponse(client.api.getCoverArt(id, size).execute())
|
||||||
|
|
||||||
|
// Handle the response
|
||||||
if (!response.hasError() && response.stream != null) {
|
if (!response.hasError() && response.stream != null) {
|
||||||
return Result(Okio.source(response.stream!!), NETWORK)
|
return Result(Okio.source(response.stream!!), NETWORK)
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,9 @@ import java.io.OutputStream
|
|||||||
import org.moire.ultrasonic.BuildConfig
|
import org.moire.ultrasonic.BuildConfig
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
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.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.service.RESTMusicService
|
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -24,9 +25,12 @@ import timber.log.Timber
|
|||||||
*/
|
*/
|
||||||
class ImageLoader(
|
class ImageLoader(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val apiClient: SubsonicAPIClient,
|
apiClient: SubsonicAPIClient,
|
||||||
private val config: ImageLoaderConfig
|
private val config: ImageLoaderConfig
|
||||||
) {
|
) {
|
||||||
|
// Shortcut
|
||||||
|
@Suppress("VariableNaming", "PropertyName")
|
||||||
|
val API = apiClient.api
|
||||||
|
|
||||||
private val picasso = Picasso.Builder(context)
|
private val picasso = Picasso.Builder(context)
|
||||||
.addRequestHandler(CoverArtRequestHandler(apiClient))
|
.addRequestHandler(CoverArtRequestHandler(apiClient))
|
||||||
@ -143,8 +147,8 @@ class ImageLoader(
|
|||||||
|
|
||||||
// Query the API
|
// Query the API
|
||||||
Timber.d("Loading cover art for: %s", entry)
|
Timber.d("Loading cover art for: %s", entry)
|
||||||
val response = apiClient.getCoverArt(id!!, size.toLong())
|
val response = API.getCoverArt(id!!, size.toLong()).execute().toStreamResponse()
|
||||||
RESTMusicService.checkStreamResponseError(response)
|
response.throwOnFailure()
|
||||||
|
|
||||||
// Check for failure
|
// Check for failure
|
||||||
if (response.stream == null) return
|
if (response.stream == null) return
|
||||||
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -28,6 +28,7 @@ import java.security.cert.CertificateException
|
|||||||
import javax.net.ssl.SSLException
|
import javax.net.ssl.SSLException
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
|
||||||
import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
|
import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -13,11 +13,14 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
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.AlbumListType.Companion.fromName
|
||||||
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction
|
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.PermanentFileStorage
|
||||||
import org.moire.ultrasonic.cache.serializers.getIndexesSerializer
|
import org.moire.ultrasonic.cache.serializers.getIndexesSerializer
|
||||||
import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer
|
import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer
|
||||||
@ -50,20 +53,23 @@ import timber.log.Timber
|
|||||||
*/
|
*/
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
open class RESTMusicService(
|
open class RESTMusicService(
|
||||||
private val subsonicAPIClient: SubsonicAPIClient,
|
subsonicAPIClient: SubsonicAPIClient,
|
||||||
private val fileStorage: PermanentFileStorage,
|
private val fileStorage: PermanentFileStorage,
|
||||||
private val activeServerProvider: ActiveServerProvider,
|
private val activeServerProvider: ActiveServerProvider
|
||||||
private val responseChecker: ApiCallResponseChecker
|
|
||||||
) : MusicService {
|
) : MusicService {
|
||||||
|
|
||||||
|
// Shortcut to the API
|
||||||
|
@Suppress("VariableNaming", "PropertyName")
|
||||||
|
val API = subsonicAPIClient.api
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun ping() {
|
override fun ping() {
|
||||||
responseChecker.callWithResponseCheck { api -> api.ping().execute() }
|
API.ping().execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun isLicenseValid(): Boolean {
|
override fun isLicenseValid(): Boolean {
|
||||||
val response = responseChecker.callWithResponseCheck { api -> api.getLicense().execute() }
|
val response = API.getLicense().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.license.valid
|
return response.body()!!.license.valid
|
||||||
}
|
}
|
||||||
@ -78,9 +84,7 @@ open class RESTMusicService(
|
|||||||
|
|
||||||
if (cachedMusicFolders != null && !refresh) return cachedMusicFolders
|
if (cachedMusicFolders != null && !refresh) return cachedMusicFolders
|
||||||
|
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getMusicFolders().execute().throwOnFailure()
|
||||||
api.getMusicFolders().execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
val musicFolders = response.body()!!.musicFolders.toDomainEntityList()
|
val musicFolders = response.body()!!.musicFolders.toDomainEntityList()
|
||||||
fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer())
|
fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer())
|
||||||
@ -98,9 +102,7 @@ open class RESTMusicService(
|
|||||||
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
|
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
|
||||||
if (cachedIndexes != null && !refresh) return cachedIndexes
|
if (cachedIndexes != null && !refresh) return cachedIndexes
|
||||||
|
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
|
||||||
api.getIndexes(musicFolderId, null).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
val indexes = response.body()!!.indexes.toDomainEntity()
|
val indexes = response.body()!!.indexes.toDomainEntity()
|
||||||
fileStorage.store(indexName, indexes, getIndexesSerializer())
|
fileStorage.store(indexName, indexes, getIndexesSerializer())
|
||||||
@ -114,9 +116,7 @@ open class RESTMusicService(
|
|||||||
val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer())
|
val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer())
|
||||||
if (cachedArtists != null && !refresh) return cachedArtists
|
if (cachedArtists != null && !refresh) return cachedArtists
|
||||||
|
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getArtists(null).execute().throwOnFailure()
|
||||||
api.getArtists(null).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
val indexes = response.body()!!.indexes.toDomainEntity()
|
val indexes = response.body()!!.indexes.toDomainEntity()
|
||||||
fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer())
|
fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer())
|
||||||
@ -129,7 +129,7 @@ open class RESTMusicService(
|
|||||||
albumId: String?,
|
albumId: String?,
|
||||||
artistId: String?
|
artistId: String?
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api -> api.star(id, albumId, artistId).execute() }
|
API.star(id, albumId, artistId).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -138,7 +138,7 @@ open class RESTMusicService(
|
|||||||
albumId: String?,
|
albumId: String?,
|
||||||
artistId: String?
|
artistId: String?
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api -> api.unstar(id, albumId, artistId).execute() }
|
API.unstar(id, albumId, artistId).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -146,7 +146,7 @@ open class RESTMusicService(
|
|||||||
id: String,
|
id: String,
|
||||||
rating: Int
|
rating: Int
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api -> api.setRating(id, rating).execute() }
|
API.setRating(id, rating).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -155,9 +155,7 @@ open class RESTMusicService(
|
|||||||
name: String?,
|
name: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getMusicDirectory(id).execute().throwOnFailure()
|
||||||
api.getMusicDirectory(id).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.musicDirectory.toDomainEntity()
|
return response.body()!!.musicDirectory.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -168,7 +166,7 @@ open class RESTMusicService(
|
|||||||
name: String?,
|
name: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api -> api.getArtist(id).execute() }
|
val response = API.getArtist(id).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.artist.toMusicDirectoryDomainEntity()
|
return response.body()!!.artist.toMusicDirectoryDomainEntity()
|
||||||
}
|
}
|
||||||
@ -179,7 +177,7 @@ open class RESTMusicService(
|
|||||||
name: String?,
|
name: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api -> api.getAlbum(id).execute() }
|
val response = API.getAlbum(id).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.album.toMusicDirectoryDomainEntity()
|
return response.body()!!.album.toMusicDirectoryDomainEntity()
|
||||||
}
|
}
|
||||||
@ -207,10 +205,9 @@ open class RESTMusicService(
|
|||||||
private fun searchOld(
|
private fun searchOld(
|
||||||
criteria: SearchCriteria
|
criteria: SearchCriteria
|
||||||
): SearchResult {
|
): SearchResult {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response =
|
||||||
api.search(null, null, null, criteria.query, criteria.songCount, null, null)
|
API.search(null, null, null, criteria.query, criteria.songCount, null, null)
|
||||||
.execute()
|
.execute().throwOnFailure()
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.searchResult.toDomainEntity()
|
return response.body()!!.searchResult.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -223,12 +220,10 @@ open class RESTMusicService(
|
|||||||
criteria: SearchCriteria
|
criteria: SearchCriteria
|
||||||
): SearchResult {
|
): SearchResult {
|
||||||
requireNotNull(criteria.query) { "Query param is null" }
|
requireNotNull(criteria.query) { "Query param is null" }
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.search2(
|
||||||
api.search2(
|
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
|
||||||
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
|
criteria.songCount, null
|
||||||
criteria.songCount, null
|
).execute().throwOnFailure()
|
||||||
).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.searchResult.toDomainEntity()
|
return response.body()!!.searchResult.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -238,12 +233,10 @@ open class RESTMusicService(
|
|||||||
criteria: SearchCriteria
|
criteria: SearchCriteria
|
||||||
): SearchResult {
|
): SearchResult {
|
||||||
requireNotNull(criteria.query) { "Query param is null" }
|
requireNotNull(criteria.query) { "Query param is null" }
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.search3(
|
||||||
api.search3(
|
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
|
||||||
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
|
criteria.songCount, null
|
||||||
criteria.songCount, null
|
).execute().throwOnFailure()
|
||||||
).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.searchResult.toDomainEntity()
|
return response.body()!!.searchResult.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -253,9 +246,7 @@ open class RESTMusicService(
|
|||||||
id: String,
|
id: String,
|
||||||
name: String
|
name: String
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getPlaylist(id).execute().throwOnFailure()
|
||||||
api.getPlaylist(id).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity()
|
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity()
|
||||||
savePlaylist(name, playlist)
|
savePlaylist(name, playlist)
|
||||||
@ -300,9 +291,7 @@ open class RESTMusicService(
|
|||||||
override fun getPlaylists(
|
override fun getPlaylists(
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): List<Playlist> {
|
): List<Playlist> {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getPlaylists(null).execute().throwOnFailure()
|
||||||
api.getPlaylists(null).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.playlists.toDomainEntitiesList()
|
return response.body()!!.playlists.toDomainEntitiesList()
|
||||||
}
|
}
|
||||||
@ -318,16 +307,15 @@ open class RESTMusicService(
|
|||||||
for ((id1) in entries) {
|
for ((id1) in entries) {
|
||||||
pSongIds.add(id1)
|
pSongIds.add(id1)
|
||||||
}
|
}
|
||||||
responseChecker.callWithResponseCheck { api ->
|
|
||||||
api.createPlaylist(id, name, pSongIds.toList()).execute()
|
API.createPlaylist(id, name, pSongIds.toList()).execute().throwOnFailure()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun deletePlaylist(
|
override fun deletePlaylist(
|
||||||
id: String
|
id: String
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api -> api.deletePlaylist(id).execute() }
|
API.deletePlaylist(id).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -337,19 +325,15 @@ open class RESTMusicService(
|
|||||||
comment: String?,
|
comment: String?,
|
||||||
pub: Boolean
|
pub: Boolean
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api ->
|
API.updatePlaylist(id, name, comment, pub, null, null)
|
||||||
api.updatePlaylist(id, name, comment, pub, null, null)
|
.execute().throwOnFailure()
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getPodcastsChannels(
|
override fun getPodcastsChannels(
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): List<PodcastsChannel> {
|
): List<PodcastsChannel> {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getPodcasts(false, null).execute().throwOnFailure()
|
||||||
api.getPodcasts(false, null).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.podcastChannels.toDomainEntitiesList()
|
return response.body()!!.podcastChannels.toDomainEntitiesList()
|
||||||
}
|
}
|
||||||
@ -358,9 +342,7 @@ open class RESTMusicService(
|
|||||||
override fun getPodcastEpisodes(
|
override fun getPodcastEpisodes(
|
||||||
podcastChannelId: String?
|
podcastChannelId: String?
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getPodcasts(true, podcastChannelId).execute().throwOnFailure()
|
||||||
api.getPodcasts(true, podcastChannelId).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
val podcastEntries = response.body()!!.podcastChannels[0].episodeList
|
val podcastEntries = response.body()!!.podcastChannels[0].episodeList
|
||||||
val musicDirectory = MusicDirectory()
|
val musicDirectory = MusicDirectory()
|
||||||
@ -384,9 +366,7 @@ open class RESTMusicService(
|
|||||||
artist: String,
|
artist: String,
|
||||||
title: String
|
title: String
|
||||||
): Lyrics {
|
): Lyrics {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getLyrics(artist, title).execute().throwOnFailure()
|
||||||
api.getLyrics(artist, title).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.lyrics.toDomainEntity()
|
return response.body()!!.lyrics.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -396,9 +376,7 @@ open class RESTMusicService(
|
|||||||
id: String,
|
id: String,
|
||||||
submission: Boolean
|
submission: Boolean
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api ->
|
API.scrobble(id, null, submission).execute().throwOnFailure()
|
||||||
api.scrobble(id, null, submission).execute()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -408,10 +386,15 @@ open class RESTMusicService(
|
|||||||
offset: Int,
|
offset: Int,
|
||||||
musicFolderId: String?
|
musicFolderId: String?
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getAlbumList(
|
||||||
api.getAlbumList(fromName(type), size, offset, null, null, null, musicFolderId)
|
fromName(type),
|
||||||
.execute()
|
size,
|
||||||
}
|
offset,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
musicFolderId
|
||||||
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
val childList = response.body()!!.albumList.toDomainEntityList()
|
val childList = response.body()!!.albumList.toDomainEntityList()
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
@ -427,17 +410,15 @@ open class RESTMusicService(
|
|||||||
offset: Int,
|
offset: Int,
|
||||||
musicFolderId: String?
|
musicFolderId: String?
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getAlbumList2(
|
||||||
api.getAlbumList2(
|
fromName(type),
|
||||||
fromName(type),
|
size,
|
||||||
size,
|
offset,
|
||||||
offset,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
musicFolderId
|
||||||
musicFolderId
|
).execute().throwOnFailure()
|
||||||
).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
result.addAll(response.body()!!.albumList.toDomainEntityList())
|
result.addAll(response.body()!!.albumList.toDomainEntityList())
|
||||||
@ -449,15 +430,13 @@ open class RESTMusicService(
|
|||||||
override fun getRandomSongs(
|
override fun getRandomSongs(
|
||||||
size: Int
|
size: Int
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getRandomSongs(
|
||||||
api.getRandomSongs(
|
size,
|
||||||
size,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null
|
||||||
null
|
).execute().throwOnFailure()
|
||||||
).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
result.addAll(response.body()!!.songsList.toDomainEntityList())
|
result.addAll(response.body()!!.songsList.toDomainEntityList())
|
||||||
@ -467,18 +446,14 @@ open class RESTMusicService(
|
|||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getStarred(): SearchResult {
|
override fun getStarred(): SearchResult {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getStarred(null).execute().throwOnFailure()
|
||||||
api.getStarred(null).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.starred.toDomainEntity()
|
return response.body()!!.starred.toDomainEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getStarred2(): SearchResult {
|
override fun getStarred2(): SearchResult {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getStarred2(null).execute().throwOnFailure()
|
||||||
api.getStarred2(null).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.starred2.toDomainEntity()
|
return response.body()!!.starred2.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -491,8 +466,10 @@ open class RESTMusicService(
|
|||||||
): Pair<InputStream, Boolean> {
|
): Pair<InputStream, Boolean> {
|
||||||
val songOffset = if (offset < 0) 0 else offset
|
val songOffset = if (offset < 0) 0 else offset
|
||||||
|
|
||||||
val response = subsonicAPIClient.stream(song.id, maxBitrate, songOffset)
|
val response = API.stream(song.id, maxBitrate, offset = songOffset)
|
||||||
checkStreamResponseError(response)
|
.execute().toStreamResponse()
|
||||||
|
|
||||||
|
response.throwOnFailure()
|
||||||
|
|
||||||
if (response.stream == null) {
|
if (response.stream == null) {
|
||||||
throw IOException("Null stream response")
|
throw IOException("Null stream response")
|
||||||
@ -518,13 +495,18 @@ open class RESTMusicService(
|
|||||||
|
|
||||||
Thread(
|
Thread(
|
||||||
{
|
{
|
||||||
expectedResult[0] = subsonicAPIClient.getStreamUrl(id) + "&format=raw"
|
expectedResult[0] = API.getStreamUrl(id)
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
},
|
},
|
||||||
"Get-Video-Url"
|
"Get-Video-Url"
|
||||||
).start()
|
).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]!!
|
return expectedResult[0]!!
|
||||||
}
|
}
|
||||||
@ -533,10 +515,8 @@ open class RESTMusicService(
|
|||||||
override fun updateJukeboxPlaylist(
|
override fun updateJukeboxPlaylist(
|
||||||
ids: List<String>?
|
ids: List<String>?
|
||||||
): JukeboxStatus {
|
): JukeboxStatus {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.jukeboxControl(JukeboxAction.SET, null, null, ids, null)
|
||||||
api.jukeboxControl(JukeboxAction.SET, null, null, ids, null)
|
.execute().throwOnFailure()
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.jukebox.toDomainEntity()
|
return response.body()!!.jukebox.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -546,40 +526,32 @@ open class RESTMusicService(
|
|||||||
index: Int,
|
index: Int,
|
||||||
offsetSeconds: Int
|
offsetSeconds: Int
|
||||||
): JukeboxStatus {
|
): JukeboxStatus {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null)
|
||||||
api.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null)
|
.execute().throwOnFailure()
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.jukebox.toDomainEntity()
|
return response.body()!!.jukebox.toDomainEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun stopJukebox(): JukeboxStatus {
|
override fun stopJukebox(): JukeboxStatus {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.jukeboxControl(JukeboxAction.STOP, null, null, null, null)
|
||||||
api.jukeboxControl(JukeboxAction.STOP, null, null, null, null)
|
.execute().throwOnFailure()
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.jukebox.toDomainEntity()
|
return response.body()!!.jukebox.toDomainEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun startJukebox(): JukeboxStatus {
|
override fun startJukebox(): JukeboxStatus {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.jukeboxControl(JukeboxAction.START, null, null, null, null)
|
||||||
api.jukeboxControl(JukeboxAction.START, null, null, null, null)
|
.execute().throwOnFailure()
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.jukebox.toDomainEntity()
|
return response.body()!!.jukebox.toDomainEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getJukeboxStatus(): JukeboxStatus {
|
override fun getJukeboxStatus(): JukeboxStatus {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.jukeboxControl(JukeboxAction.STATUS, null, null, null, null)
|
||||||
api.jukeboxControl(JukeboxAction.STATUS, null, null, null, null)
|
.execute().throwOnFailure()
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.jukebox.toDomainEntity()
|
return response.body()!!.jukebox.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -588,10 +560,8 @@ open class RESTMusicService(
|
|||||||
override fun setJukeboxGain(
|
override fun setJukeboxGain(
|
||||||
gain: Float
|
gain: Float
|
||||||
): JukeboxStatus {
|
): JukeboxStatus {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain)
|
||||||
api.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain)
|
.execute().throwOnFailure()
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.jukebox.toDomainEntity()
|
return response.body()!!.jukebox.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -600,7 +570,7 @@ open class RESTMusicService(
|
|||||||
override fun getShares(
|
override fun getShares(
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): List<Share> {
|
): List<Share> {
|
||||||
val response = responseChecker.callWithResponseCheck { api -> api.getShares().execute() }
|
val response = API.getShares().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.shares.toDomainEntitiesList()
|
return response.body()!!.shares.toDomainEntitiesList()
|
||||||
}
|
}
|
||||||
@ -609,7 +579,7 @@ open class RESTMusicService(
|
|||||||
override fun getGenres(
|
override fun getGenres(
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): List<Genre>? {
|
): List<Genre>? {
|
||||||
val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() }
|
val response = API.getGenres().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.genresList.toDomainEntityList()
|
return response.body()!!.genresList.toDomainEntityList()
|
||||||
}
|
}
|
||||||
@ -620,9 +590,7 @@ open class RESTMusicService(
|
|||||||
count: Int,
|
count: Int,
|
||||||
offset: Int
|
offset: Int
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getSongsByGenre(genre, count, offset, null).execute().throwOnFailure()
|
||||||
api.getSongsByGenre(genre, count, offset, null).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
result.addAll(response.body()!!.songsList.toDomainEntityList())
|
result.addAll(response.body()!!.songsList.toDomainEntityList())
|
||||||
@ -634,9 +602,7 @@ open class RESTMusicService(
|
|||||||
override fun getUser(
|
override fun getUser(
|
||||||
username: String
|
username: String
|
||||||
): UserInfo {
|
): UserInfo {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getUser(username).execute().throwOnFailure()
|
||||||
api.getUser(username).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.user.toDomainEntity()
|
return response.body()!!.user.toDomainEntity()
|
||||||
}
|
}
|
||||||
@ -645,9 +611,7 @@ open class RESTMusicService(
|
|||||||
override fun getChatMessages(
|
override fun getChatMessages(
|
||||||
since: Long?
|
since: Long?
|
||||||
): List<ChatMessage> {
|
): List<ChatMessage> {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.getChatMessages(since).execute().throwOnFailure()
|
||||||
api.getChatMessages(since).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.chatMessages.toDomainEntitiesList()
|
return response.body()!!.chatMessages.toDomainEntitiesList()
|
||||||
}
|
}
|
||||||
@ -656,12 +620,12 @@ open class RESTMusicService(
|
|||||||
override fun addChatMessage(
|
override fun addChatMessage(
|
||||||
message: String
|
message: String
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api -> api.addChatMessage(message).execute() }
|
API.addChatMessage(message).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getBookmarks(): List<Bookmark> {
|
override fun getBookmarks(): List<Bookmark> {
|
||||||
val response = responseChecker.callWithResponseCheck { api -> api.getBookmarks().execute() }
|
val response = API.getBookmarks().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.bookmarkList.toDomainEntitiesList()
|
return response.body()!!.bookmarkList.toDomainEntitiesList()
|
||||||
}
|
}
|
||||||
@ -671,23 +635,21 @@ open class RESTMusicService(
|
|||||||
id: String,
|
id: String,
|
||||||
position: Int
|
position: Int
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api ->
|
API.createBookmark(id, position.toLong(), null).execute().throwOnFailure()
|
||||||
api.createBookmark(id, position.toLong(), null).execute()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun deleteBookmark(
|
override fun deleteBookmark(
|
||||||
id: String
|
id: String
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api -> api.deleteBookmark(id).execute() }
|
API.deleteBookmark(id).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getVideos(
|
override fun getVideos(
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api -> api.getVideos().execute() }
|
val response = API.getVideos().execute().throwOnFailure()
|
||||||
|
|
||||||
val musicDirectory = MusicDirectory()
|
val musicDirectory = MusicDirectory()
|
||||||
musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList())
|
musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList())
|
||||||
@ -701,9 +663,7 @@ open class RESTMusicService(
|
|||||||
description: String?,
|
description: String?,
|
||||||
expires: Long?
|
expires: Long?
|
||||||
): List<Share> {
|
): List<Share> {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = API.createShare(ids, description, expires).execute().throwOnFailure()
|
||||||
api.createShare(ids, description, expires).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body()!!.shares.toDomainEntitiesList()
|
return response.body()!!.shares.toDomainEntitiesList()
|
||||||
}
|
}
|
||||||
@ -712,7 +672,7 @@ open class RESTMusicService(
|
|||||||
override fun deleteShare(
|
override fun deleteShare(
|
||||||
id: String
|
id: String
|
||||||
) {
|
) {
|
||||||
responseChecker.callWithResponseCheck { api -> api.deleteShare(id).execute() }
|
API.deleteShare(id).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -726,8 +686,15 @@ open class RESTMusicService(
|
|||||||
expiresValue = null
|
expiresValue = null
|
||||||
}
|
}
|
||||||
|
|
||||||
responseChecker.callWithResponseCheck { api ->
|
API.updateShare(id, description, expiresValue).execute().throwOnFailure()
|
||||||
api.updateShare(id, description, expiresValue).execute()
|
}
|
||||||
|
|
||||||
|
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 +702,5 @@ open class RESTMusicService(
|
|||||||
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
|
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
|
||||||
private const val INDEXES_STORAGE_NAME = "indexes"
|
private const val INDEXES_STORAGE_NAME = "indexes"
|
||||||
private const val ARTISTS_STORAGE_NAME = "artists"
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.TrialPeriodIsOver
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicError.UserNotAuthorizedForOperation
|
import org.moire.ultrasonic.api.subsonic.SubsonicError.UserNotAuthorizedForOperation
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicError.WrongUsernameOrPassword
|
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
|
* 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 =
|
fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String =
|
||||||
when (error) {
|
when (error) {
|
||||||
is Generic -> {
|
is Generic -> {
|
||||||
val message = error.message
|
val message = (error as Generic).message
|
||||||
val errorMessage = if (message == "") {
|
val errorMessage = if (message == "") {
|
||||||
context.getString(R.string.api_subsonic_generic_no_message)
|
context.getString(R.string.api_subsonic_generic_no_message)
|
||||||
} else {
|
} else {
|
||||||
|
@ -9,18 +9,20 @@ import org.amshove.kluent.`should throw`
|
|||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.Answers
|
||||||
import org.mockito.kotlin.any
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
|
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
|
||||||
|
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
@Config(manifest = Config.NONE)
|
@Config(manifest = Config.NONE)
|
||||||
class AvatarRequestHandlerTest {
|
class AvatarRequestHandlerTest {
|
||||||
private val mockApiClient: SubsonicAPIClient = mock()
|
private val mockApiClient: SubsonicAPIClient = mock(defaultAnswer = Answers.RETURNS_DEEP_STUBS)
|
||||||
private val handler = AvatarRequestHandler(mockApiClient)
|
private val handler = AvatarRequestHandler(mockApiClient)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -59,8 +61,10 @@ class AvatarRequestHandlerTest {
|
|||||||
apiError = null,
|
apiError = null,
|
||||||
responseHttpCode = 200
|
responseHttpCode = 200
|
||||||
)
|
)
|
||||||
whenever(mockApiClient.getAvatar(any()))
|
|
||||||
.thenReturn(streamResponse)
|
whenever(
|
||||||
|
mockApiClient.toStreamResponse(any())
|
||||||
|
).thenReturn(streamResponse)
|
||||||
|
|
||||||
val response = handler.load(
|
val response = handler.load(
|
||||||
createLoadAvatarRequest("some-username").buildRequest(), 0
|
createLoadAvatarRequest("some-username").buildRequest(), 0
|
||||||
|
@ -10,8 +10,8 @@ import org.amshove.kluent.`should throw`
|
|||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.Answers
|
||||||
import org.mockito.kotlin.any
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.kotlin.anyOrNull
|
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
@ -20,7 +20,7 @@ import org.robolectric.RobolectricTestRunner
|
|||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class CoverArtRequestHandlerTest {
|
class CoverArtRequestHandlerTest {
|
||||||
private val mockApiClient: SubsonicAPIClient = mock()
|
private val mockApiClient: SubsonicAPIClient = mock(defaultAnswer = Answers.RETURNS_DEEP_STUBS)
|
||||||
private val handler = CoverArtRequestHandler(mockApiClient)
|
private val handler = CoverArtRequestHandler(mockApiClient)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -56,7 +56,9 @@ class CoverArtRequestHandlerTest {
|
|||||||
fun `Should throw IOException when request to api failed`() {
|
fun `Should throw IOException when request to api failed`() {
|
||||||
val streamResponse = StreamResponse(null, null, 500)
|
val streamResponse = StreamResponse(null, null, 500)
|
||||||
|
|
||||||
whenever(mockApiClient.getCoverArt(any(), anyOrNull())).thenReturn(streamResponse)
|
whenever(
|
||||||
|
mockApiClient.toStreamResponse(any())
|
||||||
|
).thenReturn(streamResponse)
|
||||||
|
|
||||||
val fail = {
|
val fail = {
|
||||||
handler.load(createLoadCoverArtRequest("some").buildRequest(), 0)
|
handler.load(createLoadCoverArtRequest("some").buildRequest(), 0)
|
||||||
@ -73,7 +75,9 @@ class CoverArtRequestHandlerTest {
|
|||||||
responseHttpCode = 200
|
responseHttpCode = 200
|
||||||
)
|
)
|
||||||
|
|
||||||
whenever(mockApiClient.getCoverArt(any(), anyOrNull())).thenReturn(streamResponse)
|
whenever(
|
||||||
|
mockApiClient.toStreamResponse(any())
|
||||||
|
).thenReturn(streamResponse)
|
||||||
|
|
||||||
val response = handler.load(
|
val response = handler.load(
|
||||||
createLoadCoverArtRequest("some").buildRequest(), 0
|
createLoadCoverArtRequest("some").buildRequest(), 0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user