From 336f505ba5afdd79d53247bab91649edf4f5fc82 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sun, 3 Dec 2017 14:14:01 +0100 Subject: [PATCH 1/2] Replace ServerTooOldException with more proper ApiNotSupportedException. --- .../api/subsonic/ApiNotSupportedException.kt | 11 +++++ .../ultrasonic/activity/PodcastsActivity.java | 27 ----------- .../activity/SelectPlaylistActivity.java | 7 +-- .../ultrasonic/activity/ShareActivity.java | 7 +-- .../ultrasonic/service/JukeboxService.java | 3 +- .../ultrasonic/service/RESTMusicService.java | 3 +- .../service/ServerTooOldException.java | 48 ------------------- 7 files changed, 23 insertions(+), 83 deletions(-) create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiNotSupportedException.kt delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/ServerTooOldException.java diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiNotSupportedException.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiNotSupportedException.kt new file mode 100644 index 00000000..fce439a6 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiNotSupportedException.kt @@ -0,0 +1,11 @@ +package org.moire.ultrasonic.api.subsonic + +import java.io.IOException + +/** + * Special [IOException] to indicate that called api is not yet supported + * by current server api version. + */ +class ApiNotSupportedException( + serverApiVersion: SubsonicAPIVersions) + : IOException("Server api $serverApiVersion does not support this call") diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/PodcastsActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/PodcastsActivity.java index 74f9a223..2b7e3958 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/PodcastsActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/PodcastsActivity.java @@ -19,47 +19,20 @@ package org.moire.ultrasonic.activity; -import android.app.AlertDialog; -import android.app.ListActivity; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; -import android.text.Editable; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.method.LinkMovementMethod; -import android.text.util.Linkify; -import android.view.ContextMenu; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; -import android.widget.CheckBox; -import android.widget.EditText; import android.widget.ListView; -import android.widget.TextView; - -import com.handmark.pulltorefresh.library.PullToRefreshBase; -import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener; -import com.handmark.pulltorefresh.library.PullToRefreshListView; import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.Playlist; import org.moire.ultrasonic.domain.PodcastsChannel; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.service.OfflineException; -import org.moire.ultrasonic.service.ServerTooOldException; import org.moire.ultrasonic.util.BackgroundTask; -import org.moire.ultrasonic.util.CacheCleaner; import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.LoadingTask; import org.moire.ultrasonic.util.TabActivityBackgroundTask; -import org.moire.ultrasonic.util.Util; -import org.moire.ultrasonic.view.PlaylistAdapter; import org.moire.ultrasonic.view.PodcastsChannelsAdapter; import java.util.List; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java index b0874491..5777f65e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java @@ -43,12 +43,13 @@ import android.widget.TextView; import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener; import com.handmark.pulltorefresh.library.PullToRefreshListView; + import org.moire.ultrasonic.R; +import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; import org.moire.ultrasonic.domain.Playlist; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.OfflineException; -import org.moire.ultrasonic.service.ServerTooOldException; import org.moire.ultrasonic.util.BackgroundTask; import org.moire.ultrasonic.util.CacheCleaner; import org.moire.ultrasonic.util.Constants; @@ -271,7 +272,7 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt protected void error(Throwable error) { String msg; - msg = error instanceof OfflineException || error instanceof ServerTooOldException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()), getErrorMessage(error)); + msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()), getErrorMessage(error)); Util.toast(SelectPlaylistActivity.this, msg, false); } @@ -360,7 +361,7 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt protected void error(Throwable error) { String msg; - msg = error instanceof OfflineException || error instanceof ServerTooOldException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, playlist.getName()), getErrorMessage(error)); + msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, playlist.getName()), getErrorMessage(error)); Util.toast(SelectPlaylistActivity.this, msg, false); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ShareActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ShareActivity.java index 65670e11..d7a2d237 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ShareActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/ShareActivity.java @@ -44,12 +44,13 @@ import android.widget.TextView; import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener; import com.handmark.pulltorefresh.library.PullToRefreshListView; + import org.moire.ultrasonic.R; +import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; import org.moire.ultrasonic.domain.Share; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.OfflineException; -import org.moire.ultrasonic.service.ServerTooOldException; import org.moire.ultrasonic.util.BackgroundTask; import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.LoadingTask; @@ -251,7 +252,7 @@ public class ShareActivity extends SubsonicTabActivity implements AdapterView.On protected void error(Throwable error) { String msg; - msg = error instanceof OfflineException || error instanceof ServerTooOldException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_share_error, share.getName()), getErrorMessage(error)); + msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_share_error, share.getName()), getErrorMessage(error)); Util.toast(ShareActivity.this, msg, false); } @@ -356,7 +357,7 @@ public class ShareActivity extends SubsonicTabActivity implements AdapterView.On protected void error(Throwable error) { String msg; - msg = error instanceof OfflineException || error instanceof ServerTooOldException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, share.getName()), getErrorMessage(error)); + msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, share.getName()), getErrorMessage(error)); Util.toast(ShareActivity.this, msg, false); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java index a43e0b98..849d73ac 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java @@ -28,6 +28,7 @@ import android.widget.ProgressBar; import android.widget.Toast; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; import org.moire.ultrasonic.domain.JukeboxStatus; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.service.parser.SubsonicRESTException; @@ -185,7 +186,7 @@ public class JukeboxService private void onError(JukeboxTask task, Throwable x) { - if (x instanceof ServerTooOldException && !(task instanceof Stop)) + if (x instanceof ApiNotSupportedException && !(task instanceof Stop)) { disableJukeboxOnError(x, R.string.download_jukebox_server_too_old); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java index f6fe612e..18d8df3e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -26,6 +26,7 @@ import android.support.annotation.StringRes; import android.util.Log; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; import org.moire.ultrasonic.api.subsonic.models.AlbumListType; import org.moire.ultrasonic.api.subsonic.models.JukeboxAction; @@ -339,7 +340,7 @@ public class RESTMusicService implements MusicService { Util.getShouldUseId3Tags(context) ? search3(criteria, context, progressListener) : search2(criteria, context, progressListener); - } catch (ServerTooOldException x) { + } catch (ApiNotSupportedException ignored) { // Ensure backward compatibility with REST 1.3. return searchOld(criteria, context, progressListener); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/ServerTooOldException.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/ServerTooOldException.java deleted file mode 100644 index b0c2bd0f..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/ServerTooOldException.java +++ /dev/null @@ -1,48 +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 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.service; - -/** - * Thrown if the REST API version implemented by the server is too old. - * - * @author Sindre Mehus - * @version $Id$ - */ -public class ServerTooOldException extends Exception -{ - private static final long serialVersionUID = -7955245839000220002L; - - public ServerTooOldException(String text) - { - super(createMessage(text)); - } - - private static String createMessage(String text) - { - StringBuilder builder = new StringBuilder(25); - - if (text != null) - { - builder.append(text).append(' '); - } - - builder.append("Subsonic server version is too old. Please upgrade."); - return builder.toString(); - } -} From 3fddef0ec5f91aadb115b9c8d17d93304f28b0e8 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sun, 10 Dec 2017 10:43:45 +0100 Subject: [PATCH 2/2] Check call if it supported by current server version. Add a api wrapper that checks if current protocol version is supported for this call and fail fast if not. Signed-off-by: Yahor Berdnikau --- .../api/subsonic/CommonFunctions.kt | 2 +- .../api/subsonic/ApiVersionCheckWrapper.kt | 303 ++++++++++++++++++ .../api/subsonic/SubsonicAPIClient.kt | 11 +- .../subsonic/ApiVersionCheckWrapperTest.kt | 46 +++ .../service/MusicServiceFactory.java | 2 + 5 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt create mode 100644 subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapperTest.kt diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt index 564d02a1..b44a3b03 100644 --- a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt @@ -16,7 +16,7 @@ import java.util.TimeZone const val USERNAME = "some-user" const val PASSWORD = "some-password" -val CLIENT_VERSION = SubsonicAPIVersions.V1_13_0 +val CLIENT_VERSION = SubsonicAPIVersions.V1_16_0 const val CLIENT_ID = "test-client" val dateFormat by lazy(LazyThreadSafetyMode.NONE, { diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt new file mode 100644 index 00000000..55bbfc36 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt @@ -0,0 +1,303 @@ +package org.moire.ultrasonic.api.subsonic + +import okhttp3.ResponseBody +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_11_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_12_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_14_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_2_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_3_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_4_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_5_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_6_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_7_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_8_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_9_0 +import org.moire.ultrasonic.api.subsonic.models.AlbumListType +import org.moire.ultrasonic.api.subsonic.models.JukeboxAction +import org.moire.ultrasonic.api.subsonic.response.BookmarksResponse +import org.moire.ultrasonic.api.subsonic.response.ChatMessagesResponse +import org.moire.ultrasonic.api.subsonic.response.GenresResponse +import org.moire.ultrasonic.api.subsonic.response.GetAlbumList2Response +import org.moire.ultrasonic.api.subsonic.response.GetAlbumListResponse +import org.moire.ultrasonic.api.subsonic.response.GetAlbumResponse +import org.moire.ultrasonic.api.subsonic.response.GetArtistResponse +import org.moire.ultrasonic.api.subsonic.response.GetArtistsResponse +import org.moire.ultrasonic.api.subsonic.response.GetLyricsResponse +import org.moire.ultrasonic.api.subsonic.response.GetPlaylistsResponse +import org.moire.ultrasonic.api.subsonic.response.GetPodcastsResponse +import org.moire.ultrasonic.api.subsonic.response.GetRandomSongsResponse +import org.moire.ultrasonic.api.subsonic.response.GetSongsByGenreResponse +import org.moire.ultrasonic.api.subsonic.response.GetStarredResponse +import org.moire.ultrasonic.api.subsonic.response.GetStarredTwoResponse +import org.moire.ultrasonic.api.subsonic.response.GetUserResponse +import org.moire.ultrasonic.api.subsonic.response.JukeboxResponse +import org.moire.ultrasonic.api.subsonic.response.SearchThreeResponse +import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse +import org.moire.ultrasonic.api.subsonic.response.SharesResponse +import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse +import org.moire.ultrasonic.api.subsonic.response.VideosResponse +import retrofit2.Call + +/** + * Special wrapper for [SubsonicAPIDefinition] that checks if [currentApiVersion] is suitable + * for this call. + */ +internal class ApiVersionCheckWrapper( + val api: SubsonicAPIDefinition, + var currentApiVersion: SubsonicAPIVersions) : SubsonicAPIDefinition by api { + override fun getArtists(musicFolderId: Long?): Call { + checkVersion(V1_8_0) + return api.getArtists(musicFolderId) + } + + override fun star(id: Long?, albumId: Long?, artistId: Long?): Call { + checkVersion(V1_8_0) + return api.star(id, albumId, artistId) + } + + override fun unstar(id: Long?, albumId: Long?, artistId: Long?): Call { + checkVersion(V1_8_0) + return api.unstar(id, albumId, artistId) + } + + override fun getArtist(id: Long): Call { + checkVersion(V1_8_0) + return api.getArtist(id) + } + + override fun getAlbum(id: Long): Call { + checkVersion(V1_8_0) + return api.getAlbum(id) + } + + override fun search2(query: String, + artistCount: Int?, + artistOffset: Int?, + albumCount: Int?, + albumOffset: Int?, + songCount: Int?, + musicFolderId: Long?): Call { + checkVersion(V1_4_0) + checkParamVersion(musicFolderId, V1_12_0) + return api.search2(query, artistCount, artistOffset, albumCount, albumOffset, songCount, + musicFolderId) + } + + override fun search3(query: String, + artistCount: Int?, + artistOffset: Int?, + albumCount: Int?, + albumOffset: Int?, + songCount: Int?, + musicFolderId: Long?): Call { + checkVersion(V1_8_0) + checkParamVersion(musicFolderId, V1_12_0) + return api.search3(query, artistCount, artistOffset, albumCount, albumOffset, + songCount, musicFolderId) + } + + override fun getPlaylists(username: String?): Call { + checkParamVersion(username, V1_8_0) + return api.getPlaylists(username) + } + + override fun createPlaylist(id: Long?, + name: String?, + songIds: List?): Call { + checkVersion(V1_2_0) + return api.createPlaylist(id, name, songIds) + } + + override fun deletePlaylist(id: Long): Call { + checkVersion(V1_2_0) + return api.deletePlaylist(id) + } + + override fun updatePlaylist(id: Long, + name: String?, + comment: String?, + public: Boolean?, + songIdsToAdd: List?, + songIndexesToRemove: List?): Call { + checkVersion(V1_8_0) + return api.updatePlaylist(id, name, comment, public, songIdsToAdd, songIndexesToRemove) + } + + override fun getPodcasts(includeEpisodes: Boolean?, id: Long?): Call { + checkVersion(V1_6_0) + checkParamVersion(includeEpisodes, V1_9_0) + checkParamVersion(id, V1_9_0) + return api.getPodcasts(includeEpisodes, id) + } + + override fun getLyrics(artist: String?, title: String?): Call { + checkVersion(V1_2_0) + return api.getLyrics(artist, title) + } + + override fun scrobble(id: String, time: Long?, submission: Boolean?): Call { + checkVersion(V1_5_0) + checkParamVersion(time, V1_8_0) + return api.scrobble(id, time, submission) + } + + override fun getAlbumList(type: AlbumListType, + size: Int?, + offset: Int?, + fromYear: Int?, + toYear: Int?, + genre: String?, + musicFolderId: Long?): Call { + checkVersion(V1_2_0) + checkParamVersion(musicFolderId, V1_11_0) + return api.getAlbumList(type, size, offset, fromYear, toYear, genre, musicFolderId) + } + + override fun getAlbumList2(type: AlbumListType, + size: Int?, + offset: Int?, + fromYear: Int?, + toYear: Int?, + genre: String?, + musicFolderId: Long?): Call { + checkVersion(V1_8_0) + checkParamVersion(musicFolderId, V1_12_0) + return api.getAlbumList2(type, size, offset, fromYear, toYear, genre, musicFolderId) + } + + override fun getRandomSongs(size: Int?, + genre: String?, + fromYear: Int?, + toYear: Int?, + musicFolderId: Long?): Call { + checkVersion(V1_2_0) + return api.getRandomSongs(size, genre, fromYear, toYear, musicFolderId) + } + + override fun getStarred(musicFolderId: Long?): Call { + checkVersion(V1_8_0) + checkParamVersion(musicFolderId, V1_12_0) + return api.getStarred(musicFolderId) + } + + override fun getStarred2(musicFolderId: Long?): Call { + checkVersion(V1_8_0) + checkParamVersion(musicFolderId, V1_12_0) + return api.getStarred2(musicFolderId) + } + + override fun stream(id: String, + maxBitRate: Int?, + format: String?, + timeOffset: Int?, + videoSize: String?, + estimateContentLength: Boolean?, + converted: Boolean?, + offset: Long?): Call { + checkParamVersion(maxBitRate, V1_2_0) + checkParamVersion(format, V1_6_0) + checkParamVersion(videoSize, V1_6_0) + checkParamVersion(estimateContentLength, V1_8_0) + checkParamVersion(converted, V1_14_0) + return api.stream(id, maxBitRate, format, timeOffset, videoSize, + estimateContentLength, converted) + } + + override fun jukeboxControl(action: JukeboxAction, + index: Int?, + offset: Int?, + ids: List?, + gain: Float?): Call { + checkVersion(V1_2_0) + checkParamVersion(offset, V1_7_0) + return api.jukeboxControl(action, index, offset, ids, gain) + } + + override fun getShares(): Call { + checkVersion(V1_6_0) + return api.getShares() + } + + override fun createShare(idsToShare: List, + description: String?, + expires: Long?): Call { + checkVersion(V1_6_0) + return api.createShare(idsToShare, description, expires) + } + + override fun deleteShare(id: Long): Call { + checkVersion(V1_6_0) + return api.deleteShare(id) + } + + override fun updateShare(id: Long, + description: String?, + expires: Long?): Call { + checkVersion(V1_6_0) + return api.updateShare(id, description, expires) + } + + override fun getGenres(): Call { + checkVersion(V1_9_0) + return api.getGenres() + } + + override fun getSongsByGenre(genre: String, + count: Int, + offset: Int, + musicFolderId: Long?): Call { + checkVersion(V1_9_0) + checkParamVersion(musicFolderId, V1_12_0) + return api.getSongsByGenre(genre, count, offset, musicFolderId) + } + + override fun getUser(username: String): Call { + checkVersion(V1_3_0) + return api.getUser(username) + } + + override fun getChatMessages(since: Long?): Call { + checkVersion(V1_2_0) + return api.getChatMessages(since) + } + + override fun addChatMessage(message: String): Call { + checkVersion(V1_2_0) + return api.addChatMessage(message) + } + + override fun getBookmarks(): Call { + checkVersion(V1_9_0) + return api.getBookmarks() + } + + override fun createBookmark(id: Int, position: Long, comment: String?): Call { + checkVersion(V1_9_0) + return api.createBookmark(id, position, comment) + } + + override fun deleteBookmark(id: Int): Call { + checkVersion(V1_9_0) + return api.deleteBookmark(id) + } + + override fun getVideos(): Call { + checkVersion(V1_8_0) + return api.getVideos() + } + + override fun getAvatar(username: String): Call { + checkVersion(V1_8_0) + return api.getAvatar(username) + } + + private fun checkVersion(expectedVersion: SubsonicAPIVersions) { + if (currentApiVersion < expectedVersion) throw ApiNotSupportedException(currentApiVersion) + } + + private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) { + if (param != null) { + checkVersion(expectedVersion) + } + } +} diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index 772de383..15fc5fae 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -26,6 +26,9 @@ private const val READ_TIMEOUT = 60_000L * * For supported API calls see [SubsonicAPIDefinition]. * + * Client will automatically adjust [protocolVersion] to the current server version on + * doing successful requests. + * * @author Yahor Berdnikau */ class SubsonicAPIClient(baseUrl: String, @@ -37,6 +40,7 @@ class SubsonicAPIClient(baseUrl: String, private val versionInterceptor = VersionInterceptor(minimalProtocolVersion) { protocolVersion = it } + private val proxyPasswordInterceptor = ProxyPasswordInterceptor(minimalProtocolVersion, PasswordHexInterceptor(password), PasswordMD5Interceptor(password)) @@ -47,6 +51,7 @@ class SubsonicAPIClient(baseUrl: String, private set(value) { field = value proxyPasswordInterceptor.apiVersion = field + wrappedApi.currentApiVersion = field } private val okHttpClient = OkHttpClient.Builder() @@ -78,7 +83,11 @@ class SubsonicAPIClient(baseUrl: String, .addConverterFactory(JacksonConverterFactory.create(jacksonMapper)) .build() - val api: SubsonicAPIDefinition = retrofit.create(SubsonicAPIDefinition::class.java) + private val wrappedApi = ApiVersionCheckWrapper( + retrofit.create(SubsonicAPIDefinition::class.java), + minimalProtocolVersion) + + val api: SubsonicAPIDefinition get() = wrappedApi /** * Convenient method to get cover art from api using item [id] and optional maximum [size]. diff --git a/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapperTest.kt b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapperTest.kt new file mode 100644 index 00000000..d139b863 --- /dev/null +++ b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapperTest.kt @@ -0,0 +1,46 @@ +package org.moire.ultrasonic.api.subsonic + +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.never +import com.nhaarman.mockito_kotlin.verify +import org.amshove.kluent.`should throw` +import org.junit.Test +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_1_0 +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_2_0 +import org.moire.ultrasonic.api.subsonic.models.AlbumListType.BY_GENRE + +/** + * Unit test for [ApiVersionCheckWrapper]. + */ +class ApiVersionCheckWrapperTest { + private val apiMock = mock() + private val wrapper = ApiVersionCheckWrapper(apiMock, V1_1_0) + + @Test + fun `Should just call real api for ping`() { + wrapper.ping() + + verify(apiMock).ping() + } + + @Test + fun `Should throw ApiNotSupportedException when current api level is too low for call`() { + val throwCall = { wrapper.getBookmarks() } + + throwCall `should throw` ApiNotSupportedException::class + verify(apiMock, never()).getBookmarks() + } + + @Test + fun `Should throw ApiNotSupportedException when call param is not supported by current api`() { + wrapper.currentApiVersion = V1_2_0 + + wrapper.getAlbumList(BY_GENRE) + + val throwCall = { wrapper.getAlbumList(BY_GENRE, musicFolderId = 12L) } + + throwCall `should throw` ApiNotSupportedException::class + verify(apiMock).getAlbumList(BY_GENRE) + verify(apiMock, never()).getAlbumList(BY_GENRE, musicFolderId = 12L) + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java index d7d35c36..3fa24033 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java @@ -43,6 +43,7 @@ public class MusicServiceFactory { if (OFFLINE_MUSIC_SERVICE == null) { synchronized (MusicServiceFactory.class) { if (OFFLINE_MUSIC_SERVICE == null) { + Log.d(LOG_TAG, "Creating new offline music service"); OFFLINE_MUSIC_SERVICE = new OfflineMusicService(createSubsonicApiClient(context)); } } @@ -54,6 +55,7 @@ public class MusicServiceFactory { if (REST_MUSIC_SERVICE == null) { synchronized (MusicServiceFactory.class) { if (REST_MUSIC_SERVICE == null) { + Log.d(LOG_TAG, "Creating new rest music service"); REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService( createSubsonicApiClient(context))); }