mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-11 09:10:57 +01:00
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 <egorr.berd@gmail.com>
This commit is contained in:
parent
336f505ba5
commit
3fddef0ec5
@ -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, {
|
||||
|
@ -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<GetArtistsResponse> {
|
||||
checkVersion(V1_8_0)
|
||||
return api.getArtists(musicFolderId)
|
||||
}
|
||||
|
||||
override fun star(id: Long?, albumId: Long?, artistId: Long?): Call<SubsonicResponse> {
|
||||
checkVersion(V1_8_0)
|
||||
return api.star(id, albumId, artistId)
|
||||
}
|
||||
|
||||
override fun unstar(id: Long?, albumId: Long?, artistId: Long?): Call<SubsonicResponse> {
|
||||
checkVersion(V1_8_0)
|
||||
return api.unstar(id, albumId, artistId)
|
||||
}
|
||||
|
||||
override fun getArtist(id: Long): Call<GetArtistResponse> {
|
||||
checkVersion(V1_8_0)
|
||||
return api.getArtist(id)
|
||||
}
|
||||
|
||||
override fun getAlbum(id: Long): Call<GetAlbumResponse> {
|
||||
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<SearchTwoResponse> {
|
||||
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<SearchThreeResponse> {
|
||||
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<GetPlaylistsResponse> {
|
||||
checkParamVersion(username, V1_8_0)
|
||||
return api.getPlaylists(username)
|
||||
}
|
||||
|
||||
override fun createPlaylist(id: Long?,
|
||||
name: String?,
|
||||
songIds: List<Long>?): Call<SubsonicResponse> {
|
||||
checkVersion(V1_2_0)
|
||||
return api.createPlaylist(id, name, songIds)
|
||||
}
|
||||
|
||||
override fun deletePlaylist(id: Long): Call<SubsonicResponse> {
|
||||
checkVersion(V1_2_0)
|
||||
return api.deletePlaylist(id)
|
||||
}
|
||||
|
||||
override fun updatePlaylist(id: Long,
|
||||
name: String?,
|
||||
comment: String?,
|
||||
public: Boolean?,
|
||||
songIdsToAdd: List<Long>?,
|
||||
songIndexesToRemove: List<Int>?): Call<SubsonicResponse> {
|
||||
checkVersion(V1_8_0)
|
||||
return api.updatePlaylist(id, name, comment, public, songIdsToAdd, songIndexesToRemove)
|
||||
}
|
||||
|
||||
override fun getPodcasts(includeEpisodes: Boolean?, id: Long?): Call<GetPodcastsResponse> {
|
||||
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<GetLyricsResponse> {
|
||||
checkVersion(V1_2_0)
|
||||
return api.getLyrics(artist, title)
|
||||
}
|
||||
|
||||
override fun scrobble(id: String, time: Long?, submission: Boolean?): Call<SubsonicResponse> {
|
||||
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<GetAlbumListResponse> {
|
||||
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<GetAlbumList2Response> {
|
||||
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<GetRandomSongsResponse> {
|
||||
checkVersion(V1_2_0)
|
||||
return api.getRandomSongs(size, genre, fromYear, toYear, musicFolderId)
|
||||
}
|
||||
|
||||
override fun getStarred(musicFolderId: Long?): Call<GetStarredResponse> {
|
||||
checkVersion(V1_8_0)
|
||||
checkParamVersion(musicFolderId, V1_12_0)
|
||||
return api.getStarred(musicFolderId)
|
||||
}
|
||||
|
||||
override fun getStarred2(musicFolderId: Long?): Call<GetStarredTwoResponse> {
|
||||
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<ResponseBody> {
|
||||
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<String>?,
|
||||
gain: Float?): Call<JukeboxResponse> {
|
||||
checkVersion(V1_2_0)
|
||||
checkParamVersion(offset, V1_7_0)
|
||||
return api.jukeboxControl(action, index, offset, ids, gain)
|
||||
}
|
||||
|
||||
override fun getShares(): Call<SharesResponse> {
|
||||
checkVersion(V1_6_0)
|
||||
return api.getShares()
|
||||
}
|
||||
|
||||
override fun createShare(idsToShare: List<String>,
|
||||
description: String?,
|
||||
expires: Long?): Call<SharesResponse> {
|
||||
checkVersion(V1_6_0)
|
||||
return api.createShare(idsToShare, description, expires)
|
||||
}
|
||||
|
||||
override fun deleteShare(id: Long): Call<SubsonicResponse> {
|
||||
checkVersion(V1_6_0)
|
||||
return api.deleteShare(id)
|
||||
}
|
||||
|
||||
override fun updateShare(id: Long,
|
||||
description: String?,
|
||||
expires: Long?): Call<SubsonicResponse> {
|
||||
checkVersion(V1_6_0)
|
||||
return api.updateShare(id, description, expires)
|
||||
}
|
||||
|
||||
override fun getGenres(): Call<GenresResponse> {
|
||||
checkVersion(V1_9_0)
|
||||
return api.getGenres()
|
||||
}
|
||||
|
||||
override fun getSongsByGenre(genre: String,
|
||||
count: Int,
|
||||
offset: Int,
|
||||
musicFolderId: Long?): Call<GetSongsByGenreResponse> {
|
||||
checkVersion(V1_9_0)
|
||||
checkParamVersion(musicFolderId, V1_12_0)
|
||||
return api.getSongsByGenre(genre, count, offset, musicFolderId)
|
||||
}
|
||||
|
||||
override fun getUser(username: String): Call<GetUserResponse> {
|
||||
checkVersion(V1_3_0)
|
||||
return api.getUser(username)
|
||||
}
|
||||
|
||||
override fun getChatMessages(since: Long?): Call<ChatMessagesResponse> {
|
||||
checkVersion(V1_2_0)
|
||||
return api.getChatMessages(since)
|
||||
}
|
||||
|
||||
override fun addChatMessage(message: String): Call<SubsonicResponse> {
|
||||
checkVersion(V1_2_0)
|
||||
return api.addChatMessage(message)
|
||||
}
|
||||
|
||||
override fun getBookmarks(): Call<BookmarksResponse> {
|
||||
checkVersion(V1_9_0)
|
||||
return api.getBookmarks()
|
||||
}
|
||||
|
||||
override fun createBookmark(id: Int, position: Long, comment: String?): Call<SubsonicResponse> {
|
||||
checkVersion(V1_9_0)
|
||||
return api.createBookmark(id, position, comment)
|
||||
}
|
||||
|
||||
override fun deleteBookmark(id: Int): Call<SubsonicResponse> {
|
||||
checkVersion(V1_9_0)
|
||||
return api.deleteBookmark(id)
|
||||
}
|
||||
|
||||
override fun getVideos(): Call<VideosResponse> {
|
||||
checkVersion(V1_8_0)
|
||||
return api.getVideos()
|
||||
}
|
||||
|
||||
override fun getAvatar(username: String): Call<ResponseBody> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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].
|
||||
|
@ -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<SubsonicAPIDefinition>()
|
||||
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)
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user