From cd617c7dd811948105c2fcd76ea5390c07cbe0be Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 13 Sep 2017 22:45:10 +0200 Subject: [PATCH 1/6] Add getAlbumList call. Signed-off-by: Yahor Berdnikau --- .../SubsonicApiGetAlbumListRequestTest.kt | 109 ++++++++++++++++++ .../resources/get_album_list_ok.json | 33 ++++++ .../api/subsonic/SubsonicAPIDefinition.kt | 11 ++ .../api/subsonic/models/AlbumListType.kt | 24 ++++ .../subsonic/response/GetAlbumListResponse.kt | 19 +++ 5 files changed, 196 insertions(+) create mode 100644 subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAlbumListRequestTest.kt create mode 100644 subsonic-api/src/integrationTest/resources/get_album_list_ok.json create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAlbumListRequestTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAlbumListRequestTest.kt new file mode 100644 index 00000000..258b3489 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetAlbumListRequestTest.kt @@ -0,0 +1,109 @@ +package org.moire.ultrasonic.api.subsonic + +import org.amshove.kluent.`should equal to` +import org.amshove.kluent.`should equal` +import org.junit.Test +import org.moire.ultrasonic.api.subsonic.models.AlbumListType +import org.moire.ultrasonic.api.subsonic.models.AlbumListType.BY_GENRE +import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild + +/** + * Integration tests for [SubsonicAPIDefinition] for getAlbumList call. + */ +class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() { + @Test + fun `Should handle error response`() { + val response = checkErrorCallParsed(mockWebServerRule) { + client.api.getAlbumList(BY_GENRE).execute() + } + + response.albumList `should equal` emptyList() + } + + @Test + fun `Should handle ok response`() { + mockWebServerRule.enqueueResponse("get_album_list_ok.json") + + val response = client.api.getAlbumList(BY_GENRE).execute() + + assertResponseSuccessful(response) + with(response.body().albumList) { + size `should equal to` 2 + this[1] `should equal` MusicDirectoryChild(id = 9997, parent = 9996, isDir = true, + title = "Endless Forms Most Beautiful", album = "Endless Forms Most Beautiful", + artist = "Nightwish", year = 2015, genre = "Symphonic Metal", + coverArt = "9997", playCount = 11, + created = parseDate("2017-09-02T16:22:49.000Z")) + } + } + + @Test + fun `Should pass type in request params`() { + val listType = AlbumListType.HIGHEST + + mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", + expectedParam = "type=${listType.typeName}") { + client.api.getAlbumList(type = listType).execute() + } + } + + @Test + fun `Should pass size in request params`() { + val size = 45 + + mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", + expectedParam = "size=$size") { + client.api.getAlbumList(type = BY_GENRE, size = size).execute() + } + } + + @Test + fun `Should pass offset in request params`() { + val offset = 3 + + mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", + expectedParam = "offset=$offset") { + client.api.getAlbumList(type = BY_GENRE, offset = offset).execute() + } + } + + @Test + fun `Should pass from year in request params`() { + val fromYear = 2001 + + mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", + expectedParam = "fromYear=$fromYear") { + client.api.getAlbumList(type = BY_GENRE, fromYear = fromYear).execute() + } + } + + @Test + fun `Should pass to year in request params`() { + val toYear = 2017 + + mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", + expectedParam = "toYear=$toYear") { + client.api.getAlbumList(type = BY_GENRE, toYear = toYear).execute() + } + } + + @Test + fun `Should pass genre in request params`() { + val genre = "Rock" + + mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", + expectedParam = "genre=$genre") { + client.api.getAlbumList(type = BY_GENRE, genre = genre).execute() + } + } + + @Test + fun `Should pass music folder id in request params`() { + val folderId = 545L + + mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", + expectedParam = "musicFolderId=$folderId") { + client.api.getAlbumList(type = BY_GENRE, musicFolderId = folderId).execute() + } + } +} diff --git a/subsonic-api/src/integrationTest/resources/get_album_list_ok.json b/subsonic-api/src/integrationTest/resources/get_album_list_ok.json new file mode 100644 index 00000000..9de7d2e1 --- /dev/null +++ b/subsonic-api/src/integrationTest/resources/get_album_list_ok.json @@ -0,0 +1,33 @@ +{ + "subsonic-response" : { + "status" : "ok", + "version" : "1.15.0", + "albumList" : { + "album" : [ { + "id" : "10020", + "parent" : "490", + "isDir" : true, + "title" : "Fury", + "album" : "Fury", + "artist" : "Sick Puppies", + "year" : 2016, + "genre" : "Alternative Rock", + "coverArt" : "10020", + "playCount" : 13, + "created" : "2017-09-02T17:34:51.000Z" + }, { + "id" : "9997", + "parent" : "9996", + "isDir" : true, + "title" : "Endless Forms Most Beautiful", + "album" : "Endless Forms Most Beautiful", + "artist" : "Nightwish", + "year" : 2015, + "genre" : "Symphonic Metal", + "coverArt" : "9997", + "playCount" : 11, + "created" : "2017-09-02T16:22:49.000Z" + } ] + } + } +} \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt index ad396526..4bc2b692 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt @@ -1,5 +1,7 @@ package org.moire.ultrasonic.api.subsonic +import org.moire.ultrasonic.api.subsonic.models.AlbumListType +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 @@ -123,4 +125,13 @@ interface SubsonicAPIDefinition { fun scrobble(@Query("id") id: String, @Query("time") time: Long? = null, @Query("submission") submission: Boolean? = null): Call + + @GET("getAlbumList.view") + fun getAlbumList(@Query("type") type: AlbumListType, + @Query("size") size: Int? = null, + @Query("offset") offset: Int? = null, + @Query("fromYear") fromYear: Int? = null, + @Query("toYear") toYear: Int? = null, + @Query("genre") genre: String? = null, + @Query("musicFolderId") musicFolderId: Long? = null): Call } diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt new file mode 100644 index 00000000..bd29f3e8 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt @@ -0,0 +1,24 @@ +package org.moire.ultrasonic.api.subsonic.models + +/** + * Type of album list used in [org.moire.ultrasonic.api.subsonic.SubsonicAPIDefinition.getAlbumList] + * calls. + * + * @author Yahor Berdnikau + */ +enum class AlbumListType(val typeName: String) { + RANDOM("random"), + NEWEST("newest"), + HIGHEST("highest"), + FREQUENT("frequent"), + RECENT("recent"), + SORTED_BY_NAME("alphabeticalByName"), + SORTED_BY_ARTIST("alphabeticalByArtist"), + STARRED("starred"), + BY_YEAR("byYear"), + BY_GENRE("byGenre"); + + override fun toString(): String { + return super.toString().toLowerCase() + } +} diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt new file mode 100644 index 00000000..44e1cccf --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt @@ -0,0 +1,19 @@ +package org.moire.ultrasonic.api.subsonic.response + +import com.fasterxml.jackson.annotation.JsonProperty +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions +import org.moire.ultrasonic.api.subsonic.SubsonicError +import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild + +class GetAlbumListResponse(status: Status, + version: SubsonicAPIVersions, + error: SubsonicError?) + : SubsonicResponse(status, version, error) { + @JsonProperty("albumList") private val albumWrapper = AlbumWrapper() + + val albumList: List + get() = albumWrapper.albumList +} + +private class AlbumWrapper( + @JsonProperty("album") val albumList: List = emptyList()) From 046baf0ffe6497d3dddd3cb604dba2f373ba91f8 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Thu, 14 Sep 2017 21:41:32 +0200 Subject: [PATCH 2/6] Add missing user rating and average rating to MusicDirectoryChild. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/api/subsonic/models/MusicDirectoryChild.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/MusicDirectoryChild.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/MusicDirectoryChild.kt index fa0592b6..2f1f5843 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/MusicDirectoryChild.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/MusicDirectoryChild.kt @@ -32,4 +32,6 @@ data class MusicDirectoryChild(val id: Long = -1L, val channelId: Long = -1, val description: String = "", val status: String = "", - val publishDate: Calendar? = null) + val publishDate: Calendar? = null, + val userRating: Int? = null, + val averageRating: Float? = null) From 2d12aea79ff28c60af0289297e7f35c66d6bc88f Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Thu, 14 Sep 2017 21:51:56 +0200 Subject: [PATCH 3/6] Enable setting to not fail parsing on unknown field. Signed-off-by: Yahor Berdnikau --- .../org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt | 1 + 1 file changed, 1 insertion(+) 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 4dea2520..dd89c8f4 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 @@ -54,6 +54,7 @@ class SubsonicAPIClient(baseUrl: String, private val jacksonMapper = ObjectMapper() .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .registerModule(KotlinModule()) private val retrofit = Retrofit.Builder() From 645728c0f76d1db97470a4989c543948183729d1 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Thu, 14 Sep 2017 22:01:55 +0200 Subject: [PATCH 4/6] Add converting MusicDirectoryChild list to domain entities list. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/data/APIMusicDirectoryConverter.kt | 2 ++ .../data/APIMusicDirectoryConverterTest.kt | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIMusicDirectoryConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIMusicDirectoryConverter.kt index b5e98322..496723d5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIMusicDirectoryConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIMusicDirectoryConverter.kt @@ -48,6 +48,8 @@ fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory. } } +fun List.toDomainEntityList() = this.map { it.toDomainEntity() } + fun APIMusicDirectory.toDomainEntity(): MusicDirectory = MusicDirectory().apply { name = this@toDomainEntity.name addAll(this@toDomainEntity.childList.map { it.toDomainEntity() }) diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIMusicDirectoryConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIMusicDirectoryConverterTest.kt index dbbd6673..b7eb28f5 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIMusicDirectoryConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIMusicDirectoryConverterTest.kt @@ -71,7 +71,7 @@ class APIMusicDirectoryConverterTest { } @Test - fun `Should convert MusicDirectoryChild podact entity`() { + fun `Should convert MusicDirectoryChild podcast entity`() { val entity = MusicDirectoryChild(id = 584, streamId = 394, artist = "some-artist", publishDate = Calendar.getInstance()) @@ -82,4 +82,16 @@ class APIMusicDirectoryConverterTest { artist `should equal to` dateFormat.format(entity.publishDate?.time) } } + + @Test + fun `Should convert list of MusicDirectoryChild to domain entity list`() { + val entitiesList = listOf(MusicDirectoryChild(id = 45), MusicDirectoryChild(id = 34)) + + val domainList = entitiesList.toDomainEntityList() + + domainList.size `should equal to` entitiesList.size + domainList.forEachIndexed { index, entry -> + entry `should equal` entitiesList[index].toDomainEntity() + } + } } From 8e895685fc42b55dd266b5e63eacd7d5a41094c1 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Thu, 14 Sep 2017 22:32:24 +0200 Subject: [PATCH 5/6] Add method to map string type to AlbumListType enum. Signed-off-by: Yahor Berdnikau --- .../api/subsonic/models/AlbumListType.kt | 21 +++++++++- .../api/subsonic/models/AlbumListTypeTest.kt | 41 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt index bd29f3e8..8ab52824 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt @@ -19,6 +19,25 @@ enum class AlbumListType(val typeName: String) { BY_GENRE("byGenre"); override fun toString(): String { - return super.toString().toLowerCase() + return typeName + } + + companion object { + @JvmStatic + fun fromName(typeName: String): AlbumListType = when (typeName) { + in RANDOM.typeName -> RANDOM + in NEWEST.typeName -> NEWEST + in HIGHEST.typeName -> HIGHEST + in FREQUENT.typeName -> FREQUENT + in RECENT.typeName -> RECENT + in SORTED_BY_NAME.typeName -> SORTED_BY_NAME + in SORTED_BY_ARTIST.typeName -> SORTED_BY_ARTIST + in STARRED.typeName -> STARRED + in BY_YEAR.typeName -> BY_YEAR + in BY_GENRE.typeName -> BY_GENRE + else -> throw IllegalArgumentException("Unknown type: $typeName") + } + + private operator fun String.contains(other: String) = this.equals(other, true) } } diff --git a/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt new file mode 100644 index 00000000..ef0948d7 --- /dev/null +++ b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt @@ -0,0 +1,41 @@ +package org.moire.ultrasonic.api.subsonic.models + +import org.amshove.kluent.`should equal to` +import org.amshove.kluent.`should equal` +import org.amshove.kluent.`should throw` +import org.junit.Test + +/** + * Unit test for [AlbumListType] class. + */ +class AlbumListTypeTest { + @Test + fun `Should create type from string ignoring case`() { + val type = AlbumListType.SORTED_BY_NAME.typeName.toLowerCase() + + val albumListType = AlbumListType.fromName(type) + + albumListType `should equal` AlbumListType.SORTED_BY_NAME + } + + @Test + fun `Should throw IllegalArgumentException for unknown type`() { + val failCall = { + AlbumListType.fromName("some-not-existing-type") + } + + failCall `should throw` IllegalArgumentException::class + } + + @Test + fun `Should convert type string to corresponding AlbumListType`() { + AlbumListType.values().forEach { + AlbumListType.fromName(it.typeName) `should equal` it + } + } + + @Test + fun `Should return type name for toString call`() { + AlbumListType.STARRED.typeName `should equal to` AlbumListType.STARRED.toString() + } +} From 69ac8551c6b8024a245c81fac1ea8b35ee649cef Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Thu, 14 Sep 2017 22:34:30 +0200 Subject: [PATCH 6/6] Use new subsonic api getAlbumList() method. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/service/RESTMusicService.java | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) 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 d330e39d..1f8e002c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -56,7 +56,9 @@ import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HttpContext; import org.moire.ultrasonic.R; import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; +import org.moire.ultrasonic.api.subsonic.models.AlbumListType; import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild; +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; @@ -653,21 +655,28 @@ public class RESTMusicService implements MusicService checkResponseSuccessful(response); } - @Override - public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception - { - checkServerVersion(context, "1.2", "Album list not supported."); + @Override + public MusicDirectory getAlbumList(String type, + int size, + int offset, + Context context, + ProgressListener progressListener) throws Exception { + if (type == null) { + throw new IllegalArgumentException("Type is null!"); + } - Reader reader = getReader(context, progressListener, "getAlbumList", null, asList("type", "size", "offset"), Arrays.asList(type, size, offset)); - try - { - return new AlbumListParser(context).parse(reader, progressListener, false); - } - finally - { - Util.close(reader); - } - } + updateProgressListener(progressListener, R.string.parser_reading); + Response response = subsonicAPIClient.getApi() + .getAlbumList(AlbumListType.fromName(type), size, offset, null, + null, null, null).execute(); + checkResponseSuccessful(response); + + List childList = APIMusicDirectoryConverter + .toDomainEntityList(response.body().getAlbumList()); + MusicDirectory result = new MusicDirectory(); + result.addAll(childList); + return result; + } @Override public MusicDirectory getAlbumList2(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception