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()
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(KotlinModule())
private val retrofit = Retrofit.Builder()

View File

@ -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<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 description: 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.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.<Object>asList(type, size, offset));
try
{
return new AlbumListParser(context).parse(reader, progressListener, false);
}
finally
{
Util.close(reader);
}
}
updateProgressListener(progressListener, R.string.parser_reading);
Response<GetAlbumListResponse> response = subsonicAPIClient.getApi()
.getAlbumList(AlbumListType.fromName(type), size, offset, null,
null, null, null).execute();
checkResponseSuccessful(response);
List<MusicDirectory.Entry> 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

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 {
name = this@toDomainEntity.name
addAll(this@toDomainEntity.childList.map { it.toDomainEntity() })

View File

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