diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt index e57b53e1..817c0ada 100644 --- a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/CommonFunctions.kt @@ -3,6 +3,7 @@ package org.moire.ultrasonic.api.subsonic import okhttp3.mockwebserver.MockResponse import okio.Okio import org.amshove.kluent.`should be` +import org.amshove.kluent.`should contain` import org.amshove.kluent.`should not be` import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule @@ -63,3 +64,14 @@ fun SubsonicResponse.assertBaseResponseOk() { version `should be` SubsonicAPIVersions.V1_13_0 error `should be` null } + +fun MockWebServerRule.assertRequestParam(responseResourceName: String, + apiRequest: () -> Response, + expectedParam: String) { + this.enqueueResponse(responseResourceName) + apiRequest() + + val request = this.mockWebServer.takeRequest() + + request.requestLine `should contain` expectedParam +} diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTwoTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTwoTest.kt new file mode 100644 index 00000000..2ea69087 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTwoTest.kt @@ -0,0 +1,111 @@ +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.Artist +import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild + +/** + * Integration test for [SubsonicAPIClient] for search2 call. + */ +class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() { + @Test + fun `Should handle error response`() { + checkErrorCallParsed(mockWebServerRule, { + client.api.search2("some-query").execute() + }) + } + + @Test + fun `Should parse ok response`() { + mockWebServerRule.enqueueResponse("search2_ok.json") + + val response = client.api.search2("some-query").execute() + + assertResponseSuccessful(response) + with(response.body().searchResult) { + artistList.size `should equal to` 1 + artistList[0] `should equal` Artist(id = 522, name = "The Prodigy") + albumList.size `should equal to` 1 + albumList[0] `should equal` MusicDirectoryChild(id = 8867, parent = 522, isDir = true, + title = "Always Outnumbered, Never Outgunned", + album = "Always Outnumbered, Never Outgunned", artist = "The Prodigy", + year = 2004, genre = "Electronic", coverArt = "8867", playCount = 0, + created = parseDate("2016-10-23T20:57:27.000Z")) + songList.size `should equal to` 1 + songList[0] `should equal` MusicDirectoryChild(id = 5831, parent = 5766, isDir = false, + title = "You'll Be Under My Wheels", album = "Need for Speed Most Wanted", + artist = "The Prodigy", track = 17, year = 2005, genre = "Rap", + coverArt = "5766", size = 5607024, contentType = "audio/mpeg", + suffix = "mp3", duration = 233, bitRate = 192, + path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels.mp3", + isVideo = false, playCount = 0, discNumber = 1, + created = parseDate("2016-10-23T20:09:02.000Z"), + albumId = 568, artistId = 505, type = "music") + } + } + + @Test + fun `Should pass query id in request param`() { + val query = "some" + + mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", apiRequest = { + client.api.search2(query).execute() + }, expectedParam = "query=$query") + } + + @Test + fun `Should pass artist count in request param`() { + val artistCount = 45 + + mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", apiRequest = { + client.api.search2("some", artistCount = artistCount).execute() + }, expectedParam = "artistCount=$artistCount") + } + + @Test + fun `Should pass artist offset in request param`() { + val artistOffset = 13 + + mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", apiRequest = { + client.api.search2("some", artistOffset = artistOffset).execute() + }, expectedParam = "artistOffset=$artistOffset") + } + + @Test + fun `Should pass album count in request param`() { + val albumCount = 30 + + mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", apiRequest = { + client.api.search2("some", albumCount = albumCount).execute() + }, expectedParam = "albumCount=$albumCount") + } + + @Test + fun `Should pass album offset in request param`() { + val albumOffset = 91 + + mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", apiRequest = { + client.api.search2("some", albumOffset = albumOffset).execute() + }, expectedParam = "albumOffset=$albumOffset") + } + + @Test + fun `Should pass song count in request param`() { + val songCount = 22 + + mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", apiRequest = { + client.api.search2("some", songCount = songCount).execute() + }, expectedParam = "songCount=$songCount") + } + + @Test + fun `Should pass music folder id in request param`() { + val musicFolderId = 565L + + mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", apiRequest = { + client.api.search2("some", musicFolderId = musicFolderId).execute() + }, expectedParam = "musicFolderId=$musicFolderId") + } +} diff --git a/subsonic-api/src/integrationTest/resources/search2_ok.json b/subsonic-api/src/integrationTest/resources/search2_ok.json new file mode 100644 index 00000000..ee321cab --- /dev/null +++ b/subsonic-api/src/integrationTest/resources/search2_ok.json @@ -0,0 +1,50 @@ +{ + "subsonic-response" : { + "status" : "ok", + "version" : "1.15.0", + "searchResult2" : { + "artist" : [ { + "id" : "522", + "name" : "The Prodigy" + } ], + "album" : [ { + "id" : "8867", + "parent" : "522", + "isDir" : true, + "title" : "Always Outnumbered, Never Outgunned", + "album" : "Always Outnumbered, Never Outgunned", + "artist" : "The Prodigy", + "year" : 2004, + "genre" : "Electronic", + "coverArt" : "8867", + "playCount" : 0, + "created" : "2016-10-23T20:57:27.000Z" + } ], + "song" : [ { + "id" : "5831", + "parent" : "5766", + "isDir" : false, + "title" : "You'll Be Under My Wheels", + "album" : "Need for Speed Most Wanted", + "artist" : "The Prodigy", + "track" : 17, + "year" : 2005, + "genre" : "Rap", + "coverArt" : "5766", + "size" : 5607024, + "contentType" : "audio/mpeg", + "suffix" : "mp3", + "duration" : 233, + "bitRate" : 192, + "path" : "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels.mp3", + "isVideo" : false, + "playCount" : 0, + "discNumber" : 1, + "created" : "2016-10-23T20:09:02.000Z", + "albumId" : "568", + "artistId" : "505", + "type" : "music" + } ] + } + } +} \ 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 ce3167ef..7c3d3ad3 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 @@ -7,6 +7,7 @@ import org.moire.ultrasonic.api.subsonic.response.GetIndexesResponse import org.moire.ultrasonic.api.subsonic.response.GetMusicDirectoryResponse import org.moire.ultrasonic.api.subsonic.response.LicenseResponse import org.moire.ultrasonic.api.subsonic.response.MusicFoldersResponse +import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse import org.moire.ultrasonic.api.subsonic.response.SearchResponse import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import retrofit2.Call @@ -63,4 +64,13 @@ interface SubsonicAPIDefinition { @Query("count") count: Int? = null, @Query("offset") offset: Int? = null, @Query("newerThan") newerThan: Long? = null): Call + + @GET("search2.view") + fun search2(@Query("query") query: String, + @Query("artistCount") artistCount: Int? = null, + @Query("artistOffset") artistOffset: Int? = null, + @Query("albumCount") albumCount: Int? = null, + @Query("albumOffset") albumOffset: Int? = null, + @Query("songCount") songCount: Int? = null, + @Query("musicFolderId") musicFolderId: Long? = null): Call } diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt new file mode 100644 index 00000000..4a0fbb92 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt @@ -0,0 +1,9 @@ +package org.moire.ultrasonic.api.subsonic.models + +import com.fasterxml.jackson.annotation.JsonProperty + +data class SearchTwoResult( + @JsonProperty("artist") val artistList: List = emptyList(), + @JsonProperty("album") val albumList: List = emptyList(), + @JsonProperty("song") val songList: List = emptyList() +) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SearchTwoResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SearchTwoResponse.kt new file mode 100644 index 00000000..32a90b41 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SearchTwoResponse.kt @@ -0,0 +1,12 @@ +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.SearchTwoResult + +class SearchTwoResponse(status: Status, + version: SubsonicAPIVersions, + error: SubsonicError?, + @JsonProperty("searchResult2") val searchResult: SearchTwoResult = SearchTwoResult()) + : SubsonicResponse(status, version, error) 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 838cc8d3..b79bf0a4 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -64,6 +64,7 @@ import org.moire.ultrasonic.api.subsonic.response.GetMusicDirectoryResponse; import org.moire.ultrasonic.api.subsonic.response.LicenseResponse; import org.moire.ultrasonic.api.subsonic.response.MusicFoldersResponse; import org.moire.ultrasonic.api.subsonic.response.SearchResponse; +import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse; import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse; import org.moire.ultrasonic.data.APIConverter; import org.moire.ultrasonic.domain.Bookmark; @@ -437,24 +438,23 @@ public class RESTMusicService implements MusicService } /** - * Search using the "search2" REST method, available in 1.4.0 and later. - */ - private SearchResult search2(SearchCriteria criteria, Context context, ProgressListener progressListener) throws Exception - { - checkServerVersion(context, "1.4", "Search2 not supported."); + * Search using the "search2" REST method, available in 1.4.0 and later. + */ + private SearchResult search2(SearchCriteria criteria, + Context context, + ProgressListener progressListener) throws Exception { + if (criteria.getQuery() == null) { + throw new IllegalArgumentException("Query param is null"); + } - List parameterNames = asList("query", "artistCount", "albumCount", "songCount"); - List parameterValues = Arrays.asList(criteria.getQuery(), criteria.getArtistCount(), criteria.getAlbumCount(), criteria.getSongCount()); - Reader reader = getReader(context, progressListener, "search2", null, parameterNames, parameterValues); - try - { - return new SearchResult2Parser(context).parse(reader, progressListener, false); - } - finally - { - Util.close(reader); - } - } + updateProgressListener(progressListener, R.string.parser_reading); + Response response = subsonicAPIClient.getApi().search2(criteria.getQuery(), + criteria.getArtistCount(), null, criteria.getAlbumCount(), null, + criteria.getSongCount(), null).execute(); + checkResponseSuccessful(response); + + return APIConverter.toDomainEntity(response.body().getSearchResult()); + } private SearchResult search3(SearchCriteria criteria, Context context, ProgressListener progressListener) throws Exception { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt index 1a30167c..3d34b847 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt @@ -5,6 +5,7 @@ package org.moire.ultrasonic.data import org.moire.ultrasonic.api.subsonic.models.Album import org.moire.ultrasonic.api.subsonic.models.Index import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild +import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Indexes import org.moire.ultrasonic.domain.MusicDirectory @@ -90,3 +91,6 @@ fun APIMusicDirectory.toDomainEntity(): MusicDirectory = MusicDirectory().apply fun APISearchResult.toDomainEntity(): SearchResult = SearchResult(emptyList(), emptyList(), this.matchList.map { it.toDomainEntity() }) + +fun SearchTwoResult.toDomainEntity(): SearchResult = SearchResult(this.artistList.map { it.toDomainEntity() }, + this.albumList.map { it.toDomainEntity() }, this.songList.map { it.toDomainEntity() }) diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt index 5d9542d5..a7ace0b1 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt @@ -14,6 +14,7 @@ import org.moire.ultrasonic.api.subsonic.models.MusicDirectory import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild import org.moire.ultrasonic.api.subsonic.models.MusicFolder import org.moire.ultrasonic.api.subsonic.models.SearchResult +import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult import java.util.Calendar /** @@ -212,6 +213,28 @@ class APIConverterTest { } } + @Test + fun `Should convert SearchTwoResult to domain entity`() { + val entity = SearchTwoResult(listOf( + Artist(id = 82, name = "great-artist-name") + ), listOf( + MusicDirectoryChild(id = 762, artist = "bzz") + ), listOf( + MusicDirectoryChild(id = 9118, parent = 112) + )) + + val convertedEntity = entity.toDomainEntity() + + with(convertedEntity) { + artists.size `should equal to` entity.artistList.size + artists[0] `should equal` entity.artistList[0].toDomainEntity() + albums.size `should equal to` entity.albumList.size + albums[0] `should equal` entity.albumList[0].toDomainEntity() + songs.size `should equal to` entity.songList.size + songs[0] `should equal` entity.songList[0].toDomainEntity() + } + } + private fun createMusicFolder(id: Long = 0, name: String = ""): MusicFolder = MusicFolder(id, name)