diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt deleted file mode 100644 index 259f714b..00000000 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.moire.ultrasonic.api.subsonic - -import okhttp3.mockwebserver.MockResponse -import org.amshove.kluent.`should be equal to` -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_6_0 -import org.moire.ultrasonic.api.subsonic.interceptors.toHexBytes -import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule - -/** - * Integration test for [SubsonicAPIClient.getStreamUrl] method. - */ -class GetStreamUrlTest { - @JvmField @Rule val mockWebServerRule = MockWebServerRule() - - val id = "boom" - private lateinit var client: SubsonicAPIClient - private lateinit var expectedUrl: String - - @Before - fun setUp() { - val config = SubsonicClientConfiguration( - mockWebServerRule.mockWebServer.url("/").toString(), - USERNAME, - PASSWORD, - V1_6_0, - CLIENT_ID - ) - client = SubsonicAPIClient(config) - val baseExpectedUrl = mockWebServerRule.mockWebServer.url("").toString() - expectedUrl = "$baseExpectedUrl/rest/stream.view?id=$id&u=$USERNAME" + - "&c=$CLIENT_ID&f=json&v=${V1_6_0.restApiVersion}&p=enc:${PASSWORD.toHexBytes()}" - } - - @Test - fun `Should return valid stream url`() { - mockWebServerRule.enqueueResponse("ping_ok.json") - - val streamUrl = client.getStreamUrl(id) - - streamUrl `should be equal to` expectedUrl - } - - @Test - fun `Should still return stream url if connection failed`() { - mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(500)) - - val streamUrl = client.getStreamUrl(id) - - streamUrl `should be equal to` expectedUrl - } -} diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAvatarTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAvatarTest.kt index 3bb637ac..d6a11332 100644 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAvatarTest.kt +++ b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAvatarTest.kt @@ -7,14 +7,14 @@ import org.amshove.kluent.`should not be` import org.junit.Test /** - * Integration test for [SubsonicAPIClient.getAvatar] call. + * Integration test for [SubsonicAPIDefinition.getAvatar] call. */ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() { @Test fun `Should handle api error response`() { mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json") - val response = client.getAvatar("some") + val response = client.api.getAvatar("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -28,7 +28,7 @@ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() { val httpErrorCode = 500 mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode)) - val response = client.getAvatar("some") + val response = client.api.getAvatar("some-id").execute().toStreamResponse() with(response) { stream `should be equal to` null @@ -44,7 +44,7 @@ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() { .setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")) ) - val response = client.stream("some") + val response = client.api.stream("some-id").execute().toStreamResponse() with(response) { responseHttpCode `should be equal to` 200 diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt index f760f98a..2c047ee8 100644 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt +++ b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetCoverArtTest.kt @@ -14,7 +14,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() { fun `Should handle api error response`() { mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json") - val response = client.getCoverArt("some-id") + val response = client.api.getCoverArt("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -28,7 +28,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() { val httpErrorCode = 404 mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode)) - val response = client.getCoverArt("some-id") + val response = client.api.getCoverArt("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -44,7 +44,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() { .setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")) ) - val response = client.getCoverArt("some-id") + val response = client.api.getCoverArt("some-id").execute().toStreamResponse() with(response) { responseHttpCode `should be equal to` 200 diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt index b0bd27db..ad16462b 100644 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt +++ b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiStreamTest.kt @@ -14,7 +14,7 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() { fun `Should handle api error response`() { mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json") - val response = client.stream("some-id") + val response = client.api.stream("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -28,7 +28,7 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() { val httpErrorCode = 404 mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode)) - val response = client.stream("some-id") + val response = client.api.stream("some-id").execute().toStreamResponse() with(response) { stream `should be` null @@ -38,13 +38,13 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() { } @Test - fun `Should return successfull call stream`() { + fun `Should return successful call stream`() { mockWebServerRule.mockWebServer.enqueue( MockResponse() .setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")) ) - val response = client.stream("some-id") + val response = client.api.stream("some-id").execute().toStreamResponse() with(response) { responseHttpCode `should be equal to` 200 diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt index 20923b0b..d2da7070 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt @@ -45,7 +45,8 @@ import retrofit2.Call @Suppress("TooManyFunctions") internal class ApiVersionCheckWrapper( val api: SubsonicAPIDefinition, - var currentApiVersion: SubsonicAPIVersions + var currentApiVersion: SubsonicAPIVersions, + var isRealProtocolVersion: Boolean = false ) : SubsonicAPIDefinition by api { override fun getArtists(musicFolderId: String?): Call { checkVersion(V1_8_0) @@ -325,10 +326,15 @@ internal class ApiVersionCheckWrapper( } 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) { + // If it is true, it is probably the first call with this server + if (!isRealProtocolVersion) return if (param != null) { checkVersion(expectedVersion) } diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt new file mode 100644 index 00000000..f9d48d23 --- /dev/null +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt @@ -0,0 +1,85 @@ +package org.moire.ultrasonic.api.subsonic + +import com.fasterxml.jackson.module.kotlin.readValue +import java.io.IOException +import okhttp3.ResponseBody +import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse +import retrofit2.Response + +/** + * Converts a Response to a StreamResponse + */ +fun Response.toStreamResponse(): StreamResponse { + val response = this + return if (response.isSuccessful) { + val responseBody = response.body() + val contentType = responseBody?.contentType() + if ( + contentType != null && + contentType.type().equals("application", true) && + contentType.subtype().equals("json", true) + ) { + val error = SubsonicAPIClient.jacksonMapper.readValue( + responseBody.byteStream() + ) + StreamResponse(apiError = error.error, responseHttpCode = response.code()) + } else { + StreamResponse( + stream = responseBody?.byteStream(), + responseHttpCode = response.code() + ) + } + } else { + StreamResponse(responseHttpCode = response.code()) + } +} + +/** + * This extension checks API call results for errors, API version, etc + * It creates Exceptions from the results returned by the Subsonic API + */ +@Suppress("ThrowsCount") +fun Response.throwOnFailure(): Response { + val response = this + + if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) { + return this as Response + } + 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.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 +} diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index 6484208e..b5847c95 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -3,7 +3,6 @@ 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 @@ -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.VersionInterceptor import org.moire.ultrasonic.api.subsonic.response.StreamResponse -import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import retrofit2.Response import retrofit2.Retrofit @@ -48,18 +46,23 @@ 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) { field = value proxyPasswordInterceptor.apiVersion = field wrappedApi.currentApiVersion = field + wrappedApi.isRealProtocolVersion = true versionInterceptor.protocolVersion = field + onProtocolChange(field) } - private val okHttpClient = baseOkClient.newBuilder() + val okHttpClient: OkHttpClient = baseOkClient.newBuilder() .readTimeout(READ_TIMEOUT, MILLISECONDS) .apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() } .addInterceptor { chain -> @@ -78,18 +81,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()) - - private val retrofit = Retrofit.Builder() + // 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 + val retrofit: Retrofit = Retrofit.Builder() .baseUrl("${config.baseUrl}/rest/") .client(okHttpClient) .addConverterFactory( VersionAwareJacksonConverterFactory.create( - { protocolVersion = it }, + { + // Only trigger update on change, or if still using the default + if (protocolVersion != it || !config.isRealProtocolVersion) { + protocolVersion = it + } + }, jacksonMapper ) ) @@ -97,90 +101,12 @@ class SubsonicAPIClient( private val wrappedApi = ApiVersionCheckWrapper( retrofit.create(SubsonicAPIDefinition::class.java), - config.minimalProtocolVersion + config.minimalProtocolVersion, + config.isRealProtocolVersion ) val api: SubsonicAPIDefinition get() = wrappedApi - /** - * TODO: Remove this in favour of handling the stream response inside RESTService - * Convenient method to get cover art from api using item [id] and optional maximum [size]. - * - * It detects the response `Content-Type` and tries to parse subsonic error if there is one. - * - * Prefer this method over [SubsonicAPIDefinition.getCoverArt] as this handles error cases. - */ - fun getCoverArt(id: String, size: Long? = null): StreamResponse = handleStreamResponse { - api.getCoverArt(id, size).execute() - } - - /** - * TODO: Remove this in favour of handling the stream response inside RESTService - * Convenient method to get media stream from api using item [id] and optional [maxBitrate]. - * - * Optionally also you can provide [offset] that stream should start from. - * - * It detects the response `Content-Type` and tries to parse subsonic error if there is one. - * - * Prefer this method over [SubsonicAPIDefinition.stream] as this handles error cases. - */ - fun stream(id: String, maxBitrate: Int? = null, offset: Long? = null): StreamResponse = - handleStreamResponse { - api.stream(id, maxBitrate, offset = offset).execute() - } - - /** - * TODO: Remove this in favour of handling the stream response inside RESTService - * Convenient method to get user avatar using [username]. - * - * It detects the response `Content-Type` and tries to parse subsonic error if there is one. - * - * Prefer this method over [SubsonicAPIDefinition.getAvatar] as this handles error cases. - */ - fun getAvatar(username: String): StreamResponse = handleStreamResponse { - api.getAvatar(username).execute() - } - - // TODO: Move this to response checker - private inline fun handleStreamResponse(apiCall: () -> Response): StreamResponse { - val response = apiCall() - return if (response.isSuccessful) { - val responseBody = response.body() - val contentType = responseBody?.contentType() - if ( - contentType != null && - contentType.type().equals("application", true) && - contentType.subtype().equals("json", true) - ) { - val error = jacksonMapper.readValue(responseBody.byteStream()) - StreamResponse(apiError = error.error, responseHttpCode = response.code()) - } else { - StreamResponse( - stream = responseBody?.byteStream(), - responseHttpCode = response.code() - ) - } - } else { - StreamResponse(responseHttpCode = response.code()) - } - } - - /** - * Get stream url. - * - * Calling this method do actual connection to the backend, though not downloading all content. - * - * Consider do not use this method, but [stream] call. - */ - fun getStreamUrl(id: String): String { - val request = api.stream(id).execute() - val url = request.raw().request().url().toString() - if (request.isSuccessful) { - request.body()?.close() - } - return url - } - private fun OkHttpClient.Builder.addLogging() { val loggingInterceptor = HttpLoggingInterceptor(okLogger) loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY @@ -202,4 +128,19 @@ class SubsonicAPIClient( hostnameVerifier { _, _ -> true } } + + /** + * This function is necessary because Mockito has problems with stubbing chained calls + */ + fun toStreamResponse(call: Response): 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()) + } } diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicClientConfiguration.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicClientConfiguration.kt index 732efe7a..ec0655ec 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicClientConfiguration.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicClientConfiguration.kt @@ -11,5 +11,6 @@ data class SubsonicClientConfiguration( val clientID: String, val allowSelfSignedCertificate: Boolean = false, val enableLdapUserSupport: Boolean = false, - val debug: Boolean = false + val debug: Boolean = false, + val isRealProtocolVersion: Boolean = false ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/SubsonicRESTException.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicRESTException.kt similarity index 67% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/SubsonicRESTException.kt rename to core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicRESTException.kt index 4d315d0a..0ce8861c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/SubsonicRESTException.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicRESTException.kt @@ -1,6 +1,4 @@ -package org.moire.ultrasonic.service - -import org.moire.ultrasonic.api.subsonic.SubsonicError +package org.moire.ultrasonic.api.subsonic /** * Exception returned by API with given `code`. diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/VersionAwareJacksonConverterFactory.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/VersionAwareJacksonConverterFactory.kt index 66ba8d8b..7863d7a1 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/VersionAwareJacksonConverterFactory.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/VersionAwareJacksonConverterFactory.kt @@ -63,7 +63,6 @@ class VersionAwareJacksonConverterFactory( } } - @Suppress("SwallowedException") class VersionAwareResponseBodyConverter ( private val notifier: (SubsonicAPIVersions) -> Unit = {}, private val adapter: ObjectReader @@ -77,7 +76,7 @@ class VersionAwareJacksonConverterFactory( if (response is SubsonicResponse) { try { notifier(response.version) - } catch (e: IllegalArgumentException) { + } catch (ignored: IllegalArgumentException) { // no-op } } diff --git a/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapperTest.kt b/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapperTest.kt index fbf8deb7..bbb448bf 100644 --- a/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapperTest.kt +++ b/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapperTest.kt @@ -14,7 +14,7 @@ import org.moire.ultrasonic.api.subsonic.models.AlbumListType.BY_GENRE */ class ApiVersionCheckWrapperTest { private val apiMock = mock() - private val wrapper = ApiVersionCheckWrapper(apiMock, V1_1_0) + private val wrapper = ApiVersionCheckWrapper(apiMock, V1_1_0, isRealProtocolVersion = true) @Test fun `Should just call real api for ping`() { diff --git a/detekt-baseline.xml b/detekt-baseline.xml index e1cb4ed7..7235a790 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -5,14 +5,11 @@ ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt" ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED ) - ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo && Util.getVideoPlayerType() !== VideoPlayerType.FLASH ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons() ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) - EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ } - EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$() EmptyFunctionBlock:SongView.kt$SongView${} FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent() ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song) @@ -29,7 +26,6 @@ ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber) ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate) ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix) - LargeClass:RESTMusicService.kt$RESTMusicService : MusicService LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) @@ -59,7 +55,6 @@ MagicNumber:MediaPlayerService.kt$MediaPlayerService$3 MagicNumber:MediaPlayerService.kt$MediaPlayerService$4 MagicNumber:RESTMusicService.kt$RESTMusicService$206 - MagicNumber:RESTMusicService.kt$RESTMusicService$5 MagicNumber:SongView.kt$SongView$3 MagicNumber:SongView.kt$SongView$4 MagicNumber:SongView.kt$SongView$60 @@ -68,14 +63,10 @@ NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean ) NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String - ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() } - ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>) TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable - TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$e: Throwable TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index 21a3f032..a0874cbb 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -19,7 +19,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -172,21 +172,10 @@ errorLine2=" ~~~~"> - - - - - - - - @@ -238,7 +216,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -249,7 +227,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -260,7 +238,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -271,7 +249,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -282,7 +260,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -293,7 +271,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -315,7 +293,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -326,18 +304,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - @@ -348,7 +315,7 @@ errorLine2=" ^"> @@ -359,7 +326,7 @@ errorLine2=" ^"> @@ -370,7 +337,7 @@ errorLine2=" ^"> @@ -381,7 +348,7 @@ errorLine2=" ^"> @@ -392,7 +359,7 @@ errorLine2=" ^"> @@ -403,7 +370,7 @@ errorLine2=" ^"> @@ -414,7 +381,7 @@ errorLine2=" ^"> @@ -738,7 +705,7 @@ column="13"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1532,7 +1208,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1543,7 +1219,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1554,55 +1230,55 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - + @@ -1613,51 +1289,51 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - + + + + @@ -1668,55 +1344,55 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - + + + @@ -1727,51 +1403,51 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1782,7 +1458,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1793,43 +1469,43 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1873,7 +1549,7 @@ errorLine2=" ^"> @@ -1884,7 +1560,7 @@ errorLine2=" ^"> @@ -1895,7 +1571,7 @@ errorLine2=" ^"> @@ -1906,7 +1582,7 @@ errorLine2=" ^"> @@ -1917,18 +1593,7 @@ errorLine2=" ^"> - - - - @@ -1939,18 +1604,7 @@ errorLine2=" ^"> - - - - @@ -1961,7 +1615,29 @@ errorLine2=" ^"> + + + + + + + + @@ -1972,7 +1648,7 @@ errorLine2=" ^"> @@ -1994,7 +1670,7 @@ errorLine2=" ^"> @@ -2113,7 +1789,7 @@ errorLine2=" ^"> @@ -2124,7 +1800,7 @@ errorLine2=" ~~~~~~~"> @@ -2135,7 +1811,7 @@ errorLine2=" ^"> @@ -2146,7 +1822,7 @@ errorLine2=" ~~~~~~~"> @@ -2157,7 +1833,7 @@ errorLine2=" ~~~~~~~"> diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/Test/service/GetPodcastEpisodesTestReaderProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/Test/service/GetPodcastEpisodesTestReaderProvider.java deleted file mode 100644 index 538ad595..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/Test/service/GetPodcastEpisodesTestReaderProvider.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.moire.ultrasonic.Test.service; - -import java.io.Reader; -import java.io.StringReader; - -/** - * Created by rcocula on 11/03/2016. - */ -public class GetPodcastEpisodesTestReaderProvider { - - private static String data = "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n"; - - - public static Reader getReader() { - - return new StringReader(data); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/Test/service/GetPodcastTestReaderProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/Test/service/GetPodcastTestReaderProvider.java deleted file mode 100644 index 91d3f62d..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/Test/service/GetPodcastTestReaderProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.moire.ultrasonic.Test.service; - -import java.io.Reader; -import java.io.StringReader; - -/** - * Created by rcocula on 11/03/2016. - */ -public class GetPodcastTestReaderProvider { - - private static String data = "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n"; - - - public static Reader getReader() { - - return new StringReader(data); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index 66da521d..5a6cfdd8 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -55,7 +55,6 @@ public class SettingsFragment extends PreferenceFragmentCompat private Preference addServerPreference; private ListPreference theme; - private ListPreference videoPlayer; private ListPreference maxBitrateWifi; private ListPreference maxBitrateMobile; private ListPreference cacheSize; @@ -110,7 +109,6 @@ public class SettingsFragment extends PreferenceFragmentCompat addServerPreference = findPreference(Constants.PREFERENCES_KEY_SERVERS_EDIT); theme = findPreference(Constants.PREFERENCES_KEY_THEME); - videoPlayer = findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER); maxBitrateWifi = findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI); maxBitrateMobile = findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE); cacheSize = findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE); @@ -411,7 +409,6 @@ public class SettingsFragment extends PreferenceFragmentCompat private void update() { theme.setSummary(theme.getEntry()); - videoPlayer.setSummary(videoPlayer.getEntry()); maxBitrateWifi.setSummary(maxBitrateWifi.getEntry()); maxBitrateMobile.setSummary(maxBitrateMobile.getEntry()); cacheSize.setSummary(cacheSize.getEntry()); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java index f7c24cd8..3c14ad65 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java @@ -30,6 +30,7 @@ import android.widget.Toast; import org.jetbrains.annotations.NotNull; import org.moire.ultrasonic.R; import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; +import org.moire.ultrasonic.api.subsonic.SubsonicRESTException; import org.moire.ultrasonic.app.UApp; import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.JukeboxStatus; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java index 14806a76..880564d8 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java @@ -117,7 +117,6 @@ public final class Constants public static final String PREFERENCES_KEY_CLEAR_PLAYLIST = "clearPlaylist"; public static final String PREFERENCES_KEY_CLEAR_BOOKMARK = "clearBookmark"; public static final String PREFERENCES_KEY_DISC_SORT = "discAndTrackSort"; - public static final String PREFERENCES_KEY_VIDEO_PLAYER = "videoPlayer"; public static final String PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS = "sendBluetoothNotifications"; public static final String PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART = "sendBluetoothAlbumArt"; public static final String PREFERENCES_KEY_VIEW_REFRESH = "viewRefresh"; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java index 82366ed6..e93d7cc4 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -21,8 +21,9 @@ package org.moire.ultrasonic.util; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; -import android.content.*; -import android.content.pm.ApplicationInfo; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; @@ -39,7 +40,6 @@ import android.os.Build; import android.os.Environment; import android.os.Parcelable; import android.util.DisplayMetrics; -import timber.log.Timber; import android.util.TypedValue; import android.view.Gravity; import android.view.View; @@ -51,19 +51,32 @@ import androidx.preference.PreferenceManager; import org.moire.ultrasonic.R; import org.moire.ultrasonic.app.UApp; import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.*; +import org.moire.ultrasonic.domain.Bookmark; +import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; +import org.moire.ultrasonic.domain.PlayerState; +import org.moire.ultrasonic.domain.RepeatMode; +import org.moire.ultrasonic.domain.SearchResult; import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.MediaPlayerService; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.text.DecimalFormat; -import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; +import timber.log.Timber; + /** * @author Sindre Mehus * @version $Id$ @@ -1148,36 +1161,6 @@ public class Util else return minutes > 0 ? String.format(Locale.getDefault(), "%d:%02d", minutes, seconds) : String.format(Locale.getDefault(), "0:%02d", seconds); } - public static VideoPlayerType getVideoPlayerType() - { - SharedPreferences preferences = getPreferences(); - return VideoPlayerType.forKey(preferences.getString(Constants.PREFERENCES_KEY_VIDEO_PLAYER, VideoPlayerType.MX.getKey())); - } - - public static boolean isPackageInstalled(Context context, String packageName) - { - PackageManager pm = context.getPackageManager(); - List packages = null; - - if (pm != null) - { - packages = pm.getInstalledApplications(0); - } - - if (packages != null) - { - for (ApplicationInfo packageInfo : packages) - { - if (packageInfo.packageName.equals(packageName)) - { - return true; - } - } - } - - return false; - } - public static String getVersionName(Context context) { String versionName = null; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/VideoPlayerType.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/VideoPlayerType.java deleted file mode 100644 index ff005e73..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/VideoPlayerType.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2013 (C) Sindre Mehus - */ -package org.moire.ultrasonic.util; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.service.MusicServiceFactory; - -/** - * @author Sindre Mehus - * @version $Id: VideoPlayerType.java 3473 2013-05-23 16:42:49Z sindre_mehus $ - */ -public enum VideoPlayerType -{ - - MX("mx") - { - @Override - public void playVideo(final Context context, MusicDirectory.Entry entry) throws Exception - { - - // Check if MX Player is installed. - boolean installedAd = Util.isPackageInstalled(context, PACKAGE_NAME_MX_AD); - boolean installedPro = Util.isPackageInstalled(context, PACKAGE_NAME_MX_PRO); - - if (!installedAd && !installedPro) - { - new AlertDialog.Builder(context).setMessage(R.string.video_get_mx_player_text).setPositiveButton(R.string.video_get_mx_player_button, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int i) - { - try - { - context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("market://details?id=%s", PACKAGE_NAME_MX_AD)))); - } - catch (android.content.ActivityNotFoundException x) - { - context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("http://play.google.com/store/apps/details?id=%s", PACKAGE_NAME_MX_AD)))); - } - - dialog.dismiss(); - } - }).setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int i) - { - dialog.dismiss(); - } - }).show(); - - } - else - { - // See documentation on https://sites.google.com/site/mxvpen/api - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setPackage(installedPro ? PACKAGE_NAME_MX_PRO : PACKAGE_NAME_MX_AD); - intent.putExtra("title", entry.getTitle()); - intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService().getVideoUrl(entry.getId(), false)), "video/*"); - context.startActivity(intent); - } - } - }, - - FLASH("flash") - { - @Override - public void playVideo(Context context, MusicDirectory.Entry entry) throws Exception - { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(MusicServiceFactory.getMusicService().getVideoUrl(entry.getId(), true))); - context.startActivity(intent); - } - }, - - DEFAULT("default") - { - @Override - public void playVideo(Context context, MusicDirectory.Entry entry) throws Exception - { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService().getVideoUrl(entry.getId(), false)), "video/*"); - context.startActivity(intent); - } - }; - - private final String key; - - VideoPlayerType(String key) - { - this.key = key; - } - - public String getKey() - { - return key; - } - - public static VideoPlayerType forKey(String key) - { - for (VideoPlayerType type : VideoPlayerType.values()) - { - if (type.key.equals(key)) - { - return type; - } - } - return null; - } - - public abstract void playVideo(Context context, MusicDirectory.Entry entry) throws Exception; - - private static final String PACKAGE_NAME_MX_AD = "com.mxtech.videoplayer.ad"; - private static final String PACKAGE_NAME_MX_PRO = "com.mxtech.videoplayer.pro"; - -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 9f029926..c770efe4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -29,7 +29,6 @@ import androidx.preference.PreferenceManager import com.google.android.material.navigation.NavigationView import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.java.KoinJavaComponent.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline @@ -151,7 +150,12 @@ class NavigationActivity : AppCompatActivity() { showWelcomeScreen = showWelcomeScreen and !areServersMigrated loadSettings() - showInfoDialog(showWelcomeScreen) + + // This is a first run with only the demo entry inside the database + // We set the active server to the demo one and show the welcome dialog + if (showWelcomeScreen) { + showWelcomeDialog() + } nowPlayingEventListener = object : NowPlayingEventListener { override fun onDismissNowPlaying() { @@ -313,19 +317,27 @@ class NavigationActivity : AppCompatActivity() { finish() } - private fun showInfoDialog(show: Boolean) { + private fun showWelcomeDialog() { if (!infoDialogDisplayed) { infoDialogDisplayed = true - if (show) { - AlertDialog.Builder(this) - .setIcon(android.R.drawable.ic_dialog_info) - .setTitle(R.string.main_welcome_title) - .setMessage(R.string.main_welcome_text) - .setPositiveButton(R.string.common_ok) { dialog, _ -> - dialog.dismiss() - findNavController(R.id.nav_host_fragment).navigate(R.id.settingsFragment) - }.show() - } + + AlertDialog.Builder(this) + .setIcon(android.R.drawable.ic_dialog_info) + .setTitle(R.string.main_welcome_title) + .setMessage(R.string.main_welcome_text_demo) + .setNegativeButton(R.string.main_welcome_cancel) { dialog, _ -> + // Go to the settings screen + dialog.dismiss() + findNavController(R.id.nav_host_fragment).navigate(R.id.settingsFragment) + } + .setPositiveButton(R.string.common_ok) { dialog, _ -> + // Add the demo server + val activeServerProvider: ActiveServerProvider by inject() + val demoIndex = serverSettingsModel.addDemoServer() + activeServerProvider.setActiveServerByIndex(demoIndex) + findNavController(R.id.nav_host_fragment).navigate(R.id.mainFragment) + dialog.dismiss() + }.show() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index a8e9b819..d9c11c86 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -37,12 +37,15 @@ class ActiveServerProvider( cachedServer = repository.findById(serverId) } Timber.d( - "getActiveServer retrieved from DataBase, id: $serverId; " + - "cachedServer: $cachedServer" + "getActiveServer retrieved from DataBase, id: %s cachedServer: %s", + serverId, cachedServer ) } - if (cachedServer != null) return cachedServer!! + if (cachedServer != null) { + return cachedServer!! + } + setActiveServerId(0) } @@ -105,7 +108,7 @@ class ActiveServerProvider( * @param method: The Rest resource to use * @return The Rest Url of the method on the server */ - fun getRestUrl(method: String?): String? { + fun getRestUrl(method: String?): String { val builder = StringBuilder(8192) val activeServer = getActiveServer() val serverUrl: String = activeServer.url diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt index 1b430ae1..0758ade8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt @@ -12,6 +12,7 @@ import org.moire.ultrasonic.fragment.ServerSettingsModel import org.moire.ultrasonic.util.Util const val SP_NAME = "Default_SP" +const val DB_FILENAME = "ultrasonic-database" /** * This Koin module contains registration of classes related to permanent storage @@ -23,11 +24,10 @@ val appPermanentStorage = module { Room.databaseBuilder( androidContext(), AppDatabase::class.java, - "ultrasonic-database" + DB_FILENAME ) .addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_2_3) - .fallbackToDestructiveMigrationOnDowngrade() .build() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index c2c0b6d2..c6d18ee5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -14,7 +14,6 @@ import org.moire.ultrasonic.cache.PermanentFileStorage import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.log.TimberOkHttpLogger -import org.moire.ultrasonic.service.ApiCallResponseChecker import org.moire.ultrasonic.service.CachedMusicService import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.OfflineMusicService @@ -50,28 +49,29 @@ val musicServiceModule = module { } single { + val server = get().getActiveServer() + return@single SubsonicClientConfiguration( - baseUrl = get().getActiveServer().url, - username = get().getActiveServer().userName, - password = get().getActiveServer().password, + baseUrl = server.url, + username = server.userName, + password = server.password, minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion( - get().getActiveServer().minimumApiVersion + server.minimumApiVersion ?: Constants.REST_PROTOCOL_VERSION ), clientID = Constants.REST_CLIENT_ID, - allowSelfSignedCertificate = get() - .getActiveServer().allowSelfSignedCertificate, - enableLdapUserSupport = get().getActiveServer().ldapSupport, - debug = BuildConfig.DEBUG + allowSelfSignedCertificate = server.allowSelfSignedCertificate, + enableLdapUserSupport = server.ldapSupport, + debug = BuildConfig.DEBUG, + isRealProtocolVersion = server.minimumApiVersion != null ) } single { TimberOkHttpLogger() } single { SubsonicAPIClient(get(), get()) } - single { ApiCallResponseChecker(get(), get()) } single(named(ONLINE_MUSIC_SERVICE)) { - CachedMusicService(RESTMusicService(get(), get(), get(), get())) + CachedMusicService(RESTMusicService(get(), get(), get())) } single(named(OFFLINE_MUSIC_SERVICE)) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index 39dc892d..077a7587 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -22,12 +22,13 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration +import org.moire.ultrasonic.api.subsonic.SubsonicRESTException +import org.moire.ultrasonic.api.subsonic.falseOnFailure import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse +import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting -import org.moire.ultrasonic.service.ApiCallResponseChecker import org.moire.ultrasonic.service.MusicServiceFactory -import org.moire.ultrasonic.service.SubsonicRESTException import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.ModalBackgroundTask @@ -360,7 +361,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { // Execute a ping to check the authentication, now using the correct API version. pingResponse = subsonicApiClient.api.ping().execute() - ApiCallResponseChecker.checkResponseSuccessful(pingResponse) + pingResponse.throwOnFailure() currentServerSetting!!.chatSupport = isServerFunctionAvailable { subsonicApiClient.api.getChatMessages().execute() @@ -387,7 +388,8 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { updateProgress(getProgress()) val licenseResponse = subsonicApiClient.api.getLicense().execute() - ApiCallResponseChecker.checkResponseSuccessful(licenseResponse) + licenseResponse.throwOnFailure() + if (!licenseResponse.body()!!.license.valid) { return getProgress() + "\n" + resources.getString(R.string.settings_testing_unlicensed) @@ -438,9 +440,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { private fun isServerFunctionAvailable(function: () -> Response): Boolean { return try { - val response = function() - ApiCallResponseChecker.checkResponseSuccessful(response) - true + function().falseOnFailure() } catch (_: IOException) { false } catch (_: SubsonicRESTException) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt index bf377eed..468802a2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt @@ -10,8 +10,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import java.net.ConnectException -import java.net.UnknownHostException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -89,10 +87,8 @@ open class GenericListModel(application: Application) : try { load(isOffline, useId3Tags, musicService, refresh, bundle) - } catch (exception: ConnectException) { - handleException(exception, swipe.context) - } catch (exception: UnknownHostException) { - handleException(exception, swipe.context) + } catch (all: Exception) { + handleException(all, swipe.context) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt index af33133c..ad8499bf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt @@ -11,6 +11,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.data.ServerSettingDao @@ -25,20 +27,6 @@ class ServerSettingsModel( application: Application ) : AndroidViewModel(application) { - companion object { - private const val PREFERENCES_KEY_SERVER_MIGRATED = "serverMigrated" - // These constants were removed from Constants.java as they are deprecated and only used here - private const val PREFERENCES_KEY_JUKEBOX_BY_DEFAULT = "jukeboxEnabled" - private const val PREFERENCES_KEY_SERVER_NAME = "serverName" - private const val PREFERENCES_KEY_SERVER_URL = "serverUrl" - private const val PREFERENCES_KEY_ACTIVE_SERVERS = "activeServers" - private const val PREFERENCES_KEY_USERNAME = "username" - private const val PREFERENCES_KEY_PASSWORD = "password" - private const val PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate" - private const val PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport" - private const val PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId" - } - private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) /** @@ -67,8 +55,8 @@ class ServerSettingsModel( repository.insert(newServerSetting) index++ Timber.i( - "Imported server from Preferences to Database:" + - " ${newServerSetting.name}" + "Imported server from Preferences to Database: %s", + newServerSetting.name ) } } @@ -187,6 +175,23 @@ class ServerSettingsModel( } } + /** + * Inserts a new Setting into the database + * @return The id of the demo server + */ + fun addDemoServer(): Int { + val demo = DEMO_SERVER_CONFIG.copy() + + runBlocking { + demo.index = (repository.count() ?: 0) + 1 + demo.id = (repository.getMaxId() ?: 0) + 1 + repository.insert(demo) + Timber.d("Added demo server") + } + + return demo.id + } + /** * Reads up a Server Setting stored in the obsolete Preferences */ @@ -262,4 +267,36 @@ class ServerSettingsModel( editor.putBoolean(PREFERENCES_KEY_SERVER_MIGRATED + preferenceId, true) editor.apply() } + + companion object { + private const val PREFERENCES_KEY_SERVER_MIGRATED = "serverMigrated" + // These constants were removed from Constants.java as they are deprecated and only used here + private const val PREFERENCES_KEY_JUKEBOX_BY_DEFAULT = "jukeboxEnabled" + private const val PREFERENCES_KEY_SERVER_NAME = "serverName" + private const val PREFERENCES_KEY_SERVER_URL = "serverUrl" + private const val PREFERENCES_KEY_ACTIVE_SERVERS = "activeServers" + private const val PREFERENCES_KEY_USERNAME = "username" + private const val PREFERENCES_KEY_PASSWORD = "password" + private const val PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate" + private const val PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport" + private const val PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId" + + private val DEMO_SERVER_CONFIG = ServerSetting( + id = 0, + index = 0, + name = UApp.applicationContext().getString(R.string.server_menu_demo), + url = "https://demo.ampache.dev", + userName = "ultrasonic_demo", + password = "W7DumQ3ZUR89Se3", + jukeboxByDefault = false, + allowSelfSignedCertificate = false, + ldapSupport = false, + musicFolderId = null, + minimumApiVersion = "1.13.0", + chatSupport = true, + bookmarkSupport = true, + shareSupport = true, + podcastSupport = true + ) + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt index e009f58c..b63549fb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt @@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient * Loads avatars from subsonic api. */ class AvatarRequestHandler( - private val apiClient: SubsonicAPIClient + private val client: SubsonicAPIClient ) : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { return with(data.uri) { @@ -23,7 +23,9 @@ class AvatarRequestHandler( val username = request.uri.getQueryParameter(QUERY_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) { throw IOException("${response.apiError}") } else { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt index a6aeb048..3cdc2039 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt @@ -7,13 +7,14 @@ import com.squareup.picasso.RequestHandler import java.io.IOException import okio.Okio import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.toStreamResponse import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL /** * Loads cover arts from subsonic api. */ -class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : RequestHandler() { +class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { return with(data.uri) { scheme == SCHEME && @@ -38,7 +39,10 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request } // 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) { return Result(Okio.source(response.stream!!), NETWORK) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 52f7e5ef..9d40ad28 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -13,8 +13,9 @@ import java.io.OutputStream import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.throwOnFailure +import org.moire.ultrasonic.api.subsonic.toStreamResponse import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.service.RESTMusicService import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -24,9 +25,12 @@ import timber.log.Timber */ class ImageLoader( context: Context, - private val apiClient: SubsonicAPIClient, + apiClient: SubsonicAPIClient, private val config: ImageLoaderConfig ) { + // Shortcut + @Suppress("VariableNaming", "PropertyName") + val API = apiClient.api private val picasso = Picasso.Builder(context) .addRequestHandler(CoverArtRequestHandler(apiClient)) @@ -143,8 +147,8 @@ class ImageLoader( // Query the API Timber.d("Loading cover art for: %s", entry) - val response = apiClient.getCoverArt(id!!, size.toLong()) - RESTMusicService.checkStreamResponseError(response) + val response = API.getCoverArt(id!!, size.toLong()).execute().toStreamResponse() + response.throwOnFailure() // Check for failure if (response.stream == null) return diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt deleted file mode 100644 index 51bd4b0a..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.moire.ultrasonic.service - -import java.io.IOException -import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient -import org.moire.ultrasonic.api.subsonic.SubsonicAPIDefinition -import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse -import org.moire.ultrasonic.data.ActiveServerProvider -import retrofit2.Response -import timber.log.Timber - -/** - * This call wraps Subsonic API calls so their results can be checked for errors, API version, etc - */ -class ApiCallResponseChecker( - private val subsonicAPIClient: SubsonicAPIClient, - private val activeServerProvider: ActiveServerProvider -) { - /** - * Executes a Subsonic API call with response check - */ - @Throws(SubsonicRESTException::class, IOException::class) - fun > callWithResponseCheck( - call: (SubsonicAPIDefinition) -> T - ): T { - // Check for API version when first contacting the server - if (activeServerProvider.getActiveServer().minimumApiVersion == null) { - try { - val response = subsonicAPIClient.api.ping().execute() - if (response.body() != null) { - val restApiVersion = response.body()!!.version.restApiVersion - Timber.i("Server minimum API version set to %s", restApiVersion) - activeServerProvider.setMinimumApiVersion(restApiVersion) - } - } catch (ignored: Exception) { - // This Ping is only used to get the API Version, if it fails, that's no problem. - } - } - - // This call will be now executed with the correct API Version, so it shouldn't fail - val result = call.invoke(subsonicAPIClient.api) - checkResponseSuccessful(result) - return result - } - - /** - * Creates Exceptions from the results returned by the Subsonic API - */ - companion object { - @Throws(SubsonicRESTException::class, IOException::class) - fun checkResponseSuccessful(response: Response) { - if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) { - return - } - if (!response.isSuccessful) { - throw IOException("Server error, code: " + response.code()) - } else if ( - response.body()!!.status === SubsonicResponse.Status.ERROR && - response.body()!!.error != null - ) { - throw SubsonicRESTException(response.body()!!.error!!) - } else { - throw IOException("Failed to perform request: " + response.code()) - } - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index 30e5ff10..6e329d39 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -264,8 +264,8 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, } @Throws(Exception::class) - override fun getVideoUrl(id: String, useFlash: Boolean): String? { - return musicService.getVideoUrl(id, useFlash) + override fun getVideoUrl(id: String): String? { + return musicService.getVideoUrl(id) } @Throws(Exception::class) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt index 588ac266..1b876fb5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt @@ -28,6 +28,7 @@ import java.security.cert.CertificateException import javax.net.ssl.SSLException import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException +import org.moire.ultrasonic.api.subsonic.SubsonicRESTException import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage import org.moire.ultrasonic.util.Util import timber.log.Timber diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index b9e5f5f3..cce41209 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -123,7 +123,7 @@ interface MusicService { // TODO: Refactor and remove this call (see RestMusicService implementation) @Throws(Exception::class) - fun getVideoUrl(id: String, useFlash: Boolean): String? + fun getVideoUrl(id: String): String? @Throws(Exception::class) fun updateJukeboxPlaylist(ids: List?): JukeboxStatus diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index a4ad2ca9..f8519561 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -396,7 +396,7 @@ class OfflineMusicService : MusicService, KoinComponent { } @Throws(OfflineException::class) - override fun getVideoUrl(id: String, useFlash: Boolean): String? { + override fun getVideoUrl(id: String): String? { throw OfflineException("getVideoUrl isn't available in offline mode") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index d3e3e708..4876bd9e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -11,13 +11,15 @@ import java.io.File import java.io.FileWriter import java.io.IOException import java.io.InputStream -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.ResponseBody import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient 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 +52,23 @@ import timber.log.Timber */ @Suppress("LargeClass") open class RESTMusicService( - private val subsonicAPIClient: SubsonicAPIClient, + val 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().throwOnFailure() return response.body()!!.license.valid } @@ -78,9 +83,7 @@ open class RESTMusicService( if (cachedMusicFolders != null && !refresh) return cachedMusicFolders - val response = responseChecker.callWithResponseCheck { api -> - api.getMusicFolders().execute() - } + val response = API.getMusicFolders().execute().throwOnFailure() val musicFolders = response.body()!!.musicFolders.toDomainEntityList() fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer()) @@ -98,9 +101,7 @@ 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().throwOnFailure() val indexes = response.body()!!.indexes.toDomainEntity() fileStorage.store(indexName, indexes, getIndexesSerializer()) @@ -114,9 +115,7 @@ 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().throwOnFailure() val indexes = response.body()!!.indexes.toDomainEntity() fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer()) @@ -129,7 +128,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 +137,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 +145,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 +154,7 @@ open class RESTMusicService( name: String?, refresh: Boolean ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> - api.getMusicDirectory(id).execute() - } + val response = API.getMusicDirectory(id).execute().throwOnFailure() return response.body()!!.musicDirectory.toDomainEntity() } @@ -168,7 +165,7 @@ open class RESTMusicService( name: String?, refresh: Boolean ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> api.getArtist(id).execute() } + val response = API.getArtist(id).execute().throwOnFailure() return response.body()!!.artist.toMusicDirectoryDomainEntity() } @@ -179,7 +176,7 @@ open class RESTMusicService( name: String?, refresh: Boolean ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> api.getAlbum(id).execute() } + val response = API.getAlbum(id).execute().throwOnFailure() return response.body()!!.album.toMusicDirectoryDomainEntity() } @@ -207,10 +204,9 @@ 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) - .execute() - } + val response = + API.search(null, null, null, criteria.query, criteria.songCount, null, null) + .execute().throwOnFailure() return response.body()!!.searchResult.toDomainEntity() } @@ -223,12 +219,10 @@ 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().throwOnFailure() return response.body()!!.searchResult.toDomainEntity() } @@ -238,12 +232,10 @@ 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().throwOnFailure() return response.body()!!.searchResult.toDomainEntity() } @@ -253,9 +245,7 @@ open class RESTMusicService( id: String, name: String ): MusicDirectory { - val response = responseChecker.callWithResponseCheck { api -> - api.getPlaylist(id).execute() - } + val response = API.getPlaylist(id).execute().throwOnFailure() val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity() savePlaylist(name, playlist) @@ -300,9 +290,7 @@ open class RESTMusicService( override fun getPlaylists( refresh: Boolean ): List { - val response = responseChecker.callWithResponseCheck { api -> - api.getPlaylists(null).execute() - } + val response = API.getPlaylists(null).execute().throwOnFailure() return response.body()!!.playlists.toDomainEntitiesList() } @@ -318,16 +306,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 +324,15 @@ open class RESTMusicService( comment: String?, pub: Boolean ) { - responseChecker.callWithResponseCheck { api -> - api.updatePlaylist(id, name, comment, pub, null, null) - .execute() - } + API.updatePlaylist(id, name, comment, pub, null, null) + .execute().throwOnFailure() } @Throws(Exception::class) override fun getPodcastsChannels( refresh: Boolean ): List { - val response = responseChecker.callWithResponseCheck { api -> - api.getPodcasts(false, null).execute() - } + val response = API.getPodcasts(false, null).execute().throwOnFailure() return response.body()!!.podcastChannels.toDomainEntitiesList() } @@ -358,9 +341,7 @@ 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().throwOnFailure() val podcastEntries = response.body()!!.podcastChannels[0].episodeList val musicDirectory = MusicDirectory() @@ -384,9 +365,7 @@ 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().throwOnFailure() return response.body()!!.lyrics.toDomainEntity() } @@ -396,9 +375,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 +385,15 @@ 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().throwOnFailure() val childList = response.body()!!.albumList.toDomainEntityList() val result = MusicDirectory() @@ -427,17 +409,15 @@ 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().throwOnFailure() val result = MusicDirectory() result.addAll(response.body()!!.albumList.toDomainEntityList()) @@ -449,15 +429,13 @@ 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().throwOnFailure() val result = MusicDirectory() result.addAll(response.body()!!.songsList.toDomainEntityList()) @@ -467,18 +445,14 @@ 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().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().throwOnFailure() return response.body()!!.starred2.toDomainEntity() } @@ -491,8 +465,10 @@ open class RESTMusicService( ): Pair { val songOffset = if (offset < 0) 0 else offset - val response = subsonicAPIClient.stream(song.id, maxBitrate, songOffset) - checkStreamResponseError(response) + val response = API.stream(song.id, maxBitrate, offset = songOffset) + .execute().toStreamResponse() + + response.throwOnFailure() if (response.stream == null) { throw IOException("Null stream response") @@ -502,41 +478,51 @@ open class RESTMusicService( return Pair(response.stream!!, partial) } + /** + * We currently don't handle video playback in the app, but just create an Intent which video + * players can respond to. For this intent we need the full URL of the stream, including the + * authentication params. This is a bit tricky, because we want to avoid actually executing the + * call because that could take a long time. + */ @Throws(Exception::class) override fun getVideoUrl( - id: String, - useFlash: Boolean + id: String ): String { - // TODO This method should not exists as video should be loaded using stream method - // Previous method implementation uses assumption that video will be available - // by videoPlayer.view?id=&maxBitRate=500&autoplay=true, but this url is not - // official Subsonic API call. - val expectedResult = arrayOfNulls(1) - expectedResult[0] = null + // Create a new modified okhttp client to intercept the URL + val builder = subsonicAPIClient.okHttpClient.newBuilder() - val latch = CountDownLatch(1) + builder.addInterceptor { chain -> + // Returns a dummy response + Response.Builder() + .code(100) + .body(ResponseBody.create(null, "")) + .protocol(Protocol.HTTP_2) + .message("Empty response") + .request(chain.request()) + .build() + } - Thread( - { - expectedResult[0] = subsonicAPIClient.getStreamUrl(id) + "&format=raw" - latch.countDown() - }, - "Get-Video-Url" - ).start() + // Create a new Okhttp client + val client = builder.build() - latch.await(5, TimeUnit.SECONDS) + // Get the request from Retrofit, but don't execute it! + val request = API.stream(id, format = "raw").request() - return expectedResult[0]!! + // Create a new call with the request, and execute ist on our custom client + val response = client.newCall(request).execute() + + // The complete url :) + val url = response.request().url() + + return url.toString() } @Throws(Exception::class) override fun updateJukeboxPlaylist( ids: List? ): JukeboxStatus { - val response = responseChecker.callWithResponseCheck { api -> - api.jukeboxControl(JukeboxAction.SET, null, null, ids, null) - .execute() - } + val response = API.jukeboxControl(JukeboxAction.SET, null, null, ids, null) + .execute().throwOnFailure() return response.body()!!.jukebox.toDomainEntity() } @@ -546,40 +532,32 @@ 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().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().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().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().throwOnFailure() return response.body()!!.jukebox.toDomainEntity() } @@ -588,10 +566,8 @@ 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().throwOnFailure() return response.body()!!.jukebox.toDomainEntity() } @@ -600,7 +576,7 @@ open class RESTMusicService( override fun getShares( refresh: Boolean ): List { - val response = responseChecker.callWithResponseCheck { api -> api.getShares().execute() } + val response = API.getShares().execute().throwOnFailure() return response.body()!!.shares.toDomainEntitiesList() } @@ -609,7 +585,7 @@ open class RESTMusicService( override fun getGenres( refresh: Boolean ): List? { - val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() } + val response = API.getGenres().execute().throwOnFailure() return response.body()!!.genresList.toDomainEntityList() } @@ -620,9 +596,7 @@ 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().throwOnFailure() val result = MusicDirectory() result.addAll(response.body()!!.songsList.toDomainEntityList()) @@ -634,9 +608,7 @@ open class RESTMusicService( override fun getUser( username: String ): UserInfo { - val response = responseChecker.callWithResponseCheck { api -> - api.getUser(username).execute() - } + val response = API.getUser(username).execute().throwOnFailure() return response.body()!!.user.toDomainEntity() } @@ -645,9 +617,7 @@ open class RESTMusicService( override fun getChatMessages( since: Long? ): List { - val response = responseChecker.callWithResponseCheck { api -> - api.getChatMessages(since).execute() - } + val response = API.getChatMessages(since).execute().throwOnFailure() return response.body()!!.chatMessages.toDomainEntitiesList() } @@ -656,12 +626,12 @@ open class RESTMusicService( override fun addChatMessage( message: String ) { - responseChecker.callWithResponseCheck { api -> api.addChatMessage(message).execute() } + API.addChatMessage(message).execute().throwOnFailure() } @Throws(Exception::class) override fun getBookmarks(): List { - val response = responseChecker.callWithResponseCheck { api -> api.getBookmarks().execute() } + val response = API.getBookmarks().execute().throwOnFailure() return response.body()!!.bookmarkList.toDomainEntitiesList() } @@ -671,23 +641,21 @@ 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().throwOnFailure() val musicDirectory = MusicDirectory() musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList()) @@ -701,9 +669,7 @@ open class RESTMusicService( description: String?, expires: Long? ): List { - val response = responseChecker.callWithResponseCheck { api -> - api.createShare(ids, description, expires).execute() - } + val response = API.createShare(ids, description, expires).execute().throwOnFailure() return response.body()!!.shares.toDomainEntitiesList() } @@ -712,7 +678,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 +692,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 +708,5 @@ open class RESTMusicService( private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder" private const val INDEXES_STORAGE_NAME = "indexes" private const val ARTISTS_STORAGE_NAME = "artists" - - // TODO: Move to response checker - @Throws(SubsonicRESTException::class, IOException::class) - fun checkStreamResponseError(response: StreamResponse) { - if (response.hasError() || response.stream == null) { - if (response.apiError != null) { - throw SubsonicRESTException(response.apiError!!) - } else { - throw IOException( - "Failed to make endpoint request, code: " + response.responseHttpCode - ) - } - } - } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/RestErrorMapper.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/RestErrorMapper.kt index 1ab7f043..2f2e2304 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/RestErrorMapper.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/RestErrorMapper.kt @@ -12,7 +12,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicError.TokenAuthNotSupportedForL import org.moire.ultrasonic.api.subsonic.SubsonicError.TrialPeriodIsOver import org.moire.ultrasonic.api.subsonic.SubsonicError.UserNotAuthorizedForOperation import org.moire.ultrasonic.api.subsonic.SubsonicError.WrongUsernameOrPassword -import org.moire.ultrasonic.service.SubsonicRESTException +import org.moire.ultrasonic.api.subsonic.SubsonicRESTException /** * Extension for [SubsonicRESTException] that returns localized error string, that can used to @@ -21,7 +21,7 @@ import org.moire.ultrasonic.service.SubsonicRESTException fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String = when (error) { is Generic -> { - val message = error.message + val message = (error as Generic).message val errorMessage = if (message == "") { context.getString(R.string.api_subsonic_generic_no_message) } else { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt index fb03a627..152aa89a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt @@ -1,22 +1,30 @@ package org.moire.ultrasonic.subsonic import android.content.Context +import android.content.Intent +import android.net.Uri import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Util /** * This utility class helps starting video playback */ -class VideoPlayer() { +class VideoPlayer { fun playVideo(context: Context, entry: MusicDirectory.Entry?) { - if (!Util.isNetworkConnected()) { + if (!Util.isNetworkConnected() || entry == null) { Util.toast(context, R.string.select_album_no_network) return } - val player = Util.getVideoPlayerType() try { - player.playVideo(context, entry) + val intent = Intent(Intent.ACTION_VIEW) + val url = MusicServiceFactory.getMusicService().getVideoUrl(entry.id) + intent.setDataAndType( + Uri.parse(url), + "video/*" + ) + context.startActivity(intent) } catch (e: Exception) { Util.toast(context, e.toString(), false) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt index 3a4638c7..54fcf3a1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt @@ -36,7 +36,6 @@ import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.Util -import org.moire.ultrasonic.util.VideoPlayerType import org.moire.ultrasonic.view.EntryAdapter.SongViewHolder import timber.log.Timber @@ -111,8 +110,7 @@ class SongView(context: Context) : UpdateView(context), Checkable, KoinComponent val transcodedSuffix = song.transcodedSuffix fileFormat = if ( - TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || - song.isVideo && Util.getVideoPlayerType() !== VideoPlayerType.FLASH + TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo ) suffix else String.format("%s > %s", suffix, transcodedSuffix) val artistName = song.artist diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 55ba92ac..644962e5 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -101,7 +101,6 @@ Označené hvězdičkou Skladby Videa - Vítejte v aplikaci Ultrasonic! Aplikace není ještě nakonfigurována. Poté co nastavíte svůj osobní server (dostupný na subsonic.org), vyberte Správa serverů v Nastavení a připojte aplikaci. Vítejte! O aplikaci Další @@ -130,16 +129,11 @@ Kliknout pro vyhledání Skladby Hledat - Zkušební doba vypršela - Pozdějí - Získejte neomezená stahování přispěním na Subsonic. - Hned Média nenalezena %d skladeb označeno. %d skladeb odznačeno. Varování: Připojení nedostupné. Chyba: SD karta nedostupná. - Server bez licence. Zbývá %d dní zkušební doby. Přehrát vše Všechny adresáře Vybrat adresář @@ -311,8 +305,7 @@ Očekává jména hlavních adresářů obsahující jména umělců Procházet za použití ID3 tagů Používat metodu ID3 tagů místo jmen na základě adresářové struktury - Video - Videopřehrávač + Video Obnovení náhledu .5 sekundy 1 sekunda @@ -334,8 +327,6 @@ 0.00 MB -:-- 0:00 - MX Player není nainstalován. Nainstalujte z Obchodu Play nebo změňte nastavení videí. - Stáhnout MX Player Ťuknutím vybrat hudbu SD karta nedostupná Chybí SD karta @@ -371,9 +362,6 @@ Výchozí pozdrav sdílení Mrkni na hudbu sdílenou z %s Sdílet skladby přes - MX Player - Výchozí - Flash Sdílení Všechny skladby od %s Zobrazit všechny skladby umělce diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index aab075fe..6e556e71 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -100,7 +100,6 @@ Mit Stern Titel Filme - Willkommen bei Ultrasonic! Die App ist zurzeit unkonfiguriert. Nachdem du deinen Server konfiguriert hast (siehe subsonic.org), bitte Server hinzufügen in den Einstellungen klicken um die Verbindun herzustellen. Willkommen Über Allgemein @@ -129,16 +128,11 @@ Neue Suche Titel Suche - Testperiode zu Ende - Später - Unbegresnze Downloads bei Spende an Sunsonic - Jetzt Keine Medien gefunden %d Titel ausgewählt. %d Titel abgewählt. Warnung: kein Netz. Fehler: Keine SD Karte verfügbar. - Server nicht lizenziert. Noch %d Testtage Alles wiedergeben Alle Ordner Ordner wählen @@ -308,8 +302,7 @@ Annehmen, dass der Ordner der obersten Ebene der Name des Albumkünstlers ist Durchsuchen von ID3-Tags Nutze ID3 Tag Methode anstatt Dateisystem-Methode - Film - Filmwiedergabe + Film Aktualisierungsinterval .5 Sekunden 1 Sekunde @@ -331,8 +324,6 @@ 0.00 MB -:-- 0:00 - MX Player ist nicht installiert. Holen Sie ihn sich kostenlos im Play Store, oder ändern Sie die Filmeinstellungen. - MX Player holen Berühren, um Musik auszuwählen SD Karte nicht verfügbar Keine SD Karte @@ -368,9 +359,6 @@ Standard Begrüßung beim Teilen Hör dir mal die Musik an, die ich mit dir über %s geteilt habe. Titel teilen über - MX Player - Standard - Flash Freigabe Alle Titel von %s Alle Titel nach Künstler sortieren diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 35192166..4ba33dc7 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -111,7 +111,6 @@ Me gusta Canciones Vídeos - Te damos la bienvenida a Ultrasonic! La aplicación no está configurada actualmente. Después de que hayas configurado tu servidor personal (disponible desde subsonic.org), por favor haz click en Administrar servidores en Configuración para conectarte con él. ¡Saludos! Acerca de Común @@ -140,16 +139,11 @@ Haz click para buscar Canciones Buscar - El periodo de prueba ha finalizado - Mas tarde - Consigue descargas ilimitadas donando a Subsonic. - Ahora No se han encontrado medios %d pista(s) seleccionada(s). %d pista(s) deseleccionada(s). Atención: No hay red disponible. Error: No hay tarjeta SD disponible. - Servidor sin licencia. Quedan %d dia(s) de prueba. Reproducir todo Todas las carpetas Seleccionar la carpeta @@ -325,8 +319,7 @@ Usar el método de etiquetas ID3 en lugar del método basado en el sistema de ficheros Mostrar la imagen del artista en la lista de artistas Muestra la imagen del artista en la lista de artistas si está disponible - Vídeo - Reproductor de vídeo + Vídeo Refresco de la vista .5 segundos 1 segundo @@ -348,8 +341,6 @@ 0.00 MB -:-- 0:00 - El MX Player no esta instalado. Descárgalo grátis de la Play Store, o cambia la configuración de vídeo. - Obtener MX Player Toca para seleccionar música Tarjeta SD no disponible No hay tarjeta SD @@ -385,9 +376,6 @@ Saludo predeterminado para los compartidos Echa un vistazo a esta música que te comparto desde %s Compartir canciones vía - MX Player - Por defecto - Flash Compartir Todas las canciones por %s Mostrar todas las canciones por artista diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 5bc3b7b7..0353ff31 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -101,7 +101,6 @@ Favoris Titres Vidéos - Bienvenue dans Ultrasonic ! L\'application n\'est pas configurée. Après avoir configuré votre serveur personnel (disponible à partir de subsonic.org), veuillez accéder aux Paramètres et modifier la configuration pour vous y connecter. Bienvenue ! À propos Général @@ -130,16 +129,11 @@ Cliquer pour rechercher Titres Recherche - La période d\'essai est terminée - Plus tard - Obtenez des téléchargements illimités en faisant un don pour Subsonic. - Maintenant Aucun titre trouvé %d pistes sélectionnées. %d pistes non sélectionnés. Avertissement : Aucun réseau disponible. Erreur : Aucune carte SD disponible. - Serveur sans licence. %d jours d\'essai restants. Tout jouer Tous les dossiers Sélectionner le dossier @@ -313,8 +307,7 @@ Utiliser ID3 Tags à la place du système de fichier basique Afficher l’image de l’artiste dans la liste Affiche l’image de l’artiste dans la liste des artistes si celle-ci est disponible - Vidéo - Lecteur vidéo + Vidéo Actualisation de la vue 0,5 secondes 1 seconde @@ -336,8 +329,6 @@ 0.00 Mo —:—— 0:00 - MX Player n\'est pas installé. Récupérez le gratuitement sur Play Store, ou modifier les paramètres vidéo. - Obtenez MX Player Touchez pour sélectionner un titre Carte SD non disponible Aucune carte SD @@ -373,9 +364,6 @@ Texte par défaut lors d\'un partage Regardez cette musique que j\'ai partagée depuis %s Partager des titres via - MX Player - Défaut - Flash Partager Tous les titres de %s Voir tous les titres par artiste diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 5476f2c3..ebdc58e8 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -111,7 +111,6 @@ Csillaggal megjelölt Dalok Videók - Üdvözli az Ultrasonic! Az alkalmazás még nincs beállítva. Miután konfigurálta saját kiszolgálóját (elérhető: subsonic.org), húzza balról jobbra az oldalsávot, lépjen be a Beállítások menüpontba, és adja meg csatlakozási adatokat! Üdvözlet! Névjegy Általános @@ -140,16 +139,11 @@ Érintse meg a kereséshez Dalok Keresés - A próbaidőszak lejárt! - Később - Korlátlan letöltéshez juthat a Subsonic támogatásával. - Most Nem található média! %d dal kijelölve. %d dal visszavonva. Figyelem: Hálózat nem áll rendelkezésre! Hiba: SD kártya nem áll rendelkezésre! - A kiszolgálónak nincs licence! %d próba nap van hátra! Összes lejátszása Összes mappa Mappa kiválasztása @@ -325,8 +319,7 @@ ID3 Tag módszer használata a fájlredszer alapú mód helyett. Előadó képének megjelenítése Az előadó listában megjeleníti a képeket, amennyiben elérhetőek - Videó - Videólejátszó + Videó Nézet frissítési gyakorisága .5 másodperc 1 másodperc @@ -348,8 +341,6 @@ 0.00 MB -:-- 0:00 - Az MX Player nincs telepítve. Töltse le díjmentesen a Play Áruházból, vagy módosítsa a videó beállításait! - MX Player letöltése Érintse meg a zene kiválasztásához Az SD kártya nem elérhető! Nincs SD kártya! @@ -385,9 +376,6 @@ Alapértelmezett megosztási üzenet Hallgasd meg ezt a zenét, megosztottam innen: %s Dalok megosztása ezzel - MX Player - Alapértelmezett - Flash Megosztás %s minden dala Az előadó összes dalának megjelenítése diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index f7031d63..2a92a780 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -125,16 +125,11 @@ Selezione per cercare Canzoni Cerca - Periodo di prova terminato - Dopo - Ottieni download illimitato con una donazione a Subsonic. - Ora Nessun media trovato %dtracce selezionate. %d tracce non selezionate. Attenzione: nessuna rete disponibile. Errore: Nessuna memoria SD disponibile. - Nessuna licenza presente. %d giorni di prova rimanenti. Riproduci tutto Tutte le cartelle Seleziona cartella @@ -299,8 +294,7 @@ Presumi che la cartella superiore sia il nome dell\'artista dell\'album Sfoglia Utilizzando Tag ID3 Usa metodi tag ID3 invece dei metodi basati sul filesystem - Video - Riproduttore video + Video .5 secondo 1 secondo 1.5 secondi @@ -319,8 +313,6 @@ 0.00 MB -:-- 0:00 - MX Player non è installato. Scaricalo gratuitamente dal Play Store, o cambia le impostazioni video. - Ottieni MX Player Tocca per selezionare musica Scheda SD non disponibile Nessuna scheda SD @@ -336,8 +328,6 @@ Commenta \"%s\" è stato rimosso dalla playlist Condividi canzoni via - MX Player - Predefinito 1 canzone %d canzoni diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 9cd9fd00..05e97354 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -111,7 +111,6 @@ Favorieten Nummers Video\'s - Welkom bij Ultrasonic! De app is nog niet ingesteld. Nadat je je persoonlijke server hebt opgezet (beschikbaar op subsonic.org), kun je naar de Instellingen gaan en drukken op Server toevoegen. Welkom! Over Algemeen @@ -140,16 +139,11 @@ Druk om te zoeken Nummers Zoeken - Proefperiode is afgelopen - Later - Verkrijg ongelimiteerde downloads door te doneren aan Subsonic. - Nu Geen media gevonden %d nummers geselecteerd. %d nummers gedeselecteerd. Waarschuwing: geen internetverbinding. Fout: geen SD-kaart beschikbaar. - Geen serverlicentie; nog %d dagen resterend van de proefperiode. Alles afspelen Alle mappen Map kiezen @@ -325,8 +319,7 @@ ID3-labels gebruiken in plaats van systeemlabels Artiestfoto tonen op artiestenlijst Toont de artiestfoto op de artiestenlijst (indien beschikbaar) - Video - Videospeler + Video Verversen 0,5 seconden 1 seconde @@ -348,8 +341,6 @@ 0,00 MB -:-- 0:00 - MX Player is niet geïnstalleerd. Installeer deze gratis via de Play Store of wijzig de video-instellingen. - MX Player installeren Druk om muziek te selecteren SD-kaart niet beschikbaar Geen SD-kaart @@ -385,9 +376,6 @@ Standaard deelbericht Hé, luister eens naar de muziek die ik heb gedeeld via %s Nummers delen via - MX Player - Standaard - Flash Delen Alle nummers van %s Alle nummers van artiest tonen diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 4f7b15d1..78588a60 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -100,7 +100,6 @@ Ulubione Utwory Klipy wideo - Witaj w Ultrasonic! Obecnie aplikacja nie jest skonfigurowana. Jeśli masz uruchomiony własny serwer (dostępny na subsonic.org), proszę wybrać Dodaj serwer w Ustawieniach aby się z nim połączyć. Witaj! O aplikacji Wspólne @@ -128,16 +127,11 @@ Kliknij, aby wyszukać Utwory Wyszukiwanie - Okres próbny zakończył się - Później - Uzyskaj możliwość nieograniczonych pobrań przekazując darowiznę na rzecz Subsonic. - Teraz Brak mediów Zaznaczono %d utworów. Odznaczono %d utworów. Uwaga: sieć niedostępna. Błąd: Niedostępna karta SD. - Serwer bez licencji. Pozostało %d dni próbnych. Odtwórz wszystkie Wszystkie foldery Wybierz folder @@ -308,8 +302,7 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników Zakłada, że folder najwyższego poziomu jest nazwą artysty albumu Przeglądaj używając tagów ID3 Używa metod z tagów ID3 zamiast metod opartych na systemie plików - Wideo - Odtwarzacz wideo + Wideo Odświeżanie widoku co pół sekundy co 1 sekundę @@ -331,8 +324,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników 0.00 MB -:-- 0:00 - MX Player nie jest zainstalowany, Pobierz go za darmo w Sklepie Play, lub zmień ustawiena wideo. - Pobierz MX Player Dotknij, aby wybrać muzykę Karta SD jest niedostępna Brak karty SD @@ -368,9 +359,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników Domyślny tekst podczas udostępniania Sprawdź muzykę, którą udostępniam na %s Udostępnij utwory za pomocą - MX Player - Domyślny - Flash Udostępnianie Wszystkie utwory %s Wyświetlaj wszystkie utwory artysty diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 903d7b6a..8e418d39 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -101,7 +101,6 @@ Favoritas Músicas Vídeos - Bem-vindo ao Ultrasonic! O aplicativo ainda não está configurado. Após configurar seu servidor pessoal (disponível em subsonic.org), clique em Adicionar Servidor em Configurações para a conexão. Bem-vindo! Sobre Comum @@ -130,16 +129,11 @@ Clique para pesquisar Músicas Pesquisar - O período de teste acabou - Mais tarde - Obtenha downloads ilimitados fazendo uma doação ao Subsonic. - Agora Nenhuma mídia encontrada %d faixas selecionadas. %d faixas desselecionadas. Aviso: Nenhuma rede disponível. Erro: Nenhum cartão SD disponível. - Servidor não licenciado. Restam %d dias de teste. Tocar Tudo Todas as Pastas Selecionar Pasta @@ -313,8 +307,7 @@ Usar as etiquetas ID3 ao invés do sistema de arquivos Mostrar Foto do Artista na Lista Mostrar a imagem do artista na lista de artistas, se disponível - Vídeo - Player de Vídeo + Vídeo Atualização da Tela .5 segundos 1 segundo @@ -336,8 +329,6 @@ 0.00 MB -:-- 0:00 - O MX Player não está instalado. Baixe da graça pela Play Store ou modifique as configurações de vídeo. - Baixar MX Player Toque para selecionar a música Cartão SD indisponível Sem cartão SD @@ -373,9 +364,6 @@ Saudação Padrão do Compartilhamento Confira esta música que compartilhei do %s Compartilhar músicas via - MX Player - Padrão - Flash Compartilhar Todas as Músicas de %s Mostrar Todas as Músicas por Artista diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 8fb5bb12..2ff02ba4 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -100,7 +100,6 @@ Favoritas Músicas Vídeos - Bem-vindo ao Ultrasonic! O aplicativo ainda não está configurado. Após configurar seu servidor pessoal (disponível em subsonic.org), clique em Adicionar Servidor em Configurações para a conexão. Bem-vindo! Sobre Comum @@ -128,16 +127,11 @@ Clique para pesquisar Músicas Pesquisar - O período de teste acabou - Mais tarde - Obtenha downloads ilimitados fazendo uma doação ao Subsonic. - Agora Nenhuma mídia encontrada %d faixas selecionadas. %d faixas desselecionadas. Aviso: Nenhuma rede disponível. Erro: Nenhum cartão SD disponível. - Servidor não licenciado. Restam %d dias de teste. Tocar Tudo Todas as Pastas Selecionar Pasta @@ -308,8 +302,7 @@ Assumir que a pasta mais acima é o nome do artista Navegar Usando Etiquetas ID3 Usa as etiquetas ID3 ao invés do sistema de ficheiros - Vídeo - Player de Vídeo + Vídeo Atualização do Ecrã .5 segundos 1 segundo @@ -331,8 +324,6 @@ 0.00 MB —:—— 0:00 - O MX Player não está instalado. Descarregue da graça pela Play Store ou modifique as configurações de vídeo. - Descarregar MX Player Toque para selecionar a música Cartão SD indisponível Sem cartão SD @@ -368,9 +359,6 @@ Saudação Padrão Confira esta música que compartilhei do %s Compartilhar músicas via - MX Player - Padrão - Flash Compartilhar Todas as Músicas de %s Todas as Músicas do Artista diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 0897075f..b5afcad1 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -125,16 +125,11 @@ Нажми для поиска Песни Поиск - Пробный период окончен - Позже - Получите неограниченное количество загрузок, пожертвовав Subsonic - Сейчас Медиа не найдена %d треки выбраны. %d треки не выбраны. Предупреждение: сеть недоступна. Ошибка: нет SD-карты - Сервер не лицензирован. %d пробные дни остались. Воспроизвести все Все папки Выбрать папку @@ -300,8 +295,7 @@ Предположим, папка верхнего уровня - это имя исполнителя альбома Обзор с использованием тегов ID3 Используйте методы тегов ID3 ​​вместо методов на основе файловой системы - Видео - Видеоплеер + Видео Посмотреть Обновить .5 секунд 1 секунда @@ -323,8 +317,6 @@ 0.00 MB -:-- 0:00 - MX Player не установлен. Получите его бесплатно в магазине Play Store или измените настройки видео. - Получить MX Player Нажмите, чтобы выбрать музыку SD-карта недоступна Нет SD-карты @@ -360,9 +352,6 @@ Поделиться приветствием по умолчанию Проверьте эту музыку, с которой я поделился %s Поделиться треками через - MX Player - По умолчанию - Flash Поделиться Все треки %s Показать все треки исполнителя diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 60067c12..171e18c5 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -106,10 +106,6 @@ 点击搜索 歌曲 搜索 - 试用已结束 - 稍后 - 通过捐赠 Subsonic 得到无限制的下载。 - 现在 找不到歌曲 警告:网络不可用 错误:没有SD卡 @@ -224,8 +220,7 @@ 连接正常, 服务器未授权。 主题 允许自签名 HTTPS 证书 - 视频 - 视频播放器 + 视频 刷新视图 .5 秒 1 秒 @@ -261,8 +256,6 @@ 评论 %s已从播放列表中移除 分享播放列表 - MX Player - 默认 分享 已禁用 删除文件 diff --git a/ultrasonic/src/main/res/values/arrays.xml b/ultrasonic/src/main/res/values/arrays.xml index d18959e9..22b3cdff 100644 --- a/ultrasonic/src/main/res/values/arrays.xml +++ b/ultrasonic/src/main/res/values/arrays.xml @@ -224,16 +224,6 @@ @string/settings.search_250 @string/settings.search_500 - - mx - default - flash - - - @string/settings.video_mx_player - @string/settings.video_default - @string/settings.video_flash - @string/settings.view_refresh_500 @string/settings.view_refresh_1000 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 62556828..4daeda54 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - + Loading… A network error occurred. Please check the server address or try again later. @@ -111,8 +111,9 @@ Starred Songs Videos - Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from subsonic.org), please click Manage Servers in Settings to connect to it. - Welcome! + To use Ultrasonic with your own music you will need your own server. \n\n➤ In case you want to try out the app first, it can add a demo server now. \n\n➤ Otherwise you can configure your server in the settings. + Welcome to Ultrasonic! + Take me to the settings About Common Deleted playlist %s @@ -140,16 +141,11 @@ Click to search Songs Search - Trial period is over - Later - Get unlimited downloads by donating to Subsonic. - Now No media found %d tracks selected. %d tracks unselected. Warning: No network available. Error: No SD card available. - Server not licensed. %d trial days left. Play All All Folders Select Folder @@ -327,8 +323,7 @@ Use ID3 tag methods instead of file system based methods Show artist picture in artist list Displays the artist picture in the artist list if available - Video - Video player + Video View Refresh .5 seconds 1 second @@ -348,10 +343,8 @@ 0.00 GB 0 KB 0.00 MB - -:-- + -:-- 0:00 - MX Player is not installed. Get it for free on Play Store, or change video settings. - Get MX Player Touch to select music SD card unavailable No SD card @@ -387,9 +380,6 @@ Default Share Greeting Check out this music I shared from %s Share songs via - MX Player - Default - Flash Share All Songs by %s Show All Songs By Artist @@ -449,6 +439,7 @@ Authentication Advanced settings One or more features were disabled because the server doesn\'t support them.\nYou can run this test again anytime. + Demo Server 1 song diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index ce724706..46ad73b9 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -70,13 +70,13 @@ a:key="playbackControlSettings" app:iconSpaceReserved="false"> - - - diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt index 0b27cbcf..ddf283bb 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt @@ -9,18 +9,20 @@ import org.amshove.kluent.`should throw` import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Answers import org.mockito.kotlin.any 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 @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE) class AvatarRequestHandlerTest { - private val mockApiClient: SubsonicAPIClient = mock() + private val mockApiClient: SubsonicAPIClient = mock(defaultAnswer = Answers.RETURNS_DEEP_STUBS) private val handler = AvatarRequestHandler(mockApiClient) @Test @@ -59,8 +61,10 @@ class AvatarRequestHandlerTest { apiError = null, responseHttpCode = 200 ) - whenever(mockApiClient.getAvatar(any())) - .thenReturn(streamResponse) + + whenever( + mockApiClient.toStreamResponse(any()) + ).thenReturn(streamResponse) val response = handler.load( createLoadAvatarRequest("some-username").buildRequest(), 0 diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt index 17dda53e..fdef0b0c 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt @@ -10,8 +10,8 @@ import org.amshove.kluent.`should throw` import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Answers import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient @@ -20,7 +20,7 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class CoverArtRequestHandlerTest { - private val mockApiClient: SubsonicAPIClient = mock() + private val mockApiClient: SubsonicAPIClient = mock(defaultAnswer = Answers.RETURNS_DEEP_STUBS) private val handler = CoverArtRequestHandler(mockApiClient) @Test @@ -56,7 +56,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.toStreamResponse(any()) + ).thenReturn(streamResponse) val fail = { handler.load(createLoadCoverArtRequest("some").buildRequest(), 0) @@ -73,7 +75,9 @@ class CoverArtRequestHandlerTest { responseHttpCode = 200 ) - whenever(mockApiClient.getCoverArt(any(), anyOrNull())).thenReturn(streamResponse) + whenever( + mockApiClient.toStreamResponse(any()) + ).thenReturn(streamResponse) val response = handler.load( createLoadCoverArtRequest("some").buildRequest(), 0