Merge pull request #49 from ultrasonic/add-get-album-list

Add get album list
This commit is contained in:
Yahor Berdnikau 2017-09-14 22:39:20 +02:00 committed by GitHub
commit 0a22f7bcc7
11 changed files with 298 additions and 16 deletions

View File

@ -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()
}
}
}

View File

@ -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"
} ]
}
}
}

View File

@ -54,6 +54,7 @@ class SubsonicAPIClient(baseUrl: String,
private val jacksonMapper = ObjectMapper() private val jacksonMapper = ObjectMapper()
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true) .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(KotlinModule()) .registerModule(KotlinModule())
private val retrofit = Retrofit.Builder() private val retrofit = Retrofit.Builder()

View File

@ -1,5 +1,7 @@
package org.moire.ultrasonic.api.subsonic 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.GetAlbumResponse
import org.moire.ultrasonic.api.subsonic.response.GetArtistResponse import org.moire.ultrasonic.api.subsonic.response.GetArtistResponse
import org.moire.ultrasonic.api.subsonic.response.GetArtistsResponse import org.moire.ultrasonic.api.subsonic.response.GetArtistsResponse
@ -123,4 +125,13 @@ interface SubsonicAPIDefinition {
fun scrobble(@Query("id") id: String, fun scrobble(@Query("id") id: String,
@Query("time") time: Long? = null, @Query("time") time: Long? = null,
@Query("submission") submission: Boolean? = null): Call<SubsonicResponse> @Query("submission") submission: Boolean? = null): Call<SubsonicResponse>
@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<GetAlbumListResponse>
} }

View File

@ -0,0 +1,43 @@
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 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)
}
}

View File

@ -32,4 +32,6 @@ data class MusicDirectoryChild(val id: Long = -1L,
val channelId: Long = -1, val channelId: Long = -1,
val description: String = "", val description: String = "",
val status: String = "", val status: String = "",
val publishDate: Calendar? = null) val publishDate: Calendar? = null,
val userRating: Int? = null,
val averageRating: Float? = null)

View File

@ -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<MusicDirectoryChild>
get() = albumWrapper.albumList
}
private class AlbumWrapper(
@JsonProperty("album") val albumList: List<MusicDirectoryChild> = emptyList())

View File

@ -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()
}
}

View File

@ -56,7 +56,9 @@ import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpContext;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient;
import org.moire.ultrasonic.api.subsonic.models.AlbumListType;
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild; 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.GetAlbumResponse;
import org.moire.ultrasonic.api.subsonic.response.GetArtistResponse; import org.moire.ultrasonic.api.subsonic.response.GetArtistResponse;
import org.moire.ultrasonic.api.subsonic.response.GetArtistsResponse; import org.moire.ultrasonic.api.subsonic.response.GetArtistsResponse;
@ -653,21 +655,28 @@ public class RESTMusicService implements MusicService
checkResponseSuccessful(response); checkResponseSuccessful(response);
} }
@Override @Override
public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception public MusicDirectory getAlbumList(String type,
{ int size,
checkServerVersion(context, "1.2", "Album list not supported."); 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.<Object>asList(type, size, offset)); updateProgressListener(progressListener, R.string.parser_reading);
try Response<GetAlbumListResponse> response = subsonicAPIClient.getApi()
{ .getAlbumList(AlbumListType.fromName(type), size, offset, null,
return new AlbumListParser(context).parse(reader, progressListener, false); null, null, null).execute();
} checkResponseSuccessful(response);
finally
{ List<MusicDirectory.Entry> childList = APIMusicDirectoryConverter
Util.close(reader); .toDomainEntityList(response.body().getAlbumList());
} MusicDirectory result = new MusicDirectory();
} result.addAll(childList);
return result;
}
@Override @Override
public MusicDirectory getAlbumList2(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception public MusicDirectory getAlbumList2(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception

View File

@ -48,6 +48,8 @@ fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.
} }
} }
fun List<MusicDirectoryChild>.toDomainEntityList() = this.map { it.toDomainEntity() }
fun APIMusicDirectory.toDomainEntity(): MusicDirectory = MusicDirectory().apply { fun APIMusicDirectory.toDomainEntity(): MusicDirectory = MusicDirectory().apply {
name = this@toDomainEntity.name name = this@toDomainEntity.name
addAll(this@toDomainEntity.childList.map { it.toDomainEntity() }) addAll(this@toDomainEntity.childList.map { it.toDomainEntity() })

View File

@ -71,7 +71,7 @@ class APIMusicDirectoryConverterTest {
} }
@Test @Test
fun `Should convert MusicDirectoryChild podact entity`() { fun `Should convert MusicDirectoryChild podcast entity`() {
val entity = MusicDirectoryChild(id = 584, streamId = 394, val entity = MusicDirectoryChild(id = 584, streamId = 394,
artist = "some-artist", publishDate = Calendar.getInstance()) artist = "some-artist", publishDate = Calendar.getInstance())
@ -82,4 +82,16 @@ class APIMusicDirectoryConverterTest {
artist `should equal to` dateFormat.format(entity.publishDate?.time) 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()
}
}
} }