From 5a200829383e9aae9e48d7bc971167e52b456df0 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 23 Aug 2017 22:14:36 +0200 Subject: [PATCH 1/4] Add search api call. Signed-off-by: Yahor Berdnikau --- .../api/subsonic/SubsonicApiSearchTest.kt | 125 ++++++++++++++++++ .../integrationTest/resources/search_ok.json | 35 +++++ .../api/subsonic/SubsonicAPIDefinition.kt | 11 ++ .../api/subsonic/models/SearchResult.kt | 7 + .../api/subsonic/response/SearchResponse.kt | 11 ++ 5 files changed, 189 insertions(+) create mode 100644 subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTest.kt create mode 100644 subsonic-api/src/integrationTest/resources/search_ok.json create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchResult.kt create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SearchResponse.kt diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTest.kt new file mode 100644 index 00000000..e0e868d2 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiSearchTest.kt @@ -0,0 +1,125 @@ +package org.moire.ultrasonic.api.subsonic + +import org.amshove.kluent.`should contain` +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 java.util.Calendar + +/** + * Integration test for [SubsonicAPIClient] for search call. + */ +class SubsonicApiSearchTest : SubsonicAPIClientTest() { + @Test + fun `Should parse error response`() { + checkErrorCallParsed(mockWebServerRule, { + client.api.search().execute() + }) + } + + @Test + fun `Should parse ok response`() { + enqueueOkResponse() + + val response = client.api.search().execute() + + assertResponseSuccessful(response) + with(response.body().searchResult) { + offset `should equal to` 10 + totalHits `should equal to` 53 + matchList.size `should equal to` 1 + matchList[0] `should equal` MusicDirectoryChild(id = 5831L, parent = 5766L, + 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 artist param`() { + enqueueOkResponse() + val artist = "some-artist" + client.api.search(artist = artist).execute() + + val request = mockWebServerRule.mockWebServer.takeRequest() + + request.requestLine `should contain` "artist=$artist" + } + + @Test + fun `Should pass album param`() { + enqueueOkResponse() + val album = "some-album" + client.api.search(album = album).execute() + + val request = mockWebServerRule.mockWebServer.takeRequest() + + request.requestLine `should contain` "album=$album" + } + + @Test + fun `Should pass title param`() { + enqueueOkResponse() + val title = "some-title" + client.api.search(title = title).execute() + + val request = mockWebServerRule.mockWebServer.takeRequest() + + request.requestLine `should contain` "title=$title" + } + + @Test + fun `Should contain any param`() { + enqueueOkResponse() + val any = "AnyString" + client.api.search(any = any).execute() + + val request = mockWebServerRule.mockWebServer.takeRequest() + + request.requestLine `should contain` "any=$any" + } + + @Test + fun `Should contain count param`() { + enqueueOkResponse() + val count = 11 + client.api.search(count = count).execute() + + val request = mockWebServerRule.mockWebServer.takeRequest() + + request.requestLine `should contain` "count=$count" + } + + @Test + fun `Should contain offset param`() { + enqueueOkResponse() + val offset = 54 + client.api.search(offset = offset).execute() + + val request = mockWebServerRule.mockWebServer.takeRequest() + + request.requestLine `should contain` "offset=$offset" + } + + @Test + fun `Should contain newerThan param`() { + enqueueOkResponse() + val newerThan = Calendar.getInstance() + client.api.search(newerThan = newerThan.time.time).execute() + + val request = mockWebServerRule.mockWebServer.takeRequest() + + request.requestLine `should contain` "newerThan=${newerThan.time.time}" + } + + private fun enqueueOkResponse() { + mockWebServerRule.enqueueResponse("search_ok.json") + } +} diff --git a/subsonic-api/src/integrationTest/resources/search_ok.json b/subsonic-api/src/integrationTest/resources/search_ok.json new file mode 100644 index 00000000..47ea4a23 --- /dev/null +++ b/subsonic-api/src/integrationTest/resources/search_ok.json @@ -0,0 +1,35 @@ +{ + "subsonic-response" : { + "status" : "ok", + "version" : "1.15.0", + "searchResult" : { + "offset" : 10, + "totalHits" : 53, + "match" : [ { + "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 e5a38a1c..ce3167ef 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.SearchResponse import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import retrofit2.Call import retrofit2.http.GET @@ -17,6 +18,7 @@ import retrofit2.http.Query * * For methods description see [http://www.subsonic.org/pages/api.jsp]. */ +@Suppress("TooManyFunctions", "LongParameterList") interface SubsonicAPIDefinition { @GET("ping.view") fun ping(): Call @@ -52,4 +54,13 @@ interface SubsonicAPIDefinition { @GET("getAlbum.view") fun getAlbum(@Query("id") id: Long): Call + + @GET("search.view") + fun search(@Query("artist") artist: String? = null, + @Query("album") album: String? = null, + @Query("title") title: String? = null, + @Query("any") any: String? = null, + @Query("count") count: Int? = null, + @Query("offset") offset: Int? = null, + @Query("newerThan") newerThan: Long? = null): Call } diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchResult.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchResult.kt new file mode 100644 index 00000000..405feb13 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchResult.kt @@ -0,0 +1,7 @@ +package org.moire.ultrasonic.api.subsonic.models + +import com.fasterxml.jackson.annotation.JsonProperty + +data class SearchResult(val offset: Int = 0, + val totalHits: Int = 0, + @JsonProperty("match") val matchList: List = emptyList()) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SearchResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SearchResponse.kt new file mode 100644 index 00000000..aac0a817 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SearchResponse.kt @@ -0,0 +1,11 @@ +package org.moire.ultrasonic.api.subsonic.response + +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions +import org.moire.ultrasonic.api.subsonic.SubsonicError +import org.moire.ultrasonic.api.subsonic.models.SearchResult + +class SearchResponse(status: Status, + version: SubsonicAPIVersions, + error: SubsonicError?, + val searchResult: SearchResult = SearchResult()) + : SubsonicResponse(status, version, error) From e71d8a09af86fc7d79f71cfdbf537ad4358f60ff Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 23 Aug 2017 22:30:48 +0200 Subject: [PATCH 2/4] Increase allowed functions count in detekt config. Signed-off-by: Yahor Berdnikau --- detekt-config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detekt-config.yml b/detekt-config.yml index 985b704c..bc94cdbe 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -49,7 +49,7 @@ complexity: ComplexMethod: threshold: 10 TooManyFunctions: - threshold: 10 + threshold: 20 ComplexCondition: threshold: 3 LabeledExpression: From 6bfd091b6b7afd5e1b592d4932080fc8d8fdad46 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 23 Aug 2017 22:31:36 +0200 Subject: [PATCH 3/4] Add function that converts SearchResult to domain entity. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/data/SubsonicAPIConverter.kt | 5 +++++ .../moire/ultrasonic/data/APIConverterTest.kt | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) 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 3b4462ce..1a30167c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt @@ -9,10 +9,12 @@ import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Indexes import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicFolder +import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes import org.moire.ultrasonic.api.subsonic.models.MusicDirectory as APIMusicDirectory import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder +import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult fun APIMusicFolder.toDomainEntity(): MusicFolder = MusicFolder(this.id.toString(), this.name) @@ -85,3 +87,6 @@ fun APIMusicDirectory.toDomainEntity(): MusicDirectory = MusicDirectory().apply name = this@toDomainEntity.name addAll(this@toDomainEntity.childList.map { it.toDomainEntity() }) } + +fun APISearchResult.toDomainEntity(): SearchResult = SearchResult(emptyList(), emptyList(), + this.matchList.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 029f061a..5d9542d5 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt @@ -4,6 +4,7 @@ package org.moire.ultrasonic.data import org.amshove.kluent.`should equal to` import org.amshove.kluent.`should equal` +import org.amshove.kluent.`should not equal` import org.junit.Test import org.moire.ultrasonic.api.subsonic.models.Album import org.moire.ultrasonic.api.subsonic.models.Artist @@ -12,6 +13,7 @@ import org.moire.ultrasonic.api.subsonic.models.Indexes 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 java.util.Calendar /** @@ -192,6 +194,24 @@ class APIConverterTest { } } + @Test + fun `Should convert SearchResult to domain entity`() { + val entity = SearchResult(offset = 10, totalHits = 3, matchList = listOf( + MusicDirectoryChild(id = 101L) + )) + + val convertedEntity = entity.toDomainEntity() + + with(convertedEntity) { + albums `should not equal` null + albums.size `should equal to` 0 + artists `should not equal` null + artists.size `should equal to` 0 + songs.size `should equal to` entity.matchList.size + songs[0] `should equal` entity.matchList[0].toDomainEntity() + } + } + private fun createMusicFolder(id: Long = 0, name: String = ""): MusicFolder = MusicFolder(id, name) From 383b05e4dc1df95906ca359313534037c46cb31e Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 23 Aug 2017 22:32:49 +0200 Subject: [PATCH 4/4] Use new api method for search call. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/service/RESTMusicService.java | 61 +++++++-------- .../service/parser/SearchResultParser.java | 77 ------------------- 2 files changed, 29 insertions(+), 109 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/SearchResultParser.java 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 f818fe6d..838cc8d3 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -63,6 +63,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.SearchResponse; import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse; import org.moire.ultrasonic.data.APIConverter; import org.moire.ultrasonic.domain.Bookmark; @@ -94,7 +95,6 @@ import org.moire.ultrasonic.service.parser.PodcastEpisodeParser; import org.moire.ultrasonic.service.parser.PodcastsChannelsParser; import org.moire.ultrasonic.service.parser.RandomSongsParser; import org.moire.ultrasonic.service.parser.SearchResult2Parser; -import org.moire.ultrasonic.service.parser.SearchResultParser; import org.moire.ultrasonic.service.parser.ShareParser; import org.moire.ultrasonic.service.parser.UserInfoParser; import org.moire.ultrasonic.service.ssl.SSLSocketFactory; @@ -407,39 +407,36 @@ public class RESTMusicService implements MusicService return APIConverter.toMusicDirectoryDomainEntity(response.body().getAlbum()); } - @Override - public SearchResult search(SearchCriteria criteria, Context context, ProgressListener progressListener) throws Exception - { - try - { - return !Util.isOffline(context) && Util.getShouldUseId3Tags(context) ? search3(criteria, context, progressListener) : search2(criteria, context, progressListener); - } - catch (ServerTooOldException x) - { - // Ensure backward compatibility with REST 1.3. - return searchOld(criteria, context, progressListener); - } - } + @Override + public SearchResult search(SearchCriteria criteria, + Context context, + ProgressListener progressListener) throws Exception { + try { + return !Util.isOffline(context) && + Util.getShouldUseId3Tags(context) ? + search3(criteria, context, progressListener) : + search2(criteria, context, progressListener); + } catch (ServerTooOldException x) { + // Ensure backward compatibility with REST 1.3. + return searchOld(criteria, context, progressListener); + } + } - /** - * Search using the "search" REST method. - */ - private SearchResult searchOld(SearchCriteria criteria, Context context, ProgressListener progressListener) throws Exception - { - List parameterNames = asList("any", "songCount"); - List parameterValues = Arrays.asList(criteria.getQuery(), criteria.getSongCount()); - Reader reader = getReader(context, progressListener, "search", null, parameterNames, parameterValues); - try - { - return new SearchResultParser(context).parse(reader, progressListener); - } - finally - { - Util.close(reader); - } - } + /** + * Search using the "search" REST method. + */ + private SearchResult searchOld(SearchCriteria criteria, + Context context, + ProgressListener progressListener) throws Exception { + updateProgressListener(progressListener, R.string.parser_reading); + Response response = subsonicAPIClient.getApi().search(null, null, null, criteria.getQuery(), + criteria.getSongCount(), null, null).execute(); + checkResponseSuccessful(response); - /** + return APIConverter.toDomainEntity(response.body().getSearchResult()); + } + + /** * Search using the "search2" REST method, available in 1.4.0 and later. */ private SearchResult search2(SearchCriteria criteria, Context context, ProgressListener progressListener) throws Exception diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/SearchResultParser.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/SearchResultParser.java deleted file mode 100644 index 238748f8..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/SearchResultParser.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.service.parser; - -import android.content.Context; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.domain.SearchResult; -import org.moire.ultrasonic.domain.Artist; -import org.moire.ultrasonic.util.ProgressListener; - -import org.xmlpull.v1.XmlPullParser; - -import java.io.Reader; -import java.util.Collections; -import java.util.List; -import java.util.ArrayList; - -/** - * @author Sindre Mehus - */ -public class SearchResultParser extends MusicDirectoryEntryParser -{ - - public SearchResultParser(Context context) - { - super(context); - } - - public SearchResult parse(Reader reader, ProgressListener progressListener) throws Exception - { - updateProgress(progressListener, R.string.parser_reading); - init(reader); - - List songs = new ArrayList(); - int eventType; - do - { - eventType = nextParseEvent(); - if (eventType == XmlPullParser.START_TAG) - { - String name = getElementName(); - if ("match".equals(name)) - { - songs.add(parseEntry("", false, 0)); - } - else if ("error".equals(name)) - { - handleError(); - } - } - } while (eventType != XmlPullParser.END_DOCUMENT); - - validate(); - updateProgress(progressListener, R.string.parser_reading_done); - - return new SearchResult(Collections.emptyList(), Collections.emptyList(), songs); - } - -}