From d5cdc1817434d904ecda3cc6d5caa4b27232c2bf Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Fri, 10 Nov 2017 21:01:32 +0100 Subject: [PATCH 1/3] Add getShares call. Signed-off-by: Yahor Berdnikau --- .../api/subsonic/SubsonicApiGetSharesTest.kt | 50 +++++++++++++++++++ .../resources/get_shares_ok.json | 43 ++++++++++++++++ .../api/subsonic/SubsonicAPIDefinition.kt | 4 ++ .../ultrasonic/api/subsonic/models/Share.kt | 15 ++++++ .../api/subsonic/response/SharesResponse.kt | 17 +++++++ 5 files changed, 129 insertions(+) create mode 100644 subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetSharesTest.kt create mode 100644 subsonic-api/src/integrationTest/resources/get_shares_ok.json create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Share.kt create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SharesResponse.kt diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetSharesTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetSharesTest.kt new file mode 100644 index 00000000..dd98c2d2 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetSharesTest.kt @@ -0,0 +1,50 @@ +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.MusicDirectoryChild + +/** + * Integration test for [SubsonicAPIDefinition.getShares] call. + */ +class SubsonicApiGetSharesTest : SubsonicAPIClientTest() { + @Test + fun `Should handle error response`() { + val response = checkErrorCallParsed(mockWebServerRule) { + client.api.getShares().execute() + } + + response.shares `should equal` emptyList() + } + + @Test + fun `Should handle ok response`() { + mockWebServerRule.enqueueResponse("get_shares_ok.json") + + val response = client.api.getShares().execute() + + assertResponseSuccessful(response) + response.body().shares.size `should equal to` 1 + with(response.body().shares[0]) { + id `should equal to` 0 + url `should equal to` "https://airsonic.egorr.by/ext/share/awdwo?jwt=eyJhbGciOiJIUzI1" + + "NiJ9.eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8" + + "hJ692UxorHdHWFU2RB-fMCmCA4IJ_dTw" + username `should equal to` "admin" + created `should equal` parseDate("2017-11-07T21:33:51.748Z") + expires `should equal` parseDate("2018-11-07T21:33:51.748Z") + lastVisited `should equal` parseDate("2018-11-07T21:33:51.748Z") + visitCount `should equal to` 0 + description `should equal to` "Awesome link!" + items.size `should equal to` 1 + items[0] `should equal` MusicDirectoryChild(id = 4212, parent = 4186, isDir = false, + title = "Heaven Knows", album = "Going to Hell", artist = "The Pretty Reckless", + track = 3, year = 2014, genre = "Hard Rock", coverArt = "4186", size = 9025090, + contentType = "audio/mpeg", suffix = "mp3", duration = 225, bitRate = 320, + path = "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3", isVideo = false, + playCount = 2, discNumber = 1, created = parseDate("2016-10-23T21:30:40.000Z"), + albumId = 388, artistId = 238, type = "music") + } + } +} diff --git a/subsonic-api/src/integrationTest/resources/get_shares_ok.json b/subsonic-api/src/integrationTest/resources/get_shares_ok.json new file mode 100644 index 00000000..1a165c94 --- /dev/null +++ b/subsonic-api/src/integrationTest/resources/get_shares_ok.json @@ -0,0 +1,43 @@ +{ + "subsonic-response" : { + "status" : "ok", + "version" : "1.15.0", + "shares" : { + "share" : [ { + "id" : "0", + "url" : "https://airsonic.egorr.by/ext/share/awdwo?jwt=eyJhbGciOiJIUzI1NiJ9.eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8hJ692UxorHdHWFU2RB-fMCmCA4IJ_dTw", + "username" : "admin", + "created" : "2017-11-07T21:33:51.748Z", + "expires" : "2018-11-07T21:33:51.748Z", + "lastVisited" : "2018-11-07T21:33:51.748Z", + "description" : "Awesome link!", + "visitCount" : 0, + "entry" : [ { + "id" : "4212", + "parent" : "4186", + "isDir" : false, + "title" : "Heaven Knows", + "album" : "Going to Hell", + "artist" : "The Pretty Reckless", + "track" : 3, + "year" : 2014, + "genre" : "Hard Rock", + "coverArt" : "4186", + "size" : 9025090, + "contentType" : "audio/mpeg", + "suffix" : "mp3", + "duration" : 225, + "bitRate" : 320, + "path" : "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3", + "isVideo" : false, + "playCount" : 2, + "discNumber" : 1, + "created" : "2016-10-23T21:30:40.000Z", + "albumId" : "388", + "artistId" : "238", + "type" : "music" + } ] + } ] + } + } +} 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 0f760389..4650afba 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 @@ -23,6 +23,7 @@ import org.moire.ultrasonic.api.subsonic.response.MusicFoldersResponse import org.moire.ultrasonic.api.subsonic.response.SearchResponse 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 retrofit2.Call import retrofit2.http.GET @@ -188,4 +189,7 @@ interface SubsonicAPIDefinition { @Query("offset") offset: Int? = null, @Query("id") ids: List? = null, @Query("gain") gain: Float? = null): Call + + @GET("getShares.view") + fun getShares(): Call } diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Share.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Share.kt new file mode 100644 index 00000000..315c1bbf --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Share.kt @@ -0,0 +1,15 @@ +package org.moire.ultrasonic.api.subsonic.models + +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.Calendar + +data class Share( + val id: Long = -1L, + val url: String = "", + val username: String = "", + val created: Calendar? = null, + val expires: Calendar? = null, + val visitCount: Int = 0, + val description: String = "", + val lastVisited: Calendar? = null, + @JsonProperty("entry") val items: List = emptyList()) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SharesResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SharesResponse.kt new file mode 100644 index 00000000..c3d4fb0a --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SharesResponse.kt @@ -0,0 +1,17 @@ +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.Share + +class SharesResponse(status: Status, + version: SubsonicAPIVersions, + error: SubsonicError?) + : SubsonicResponse(status, version, error) { + @JsonProperty("shares") private val wrappedShares = SharesWrapper() + + val shares get() = wrappedShares.share +} + +internal class SharesWrapper(val share: List = emptyList()) From 3bbd1fb16b5b0b926d52dc5e44494ea9313fc31e Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Fri, 10 Nov 2017 21:31:48 +0100 Subject: [PATCH 2/3] Add methods to convert api share entity to domain entity. Signed-off-by: Yahor Berdnikau --- .../org/moire/ultrasonic/domain/Share.java | 39 +++++++++++++- .../ultrasonic/data/APIPlaylistConverter.kt | 5 +- .../ultrasonic/data/APIShareConverter.kt | 26 +++++++++ .../data/APIPlaylistConverterTest.kt | 6 +-- .../ultrasonic/data/APIShareConverterTest.kt | 54 +++++++++++++++++++ 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIShareConverter.kt create mode 100644 ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIShareConverterTest.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/domain/Share.java b/ultrasonic/src/main/java/org/moire/ultrasonic/domain/Share.java index 3c5e5392..30111698 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/domain/Share.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/domain/Share.java @@ -7,8 +7,7 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; -public class Share implements Serializable -{ +public class Share implements Serializable { private static final long serialVersionUID = 1487561657691009668L; private static final Pattern urlPattern = Pattern.compile(".*/([^/?]+).*"); private String id; @@ -120,4 +119,40 @@ public class Share implements Serializable { entries.add(entry); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Share share = (Share) o; + + if (id != null ? !id.equals(share.id) : share.id != null) return false; + if (url != null ? !url.equals(share.url) : share.url != null) return false; + if (description != null ? !description.equals(share.description) : share.description != null) + return false; + if (username != null ? !username.equals(share.username) : share.username != null) + return false; + if (created != null ? !created.equals(share.created) : share.created != null) return false; + if (lastVisited != null ? !lastVisited.equals(share.lastVisited) : share.lastVisited != null) + return false; + if (expires != null ? !expires.equals(share.expires) : share.expires != null) return false; + if (visitCount != null ? !visitCount.equals(share.visitCount) : share.visitCount != null) + return false; + return entries != null ? entries.equals(share.entries) : share.entries == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (url != null ? url.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (username != null ? username.hashCode() : 0); + result = 31 * result + (created != null ? created.hashCode() : 0); + result = 31 * result + (lastVisited != null ? lastVisited.hashCode() : 0); + result = 31 * result + (expires != null ? expires.hashCode() : 0); + result = 31 * result + (visitCount != null ? visitCount.hashCode() : 0); + result = 31 * result + (entries != null ? entries.hashCode() : 0); + return result; + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIPlaylistConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIPlaylistConverter.kt index 1dfc0efb..28c6e678 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIPlaylistConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIPlaylistConverter.kt @@ -6,8 +6,11 @@ package org.moire.ultrasonic.data import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Playlist import java.text.SimpleDateFormat +import kotlin.LazyThreadSafetyMode.NONE import org.moire.ultrasonic.api.subsonic.models.Playlist as APIPlaylist +internal val playlistDateFormat by lazy(NONE) { SimpleDateFormat.getInstance() } + fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply { name = this@toMusicDirectoryDomainEntity.name addAll(this@toMusicDirectoryDomainEntity.entriesList.map { it.toDomainEntity() }) @@ -15,7 +18,7 @@ fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory( fun APIPlaylist.toDomainEntity(): Playlist = Playlist(this.id.toString(), this.name, this.owner, this.comment, this.songCount.toString(), - this.created?.let { SimpleDateFormat.getDateTimeInstance().format(it.time) }, + this.created?.let { playlistDateFormat.format(it.time) }, public.toString()) fun List.toDomainEntitiesList(): List = this.map { it.toDomainEntity() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIShareConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIShareConverter.kt new file mode 100644 index 00000000..c0275878 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIShareConverter.kt @@ -0,0 +1,26 @@ +// Contains helper method to convert subsonic api share to domain model +@file:JvmName("APIShareConverter") +package org.moire.ultrasonic.data + +import org.moire.ultrasonic.domain.Share +import java.text.SimpleDateFormat +import kotlin.LazyThreadSafetyMode.NONE +import org.moire.ultrasonic.api.subsonic.models.Share as APIShare + +internal val shareTimeFormat by lazy(NONE) { SimpleDateFormat.getInstance() } + +fun List.toDomainEntitiesList(): List = this.map { + it.toDomainEntity() +} + +fun APIShare.toDomainEntity(): Share = Share().apply { + created = this@toDomainEntity.created?.let { shareTimeFormat.format(it.time) } + description = this@toDomainEntity.description + expires = this@toDomainEntity.expires?.let { shareTimeFormat.format(it.time) } + id = this@toDomainEntity.id.toString() + lastVisited = this@toDomainEntity.lastVisited?.let { shareTimeFormat.format(it.time) } + url = this@toDomainEntity.url + username = this@toDomainEntity.username + visitCount = this@toDomainEntity.visitCount.toLong() + entries.addAll(this@toDomainEntity.items.toDomainEntityList()) +} diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIPlaylistConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIPlaylistConverterTest.kt index e04fc01a..4f1be508 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIPlaylistConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIPlaylistConverterTest.kt @@ -7,11 +7,10 @@ import org.amshove.kluent.`should equal` import org.junit.Test import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild import org.moire.ultrasonic.api.subsonic.models.Playlist -import java.text.SimpleDateFormat import java.util.Calendar /** - * Unit test for extension functions in [APIPlaylistConverter.kt] file. + * Unit test for extension functions that converts api playlist entity to domain. */ class APIPlaylistConverterTest { @Test @@ -47,8 +46,7 @@ class APIPlaylistConverterTest { owner `should equal to` entity.owner public `should equal to` entity.public songCount `should equal to` entity.songCount.toString() - created `should equal to` SimpleDateFormat.getDateTimeInstance() - .format(entity.created?.time) + created `should equal to` playlistDateFormat.format(entity.created?.time) } } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIShareConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIShareConverterTest.kt new file mode 100644 index 00000000..bd40d5f3 --- /dev/null +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIShareConverterTest.kt @@ -0,0 +1,54 @@ +@file:Suppress("IllegalIdentifier") + +package org.moire.ultrasonic.data + +import org.amshove.kluent.`should equal to` +import org.amshove.kluent.`should equal` +import org.junit.Test +import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild +import org.moire.ultrasonic.api.subsonic.models.Share +import java.util.Calendar + +/** + * Unit test for api to domain share entity converter functions. + */ +class APIShareConverterTest { + @Test + fun `Should convert share entity to domain`() { + val entity = createFakeShare() + + val domainEntity = entity.toDomainEntity() + + with(domainEntity) { + id `should equal to` entity.id.toString() + url `should equal to` entity.url + description `should equal to` entity.description + username `should equal to` entity.username + created `should equal to` shareTimeFormat.format(entity.created?.time) + lastVisited `should equal to` shareTimeFormat.format(entity.lastVisited?.time) + expires `should equal to` shareTimeFormat.format(entity.expires?.time) + visitCount `should equal to` entity.visitCount.toLong() + entries `should equal` entity.items.toDomainEntityList() + } + } + + private fun createFakeShare(): Share { + return Share(id = 45L, url = "some-long-url", username = "Bender", + created = Calendar.getInstance(), expires = Calendar.getInstance(), visitCount = 24, + description = "Kiss my shiny metal ass", lastVisited = Calendar.getInstance(), + items = listOf(MusicDirectoryChild())) + } + + @Test + fun `Should parse list of shares into domain entity list`() { + val entityList = listOf( + createFakeShare(), + createFakeShare().copy(id = 554L, lastVisited = null)) + + val domainEntityList = entityList.toDomainEntitiesList() + + domainEntityList.size `should equal to` entityList.size + domainEntityList[0] `should equal` entityList[0].toDomainEntity() + domainEntityList[1] `should equal` entityList[1].toDomainEntity() + } +} From 04df5b20dabe149734b6585a4c3d3593dc9c5424 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Fri, 10 Nov 2017 21:38:05 +0100 Subject: [PATCH 3/3] Use new api getShares call in RESTMusicService. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/service/RESTMusicService.java | 27 +++++++++---------- 1 file changed, 13 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 7133a0ce..abd2a32e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -78,6 +78,7 @@ import org.moire.ultrasonic.api.subsonic.response.MusicFoldersResponse; import org.moire.ultrasonic.api.subsonic.response.SearchResponse; 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.StreamResponse; import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse; import org.moire.ultrasonic.data.APIAlbumConverter; @@ -90,6 +91,7 @@ import org.moire.ultrasonic.data.APIMusicFolderConverter; import org.moire.ultrasonic.data.APIPlaylistConverter; import org.moire.ultrasonic.data.APIPodcastConverter; import org.moire.ultrasonic.data.APISearchConverter; +import org.moire.ultrasonic.data.APIShareConverter; import org.moire.ultrasonic.domain.Bookmark; import org.moire.ultrasonic.domain.ChatMessage; import org.moire.ultrasonic.domain.Genre; @@ -947,20 +949,17 @@ public class RESTMusicService implements MusicService return APIJukeboxConverter.toDomainEntity(response.body().getJukebox()); } - @Override - public List getShares(boolean refresh, Context context, ProgressListener progressListener) throws Exception - { - checkServerVersion(context, "1.6", "Shares not supported."); - Reader reader = getReader(context, progressListener, "getShares", null); - try - { - return new ShareParser(context).parse(reader, progressListener); - } - finally - { - Util.close(reader); - } - } + @Override + public List getShares(boolean refresh, + Context context, + ProgressListener progressListener) throws Exception { + updateProgressListener(progressListener, R.string.parser_reading); + + Response response = subsonicAPIClient.getApi().getShares().execute(); + checkResponseSuccessful(response); + + return APIShareConverter.toDomainEntitiesList(response.body().getShares()); + } private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception {